From 81ef7698f68aada137b99ed781cd8ac4fe6424bf Mon Sep 17 00:00:00 2001 From: MPeter <> Date: Thu, 1 Sep 2022 15:08:02 +0200 Subject: [PATCH] ZIP utility class overhaul --- .../gadgetbridge/util/ZipFile.java | 109 ++++++++++++++++++ .../gadgetbridge/util/ZipFileException.java | 7 ++ .../gadgetbridge/util/ZipUtils.java | 55 --------- 3 files changed, 116 insertions(+), 55 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipFile.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipFileException.java delete mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipUtils.java diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipFile.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipFile.java new file mode 100644 index 000000000..e985fcf5c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipFile.java @@ -0,0 +1,109 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipException; +import java.util.zip.ZipInputStream; + +import androidx.annotation.Nullable; + +public class ZipFile implements AutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(ZipFile.class); + public static final byte[] ZIP_HEADER = new byte[]{ + 0x50, 0x4B, 0x03, 0x04 + }; + + private final ZipInputStream zipInputStream; + + /** + * Open ZIP file from byte array in memory + * @param zipBytes + */ + public ZipFile(byte[] zipBytes) { + zipInputStream = new ZipInputStream(new ByteArrayInputStream(zipBytes)); + } + + /** + * Checks if data resembles a ZIP file.
+ * The check is not infallible: it may report self-extracting or other exotic ZIP archives as not a ZIP file, and it may report a corrupted ZIP file as a ZIP file. + * @param data The data to check. + * @return Whether data resembles a ZIP file. + */ + public static boolean isZipFile(byte[] data) { + return ArrayUtils.equals(data, ZIP_HEADER, 0); + } + + /** + * Reads the contents of file at path into a byte array. + * @param path Path of the file in the ZIP file. + * @return byte array contatining the contents of the requested file. + * @throws ZipFileException If the specified path does not exist or references a directory, or if some other I/O error occurs. In other words, if return value would otherwise be null. + */ + public byte[] getFileFromZip(final String path) throws ZipFileException { + try { + ZipEntry zipEntry; + while ((zipEntry = zipInputStream.getNextEntry()) != null) { + if (!zipEntry.getName().equals(path)) continue; // TODO: is this always a path? The documentation is very vague. + + if (zipEntry.isDirectory()) { + throw new ZipFileException(String.format("Path in ZIP file is a directory: %s", path)); + } + + return readAllBytes(zipInputStream); + } + + throw new ZipFileException(String.format("Path in ZIP file was not found: %s", path)); + + } catch (ZipException e) { + throw new ZipFileException("The ZIP file might be corrupted"); + } catch (IOException e) { + throw new ZipFileException("General IO error"); + } + } + + /** + * Tries to obtain file from ZIP file without much hassle, but is not safe.
+ * Please only use this in place of old code where correctness of the result is checked only later on.
+ * Use getFileFromZip of ZipFile instance instead. + * @param zipBytes + * @param path Path of the file in the ZIP file. + * @return Contents of requested file or null. + */ + @Deprecated + @Nullable + public static byte[] tryReadFileQuick(final byte[] zipBytes, final String path) { + try (ZipFile zip = new ZipFile(zipBytes)) { + return zip.getFileFromZip(path); + } catch (ZipFileException e) { + LOG.error("Quick ZIP reading failed.", e); + } catch (Exception e) { + LOG.error("Unable to close ZipFile.", e); + } + + return null; + } + + private static byte[] readAllBytes(final InputStream is) throws IOException { + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + int n; + byte[] buf = new byte[16384]; + + while ((n = is.read(buf, 0, buf.length)) != -1) { + buffer.write(buf, 0, n); + } + + return buffer.toByteArray(); + } + + @Override + public void close() throws Exception { + zipInputStream.close(); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipFileException.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipFileException.java new file mode 100644 index 000000000..f7c87ceab --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipFileException.java @@ -0,0 +1,7 @@ +package nodomain.freeyourgadget.gadgetbridge.util; + +public class ZipFileException extends Exception { + public ZipFileException(String message) { + super(String.format("Error while reading ZIP file: %s", message)); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipUtils.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipUtils.java deleted file mode 100644 index 336b229c5..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/ZipUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -package nodomain.freeyourgadget.gadgetbridge.util; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -public class ZipUtils { - private static final Logger LOG = LoggerFactory.getLogger(ZipUtils.class); - - public static byte[] getFileFromZip(final byte[] zipBytes, final String path) { - try (ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(zipBytes))) { - - ZipEntry zipEntry; - while ((zipEntry = zipInputStream.getNextEntry()) != null) { - if (!zipEntry.getName().equals(path)) continue; // TODO: is this always a path? The documentation is very vague. - - // Found, but not a file - if (zipEntry.isDirectory()) { - LOG.error(String.format("Path in ZIP file is a directory: %s", path)); - return null; - } - - // Found, and it is a file - return readAllBytes(zipInputStream); - } - - // Not found - LOG.error(String.format("Path in ZIP file was not found: %s", path)); - return null; - - } catch (final IOException e) { - LOG.error(String.format("Unknown error while reading file from ZIP file: %s", path), e); - return null; - } - } - - private static byte[] readAllBytes(final InputStream is) throws IOException { - final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); - - int n; - byte[] buf = new byte[16384]; - - while ((n = is.read(buf, 0, buf.length)) != -1) { - buffer.write(buf, 0, n); - } - - return buffer.toByteArray(); - } -}