]> git.ipfire.org Git - thirdparty/libarchive.git/commitdiff
ZIP reader: added support for Zstd decompression
authorjo620kix <81214276+jo620kix@users.noreply.github.com>
Mon, 29 Mar 2021 05:02:57 +0000 (14:02 +0900)
committerjo620kix <81214276+jo620kix@users.noreply.github.com>
Mon, 29 Mar 2021 05:02:57 +0000 (14:02 +0900)
libarchive/archive_read_support_format_zip.c
libarchive/test/test_read_format_zip.c
libarchive/test/test_read_format_zip_zstd.zipx.uu [new file with mode: 0644]
libarchive/test/test_read_format_zip_zstd_multi.zipx.uu [new file with mode: 0644]

index 5e8895af0a354c592eb49c220c669e70a38575b9..178122d62c374d5888c8d2c050c470906fcebac4 100644 (file)
@@ -58,6 +58,9 @@ __FBSDID("$FreeBSD: head/lib/libarchive/archive_read_support_format_zip.c 201102
 #ifdef HAVE_LZMA_H
 #include <lzma.h>
 #endif
+#ifdef HAVE_ZSTD_H
+#include <zstd.h>
+#endif
 
 #include "archive.h"
 #include "archive_digest_private.h"
@@ -191,6 +194,11 @@ struct zip {
        char            bzstream_valid;
 #endif
 
+#if HAVE_ZSTD_H && HAVE_LIBZSTD
+       ZSTD_DStream    *zstdstream;
+       char            zstdstream_valid;
+#endif
+
        IByteIn                 zipx_ppmd_stream;
        ssize_t                 zipx_ppmd_read_compressed;
        CPpmd8                  ppmd8;
@@ -424,6 +432,7 @@ static const struct {
        {17, "reserved"}, /* Reserved by PKWARE */
        {18, "ibm-terse-new"}, /* File is compressed using IBM TERSE (new) */
        {19, "ibm-lz777"},/* IBM LZ77 z Architecture (PFS) */
+       {93, "zstd"},     /*  Zstandard (zstd) Compression */
        {95, "xz"},       /* XZ compressed data */
        {96, "jpeg"},     /* JPEG compressed data */
        {97, "wav-pack"}, /* WavPack compressed data */
@@ -2299,6 +2308,129 @@ zip_read_data_zipx_bzip2(struct archive_read *a, const void **buff,
 
 #endif
 
+#if HAVE_ZSTD_H && HAVE_LIBZSTD
+static int
+zipx_zstd_init(struct archive_read *a, struct zip *zip)
+{
+       size_t r;
+
+       /* Deallocate already existing Zstd decompression context if it
+        * exists. */
+       if(zip->zstdstream_valid) {
+               ZSTD_freeDStream(zip->zstdstream);
+               zip->zstdstream_valid = 0;
+       }
+
+       /* Allocate a new Zstd decompression context. */
+       zip->zstdstream = ZSTD_createDStream();
+
+       r = ZSTD_initDStream(zip->zstdstream);
+       if (ZSTD_isError(r)) {
+                archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC,
+                       "Error initializing zstd decompressor: %s",
+                       ZSTD_getErrorName(r));
+
+               return ARCHIVE_FAILED;
+       }
+
+       /* Mark the zstdstream field to be released in cleanup phase. */
+       zip->zstdstream_valid = 1;
+
+       /* (Re)allocate the buffer that will contain decompressed bytes. */
+       free(zip->uncompressed_buffer);
+
+       zip->uncompressed_buffer_size = ZSTD_DStreamOutSize();
+       zip->uncompressed_buffer =
+           (uint8_t*) malloc(zip->uncompressed_buffer_size);
+       if (zip->uncompressed_buffer == NULL) {
+               archive_set_error(&a->archive, ENOMEM,
+                       "No memory for Zstd decompression");
+
+               return ARCHIVE_FATAL;
+       }
+
+       /* Initialization done. */
+       zip->decompress_init = 1;
+       return ARCHIVE_OK;
+}
+
+static int
+zip_read_data_zipx_zstd(struct archive_read *a, const void **buff,
+    size_t *size, int64_t *offset)
+{
+       struct zip *zip = (struct zip *)(a->format->data);
+       ssize_t bytes_avail = 0, in_bytes, to_consume;
+       const void *compressed_buff;
+       int r;
+       size_t ret;
+       uint64_t total_out;
+       ZSTD_outBuffer out;
+       ZSTD_inBuffer in;
+
+       /* Initialize decompression context if we're here for the first time. */
+       if(!zip->decompress_init) {
+               r = zipx_zstd_init(a, zip);
+               if(r != ARCHIVE_OK)
+                       return r;
+       }
+
+       /* Fetch more compressed bytes */
+       compressed_buff = __archive_read_ahead(a, 1, &bytes_avail);
+       if(bytes_avail < 0) {
+               archive_set_error(&a->archive, ARCHIVE_ERRNO_FILE_FORMAT,
+                   "Truncated zstd file body");
+               return (ARCHIVE_FATAL);
+       }
+
+       in_bytes = zipmin(zip->entry_bytes_remaining, bytes_avail);
+
+       /* Setup buffer boundaries */
+       in.src = compressed_buff;
+       in.size = in_bytes;
+       in.pos = 0;
+       out = (ZSTD_outBuffer) { zip->uncompressed_buffer, zip->uncompressed_buffer_size, 0 };
+
+       /* Perform the decompression. */
+       ret = ZSTD_decompressStream(zip->zstdstream, &out, &in);
+       if (ZSTD_isError(ret)) {
+               archive_set_error(&a->archive, ARCHIVE_ERRNO_MISC,
+                       "Error during zstd decompression: %s",
+                       ZSTD_getErrorName(ret));
+               return (ARCHIVE_FATAL);
+       }
+
+       /* Check end of the stream. */
+       if (ret == 0) {
+               if ((in.pos == in.size) && (out.pos < out.size)) {
+                       zip->end_of_entry = 1;
+                       ZSTD_freeDStream(zip->zstdstream);
+                       zip->zstdstream_valid = 0;
+               }
+       }
+
+       /* Update the pointers so decompressor can continue decoding. */
+       to_consume = in.pos;
+       __archive_read_consume(a, to_consume);
+
+       total_out = out.pos;
+
+       zip->entry_bytes_remaining -= to_consume;
+       zip->entry_compressed_bytes_read += to_consume;
+       zip->entry_uncompressed_bytes_read += total_out;
+
+       /* Give libarchive its due. */
+       *size = total_out;
+       *buff = zip->uncompressed_buffer;
+
+       /* Seek for optional marker, like in other entries. */
+       r = consume_optional_marker(a, zip);
+       if(r != ARCHIVE_OK)
+               return r;
+
+       return ARCHIVE_OK;
+}
+#endif
+
 #ifdef HAVE_ZLIB_H
 static int
 zip_deflate_init(struct archive_read *a, struct zip *zip)
@@ -2918,6 +3050,11 @@ archive_read_format_zip_read_data(struct archive_read *a,
        case 95: /* ZIPx XZ compression. */
                r = zip_read_data_zipx_xz(a, buff, size, offset);
                break;
+#endif
+#if HAVE_ZSTD_H && HAVE_LIBZSTD
+       case 93: /* ZIPx Zstd compression. */
+               r = zip_read_data_zipx_zstd(a, buff, size, offset);
+               break;
 #endif
        /* PPMd support is built-in, so we don't need any #if guards. */
        case 98: /* ZIPx PPMd compression. */
@@ -3009,6 +3146,12 @@ archive_read_format_zip_cleanup(struct archive_read *a)
        }
 #endif
 
+#if HAVE_ZSTD_H && HAVE_LIBZSTD
+       if (zip->zstdstream_valid) {
+               ZSTD_freeDStream(zip->zstdstream);
+       }
+#endif
+
        free(zip->uncompressed_buffer);
 
        if (zip->ppmd8_valid)
index 31f66f0127677422f44219398ef9365e5f4f2633..fdbfdca61de06fc9391fb21f043fa321afef14a5 100644 (file)
@@ -736,6 +736,130 @@ DEFINE_TEST(test_read_format_zip_bzip2_multi_blockread)
        assertEqualIntA(a, ARCHIVE_OK, archive_read_free(a));
 }
 
+DEFINE_TEST(test_read_format_zip_zstd_one_file)
+{
+       const char *refname = "test_read_format_zip_zstd.zipx";
+       struct archive *a;
+       struct archive_entry *ae;
+
+       assert((a = archive_read_new()) != NULL);
+       if (ARCHIVE_OK != archive_read_support_filter_zstd(a)) {
+               skipping("zstd is not fully supported on this platform");
+               archive_read_close(a);
+               return;
+       }
+       extract_reference_file(refname);
+
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_support_format_zip(a));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_open_filename(a, refname, 37));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae));
+       assertEqualString("ZIP 2.0 (zstd)", archive_format_name(a));
+       assertEqualString("vimrc", archive_entry_pathname(ae));
+       assertEqualIntA(a, 0, extract_one(a, ae, 0xBA8E3BAA));
+       assertEqualIntA(a, ARCHIVE_EOF, archive_read_next_header(a, &ae));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_close(a));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_free(a));
+}
+
+DEFINE_TEST(test_read_format_zip_zstd_one_file_blockread)
+{
+       const char *refname = "test_read_format_zip_zstd.zipx";
+       struct archive *a;
+       struct archive_entry *ae;
+
+       assert((a = archive_read_new()) != NULL);
+       if (ARCHIVE_OK != archive_read_support_filter_zstd(a)) {
+               skipping("zstd is not fully supported on this platform");
+               archive_read_close(a);
+               return;
+       }
+       extract_reference_file(refname);
+
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_support_format_zip(a));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_open_filename(a, refname, 37));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae));
+       assertEqualString("ZIP 2.0 (zstd)", archive_format_name(a));
+       assertEqualString("vimrc", archive_entry_pathname(ae));
+       assertEqualIntA(a, 0, extract_one_using_blocks(a, 13, 0xBA8E3BAA));
+       assertEqualIntA(a, ARCHIVE_EOF, archive_read_next_header(a, &ae));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_close(a));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_free(a));
+}
+
+DEFINE_TEST(test_read_format_zip_zstd_multi)
+{
+       const char *refname = "test_read_format_zip_zstd_multi.zipx";
+       struct archive *a;
+       struct archive_entry *ae;
+
+       assert((a = archive_read_new()) != NULL);
+       if (ARCHIVE_OK != archive_read_support_filter_zstd(a)) {
+               skipping("zstd is not fully supported on this platform");
+               archive_read_close(a);
+               return;
+       }
+       extract_reference_file(refname);
+
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_support_format_zip(a));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_open_filename(a, refname, 37));
+
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae));
+       assertEqualString("ZIP 2.0 (zstd)", archive_format_name(a));
+       assertEqualString("smartd.conf", archive_entry_pathname(ae));
+       assertEqualIntA(a, 0, extract_one(a, ae, 0x8DD7379E));
+
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae));
+       assertEqualString("ZIP 2.0 (zstd)", archive_format_name(a));
+       assertEqualString("ts.conf", archive_entry_pathname(ae));
+       assertEqualIntA(a, 0, extract_one(a, ae, 0x7AE59B31));
+
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae));
+       assertEqualString("ZIP 2.0 (zstd)", archive_format_name(a));
+       assertEqualString("vimrc", archive_entry_pathname(ae));
+       assertEqualIntA(a, 0, extract_one(a, ae, 0xBA8E3BAA));
+
+       assertEqualIntA(a, ARCHIVE_EOF, archive_read_next_header(a, &ae));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_close(a));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_free(a));
+}
+
+DEFINE_TEST(test_read_format_zip_zstd_multi_blockread)
+{
+       const char *refname = "test_read_format_zip_zstd_multi.zipx";
+       struct archive *a;
+       struct archive_entry *ae;
+
+       assert((a = archive_read_new()) != NULL);
+       if (ARCHIVE_OK != archive_read_support_filter_zstd(a)) {
+               skipping("zstd is not fully supported on this platform");
+               archive_read_close(a);
+               return;
+       }
+       extract_reference_file(refname);
+
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_support_format_zip(a));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_open_filename(a, refname, 37));
+
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae));
+       assertEqualString("ZIP 2.0 (zstd)", archive_format_name(a));
+       assertEqualString("smartd.conf", archive_entry_pathname(ae));
+       assertEqualIntA(a, 0, extract_one_using_blocks(a, 12, 0x8DD7379E));
+
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae));
+       assertEqualString("ZIP 2.0 (zstd)", archive_format_name(a));
+       assertEqualString("ts.conf", archive_entry_pathname(ae));
+       assertEqualIntA(a, 0, extract_one_using_blocks(a, 13, 0x7AE59B31));
+
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae));
+       assertEqualString("ZIP 2.0 (zstd)", archive_format_name(a));
+       assertEqualString("vimrc", archive_entry_pathname(ae));
+       assertEqualIntA(a, 0, extract_one_using_blocks(a, 14, 0xBA8E3BAA));
+
+       assertEqualIntA(a, ARCHIVE_EOF, archive_read_next_header(a, &ae));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_close(a));
+       assertEqualIntA(a, ARCHIVE_OK, archive_read_free(a));
+}
+
 DEFINE_TEST(test_read_format_zip_xz_multi)
 {
        const char *refname = "test_read_format_zip_xz_multi.zipx";
diff --git a/libarchive/test/test_read_format_zip_zstd.zipx.uu b/libarchive/test/test_read_format_zip_zstd.zipx.uu
new file mode 100644 (file)
index 0000000..58a2958
--- /dev/null
@@ -0,0 +1,18 @@
+begin 644 test_read_format_zip_zstd.zipx
+M4$L#!!0```!=`#TQD4VJ.XZZ&P(``)`#```%````=FEM<F,HM2_]`%B5$``F
+M*&PC(&_;!LR_L%[])MU@R?]OORW$@9`DQ+<"F#-&'$Y6"9""0&%E`%\`7@"1
+M7?FX:LJ\X?*WT9.43+L]A_#MKX7+T^E'CZM<W%N^:O@!SJ4,4TAA"BGIT&'%
+M\5.>I.97\6@M?/P$SA1R6+RFRUD<*@L-#!$.$A`-#!,!;_I.?CH\]WK(I'HJ
+MC1"P;([X:RD\"4NZL[X9?H=*YI9Q^9SUG`U"%GS.%'JF0M<\[PV?3#MR">63
+MU+);7<0EM,_'U]+(<VG*;PPLCBFD+)V*P',I<U:G]:N>)+7>.MS@IFGS.GUF
+M'S5\-@V=Y=MW6",,P^\(`,FALG&E(V`ZX;1&#G$;^?8[^=%R^:Q[GB1^JPZA
+M-SI]3O>XY5O/>)70LG3/QWO;L^DZJQ/Q)"5'6^UGS\CUT,:'[<)XDMY"B%WV
+MPK<V35TV@^>$('N2MJ]2M61L`\D@@2E$0?$HA-^YDM0T959VUCN4BS.J<-=[
+M6P)HFD)TO47V[FWT;?<:9=HN25KX&K(SA?9*P8'"=&]T1G?9G8^[AR>)P11R
+MY'H86B0%XTKA&U$O;*N]M,>LJ)E`K77)I'N2E`(B(!!&C.+TY**V^BU(V9R8
+M!ZLXW)6;"!YPP'E,4_F\OB!T"4Z:6/#"Z823+@5V#BNHGHC-+4@`)>?""C,)
+M2+G'/1,#D-1'B&&R288:'(KD,I>"_/85<Z&-7Q#VF647!5!+`0(_`Q0```!=
+M`#TQD4VJ.XZZ&P(``)`#```%``````````````"D@0````!V:6UR8U!+!08`
+1`````0`!`#,````^`@``````
+`
+end
diff --git a/libarchive/test/test_read_format_zip_zstd_multi.zipx.uu b/libarchive/test/test_read_format_zip_zstd_multi.zipx.uu
new file mode 100644 (file)
index 0000000..502024e
--- /dev/null
@@ -0,0 +1,94 @@
+begin 644 test_read_format_zip_zstd_multi.zipx
+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
+I``````````"D@08-``!V:6UR8U!+!08``````P`#`*$```!$#P``````
+`
+end