Home | History | Annotate | Download | only in aapt
      1 //
      2 // Copyright 2006 The Android Open Source Project
      3 //
      4 // Package assets into Zip files.
      5 //
      6 #include "Main.h"
      7 #include "AaptAssets.h"
      8 #include "ResourceTable.h"
      9 
     10 #include <utils/Log.h>
     11 #include <utils/threads.h>
     12 #include <utils/List.h>
     13 #include <utils/Errors.h>
     14 
     15 #include <sys/types.h>
     16 #include <dirent.h>
     17 #include <ctype.h>
     18 #include <errno.h>
     19 
     20 using namespace android;
     21 
     22 static const char* kExcludeExtension = ".EXCLUDE";
     23 
     24 /* these formats are already compressed, or don't compress well */
     25 static const char* kNoCompressExt[] = {
     26     ".jpg", ".jpeg", ".png", ".gif",
     27     ".wav", ".mp2", ".mp3", ".ogg", ".aac",
     28     ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
     29     ".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
     30     ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
     31     ".amr", ".awb", ".wma", ".wmv"
     32 };
     33 
     34 /* fwd decls, so I can write this downward */
     35 ssize_t processAssets(Bundle* bundle, ZipFile* zip, const sp<AaptAssets>& assets);
     36 ssize_t processAssets(Bundle* bundle, ZipFile* zip,
     37                         const sp<AaptDir>& dir, const AaptGroupEntry& ge);
     38 bool processFile(Bundle* bundle, ZipFile* zip,
     39                         const sp<AaptGroup>& group, const sp<AaptFile>& file);
     40 bool okayToCompress(Bundle* bundle, const String8& pathName);
     41 ssize_t processJarFiles(Bundle* bundle, ZipFile* zip);
     42 
     43 /*
     44  * The directory hierarchy looks like this:
     45  * "outputDir" and "assetRoot" are existing directories.
     46  *
     47  * On success, "bundle->numPackages" will be the number of Zip packages
     48  * we created.
     49  */
     50 status_t writeAPK(Bundle* bundle, const sp<AaptAssets>& assets,
     51                        const String8& outputFile)
     52 {
     53     status_t result = NO_ERROR;
     54     ZipFile* zip = NULL;
     55     int count;
     56 
     57     //bundle->setPackageCount(0);
     58 
     59     /*
     60      * Prep the Zip archive.
     61      *
     62      * If the file already exists, fail unless "update" or "force" is set.
     63      * If "update" is set, update the contents of the existing archive.
     64      * Else, if "force" is set, remove the existing archive.
     65      */
     66     FileType fileType = getFileType(outputFile.string());
     67     if (fileType == kFileTypeNonexistent) {
     68         // okay, create it below
     69     } else if (fileType == kFileTypeRegular) {
     70         if (bundle->getUpdate()) {
     71             // okay, open it below
     72         } else if (bundle->getForce()) {
     73             if (unlink(outputFile.string()) != 0) {
     74                 fprintf(stderr, "ERROR: unable to remove '%s': %s\n", outputFile.string(),
     75                         strerror(errno));
     76                 goto bail;
     77             }
     78         } else {
     79             fprintf(stderr, "ERROR: '%s' exists (use '-f' to force overwrite)\n",
     80                     outputFile.string());
     81             goto bail;
     82         }
     83     } else {
     84         fprintf(stderr, "ERROR: '%s' exists and is not a regular file\n", outputFile.string());
     85         goto bail;
     86     }
     87 
     88     if (bundle->getVerbose()) {
     89         printf("%s '%s'\n", (fileType == kFileTypeNonexistent) ? "Creating" : "Opening",
     90                 outputFile.string());
     91     }
     92 
     93     status_t status;
     94     zip = new ZipFile;
     95     status = zip->open(outputFile.string(), ZipFile::kOpenReadWrite | ZipFile::kOpenCreate);
     96     if (status != NO_ERROR) {
     97         fprintf(stderr, "ERROR: unable to open '%s' as Zip file for writing\n",
     98                 outputFile.string());
     99         goto bail;
    100     }
    101 
    102     if (bundle->getVerbose()) {
    103         printf("Writing all files...\n");
    104     }
    105 
    106     count = processAssets(bundle, zip, assets);
    107     if (count < 0) {
    108         fprintf(stderr, "ERROR: unable to process assets while packaging '%s'\n",
    109                 outputFile.string());
    110         result = count;
    111         goto bail;
    112     }
    113 
    114     if (bundle->getVerbose()) {
    115         printf("Generated %d file%s\n", count, (count==1) ? "" : "s");
    116     }
    117 
    118     count = processJarFiles(bundle, zip);
    119     if (count < 0) {
    120         fprintf(stderr, "ERROR: unable to process jar files while packaging '%s'\n",
    121                 outputFile.string());
    122         result = count;
    123         goto bail;
    124     }
    125 
    126     if (bundle->getVerbose())
    127         printf("Included %d file%s from jar/zip files.\n", count, (count==1) ? "" : "s");
    128 
    129     result = NO_ERROR;
    130 
    131     /*
    132      * Check for cruft.  We set the "marked" flag on all entries we created
    133      * or decided not to update.  If the entry isn't already slated for
    134      * deletion, remove it now.
    135      */
    136     {
    137         if (bundle->getVerbose())
    138             printf("Checking for deleted files\n");
    139         int i, removed = 0;
    140         for (i = 0; i < zip->getNumEntries(); i++) {
    141             ZipEntry* entry = zip->getEntryByIndex(i);
    142 
    143             if (!entry->getMarked() && entry->getDeleted()) {
    144                 if (bundle->getVerbose()) {
    145                     printf("      (removing crufty '%s')\n",
    146                         entry->getFileName());
    147                 }
    148                 zip->remove(entry);
    149                 removed++;
    150             }
    151         }
    152         if (bundle->getVerbose() && removed > 0)
    153             printf("Removed %d file%s\n", removed, (removed==1) ? "" : "s");
    154     }
    155 
    156     /* tell Zip lib to process deletions and other pending changes */
    157     result = zip->flush();
    158     if (result != NO_ERROR) {
    159         fprintf(stderr, "ERROR: Zip flush failed, archive may be hosed\n");
    160         goto bail;
    161     }
    162 
    163     /* anything here? */
    164     if (zip->getNumEntries() == 0) {
    165         if (bundle->getVerbose()) {
    166             printf("Archive is empty -- removing %s\n", outputFile.getPathLeaf().string());
    167         }
    168         delete zip;        // close the file so we can remove it in Win32
    169         zip = NULL;
    170         if (unlink(outputFile.string()) != 0) {
    171             fprintf(stderr, "warning: could not unlink '%s'\n", outputFile.string());
    172         }
    173     }
    174 
    175     assert(result == NO_ERROR);
    176 
    177 bail:
    178     delete zip;        // must close before remove in Win32
    179     if (result != NO_ERROR) {
    180         if (bundle->getVerbose()) {
    181             printf("Removing %s due to earlier failures\n", outputFile.string());
    182         }
    183         if (unlink(outputFile.string()) != 0) {
    184             fprintf(stderr, "warning: could not unlink '%s'\n", outputFile.string());
    185         }
    186     }
    187 
    188     if (result == NO_ERROR && bundle->getVerbose())
    189         printf("Done!\n");
    190     return result;
    191 }
    192 
    193 ssize_t processAssets(Bundle* bundle, ZipFile* zip,
    194                       const sp<AaptAssets>& assets)
    195 {
    196     ResourceFilter filter;
    197     status_t status = filter.parse(bundle->getConfigurations());
    198     if (status != NO_ERROR) {
    199         return -1;
    200     }
    201 
    202     ssize_t count = 0;
    203 
    204     const size_t N = assets->getGroupEntries().size();
    205     for (size_t i=0; i<N; i++) {
    206         const AaptGroupEntry& ge = assets->getGroupEntries()[i];
    207         if (!filter.match(ge.toParams())) {
    208             continue;
    209         }
    210         ssize_t res = processAssets(bundle, zip, assets, ge);
    211         if (res < 0) {
    212             return res;
    213         }
    214         count += res;
    215     }
    216 
    217     return count;
    218 }
    219 
    220 ssize_t processAssets(Bundle* bundle, ZipFile* zip,
    221                       const sp<AaptDir>& dir, const AaptGroupEntry& ge)
    222 {
    223     ssize_t count = 0;
    224 
    225     const size_t ND = dir->getDirs().size();
    226     size_t i;
    227     for (i=0; i<ND; i++) {
    228         ssize_t res = processAssets(bundle, zip, dir->getDirs().valueAt(i), ge);
    229         if (res < 0) {
    230             return res;
    231         }
    232         count += res;
    233     }
    234 
    235     const size_t NF = dir->getFiles().size();
    236     for (i=0; i<NF; i++) {
    237         sp<AaptGroup> gp = dir->getFiles().valueAt(i);
    238         ssize_t fi = gp->getFiles().indexOfKey(ge);
    239         if (fi >= 0) {
    240             sp<AaptFile> fl = gp->getFiles().valueAt(fi);
    241             if (!processFile(bundle, zip, gp, fl)) {
    242                 return UNKNOWN_ERROR;
    243             }
    244             count++;
    245         }
    246     }
    247 
    248     return count;
    249 }
    250 
    251 /*
    252  * Process a regular file, adding it to the archive if appropriate.
    253  *
    254  * If we're in "update" mode, and the file already exists in the archive,
    255  * delete the existing entry before adding the new one.
    256  */
    257 bool processFile(Bundle* bundle, ZipFile* zip,
    258                  const sp<AaptGroup>& group, const sp<AaptFile>& file)
    259 {
    260     const bool hasData = file->hasData();
    261 
    262     String8 storageName(group->getPath());
    263     storageName.convertToResPath();
    264     ZipEntry* entry;
    265     bool fromGzip = false;
    266     status_t result;
    267 
    268     /*
    269      * See if the filename ends in ".EXCLUDE".  We can't use
    270      * String8::getPathExtension() because the length of what it considers
    271      * to be an extension is capped.
    272      *
    273      * The Asset Manager doesn't check for ".EXCLUDE" in Zip archives,
    274      * so there's no value in adding them (and it makes life easier on
    275      * the AssetManager lib if we don't).
    276      *
    277      * NOTE: this restriction has been removed.  If you're in this code, you
    278      * should clean this up, but I'm in here getting rid of Path Name, and I
    279      * don't want to make other potentially breaking changes --joeo
    280      */
    281     int fileNameLen = storageName.length();
    282     int excludeExtensionLen = strlen(kExcludeExtension);
    283     if (fileNameLen > excludeExtensionLen
    284             && (0 == strcmp(storageName.string() + (fileNameLen - excludeExtensionLen),
    285                             kExcludeExtension))) {
    286         fprintf(stderr, "warning: '%s' not added to Zip\n", storageName.string());
    287         return true;
    288     }
    289 
    290     if (strcasecmp(storageName.getPathExtension().string(), ".gz") == 0) {
    291         fromGzip = true;
    292         storageName = storageName.getBasePath();
    293     }
    294 
    295     if (bundle->getUpdate()) {
    296         entry = zip->getEntryByName(storageName.string());
    297         if (entry != NULL) {
    298             /* file already exists in archive; there can be only one */
    299             if (entry->getMarked()) {
    300                 fprintf(stderr,
    301                         "ERROR: '%s' exists twice (check for with & w/o '.gz'?)\n",
    302                         file->getPrintableSource().string());
    303                 return false;
    304             }
    305             if (!hasData) {
    306                 const String8& srcName = file->getSourceFile();
    307                 time_t fileModWhen;
    308                 fileModWhen = getFileModDate(srcName.string());
    309                 if (fileModWhen == (time_t) -1) { // file existence tested earlier,
    310                     return false;                 //  not expecting an error here
    311                 }
    312 
    313                 if (fileModWhen > entry->getModWhen()) {
    314                     // mark as deleted so add() will succeed
    315                     if (bundle->getVerbose()) {
    316                         printf("      (removing old '%s')\n", storageName.string());
    317                     }
    318 
    319                     zip->remove(entry);
    320                 } else {
    321                     // version in archive is newer
    322                     if (bundle->getVerbose()) {
    323                         printf("      (not updating '%s')\n", storageName.string());
    324                     }
    325                     entry->setMarked(true);
    326                     return true;
    327                 }
    328             } else {
    329                 // Generated files are always replaced.
    330                 zip->remove(entry);
    331             }
    332         }
    333     }
    334 
    335     //android_setMinPriority(NULL, ANDROID_LOG_VERBOSE);
    336 
    337     if (fromGzip) {
    338         result = zip->addGzip(file->getSourceFile().string(), storageName.string(), &entry);
    339     } else if (!hasData) {
    340         /* don't compress certain files, e.g. PNGs */
    341         int compressionMethod = bundle->getCompressionMethod();
    342         if (!okayToCompress(bundle, storageName)) {
    343             compressionMethod = ZipEntry::kCompressStored;
    344         }
    345         result = zip->add(file->getSourceFile().string(), storageName.string(), compressionMethod,
    346                             &entry);
    347     } else {
    348         result = zip->add(file->getData(), file->getSize(), storageName.string(),
    349                            file->getCompressionMethod(), &entry);
    350     }
    351     if (result == NO_ERROR) {
    352         if (bundle->getVerbose()) {
    353             printf("      '%s'%s", storageName.string(), fromGzip ? " (from .gz)" : "");
    354             if (entry->getCompressionMethod() == ZipEntry::kCompressStored) {
    355                 printf(" (not compressed)\n");
    356             } else {
    357                 printf(" (compressed %d%%)\n", calcPercent(entry->getUncompressedLen(),
    358                             entry->getCompressedLen()));
    359             }
    360         }
    361         entry->setMarked(true);
    362     } else {
    363         if (result == ALREADY_EXISTS) {
    364             fprintf(stderr, "      Unable to add '%s': file already in archive (try '-u'?)\n",
    365                     file->getPrintableSource().string());
    366         } else {
    367             fprintf(stderr, "      Unable to add '%s': Zip add failed\n",
    368                     file->getPrintableSource().string());
    369         }
    370         return false;
    371     }
    372 
    373     return true;
    374 }
    375 
    376 /*
    377  * Determine whether or not we want to try to compress this file based
    378  * on the file extension.
    379  */
    380 bool okayToCompress(Bundle* bundle, const String8& pathName)
    381 {
    382     String8 ext = pathName.getPathExtension();
    383     int i;
    384 
    385     if (ext.length() == 0)
    386         return true;
    387 
    388     for (i = 0; i < NELEM(kNoCompressExt); i++) {
    389         if (strcasecmp(ext.string(), kNoCompressExt[i]) == 0)
    390             return false;
    391     }
    392 
    393     const android::Vector<const char*>& others(bundle->getNoCompressExtensions());
    394     for (i = 0; i < (int)others.size(); i++) {
    395         const char* str = others[i];
    396         int pos = pathName.length() - strlen(str);
    397         if (pos < 0) {
    398             continue;
    399         }
    400         const char* path = pathName.string();
    401         if (strcasecmp(path + pos, str) == 0) {
    402             return false;
    403         }
    404     }
    405 
    406     return true;
    407 }
    408 
    409 bool endsWith(const char* haystack, const char* needle)
    410 {
    411     size_t a = strlen(haystack);
    412     size_t b = strlen(needle);
    413     if (a < b) return false;
    414     return strcasecmp(haystack+(a-b), needle) == 0;
    415 }
    416 
    417 ssize_t processJarFile(ZipFile* jar, ZipFile* out)
    418 {
    419     status_t err;
    420     size_t N = jar->getNumEntries();
    421     size_t count = 0;
    422     for (size_t i=0; i<N; i++) {
    423         ZipEntry* entry = jar->getEntryByIndex(i);
    424         const char* storageName = entry->getFileName();
    425         if (endsWith(storageName, ".class")) {
    426             int compressionMethod = entry->getCompressionMethod();
    427             size_t size = entry->getUncompressedLen();
    428             const void* data = jar->uncompress(entry);
    429             if (data == NULL) {
    430                 fprintf(stderr, "ERROR: unable to uncompress entry '%s'\n",
    431                     storageName);
    432                 return -1;
    433             }
    434             out->add(data, size, storageName, compressionMethod, NULL);
    435             free((void*)data);
    436         }
    437         count++;
    438     }
    439     return count;
    440 }
    441 
    442 ssize_t processJarFiles(Bundle* bundle, ZipFile* zip)
    443 {
    444     ssize_t err;
    445     ssize_t count = 0;
    446     const android::Vector<const char*>& jars = bundle->getJarFiles();
    447 
    448     size_t N = jars.size();
    449     for (size_t i=0; i<N; i++) {
    450         ZipFile jar;
    451         err = jar.open(jars[i], ZipFile::kOpenReadOnly);
    452         if (err != 0) {
    453             fprintf(stderr, "ERROR: unable to open '%s' as a zip file: %zd\n",
    454                 jars[i], err);
    455             return err;
    456         }
    457         err += processJarFile(&jar, zip);
    458         if (err < 0) {
    459             fprintf(stderr, "ERROR: unable to process '%s'\n", jars[i]);
    460             return err;
    461         }
    462         count += err;
    463     }
    464 
    465     return count;
    466 }
    467