1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// rezip is a tool which is used to modify zip files. It reads in a
6// zip file and outputs a new zip file after applying various
7// transforms. The tool is used in the Android Chromium build process
8// to modify an APK file (which are zip files). The main application
9// of this is to modify the APK so that the shared library is no
10// longer compressed. Ironically, this saves both transmission and
11// device drive space. It saves transmission space because
12// uncompressed libraries make much smaller deltas with previous
13// versions. It saves device drive space because it is no longer
14// necessary to have both a compressed and uncompressed shared library
15// on the device. To achieve this the uncompressed library is opened
16// directly from within the APK using the "crazy" linker.
17
18#include <assert.h>
19#include <string.h>
20
21#include <iostream>
22#include <sstream>
23#include <string>
24
25#include "third_party/zlib/contrib/minizip/unzip.h"
26#include "third_party/zlib/contrib/minizip/zip.h"
27
28const int kMaxFilenameInZip = 256;
29const int kMaxExtraFieldInZip = 8192;
30const int kBufferSize = 4096;
31// Note do not use sysconf(_SC_PAGESIZE) here as that will give you the
32// page size of the host, this should be the page size of the target.
33const int kPageSizeOnDevice = 4096;
34
35// This is done to avoid having to make a dependency on all of base.
36class LogStream {
37 public:
38  ~LogStream() {
39    stream_.flush();
40    std::cerr << stream_.str() << std::endl;
41  }
42  std::ostream& stream() {
43    return stream_;
44  }
45 private:
46  std::ostringstream stream_;
47};
48
49#define LOG(tag) (LogStream().stream() << #tag << ":")
50
51// Copy the data from the currently opened file in the zipfile we are unzipping
52// into the currently opened file of the zipfile we are zipping.
53static bool CopySubfile(unzFile in_file,
54                        zipFile out_file,
55                        const char* in_zip_filename,
56                        const char* out_zip_filename,
57                        const char* in_filename,
58                        const char* out_filename) {
59  char buf[kBufferSize];
60
61  int bytes = 0;
62  while (true) {
63    bytes = unzReadCurrentFile(in_file, buf, sizeof(buf));
64    if (bytes < 0) {
65      LOG(ERROR) << "failed to read from " << in_filename << " in zipfile "
66                 << in_zip_filename;
67      return false;
68    }
69
70    if (bytes == 0) {
71      break;
72    }
73
74    if (ZIP_OK != zipWriteInFileInZip(out_file, buf, bytes)) {
75      LOG(ERROR) << "failed to write from " << out_filename << " in zipfile "
76                 << out_zip_filename;
77      return false;
78    }
79  }
80
81  return true;
82}
83
84static zip_fileinfo BuildOutInfo(const unz_file_info& in_info) {
85  zip_fileinfo out_info;
86  out_info.tmz_date.tm_sec = in_info.tmu_date.tm_sec;
87  out_info.tmz_date.tm_min = in_info.tmu_date.tm_min;
88  out_info.tmz_date.tm_hour = in_info.tmu_date.tm_hour;
89  out_info.tmz_date.tm_mday = in_info.tmu_date.tm_mday;
90  out_info.tmz_date.tm_mon = in_info.tmu_date.tm_mon;
91  out_info.tmz_date.tm_year = in_info.tmu_date.tm_year;
92
93  out_info.dosDate = in_info.dosDate;
94  out_info.internal_fa = in_info.internal_fa;
95  out_info.external_fa = in_info.external_fa;
96  return out_info;
97}
98
99// RAII pattern for closing the unzip file.
100class ScopedUnzip {
101 public:
102  ScopedUnzip(const char* z_filename)
103      : z_file_(NULL), z_filename_(z_filename) {}
104
105  unzFile OpenOrDie() {
106    z_file_ = unzOpen(z_filename_);
107    if (z_file_ == NULL) {
108      LOG(ERROR) << "failed to open zipfile " << z_filename_;
109      exit(1);
110    }
111    return z_file_;
112  }
113
114  ~ScopedUnzip() {
115    if (z_file_ != NULL && unzClose(z_file_) != UNZ_OK) {
116      LOG(ERROR) << "failed to close input zipfile " << z_filename_;
117      exit(1);
118    }
119  }
120
121 private:
122  const char* z_filename_;
123  unzFile z_file_;
124};
125
126// RAII pattern for closing the out zip file.
127class ScopedZip {
128 public:
129  ScopedZip(const char* z_filename)
130      : z_file_(NULL), z_filename_(z_filename) {}
131
132  zipFile OpenOrDie() {
133    z_file_ = zipOpen(z_filename_, APPEND_STATUS_CREATE);
134    if (z_file_ == NULL) {
135      LOG(ERROR) << "failed to open zipfile " << z_filename_;
136      exit(1);
137    }
138    return z_file_;
139  }
140
141  ~ScopedZip() {
142    if (z_file_ != NULL && zipClose(z_file_, NULL) != ZIP_OK) {
143      LOG(ERROR) << "failed to close output zipfile" << z_filename_;
144      exit(1);
145    }
146  }
147
148 private:
149  const char* z_filename_;
150  zipFile z_file_;
151};
152
153typedef std::string (*RenameFun)(const char* in_filename);
154typedef int (*AlignFun)(const char* in_filename,
155                        unzFile in_file,
156                        char* extra_buffer,
157                        int size);
158typedef bool (*InflatePredicateFun)(const char* filename);
159
160static bool IsPrefixLibraryFilename(const char* filename,
161                                    const char* base_prefix) {
162  // We are basically matching "lib/[^/]*/<base_prefix>lib.*[.]so".
163  // However, we don't have C++11 regex, so we just handroll the test.
164  // Also we exclude "libchromium_android_linker.so" as a match.
165  const std::string filename_str = filename;
166  const std::string prefix = "lib/";
167  const std::string suffix = ".so";
168
169  if (filename_str.length() < suffix.length() + prefix.length()) {
170    // too short
171    return false;
172  }
173
174  if (filename_str.compare(0, prefix.size(), prefix) != 0) {
175    // does not start with "lib/"
176    return false;
177  }
178
179  if (filename_str.compare(filename_str.length() - suffix.length(),
180                           suffix.length(),
181                           suffix) != 0) {
182    // does not end with ".so"
183    return false;
184  }
185
186  const size_t last_slash = filename_str.find_last_of('/');
187  if (last_slash < prefix.length()) {
188    // Only one slash
189    return false;
190  }
191
192  const size_t second_slash = filename_str.find_first_of('/', prefix.length());
193  if (second_slash != last_slash) {
194    // filename_str contains more than two slashes.
195    return false;
196  }
197
198  const std::string libprefix = std::string(base_prefix) + "lib";
199  if (filename_str.compare(last_slash + 1, libprefix.length(), libprefix) !=
200      0) {
201    // basename piece does not start with <base_prefix>"lib"
202    return false;
203  }
204
205  const std::string linker = "libchromium_android_linker.so";
206  if (last_slash + 1 + linker.length() == filename_str.length() &&
207      filename_str.compare(last_slash + 1, linker.length(), linker) == 0) {
208    // Do not match the linker.
209    return false;
210  }
211  return true;
212}
213
214static bool IsLibraryFilename(const char* filename) {
215  return IsPrefixLibraryFilename(filename, "");
216}
217
218static bool IsCrazyLibraryFilename(const char* filename) {
219  return IsPrefixLibraryFilename(filename, "crazy.");
220}
221
222static std::string RenameLibraryForCrazyLinker(const char* in_filename) {
223  if (!IsLibraryFilename(in_filename)) {
224    // Don't rename
225    return in_filename;
226  }
227
228  std::string filename_str = in_filename;
229  size_t last_slash = filename_str.find_last_of('/');
230  if (last_slash == std::string::npos ||
231      last_slash == filename_str.length() - 1) {
232    return in_filename;
233  }
234
235  // We rename the library, so that the Android Package Manager
236  // no longer extracts the library.
237  const std::string basename_prefix = "crazy.";
238  return filename_str.substr(0, last_slash + 1) + basename_prefix +
239         filename_str.substr(last_slash + 1);
240}
241
242// For any file which matches the crazy library pattern "lib/../crazy.lib*.so"
243// add sufficient padding to the header that the start of the file will be
244// page aligned on the target device.
245static int PageAlignCrazyLibrary(const char* in_filename,
246                                 unzFile in_file,
247                                 char* extra_buffer,
248                                 int extra_size) {
249  if (!IsCrazyLibraryFilename(in_filename)) {
250    return extra_size;
251  }
252  const ZPOS64_T pos = unzGetCurrentFileZStreamPos64(in_file);
253  const int padding = kPageSizeOnDevice - (pos % kPageSizeOnDevice);
254  if (padding == kPageSizeOnDevice) {
255    return extra_size;
256  }
257
258  assert(extra_size < kMaxExtraFieldInZip - padding);
259  memset(extra_buffer + extra_size, 0, padding);
260  return extra_size + padding;
261}
262
263// As only the read side API provides offsets, we check that we added the
264// correct amount of padding by reading the zip file we just generated.
265// Also enforce that only one library is in the APK.
266static bool CheckPageAlignAndOnlyOneLibrary(const char* out_zip_filename) {
267  ScopedUnzip scoped_unzip(out_zip_filename);
268  unzFile in_file = scoped_unzip.OpenOrDie();
269
270  int err = 0;
271  int count = 0;
272  bool checked = false;
273  while (true) {
274    char in_filename[kMaxFilenameInZip + 1];
275    // Get info and extra field for current file.
276    unz_file_info in_info;
277    err = unzGetCurrentFileInfo(in_file,
278                                &in_info,
279                                in_filename,
280                                sizeof(in_filename) - 1,
281                                NULL,
282                                0,
283                                NULL,
284                                0);
285    if (err != UNZ_OK) {
286      LOG(ERROR) << "failed to get filename" << out_zip_filename;
287      return false;
288    }
289    assert(in_info.size_filename <= kMaxFilenameInZip);
290    in_filename[in_info.size_filename] = '\0';
291
292    if (IsCrazyLibraryFilename(in_filename)) {
293      count++;
294      if (count > 1) {
295        LOG(ERROR)
296            << "Found more than one library in " << out_zip_filename << "\n"
297            << "Multiple libraries are not supported for APKs that use "
298            << "'load_library_from_zip_file'.\n"
299            << "See crbug/388223.\n"
300            << "Note, check that your build is clean.\n"
301            << "An unclean build can incorrectly incorporate old "
302            << "libraries in the APK.";
303        return false;
304      }
305      err = unzOpenCurrentFile(in_file);
306      if (err != UNZ_OK) {
307        LOG(ERROR) << "failed to open subfile" << out_zip_filename << " "
308                   << in_filename;
309        return false;
310      }
311
312      const ZPOS64_T pos = unzGetCurrentFileZStreamPos64(in_file);
313      const int alignment = pos % kPageSizeOnDevice;
314      checked = (alignment == 0);
315      if (!checked) {
316        LOG(ERROR) << "Failed to page align library " << in_filename
317                   << ", position " << pos << " alignment " << alignment;
318      }
319
320      err = unzCloseCurrentFile(in_file);
321      if (err != UNZ_OK) {
322        LOG(ERROR) << "failed to close subfile" << out_zip_filename << " "
323                   << in_filename;
324        return false;
325      }
326    }
327
328    const int next = unzGoToNextFile(in_file);
329    if (next == UNZ_END_OF_LIST_OF_FILE) {
330      break;
331    }
332    if (next != UNZ_OK) {
333      LOG(ERROR) << "failed to go to next file" << out_zip_filename;
334      return false;
335    }
336  }
337  return checked;
338}
339
340// Copy files from one archive to another applying alignment, rename and
341// inflate transformations if given.
342static bool Rezip(const char* in_zip_filename,
343                  const char* out_zip_filename,
344                  AlignFun align_fun,
345                  RenameFun rename_fun,
346                  InflatePredicateFun inflate_predicate_fun) {
347  ScopedUnzip scoped_unzip(in_zip_filename);
348  unzFile in_file = scoped_unzip.OpenOrDie();
349
350  ScopedZip scoped_zip(out_zip_filename);
351  zipFile out_file = scoped_zip.OpenOrDie();
352  if (unzGoToFirstFile(in_file) != UNZ_OK) {
353    LOG(ERROR) << "failed to go to first file in " << in_zip_filename;
354    return false;
355  }
356
357  int err = 0;
358  while (true) {
359    char in_filename[kMaxFilenameInZip + 1];
360    // Get info and extra field for current file.
361    char extra_buffer[kMaxExtraFieldInZip];
362    unz_file_info in_info;
363    err = unzGetCurrentFileInfo(in_file,
364                                &in_info,
365                                in_filename,
366                                sizeof(in_filename) - 1,
367                                &extra_buffer,
368                                sizeof(extra_buffer),
369                                NULL,
370                                0);
371    if (err != UNZ_OK) {
372      LOG(ERROR) << "failed to get filename " << in_zip_filename;
373      return false;
374    }
375    assert(in_info.size_filename <= kMaxFilenameInZip);
376    in_filename[in_info.size_filename] = '\0';
377
378    std::string out_filename = in_filename;
379    if (rename_fun != NULL) {
380      out_filename = rename_fun(in_filename);
381    }
382
383    bool inflate = false;
384    if (inflate_predicate_fun != NULL) {
385      inflate = inflate_predicate_fun(in_filename);
386    }
387
388    // Open the current file.
389    int method = 0;
390    int level = 0;
391    int raw = !inflate;
392    err = unzOpenCurrentFile2(in_file, &method, &level, raw);
393    if (inflate) {
394      method = Z_NO_COMPRESSION;
395      level = 0;
396    }
397
398    if (err != UNZ_OK) {
399      LOG(ERROR) << "failed to open subfile " << in_zip_filename << " "
400                 << in_filename;
401      return false;
402    }
403
404    // Get the extra field from the local header.
405    char local_extra_buffer[kMaxExtraFieldInZip];
406    int local_extra_size = unzGetLocalExtrafield(
407        in_file, &local_extra_buffer, sizeof(local_extra_buffer));
408
409    if (align_fun != NULL) {
410      local_extra_size =
411          align_fun(in_filename, in_file, local_extra_buffer, local_extra_size);
412    }
413
414    const char* local_extra = local_extra_size > 0 ? local_extra_buffer : NULL;
415    const char* extra = in_info.size_file_extra > 0 ? extra_buffer : NULL;
416
417    // Build the output info structure from the input info structure.
418    const zip_fileinfo out_info = BuildOutInfo(in_info);
419
420    const int ret = zipOpenNewFileInZip4(out_file,
421                                         out_filename.c_str(),
422                                         &out_info,
423                                         local_extra,
424                                         local_extra_size,
425                                         extra,
426                                         in_info.size_file_extra,
427                                         /* comment */ NULL,
428                                         method,
429                                         level,
430                                         /* raw */ 1,
431                                         /* windowBits */ 0,
432                                         /* memLevel */ 0,
433                                         /* strategy */ 0,
434                                         /* password */ NULL,
435                                         /* crcForCrypting */ 0,
436                                         in_info.version,
437                                         /* flagBase */ 0);
438
439    if (ZIP_OK != ret) {
440      LOG(ERROR) << "failed to open subfile " << out_zip_filename << " "
441                 << out_filename;
442      return false;
443    }
444
445    if (!CopySubfile(in_file,
446                     out_file,
447                     in_zip_filename,
448                     out_zip_filename,
449                     in_filename,
450                     out_filename.c_str())) {
451      return false;
452    }
453
454    if (ZIP_OK != zipCloseFileInZipRaw(
455                      out_file, in_info.uncompressed_size, in_info.crc)) {
456      LOG(ERROR) << "failed to close subfile " << out_zip_filename << " "
457                 << out_filename;
458      return false;
459    }
460
461    err = unzCloseCurrentFile(in_file);
462    if (err != UNZ_OK) {
463      LOG(ERROR) << "failed to close subfile " << in_zip_filename << " "
464                 << in_filename;
465      return false;
466    }
467    const int next = unzGoToNextFile(in_file);
468    if (next == UNZ_END_OF_LIST_OF_FILE) {
469      break;
470    }
471    if (next != UNZ_OK) {
472      LOG(ERROR) << "failed to go to next file" << in_zip_filename;
473      return false;
474    }
475  }
476
477  return true;
478}
479
480int main(int argc, const char* argv[]) {
481  if (argc != 4) {
482    LOG(ERROR) << "Usage: <action> <in_zipfile> <out_zipfile>";
483    LOG(ERROR) << " <action> is 'inflatealign', 'dropdescriptors' or 'rename'";
484    LOG(ERROR) << " 'inflatealign'";
485    LOG(ERROR) << "   inflate and page aligns files of the form "
486        "lib/*/crazy.lib*.so";
487    LOG(ERROR) << " 'dropdescriptors':";
488    LOG(ERROR) << "   remove zip data descriptors from the zip file";
489    LOG(ERROR) << " 'rename':";
490    LOG(ERROR) << "   renames files of the form lib/*/lib*.so to "
491        "lib/*/crazy.lib*.so. Note libchromium_android_linker.so is "
492        "not renamed as the crazy linker can not load itself.";
493    exit(1);
494  }
495
496  const char* action = argv[1];
497  const char* in_zip_filename = argv[2];
498  const char* out_zip_filename = argv[3];
499
500  InflatePredicateFun inflate_predicate_fun = NULL;
501  AlignFun align_fun = NULL;
502  RenameFun rename_fun = NULL;
503  bool check_page_align = false;
504  if (strcmp("inflatealign", action) == 0) {
505    inflate_predicate_fun = &IsCrazyLibraryFilename;
506    align_fun = &PageAlignCrazyLibrary;
507    check_page_align = true;
508  } else if (strcmp("rename", action) == 0) {
509    rename_fun = &RenameLibraryForCrazyLinker;
510  } else if (strcmp("dropdescriptors", action) == 0) {
511    // Minizip does not know about data descriptors, so the default
512    // copying action will drop the descriptors. This should be fine
513    // as data descriptors are redundant information.
514    // Note we need to explicitly drop the descriptors before trying to
515    // do alignment otherwise we will miscalculate the position because
516    // we don't know about the data descriptors.
517  } else {
518    LOG(ERROR) << "Usage: <action> should be 'inflatealign', "
519                  "'dropdescriptors' or 'rename'";
520    exit(1);
521  }
522
523  if (!Rezip(in_zip_filename,
524             out_zip_filename,
525             align_fun,
526             rename_fun,
527             inflate_predicate_fun)) {
528    exit(1);
529  }
530  if (check_page_align && !CheckPageAlignAndOnlyOneLibrary(out_zip_filename)) {
531    exit(1);
532  }
533  return 0;
534}
535