diff --git a/libziparchive/include/ziparchive/zip_archive.h b/libziparchive/include/ziparchive/zip_archive.h index f0f5a1d23..047af90f2 100644 --- a/libziparchive/include/ziparchive/zip_archive.h +++ b/libziparchive/include/ziparchive/zip_archive.h @@ -40,8 +40,8 @@ enum { * Represents information about a zip entry in a zip file. */ struct ZipEntry { - // Compression method: One of kCompressStored or - // kCompressDeflated. + // Compression method. One of kCompressStored or kCompressDeflated. + // See also `gpbf` for deflate subtypes. uint16_t method; // Modification time. The zipfile format specifies @@ -55,7 +55,7 @@ struct ZipEntry { struct tm GetModificationTime() const; // Suggested Unix mode for this entry, from the zip archive if created on - // Unix, or a default otherwise. + // Unix, or a default otherwise. See also `external_file_attributes`. mode_t unix_mode; // 1 if this entry contains a data descriptor segment, 0 @@ -80,10 +80,16 @@ struct ZipEntry { // The offset to the start of data for this ZipEntry. off64_t offset; - // The version of zip and the host file system this came from. + // The version of zip and the host file system this came from (for zipinfo). uint16_t version_made_by; - // Whether this entry is believed to be text or binary. + // The raw attributes, whose interpretation depends on the host + // file system in `version_made_by` (for zipinfo). See also `unix_mode`. + uint32_t external_file_attributes; + + // Specifics about the deflation (for zipinfo). + uint16_t gpbf; + // Whether this entry is believed to be text or binary (for zipinfo). bool is_text; }; diff --git a/libziparchive/unzip.cpp b/libziparchive/unzip.cpp index e93661441..6c2221ed3 100644 --- a/libziparchive/unzip.cpp +++ b/libziparchive/unzip.cpp @@ -34,13 +34,21 @@ #include #include +using android::base::EndsWith; +using android::base::StartsWith; + enum OverwriteMode { kAlways, kNever, kPrompt, }; -static bool is_unzip; +enum Role { + kUnzip, + kZipinfo, +}; + +static Role role; static OverwriteMode overwrite_mode = kPrompt; static bool flag_1 = false; static const char* flag_d = nullptr; @@ -92,7 +100,7 @@ static int CompressionRatio(int64_t uncompressed, int64_t compressed) { } static void MaybeShowHeader(ZipArchiveHandle zah) { - if (is_unzip) { + if (role == kUnzip) { // unzip has three formats. if (!flag_q) printf("Archive: %s\n", archive_name); if (flag_v) { @@ -116,7 +124,7 @@ static void MaybeShowHeader(ZipArchiveHandle zah) { } static void MaybeShowFooter() { - if (is_unzip) { + if (role == kUnzip) { if (flag_v) { printf( "-------- ------- --- -------\n" @@ -185,8 +193,7 @@ static void ExtractToPipe(ZipArchiveHandle zah, ZipEntry& entry, const std::stri static void ExtractOne(ZipArchiveHandle zah, ZipEntry& entry, const std::string& name) { // Bad filename? - if (android::base::StartsWith(name, "/") || android::base::StartsWith(name, "../") || - name.find("/../") != std::string::npos) { + if (StartsWith(name, "/") || StartsWith(name, "../") || name.find("/../") != std::string::npos) { error(1, 0, "bad filename %s", name.c_str()); } @@ -194,7 +201,7 @@ static void ExtractOne(ZipArchiveHandle zah, ZipEntry& entry, const std::string& std::string dst; if (flag_d) { dst = flag_d; - if (!android::base::EndsWith(dst, "/")) dst += '/'; + if (!EndsWith(dst, "/")) dst += '/'; } dst += name; @@ -204,7 +211,7 @@ static void ExtractOne(ZipArchiveHandle zah, ZipEntry& entry, const std::string& } // An entry in a zip file can just be a directory itself. - if (android::base::EndsWith(name, "/")) { + if (EndsWith(name, "/")) { if (mkdir(name.c_str(), entry.unix_mode) == -1) { // If the directory already exists, that's fine. if (errno == EEXIST) { @@ -258,9 +265,26 @@ static void InfoOne(const ZipEntry& entry, const std::string& name) { int version = entry.version_made_by & 0xff; int os = (entry.version_made_by >> 8) & 0xff; - // TODO: Support suid/sgid? Non-Unix host file system attributes? - char mode[] = "??????????"; - if (os == 3) { + // TODO: Support suid/sgid? Non-Unix/non-FAT host file system attributes? + const char* src_fs = "???"; + char mode[] = "??? "; + if (os == 0) { + src_fs = "fat"; + // https://docs.microsoft.com/en-us/windows/win32/fileio/file-attribute-constants + int attrs = entry.external_file_attributes & 0xff; + mode[0] = (attrs & 0x10) ? 'd' : '-'; + mode[1] = 'r'; + mode[2] = (attrs & 0x01) ? '-' : 'w'; + // The man page also mentions ".btm", but that seems to be obsolete? + mode[3] = EndsWith(name, ".exe") || EndsWith(name, ".com") || EndsWith(name, ".bat") || + EndsWith(name, ".cmd") + ? 'x' + : '-'; + mode[4] = (attrs & 0x20) ? 'a' : '-'; + mode[5] = (attrs & 0x02) ? 'h' : '-'; + mode[6] = (attrs & 0x04) ? 's' : '-'; + } else if (os == 3) { + src_fs = "unx"; mode[0] = S_ISDIR(entry.unix_mode) ? 'd' : (S_ISREG(entry.unix_mode) ? '-' : '?'); mode[1] = entry.unix_mode & S_IRUSR ? 'r' : '-'; mode[2] = entry.unix_mode & S_IWUSR ? 'w' : '-'; @@ -273,6 +297,11 @@ static void InfoOne(const ZipEntry& entry, const std::string& name) { mode[9] = entry.unix_mode & S_IXOTH ? 'x' : '-'; } + char method[5] = "stor"; + if (entry.method == kCompressDeflated) { + snprintf(method, sizeof(method), "def%c", "NXFS"[(entry.gpbf >> 1) & 0x3]); + } + // TODO: zipinfo (unlike unzip) sometimes uses time zone? // TODO: this uses 4-digit years because we're not barbarians unless interoperability forces it. tm t = entry.GetModificationTime(); @@ -281,14 +310,13 @@ static void InfoOne(const ZipEntry& entry, const std::string& name) { t.tm_mday, t.tm_hour, t.tm_min); // "-rw-r--r-- 3.0 unx 577 t- defX 19-Feb-12 16:09 android-ndk-r19b/sources/android/NOTICE" - printf("%s %2d.%d %s %8d %c%c %s %s %s\n", mode, version / 10, version % 10, - os == 3 ? "unx" : "???", entry.uncompressed_length, entry.is_text ? 't' : 'b', - entry.has_data_descriptor ? 'X' : 'x', entry.method == kCompressStored ? "stor" : "defX", - time, name.c_str()); + printf("%s %2d.%d %s %8d %c%c %s %s %s\n", mode, version / 10, version % 10, src_fs, + entry.uncompressed_length, entry.is_text ? 't' : 'b', + entry.has_data_descriptor ? 'X' : 'x', method, time, name.c_str()); } static void ProcessOne(ZipArchiveHandle zah, ZipEntry& entry, const std::string& name) { - if (is_unzip) { + if (role == kUnzip) { if (flag_l || flag_v) { // -l or -lv or -lq or -v. ListOne(entry, name); @@ -333,7 +361,7 @@ static void ProcessAll(ZipArchiveHandle zah) { } static void ShowHelp(bool full) { - if (is_unzip) { + if (role == kUnzip) { fprintf(full ? stdout : stderr, "usage: unzip [-d DIR] [-lnopqv] ZIP [FILE...] [-x FILE...]\n"); if (!full) exit(EXIT_FAILURE); @@ -391,12 +419,22 @@ static void HandleCommonOption(int opt) { } int main(int argc, char* argv[]) { - static struct option opts[] = { + // Who am I, and what am I doing? + const char* base = basename(argv[0]); + if (!strcmp(base, "ziptool") && argc > 1) return main(argc - 1, argv + 1); + if (!strcmp(base, "unzip")) { + role = kUnzip; + } else if (!strcmp(base, "zipinfo")) { + role = kZipinfo; + } else { + error(1, 0, "run as ziptool with unzip or zipinfo as the first argument, or symlink"); + } + + static const struct option opts[] = { {"help", no_argument, 0, 'h'}, }; - is_unzip = !strcmp(basename(argv[0]), "unzip"); - if (is_unzip) { + if (role == kUnzip) { int opt; while ((opt = getopt_long(argc, argv, "-d:hlnopqvx", opts, nullptr)) != -1) { switch (opt) { diff --git a/libziparchive/zip_archive.cc b/libziparchive/zip_archive.cc index caf8fae60..ef29188fb 100644 --- a/libziparchive/zip_archive.cc +++ b/libziparchive/zip_archive.cc @@ -622,12 +622,16 @@ static int32_t FindEntry(const ZipArchive* archive, const int32_t ent, ZipEntry* // 4.4.2.1: the upper byte of `version_made_by` gives the source OS. Unix is 3. data->version_made_by = cdr->version_made_by; + data->external_file_attributes = cdr->external_file_attributes; if ((data->version_made_by >> 8) == 3) { data->unix_mode = (cdr->external_file_attributes >> 16) & 0xffff; } else { data->unix_mode = 0777; } + // 4.4.4: general purpose bit flags. + data->gpbf = lfh->gpb_flags; + // 4.4.14: the lowest bit of the internal file attributes field indicates text. // Currently only needed to implement zipinfo. data->is_text = (cdr->internal_file_attributes & 1);