blob: 6756007af3ba3c09ab6f4c31b670190bba485996 [file] [log] [blame]
Elliott Hughes55fd2932017-05-28 22:59:04 -07001/*
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
36enum OverwriteMode {
37 kAlways,
38 kNever,
39 kPrompt,
40};
41
42static OverwriteMode overwrite_mode = kPrompt;
43static const char* flag_d = nullptr;
44static bool flag_l = false;
45static bool flag_p = false;
46static bool flag_q = false;
47static bool flag_v = false;
48static const char* archive_name = nullptr;
49static std::set<std::string> includes;
50static std::set<std::string> excludes;
51static uint64_t total_uncompressed_length = 0;
52static uint64_t total_compressed_length = 0;
53static size_t file_count = 0;
54
55static 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
61static 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
73static int CompressionRatio(int64_t uncompressed, int64_t compressed) {
74 if (uncompressed == 0) return 0;
75 return (100LL * (uncompressed - compressed)) / uncompressed;
76}
77
78static 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
91static 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
107static 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
137static 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
151static 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
201static 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
216static 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
233static 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
257static 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
276int 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}