1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 #include <errno.h>
18 #include <error.h>
19 #include <fcntl.h>
20 #include <getopt.h>
21 #include <inttypes.h>
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <sys/stat.h>
25 #include <sys/types.h>
26 #include <time.h>
27 #include <unistd.h>
28 
29 #include <set>
30 #include <string>
31 
32 #include <android-base/file.h>
33 #include <android-base/strings.h>
34 #include <ziparchive/zip_archive.h>
35 
36 enum OverwriteMode {
37   kAlways,
38   kNever,
39   kPrompt,
40 };
41 
42 static OverwriteMode overwrite_mode = kPrompt;
43 static const char* flag_d = nullptr;
44 static bool flag_l = false;
45 static bool flag_p = false;
46 static bool flag_q = false;
47 static bool flag_v = false;
48 static const char* archive_name = nullptr;
49 static std::set<std::string> includes;
50 static std::set<std::string> excludes;
51 static uint64_t total_uncompressed_length = 0;
52 static uint64_t total_compressed_length = 0;
53 static size_t file_count = 0;
54 
Filter(const std::string & name)55 static bool Filter(const std::string& name) {
56   if (!excludes.empty() && excludes.find(name) != excludes.end()) return true;
57   if (!includes.empty() && includes.find(name) == includes.end()) return true;
58   return false;
59 }
60 
MakeDirectoryHierarchy(const std::string & path)61 static bool MakeDirectoryHierarchy(const std::string& path) {
62   // stat rather than lstat because a symbolic link to a directory is fine too.
63   struct stat sb;
64   if (stat(path.c_str(), &sb) != -1 && S_ISDIR(sb.st_mode)) return true;
65 
66   // Ensure the parent directories exist first.
67   if (!MakeDirectoryHierarchy(android::base::Dirname(path))) return false;
68 
69   // Then try to create this directory.
70   return (mkdir(path.c_str(), 0777) != -1);
71 }
72 
CompressionRatio(int64_t uncompressed,int64_t compressed)73 static int CompressionRatio(int64_t uncompressed, int64_t compressed) {
74   if (uncompressed == 0) return 0;
75   return (100LL * (uncompressed - compressed)) / uncompressed;
76 }
77 
MaybeShowHeader()78 static void MaybeShowHeader() {
79   if (!flag_q) printf("Archive:  %s\n", archive_name);
80   if (flag_v) {
81     printf(
82         " Length   Method    Size  Cmpr    Date    Time   CRC-32   Name\n"
83         "--------  ------  ------- ---- ---------- ----- --------  ----\n");
84   } else if (flag_l) {
85     printf(
86         "  Length      Date    Time    Name\n"
87         "---------  ---------- -----   ----\n");
88   }
89 }
90 
MaybeShowFooter()91 static void MaybeShowFooter() {
92   if (flag_v) {
93     printf(
94         "--------          -------  ---                            -------\n"
95         "%8" PRId64 "         %8" PRId64 " %3d%%                            %zu file%s\n",
96         total_uncompressed_length, total_compressed_length,
97         CompressionRatio(total_uncompressed_length, total_compressed_length), file_count,
98         (file_count == 1) ? "" : "s");
99   } else if (flag_l) {
100     printf(
101         "---------                     -------\n"
102         "%9" PRId64 "                     %zu file%s\n",
103         total_uncompressed_length, file_count, (file_count == 1) ? "" : "s");
104   }
105 }
106 
PromptOverwrite(const std::string & dst)107 static bool PromptOverwrite(const std::string& dst) {
108   // TODO: [r]ename not implemented because it doesn't seem useful.
109   printf("replace %s? [y]es, [n]o, [A]ll, [N]one: ", dst.c_str());
110   fflush(stdout);
111   while (true) {
112     char* line = nullptr;
113     size_t n;
114     if (getline(&line, &n, stdin) == -1) {
115       error(1, 0, "(EOF/read error; assuming [N]one...)");
116       overwrite_mode = kNever;
117       return false;
118     }
119     if (n == 0) continue;
120     char cmd = line[0];
121     free(line);
122     switch (cmd) {
123       case 'y':
124         return true;
125       case 'n':
126         return false;
127       case 'A':
128         overwrite_mode = kAlways;
129         return true;
130       case 'N':
131         overwrite_mode = kNever;
132         return false;
133     }
134   }
135 }
136 
ExtractToPipe(ZipArchiveHandle zah,ZipEntry & entry,const std::string & name)137 static void ExtractToPipe(ZipArchiveHandle zah, ZipEntry& entry, const std::string& name) {
138   // We need to extract to memory because ExtractEntryToFile insists on
139   // being able to seek and truncate, and you can't do that with stdout.
140   uint8_t* buffer = new uint8_t[entry.uncompressed_length];
141   int err = ExtractToMemory(zah, &entry, buffer, entry.uncompressed_length);
142   if (err < 0) {
143     error(1, 0, "failed to extract %s: %s", name.c_str(), ErrorCodeString(err));
144   }
145   if (!android::base::WriteFully(1, buffer, entry.uncompressed_length)) {
146     error(1, errno, "failed to write %s to stdout", name.c_str());
147   }
148   delete[] buffer;
149 }
150 
ExtractOne(ZipArchiveHandle zah,ZipEntry & entry,const std::string & name)151 static void ExtractOne(ZipArchiveHandle zah, ZipEntry& entry, const std::string& name) {
152   // Bad filename?
153   if (android::base::StartsWith(name, "/") || android::base::StartsWith(name, "../") ||
154       name.find("/../") != std::string::npos) {
155     error(1, 0, "bad filename %s", name.c_str());
156   }
157 
158   // Where are we actually extracting to (for human-readable output)?
159   std::string dst;
160   if (flag_d) {
161     dst = flag_d;
162     if (!android::base::EndsWith(dst, "/")) dst += '/';
163   }
164   dst += name;
165 
166   // Ensure the directory hierarchy exists.
167   if (!MakeDirectoryHierarchy(android::base::Dirname(name))) {
168     error(1, errno, "couldn't create directory hierarchy for %s", dst.c_str());
169   }
170 
171   // An entry in a zip file can just be a directory itself.
172   if (android::base::EndsWith(name, "/")) {
173     if (mkdir(name.c_str(), entry.unix_mode) == -1) {
174       // If the directory already exists, that's fine.
175       if (errno == EEXIST) {
176         struct stat sb;
177         if (stat(name.c_str(), &sb) != -1 && S_ISDIR(sb.st_mode)) return;
178       }
179       error(1, errno, "couldn't extract directory %s", dst.c_str());
180     }
181     return;
182   }
183 
184   // Create the file.
185   int fd = open(name.c_str(), O_CREAT | O_WRONLY | O_CLOEXEC | O_EXCL, entry.unix_mode);
186   if (fd == -1 && errno == EEXIST) {
187     if (overwrite_mode == kNever) return;
188     if (overwrite_mode == kPrompt && !PromptOverwrite(dst)) return;
189     // Either overwrite_mode is kAlways or the user consented to this specific case.
190     fd = open(name.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC | O_TRUNC, entry.unix_mode);
191   }
192   if (fd == -1) error(1, errno, "couldn't create file %s", dst.c_str());
193 
194   // Actually extract into the file.
195   if (!flag_q) printf("  inflating: %s\n", dst.c_str());
196   int err = ExtractEntryToFile(zah, &entry, fd);
197   if (err < 0) error(1, 0, "failed to extract %s: %s", dst.c_str(), ErrorCodeString(err));
198   close(fd);
199 }
200 
ListOne(const ZipEntry & entry,const std::string & name)201 static void ListOne(const ZipEntry& entry, const std::string& name) {
202   tm t = entry.GetModificationTime();
203   char time[32];
204   snprintf(time, sizeof(time), "%04d-%02d-%02d %02d:%02d", t.tm_year + 1900, t.tm_mon + 1,
205            t.tm_mday, t.tm_hour, t.tm_min);
206   if (flag_v) {
207     printf("%8d  %s  %7d %3d%% %s %08x  %s\n", entry.uncompressed_length,
208            (entry.method == kCompressStored) ? "Stored" : "Defl:N", entry.compressed_length,
209            CompressionRatio(entry.uncompressed_length, entry.compressed_length), time, entry.crc32,
210            name.c_str());
211   } else {
212     printf("%9d  %s   %s\n", entry.uncompressed_length, time, name.c_str());
213   }
214 }
215 
ProcessOne(ZipArchiveHandle zah,ZipEntry & entry,const std::string & name)216 static void ProcessOne(ZipArchiveHandle zah, ZipEntry& entry, const std::string& name) {
217   if (flag_l || flag_v) {
218     // -l or -lv or -lq or -v.
219     ListOne(entry, name);
220   } else {
221     // Actually extract.
222     if (flag_p) {
223       ExtractToPipe(zah, entry, name);
224     } else {
225       ExtractOne(zah, entry, name);
226     }
227   }
228   total_uncompressed_length += entry.uncompressed_length;
229   total_compressed_length += entry.compressed_length;
230   ++file_count;
231 }
232 
ProcessAll(ZipArchiveHandle zah)233 static void ProcessAll(ZipArchiveHandle zah) {
234   MaybeShowHeader();
235 
236   // libziparchive iteration order doesn't match the central directory.
237   // We could sort, but that would cost extra and wouldn't match either.
238   void* cookie;
239   int err = StartIteration(zah, &cookie, nullptr, nullptr);
240   if (err != 0) {
241     error(1, 0, "couldn't iterate %s: %s", archive_name, ErrorCodeString(err));
242   }
243 
244   ZipEntry entry;
245   ZipString string;
246   while ((err = Next(cookie, &entry, &string)) >= 0) {
247     std::string name(string.name, string.name + string.name_length);
248     if (!Filter(name)) ProcessOne(zah, entry, name);
249   }
250 
251   if (err < -1) error(1, 0, "failed iterating %s: %s", archive_name, ErrorCodeString(err));
252   EndIteration(cookie);
253 
254   MaybeShowFooter();
255 }
256 
ShowHelp(bool full)257 static void ShowHelp(bool full) {
258   fprintf(full ? stdout : stderr, "usage: unzip [-d DIR] [-lnopqv] ZIP [FILE...] [-x FILE...]\n");
259   if (!full) exit(EXIT_FAILURE);
260 
261   printf(
262       "\n"
263       "Extract FILEs from ZIP archive. Default is all files.\n"
264       "\n"
265       "-d DIR	Extract into DIR\n"
266       "-l	List contents (-lq excludes archive name, -lv is verbose)\n"
267       "-n	Never overwrite files (default: prompt)\n"
268       "-o	Always overwrite files\n"
269       "-p	Pipe to stdout\n"
270       "-q	Quiet\n"
271       "-v	List contents verbosely\n"
272       "-x FILE	Exclude files\n");
273   exit(EXIT_SUCCESS);
274 }
275 
main(int argc,char * argv[])276 int main(int argc, char* argv[]) {
277   static struct option opts[] = {
278       {"help", no_argument, 0, 'h'},
279   };
280   bool saw_x = false;
281   int opt;
282   while ((opt = getopt_long(argc, argv, "-d:hlnopqvx", opts, nullptr)) != -1) {
283     switch (opt) {
284       case 'd':
285         flag_d = optarg;
286         break;
287       case 'h':
288         ShowHelp(true);
289         break;
290       case 'l':
291         flag_l = true;
292         break;
293       case 'n':
294         overwrite_mode = kNever;
295         break;
296       case 'o':
297         overwrite_mode = kAlways;
298         break;
299       case 'p':
300         flag_p = flag_q = true;
301         break;
302       case 'q':
303         flag_q = true;
304         break;
305       case 'v':
306         flag_v = true;
307         break;
308       case 'x':
309         saw_x = true;
310         break;
311       case 1:
312         // -x swallows all following arguments, so we use '-' in the getopt
313         // string and collect files here.
314         if (!archive_name) {
315           archive_name = optarg;
316         } else if (saw_x) {
317           excludes.insert(optarg);
318         } else {
319           includes.insert(optarg);
320         }
321         break;
322       default:
323         ShowHelp(false);
324     }
325   }
326 
327   if (!archive_name) error(1, 0, "missing archive filename");
328 
329   // We can't support "-" to unzip from stdin because libziparchive relies on mmap.
330   ZipArchiveHandle zah;
331   int32_t err;
332   if ((err = OpenArchive(archive_name, &zah)) != 0) {
333     error(1, 0, "couldn't open %s: %s", archive_name, ErrorCodeString(err));
334   }
335 
336   // Implement -d by changing into that directory.
337   // We'll create implicit directories based on paths in the zip file, but we
338   // require that the -d directory already exists.
339   if (flag_d && chdir(flag_d) == -1) error(1, errno, "couldn't chdir to %s", flag_d);
340 
341   ProcessAll(zah);
342 
343   CloseArchive(zah);
344   return 0;
345 }
346