zipinfo: support DOS attributes.

golang doesn't include Unix mode by default.

Also show all the deflate variants ("defN" versus "defX").

Cope better with being called directly rather than via symlink.

Test: manual
Change-Id: I23b441c847ce9a557ea866b3c43bdf0542b26f10
diff --git a/libziparchive/include/ziparchive/zip_archive.h b/libziparchive/include/ziparchive/zip_archive.h
index f0f5a1d..047af90 100644
--- a/libziparchive/include/ziparchive/zip_archive.h
+++ b/libziparchive/include/ziparchive/zip_archive.h
@@ -40,8 +40,8 @@
  * 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 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 @@
   // 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 e936614..6c2221e 100644
--- a/libziparchive/unzip.cpp
+++ b/libziparchive/unzip.cpp
@@ -34,13 +34,21 @@
 #include <android-base/strings.h>
 #include <ziparchive/zip_archive.h>
 
+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 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 MaybeShowFooter() {
-  if (is_unzip) {
+  if (role == kUnzip) {
     if (flag_v) {
       printf(
           "--------          -------  ---                            -------\n"
@@ -185,8 +193,7 @@
 
 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 @@
   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 @@
   }
 
   // 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 @@
   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 @@
     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 @@
            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 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 @@
 }
 
 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 caf8fae..ef29188 100644
--- a/libziparchive/zip_archive.cc
+++ b/libziparchive/zip_archive.cc
@@ -622,12 +622,16 @@
 
   // 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);