ash: less hackish implementation of evaltreenr()
[oweals/busybox.git] / archival / unzip.c
index 9214935916ca300af24856ce3cb1fd7c306455f1..8ed9ae7d5ed28a7617a4b33eec6c4553b21882f6 100644 (file)
  * Zip64 + other methods
  */
 //config:config UNZIP
-//config:      bool "unzip"
+//config:      bool "unzip (24 kb)"
 //config:      default y
 //config:      help
-//config:        unzip will list or extract files from a ZIP archive,
-//config:        commonly found on DOS/WIN systems. The default behavior
-//config:        (with no options) is to extract the archive into the
-//config:        current directory.
+//config:      unzip will list or extract files from a ZIP archive,
+//config:      commonly found on DOS/WIN systems. The default behavior
+//config:      (with no options) is to extract the archive into the
+//config:      current directory.
 //config:
 //config:config FEATURE_UNZIP_CDF
 //config:      bool "Read and use Central Directory data"
 //config:      default y
 //config:      depends on UNZIP
 //config:      help
-//config:        If you know that you only need to deal with simple
-//config:        ZIP files without deleted/updated files, SFX archives etc,
-//config:        you can reduce code size by unselecting this option.
-//config:        To support less trivial ZIPs, say Y.
+//config:      If you know that you only need to deal with simple
+//config:      ZIP files without deleted/updated files, SFX archives etc,
+//config:      you can reduce code size by unselecting this option.
+//config:      To support less trivial ZIPs, say Y.
+//config:
+//config:config FEATURE_UNZIP_BZIP2
+//config:      bool "Support compression method 12 (bzip2)"
+//config:      default y
+//config:      depends on FEATURE_UNZIP_CDF && DESKTOP
+// FEATURE_UNZIP_CDF is needed, otherwise we can't find start of next file
+// DESKTOP is needed to get back uncompressed length
+//config:
+//config:config FEATURE_UNZIP_LZMA
+//config:      bool "Support compression method 14 (lzma)"
+//config:      default y
+//config:      depends on FEATURE_UNZIP_CDF && DESKTOP
+//config:
+//config:config FEATURE_UNZIP_XZ
+//config:      bool "Support compression method 95 (xz)"
+//config:      default y
+//config:      depends on FEATURE_UNZIP_CDF && DESKTOP
 
 //applet:IF_UNZIP(APPLET(unzip, BB_DIR_USR_BIN, BB_SUID_DROP))
 //kbuild:lib-$(CONFIG_UNZIP) += unzip.o
@@ -45,6 +62,7 @@
 //usage:     "\n       -l      List contents (with -q for short form)"
 //usage:     "\n       -n      Never overwrite files (default: ask)"
 //usage:     "\n       -o      Overwrite"
+//usage:     "\n       -j      Do not restore paths"
 //usage:     "\n       -p      Print to stdout"
 //usage:     "\n       -q      Quiet"
 //usage:     "\n       -x FILE Exclude FILEs"
@@ -96,6 +114,7 @@ typedef union {
 
 #define FIX_ENDIANNESS_ZIP(zip) \
 do { if (BB_BIG_ENDIAN) { \
+       (zip).fmt.method        = SWAP_LE16((zip).fmt.method      ); \
        (zip).fmt.crc32         = SWAP_LE32((zip).fmt.crc32       ); \
        (zip).fmt.cmpsize       = SWAP_LE32((zip).fmt.cmpsize     ); \
        (zip).fmt.ucmpsize      = SWAP_LE32((zip).fmt.ucmpsize    ); \
@@ -222,7 +241,7 @@ static uint32_t find_cdf_offset(void)
                end = 0;
 
        dbg("Looking for cdf_offset starting from 0x%"OFF_FMT"x", end);
-       xlseek(zip_fd, end, SEEK_SET);
+       xlseek(zip_fd, end, SEEK_SET);
        buf = xzalloc(PEEK_FROM_END);
        full_read(zip_fd, buf, PEEK_FROM_END);
 
@@ -300,6 +319,12 @@ static uint32_t read_next_cdf(uint32_t cdf_offset, cdf_header_t *cdf)
 };
 #endif
 
+static void die_if_bad_fnamesize(unsigned sz)
+{
+       if (sz > 0xfff) /* more than 4k?! no funny business please */
+               bb_error_msg_and_die("bad archive");
+}
+
 static void unzip_skip(off_t skip)
 {
        if (skip != 0)
@@ -317,34 +342,100 @@ static void unzip_create_leading_dirs(const char *fn)
        free(name);
 }
 
+#if ENABLE_FEATURE_UNZIP_CDF
+static void unzip_extract_symlink(zip_header_t *zip, const char *dst_fn)
+{
+       char *target;
+
+       die_if_bad_fnamesize(zip->fmt.ucmpsize);
+
+       if (zip->fmt.method == 0) {
+               /* Method 0 - stored (not compressed) */
+               target = xzalloc(zip->fmt.ucmpsize + 1);
+               xread(zip_fd, target, zip->fmt.ucmpsize);
+       } else {
+#if 1
+               bb_error_msg_and_die("compressed symlink is not supported");
+#else
+               transformer_state_t xstate;
+               init_transformer_state(&xstate);
+               xstate.mem_output_size_max = zip->fmt.ucmpsize;
+               /* ...unpack... */
+               if (!xstate.mem_output_buf)
+                       WTF();
+               target = xstate.mem_output_buf;
+               target = xrealloc(target, xstate.mem_output_size + 1);
+               target[xstate.mem_output_size] = '\0';
+#endif
+       }
+//TODO: libbb candidate
+       if (symlink(target, dst_fn))
+               bb_perror_msg_and_die("can't create symlink '%s'", dst_fn);
+       free(target);
+}
+#endif
+
 static void unzip_extract(zip_header_t *zip, int dst_fd)
 {
+       transformer_state_t xstate;
+
        if (zip->fmt.method == 0) {
                /* Method 0 - stored (not compressed) */
                off_t size = zip->fmt.ucmpsize;
                if (size)
                        bb_copyfd_exact_size(zip_fd, dst_fd, size);
-       } else {
+               return;
+       }
+
+       init_transformer_state(&xstate);
+       xstate.bytes_in = zip->fmt.cmpsize;
+       xstate.src_fd = zip_fd;
+       xstate.dst_fd = dst_fd;
+       if (zip->fmt.method == 8) {
                /* Method 8 - inflate */
-               transformer_state_t xstate;
-               init_transformer_state(&xstate);
-               xstate.bytes_in = zip->fmt.cmpsize;
-               xstate.src_fd = zip_fd;
-               xstate.dst_fd = dst_fd;
                if (inflate_unzip(&xstate) < 0)
                        bb_error_msg_and_die("inflate error");
                /* Validate decompression - crc */
                if (zip->fmt.crc32 != (xstate.crc32 ^ 0xffffffffL)) {
                        bb_error_msg_and_die("crc error");
                }
-               /* Validate decompression - size */
-               if (zip->fmt.ucmpsize != xstate.bytes_out) {
-                       /* Don't die. Who knows, maybe len calculation
-                        * was botched somewhere. After all, crc matched! */
-                       bb_error_msg("bad length");
-               }
        }
-       /* TODO? method 12: bzip2, method 14: LZMA */
+#if ENABLE_FEATURE_UNZIP_BZIP2
+       else if (zip->fmt.method == 12) {
+               /* Tested. Unpacker reads too much, but we use CDF
+                * and will seek to the correct beginning of next file.
+                */
+               xstate.bytes_out = unpack_bz2_stream(&xstate);
+               if (xstate.bytes_out < 0)
+                       bb_error_msg_and_die("inflate error");
+       }
+#endif
+#if ENABLE_FEATURE_UNZIP_LZMA
+       else if (zip->fmt.method == 14) {
+               /* Not tested yet */
+               xstate.bytes_out = unpack_lzma_stream(&xstate);
+               if (xstate.bytes_out < 0)
+                       bb_error_msg_and_die("inflate error");
+       }
+#endif
+#if ENABLE_FEATURE_UNZIP_XZ
+       else if (zip->fmt.method == 95) {
+               /* Not tested yet */
+               xstate.bytes_out = unpack_xz_stream(&xstate);
+               if (xstate.bytes_out < 0)
+                       bb_error_msg_and_die("inflate error");
+       }
+#endif
+       else {
+               bb_error_msg_and_die("unsupported method %u", zip->fmt.method);
+       }
+
+       /* Validate decompression - size */
+       if (zip->fmt.ucmpsize != xstate.bytes_out) {
+               /* Don't die. Who knows, maybe len calculation
+                * was botched somewhere. After all, crc matched! */
+               bb_error_msg("bad length");
+       }
 }
 
 static void my_fgets80(char *buf80)
@@ -355,16 +446,32 @@ static void my_fgets80(char *buf80)
        }
 }
 
+static int get_lstat_mode(const char *dst_fn)
+{
+       struct stat stat_buf;
+       if (lstat(dst_fn, &stat_buf) == -1) {
+               if (errno != ENOENT) {
+                       bb_perror_msg_and_die("can't stat '%s'", dst_fn);
+               }
+               /* File does not exist */
+               return -1;
+       }
+       return stat_buf.st_mode;
+}
+
 int unzip_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
 int unzip_main(int argc, char **argv)
 {
-       enum { O_PROMPT, O_NEVER, O_ALWAYS };
-
+       enum {
+               OPT_l = (1 << 0),
+               OPT_x = (1 << 1),
+               OPT_j = (1 << 2),
+       };
+       unsigned opts;
        smallint quiet = 0;
        IF_NOT_FEATURE_UNZIP_CDF(const) smallint verbose = 0;
-       smallint listing = 0;
+       enum { O_PROMPT, O_NEVER, O_ALWAYS };
        smallint overwrite = O_PROMPT;
-       smallint x_opt_seen;
        uint32_t cdf_offset;
        unsigned long total_usize;
        unsigned long total_size;
@@ -375,9 +482,8 @@ int unzip_main(int argc, char **argv)
        llist_t *zaccept = NULL;
        llist_t *zreject = NULL;
        char *base_dir = NULL;
-       int i, opt;
+       int i;
        char key_buf[80]; /* must match size used by my_fgets80 */
-       struct stat stat_buf;
 
 /* -q, -l and -v: UnZip 5.52 of 28 February 2005, by Info-ZIP:
  *
@@ -420,16 +526,16 @@ int unzip_main(int argc, char **argv)
  *    204372                   1 file
  */
 
-       x_opt_seen = 0;
+       opts = 0;
        /* '-' makes getopt return 1 for non-options */
-       while ((opt = getopt(argc, argv, "-d:lnopqxv")) != -1) {
-               switch (opt) {
+       while ((i = getopt(argc, argv, "-d:lnopqxjv")) != -1) {
+               switch (i) {
                case 'd':  /* Extract to base directory */
                        base_dir = optarg;
                        break;
 
                case 'l': /* List */
-                       listing = 1;
+                       opts |= OPT_l;
                        break;
 
                case 'n': /* Never overwrite existing files */
@@ -449,11 +555,15 @@ int unzip_main(int argc, char **argv)
 
                case 'v': /* Verbose list */
                        IF_FEATURE_UNZIP_CDF(verbose++;)
-                       listing = 1;
+                       opts |= OPT_l;
                        break;
 
                case 'x':
-                       x_opt_seen = 1;
+                       opts |= OPT_x;
+                       break;
+
+               case 'j':
+                       opts |= OPT_j;
                        break;
 
                case 1:
@@ -462,7 +572,7 @@ int unzip_main(int argc, char **argv)
                                /* +5: space for ".zip" and NUL */
                                src_fn = xmalloc(strlen(optarg) + 5);
                                strcpy(src_fn, optarg);
-                       } else if (!x_opt_seen) {
+                       } else if (!(opts & OPT_x)) {
                                /* Include files */
                                llist_add_to(&zaccept, optarg);
                        } else {
@@ -530,7 +640,7 @@ int unzip_main(int argc, char **argv)
        if (quiet <= 1) { /* not -qq */
                if (quiet == 0)
                        printf("Archive:  %s\n", src_fn);
-               if (listing) {
+               if (opts & OPT_l) {
                        puts(verbose ?
                                " Length   Method    Size  Cmpr    Date    Time   CRC-32   Name\n"
                                "--------  ------  ------- ---- ---------- ----- --------  ----"
@@ -609,14 +719,9 @@ int unzip_main(int argc, char **argv)
 
                        xread(zip_fd, zip.raw, ZIP_HEADER_LEN);
                        FIX_ENDIANNESS_ZIP(zip);
-                       if ((zip.fmt.method != 0)
-                        && (zip.fmt.method != 8)
-                       ) {
-                               /* TODO? method 12: bzip2, method 14: LZMA */
-                               bb_error_msg_and_die("unsupported method %d", zip.fmt.method);
-                       }
-                       if (zip.fmt.zip_flags & SWAP_LE16(0x0009)) {
-                               bb_error_msg_and_die("zip flags 1 and 8 are not supported");
+                       if (zip.fmt.zip_flags & SWAP_LE16(0x0008)) {
+                               bb_error_msg_and_die("zip flag %s is not supported",
+                                       "8 (streaming)");
                        }
                }
 #if ENABLE_FEATURE_UNZIP_CDF
@@ -640,6 +745,20 @@ int unzip_main(int argc, char **argv)
                                zip.fmt.cmpsize  = cdf.fmt.cmpsize;
                                zip.fmt.ucmpsize = cdf.fmt.ucmpsize;
                        }
+// Seen in some zipfiles: central directory 9 byte extra field contains
+// a subfield with ID 0x5455 and 5 data bytes, which is a Unix-style UTC mtime.
+// Local header version:
+//  u16 0x5455 ("UT")
+//  u16 size (1 + 4 * n)
+//  u8  flags: bit 0:mtime is present, bit 1:atime is present, bit 2:ctime is present
+//  u32 mtime
+//  u32 atime
+//  u32 ctime
+// Central header version:
+//  u16 0x5455 ("UT")
+//  u16 size (5 (or 1?))
+//  u8  flags: bit 0:mtime is present, bit 1:atime is present, bit 2:ctime is present
+//  u32 mtime (CDF does not store atime/ctime)
 # else
                        /* CDF has the same data as local header, no need to read the latter...
                         * ...not really. An archive was seen with cdf.extra_len == 6 but
@@ -660,7 +779,8 @@ int unzip_main(int argc, char **argv)
 
                if (zip.fmt.zip_flags & SWAP_LE16(0x0001)) {
                        /* 0x0001 - encrypted */
-                       bb_error_msg_and_die("zip flag 1 (encryption) is not supported");
+                       bb_error_msg_and_die("zip flag %s is not supported",
+                                       "1 (encryption)");
                }
                dbg("File cmpsize:0x%x extra_len:0x%x ucmpsize:0x%x",
                        (unsigned)zip.fmt.cmpsize,
@@ -670,155 +790,177 @@ int unzip_main(int argc, char **argv)
 
                /* Read filename */
                free(dst_fn);
+               die_if_bad_fnamesize(zip.fmt.filename_len);
                dst_fn = xzalloc(zip.fmt.filename_len + 1);
                xread(zip_fd, dst_fn, zip.fmt.filename_len);
-
                /* Skip extra header bytes */
                unzip_skip(zip.fmt.extra_len);
 
                /* Guard against "/abspath", "/../" and similar attacks */
                overlapping_strcpy(dst_fn, strip_unsafe_prefix(dst_fn));
 
+               if (opts & OPT_j) /* Strip paths? */
+                       overlapping_strcpy(dst_fn, bb_basename(dst_fn));
+
+               /* Did this strip everything ("DIR/" case)? Then skip */
+               if (!dst_fn[0])
+                       goto skip_cmpsize;
+
                /* Filter zip entries */
                if (find_list_entry(zreject, dst_fn)
                 || (zaccept && !find_list_entry(zaccept, dst_fn))
                ) { /* Skip entry */
-                       i = 'n';
-               } else {
-                       if (listing) {
-                               /* List entry */
-                               char dtbuf[sizeof("mm-dd-yyyy hh:mm")];
-                               sprintf(dtbuf, "%02u-%02u-%04u %02u:%02u",
-                                       (zip.fmt.moddate >> 5) & 0xf,  // mm: 0x01e0
-                                       (zip.fmt.moddate)      & 0x1f, // dd: 0x001f
-                                       (zip.fmt.moddate >> 9) + 1980, // yy: 0xfe00
-                                       (zip.fmt.modtime >> 11),       // hh: 0xf800
-                                       (zip.fmt.modtime >> 5) & 0x3f  // mm: 0x07e0
-                                       // seconds/2 not shown, encoded in -- 0x001f
-                               );
-                               if (!verbose) {
-                                       //      "  Length      Date    Time    Name\n"
-                                       //      "---------  ---------- -----   ----"
-                                       printf(       "%9u  " "%s   "         "%s\n",
-                                               (unsigned)zip.fmt.ucmpsize,
-                                               dtbuf,
-                                               dst_fn);
-                               } else {
-                                       unsigned long percents = zip.fmt.ucmpsize - zip.fmt.cmpsize;
-                                       if ((int32_t)percents < 0)
-                                               percents = 0; /* happens if ucmpsize < cmpsize */
-                                       percents = percents * 100;
-                                       if (zip.fmt.ucmpsize)
-                                               percents /= zip.fmt.ucmpsize;
-                                       //      " Length   Method    Size  Cmpr    Date    Time   CRC-32   Name\n"
-                                       //      "--------  ------  ------- ---- ---------- ----- --------  ----"
-                                       printf(      "%8u  %s"        "%9u%4u%% " "%s "         "%08x  "  "%s\n",
-                                               (unsigned)zip.fmt.ucmpsize,
-                                               zip.fmt.method == 0 ? "Stored" : "Defl:N", /* Defl is method 8 */
-/* TODO: show other methods?
- *  1 - Shrunk
- *  2 - Reduced with compression factor 1
- *  3 - Reduced with compression factor 2
- *  4 - Reduced with compression factor 3
- *  5 - Reduced with compression factor 4
- *  6 - Imploded
- *  7 - Reserved for Tokenizing compression algorithm
- *  9 - Deflate64
- * 10 - PKWARE Data Compression Library Imploding
- * 11 - Reserved by PKWARE
- * 12 - BZIP2
- * 14 - LZMA
- */
-                                               (unsigned)zip.fmt.cmpsize,
-                                               (unsigned)percents,
-                                               dtbuf,
-                                               zip.fmt.crc32,
-                                               dst_fn);
-                                       total_size += zip.fmt.cmpsize;
+                       goto skip_cmpsize;
+               }
+
+               if (opts & OPT_l) {
+                       /* List entry */
+                       char dtbuf[sizeof("mm-dd-yyyy hh:mm")];
+                       sprintf(dtbuf, "%02u-%02u-%04u %02u:%02u",
+                               (zip.fmt.moddate >> 5) & 0xf,  // mm: 0x01e0
+                               (zip.fmt.moddate)      & 0x1f, // dd: 0x001f
+                               (zip.fmt.moddate >> 9) + 1980, // yy: 0xfe00
+                               (zip.fmt.modtime >> 11),       // hh: 0xf800
+                               (zip.fmt.modtime >> 5) & 0x3f  // mm: 0x07e0
+                               // seconds/2 not shown, encoded in -- 0x001f
+                       );
+                       if (!verbose) {
+                               //      "  Length      Date    Time    Name\n"
+                               //      "---------  ---------- -----   ----"
+                               printf(       "%9u  " "%s   "         "%s\n",
+                                       (unsigned)zip.fmt.ucmpsize,
+                                       dtbuf,
+                                       dst_fn);
+                       } else {
+                               char method6[7];
+                               unsigned long percents;
+
+                               sprintf(method6, "%6u", zip.fmt.method);
+                               if (zip.fmt.method == 0) {
+                                       strcpy(method6, "Stored");
                                }
-                               total_usize += zip.fmt.ucmpsize;
-                               i = 'n';
-                       } else if (dst_fd == STDOUT_FILENO) {
-                               /* Extracting to STDOUT */
-                               i = -1;
-                       } else if (last_char_is(dst_fn, '/')) {
-                               /* Extract directory */
-                               if (stat(dst_fn, &stat_buf) == -1) {
-                                       if (errno != ENOENT) {
-                                               bb_perror_msg_and_die("can't stat '%s'", dst_fn);
-                                       }
-                                       if (!quiet) {
-                                               printf("   creating: %s\n", dst_fn);
-                                       }
-                                       unzip_create_leading_dirs(dst_fn);
-                                       if (bb_make_directory(dst_fn, dir_mode, FILEUTILS_IGNORE_CHMOD_ERR)) {
-                                               xfunc_die();
-                                       }
-                               } else {
-                                       if (!S_ISDIR(stat_buf.st_mode)) {
-                                               bb_error_msg_and_die("'%s' exists but is not a %s",
-                                                       dst_fn, "directory");
-                                       }
+                               if (zip.fmt.method == 8) {
+                                       strcpy(method6, "Defl:N");
+                                       /* normal, maximum, fast, superfast */
+                                       IF_DESKTOP(method6[5] = "NXFS"[(zip.fmt.zip_flags >> 1) & 3];)
+                               }
+                               percents = zip.fmt.ucmpsize - zip.fmt.cmpsize;
+                               if ((int32_t)percents < 0)
+                                       percents = 0; /* happens if ucmpsize < cmpsize */
+                               percents = percents * 100;
+                               if (zip.fmt.ucmpsize)
+                                       percents /= zip.fmt.ucmpsize;
+                               //      " Length   Method    Size  Cmpr    Date    Time   CRC-32   Name\n"
+                               //      "--------  ------  ------- ---- ---------- ----- --------  ----"
+                               printf(      "%8u  %s"        "%9u%4u%% " "%s "         "%08x  "  "%s\n",
+                                       (unsigned)zip.fmt.ucmpsize,
+                                       method6,
+                                       (unsigned)zip.fmt.cmpsize,
+                                       (unsigned)percents,
+                                       dtbuf,
+                                       zip.fmt.crc32,
+                                       dst_fn);
+                               total_size += zip.fmt.cmpsize;
+                       }
+                       total_usize += zip.fmt.ucmpsize;
+                       goto skip_cmpsize;
+               }
+
+               if (dst_fd == STDOUT_FILENO) {
+                       /* Extracting to STDOUT */
+                       goto do_extract;
+               }
+               if (last_char_is(dst_fn, '/')) {
+                       int mode;
+
+                       /* Extract directory */
+                       mode = get_lstat_mode(dst_fn);
+                       if (mode == -1) { /* ENOENT */
+                               if (!quiet) {
+                                       printf("   creating: %s\n", dst_fn);
+                               }
+                               unzip_create_leading_dirs(dst_fn);
+                               if (bb_make_directory(dst_fn, dir_mode, FILEUTILS_IGNORE_CHMOD_ERR)) {
+                                       xfunc_die();
                                }
-                               i = 'n';
                        } else {
-                               /* Extract file */
- check_file:
-                               if (stat(dst_fn, &stat_buf) == -1) {
-                                       /* File does not exist */
-                                       if (errno != ENOENT) {
-                                               bb_perror_msg_and_die("can't stat '%s'", dst_fn);
-                                       }
-                                       i = 'y';
-                               } else {
-                                       /* File already exists */
-                                       if (overwrite == O_NEVER) {
-                                               i = 'n';
-                                       } else if (S_ISREG(stat_buf.st_mode)) {
-                                               /* File is regular file */
-                                               if (overwrite == O_ALWAYS) {
-                                                       i = 'y';
-                                               } else {
-                                                       printf("replace %s? [y]es, [n]o, [A]ll, [N]one, [r]ename: ", dst_fn);
-                                                       my_fgets80(key_buf);
-                                                       i = key_buf[0];
-                                               }
-                                       } else {
-                                               /* File is not regular file */
-                                               bb_error_msg_and_die("'%s' exists but is not a %s",
-                                                       dst_fn, "regular file");
-                                       }
+                               if (!S_ISDIR(mode)) {
+                                       bb_error_msg_and_die("'%s' exists but is not a %s",
+                                               dst_fn, "directory");
                                }
                        }
+                       goto skip_cmpsize;
+               }
+ check_file:
+               /* Does target file already exist? */
+               {
+                       int mode = get_lstat_mode(dst_fn);
+                       if (mode == -1) {
+                               /* ENOENT: does not exist */
+                               goto do_open_and_extract;
+                       }
+                       if (overwrite == O_NEVER) {
+                               goto skip_cmpsize;
+                       }
+                       if (!S_ISREG(mode)) {
+ fishy:
+                               bb_error_msg_and_die("'%s' exists but is not a %s",
+                                       dst_fn, "regular file");
+                       }
+                       if (overwrite == O_ALWAYS) {
+                               goto do_open_and_extract;
+                       }
+                       printf("replace %s? [y]es, [n]o, [A]ll, [N]one, [r]ename: ", dst_fn);
+                       my_fgets80(key_buf);
+                       /* User input could take a long time. Is it still a regular file? */
+                       mode = get_lstat_mode(dst_fn);
+                       if (!S_ISREG(mode))
+                               goto fishy;
                }
 
-               switch (i) {
+               /* Extract (or skip) it */
+               switch (key_buf[0]) {
                case 'A':
                        overwrite = O_ALWAYS;
                case 'y': /* Open file and fall into unzip */
+ do_open_and_extract:
                        unzip_create_leading_dirs(dst_fn);
 #if ENABLE_FEATURE_UNZIP_CDF
-                       dst_fd = xopen3(dst_fn, O_WRONLY | O_CREAT | O_TRUNC, file_mode);
+                       dst_fd = -1;
+                       if (!S_ISLNK(file_mode)) {
+                               dst_fd = xopen3(dst_fn,
+                                       O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,
+                                       file_mode);
+                       }
 #else
-                       dst_fd = xopen(dst_fn, O_WRONLY | O_CREAT | O_TRUNC);
+                       /* O_NOFOLLOW defends against symlink attacks */
+                       dst_fd = xopen(dst_fn, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW);
 #endif
-               case -1: /* Unzip */
+ do_extract:
                        if (!quiet) {
                                printf(/* zip.fmt.method == 0
                                        ? " extracting: %s\n"
                                        : */ "  inflating: %s\n", dst_fn);
                        }
-                       unzip_extract(&zip, dst_fd);
-                       if (dst_fd != STDOUT_FILENO) {
-                               /* closing STDOUT is potentially bad for future business */
-                               close(dst_fd);
+#if ENABLE_FEATURE_UNZIP_CDF
+                       if (S_ISLNK(file_mode)) {
+                               if (dst_fd != STDOUT_FILENO) /* not -p? */
+                                       unzip_extract_symlink(&zip, dst_fn);
+                       } else
+#endif
+                       {
+                               unzip_extract(&zip, dst_fd);
+                               if (dst_fd != STDOUT_FILENO) {
+                                       /* closing STDOUT is potentially bad for future business */
+                                       close(dst_fd);
+                               }
                        }
                        break;
 
                case 'N':
                        overwrite = O_NEVER;
-               case 'n':
-                       /* Skip entry data */
+               case 'n': /* Skip entry data */
+ skip_cmpsize:
                        unzip_skip(zip.fmt.cmpsize);
                        break;
 
@@ -832,14 +974,14 @@ int unzip_main(int argc, char **argv)
                        goto check_file;
 
                default:
-                       printf("error: invalid response [%c]\n", (char)i);
+                       printf("error: invalid response [%c]\n", (char)key_buf[0]);
                        goto check_file;
                }
 
                total_entries++;
        }
 
-       if (listing && quiet <= 1) {
+       if ((opts & OPT_l) && quiet <= 1) {
                if (!verbose) {
                        //      "  Length      Date    Time    Name\n"
                        //      "---------  ---------- -----   ----"