diff options
Diffstat (limited to 'tools')
269 files changed, 21275 insertions, 14182 deletions
diff --git a/tools/aapt/AaptAssets.cpp b/tools/aapt/AaptAssets.cpp index d346731e63e2..3b01827e13d8 100644 --- a/tools/aapt/AaptAssets.cpp +++ b/tools/aapt/AaptAssets.cpp @@ -235,7 +235,7 @@ bool AaptLocaleValue::initFromFilterString(const String8& str) { setRegion(part2.string()); } else if (part2.length() == 4 && isAlpha(part2)) { setScript(part2.string()); - } else if (part2.length() >= 5 && part2.length() <= 8) { + } else if (part2.length() >= 4 && part2.length() <= 8) { setVariant(part2.string()); } else { valid = false; @@ -250,7 +250,7 @@ bool AaptLocaleValue::initFromFilterString(const String8& str) { if (((part3.length() == 2 && isAlpha(part3)) || (part3.length() == 3 && isNumber(part3))) && script[0]) { setRegion(part3.string()); - } else if (part3.length() >= 5 && part3.length() <= 8) { + } else if (part3.length() >= 4 && part3.length() <= 8) { setVariant(part3.string()); } else { valid = false; @@ -261,7 +261,7 @@ bool AaptLocaleValue::initFromFilterString(const String8& str) { } const String8& part4 = parts[3]; - if (part4.length() >= 5 && part4.length() <= 8) { + if (part4.length() >= 4 && part4.length() <= 8) { setVariant(part4.string()); } else { valid = false; @@ -280,7 +280,7 @@ int AaptLocaleValue::initFromDirName(const Vector<String8>& parts, const int sta String8 part = parts[currentIndex]; if (part[0] == 'b' && part[1] == '+') { - // This is a "modified" BCP-47 language tag. Same semantics as BCP-47 tags, + // This is a "modified" BCP 47 language tag. Same semantics as BCP 47 tags, // except that the separator is "+" and not "-". Vector<String8> subtags = AaptUtil::splitAndLowerCase(part, '+'); subtags.removeItemsAt(0); @@ -296,8 +296,11 @@ int AaptLocaleValue::initFromDirName(const Vector<String8>& parts, const int sta setRegion(subtags[1]); break; case 4: - setScript(subtags[1]); - break; + if (isAlpha(subtags[1])) { + setScript(subtags[1]); + break; + } + // This is not alphabetical, so we fall through to variant case 5: case 6: case 7: @@ -305,7 +308,7 @@ int AaptLocaleValue::initFromDirName(const Vector<String8>& parts, const int sta setVariant(subtags[1]); break; default: - fprintf(stderr, "ERROR: Invalid BCP-47 tag in directory name %s\n", + fprintf(stderr, "ERROR: Invalid BCP 47 tag in directory name %s\n", part.string()); return -1; } @@ -322,13 +325,13 @@ int AaptLocaleValue::initFromDirName(const Vector<String8>& parts, const int sta setRegion(subtags[1]); hasRegion = true; } else { - fprintf(stderr, "ERROR: Invalid BCP-47 tag in directory name %s\n", part.string()); + fprintf(stderr, "ERROR: Invalid BCP 47 tag in directory name %s\n", part.string()); return -1; } // The third tag can either be a region code (if the second tag was // a script), else a variant code. - if (subtags[2].size() > 4) { + if (subtags[2].size() >= 4) { setVariant(subtags[2]); } else { setRegion(subtags[2]); @@ -339,7 +342,7 @@ int AaptLocaleValue::initFromDirName(const Vector<String8>& parts, const int sta setRegion(subtags[2]); setVariant(subtags[3]); } else { - fprintf(stderr, "ERROR: Invalid BCP-47 tag in directory name: %s\n", part.string()); + fprintf(stderr, "ERROR: Invalid BCP 47 tag in directory name: %s\n", part.string()); return -1; } @@ -370,7 +373,7 @@ int AaptLocaleValue::initFromDirName(const Vector<String8>& parts, const int sta void AaptLocaleValue::initFromResTable(const ResTable_config& config) { config.unpackLanguage(language); config.unpackRegion(region); - if (config.localeScript[0]) { + if (config.localeScriptWasProvided) { memcpy(script, config.localeScript, sizeof(config.localeScript)); } @@ -385,6 +388,10 @@ void AaptLocaleValue::writeTo(ResTable_config* out) const { if (script[0]) { memcpy(out->localeScript, script, sizeof(out->localeScript)); + out->localeScriptWasProvided = true; + } else { + out->computeScript(); + out->localeScriptWasProvided = false; } if (variant[0]) { diff --git a/tools/aapt/Bundle.h b/tools/aapt/Bundle.h index cbe7c5dacc1e..c4495509614e 100644 --- a/tools/aapt/Bundle.h +++ b/tools/aapt/Bundle.h @@ -66,6 +66,7 @@ public: mErrorOnMissingConfigEntry(false), mOutputTextSymbols(NULL), mSingleCrunchInputFile(NULL), mSingleCrunchOutputFile(NULL), mBuildSharedLibrary(false), + mBuildAppAsSharedLibrary(false), mArgc(0), mArgv(NULL) {} ~Bundle(void) {} @@ -126,6 +127,12 @@ public: const android::String8& getPlatformBuildVersionName() { return mPlatformVersionName; } void setPlatformBuildVersionName(const android::String8& name) { mPlatformVersionName = name; } + const android::String8& getPrivateSymbolsPackage() const { return mPrivateSymbolsPackage; } + + void setPrivateSymbolsPackage(const android::String8& package) { + mPrivateSymbolsPackage = package; + } + bool getUTF16StringsOption() { return mWantUTF16 || !isMinSdkAtLeast(SDK_FROYO); } @@ -206,6 +213,8 @@ public: void setSingleCrunchOutputFile(const char* val) { mSingleCrunchOutputFile = val; } bool getBuildSharedLibrary() const { return mBuildSharedLibrary; } void setBuildSharedLibrary(bool val) { mBuildSharedLibrary = val; } + bool getBuildAppAsSharedLibrary() const { return mBuildAppAsSharedLibrary; } + void setBuildAppAsSharedLibrary(bool val) { mBuildAppAsSharedLibrary = val; } void setNoVersionVectors(bool val) { mNoVersionVectors = val; } bool getNoVersionVectors() const { return mNoVersionVectors; } @@ -327,8 +336,10 @@ private: const char* mSingleCrunchInputFile; const char* mSingleCrunchOutputFile; bool mBuildSharedLibrary; + bool mBuildAppAsSharedLibrary; android::String8 mPlatformVersionCode; android::String8 mPlatformVersionName; + android::String8 mPrivateSymbolsPackage; /* file specification */ int mArgc; diff --git a/tools/aapt/Command.cpp b/tools/aapt/Command.cpp index 5e2e82601b47..0bb88a7b3284 100644 --- a/tools/aapt/Command.cpp +++ b/tools/aapt/Command.cpp @@ -2529,11 +2529,11 @@ int doPackage(Bundle* bundle) // Write the R.java file into the appropriate class directory // e.g. gen/com/foo/app/R.java err = writeResourceSymbols(bundle, assets, assets->getPackage(), true, - bundle->getBuildSharedLibrary()); + bundle->getBuildSharedLibrary() || bundle->getBuildAppAsSharedLibrary()); } else { const String8 customPkg(bundle->getCustomPackage()); err = writeResourceSymbols(bundle, assets, customPkg, true, - bundle->getBuildSharedLibrary()); + bundle->getBuildSharedLibrary() || bundle->getBuildAppAsSharedLibrary()); } if (err < 0) { goto bail; @@ -2548,7 +2548,7 @@ int doPackage(Bundle* bundle) while (packageString != NULL) { // Write the R.java file out with the correct package name err = writeResourceSymbols(bundle, assets, String8(packageString), true, - bundle->getBuildSharedLibrary()); + bundle->getBuildSharedLibrary() || bundle->getBuildAppAsSharedLibrary()); if (err < 0) { goto bail; } diff --git a/tools/aapt/Images.cpp b/tools/aapt/Images.cpp index e4738f5eda7d..40466bd25451 100644 --- a/tools/aapt/Images.cpp +++ b/tools/aapt/Images.cpp @@ -873,14 +873,15 @@ static void dump_image(int w, int h, png_bytepp rows, int color_type) static void analyze_image(const char *imageName, image_info &imageInfo, int grayscaleTolerance, png_colorp rgbPalette, png_bytep alphaPalette, - int *paletteEntries, bool *hasTransparency, int *colorType, - png_bytepp outRows) + int *paletteEntries, int *alphaPaletteEntries, bool *hasTransparency, + int *colorType, png_bytepp outRows) { int w = imageInfo.width; int h = imageInfo.height; - int i, j, rr, gg, bb, aa, idx; - uint32_t colors[256], col; - int num_colors = 0; + int i, j, rr, gg, bb, aa, idx;; + uint32_t opaqueColors[256], alphaColors[256]; + uint32_t col; + int numOpaqueColors = 0, numAlphaColors = 0; int maxGrayDeviation = 0; bool isOpaque = true; @@ -891,6 +892,10 @@ static void analyze_image(const char *imageName, image_info &imageInfo, int gray // 1. Every pixel has R == G == B (grayscale) // 2. Every pixel has A == 255 (opaque) // 3. There are no more than 256 distinct RGBA colors + // We will track opaque colors separately from colors with + // alpha. This allows us to reencode the color table more + // efficiently (color tables entries without a corresponding + // alpha value are assumed to be opaque). if (kIsDebug) { printf("Initial image data:\n"); @@ -901,10 +906,34 @@ static void analyze_image(const char *imageName, image_info &imageInfo, int gray png_bytep row = imageInfo.rows[j]; png_bytep out = outRows[j]; for (i = 0; i < w; i++) { - rr = *row++; - gg = *row++; - bb = *row++; - aa = *row++; + + // Make sure any zero alpha pixels are fully zeroed. On average, + // each of our PNG assets seem to have about four distinct pixels + // with zero alpha. + // There are several advantages to setting these to zero: + // (1) Images are more likely able to be encodable with a palette. + // (2) Image palettes will be smaller. + // (3) Premultiplied and unpremultiplied PNG decodes can skip + // writing zeros to memory, often saving significant numbers + // of memory pages. + aa = *(row + 3); + if (aa == 0) { + rr = 0; + gg = 0; + bb = 0; + + // Also set red, green, and blue to zero in "row". If we later + // decide to encode the PNG as RGB or RGBA, we will use the + // values stored there. + *(row) = 0; + *(row + 1) = 0; + *(row + 2) = 0; + } else { + rr = *(row); + gg = *(row + 1); + bb = *(row + 2); + } + row += 4; int odev = maxGrayDeviation; maxGrayDeviation = MAX(ABS(rr - gg), maxGrayDeviation); @@ -943,36 +972,68 @@ static void analyze_image(const char *imageName, image_info &imageInfo, int gray if (isPalette) { col = (uint32_t) ((rr << 24) | (gg << 16) | (bb << 8) | aa); bool match = false; - for (idx = 0; idx < num_colors; idx++) { - if (colors[idx] == col) { - match = true; - break; + + if (aa == 0xff) { + for (idx = 0; idx < numOpaqueColors; idx++) { + if (opaqueColors[idx] == col) { + match = true; + break; + } + } + + if (!match) { + if (numOpaqueColors < 256) { + opaqueColors[numOpaqueColors] = col; + } + numOpaqueColors++; } - } - // Write the palette index for the pixel to outRows optimistically - // We might overwrite it later if we decide to encode as gray or - // gray + alpha - *out++ = idx; - if (!match) { - if (num_colors == 256) { - if (kIsDebug) { - printf("Found 257th color at %d, %d\n", i, j); + // Write the palette index for the pixel to outRows optimistically. + // We might overwrite it later if we decide to encode as gray or + // gray + alpha. We may also need to overwrite it when we combine + // into a single palette. + *out++ = idx; + } else { + for (idx = 0; idx < numAlphaColors; idx++) { + if (alphaColors[idx] == col) { + match = true; + break; } - isPalette = false; - } else { - colors[num_colors++] = col; } + + if (!match) { + if (numAlphaColors < 256) { + alphaColors[numAlphaColors] = col; + } + numAlphaColors++; + } + + // Write the palette index for the pixel to outRows optimistically. + // We might overwrite it later if we decide to encode as gray or + // gray + alpha. + *out++ = idx; + } + + if (numOpaqueColors + numAlphaColors > 256) { + if (kIsDebug) { + printf("Found 257th color at %d, %d\n", i, j); + } + isPalette = false; } } } } + // If we decide to encode the image using a palette, we will reset these counts + // to the appropriate values later. Initializing them here avoids compiler + // complaints about uses of possibly uninitialized variables. *paletteEntries = 0; + *alphaPaletteEntries = 0; + *hasTransparency = !isOpaque; - int bpp = isOpaque ? 3 : 4; - int paletteSize = w * h + bpp * num_colors; + int paletteSize = w * h + 3 * numOpaqueColors + 4 * numAlphaColors; + int bpp = isOpaque ? 3 : 4; if (kIsDebug) { printf("isGrayscale = %s\n", isGrayscale ? "true" : "false"); printf("isOpaque = %s\n", isOpaque ? "true" : "false"); @@ -1017,16 +1078,37 @@ static void analyze_image(const char *imageName, image_info &imageInfo, int gray // color type chosen if (*colorType == PNG_COLOR_TYPE_PALETTE) { + // Combine the alphaColors and the opaqueColors into a single palette. + // The alphaColors must be at the start of the palette. + uint32_t* colors = alphaColors; + memcpy(colors + numAlphaColors, opaqueColors, 4 * numOpaqueColors); + + // Fix the indices of the opaque colors in the image. + for (j = 0; j < h; j++) { + png_bytep row = imageInfo.rows[j]; + png_bytep out = outRows[j]; + for (i = 0; i < w; i++) { + uint32_t pixel = ((uint32_t*) row)[i]; + if (pixel >> 24 == 0xFF) { + out[i] += numAlphaColors; + } + } + } + // Create separate RGB and Alpha palettes and set the number of colors - *paletteEntries = num_colors; + int numColors = numOpaqueColors + numAlphaColors; + *paletteEntries = numColors; + *alphaPaletteEntries = numAlphaColors; // Create the RGB and alpha palettes - for (int idx = 0; idx < num_colors; idx++) { + for (int idx = 0; idx < numColors; idx++) { col = colors[idx]; rgbPalette[idx].red = (png_byte) ((col >> 24) & 0xff); rgbPalette[idx].green = (png_byte) ((col >> 16) & 0xff); rgbPalette[idx].blue = (png_byte) ((col >> 8) & 0xff); - alphaPalette[idx] = (png_byte) (col & 0xff); + if (idx < numAlphaColors) { + alphaPalette[idx] = (png_byte) (col & 0xff); + } } } else if (*colorType == PNG_COLOR_TYPE_GRAY || *colorType == PNG_COLOR_TYPE_GRAY_ALPHA) { // If the image is gray or gray + alpha, compact the pixels into outRows @@ -1090,17 +1172,10 @@ static void write_png(const char* imageName, png_color rgbPalette[256]; png_byte alphaPalette[256]; bool hasTransparency; - int paletteEntries; + int paletteEntries, alphaPaletteEntries; analyze_image(imageName, imageInfo, grayscaleTolerance, rgbPalette, alphaPalette, - &paletteEntries, &hasTransparency, &color_type, outRows); - - // If the image is a 9-patch, we need to preserve it as a ARGB file to make - // sure the pixels will not be pre-dithered/clamped until we decide they are - if (imageInfo.is9Patch && (color_type == PNG_COLOR_TYPE_RGB || - color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_PALETTE)) { - color_type = PNG_COLOR_TYPE_RGB_ALPHA; - } + &paletteEntries, &alphaPaletteEntries, &hasTransparency, &color_type, outRows); if (kIsDebug) { switch (color_type) { @@ -1131,7 +1206,8 @@ static void write_png(const char* imageName, if (color_type == PNG_COLOR_TYPE_PALETTE) { png_set_PLTE(write_ptr, write_info, rgbPalette, paletteEntries); if (hasTransparency) { - png_set_tRNS(write_ptr, write_info, alphaPalette, paletteEntries, (png_color_16p) 0); + png_set_tRNS(write_ptr, write_info, alphaPalette, alphaPaletteEntries, + (png_color_16p) 0); } png_set_filter(write_ptr, 0, PNG_NO_FILTERS); } else { @@ -1180,18 +1256,11 @@ static void write_png(const char* imageName, } for (int i = 0; i < chunk_count; i++) { - unknowns[i].location = PNG_HAVE_PLTE; + unknowns[i].location = PNG_HAVE_IHDR; } png_set_keep_unknown_chunks(write_ptr, PNG_HANDLE_CHUNK_ALWAYS, chunk_names, chunk_count); png_set_unknown_chunks(write_ptr, write_info, unknowns, chunk_count); -#if PNG_LIBPNG_VER < 10600 - /* Deal with unknown chunk location bug in 1.5.x and earlier */ - png_set_unknown_chunk_location(write_ptr, write_info, 0, PNG_HAVE_PLTE); - if (imageInfo.haveLayoutBounds) { - png_set_unknown_chunk_location(write_ptr, write_info, 1, PNG_HAVE_PLTE); - } -#endif } diff --git a/tools/aapt/Main.cpp b/tools/aapt/Main.cpp index bcf0d5e53c07..c424cc516b56 100644 --- a/tools/aapt/Main.cpp +++ b/tools/aapt/Main.cpp @@ -200,6 +200,9 @@ void usage(void) " --shared-lib\n" " Make a shared library resource package that can be loaded by an application\n" " at runtime to access the libraries resources. Implies --non-constant-id.\n" + " --app-as-shared-lib\n" + " Make an app resource package that also can be loaded as shared library at runtime.\n" + " Implies --non-constant-id.\n" " --error-on-failed-insert\n" " Forces aapt to return an error if it fails to insert values into the manifest\n" " with --debug-mode, --min-sdk-version, --target-sdk-version --version-code\n" @@ -217,7 +220,9 @@ void usage(void) " Prevents symbols from being generated for strings that do not have a default\n" " localization\n" " --no-version-vectors\n" - " Do not automatically generate versioned copies of vector XML resources.\n", + " Do not automatically generate versioned copies of vector XML resources.\n" + " --private-symbols\n" + " Java package name to use when generating R.java for private resources.\n", gDefaultIgnoreAssets); } @@ -668,6 +673,9 @@ int main(int argc, char* const argv[]) } else if (strcmp(cp, "-shared-lib") == 0) { bundle.setNonConstantId(true); bundle.setBuildSharedLibrary(true); + } else if (strcmp(cp, "-app-as-shared-lib") == 0) { + bundle.setNonConstantId(true); + bundle.setBuildAppAsSharedLibrary(true); } else if (strcmp(cp, "-no-crunch") == 0) { bundle.setUseCrunchCache(true); } else if (strcmp(cp, "-ignore-assets") == 0) { @@ -683,6 +691,16 @@ int main(int argc, char* const argv[]) bundle.setPseudolocalize(PSEUDO_ACCENTED | PSEUDO_BIDI); } else if (strcmp(cp, "-no-version-vectors") == 0) { bundle.setNoVersionVectors(true); + } else if (strcmp(cp, "-private-symbols") == 0) { + argc--; + argv++; + if (!argc) { + fprintf(stderr, "ERROR: No argument supplied for " + "'--private-symbols' option\n"); + wantUsage = true; + goto bail; + } + bundle.setPrivateSymbolsPackage(String8(argv[0])); } else { fprintf(stderr, "ERROR: Unknown option '-%s'\n", cp); wantUsage = true; diff --git a/tools/aapt/Package.cpp b/tools/aapt/Package.cpp index cb244eccfe21..641c34bd2dda 100644 --- a/tools/aapt/Package.cpp +++ b/tools/aapt/Package.cpp @@ -33,7 +33,7 @@ static const char* kNoCompressExt[] = { ".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet", ".rtttl", ".imy", ".xmf", ".mp4", ".m4a", ".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2", - ".amr", ".awb", ".wma", ".wmv" + ".amr", ".awb", ".wma", ".wmv", ".webm" }; /* fwd decls, so I can write this downward */ diff --git a/tools/aapt/Resource.cpp b/tools/aapt/Resource.cpp index 1b30d362a716..4d9ba6c95d9e 100644 --- a/tools/aapt/Resource.cpp +++ b/tools/aapt/Resource.cpp @@ -1161,6 +1161,12 @@ status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuil printf("Creating resources for package %s\n", assets->getPackage().string()); } + // Set the private symbols package if it was declared. + // This can also be declared in XML as <private-symbols name="package" /> + if (bundle->getPrivateSymbolsPackage().size() != 0) { + assets->setSymbolsPrivatePackage(bundle->getPrivateSymbolsPackage()); + } + ResourceTable::PackageType packageType = ResourceTable::App; if (bundle->getBuildSharedLibrary()) { packageType = ResourceTable::SharedLibrary; @@ -1537,12 +1543,20 @@ status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuil std::queue<CompileResourceWorkItem>& workQueue = table.getWorkQueue(); while (!workQueue.empty()) { CompileResourceWorkItem& workItem = workQueue.front(); - err = compileXmlFile(bundle, assets, workItem.resourceName, workItem.file, &table, xmlFlags); + int xmlCompilationFlags = xmlFlags | XML_COMPILE_PARSE_VALUES + | XML_COMPILE_ASSIGN_ATTRIBUTE_IDS; + if (!workItem.needsCompiling) { + xmlCompilationFlags &= ~XML_COMPILE_ASSIGN_ATTRIBUTE_IDS; + xmlCompilationFlags &= ~XML_COMPILE_PARSE_VALUES; + } + err = compileXmlFile(bundle, assets, workItem.resourceName, workItem.xmlRoot, + workItem.file, &table, xmlCompilationFlags); + if (err == NO_ERROR) { assets->addResource(workItem.resPath.getPathLeaf(), - workItem.resPath, - workItem.file, - workItem.file->getResourceType()); + workItem.resPath, + workItem.file, + workItem.file->getResourceType()); } else { hasErrors = true; } @@ -1737,9 +1751,7 @@ status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuil manifestFile->getGroupEntry(), manifestFile->getResourceType()); err = compileXmlFile(bundle, assets, String16(), manifestFile, - outManifestFile, &table, - XML_COMPILE_ASSIGN_ATTRIBUTE_IDS - | XML_COMPILE_STRIP_WHITESPACE | XML_COMPILE_STRIP_RAW_VALUES); + outManifestFile, &table, XML_COMPILE_STANDARD_RESOURCE & ~XML_COMPILE_STRIP_COMMENTS); if (err < NO_ERROR) { return err; } @@ -2120,7 +2132,7 @@ static status_t writeResourceLoadedCallback( size_t N = symbols->getSymbols().size(); for (i=0; i<N; i++) { const AaptSymbolEntry& sym = symbols->getSymbols().valueAt(i); - if (sym.typeCode == AaptSymbolEntry::TYPE_UNKNOWN) { + if (sym.typeCode != AaptSymbolEntry::TYPE_INT32) { continue; } if (!assets->isJavaSymbol(sym, includePrivate)) { diff --git a/tools/aapt/ResourceTable.cpp b/tools/aapt/ResourceTable.cpp index d5a09d817b1e..e87c7d40f1d4 100644 --- a/tools/aapt/ResourceTable.cpp +++ b/tools/aapt/ResourceTable.cpp @@ -88,8 +88,11 @@ status_t compileXmlFile(const Bundle* bundle, root->setUTF8(true); } - bool hasErrors = false; + if (table->processBundleFormat(bundle, resourceName, target, root) != NO_ERROR) { + return UNKNOWN_ERROR; + } + bool hasErrors = false; if ((options&XML_COMPILE_ASSIGN_ATTRIBUTE_IDS) != 0) { status_t err = root->assignResourceIds(assets, table); if (err != NO_ERROR) { @@ -97,9 +100,11 @@ status_t compileXmlFile(const Bundle* bundle, } } - status_t err = root->parseValues(assets, table); - if (err != NO_ERROR) { - hasErrors = true; + if ((options&XML_COMPILE_PARSE_VALUES) != 0) { + status_t err = root->parseValues(assets, table); + if (err != NO_ERROR) { + hasErrors = true; + } } if (hasErrors) { @@ -114,7 +119,7 @@ status_t compileXmlFile(const Bundle* bundle, printf("Input XML Resource:\n"); root->print(); } - err = root->flatten(target, + status_t err = root->flatten(target, (options&XML_COMPILE_STRIP_COMMENTS) != 0, (options&XML_COMPILE_STRIP_RAW_VALUES) != 0); if (err != NO_ERROR) { @@ -1136,7 +1141,15 @@ status_t compileResourceFile(Bundle* bundle, } pkg = String16(block.getAttributeStringValue(pkgIdx, &len)); if (!localHasErrors) { - assets->setSymbolsPrivatePackage(String8(pkg)); + SourcePos(in->getPrintableSource(), block.getLineNumber()).warning( + "<private-symbols> is deprecated. Use the command line flag " + "--private-symbols instead.\n"); + if (assets->havePrivateSymbols()) { + SourcePos(in->getPrintableSource(), block.getLineNumber()).warning( + "private symbol package already specified. Ignoring...\n"); + } else { + assets->setSymbolsPrivatePackage(String8(pkg)); + } } while ((code=block.next()) != ResXMLTree::END_DOCUMENT && code != ResXMLTree::BAD_DOCUMENT) { @@ -4755,9 +4768,9 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle, newConfig.sdkVersion = sdkVersionToGenerate; sp<AaptFile> newFile = new AaptFile(target->getSourceFile(), AaptGroupEntry(newConfig), target->getResourceType()); - String8 resPath = String8::format("res/%s/%s", + String8 resPath = String8::format("res/%s/%s.xml", newFile->getGroupEntry().toDirName(target->getResourceType()).string(), - target->getSourceFile().getPathLeaf().string()); + String8(resourceName).string()); resPath.convertToResPath(); // Add a resource table entry. @@ -4784,9 +4797,11 @@ status_t ResourceTable::modifyForCompat(const Bundle* bundle, item.resourceName = resourceName; item.resPath = resPath; item.file = newFile; + item.xmlRoot = newRoot; + item.needsCompiling = false; // This step occurs after we parse/assign, so we don't need + // to do it again. mWorkQueue.push(item); } - return NO_ERROR; } @@ -4825,3 +4840,226 @@ void ResourceTable::getDensityVaryingResources( } } } + +static String16 buildNamespace(const String16& package) { + return String16("http://schemas.android.com/apk/res/") + package; +} + +static sp<XMLNode> findOnlyChildElement(const sp<XMLNode>& parent) { + const Vector<sp<XMLNode> >& children = parent->getChildren(); + sp<XMLNode> onlyChild; + for (size_t i = 0; i < children.size(); i++) { + if (children[i]->getType() != XMLNode::TYPE_CDATA) { + if (onlyChild != NULL) { + return NULL; + } + onlyChild = children[i]; + } + } + return onlyChild; +} + +/** + * Detects use of the `bundle' format and extracts nested resources into their own top level + * resources. The bundle format looks like this: + * + * <!-- res/drawable/bundle.xml --> + * <animated-vector xmlns:aapt="http://schemas.android.com/aapt"> + * <aapt:attr name="android:drawable"> + * <vector android:width="60dp" + * android:height="60dp"> + * <path android:name="v" + * android:fillColor="#000000" + * android:pathData="M300,70 l 0,-70 70,..." /> + * </vector> + * </aapt:attr> + * </animated-vector> + * + * When AAPT sees the <aapt:attr> tag, it will extract its single element and its children + * into a new high-level resource, assigning it a name and ID. Then value of the `name` + * attribute must be a resource attribute. That resource attribute is inserted into the parent + * with the reference to the extracted resource as the value. + * + * <!-- res/drawable/bundle.xml --> + * <animated-vector android:drawable="@drawable/bundle_1.xml"> + * </animated-vector> + * + * <!-- res/drawable/bundle_1.xml --> + * <vector android:width="60dp" + * android:height="60dp"> + * <path android:name="v" + * android:fillColor="#000000" + * android:pathData="M300,70 l 0,-70 70,..." /> + * </vector> + */ +status_t ResourceTable::processBundleFormat(const Bundle* bundle, + const String16& resourceName, + const sp<AaptFile>& target, + const sp<XMLNode>& root) { + Vector<sp<XMLNode> > namespaces; + if (root->getType() == XMLNode::TYPE_NAMESPACE) { + namespaces.push(root); + } + return processBundleFormatImpl(bundle, resourceName, target, root, &namespaces); +} + +status_t ResourceTable::processBundleFormatImpl(const Bundle* bundle, + const String16& resourceName, + const sp<AaptFile>& target, + const sp<XMLNode>& parent, + Vector<sp<XMLNode> >* namespaces) { + const String16 kAaptNamespaceUri16("http://schemas.android.com/aapt"); + const String16 kName16("name"); + const String16 kAttr16("attr"); + const String16 kAssetPackage16(mAssets->getPackage()); + + Vector<sp<XMLNode> >& children = parent->getChildren(); + for (size_t i = 0; i < children.size(); i++) { + const sp<XMLNode>& child = children[i]; + + if (child->getType() == XMLNode::TYPE_CDATA) { + continue; + } else if (child->getType() == XMLNode::TYPE_NAMESPACE) { + namespaces->push(child); + } + + if (child->getElementNamespace() != kAaptNamespaceUri16 || + child->getElementName() != kAttr16) { + status_t result = processBundleFormatImpl(bundle, resourceName, target, child, + namespaces); + if (result != NO_ERROR) { + return result; + } + + if (child->getType() == XMLNode::TYPE_NAMESPACE) { + namespaces->pop(); + } + continue; + } + + // This is the <aapt:attr> tag. Look for the 'name' attribute. + SourcePos source(child->getFilename(), child->getStartLineNumber()); + + sp<XMLNode> nestedRoot = findOnlyChildElement(child); + if (nestedRoot == NULL) { + source.error("<%s:%s> must have exactly one child element", + String8(child->getElementNamespace()).string(), + String8(child->getElementName()).string()); + return UNKNOWN_ERROR; + } + + // Find the special attribute 'parent-attr'. This attribute's value contains + // the resource attribute for which this element should be assigned in the parent. + const XMLNode::attribute_entry* attr = child->getAttribute(String16(), kName16); + if (attr == NULL) { + source.error("inline resource definition must specify an attribute via 'name'"); + return UNKNOWN_ERROR; + } + + // Parse the attribute name. + const char* errorMsg = NULL; + String16 attrPackage, attrType, attrName; + bool result = ResTable::expandResourceRef(attr->string.string(), + attr->string.size(), + &attrPackage, &attrType, &attrName, + &kAttr16, &kAssetPackage16, + &errorMsg, NULL); + if (!result) { + source.error("invalid attribute name for 'name': %s", errorMsg); + return UNKNOWN_ERROR; + } + + if (attrType != kAttr16) { + // The value of the 'name' attribute must be an attribute reference. + source.error("value of 'name' must be an attribute reference."); + return UNKNOWN_ERROR; + } + + // Generate a name for this nested resource and try to add it to the table. + // We do this in a loop because the name may be taken, in which case we will + // increment a suffix until we succeed. + String8 nestedResourceName; + String8 nestedResourcePath; + int suffix = 1; + while (true) { + // This child element will be extracted into its own resource file. + // Generate a name and path for it from its parent. + nestedResourceName = String8::format("%s_%d", + String8(resourceName).string(), suffix++); + nestedResourcePath = String8::format("res/%s/%s.xml", + target->getGroupEntry().toDirName(target->getResourceType()) + .string(), + nestedResourceName.string()); + + // Lookup or create the entry for this name. + sp<Entry> entry = getEntry(kAssetPackage16, + String16(target->getResourceType()), + String16(nestedResourceName), + source, + false, + &target->getGroupEntry().toParams(), + true); + if (entry == NULL) { + return UNKNOWN_ERROR; + } + + if (entry->getType() == Entry::TYPE_UNKNOWN) { + // The value for this resource has never been set, + // meaning we're good! + entry->setItem(source, String16(nestedResourcePath)); + break; + } + + // We failed (name already exists), so try with a different name + // (increment the suffix). + } + + if (bundle->getVerbose()) { + source.printf("generating nested resource %s:%s/%s", + mAssets->getPackage().string(), target->getResourceType().string(), + nestedResourceName.string()); + } + + // Build the attribute reference and assign it to the parent. + String16 nestedResourceRef = String16(String8::format("@%s:%s/%s", + mAssets->getPackage().string(), target->getResourceType().string(), + nestedResourceName.string())); + + String16 attrNs = buildNamespace(attrPackage); + if (parent->getAttribute(attrNs, attrName) != NULL) { + SourcePos(parent->getFilename(), parent->getStartLineNumber()) + .error("parent of nested resource already defines attribute '%s:%s'", + String8(attrPackage).string(), String8(attrName).string()); + return UNKNOWN_ERROR; + } + + // Add the reference to the inline resource. + parent->addAttribute(attrNs, attrName, nestedResourceRef); + + // Remove the <aapt:attr> child element from here. + children.removeAt(i); + i--; + + // Append all namespace declarations that we've seen on this branch in the XML tree + // to this resource. + // We do this because the order of namespace declarations and prefix usage is determined + // by the developer and we do not want to override any decisions. Be conservative. + for (size_t nsIndex = namespaces->size(); nsIndex > 0; nsIndex--) { + const sp<XMLNode>& ns = namespaces->itemAt(nsIndex - 1); + sp<XMLNode> newNs = XMLNode::newNamespace(ns->getFilename(), ns->getNamespacePrefix(), + ns->getNamespaceUri()); + newNs->addChild(nestedRoot); + nestedRoot = newNs; + } + + // Schedule compilation of the nested resource. + CompileResourceWorkItem workItem; + workItem.resPath = nestedResourcePath; + workItem.resourceName = String16(nestedResourceName); + workItem.xmlRoot = nestedRoot; + workItem.file = new AaptFile(target->getSourceFile(), target->getGroupEntry(), + target->getResourceType()); + mWorkQueue.push(workItem); + } + return NO_ERROR; +} diff --git a/tools/aapt/ResourceTable.h b/tools/aapt/ResourceTable.h index c4bdf09d8b19..4b7b3cdcef2b 100644 --- a/tools/aapt/ResourceTable.h +++ b/tools/aapt/ResourceTable.h @@ -23,13 +23,14 @@ class ResourceTable; enum { XML_COMPILE_STRIP_COMMENTS = 1<<0, XML_COMPILE_ASSIGN_ATTRIBUTE_IDS = 1<<1, - XML_COMPILE_COMPACT_WHITESPACE = 1<<2, - XML_COMPILE_STRIP_WHITESPACE = 1<<3, - XML_COMPILE_STRIP_RAW_VALUES = 1<<4, - XML_COMPILE_UTF8 = 1<<5, + XML_COMPILE_PARSE_VALUES = 1 << 2, + XML_COMPILE_COMPACT_WHITESPACE = 1<<3, + XML_COMPILE_STRIP_WHITESPACE = 1<<4, + XML_COMPILE_STRIP_RAW_VALUES = 1<<5, + XML_COMPILE_UTF8 = 1<<6, XML_COMPILE_STANDARD_RESOURCE = - XML_COMPILE_STRIP_COMMENTS | XML_COMPILE_ASSIGN_ATTRIBUTE_IDS + XML_COMPILE_STRIP_COMMENTS | XML_COMPILE_ASSIGN_ATTRIBUTE_IDS | XML_COMPILE_PARSE_VALUES | XML_COMPILE_STRIP_WHITESPACE | XML_COMPILE_STRIP_RAW_VALUES }; @@ -83,6 +84,8 @@ struct CompileResourceWorkItem { String16 resourceName; String8 resPath; sp<AaptFile> file; + sp<XMLNode> xmlRoot; + bool needsCompiling = true; }; class ResourceTable : public ResTable::Accessor @@ -206,6 +209,12 @@ public: const sp<AaptFile>& file, const sp<XMLNode>& root); + status_t processBundleFormat(const Bundle* bundle, + const String16& resourceName, + const sp<AaptFile>& file, + const sp<XMLNode>& parent); + + sp<AaptFile> flatten(Bundle* bundle, const sp<const ResourceFilter>& filter, const bool isBase); @@ -586,6 +595,11 @@ private: Res_value* outValue); int getPublicAttributeSdkLevel(uint32_t attrId) const; + status_t processBundleFormatImpl(const Bundle* bundle, + const String16& resourceName, + const sp<AaptFile>& file, + const sp<XMLNode>& parent, + Vector<sp<XMLNode> >* namespaces); String16 mAssetsPackage; PackageType mPackageType; diff --git a/tools/aapt/XMLNode.cpp b/tools/aapt/XMLNode.cpp index dc08eb806356..5b215daeb494 100644 --- a/tools/aapt/XMLNode.cpp +++ b/tools/aapt/XMLNode.cpp @@ -693,6 +693,12 @@ const Vector<sp<XMLNode> >& XMLNode::getChildren() const return mChildren; } + +Vector<sp<XMLNode> >& XMLNode::getChildren() +{ + return mChildren; +} + const String8& XMLNode::getFilename() const { return mFilename; @@ -717,6 +723,18 @@ const XMLNode::attribute_entry* XMLNode::getAttribute(const String16& ns, return NULL; } +bool XMLNode::removeAttribute(const String16& ns, const String16& name) +{ + for (size_t i = 0; i < mAttributes.size(); i++) { + const attribute_entry& ae(mAttributes.itemAt(i)); + if (ae.ns == ns && ae.name == name) { + removeAttribute(i); + return true; + } + } + return false; +} + XMLNode::attribute_entry* XMLNode::editAttribute(const String16& ns, const String16& name) { diff --git a/tools/aapt/XMLNode.h b/tools/aapt/XMLNode.h index b9e5cd574cdc..749bf9f59bf7 100644 --- a/tools/aapt/XMLNode.h +++ b/tools/aapt/XMLNode.h @@ -55,7 +55,7 @@ public: sp<XMLNode> newCData(const String8& filename) { return new XMLNode(filename); } - + enum type { TYPE_NAMESPACE, TYPE_ELEMENT, @@ -70,6 +70,7 @@ public: const String16& getElementNamespace() const; const String16& getElementName() const; const Vector<sp<XMLNode> >& getChildren() const; + Vector<sp<XMLNode> >& getChildren(); const String8& getFilename() const; @@ -97,6 +98,7 @@ public: const Vector<attribute_entry>& getAttributes() const; const attribute_entry* getAttribute(const String16& ns, const String16& name) const; + bool removeAttribute(const String16& ns, const String16& name); attribute_entry* editAttribute(const String16& ns, const String16& name); diff --git a/tools/aapt/ZipFile.cpp b/tools/aapt/ZipFile.cpp index 36f4e73b3ac2..2840826c32a6 100644 --- a/tools/aapt/ZipFile.cpp +++ b/tools/aapt/ZipFile.cpp @@ -364,7 +364,7 @@ status_t ZipFile::addCommon(const char* fileName, const void* data, size_t size, long lfhPosn, startPosn, endPosn, uncompressedLen; FILE* inputFp = NULL; unsigned long crc; - time_t modWhen; + time_t modWhen = 0; if (mReadOnly) return INVALID_OPERATION; @@ -497,7 +497,6 @@ status_t ZipFile::addCommon(const char* fileName, const void* data, size_t size, */ pEntry->setDataInfo(uncompressedLen, endPosn - startPosn, crc, compressionMethod); - modWhen = getModTime(inputFp ? fileno(inputFp) : fileno(mZipFp)); pEntry->setModWhen(modWhen); pEntry->setLFHOffset(lfhPosn); mEOCD.mNumEntries++; diff --git a/tools/aapt2/Android.mk b/tools/aapt2/Android.mk index e5c42d5f74c1..f74b93abd796 100644 --- a/tools/aapt2/Android.mk +++ b/tools/aapt2/Android.mk @@ -25,62 +25,87 @@ LOCAL_PATH:= $(call my-dir) main := Main.cpp sources := \ - BigBuffer.cpp \ - BinaryResourceParser.cpp \ - BindingXmlPullParser.cpp \ + compile/IdAssigner.cpp \ + compile/Png.cpp \ + compile/PseudolocaleGenerator.cpp \ + compile/Pseudolocalizer.cpp \ + compile/XmlIdCollector.cpp \ + filter/ConfigFilter.cpp \ + flatten/Archive.cpp \ + flatten/TableFlattener.cpp \ + flatten/XmlFlattener.cpp \ + io/FileSystem.cpp \ + io/ZipArchive.cpp \ + link/AutoVersioner.cpp \ + link/ManifestFixer.cpp \ + link/PrivateAttributeMover.cpp \ + link/ReferenceLinker.cpp \ + link/TableMerger.cpp \ + link/XmlReferenceLinker.cpp \ + process/SymbolTable.cpp \ + unflatten/BinaryResourceParser.cpp \ + unflatten/ResChunkPullParser.cpp \ + util/BigBuffer.cpp \ + util/Files.cpp \ + util/Util.cpp \ ConfigDescription.cpp \ Debug.cpp \ - Files.cpp \ - Flag.cpp \ - JavaClassGenerator.cpp \ - Linker.cpp \ + Flags.cpp \ + java/AnnotationProcessor.cpp \ + java/JavaClassGenerator.cpp \ + java/ManifestClassGenerator.cpp \ + java/ProguardRules.cpp \ Locale.cpp \ - Logger.cpp \ - ManifestMerger.cpp \ - ManifestParser.cpp \ - ManifestValidator.cpp \ - Png.cpp \ - ProguardRules.cpp \ - ResChunkPullParser.cpp \ Resource.cpp \ ResourceParser.cpp \ ResourceTable.cpp \ - ResourceTableResolver.cpp \ + ResourceUtils.cpp \ ResourceValues.cpp \ SdkConstants.cpp \ StringPool.cpp \ - TableFlattener.cpp \ - Util.cpp \ - ScopedXmlPullParser.cpp \ - SourceXmlPullParser.cpp \ - XliffXmlPullParser.cpp \ - XmlDom.cpp \ - XmlFlattener.cpp \ - ZipEntry.cpp \ - ZipFile.cpp + xml/XmlDom.cpp \ + xml/XmlPullParser.cpp \ + xml/XmlUtil.cpp testSources := \ - BigBuffer_test.cpp \ - BindingXmlPullParser_test.cpp \ - Compat_test.cpp \ + compile/IdAssigner_test.cpp \ + compile/PseudolocaleGenerator_test.cpp \ + compile/Pseudolocalizer_test.cpp \ + compile/XmlIdCollector_test.cpp \ + filter/ConfigFilter_test.cpp \ + flatten/FileExportWriter_test.cpp \ + flatten/TableFlattener_test.cpp \ + flatten/XmlFlattener_test.cpp \ + link/AutoVersioner_test.cpp \ + link/ManifestFixer_test.cpp \ + link/PrivateAttributeMover_test.cpp \ + link/ReferenceLinker_test.cpp \ + link/TableMerger_test.cpp \ + link/XmlReferenceLinker_test.cpp \ + process/SymbolTable_test.cpp \ + unflatten/FileExportHeaderReader_test.cpp \ + util/BigBuffer_test.cpp \ + util/Maybe_test.cpp \ + util/StringPiece_test.cpp \ + util/Util_test.cpp \ ConfigDescription_test.cpp \ - JavaClassGenerator_test.cpp \ - Linker_test.cpp \ + java/AnnotationProcessor_test.cpp \ + java/JavaClassGenerator_test.cpp \ + java/ManifestClassGenerator_test.cpp \ Locale_test.cpp \ - ManifestMerger_test.cpp \ - ManifestParser_test.cpp \ - Maybe_test.cpp \ - NameMangler_test.cpp \ - ResourceParser_test.cpp \ Resource_test.cpp \ + ResourceParser_test.cpp \ ResourceTable_test.cpp \ - ScopedXmlPullParser_test.cpp \ - StringPiece_test.cpp \ + ResourceUtils_test.cpp \ StringPool_test.cpp \ - Util_test.cpp \ - XliffXmlPullParser_test.cpp \ - XmlDom_test.cpp \ - XmlFlattener_test.cpp + ValueVisitor_test.cpp \ + xml/XmlDom_test.cpp \ + xml/XmlPullParser_test.cpp \ + xml/XmlUtil_test.cpp + +toolSources := \ + compile/Compile.cpp \ + link/Link.cpp hostLdLibs := @@ -101,7 +126,7 @@ else endif cFlags := -Wall -Werror -Wno-unused-parameter -UNDEBUG -cppFlags := -std=c++11 -Wno-missing-field-initializers -Wno-unused-private-field +cppFlags := -std=c++11 -Wno-missing-field-initializers -fno-exceptions -fno-rtti # ========================================================== # Build the host static library: libaapt2 @@ -139,7 +164,7 @@ include $(BUILD_HOST_NATIVE_TEST) include $(CLEAR_VARS) LOCAL_MODULE := aapt2 -LOCAL_SRC_FILES := $(main) +LOCAL_SRC_FILES := $(main) $(toolSources) LOCAL_STATIC_LIBRARIES += libaapt2 $(hostStaticLibs) LOCAL_LDLIBS += $(hostLdLibs) diff --git a/tools/aapt2/BinaryResourceParser.cpp b/tools/aapt2/BinaryResourceParser.cpp deleted file mode 100644 index 4f1947ab8364..000000000000 --- a/tools/aapt2/BinaryResourceParser.cpp +++ /dev/null @@ -1,897 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "BinaryResourceParser.h" -#include "Logger.h" -#include "ResChunkPullParser.h" -#include "Resolver.h" -#include "ResourceParser.h" -#include "ResourceTable.h" -#include "ResourceTypeExtensions.h" -#include "ResourceValues.h" -#include "Source.h" -#include "Util.h" - -#include <androidfw/ResourceTypes.h> -#include <androidfw/TypeWrappers.h> -#include <map> -#include <string> - -namespace aapt { - -using namespace android; - -/* - * Visitor that converts a reference's resource ID to a resource name, - * given a mapping from resource ID to resource name. - */ -struct ReferenceIdToNameVisitor : ValueVisitor { - ReferenceIdToNameVisitor(const std::shared_ptr<IResolver>& resolver, - std::map<ResourceId, ResourceName>* cache) : - mResolver(resolver), mCache(cache) { - } - - void visit(Reference& reference, ValueVisitorArgs&) override { - idToName(reference); - } - - void visit(Attribute& attr, ValueVisitorArgs&) override { - for (auto& entry : attr.symbols) { - idToName(entry.symbol); - } - } - - void visit(Style& style, ValueVisitorArgs&) override { - if (style.parent.id.isValid()) { - idToName(style.parent); - } - - for (auto& entry : style.entries) { - idToName(entry.key); - entry.value->accept(*this, {}); - } - } - - void visit(Styleable& styleable, ValueVisitorArgs&) override { - for (auto& attr : styleable.entries) { - idToName(attr); - } - } - - void visit(Array& array, ValueVisitorArgs&) override { - for (auto& item : array.items) { - item->accept(*this, {}); - } - } - - void visit(Plural& plural, ValueVisitorArgs&) override { - for (auto& item : plural.values) { - if (item) { - item->accept(*this, {}); - } - } - } - -private: - void idToName(Reference& reference) { - if (!reference.id.isValid()) { - return; - } - - auto cacheIter = mCache->find(reference.id); - if (cacheIter != mCache->end()) { - reference.name = cacheIter->second; - reference.id = 0; - } else { - Maybe<ResourceName> result = mResolver->findName(reference.id); - if (result) { - reference.name = result.value(); - - // Add to cache. - mCache->insert({reference.id, reference.name}); - - reference.id = 0; - } - } - } - - std::shared_ptr<IResolver> mResolver; - std::map<ResourceId, ResourceName>* mCache; -}; - - -BinaryResourceParser::BinaryResourceParser(const std::shared_ptr<ResourceTable>& table, - const std::shared_ptr<IResolver>& resolver, - const Source& source, - const std::u16string& defaultPackage, - const void* data, - size_t len) : - mTable(table), mResolver(resolver), mSource(source), mDefaultPackage(defaultPackage), - mData(data), mDataLen(len) { -} - -bool BinaryResourceParser::parse() { - ResChunkPullParser parser(mData, mDataLen); - - bool error = false; - while(ResChunkPullParser::isGoodEvent(parser.next())) { - if (parser.getChunk()->type != android::RES_TABLE_TYPE) { - Logger::warn(mSource) - << "unknown chunk of type '" - << parser.getChunk()->type - << "'." - << std::endl; - continue; - } - - error |= !parseTable(parser.getChunk()); - } - - if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { - Logger::error(mSource) - << "bad document: " - << parser.getLastError() - << "." - << std::endl; - return false; - } - return !error; -} - -bool BinaryResourceParser::getSymbol(const void* data, ResourceNameRef* outSymbol) { - if (!mSymbolEntries || mSymbolEntryCount == 0) { - return false; - } - - if (reinterpret_cast<uintptr_t>(data) < reinterpret_cast<uintptr_t>(mData)) { - return false; - } - - // We only support 32 bit offsets right now. - const uintptr_t offset = reinterpret_cast<uintptr_t>(data) - - reinterpret_cast<uintptr_t>(mData); - if (offset > std::numeric_limits<uint32_t>::max()) { - return false; - } - - for (size_t i = 0; i < mSymbolEntryCount; i++) { - if (mSymbolEntries[i].offset == offset) { - // This offset is a symbol! - const StringPiece16 str = util::getString(mSymbolPool, - mSymbolEntries[i].stringIndex); - StringPiece16 typeStr; - ResourceParser::extractResourceName(str, &outSymbol->package, &typeStr, - &outSymbol->entry); - const ResourceType* type = parseResourceType(typeStr); - if (!type) { - return false; - } - if (outSymbol->package.empty()) { - outSymbol->package = mTable->getPackage(); - } - outSymbol->type = *type; - - // Since we scan the symbol table in order, we can start looking for the - // next symbol from this point. - mSymbolEntryCount -= i + 1; - mSymbolEntries += i + 1; - return true; - } - } - return false; -} - -bool BinaryResourceParser::parseSymbolTable(const ResChunk_header* chunk) { - const SymbolTable_header* symbolTableHeader = convertTo<SymbolTable_header>(chunk); - if (!symbolTableHeader) { - Logger::error(mSource) - << "could not parse chunk as SymbolTable_header." - << std::endl; - return false; - } - - const size_t entrySizeBytes = symbolTableHeader->count * sizeof(SymbolTable_entry); - if (entrySizeBytes > getChunkDataLen(symbolTableHeader->header)) { - Logger::error(mSource) - << "entries extend beyond chunk." - << std::endl; - return false; - } - - mSymbolEntries = reinterpret_cast<const SymbolTable_entry*>( - getChunkData(symbolTableHeader->header)); - mSymbolEntryCount = symbolTableHeader->count; - - ResChunkPullParser parser(getChunkData(symbolTableHeader->header) + entrySizeBytes, - getChunkDataLen(symbolTableHeader->header) - entrySizeBytes); - if (!ResChunkPullParser::isGoodEvent(parser.next())) { - Logger::error(mSource) - << "failed to parse chunk: " - << parser.getLastError() - << "." - << std::endl; - return false; - } - - if (parser.getChunk()->type != android::RES_STRING_POOL_TYPE) { - Logger::error(mSource) - << "expected Symbol string pool." - << std::endl; - return false; - } - - if (mSymbolPool.setTo(parser.getChunk(), parser.getChunk()->size) != NO_ERROR) { - Logger::error(mSource) - << "failed to parse symbol string pool with code: " - << mSymbolPool.getError() - << "." - << std::endl; - return false; - } - return true; -} - -bool BinaryResourceParser::parseTable(const ResChunk_header* chunk) { - const ResTable_header* tableHeader = convertTo<ResTable_header>(chunk); - if (!tableHeader) { - Logger::error(mSource) - << "could not parse chunk as ResTable_header." - << std::endl; - return false; - } - - ResChunkPullParser parser(getChunkData(tableHeader->header), - getChunkDataLen(tableHeader->header)); - while (ResChunkPullParser::isGoodEvent(parser.next())) { - switch (parser.getChunk()->type) { - case android::RES_STRING_POOL_TYPE: - if (mValuePool.getError() == NO_INIT) { - if (mValuePool.setTo(parser.getChunk(), parser.getChunk()->size) != - NO_ERROR) { - Logger::error(mSource) - << "failed to parse value string pool with code: " - << mValuePool.getError() - << "." - << std::endl; - return false; - } - - // Reserve some space for the strings we are going to add. - mTable->getValueStringPool().hintWillAdd( - mValuePool.size(), mValuePool.styleCount()); - } else { - Logger::warn(mSource) - << "unexpected string pool." - << std::endl; - } - break; - - case RES_TABLE_SYMBOL_TABLE_TYPE: - if (!parseSymbolTable(parser.getChunk())) { - return false; - } - break; - - case RES_TABLE_SOURCE_POOL_TYPE: { - if (mSourcePool.setTo(getChunkData(*parser.getChunk()), - getChunkDataLen(*parser.getChunk())) != NO_ERROR) { - Logger::error(mSource) - << "failed to parse source pool with code: " - << mSourcePool.getError() - << "." - << std::endl; - return false; - } - break; - } - - case android::RES_TABLE_PACKAGE_TYPE: - if (!parsePackage(parser.getChunk())) { - return false; - } - break; - - default: - Logger::warn(mSource) - << "unexpected chunk of type " - << parser.getChunk()->type - << "." - << std::endl; - break; - } - } - - if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { - Logger::error(mSource) - << "bad resource table: " << parser.getLastError() - << "." - << std::endl; - return false; - } - return true; -} - -bool BinaryResourceParser::parsePackage(const ResChunk_header* chunk) { - if (mValuePool.getError() != NO_ERROR) { - Logger::error(mSource) - << "no value string pool for ResTable." - << std::endl; - return false; - } - - const ResTable_package* packageHeader = convertTo<ResTable_package>(chunk); - if (!packageHeader) { - Logger::error(mSource) - << "could not parse chunk as ResTable_header." - << std::endl; - return false; - } - - if (mTable->getPackageId() == ResourceTable::kUnsetPackageId) { - // This is the first time the table has it's package ID set. - mTable->setPackageId(packageHeader->id); - } else if (mTable->getPackageId() != packageHeader->id) { - Logger::error(mSource) - << "ResTable_package has package ID " - << std::hex << packageHeader->id << std::dec - << " but ResourceTable has package ID " - << std::hex << mTable->getPackageId() << std::dec - << std::endl; - return false; - } - - size_t len = strnlen16(reinterpret_cast<const char16_t*>(packageHeader->name), - sizeof(packageHeader->name) / sizeof(packageHeader->name[0])); - if (mTable->getPackage().empty() && len == 0) { - mTable->setPackage(mDefaultPackage); - } else if (len > 0) { - StringPiece16 thisPackage(reinterpret_cast<const char16_t*>(packageHeader->name), len); - if (mTable->getPackage().empty()) { - mTable->setPackage(thisPackage); - } else if (thisPackage != mTable->getPackage()) { - Logger::error(mSource) - << "incompatible packages: " - << mTable->getPackage() - << " vs. " - << thisPackage - << std::endl; - return false; - } - } - - ResChunkPullParser parser(getChunkData(packageHeader->header), - getChunkDataLen(packageHeader->header)); - while (ResChunkPullParser::isGoodEvent(parser.next())) { - switch (parser.getChunk()->type) { - case android::RES_STRING_POOL_TYPE: - if (mTypePool.getError() == NO_INIT) { - if (mTypePool.setTo(parser.getChunk(), parser.getChunk()->size) != - NO_ERROR) { - Logger::error(mSource) - << "failed to parse type string pool with code " - << mTypePool.getError() - << "." - << std::endl; - return false; - } - } else if (mKeyPool.getError() == NO_INIT) { - if (mKeyPool.setTo(parser.getChunk(), parser.getChunk()->size) != - NO_ERROR) { - Logger::error(mSource) - << "failed to parse key string pool with code " - << mKeyPool.getError() - << "." - << std::endl; - return false; - } - } else { - Logger::warn(mSource) - << "unexpected string pool." - << std::endl; - } - break; - - case android::RES_TABLE_TYPE_SPEC_TYPE: - if (!parseTypeSpec(parser.getChunk())) { - return false; - } - break; - - case android::RES_TABLE_TYPE_TYPE: - if (!parseType(parser.getChunk())) { - return false; - } - break; - - case RES_TABLE_PUBLIC_TYPE: - if (!parsePublic(parser.getChunk())) { - return false; - } - break; - - default: - Logger::warn(mSource) - << "unexpected chunk of type " - << parser.getChunk()->type - << "." - << std::endl; - break; - } - } - - if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { - Logger::error(mSource) - << "bad package: " - << parser.getLastError() - << "." - << std::endl; - return false; - } - - // Now go through the table and change resource ID references to - // symbolic references. - - ReferenceIdToNameVisitor visitor(mResolver, &mIdIndex); - for (auto& type : *mTable) { - for (auto& entry : type->entries) { - for (auto& configValue : entry->values) { - configValue.value->accept(visitor, {}); - } - } - } - return true; -} - -bool BinaryResourceParser::parsePublic(const ResChunk_header* chunk) { - const Public_header* header = convertTo<Public_header>(chunk); - - if (header->typeId == 0) { - Logger::error(mSource) - << "invalid type ID " << header->typeId << std::endl; - return false; - } - - const ResourceType* parsedType = parseResourceType(util::getString(mTypePool, - header->typeId - 1)); - if (!parsedType) { - Logger::error(mSource) - << "invalid type " << util::getString(mTypePool, header->typeId - 1) << std::endl; - return false; - } - - const uintptr_t chunkEnd = reinterpret_cast<uintptr_t>(chunk) + chunk->size; - const Public_entry* entry = reinterpret_cast<const Public_entry*>( - getChunkData(header->header)); - for (uint32_t i = 0; i < header->count; i++) { - if (reinterpret_cast<uintptr_t>(entry) + sizeof(*entry) > chunkEnd) { - Logger::error(mSource) - << "Public_entry extends beyond chunk." - << std::endl; - return false; - } - - const ResourceId resId = { mTable->getPackageId(), header->typeId, entry->entryId }; - const ResourceName name = { - mTable->getPackage(), - *parsedType, - util::getString(mKeyPool, entry->key.index).toString() }; - - SourceLine source; - if (mSourcePool.getError() == NO_ERROR) { - source.path = util::utf16ToUtf8(util::getString(mSourcePool, entry->source.index)); - source.line = entry->sourceLine; - } - - if (!mTable->markPublicAllowMangled(name, resId, source)) { - return false; - } - - // Add this resource name->id mapping to the index so - // that we can resolve all ID references to name references. - auto cacheIter = mIdIndex.find(resId); - if (cacheIter == mIdIndex.end()) { - mIdIndex.insert({ resId, name }); - } - - entry++; - } - return true; -} - -bool BinaryResourceParser::parseTypeSpec(const ResChunk_header* chunk) { - if (mTypePool.getError() != NO_ERROR) { - Logger::error(mSource) - << "no type string pool available for ResTable_typeSpec." - << std::endl; - return false; - } - - const ResTable_typeSpec* typeSpec = convertTo<ResTable_typeSpec>(chunk); - if (!typeSpec) { - Logger::error(mSource) - << "could not parse chunk as ResTable_typeSpec." - << std::endl; - return false; - } - - if (typeSpec->id == 0) { - Logger::error(mSource) - << "ResTable_typeSpec has invalid id: " - << typeSpec->id - << "." - << std::endl; - return false; - } - return true; -} - -bool BinaryResourceParser::parseType(const ResChunk_header* chunk) { - if (mTypePool.getError() != NO_ERROR) { - Logger::error(mSource) - << "no type string pool available for ResTable_typeSpec." - << std::endl; - return false; - } - - if (mKeyPool.getError() != NO_ERROR) { - Logger::error(mSource) - << "no key string pool available for ResTable_type." - << std::endl; - return false; - } - - const ResTable_type* type = convertTo<ResTable_type>(chunk); - if (!type) { - Logger::error(mSource) - << "could not parse chunk as ResTable_type." - << std::endl; - return false; - } - - if (type->id == 0) { - Logger::error(mSource) - << "ResTable_type has invalid id: " - << type->id - << "." - << std::endl; - return false; - } - - const ConfigDescription config(type->config); - const StringPiece16 typeName = util::getString(mTypePool, type->id - 1); - - const ResourceType* parsedType = parseResourceType(typeName); - if (!parsedType) { - Logger::error(mSource) - << "invalid type name '" - << typeName - << "' for type with ID " - << uint32_t(type->id) - << "." << std::endl; - return false; - } - - android::TypeVariant tv(type); - for (auto it = tv.beginEntries(); it != tv.endEntries(); ++it) { - if (!*it) { - continue; - } - - const ResTable_entry* entry = *it; - const ResourceName name = { - mTable->getPackage(), - *parsedType, - util::getString(mKeyPool, entry->key.index).toString() - }; - - const ResourceId resId = { mTable->getPackageId(), type->id, it.index() }; - - std::unique_ptr<Value> resourceValue; - const ResTable_entry_source* sourceBlock = nullptr; - if (entry->flags & ResTable_entry::FLAG_COMPLEX) { - const ResTable_map_entry* mapEntry = static_cast<const ResTable_map_entry*>(entry); - if (mapEntry->size - sizeof(*mapEntry) == sizeof(*sourceBlock)) { - const uint8_t* data = reinterpret_cast<const uint8_t*>(mapEntry); - data += mapEntry->size - sizeof(*sourceBlock); - sourceBlock = reinterpret_cast<const ResTable_entry_source*>(data); - } - - // TODO(adamlesinski): Check that the entry count is valid. - resourceValue = parseMapEntry(name, config, mapEntry); - } else { - if (entry->size - sizeof(*entry) == sizeof(*sourceBlock)) { - const uint8_t* data = reinterpret_cast<const uint8_t*>(entry); - data += entry->size - sizeof(*sourceBlock); - sourceBlock = reinterpret_cast<const ResTable_entry_source*>(data); - } - - const Res_value* value = reinterpret_cast<const Res_value*>( - reinterpret_cast<const uint8_t*>(entry) + entry->size); - resourceValue = parseValue(name, config, value, entry->flags); - } - - if (!resourceValue) { - // TODO(adamlesinski): For now this is ok, but it really shouldn't be. - continue; - } - - SourceLine source = mSource.line(0); - if (sourceBlock) { - size_t len; - const char* str = mSourcePool.string8At(sourceBlock->pathIndex, &len); - if (str) { - source.path.assign(str, len); - } - source.line = sourceBlock->line; - } - - if (!mTable->addResourceAllowMangled(name, config, source, std::move(resourceValue))) { - return false; - } - - if ((entry->flags & ResTable_entry::FLAG_PUBLIC) != 0) { - if (!mTable->markPublicAllowMangled(name, resId, mSource.line(0))) { - return false; - } - } - - // Add this resource name->id mapping to the index so - // that we can resolve all ID references to name references. - auto cacheIter = mIdIndex.find(resId); - if (cacheIter == mIdIndex.end()) { - mIdIndex.insert({ resId, name }); - } - } - return true; -} - -std::unique_ptr<Item> BinaryResourceParser::parseValue(const ResourceNameRef& name, - const ConfigDescription& config, - const Res_value* value, - uint16_t flags) { - if (name.type == ResourceType::kId) { - return util::make_unique<Id>(); - } - - if (value->dataType == Res_value::TYPE_STRING) { - StringPiece16 str = util::getString(mValuePool, value->data); - - const ResStringPool_span* spans = mValuePool.styleAt(value->data); - if (spans != nullptr) { - StyleString styleStr = { str.toString() }; - while (spans->name.index != ResStringPool_span::END) { - styleStr.spans.push_back(Span{ - util::getString(mValuePool, spans->name.index).toString(), - spans->firstChar, - spans->lastChar - }); - spans++; - } - return util::make_unique<StyledString>( - mTable->getValueStringPool().makeRef( - styleStr, StringPool::Context{1, config})); - } else { - if (name.type != ResourceType::kString && - util::stringStartsWith<char16_t>(str, u"res/")) { - // This must be a FileReference. - return util::make_unique<FileReference>(mTable->getValueStringPool().makeRef( - str, StringPool::Context{ 0, config })); - } - - // There are no styles associated with this string, so treat it as - // a simple string. - return util::make_unique<String>( - mTable->getValueStringPool().makeRef( - str, StringPool::Context{1, config})); - } - } - - if (value->dataType == Res_value::TYPE_REFERENCE || - value->dataType == Res_value::TYPE_ATTRIBUTE) { - const Reference::Type type = (value->dataType == Res_value::TYPE_REFERENCE) ? - Reference::Type::kResource : Reference::Type::kAttribute; - - if (value->data != 0) { - // This is a normal reference. - return util::make_unique<Reference>(value->data, type); - } - - // This reference has an invalid ID. Check if it is an unresolved symbol. - ResourceNameRef symbol; - if (getSymbol(&value->data, &symbol)) { - return util::make_unique<Reference>(symbol, type); - } - - // This is not an unresolved symbol, so it must be the magic @null reference. - Res_value nullType = {}; - nullType.dataType = Res_value::TYPE_REFERENCE; - return util::make_unique<BinaryPrimitive>(nullType); - } - - if (value->dataType == ExtendedTypes::TYPE_RAW_STRING) { - return util::make_unique<RawString>( - mTable->getValueStringPool().makeRef(util::getString(mValuePool, value->data), - StringPool::Context{ 1, config })); - } - - // Treat this as a raw binary primitive. - return util::make_unique<BinaryPrimitive>(*value); -} - -std::unique_ptr<Value> BinaryResourceParser::parseMapEntry(const ResourceNameRef& name, - const ConfigDescription& config, - const ResTable_map_entry* map) { - switch (name.type) { - case ResourceType::kStyle: - return parseStyle(name, config, map); - case ResourceType::kAttr: - return parseAttr(name, config, map); - case ResourceType::kArray: - return parseArray(name, config, map); - case ResourceType::kStyleable: - return parseStyleable(name, config, map); - case ResourceType::kPlurals: - return parsePlural(name, config, map); - default: - break; - } - return {}; -} - -std::unique_ptr<Style> BinaryResourceParser::parseStyle(const ResourceNameRef& name, - const ConfigDescription& config, - const ResTable_map_entry* map) { - std::unique_ptr<Style> style = util::make_unique<Style>(); - if (map->parent.ident == 0) { - // The parent is either not set or it is an unresolved symbol. - // Check to see if it is a symbol. - ResourceNameRef symbol; - if (getSymbol(&map->parent.ident, &symbol)) { - style->parent.name = symbol.toResourceName(); - } - } else { - // The parent is a regular reference to a resource. - style->parent.id = map->parent.ident; - } - - for (const ResTable_map& mapEntry : map) { - style->entries.emplace_back(); - Style::Entry& styleEntry = style->entries.back(); - - if (mapEntry.name.ident == 0) { - // The map entry's key (attribute) is not set. This must be - // a symbol reference, so resolve it. - ResourceNameRef symbol; - bool result = getSymbol(&mapEntry.name.ident, &symbol); - assert(result); - styleEntry.key.name = symbol.toResourceName(); - } else { - // The map entry's key (attribute) is a regular reference. - styleEntry.key.id = mapEntry.name.ident; - } - - // Parse the attribute's value. - styleEntry.value = parseValue(name, config, &mapEntry.value, 0); - assert(styleEntry.value); - } - return style; -} - -std::unique_ptr<Attribute> BinaryResourceParser::parseAttr(const ResourceNameRef& name, - const ConfigDescription& config, - const ResTable_map_entry* map) { - const bool isWeak = (map->flags & ResTable_entry::FLAG_WEAK) != 0; - std::unique_ptr<Attribute> attr = util::make_unique<Attribute>(isWeak); - - // First we must discover what type of attribute this is. Find the type mask. - auto typeMaskIter = std::find_if(begin(map), end(map), [](const ResTable_map& entry) -> bool { - return entry.name.ident == ResTable_map::ATTR_TYPE; - }); - - if (typeMaskIter != end(map)) { - attr->typeMask = typeMaskIter->value.data; - } - - if (attr->typeMask & (ResTable_map::TYPE_ENUM | ResTable_map::TYPE_FLAGS)) { - for (const ResTable_map& mapEntry : map) { - if (Res_INTERNALID(mapEntry.name.ident)) { - continue; - } - - Attribute::Symbol symbol; - symbol.value = mapEntry.value.data; - if (mapEntry.name.ident == 0) { - // The map entry's key (id) is not set. This must be - // a symbol reference, so resolve it. - ResourceNameRef symbolName; - bool result = getSymbol(&mapEntry.name.ident, &symbolName); - assert(result); - symbol.symbol.name = symbolName.toResourceName(); - } else { - // The map entry's key (id) is a regular reference. - symbol.symbol.id = mapEntry.name.ident; - } - - attr->symbols.push_back(std::move(symbol)); - } - } - - // TODO(adamlesinski): Find min, max, i80n, etc attributes. - return attr; -} - -std::unique_ptr<Array> BinaryResourceParser::parseArray(const ResourceNameRef& name, - const ConfigDescription& config, - const ResTable_map_entry* map) { - std::unique_ptr<Array> array = util::make_unique<Array>(); - for (const ResTable_map& mapEntry : map) { - array->items.push_back(parseValue(name, config, &mapEntry.value, 0)); - } - return array; -} - -std::unique_ptr<Styleable> BinaryResourceParser::parseStyleable(const ResourceNameRef& name, - const ConfigDescription& config, - const ResTable_map_entry* map) { - std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); - for (const ResTable_map& mapEntry : map) { - if (mapEntry.name.ident == 0) { - // The map entry's key (attribute) is not set. This must be - // a symbol reference, so resolve it. - ResourceNameRef symbol; - bool result = getSymbol(&mapEntry.name.ident, &symbol); - assert(result); - styleable->entries.emplace_back(symbol); - } else { - // The map entry's key (attribute) is a regular reference. - styleable->entries.emplace_back(mapEntry.name.ident); - } - } - return styleable; -} - -std::unique_ptr<Plural> BinaryResourceParser::parsePlural(const ResourceNameRef& name, - const ConfigDescription& config, - const ResTable_map_entry* map) { - std::unique_ptr<Plural> plural = util::make_unique<Plural>(); - for (const ResTable_map& mapEntry : map) { - std::unique_ptr<Item> item = parseValue(name, config, &mapEntry.value, 0); - - switch (mapEntry.name.ident) { - case android::ResTable_map::ATTR_ZERO: - plural->values[Plural::Zero] = std::move(item); - break; - case android::ResTable_map::ATTR_ONE: - plural->values[Plural::One] = std::move(item); - break; - case android::ResTable_map::ATTR_TWO: - plural->values[Plural::Two] = std::move(item); - break; - case android::ResTable_map::ATTR_FEW: - plural->values[Plural::Few] = std::move(item); - break; - case android::ResTable_map::ATTR_MANY: - plural->values[Plural::Many] = std::move(item); - break; - case android::ResTable_map::ATTR_OTHER: - plural->values[Plural::Other] = std::move(item); - break; - } - } - return plural; -} - -} // namespace aapt diff --git a/tools/aapt2/BindingXmlPullParser.cpp b/tools/aapt2/BindingXmlPullParser.cpp deleted file mode 100644 index 4b7a656deac6..000000000000 --- a/tools/aapt2/BindingXmlPullParser.cpp +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "BindingXmlPullParser.h" -#include "Util.h" - -#include <iostream> -#include <sstream> -#include <string> -#include <vector> - -namespace aapt { - -constexpr const char16_t* kBindingNamespaceUri = u"http://schemas.android.com/apk/binding"; -constexpr const char16_t* kAndroidNamespaceUri = u"http://schemas.android.com/apk/res/android"; -constexpr const char16_t* kVariableTagName = u"variable"; -constexpr const char* kBindingTagPrefix = "android:binding_"; - -BindingXmlPullParser::BindingXmlPullParser(const std::shared_ptr<XmlPullParser>& parser) : - mParser(parser), mOverride(false), mNextTagId(0) { -} - -bool BindingXmlPullParser::readVariableDeclaration() { - VarDecl var; - - const auto endAttrIter = mParser->endAttributes(); - for (auto attrIter = mParser->beginAttributes(); attrIter != endAttrIter; ++attrIter) { - if (!attrIter->namespaceUri.empty()) { - continue; - } - - if (attrIter->name == u"name") { - var.name = util::utf16ToUtf8(attrIter->value); - } else if (attrIter->name == u"type") { - var.type = util::utf16ToUtf8(attrIter->value); - } - } - - XmlPullParser::skipCurrentElement(mParser.get()); - - if (var.name.empty()) { - mLastError = "variable declaration missing name"; - return false; - } - - if (var.type.empty()) { - mLastError = "variable declaration missing type"; - return false; - } - - mVarDecls.push_back(std::move(var)); - return true; -} - -bool BindingXmlPullParser::readExpressions() { - mOverride = true; - std::vector<XmlPullParser::Attribute> expressions; - std::string idValue; - - const auto endAttrIter = mParser->endAttributes(); - for (auto attr = mParser->beginAttributes(); attr != endAttrIter; ++attr) { - if (attr->namespaceUri == kAndroidNamespaceUri && attr->name == u"id") { - idValue = util::utf16ToUtf8(attr->value); - } else { - StringPiece16 value = util::trimWhitespace(attr->value); - if (util::stringStartsWith<char16_t>(value, u"@{") && - util::stringEndsWith<char16_t>(value, u"}")) { - // This is attribute's value is an expression of the form - // @{expression}. We need to capture the expression inside. - expressions.push_back(XmlPullParser::Attribute{ - attr->namespaceUri, - attr->name, - value.substr(2, value.size() - 3).toString() - }); - } else { - // This is a normal attribute, use as is. - mAttributes.emplace_back(*attr); - } - } - } - - // Check if we have any expressions. - if (!expressions.empty()) { - // We have expressions, so let's assign the target a tag number - // and add it to our targets list. - int32_t targetId = mNextTagId++; - mTargets.push_back(Target{ - util::utf16ToUtf8(mParser->getElementName()), - idValue, - targetId, - std::move(expressions) - }); - - std::stringstream numGen; - numGen << kBindingTagPrefix << targetId; - mAttributes.push_back(XmlPullParser::Attribute{ - std::u16string(kAndroidNamespaceUri), - std::u16string(u"tag"), - util::utf8ToUtf16(numGen.str()) - }); - } - return true; -} - -XmlPullParser::Event BindingXmlPullParser::next() { - // Clear old state in preparation for the next event. - mOverride = false; - mAttributes.clear(); - - while (true) { - Event event = mParser->next(); - if (event == Event::kStartElement) { - if (mParser->getElementNamespace().empty() && - mParser->getElementName() == kVariableTagName) { - // This is a variable tag. Record data from it, and - // then discard the entire element. - if (!readVariableDeclaration()) { - // mLastError is set, so getEvent will return kBadDocument. - return getEvent(); - } - continue; - } else { - // Check for expressions of the form @{} in attribute text. - const auto endAttrIter = mParser->endAttributes(); - for (auto attr = mParser->beginAttributes(); attr != endAttrIter; ++attr) { - StringPiece16 value = util::trimWhitespace(attr->value); - if (util::stringStartsWith<char16_t>(value, u"@{") && - util::stringEndsWith<char16_t>(value, u"}")) { - if (!readExpressions()) { - return getEvent(); - } - break; - } - } - } - } else if (event == Event::kStartNamespace || event == Event::kEndNamespace) { - if (mParser->getNamespaceUri() == kBindingNamespaceUri) { - // Skip binding namespace tags. - continue; - } - } - return event; - } - return Event::kBadDocument; -} - -bool BindingXmlPullParser::writeToFile(std::ostream& out) const { - out << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; - out << "<Layout directory=\"\" layout=\"\" layoutId=\"\">\n"; - - // Write the variables. - out << " <Variables>\n"; - for (const VarDecl& v : mVarDecls) { - out << " <entries name=\"" << v.name << "\" type=\"" << v.type << "\"/>\n"; - } - out << " </Variables>\n"; - - // Write the imports. - - std::stringstream tagGen; - - // Write the targets. - out << " <Targets>\n"; - for (const Target& t : mTargets) { - tagGen.str({}); - tagGen << kBindingTagPrefix << t.tagId; - out << " <Target boundClass=\"" << t.className << "\" id=\"" << t.id - << "\" tag=\"" << tagGen.str() << "\">\n"; - out << " <Expressions>\n"; - for (const XmlPullParser::Attribute& a : t.expressions) { - out << " <Expression attribute=\"" << a.namespaceUri << ":" << a.name - << "\" text=\"" << a.value << "\"/>\n"; - } - out << " </Expressions>\n"; - out << " </Target>\n"; - } - out << " </Targets>\n"; - - out << "</Layout>\n"; - return bool(out); -} - -XmlPullParser::const_iterator BindingXmlPullParser::beginAttributes() const { - if (mOverride) { - return mAttributes.begin(); - } - return mParser->beginAttributes(); -} - -XmlPullParser::const_iterator BindingXmlPullParser::endAttributes() const { - if (mOverride) { - return mAttributes.end(); - } - return mParser->endAttributes(); -} - -size_t BindingXmlPullParser::getAttributeCount() const { - if (mOverride) { - return mAttributes.size(); - } - return mParser->getAttributeCount(); -} - -XmlPullParser::Event BindingXmlPullParser::getEvent() const { - if (!mLastError.empty()) { - return Event::kBadDocument; - } - return mParser->getEvent(); -} - -const std::string& BindingXmlPullParser::getLastError() const { - if (!mLastError.empty()) { - return mLastError; - } - return mParser->getLastError(); -} - -const std::u16string& BindingXmlPullParser::getComment() const { - return mParser->getComment(); -} - -size_t BindingXmlPullParser::getLineNumber() const { - return mParser->getLineNumber(); -} - -size_t BindingXmlPullParser::getDepth() const { - return mParser->getDepth(); -} - -const std::u16string& BindingXmlPullParser::getText() const { - return mParser->getText(); -} - -const std::u16string& BindingXmlPullParser::getNamespacePrefix() const { - return mParser->getNamespacePrefix(); -} - -const std::u16string& BindingXmlPullParser::getNamespaceUri() const { - return mParser->getNamespaceUri(); -} - -bool BindingXmlPullParser::applyPackageAlias(std::u16string* package, - const std::u16string& defaultPackage) const { - return mParser->applyPackageAlias(package, defaultPackage); -} - -const std::u16string& BindingXmlPullParser::getElementNamespace() const { - return mParser->getElementNamespace(); -} - -const std::u16string& BindingXmlPullParser::getElementName() const { - return mParser->getElementName(); -} - -} // namespace aapt diff --git a/tools/aapt2/BindingXmlPullParser.h b/tools/aapt2/BindingXmlPullParser.h deleted file mode 100644 index cfb16ef477c9..000000000000 --- a/tools/aapt2/BindingXmlPullParser.h +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_BINDING_XML_PULL_PARSER_H -#define AAPT_BINDING_XML_PULL_PARSER_H - -#include "XmlPullParser.h" - -#include <iostream> -#include <memory> -#include <string> - -namespace aapt { - -class BindingXmlPullParser : public XmlPullParser { -public: - BindingXmlPullParser(const std::shared_ptr<XmlPullParser>& parser); - BindingXmlPullParser(const BindingXmlPullParser& rhs) = delete; - - Event getEvent() const override; - const std::string& getLastError() const override; - Event next() override; - - const std::u16string& getComment() const override; - size_t getLineNumber() const override; - size_t getDepth() const override; - - const std::u16string& getText() const override; - - const std::u16string& getNamespacePrefix() const override; - const std::u16string& getNamespaceUri() const override; - bool applyPackageAlias(std::u16string* package, const std::u16string& defaultPackage) - const override; - - const std::u16string& getElementNamespace() const override; - const std::u16string& getElementName() const override; - - const_iterator beginAttributes() const override; - const_iterator endAttributes() const override; - size_t getAttributeCount() const override; - - bool writeToFile(std::ostream& out) const; - -private: - struct VarDecl { - std::string name; - std::string type; - }; - - struct Import { - std::string name; - std::string type; - }; - - struct Target { - std::string className; - std::string id; - int32_t tagId; - - std::vector<XmlPullParser::Attribute> expressions; - }; - - bool readVariableDeclaration(); - bool readExpressions(); - - std::shared_ptr<XmlPullParser> mParser; - std::string mLastError; - bool mOverride; - std::vector<XmlPullParser::Attribute> mAttributes; - std::vector<VarDecl> mVarDecls; - std::vector<Target> mTargets; - int32_t mNextTagId; -}; - -} // namespace aapt - -#endif // AAPT_BINDING_XML_PULL_PARSER_H diff --git a/tools/aapt2/BindingXmlPullParser_test.cpp b/tools/aapt2/BindingXmlPullParser_test.cpp deleted file mode 100644 index 28edcb672840..000000000000 --- a/tools/aapt2/BindingXmlPullParser_test.cpp +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "SourceXmlPullParser.h" -#include "BindingXmlPullParser.h" - -#include <gtest/gtest.h> -#include <sstream> -#include <string> - -namespace aapt { - -constexpr const char16_t* kAndroidNamespaceUri = u"http://schemas.android.com/apk/res/android"; - -TEST(BindingXmlPullParserTest, SubstituteBindingExpressionsWithTag) { - std::stringstream input; - input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" - << "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" - << " xmlns:bind=\"http://schemas.android.com/apk/binding\"\n" - << " android:id=\"@+id/content\">\n" - << " <variable name=\"user\" type=\"com.android.test.User\"/>\n" - << " <TextView android:text=\"@{user.name}\" android:layout_width=\"wrap_content\"\n" - << " android:layout_height=\"wrap_content\"/>\n" - << "</LinearLayout>\n"; - std::shared_ptr<XmlPullParser> sourceParser = std::make_shared<SourceXmlPullParser>(input); - BindingXmlPullParser parser(sourceParser); - - ASSERT_EQ(XmlPullParser::Event::kStartNamespace, parser.next()); - EXPECT_EQ(std::u16string(u"http://schemas.android.com/apk/res/android"), - parser.getNamespaceUri()); - - ASSERT_EQ(XmlPullParser::Event::kStartElement, parser.next()); - EXPECT_EQ(std::u16string(u"LinearLayout"), parser.getElementName()); - - while (parser.next() == XmlPullParser::Event::kText) {} - - ASSERT_EQ(XmlPullParser::Event::kStartElement, parser.getEvent()); - EXPECT_EQ(std::u16string(u"TextView"), parser.getElementName()); - - ASSERT_EQ(3u, parser.getAttributeCount()); - const auto endAttr = parser.endAttributes(); - EXPECT_NE(endAttr, parser.findAttribute(kAndroidNamespaceUri, u"layout_width")); - EXPECT_NE(endAttr, parser.findAttribute(kAndroidNamespaceUri, u"layout_height")); - EXPECT_NE(endAttr, parser.findAttribute(kAndroidNamespaceUri, u"tag")); - - while (parser.next() == XmlPullParser::Event::kText) {} - - ASSERT_EQ(XmlPullParser::Event::kEndElement, parser.getEvent()); - - while (parser.next() == XmlPullParser::Event::kText) {} - - ASSERT_EQ(XmlPullParser::Event::kEndElement, parser.getEvent()); - ASSERT_EQ(XmlPullParser::Event::kEndNamespace, parser.next()); -} - -TEST(BindingXmlPullParserTest, GenerateVariableDeclarations) { - std::stringstream input; - input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" - << "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" - << " xmlns:bind=\"http://schemas.android.com/apk/binding\"\n" - << " android:id=\"@+id/content\">\n" - << " <variable name=\"user\" type=\"com.android.test.User\"/>\n" - << "</LinearLayout>\n"; - std::shared_ptr<XmlPullParser> sourceParser = std::make_shared<SourceXmlPullParser>(input); - BindingXmlPullParser parser(sourceParser); - - while (XmlPullParser::isGoodEvent(parser.next())) { - ASSERT_NE(XmlPullParser::Event::kBadDocument, parser.getEvent()); - } - - std::stringstream output; - ASSERT_TRUE(parser.writeToFile(output)); - - std::string result = output.str(); - EXPECT_NE(std::string::npos, - result.find("<entries name=\"user\" type=\"com.android.test.User\"/>")); -} - -TEST(BindingXmlPullParserTest, FailOnMissingNameOrTypeInVariableDeclaration) { - std::stringstream input; - input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" - << "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" - << " xmlns:bind=\"http://schemas.android.com/apk/binding\"\n" - << " android:id=\"@+id/content\">\n" - << " <variable name=\"user\"/>\n" - << "</LinearLayout>\n"; - std::shared_ptr<XmlPullParser> sourceParser = std::make_shared<SourceXmlPullParser>(input); - BindingXmlPullParser parser(sourceParser); - - while (XmlPullParser::isGoodEvent(parser.next())) {} - - EXPECT_EQ(XmlPullParser::Event::kBadDocument, parser.getEvent()); - EXPECT_FALSE(parser.getLastError().empty()); -} - - -} // namespace aapt diff --git a/tools/aapt2/ConfigDescription.cpp b/tools/aapt2/ConfigDescription.cpp index 6ddf94a681b8..13f8b3b54f68 100644 --- a/tools/aapt2/ConfigDescription.cpp +++ b/tools/aapt2/ConfigDescription.cpp @@ -17,8 +17,8 @@ #include "ConfigDescription.h" #include "Locale.h" #include "SdkConstants.h" -#include "StringPiece.h" -#include "Util.h" +#include "util/StringPiece.h" +#include "util/Util.h" #include <androidfw/ResourceTypes.h> #include <string> @@ -30,6 +30,11 @@ using android::ResTable_config; static const char* kWildcardName = "any"; +const ConfigDescription& ConfigDescription::defaultConfig() { + static ConfigDescription config = {}; + return config; +} + static bool parseMcc(const char* name, ResTable_config* out) { if (strcmp(name, kWildcardName) == 0) { if (out) out->mcc = 0; @@ -164,6 +169,26 @@ static bool parseScreenLayoutLong(const char* name, ResTable_config* out) { return false; } +static bool parseScreenRound(const char* name, ResTable_config* out) { + if (strcmp(name, kWildcardName) == 0) { + if (out) out->screenLayout2 = + (out->screenLayout2&~ResTable_config::MASK_SCREENROUND) + | ResTable_config::SCREENROUND_ANY; + return true; + } else if (strcmp(name, "round") == 0) { + if (out) out->screenLayout2 = + (out->screenLayout2&~ResTable_config::MASK_SCREENROUND) + | ResTable_config::SCREENROUND_YES; + return true; + } else if (strcmp(name, "notround") == 0) { + if (out) out->screenLayout2 = + (out->screenLayout2&~ResTable_config::MASK_SCREENROUND) + | ResTable_config::SCREENROUND_NO; + return true; + } + return false; +} + static bool parseOrientation(const char* name, ResTable_config* out) { if (strcmp(name, kWildcardName) == 0) { if (out) out->orientation = out->ORIENTATION_ANY; @@ -635,6 +660,13 @@ bool ConfigDescription::parse(const StringPiece& str, ConfigDescription* out) { } } + if (parseScreenRound(partIter->c_str(), &config)) { + ++partIter; + if (partIter == partsEnd) { + goto success; + } + } + if (parseOrientation(partIter->c_str(), &config)) { ++partIter; if (partIter == partsEnd) { @@ -725,7 +757,9 @@ success: void ConfigDescription::applyVersionForCompatibility(ConfigDescription* config) { uint16_t minSdk = 0; - if (config->density == ResTable_config::DENSITY_ANY) { + if (config->screenLayout2 & ResTable_config::MASK_SCREENROUND) { + minSdk = SDK_MARSHMALLOW; + } else if (config->density == ResTable_config::DENSITY_ANY) { minSdk = SDK_LOLLIPOP; } else if (config->smallestScreenWidthDp != ResTable_config::SCREENWIDTH_ANY || config->screenWidthDp != ResTable_config::SCREENWIDTH_ANY diff --git a/tools/aapt2/ConfigDescription.h b/tools/aapt2/ConfigDescription.h index 67b4b75cce0b..5749816f5124 100644 --- a/tools/aapt2/ConfigDescription.h +++ b/tools/aapt2/ConfigDescription.h @@ -17,7 +17,7 @@ #ifndef AAPT_CONFIG_DESCRIPTION_H #define AAPT_CONFIG_DESCRIPTION_H -#include "StringPiece.h" +#include "util/StringPiece.h" #include <androidfw/ResourceTypes.h> #include <ostream> @@ -29,6 +29,11 @@ namespace aapt { * initialization and comparison methods. */ struct ConfigDescription : public android::ResTable_config { + /** + * Returns an immutable default config. + */ + static const ConfigDescription& defaultConfig(); + /* * Parse a string of the form 'fr-sw600dp-land' and fill in the * given ResTable_config with resulting configuration parameters. diff --git a/tools/aapt2/ConfigDescription_test.cpp b/tools/aapt2/ConfigDescription_test.cpp index c57e35191a76..e68d6be536df 100644 --- a/tools/aapt2/ConfigDescription_test.cpp +++ b/tools/aapt2/ConfigDescription_test.cpp @@ -15,7 +15,9 @@ */ #include "ConfigDescription.h" -#include "StringPiece.h" +#include "SdkConstants.h" + +#include "util/StringPiece.h" #include <gtest/gtest.h> #include <string> @@ -79,4 +81,19 @@ TEST(ConfigDescriptionTest, ParseCarAttribute) { EXPECT_EQ(android::ResTable_config::UI_MODE_TYPE_CAR, config.uiMode); } +TEST(ConfigDescriptionTest, TestParsingRoundQualifier) { + ConfigDescription config; + EXPECT_TRUE(TestParse("round", &config)); + EXPECT_EQ(android::ResTable_config::SCREENROUND_YES, + config.screenLayout2 & android::ResTable_config::MASK_SCREENROUND); + EXPECT_EQ(SDK_MARSHMALLOW, config.sdkVersion); + EXPECT_EQ(std::string("round-v23"), config.toString().string()); + + EXPECT_TRUE(TestParse("notround", &config)); + EXPECT_EQ(android::ResTable_config::SCREENROUND_NO, + config.screenLayout2 & android::ResTable_config::MASK_SCREENROUND); + EXPECT_EQ(SDK_MARSHMALLOW, config.sdkVersion); + EXPECT_EQ(std::string("notround-v23"), config.toString().string()); +} + } // namespace aapt diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index cf222c68de55..b4e75f9be3a9 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -17,7 +17,8 @@ #include "Debug.h" #include "ResourceTable.h" #include "ResourceValues.h" -#include "Util.h" +#include "util/Util.h" +#include "ValueVisitor.h" #include <algorithm> #include <iostream> @@ -29,102 +30,124 @@ namespace aapt { -struct PrintVisitor : ConstValueVisitor { - void visit(const Attribute& attr, ValueVisitorArgs&) override { +struct PrintVisitor : public ValueVisitor { + using ValueVisitor::visit; + + void visit(Attribute* attr) override { std::cout << "(attr) type="; - attr.printMask(std::cout); + attr->printMask(&std::cout); static constexpr uint32_t kMask = android::ResTable_map::TYPE_ENUM | android::ResTable_map::TYPE_FLAGS; - if (attr.typeMask & kMask) { - for (const auto& symbol : attr.symbols) { - std::cout << "\n " - << symbol.symbol.name.entry << " (" << symbol.symbol.id << ") = " - << symbol.value; + if (attr->typeMask & kMask) { + for (const auto& symbol : attr->symbols) { + std::cout << "\n " << symbol.symbol.name.value().entry; + if (symbol.symbol.id) { + std::cout << " (" << symbol.symbol.id.value() << ")"; + } + std::cout << " = " << symbol.value; } } } - void visit(const Style& style, ValueVisitorArgs&) override { + void visit(Style* style) override { std::cout << "(style)"; - if (style.parent.name.isValid() || style.parent.id.isValid()) { + if (style->parent) { + const Reference& parentRef = style->parent.value(); std::cout << " parent="; - if (style.parent.name.isValid()) { - std::cout << style.parent.name << " "; + if (parentRef.name) { + if (parentRef.privateReference) { + std::cout << "*"; + } + std::cout << parentRef.name.value() << " "; } - if (style.parent.id.isValid()) { - std::cout << style.parent.id; + if (parentRef.id) { + std::cout << parentRef.id.value(); } } - for (const auto& entry : style.entries) { + for (const auto& entry : style->entries) { std::cout << "\n "; - if (entry.key.name.isValid()) { - std::cout << entry.key.name.package << ":" << entry.key.name.entry; + if (entry.key.name) { + std::cout << entry.key.name.value().package << ":" << entry.key.name.value().entry; } - if (entry.key.id.isValid()) { - std::cout << "(" << entry.key.id << ")"; + if (entry.key.id) { + std::cout << "(" << entry.key.id.value() << ")"; } std::cout << "=" << *entry.value; } } - void visit(const Array& array, ValueVisitorArgs&) override { - array.print(std::cout); + void visit(Array* array) override { + array->print(&std::cout); } - void visit(const Plural& plural, ValueVisitorArgs&) override { - plural.print(std::cout); + void visit(Plural* plural) override { + plural->print(&std::cout); } - void visit(const Styleable& styleable, ValueVisitorArgs&) override { - styleable.print(std::cout); + void visit(Styleable* styleable) override { + styleable->print(&std::cout); } - void visitItem(const Item& item, ValueVisitorArgs& args) override { - item.print(std::cout); + void visitItem(Item* item) override { + item->print(&std::cout); } }; -void Debug::printTable(const std::shared_ptr<ResourceTable>& table) { - std::cout << "Package name=" << table->getPackage(); - if (table->getPackageId() != ResourceTable::kUnsetPackageId) { - std::cout << " id=" << std::hex << table->getPackageId() << std::dec; - } - std::cout << std::endl; - - for (const auto& type : *table) { - std::cout << " type " << type->type; - if (type->typeId != ResourceTableType::kUnsetTypeId) { - std::cout << " id=" << std::hex << type->typeId << std::dec; - } - std::cout << " entryCount=" << type->entries.size() << std::endl; - - std::vector<const ResourceEntry*> sortedEntries; - for (const auto& entry : type->entries) { - auto iter = std::lower_bound(sortedEntries.begin(), sortedEntries.end(), entry.get(), - [](const ResourceEntry* a, const ResourceEntry* b) -> bool { - return a->entryId < b->entryId; - }); - sortedEntries.insert(iter, entry.get()); +void Debug::printTable(ResourceTable* table) { + for (auto& package : table->packages) { + std::cout << "Package name=" << package->name; + if (package->id) { + std::cout << " id=" << std::hex << (int) package->id.value() << std::dec; } + std::cout << std::endl; - for (const ResourceEntry* entry : sortedEntries) { - ResourceId id = { table->getPackageId(), type->typeId, entry->entryId }; - ResourceName name = { table->getPackage(), type->type, entry->name }; - std::cout << " spec resource " << id << " " << name; - if (entry->publicStatus.isPublic) { - std::cout << " PUBLIC"; + for (const auto& type : package->types) { + std::cout << " type " << type->type; + if (type->id) { + std::cout << " id=" << std::hex << (int) type->id.value() << std::dec; + } + std::cout << " entryCount=" << type->entries.size() << std::endl; + + std::vector<const ResourceEntry*> sortedEntries; + for (const auto& entry : type->entries) { + auto iter = std::lower_bound(sortedEntries.begin(), sortedEntries.end(), entry.get(), + [](const ResourceEntry* a, const ResourceEntry* b) -> bool { + if (a->id && b->id) { + return a->id.value() < b->id.value(); + } else if (a->id) { + return true; + } else { + return false; + } + }); + sortedEntries.insert(iter, entry.get()); } - std::cout << std::endl; - PrintVisitor visitor; - for (const auto& value : entry->values) { - std::cout << " (" << value.config << ") "; - value.value->accept(visitor, {}); + for (const ResourceEntry* entry : sortedEntries) { + ResourceId id(package->id ? package->id.value() : uint8_t(0), + type->id ? type->id.value() : uint8_t(0), + entry->id ? entry->id.value() : uint16_t(0)); + ResourceName name(package->name, type->type, entry->name); + + std::cout << " spec resource " << id << " " << name; + switch (entry->symbolStatus.state) { + case SymbolState::kPublic: std::cout << " PUBLIC"; break; + case SymbolState::kPrivate: std::cout << " _PRIVATE_"; break; + default: break; + } + std::cout << std::endl; + + PrintVisitor visitor; + for (const auto& value : entry->values) { + std::cout << " (" << value.config << ") "; + value.value->accept(&visitor); + std::cout << std::endl; + } } } } @@ -136,8 +159,7 @@ static size_t getNodeIndex(const std::vector<ResourceName>& names, const Resourc return std::distance(names.begin(), iter); } -void Debug::printStyleGraph(const std::shared_ptr<ResourceTable>& table, - const ResourceName& targetStyle) { +void Debug::printStyleGraph(ResourceTable* table, const ResourceName& targetStyle) { std::map<ResourceName, std::set<ResourceName>> graph; std::queue<ResourceName> stylesToVisit; @@ -150,17 +172,16 @@ void Debug::printStyleGraph(const std::shared_ptr<ResourceTable>& table, continue; } - const ResourceTableType* type; - const ResourceEntry* entry; - std::tie(type, entry) = table->findResource(styleName); - if (entry) { + Maybe<ResourceTable::SearchResult> result = table->findResource(styleName); + if (result) { + ResourceEntry* entry = result.value().entry; for (const auto& value : entry->values) { - visitFunc<Style>(*value.value, [&](const Style& style) { - if (style.parent.name.isValid()) { - parents.insert(style.parent.name); - stylesToVisit.push(style.parent.name); + if (Style* style = valueCast<Style>(value.value.get())) { + if (style->parent && style->parent.value().name) { + parents.insert(style->parent.value().name.value()); + stylesToVisit.push(style->parent.value().name.value()); } - }); + } } } } @@ -189,4 +210,19 @@ void Debug::printStyleGraph(const std::shared_ptr<ResourceTable>& table, std::cout << "}" << std::endl; } +void Debug::dumpHex(const void* data, size_t len) { + const uint8_t* d = (const uint8_t*) data; + for (size_t i = 0; i < len; i++) { + std::cerr << std::hex << std::setfill('0') << std::setw(2) << (uint32_t) d[i] << " "; + if (i % 8 == 7) { + std::cerr << "\n"; + } + } + + if (len - 1 % 8 != 7) { + std::cerr << std::endl; + } +} + + } // namespace aapt diff --git a/tools/aapt2/Debug.h b/tools/aapt2/Debug.h index cdb3dcb6a5d8..ba05be91e91d 100644 --- a/tools/aapt2/Debug.h +++ b/tools/aapt2/Debug.h @@ -20,14 +20,16 @@ #include "Resource.h" #include "ResourceTable.h" -#include <memory> +// Include for printf-like debugging. +#include <iostream> namespace aapt { struct Debug { - static void printTable(const std::shared_ptr<ResourceTable>& table); - static void printStyleGraph(const std::shared_ptr<ResourceTable>& table, + static void printTable(ResourceTable* table); + static void printStyleGraph(ResourceTable* table, const ResourceName& targetStyle); + static void dumpHex(const void* data, size_t len); }; } // namespace aapt diff --git a/tools/aapt2/Diagnostics.h b/tools/aapt2/Diagnostics.h new file mode 100644 index 000000000000..ab4d284516e1 --- /dev/null +++ b/tools/aapt2/Diagnostics.h @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_DIAGNOSTICS_H +#define AAPT_DIAGNOSTICS_H + +#include "Source.h" +#include "util/StringPiece.h" +#include "util/Util.h" + +#include <iostream> +#include <sstream> +#include <string> + +namespace aapt { + +struct DiagMessageActual { + Source source; + std::string message; +}; + +struct DiagMessage { +private: + Source mSource; + std::stringstream mMessage; + +public: + DiagMessage() = default; + + DiagMessage(const StringPiece& src) : mSource(src) { + } + + DiagMessage(const Source& src) : mSource(src) { + } + + template <typename T> DiagMessage& operator<<(const T& value) { + mMessage << value; + return *this; + } + + DiagMessageActual build() const { + return DiagMessageActual{ mSource, mMessage.str() }; + } +}; + +struct IDiagnostics { + virtual ~IDiagnostics() = default; + + virtual void error(const DiagMessage& message) = 0; + virtual void warn(const DiagMessage& message) = 0; + virtual void note(const DiagMessage& message) = 0; +}; + +struct StdErrDiagnostics : public IDiagnostics { + size_t mNumErrors = 0; + + void emit(const DiagMessage& msg, const char* tag) { + DiagMessageActual actual = msg.build(); + if (!actual.source.path.empty()) { + std::cerr << actual.source << ": "; + } + std::cerr << tag << actual.message << "." << std::endl; + } + + void error(const DiagMessage& msg) override { + if (mNumErrors < 20) { + emit(msg, "error: "); + } + mNumErrors++; + } + + void warn(const DiagMessage& msg) override { + emit(msg, "warn: "); + } + + void note(const DiagMessage& msg) override { + emit(msg, "note: "); + } +}; + +} // namespace aapt + +#endif /* AAPT_DIAGNOSTICS_H */ diff --git a/tools/aapt2/Flag.cpp b/tools/aapt2/Flag.cpp deleted file mode 100644 index 76985da99912..000000000000 --- a/tools/aapt2/Flag.cpp +++ /dev/null @@ -1,132 +0,0 @@ -#include "Flag.h" -#include "StringPiece.h" - -#include <functional> -#include <iomanip> -#include <iostream> -#include <string> -#include <vector> - -namespace aapt { -namespace flag { - -struct Flag { - std::string name; - std::string description; - std::function<bool(const StringPiece&, std::string*)> action; - bool required; - bool* flagResult; - bool flagValueWhenSet; - bool parsed; -}; - -static std::vector<Flag> sFlags; -static std::vector<std::string> sArgs; - -static std::function<bool(const StringPiece&, std::string*)> wrap( - const std::function<void(const StringPiece&)>& action) { - return [action](const StringPiece& arg, std::string*) -> bool { - action(arg); - return true; - }; -} - -void optionalFlag(const StringPiece& name, const StringPiece& description, - std::function<void(const StringPiece&)> action) { - sFlags.push_back(Flag{ - name.toString(), description.toString(), wrap(action), - false, nullptr, false, false }); -} - -void requiredFlag(const StringPiece& name, const StringPiece& description, - std::function<void(const StringPiece&)> action) { - sFlags.push_back(Flag{ name.toString(), description.toString(), wrap(action), - true, nullptr, false, false }); -} - -void requiredFlag(const StringPiece& name, const StringPiece& description, - std::function<bool(const StringPiece&, std::string*)> action) { - sFlags.push_back(Flag{ name.toString(), description.toString(), action, - true, nullptr, false, false }); -} - -void optionalSwitch(const StringPiece& name, const StringPiece& description, bool resultWhenSet, - bool* result) { - sFlags.push_back(Flag{ - name.toString(), description.toString(), {}, - false, result, resultWhenSet, false }); -} - -void usageAndDie(const StringPiece& command) { - std::cerr << command << " [options]"; - for (const Flag& flag : sFlags) { - if (flag.required) { - std::cerr << " " << flag.name << " arg"; - } - } - std::cerr << " files..." << std::endl << std::endl << "Options:" << std::endl; - - for (const Flag& flag : sFlags) { - std::string command = flag.name; - if (!flag.flagResult) { - command += " arg "; - } - std::cerr << " " << std::setw(30) << std::left << command - << flag.description << std::endl; - } - exit(1); -} - -void parse(int argc, char** argv, const StringPiece& command) { - std::string errorStr; - for (int i = 0; i < argc; i++) { - const StringPiece arg(argv[i]); - if (*arg.data() != '-') { - sArgs.push_back(arg.toString()); - continue; - } - - bool match = false; - for (Flag& flag : sFlags) { - if (arg == flag.name) { - match = true; - flag.parsed = true; - if (flag.flagResult) { - *flag.flagResult = flag.flagValueWhenSet; - } else { - i++; - if (i >= argc) { - std::cerr << flag.name << " missing argument." << std::endl - << std::endl; - usageAndDie(command); - } - - if (!flag.action(argv[i], &errorStr)) { - std::cerr << errorStr << "." << std::endl << std::endl; - usageAndDie(command); - } - } - break; - } - } - - if (!match) { - std::cerr << "unknown option '" << arg << "'." << std::endl << std::endl; - usageAndDie(command); - } - } - - for (const Flag& flag : sFlags) { - if (flag.required && !flag.parsed) { - std::cerr << "missing required flag " << flag.name << std::endl << std::endl; - usageAndDie(command); - } - } -} - -const std::vector<std::string>& getArgs() { - return sArgs; -} - -} // namespace flag -} // namespace aapt diff --git a/tools/aapt2/Flag.h b/tools/aapt2/Flag.h deleted file mode 100644 index e86374283986..000000000000 --- a/tools/aapt2/Flag.h +++ /dev/null @@ -1,34 +0,0 @@ -#ifndef AAPT_FLAG_H -#define AAPT_FLAG_H - -#include "StringPiece.h" - -#include <functional> -#include <string> -#include <vector> - -namespace aapt { -namespace flag { - -void requiredFlag(const StringPiece& name, const StringPiece& description, - std::function<void(const StringPiece&)> action); - -void requiredFlag(const StringPiece& name, const StringPiece& description, - std::function<bool(const StringPiece&, std::string*)> action); - -void optionalFlag(const StringPiece& name, const StringPiece& description, - std::function<void(const StringPiece&)> action); - -void optionalSwitch(const StringPiece& name, const StringPiece& description, bool resultWhenSet, - bool* result); - -void usageAndDie(const StringPiece& command); - -void parse(int argc, char** argv, const StringPiece& command); - -const std::vector<std::string>& getArgs(); - -} // namespace flag -} // namespace aapt - -#endif // AAPT_FLAG_H diff --git a/tools/aapt2/Flags.cpp b/tools/aapt2/Flags.cpp new file mode 100644 index 000000000000..666e8a8efff1 --- /dev/null +++ b/tools/aapt2/Flags.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Flags.h" +#include "util/StringPiece.h" +#include "util/Util.h" + +#include <iomanip> +#include <iostream> +#include <string> +#include <vector> + +namespace aapt { + +Flags& Flags::requiredFlag(const StringPiece& name, const StringPiece& description, + std::string* value) { + auto func = [value](const StringPiece& arg) -> bool { + *value = arg.toString(); + return true; + }; + + mFlags.push_back(Flag{ name.toString(), description.toString(), func, true, 1, false}); + return *this; +} + +Flags& Flags::requiredFlagList(const StringPiece& name, const StringPiece& description, + std::vector<std::string>* value) { + auto func = [value](const StringPiece& arg) -> bool { + value->push_back(arg.toString()); + return true; + }; + + mFlags.push_back(Flag{ name.toString(), description.toString(), func, true, 1, false }); + return *this; +} + +Flags& Flags::optionalFlag(const StringPiece& name, const StringPiece& description, + Maybe<std::string>* value) { + auto func = [value](const StringPiece& arg) -> bool { + *value = arg.toString(); + return true; + }; + + mFlags.push_back(Flag{ name.toString(), description.toString(), func, false, 1, false }); + return *this; +} + +Flags& Flags::optionalFlagList(const StringPiece& name, const StringPiece& description, + std::vector<std::string>* value) { + auto func = [value](const StringPiece& arg) -> bool { + value->push_back(arg.toString()); + return true; + }; + + mFlags.push_back(Flag{ name.toString(), description.toString(), func, false, 1, false }); + return *this; +} + +Flags& Flags::optionalSwitch(const StringPiece& name, const StringPiece& description, + bool* value) { + auto func = [value](const StringPiece& arg) -> bool { + *value = true; + return true; + }; + + mFlags.push_back(Flag{ name.toString(), description.toString(), func, false, 0, false }); + return *this; +} + +void Flags::usage(const StringPiece& command, std::ostream* out) { + constexpr size_t kWidth = 50; + + *out << command << " [options]"; + for (const Flag& flag : mFlags) { + if (flag.required) { + *out << " " << flag.name << " arg"; + } + } + + *out << " files...\n\nOptions:\n"; + + for (const Flag& flag : mFlags) { + std::string argLine = flag.name; + if (flag.numArgs > 0) { + argLine += " arg"; + } + + // Split the description by newlines and write out the argument (which is empty after + // the first line) followed by the description line. This will make sure that multiline + // descriptions are still right justified and aligned. + for (StringPiece line : util::tokenize<char>(flag.description, '\n')) { + *out << " " << std::setw(kWidth) << std::left << argLine << line << "\n"; + argLine = " "; + } + } + *out << " " << std::setw(kWidth) << std::left << "-h" << "Displays this help menu\n"; + out->flush(); +} + +bool Flags::parse(const StringPiece& command, const std::vector<StringPiece>& args, + std::ostream* outError) { + for (size_t i = 0; i < args.size(); i++) { + StringPiece arg = args[i]; + if (*(arg.data()) != '-') { + mArgs.push_back(arg.toString()); + continue; + } + + if (arg == "-h" || arg == "--help") { + usage(command, outError); + return false; + } + + bool match = false; + for (Flag& flag : mFlags) { + if (arg == flag.name) { + if (flag.numArgs > 0) { + i++; + if (i >= args.size()) { + *outError << flag.name << " missing argument.\n\n"; + usage(command, outError); + return false; + } + flag.action(args[i]); + } else { + flag.action({}); + } + flag.parsed = true; + match = true; + break; + } + } + + if (!match) { + *outError << "unknown option '" << arg << "'.\n\n"; + usage(command, outError); + return false; + } + } + + for (const Flag& flag : mFlags) { + if (flag.required && !flag.parsed) { + *outError << "missing required flag " << flag.name << "\n\n"; + usage(command, outError); + return false; + } + } + return true; +} + +const std::vector<std::string>& Flags::getArgs() { + return mArgs; +} + +} // namespace aapt diff --git a/tools/aapt2/Flags.h b/tools/aapt2/Flags.h new file mode 100644 index 000000000000..ce7a4857eb6e --- /dev/null +++ b/tools/aapt2/Flags.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_FLAGS_H +#define AAPT_FLAGS_H + +#include "util/Maybe.h" +#include "util/StringPiece.h" + +#include <functional> +#include <ostream> +#include <string> +#include <vector> + +namespace aapt { + +class Flags { +public: + Flags& requiredFlag(const StringPiece& name, const StringPiece& description, + std::string* value); + Flags& requiredFlagList(const StringPiece& name, const StringPiece& description, + std::vector<std::string>* value); + Flags& optionalFlag(const StringPiece& name, const StringPiece& description, + Maybe<std::string>* value); + Flags& optionalFlagList(const StringPiece& name, const StringPiece& description, + std::vector<std::string>* value); + Flags& optionalSwitch(const StringPiece& name, const StringPiece& description, + bool* value); + + void usage(const StringPiece& command, std::ostream* out); + + bool parse(const StringPiece& command, const std::vector<StringPiece>& args, + std::ostream* outError); + + const std::vector<std::string>& getArgs(); + +private: + struct Flag { + std::string name; + std::string description; + std::function<bool(const StringPiece& value)> action; + bool required; + size_t numArgs; + + bool parsed; + }; + + std::vector<Flag> mFlags; + std::vector<std::string> mArgs; +}; + +} // namespace aapt + +#endif // AAPT_FLAGS_H diff --git a/tools/aapt2/JavaClassGenerator.cpp b/tools/aapt2/JavaClassGenerator.cpp deleted file mode 100644 index e2ffe79c764d..000000000000 --- a/tools/aapt2/JavaClassGenerator.cpp +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "JavaClassGenerator.h" -#include "NameMangler.h" -#include "Resource.h" -#include "ResourceTable.h" -#include "ResourceValues.h" -#include "StringPiece.h" - -#include <algorithm> -#include <ostream> -#include <set> -#include <sstream> -#include <tuple> - -namespace aapt { - -// The number of attributes to emit per line in a Styleable array. -constexpr size_t kAttribsPerLine = 4; - -JavaClassGenerator::JavaClassGenerator(const std::shared_ptr<const ResourceTable>& table, - Options options) : - mTable(table), mOptions(options) { -} - -static void generateHeader(std::ostream& out, const StringPiece16& package) { - out << "/* AUTO-GENERATED FILE. DO NOT MODIFY.\n" - " *\n" - " * This class was automatically generated by the\n" - " * aapt tool from the resource data it found. It\n" - " * should not be modified by hand.\n" - " */\n\n"; - out << "package " << package << ";" - << std::endl - << std::endl; -} - -static const std::set<StringPiece16> sJavaIdentifiers = { - u"abstract", u"assert", u"boolean", u"break", u"byte", - u"case", u"catch", u"char", u"class", u"const", u"continue", - u"default", u"do", u"double", u"else", u"enum", u"extends", - u"final", u"finally", u"float", u"for", u"goto", u"if", - u"implements", u"import", u"instanceof", u"int", u"interface", - u"long", u"native", u"new", u"package", u"private", u"protected", - u"public", u"return", u"short", u"static", u"strictfp", u"super", - u"switch", u"synchronized", u"this", u"throw", u"throws", - u"transient", u"try", u"void", u"volatile", u"while", u"true", - u"false", u"null" -}; - -static bool isValidSymbol(const StringPiece16& symbol) { - return sJavaIdentifiers.find(symbol) == sJavaIdentifiers.end(); -} - -/* - * Java symbols can not contain . or -, but those are valid in a resource name. - * Replace those with '_'. - */ -static std::u16string transform(const StringPiece16& symbol) { - std::u16string output = symbol.toString(); - for (char16_t& c : output) { - if (c == u'.' || c == u'-') { - c = u'_'; - } - } - return output; -} - -struct GenArgs : ValueVisitorArgs { - GenArgs(std::ostream* o, const std::u16string* p, std::u16string* e) : - out(o), package(p), entryName(e) { - } - - std::ostream* out; - const std::u16string* package; - std::u16string* entryName; -}; - -void JavaClassGenerator::visit(const Styleable& styleable, ValueVisitorArgs& a) { - const StringPiece finalModifier = mOptions.useFinal ? " final" : ""; - std::ostream* out = static_cast<GenArgs&>(a).out; - const std::u16string* package = static_cast<GenArgs&>(a).package; - std::u16string* entryName = static_cast<GenArgs&>(a).entryName; - - // This must be sorted by resource ID. - std::vector<std::pair<ResourceId, ResourceNameRef>> sortedAttributes; - sortedAttributes.reserve(styleable.entries.size()); - for (const auto& attr : styleable.entries) { - // If we are not encoding final attributes, the styleable entry may have no ID - // if we are building a static library. - assert((!mOptions.useFinal || attr.id.isValid()) && "no ID set for Styleable entry"); - assert(attr.name.isValid() && "no name set for Styleable entry"); - sortedAttributes.emplace_back(attr.id, attr.name); - } - std::sort(sortedAttributes.begin(), sortedAttributes.end()); - - // First we emit the array containing the IDs of each attribute. - *out << " " - << "public static final int[] " << transform(*entryName) << " = {"; - - const size_t attrCount = sortedAttributes.size(); - for (size_t i = 0; i < attrCount; i++) { - if (i % kAttribsPerLine == 0) { - *out << std::endl << " "; - } - - *out << sortedAttributes[i].first; - if (i != attrCount - 1) { - *out << ", "; - } - } - *out << std::endl << " };" << std::endl; - - // Now we emit the indices into the array. - for (size_t i = 0; i < attrCount; i++) { - *out << " " - << "public static" << finalModifier - << " int " << transform(*entryName); - - // We may reference IDs from other packages, so prefix the entry name with - // the package. - const ResourceNameRef& itemName = sortedAttributes[i].second; - if (itemName.package != *package) { - *out << "_" << transform(itemName.package); - } - *out << "_" << transform(itemName.entry) << " = " << i << ";" << std::endl; - } -} - -bool JavaClassGenerator::generateType(const std::u16string& package, size_t packageId, - const ResourceTableType& type, std::ostream& out) { - const StringPiece finalModifier = mOptions.useFinal ? " final" : ""; - - std::u16string unmangledPackage; - std::u16string unmangledName; - for (const auto& entry : type.entries) { - ResourceId id = { packageId, type.typeId, entry->entryId }; - assert(id.isValid()); - - unmangledName = entry->name; - if (NameMangler::unmangle(&unmangledName, &unmangledPackage)) { - // The entry name was mangled, and we successfully unmangled it. - // Check that we want to emit this symbol. - if (package != unmangledPackage) { - // Skip the entry if it doesn't belong to the package we're writing. - continue; - } - } else { - if (package != mTable->getPackage()) { - // We are processing a mangled package name, - // but this is a non-mangled resource. - continue; - } - } - - if (!isValidSymbol(unmangledName)) { - ResourceNameRef resourceName = { package, type.type, unmangledName }; - std::stringstream err; - err << "invalid symbol name '" << resourceName << "'"; - mError = err.str(); - return false; - } - - if (type.type == ResourceType::kStyleable) { - assert(!entry->values.empty()); - entry->values.front().value->accept(*this, GenArgs{ &out, &package, &unmangledName }); - } else { - out << " " << "public static" << finalModifier - << " int " << transform(unmangledName) << " = " << id << ";" << std::endl; - } - } - return true; -} - -bool JavaClassGenerator::generate(const std::u16string& package, std::ostream& out) { - const size_t packageId = mTable->getPackageId(); - - generateHeader(out, package); - - out << "public final class R {" << std::endl; - - for (const auto& type : *mTable) { - out << " public static final class " << type->type << " {" << std::endl; - if (!generateType(package, packageId, *type, out)) { - return false; - } - out << " }" << std::endl; - } - - out << "}" << std::endl; - return true; -} - -} // namespace aapt diff --git a/tools/aapt2/JavaClassGenerator_test.cpp b/tools/aapt2/JavaClassGenerator_test.cpp deleted file mode 100644 index b385ff4828e1..000000000000 --- a/tools/aapt2/JavaClassGenerator_test.cpp +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "JavaClassGenerator.h" -#include "Linker.h" -#include "MockResolver.h" -#include "ResourceTable.h" -#include "ResourceTableResolver.h" -#include "ResourceValues.h" -#include "Util.h" - -#include <gtest/gtest.h> -#include <sstream> -#include <string> - -namespace aapt { - -struct JavaClassGeneratorTest : public ::testing::Test { - virtual void SetUp() override { - mTable = std::make_shared<ResourceTable>(); - mTable->setPackage(u"android"); - mTable->setPackageId(0x01); - } - - bool addResource(const ResourceNameRef& name, ResourceId id) { - return mTable->addResource(name, id, {}, SourceLine{ "test.xml", 21 }, - util::make_unique<Id>()); - } - - std::shared_ptr<ResourceTable> mTable; -}; - -TEST_F(JavaClassGeneratorTest, FailWhenEntryIsJavaKeyword) { - ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kId, u"class" }, - ResourceId{ 0x01, 0x02, 0x0000 })); - - JavaClassGenerator generator(mTable, {}); - - std::stringstream out; - EXPECT_FALSE(generator.generate(mTable->getPackage(), out)); -} - -TEST_F(JavaClassGeneratorTest, TransformInvalidJavaIdentifierCharacter) { - ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kId, u"hey-man" }, - ResourceId{ 0x01, 0x02, 0x0000 })); - - ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kAttr, u"cool.attr" }, - ResourceId{ 0x01, 0x01, 0x0000 })); - - std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); - Reference ref(ResourceName{ u"android", ResourceType::kAttr, u"cool.attr"}); - ref.id = ResourceId{ 0x01, 0x01, 0x0000 }; - styleable->entries.emplace_back(ref); - - ASSERT_TRUE(mTable->addResource(ResourceName{ {}, ResourceType::kStyleable, u"hey.dude" }, - ResourceId{ 0x01, 0x03, 0x0000 }, {}, - SourceLine{ "test.xml", 21 }, std::move(styleable))); - - JavaClassGenerator generator(mTable, {}); - - std::stringstream out; - EXPECT_TRUE(generator.generate(mTable->getPackage(), out)); - std::string output = out.str(); - - EXPECT_NE(std::string::npos, - output.find("public static final int hey_man = 0x01020000;")); - - EXPECT_NE(std::string::npos, - output.find("public static final int[] hey_dude = {")); - - EXPECT_NE(std::string::npos, - output.find("public static final int hey_dude_cool_attr = 0;")); -} - - -TEST_F(JavaClassGeneratorTest, EmitPackageMangledSymbols) { - ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kId, u"foo" }, - ResourceId{ 0x01, 0x02, 0x0000 })); - ResourceTable table; - table.setPackage(u"com.lib"); - ASSERT_TRUE(table.addResource(ResourceName{ {}, ResourceType::kId, u"test" }, {}, - SourceLine{ "lib.xml", 33 }, util::make_unique<Id>())); - ASSERT_TRUE(mTable->merge(std::move(table))); - - Linker linker(mTable, - std::make_shared<MockResolver>(mTable, std::map<ResourceName, ResourceId>()), - {}); - ASSERT_TRUE(linker.linkAndValidate()); - - JavaClassGenerator generator(mTable, {}); - - std::stringstream out; - EXPECT_TRUE(generator.generate(mTable->getPackage(), out)); - std::string output = out.str(); - EXPECT_NE(std::string::npos, output.find("int foo =")); - EXPECT_EQ(std::string::npos, output.find("int test =")); - - out.str(""); - EXPECT_TRUE(generator.generate(u"com.lib", out)); - output = out.str(); - EXPECT_NE(std::string::npos, output.find("int test =")); - EXPECT_EQ(std::string::npos, output.find("int foo =")); -} - -TEST_F(JavaClassGeneratorTest, EmitOtherPackagesAttributesInStyleable) { - std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); - styleable->entries.emplace_back(ResourceNameRef{ mTable->getPackage(), - ResourceType::kAttr, - u"bar" }); - styleable->entries.emplace_back(ResourceNameRef{ u"com.lib", ResourceType::kAttr, u"bar" }); - ASSERT_TRUE(mTable->addResource(ResourceName{ {}, ResourceType::kStyleable, u"Foo" }, {}, {}, - std::move(styleable))); - - std::shared_ptr<IResolver> resolver = std::make_shared<MockResolver>(mTable, - std::map<ResourceName, ResourceId>({ - { ResourceName{ u"android", ResourceType::kAttr, u"bar" }, - ResourceId{ 0x01, 0x01, 0x0000 } }, - { ResourceName{ u"com.lib", ResourceType::kAttr, u"bar" }, - ResourceId{ 0x02, 0x01, 0x0000 } }})); - - Linker linker(mTable, resolver, {}); - ASSERT_TRUE(linker.linkAndValidate()); - - JavaClassGenerator generator(mTable, {}); - - std::stringstream out; - EXPECT_TRUE(generator.generate(mTable->getPackage(), out)); - std::string output = out.str(); - EXPECT_NE(std::string::npos, output.find("int Foo_bar =")); - EXPECT_NE(std::string::npos, output.find("int Foo_com_lib_bar =")); -} - -} // namespace aapt diff --git a/tools/aapt2/Linker.cpp b/tools/aapt2/Linker.cpp deleted file mode 100644 index c37cc932cd3b..000000000000 --- a/tools/aapt2/Linker.cpp +++ /dev/null @@ -1,290 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Linker.h" -#include "Logger.h" -#include "NameMangler.h" -#include "Resolver.h" -#include "ResourceParser.h" -#include "ResourceTable.h" -#include "ResourceValues.h" -#include "StringPiece.h" -#include "Util.h" - -#include <androidfw/AssetManager.h> -#include <array> -#include <bitset> -#include <iostream> -#include <map> -#include <ostream> -#include <set> -#include <sstream> -#include <tuple> -#include <vector> - -namespace aapt { - -Linker::Args::Args(const ResourceNameRef& r, const SourceLine& s) : referrer(r), source(s) { -} - -Linker::Linker(const std::shared_ptr<ResourceTable>& table, - const std::shared_ptr<IResolver>& resolver, const Options& options) : - mResolver(resolver), mTable(table), mOptions(options), mError(false) { -} - -bool Linker::linkAndValidate() { - std::bitset<256> usedTypeIds; - std::array<std::set<uint16_t>, 256> usedIds; - usedTypeIds.set(0); - - // Collect which resource IDs are already taken. - for (auto& type : *mTable) { - if (type->typeId != ResourceTableType::kUnsetTypeId) { - // The ID for this type has already been set. We - // mark this ID as taken so we don't re-assign it - // later. - usedTypeIds.set(type->typeId); - } - - for (auto& entry : type->entries) { - if (type->typeId != ResourceTableType::kUnsetTypeId && - entry->entryId != ResourceEntry::kUnsetEntryId) { - // The ID for this entry has already been set. We - // mark this ID as taken so we don't re-assign it - // later. - usedIds[type->typeId].insert(entry->entryId); - } - } - } - - // Assign resource IDs that are available. - size_t nextTypeIndex = 0; - for (auto& type : *mTable) { - if (type->typeId == ResourceTableType::kUnsetTypeId) { - while (nextTypeIndex < usedTypeIds.size() && usedTypeIds[nextTypeIndex]) { - nextTypeIndex++; - } - type->typeId = nextTypeIndex++; - } - - const auto endEntryIter = std::end(usedIds[type->typeId]); - auto nextEntryIter = std::begin(usedIds[type->typeId]); - size_t nextIndex = 0; - for (auto& entry : type->entries) { - if (entry->entryId == ResourceTableType::kUnsetTypeId) { - while (nextEntryIter != endEntryIter && - nextIndex == *nextEntryIter) { - nextIndex++; - ++nextEntryIter; - } - entry->entryId = nextIndex++; - } - } - } - - // Now do reference linking. - for (auto& type : *mTable) { - for (auto& entry : type->entries) { - if (entry->publicStatus.isPublic && entry->values.empty()) { - // A public resource has no values. It will not be encoded - // properly without a symbol table. This is a unresolved symbol. - addUnresolvedSymbol(ResourceNameRef{ - mTable->getPackage(), type->type, entry->name }, - entry->publicStatus.source); - continue; - } - - for (auto& valueConfig : entry->values) { - // Dispatch to the right method of this linker - // based on the value's type. - valueConfig.value->accept(*this, Args{ - ResourceNameRef{ mTable->getPackage(), type->type, entry->name }, - valueConfig.source - }); - } - } - } - return !mError; -} - -const Linker::ResourceNameToSourceMap& Linker::getUnresolvedReferences() const { - return mUnresolvedSymbols; -} - -void Linker::doResolveReference(Reference& reference, const SourceLine& source) { - Maybe<ResourceId> result = mResolver->findId(reference.name); - if (!result) { - addUnresolvedSymbol(reference.name, source); - return; - } - assert(result.value().isValid()); - - if (mOptions.linkResourceIds) { - reference.id = result.value(); - } else { - reference.id = 0; - } -} - -const Attribute* Linker::doResolveAttribute(Reference& attribute, const SourceLine& source) { - Maybe<IResolver::Entry> result = mResolver->findAttribute(attribute.name); - if (!result || !result.value().attr) { - addUnresolvedSymbol(attribute.name, source); - return nullptr; - } - - const IResolver::Entry& entry = result.value(); - assert(entry.id.isValid()); - - if (mOptions.linkResourceIds) { - attribute.id = entry.id; - } else { - attribute.id = 0; - } - return entry.attr; -} - -void Linker::visit(Reference& reference, ValueVisitorArgs& a) { - Args& args = static_cast<Args&>(a); - - if (reference.name.entry.empty()) { - // We can't have a completely bad reference. - if (!reference.id.isValid()) { - Logger::error() << "srsly? " << args.referrer << std::endl; - assert(reference.id.isValid()); - } - - // This reference has no name but has an ID. - // It is a really bad error to have no name and have the same - // package ID. - assert(reference.id.packageId() != mTable->getPackageId()); - - // The reference goes outside this package, let it stay as a - // resource ID because it will not change. - return; - } - - doResolveReference(reference, args.source); - - // TODO(adamlesinski): Verify the referencedType is another reference - // or a compatible primitive. -} - -void Linker::processAttributeValue(const ResourceNameRef& name, const SourceLine& source, - const Attribute& attr, std::unique_ptr<Item>& value) { - std::unique_ptr<Item> convertedValue; - visitFunc<RawString>(*value, [&](RawString& str) { - // This is a raw string, so check if it can be converted to anything. - // We can NOT swap value with the converted value in here, since - // we called through the original value. - - auto onCreateReference = [&](const ResourceName& name) { - // We should never get here. All references would have been - // parsed in the parser phase. - assert(false); - }; - - convertedValue = ResourceParser::parseItemForAttribute(*str.value, attr, - onCreateReference); - if (!convertedValue && attr.typeMask & android::ResTable_map::TYPE_STRING) { - // Last effort is to parse as a string. - util::StringBuilder builder; - builder.append(*str.value); - if (builder) { - convertedValue = util::make_unique<String>( - mTable->getValueStringPool().makeRef(builder.str())); - } - } - }); - - if (convertedValue) { - value = std::move(convertedValue); - } - - // Process this new or old value (it can be a reference!). - value->accept(*this, Args{ name, source }); - - // Flatten the value to see what resource type it is. - android::Res_value resValue; - value->flatten(resValue); - - // Always allow references. - const uint32_t typeMask = attr.typeMask | android::ResTable_map::TYPE_REFERENCE; - if (!(typeMask & ResourceParser::androidTypeToAttributeTypeMask(resValue.dataType))) { - Logger::error(source) - << *value - << " is not compatible with attribute " - << attr - << "." - << std::endl; - mError = true; - } -} - -void Linker::visit(Style& style, ValueVisitorArgs& a) { - Args& args = static_cast<Args&>(a); - - if (style.parent.name.isValid() || style.parent.id.isValid()) { - visit(style.parent, a); - } - - for (Style::Entry& styleEntry : style.entries) { - const Attribute* attr = doResolveAttribute(styleEntry.key, args.source); - if (attr) { - processAttributeValue(args.referrer, args.source, *attr, styleEntry.value); - } - } -} - -void Linker::visit(Attribute& attr, ValueVisitorArgs& a) { - static constexpr uint32_t kMask = android::ResTable_map::TYPE_ENUM | - android::ResTable_map::TYPE_FLAGS; - if (attr.typeMask & kMask) { - for (auto& symbol : attr.symbols) { - visit(symbol.symbol, a); - } - } -} - -void Linker::visit(Styleable& styleable, ValueVisitorArgs& a) { - for (auto& attrRef : styleable.entries) { - visit(attrRef, a); - } -} - -void Linker::visit(Array& array, ValueVisitorArgs& a) { - Args& args = static_cast<Args&>(a); - - for (auto& item : array.items) { - item->accept(*this, Args{ args.referrer, args.source }); - } -} - -void Linker::visit(Plural& plural, ValueVisitorArgs& a) { - Args& args = static_cast<Args&>(a); - - for (auto& item : plural.values) { - if (item) { - item->accept(*this, Args{ args.referrer, args.source }); - } - } -} - -void Linker::addUnresolvedSymbol(const ResourceNameRef& name, const SourceLine& source) { - mUnresolvedSymbols[name.toResourceName()].push_back(source); -} - -} // namespace aapt diff --git a/tools/aapt2/Linker.h b/tools/aapt2/Linker.h deleted file mode 100644 index 6f0351592fcd..000000000000 --- a/tools/aapt2/Linker.h +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_LINKER_H -#define AAPT_LINKER_H - -#include "Resolver.h" -#include "ResourceTable.h" -#include "ResourceValues.h" -#include "Source.h" -#include "StringPiece.h" - -#include <androidfw/AssetManager.h> -#include <map> -#include <memory> -#include <ostream> -#include <set> -#include <vector> - -namespace aapt { - -/** - * The Linker has two jobs. It follows resource references - * and verifies that their targert exists and that their - * types are compatible. The Linker will also assign resource - * IDs and fill in all the dependent references with the newly - * assigned resource IDs. - * - * To do this, the Linker builds a graph of references. This - * can be useful to do other analysis, like building a - * dependency graph of source files. The hope is to be able to - * add functionality that operates on the graph without - * overcomplicating the Linker. - * - * TODO(adamlesinski): Build the graph first then run the separate - * steps over the graph. - */ -class Linker : ValueVisitor { -public: - struct Options { - /** - * Assign resource Ids to references when linking. - * When building a static library, set this to false. - */ - bool linkResourceIds = true; - }; - - /** - * Create a Linker for the given resource table with the sources available in - * IResolver. IResolver should contain the ResourceTable as a source too. - */ - Linker(const std::shared_ptr<ResourceTable>& table, - const std::shared_ptr<IResolver>& resolver, const Options& options); - - Linker(const Linker&) = delete; - - virtual ~Linker() = default; - - /** - * Entry point to the linker. Assigns resource IDs, follows references, - * and validates types. Returns true if all references to defined values - * are type-compatible. Missing resource references are recorded but do - * not cause this method to fail. - */ - bool linkAndValidate(); - - /** - * Returns any references to resources that were not defined in any of the - * sources. - */ - using ResourceNameToSourceMap = std::map<ResourceName, std::vector<SourceLine>>; - const ResourceNameToSourceMap& getUnresolvedReferences() const; - -protected: - virtual void doResolveReference(Reference& reference, const SourceLine& source); - virtual const Attribute* doResolveAttribute(Reference& attribute, const SourceLine& source); - - std::shared_ptr<IResolver> mResolver; - -private: - struct Args : public ValueVisitorArgs { - Args(const ResourceNameRef& r, const SourceLine& s); - - const ResourceNameRef& referrer; - const SourceLine& source; - }; - - // - // Overrides of ValueVisitor - // - void visit(Reference& reference, ValueVisitorArgs& args) override; - void visit(Attribute& attribute, ValueVisitorArgs& args) override; - void visit(Styleable& styleable, ValueVisitorArgs& args) override; - void visit(Style& style, ValueVisitorArgs& args) override; - void visit(Array& array, ValueVisitorArgs& args) override; - void visit(Plural& plural, ValueVisitorArgs& args) override; - - void processAttributeValue(const ResourceNameRef& name, const SourceLine& source, - const Attribute& attr, std::unique_ptr<Item>& value); - - void addUnresolvedSymbol(const ResourceNameRef& name, const SourceLine& source); - - std::shared_ptr<ResourceTable> mTable; - std::map<ResourceName, std::vector<SourceLine>> mUnresolvedSymbols; - Options mOptions; - bool mError; -}; - -} // namespace aapt - -#endif // AAPT_LINKER_H diff --git a/tools/aapt2/Linker_test.cpp b/tools/aapt2/Linker_test.cpp deleted file mode 100644 index d897f9824a95..000000000000 --- a/tools/aapt2/Linker_test.cpp +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Linker.h" -#include "ResourceTable.h" -#include "ResourceTableResolver.h" -#include "ResourceValues.h" -#include "Util.h" - -#include <androidfw/AssetManager.h> -#include <gtest/gtest.h> -#include <string> - -namespace aapt { - -struct LinkerTest : public ::testing::Test { - virtual void SetUp() override { - mTable = std::make_shared<ResourceTable>(); - mTable->setPackage(u"android"); - mTable->setPackageId(0x01); - mLinker = std::make_shared<Linker>(mTable, std::make_shared<ResourceTableResolver>( - mTable, std::vector<std::shared_ptr<const android::AssetManager>>()), - Linker::Options{}); - - // Create a few attributes for use in the tests. - - addResource(ResourceName{ {}, ResourceType::kAttr, u"integer" }, - util::make_unique<Attribute>(false, android::ResTable_map::TYPE_INTEGER)); - - addResource(ResourceName{ {}, ResourceType::kAttr, u"string" }, - util::make_unique<Attribute>(false, android::ResTable_map::TYPE_STRING)); - - addResource(ResourceName{ {}, ResourceType::kId, u"apple" }, util::make_unique<Id>()); - - addResource(ResourceName{ {}, ResourceType::kId, u"banana" }, util::make_unique<Id>()); - - std::unique_ptr<Attribute> flagAttr = util::make_unique<Attribute>( - false, android::ResTable_map::TYPE_FLAGS); - flagAttr->symbols.push_back(Attribute::Symbol{ - ResourceNameRef{ u"android", ResourceType::kId, u"apple" }, 1 }); - flagAttr->symbols.push_back(Attribute::Symbol{ - ResourceNameRef{ u"android", ResourceType::kId, u"banana" }, 2 }); - addResource(ResourceName{ {}, ResourceType::kAttr, u"flags" }, std::move(flagAttr)); - } - - /* - * Convenience method for adding resources with the default configuration and some - * bogus source line. - */ - bool addResource(const ResourceNameRef& name, std::unique_ptr<Value> value) { - return mTable->addResource(name, {}, SourceLine{ "test.xml", 21 }, std::move(value)); - } - - std::shared_ptr<ResourceTable> mTable; - std::shared_ptr<Linker> mLinker; -}; - -TEST_F(LinkerTest, DoNotInterpretEscapedStringAsReference) { - ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kString, u"foo" }, - util::make_unique<String>(mTable->getValueStringPool().makeRef(u"?123")))); - - ASSERT_TRUE(mLinker->linkAndValidate()); - EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); -} - -TEST_F(LinkerTest, EscapeAndConvertRawString) { - std::unique_ptr<Style> style = util::make_unique<Style>(); - style->entries.push_back(Style::Entry{ - ResourceNameRef{ u"android", ResourceType::kAttr, u"integer" }, - util::make_unique<RawString>(mTable->getValueStringPool().makeRef(u" 123")) - }); - const Style* result = style.get(); - ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, - std::move(style))); - - ASSERT_TRUE(mLinker->linkAndValidate()); - EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); - - EXPECT_NE(nullptr, dynamic_cast<BinaryPrimitive*>(result->entries.front().value.get())); -} - -TEST_F(LinkerTest, FailToConvertRawString) { - std::unique_ptr<Style> style = util::make_unique<Style>(); - style->entries.push_back(Style::Entry{ - ResourceNameRef{ u"android", ResourceType::kAttr, u"integer" }, - util::make_unique<RawString>(mTable->getValueStringPool().makeRef(u"yo what is up?")) - }); - ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, - std::move(style))); - - ASSERT_FALSE(mLinker->linkAndValidate()); -} - -TEST_F(LinkerTest, ConvertRawStringToString) { - std::unique_ptr<Style> style = util::make_unique<Style>(); - style->entries.push_back(Style::Entry{ - ResourceNameRef{ u"android", ResourceType::kAttr, u"string" }, - util::make_unique<RawString>( - mTable->getValueStringPool().makeRef(u" \"this is \\u00fa\".")) - }); - const Style* result = style.get(); - ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, - std::move(style))); - - ASSERT_TRUE(mLinker->linkAndValidate()); - EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); - - const String* str = dynamic_cast<const String*>(result->entries.front().value.get()); - ASSERT_NE(nullptr, str); - EXPECT_EQ(*str->value, u"this is \u00fa."); -} - -TEST_F(LinkerTest, ConvertRawStringToFlags) { - std::unique_ptr<Style> style = util::make_unique<Style>(); - style->entries.push_back(Style::Entry{ - ResourceNameRef{ u"android", ResourceType::kAttr, u"flags" }, - util::make_unique<RawString>(mTable->getValueStringPool().makeRef(u"banana | apple")) - }); - const Style* result = style.get(); - ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kStyle, u"foo" }, - std::move(style))); - - ASSERT_TRUE(mLinker->linkAndValidate()); - EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); - - const BinaryPrimitive* bin = dynamic_cast<const BinaryPrimitive*>( - result->entries.front().value.get()); - ASSERT_NE(nullptr, bin); - EXPECT_EQ(bin->value.data, 1u | 2u); -} - -TEST_F(LinkerTest, AllowReferenceWithOnlyResourceIdPointingToDifferentPackage) { - ASSERT_TRUE(addResource(ResourceName{ u"android", ResourceType::kInteger, u"foo" }, - util::make_unique<Reference>(ResourceId{ 0x02, 0x01, 0x01 }))); - - ASSERT_TRUE(mLinker->linkAndValidate()); - EXPECT_TRUE(mLinker->getUnresolvedReferences().empty()); -} - -} // namespace aapt diff --git a/tools/aapt2/Locale.cpp b/tools/aapt2/Locale.cpp index eed0ea71f6c0..6acf3b09d301 100644 --- a/tools/aapt2/Locale.cpp +++ b/tools/aapt2/Locale.cpp @@ -15,7 +15,7 @@ */ #include "Locale.h" -#include "Util.h" +#include "util/Util.h" #include <algorithm> #include <ctype.h> @@ -70,7 +70,7 @@ static inline bool isNumber(const std::string& str) { return std::all_of(std::begin(str), std::end(str), ::isdigit); } -bool LocaleValue::initFromFilterString(const std::string& str) { +bool LocaleValue::initFromFilterString(const StringPiece& str) { // A locale (as specified in the filter) is an underscore separated name such // as "en_US", "en_Latn_US", or "en_US_POSIX". std::vector<std::string> parts = util::splitAndLowercase(str, '_'); @@ -96,7 +96,7 @@ bool LocaleValue::initFromFilterString(const std::string& str) { setRegion(part2.c_str()); } else if (part2.length() == 4 && isAlpha(part2)) { setScript(part2.c_str()); - } else if (part2.length() >= 5 && part2.length() <= 8) { + } else if (part2.length() >= 4 && part2.length() <= 8) { setVariant(part2.c_str()); } else { valid = false; @@ -111,7 +111,7 @@ bool LocaleValue::initFromFilterString(const std::string& str) { if (((part3.length() == 2 && isAlpha(part3)) || (part3.length() == 3 && isNumber(part3))) && script[0]) { setRegion(part3.c_str()); - } else if (part3.length() >= 5 && part3.length() <= 8) { + } else if (part3.length() >= 4 && part3.length() <= 8) { setVariant(part3.c_str()); } else { valid = false; @@ -122,7 +122,7 @@ bool LocaleValue::initFromFilterString(const std::string& str) { } const std::string& part4 = parts[3]; - if (part4.length() >= 5 && part4.length() <= 8) { + if (part4.length() >= 4 && part4.length() <= 8) { setVariant(part4.c_str()); } else { valid = false; @@ -141,7 +141,7 @@ ssize_t LocaleValue::initFromParts(std::vector<std::string>::iterator iter, std::string& part = *iter; if (part[0] == 'b' && part[1] == '+') { - // This is a "modified" BCP-47 language tag. Same semantics as BCP-47 tags, + // This is a "modified" BCP 47 language tag. Same semantics as BCP 47 tags, // except that the separator is "+" and not "-". std::vector<std::string> subtags = util::splitAndLowercase(part, '+'); subtags.erase(subtags.begin()); @@ -157,8 +157,12 @@ ssize_t LocaleValue::initFromParts(std::vector<std::string>::iterator iter, setRegion(subtags[1].c_str()); break; case 4: - setScript(subtags[1].c_str()); - break; + if ('0' <= subtags[1][0] && subtags[1][0] <= '9') { + // This is a variant: fall through + } else { + setScript(subtags[1].c_str()); + break; + } case 5: case 6: case 7: @@ -184,7 +188,7 @@ ssize_t LocaleValue::initFromParts(std::vector<std::string>::iterator iter, // The third tag can either be a region code (if the second tag was // a script), else a variant code. - if (subtags[2].size() > 4) { + if (subtags[2].size() >= 4) { setVariant(subtags[2].c_str()); } else { setRegion(subtags[2].c_str()); @@ -249,7 +253,7 @@ std::string LocaleValue::toDirName() const { void LocaleValue::initFromResTable(const ResTable_config& config) { config.unpackLanguage(language); config.unpackRegion(region); - if (config.localeScript[0]) { + if (config.localeScriptWasProvided) { memcpy(script, config.localeScript, sizeof(config.localeScript)); } @@ -264,6 +268,10 @@ void LocaleValue::writeTo(ResTable_config* out) const { if (script[0]) { memcpy(out->localeScript, script, sizeof(out->localeScript)); + out->localeScriptWasProvided = true; + } else { + out->computeScript(); + out->localeScriptWasProvided = false; } if (variant[0]) { diff --git a/tools/aapt2/Locale.h b/tools/aapt2/Locale.h index ceec764ba4fd..b1c80ab27641 100644 --- a/tools/aapt2/Locale.h +++ b/tools/aapt2/Locale.h @@ -17,6 +17,8 @@ #ifndef AAPT_LOCALE_VALUE_H #define AAPT_LOCALE_VALUE_H +#include "util/StringPiece.h" + #include <androidfw/ResourceTypes.h> #include <string> #include <vector> @@ -37,7 +39,7 @@ struct LocaleValue { /** * Initialize this LocaleValue from a config string. */ - bool initFromFilterString(const std::string& config); + bool initFromFilterString(const StringPiece& config); /** * Initialize this LocaleValue from parts of a vector. diff --git a/tools/aapt2/Locale_test.cpp b/tools/aapt2/Locale_test.cpp index 4e154d6720d6..758e1e31c0e7 100644 --- a/tools/aapt2/Locale_test.cpp +++ b/tools/aapt2/Locale_test.cpp @@ -15,7 +15,7 @@ */ #include "Locale.h" -#include "Util.h" +#include "util/Util.h" #include <gtest/gtest.h> #include <string> diff --git a/tools/aapt2/Logger.cpp b/tools/aapt2/Logger.cpp deleted file mode 100644 index 384718567984..000000000000 --- a/tools/aapt2/Logger.cpp +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#include "Logger.h" -#include "Source.h" - -#include <memory> -#include <iostream> - -namespace aapt { - -Log::Log(std::ostream& _out, std::ostream& _err) : out(_out), err(_err) { -} - -std::shared_ptr<Log> Logger::sLog(std::make_shared<Log>(std::cerr, std::cerr)); - -void Logger::setLog(const std::shared_ptr<Log>& log) { - sLog = log; -} - -std::ostream& Logger::error() { - return sLog->err << "error: "; -} - -std::ostream& Logger::error(const Source& source) { - return sLog->err << source << ": error: "; -} - -std::ostream& Logger::error(const SourceLine& source) { - return sLog->err << source << ": error: "; -} - -std::ostream& Logger::warn() { - return sLog->err << "warning: "; -} - -std::ostream& Logger::warn(const Source& source) { - return sLog->err << source << ": warning: "; -} - -std::ostream& Logger::warn(const SourceLine& source) { - return sLog->err << source << ": warning: "; -} - -std::ostream& Logger::note() { - return sLog->out << "note: "; -} - -std::ostream& Logger::note(const Source& source) { - return sLog->err << source << ": note: "; -} - -std::ostream& Logger::note(const SourceLine& source) { - return sLog->err << source << ": note: "; -} - -SourceLogger::SourceLogger(const Source& source) -: mSource(source) { -} - -std::ostream& SourceLogger::error() { - return Logger::error(mSource); -} - -std::ostream& SourceLogger::error(size_t line) { - return Logger::error(SourceLine{ mSource.path, line }); -} - -std::ostream& SourceLogger::warn() { - return Logger::warn(mSource); -} - -std::ostream& SourceLogger::warn(size_t line) { - return Logger::warn(SourceLine{ mSource.path, line }); -} - -std::ostream& SourceLogger::note() { - return Logger::note(mSource); -} - -std::ostream& SourceLogger::note(size_t line) { - return Logger::note(SourceLine{ mSource.path, line }); -} - -} // namespace aapt diff --git a/tools/aapt2/Logger.h b/tools/aapt2/Logger.h deleted file mode 100644 index 1d437ebe6492..000000000000 --- a/tools/aapt2/Logger.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_LOGGER_H -#define AAPT_LOGGER_H - -#include "Source.h" - -#include <memory> -#include <ostream> -#include <string> -#include <utils/String8.h> - -namespace aapt { - -struct Log { - Log(std::ostream& out, std::ostream& err); - Log(const Log& rhs) = delete; - - std::ostream& out; - std::ostream& err; -}; - -class Logger { -public: - static void setLog(const std::shared_ptr<Log>& log); - - static std::ostream& error(); - static std::ostream& error(const Source& source); - static std::ostream& error(const SourceLine& sourceLine); - - static std::ostream& warn(); - static std::ostream& warn(const Source& source); - static std::ostream& warn(const SourceLine& sourceLine); - - static std::ostream& note(); - static std::ostream& note(const Source& source); - static std::ostream& note(const SourceLine& sourceLine); - -private: - static std::shared_ptr<Log> sLog; -}; - -class SourceLogger { -public: - SourceLogger(const Source& source); - - std::ostream& error(); - std::ostream& error(size_t line); - - std::ostream& warn(); - std::ostream& warn(size_t line); - - std::ostream& note(); - std::ostream& note(size_t line); - -private: - Source mSource; -}; - -inline ::std::ostream& operator<<(::std::ostream& out, const std::u16string& str) { - android::String8 utf8(str.data(), str.size()); - return out.write(utf8.string(), utf8.size()); -} - -} // namespace aapt - -#endif // AAPT_LOGGER_H diff --git a/tools/aapt2/Main.cpp b/tools/aapt2/Main.cpp index 54a7329359f1..248e7ad73a82 100644 --- a/tools/aapt2/Main.cpp +++ b/tools/aapt2/Main.cpp @@ -14,1262 +14,39 @@ * limitations under the License. */ -#include "AppInfo.h" -#include "BigBuffer.h" -#include "BinaryResourceParser.h" -#include "BindingXmlPullParser.h" -#include "Debug.h" -#include "Files.h" -#include "Flag.h" -#include "JavaClassGenerator.h" -#include "Linker.h" -#include "ManifestMerger.h" -#include "ManifestParser.h" -#include "ManifestValidator.h" -#include "NameMangler.h" -#include "Png.h" -#include "ProguardRules.h" -#include "ResourceParser.h" -#include "ResourceTable.h" -#include "ResourceTableResolver.h" -#include "ResourceValues.h" -#include "SdkConstants.h" -#include "SourceXmlPullParser.h" -#include "StringPiece.h" -#include "TableFlattener.h" -#include "Util.h" -#include "XmlFlattener.h" -#include "ZipFile.h" +#include "util/StringPiece.h" -#include <algorithm> -#include <androidfw/AssetManager.h> -#include <cstdlib> -#include <dirent.h> -#include <errno.h> -#include <fstream> #include <iostream> -#include <sstream> -#include <sys/stat.h> -#include <unordered_set> -#include <utils/Errors.h> +#include <vector> -constexpr const char* kAaptVersionStr = "2.0-alpha"; +namespace aapt { -using namespace aapt; +extern int compile(const std::vector<StringPiece>& args); +extern int link(const std::vector<StringPiece>& args); -/** - * Used with smart pointers to free malloc'ed memory. - */ -struct DeleteMalloc { - void operator()(void* ptr) { - free(ptr); - } -}; - -struct StaticLibraryData { - Source source; - std::unique_ptr<ZipFile> apk; -}; - -/** - * Collect files from 'root', filtering out any files that do not - * match the FileFilter 'filter'. - */ -bool walkTree(const Source& root, const FileFilter& filter, - std::vector<Source>* outEntries) { - bool error = false; - - for (const std::string& dirName : listFiles(root.path)) { - std::string dir = root.path; - appendPath(&dir, dirName); - - FileType ft = getFileType(dir); - if (!filter(dirName, ft)) { - continue; - } - - if (ft != FileType::kDirectory) { - continue; - } - - for (const std::string& fileName : listFiles(dir)) { - std::string file(dir); - appendPath(&file, fileName); - - FileType ft = getFileType(file); - if (!filter(fileName, ft)) { - continue; - } - - if (ft != FileType::kRegular) { - Logger::error(Source{ file }) << "not a regular file." << std::endl; - error = true; - continue; - } - outEntries->push_back(Source{ file }); - } - } - return !error; -} - -void versionStylesForCompat(const std::shared_ptr<ResourceTable>& table) { - for (auto& type : *table) { - if (type->type != ResourceType::kStyle) { - continue; - } - - for (auto& entry : type->entries) { - // Add the versioned styles we want to create - // here. They are added to the table after - // iterating over the original set of styles. - // - // A stack is used since auto-generated styles - // from later versions should override - // auto-generated styles from earlier versions. - // Iterating over the styles is done in order, - // so we will always visit sdkVersions from smallest - // to largest. - std::stack<ResourceConfigValue> addStack; - - for (ResourceConfigValue& configValue : entry->values) { - visitFunc<Style>(*configValue.value, [&](Style& style) { - // Collect which entries we've stripped and the smallest - // SDK level which was stripped. - size_t minSdkStripped = std::numeric_limits<size_t>::max(); - std::vector<Style::Entry> stripped; - - // Iterate over the style's entries and erase/record the - // attributes whose SDK level exceeds the config's sdkVersion. - auto iter = style.entries.begin(); - while (iter != style.entries.end()) { - if (iter->key.name.package == u"android") { - size_t sdkLevel = findAttributeSdkLevel(iter->key.name); - if (sdkLevel > 1 && sdkLevel > configValue.config.sdkVersion) { - // Record that we are about to strip this. - stripped.emplace_back(std::move(*iter)); - minSdkStripped = std::min(minSdkStripped, sdkLevel); - - // Erase this from this style. - iter = style.entries.erase(iter); - continue; - } - } - ++iter; - } - - if (!stripped.empty()) { - // We have stripped attributes, so let's create a new style to hold them. - ConfigDescription versionConfig(configValue.config); - versionConfig.sdkVersion = minSdkStripped; - - ResourceConfigValue value = { - versionConfig, - configValue.source, - {}, - - // Create a copy of the original style. - std::unique_ptr<Value>(configValue.value->clone( - &table->getValueStringPool())) - }; - - Style& newStyle = static_cast<Style&>(*value.value); - - // Move the recorded stripped attributes into this new style. - std::move(stripped.begin(), stripped.end(), - std::back_inserter(newStyle.entries)); - - // We will add this style to the table later. If we do it now, we will - // mess up iteration. - addStack.push(std::move(value)); - } - }); - } - - auto comparator = - [](const ResourceConfigValue& lhs, const ConfigDescription& rhs) -> bool { - return lhs.config < rhs; - }; - - while (!addStack.empty()) { - ResourceConfigValue& value = addStack.top(); - auto iter = std::lower_bound(entry->values.begin(), entry->values.end(), - value.config, comparator); - if (iter == entry->values.end() || iter->config != value.config) { - entry->values.insert(iter, std::move(value)); - } - addStack.pop(); - } - } - } -} - -struct CompileItem { - ResourceName name; - ConfigDescription config; - Source source; - std::string extension; -}; - -struct LinkItem { - ResourceName name; - ConfigDescription config; - Source source; - std::string originalPath; - ZipFile* apk; - std::u16string originalPackage; -}; - -template <typename TChar> -static BasicStringPiece<TChar> getExtension(const BasicStringPiece<TChar>& str) { - auto iter = std::find(str.begin(), str.end(), static_cast<TChar>('.')); - if (iter == str.end()) { - return BasicStringPiece<TChar>(); - } - size_t offset = (iter - str.begin()) + 1; - return str.substr(offset, str.size() - offset); -} - -std::string buildFileReference(const ResourceNameRef& name, const ConfigDescription& config, - const StringPiece& extension) { - std::stringstream path; - path << "res/" << name.type; - if (config != ConfigDescription{}) { - path << "-" << config; - } - path << "/" << util::utf16ToUtf8(name.entry); - if (!extension.empty()) { - path << "." << extension; - } - return path.str(); -} - -std::string buildFileReference(const CompileItem& item) { - return buildFileReference(item.name, item.config, item.extension); -} - -std::string buildFileReference(const LinkItem& item) { - return buildFileReference(item.name, item.config, getExtension<char>(item.originalPath)); -} - -template <typename T> -bool addFileReference(const std::shared_ptr<ResourceTable>& table, const T& item) { - StringPool& pool = table->getValueStringPool(); - StringPool::Ref ref = pool.makeRef(util::utf8ToUtf16(buildFileReference(item)), - StringPool::Context{ 0, item.config }); - return table->addResource(item.name, item.config, item.source.line(0), - util::make_unique<FileReference>(ref)); -} - -struct AaptOptions { - enum class Phase { - Link, - Compile, - Dump, - DumpStyleGraph, - }; - - enum class PackageType { - StandardApp, - StaticLibrary, - }; - - // The phase to process. - Phase phase; - - // The type of package to produce. - PackageType packageType = PackageType::StandardApp; - - // Details about the app. - AppInfo appInfo; - - // The location of the manifest file. - Source manifest; - - // The APK files to link. - std::vector<Source> input; - - // The libraries these files may reference. - std::vector<Source> libraries; - - // Output path. This can be a directory or file - // depending on the phase. - Source output; - - // Directory in which to write binding xml files. - Source bindingOutput; - - // Directory to in which to generate R.java. - Maybe<Source> generateJavaClass; - - // File in which to produce proguard rules. - Maybe<Source> generateProguardRules; - - // Whether to output verbose details about - // compilation. - bool verbose = false; - - // Whether or not to auto-version styles or layouts - // referencing attributes defined in a newer SDK - // level than the style or layout is defined for. - bool versionStylesAndLayouts = true; - - // The target style that will have it's style hierarchy dumped - // when the phase is DumpStyleGraph. - ResourceName dumpStyleTarget; -}; - -struct IdCollector : public xml::Visitor { - IdCollector(const Source& source, const std::shared_ptr<ResourceTable>& table) : - mSource(source), mTable(table) { - } - - virtual void visit(xml::Text* node) override {} - - virtual void visit(xml::Namespace* node) override { - for (const auto& child : node->children) { - child->accept(this); - } - } - - virtual void visit(xml::Element* node) override { - for (const xml::Attribute& attr : node->attributes) { - bool create = false; - bool priv = false; - ResourceNameRef nameRef; - if (ResourceParser::tryParseReference(attr.value, &nameRef, &create, &priv)) { - if (create) { - mTable->addResource(nameRef, {}, mSource.line(node->lineNumber), - util::make_unique<Id>()); - } - } - } - - for (const auto& child : node->children) { - child->accept(this); - } - } - -private: - Source mSource; - std::shared_ptr<ResourceTable> mTable; -}; - -bool compileXml(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, - const CompileItem& item, ZipFile* outApk) { - std::ifstream in(item.source.path, std::ifstream::binary); - if (!in) { - Logger::error(item.source) << strerror(errno) << std::endl; - return false; - } - - SourceLogger logger(item.source); - std::unique_ptr<xml::Node> root = xml::inflate(&in, &logger); - if (!root) { - return false; - } - - // Collect any resource ID's declared here. - IdCollector idCollector(item.source, table); - root->accept(&idCollector); - - BigBuffer outBuffer(1024); - if (!xml::flatten(root.get(), options.appInfo.package, &outBuffer)) { - logger.error() << "failed to encode XML." << std::endl; - return false; - } - - // Write the resulting compiled XML file to the output APK. - if (outApk->add(outBuffer, buildFileReference(item).data(), ZipEntry::kCompressStored, - nullptr) != android::NO_ERROR) { - Logger::error(options.output) << "failed to write compiled '" << item.source - << "' to apk." << std::endl; - return false; - } - return true; -} - -/** - * Determines if a layout should be auto generated based on SDK level. We do not - * generate a layout if there is already a layout defined whose SDK version is greater than - * the one we want to generate. - */ -bool shouldGenerateVersionedResource(const std::shared_ptr<const ResourceTable>& table, - const ResourceName& name, const ConfigDescription& config, - int sdkVersionToGenerate) { - assert(sdkVersionToGenerate > config.sdkVersion); - const ResourceTableType* type; - const ResourceEntry* entry; - std::tie(type, entry) = table->findResource(name); - assert(type && entry); - - auto iter = std::lower_bound(entry->values.begin(), entry->values.end(), config, - [](const ResourceConfigValue& lhs, const ConfigDescription& config) -> bool { - return lhs.config < config; - }); - - assert(iter != entry->values.end()); - ++iter; - - if (iter == entry->values.end()) { - return true; - } - - ConfigDescription newConfig = config; - newConfig.sdkVersion = sdkVersionToGenerate; - return newConfig < iter->config; -} - -bool linkXml(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, - const std::shared_ptr<IResolver>& resolver, const LinkItem& item, - const void* data, size_t dataLen, ZipFile* outApk, std::queue<LinkItem>* outQueue, - proguard::KeepSet* keepSet) { - SourceLogger logger(item.source); - std::unique_ptr<xml::Node> root = xml::inflate(data, dataLen, &logger); - if (!root) { - return false; - } - - xml::FlattenOptions xmlOptions; - if (options.packageType == AaptOptions::PackageType::StaticLibrary) { - xmlOptions.keepRawValues = true; - } - - if (options.versionStylesAndLayouts) { - // We strip attributes that do not belong in this version of the resource. - // Non-version qualified resources have an implicit version 1 requirement. - xmlOptions.maxSdkAttribute = item.config.sdkVersion ? item.config.sdkVersion : 1; - } - - if (options.generateProguardRules) { - proguard::collectProguardRules(item.name.type, item.source, root.get(), keepSet); - } - - BigBuffer outBuffer(1024); - Maybe<size_t> minStrippedSdk = xml::flattenAndLink(item.source, root.get(), - item.originalPackage, resolver, - xmlOptions, &outBuffer); - if (!minStrippedSdk) { - logger.error() << "failed to encode XML." << std::endl; - return false; - } - - if (minStrippedSdk.value() > 0) { - // Something was stripped, so let's generate a new file - // with the version of the smallest SDK version stripped. - // We can only generate a versioned layout if there doesn't exist a layout - // with sdk version greater than the current one but less than the one we - // want to generate. - if (shouldGenerateVersionedResource(table, item.name, item.config, - minStrippedSdk.value())) { - LinkItem newWork = item; - newWork.config.sdkVersion = minStrippedSdk.value(); - outQueue->push(newWork); - - if (!addFileReference(table, newWork)) { - Logger::error(options.output) << "failed to add auto-versioned resource '" - << newWork.name << "'." << std::endl; - return false; - } - } - } - - if (outApk->add(outBuffer, buildFileReference(item).data(), ZipEntry::kCompressDeflated, - nullptr) != android::NO_ERROR) { - Logger::error(options.output) << "failed to write linked file '" - << buildFileReference(item) << "' to apk." << std::endl; - return false; - } - return true; -} - -bool compilePng(const AaptOptions& options, const CompileItem& item, ZipFile* outApk) { - std::ifstream in(item.source.path, std::ifstream::binary); - if (!in) { - Logger::error(item.source) << strerror(errno) << std::endl; - return false; - } - - BigBuffer outBuffer(4096); - std::string err; - Png png; - if (!png.process(item.source, in, &outBuffer, {}, &err)) { - Logger::error(item.source) << err << std::endl; - return false; - } - - if (outApk->add(outBuffer, buildFileReference(item).data(), ZipEntry::kCompressStored, - nullptr) != android::NO_ERROR) { - Logger::error(options.output) << "failed to write compiled '" << item.source - << "' to apk." << std::endl; - return false; - } - return true; -} - -bool copyFile(const AaptOptions& options, const CompileItem& item, ZipFile* outApk) { - if (outApk->add(item.source.path.data(), buildFileReference(item).data(), - ZipEntry::kCompressStored, nullptr) != android::NO_ERROR) { - Logger::error(options.output) << "failed to copy file '" << item.source << "' to apk." - << std::endl; - return false; - } - return true; -} - -bool compileManifest(const AaptOptions& options, const std::shared_ptr<IResolver>& resolver, - const std::map<std::shared_ptr<ResourceTable>, StaticLibraryData>& libApks, - const android::ResTable& table, ZipFile* outApk, proguard::KeepSet* keepSet) { - if (options.verbose) { - Logger::note(options.manifest) << "compiling AndroidManifest.xml." << std::endl; - } - - std::ifstream in(options.manifest.path, std::ifstream::binary); - if (!in) { - Logger::error(options.manifest) << strerror(errno) << std::endl; - return false; - } - - SourceLogger logger(options.manifest); - std::unique_ptr<xml::Node> root = xml::inflate(&in, &logger); - if (!root) { - return false; - } - - ManifestMerger merger({}); - if (!merger.setAppManifest(options.manifest, options.appInfo.package, std::move(root))) { - return false; - } - - for (const auto& entry : libApks) { - ZipFile* libApk = entry.second.apk.get(); - const std::u16string& libPackage = entry.first->getPackage(); - const Source& libSource = entry.second.source; - - ZipEntry* zipEntry = libApk->getEntryByName("AndroidManifest.xml"); - if (!zipEntry) { - continue; - } - - std::unique_ptr<void, DeleteMalloc> uncompressedData = std::unique_ptr<void, DeleteMalloc>( - libApk->uncompress(zipEntry)); - assert(uncompressedData); - - SourceLogger logger(libSource); - std::unique_ptr<xml::Node> libRoot = xml::inflate(uncompressedData.get(), - zipEntry->getUncompressedLen(), &logger); - if (!libRoot) { - return false; - } - - if (!merger.mergeLibraryManifest(libSource, libPackage, std::move(libRoot))) { - return false; - } - } - - if (options.generateProguardRules) { - proguard::collectProguardRulesForManifest(options.manifest, merger.getMergedXml(), - keepSet); - } - - BigBuffer outBuffer(1024); - if (!xml::flattenAndLink(options.manifest, merger.getMergedXml(), options.appInfo.package, - resolver, {}, &outBuffer)) { - return false; - } - - std::unique_ptr<uint8_t[]> data = util::copy(outBuffer); - - android::ResXMLTree tree; - if (tree.setTo(data.get(), outBuffer.size(), false) != android::NO_ERROR) { - return false; - } - - ManifestValidator validator(table); - if (!validator.validate(options.manifest, &tree)) { - return false; - } - - if (outApk->add(data.get(), outBuffer.size(), "AndroidManifest.xml", - ZipEntry::kCompressStored, nullptr) != android::NO_ERROR) { - Logger::error(options.output) << "failed to write 'AndroidManifest.xml' to apk." - << std::endl; - return false; - } - return true; -} - -static bool compileValues(const std::shared_ptr<ResourceTable>& table, const Source& source, - const ConfigDescription& config) { - std::ifstream in(source.path, std::ifstream::binary); - if (!in) { - Logger::error(source) << strerror(errno) << std::endl; - return false; - } - - std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(in); - ResourceParser parser(table, source, config, xmlParser); - return parser.parse(); -} - -struct ResourcePathData { - std::u16string resourceDir; - std::u16string name; - std::string extension; - ConfigDescription config; -}; - -/** - * Resource file paths are expected to look like: - * [--/res/]type[-config]/name - */ -static Maybe<ResourcePathData> extractResourcePathData(const Source& source) { - // TODO(adamlesinski): Use Windows path separator on windows. - std::vector<std::string> parts = util::splitAndLowercase(source.path, '/'); - if (parts.size() < 2) { - Logger::error(source) << "bad resource path." << std::endl; - return {}; - } - - std::string& dir = parts[parts.size() - 2]; - StringPiece dirStr = dir; - - ConfigDescription config; - size_t dashPos = dir.find('-'); - if (dashPos != std::string::npos) { - StringPiece configStr = dirStr.substr(dashPos + 1, dir.size() - (dashPos + 1)); - if (!ConfigDescription::parse(configStr, &config)) { - Logger::error(source) - << "invalid configuration '" - << configStr - << "'." - << std::endl; - return {}; - } - dirStr = dirStr.substr(0, dashPos); - } - - std::string& filename = parts[parts.size() - 1]; - StringPiece name = filename; - StringPiece extension; - size_t dotPos = filename.find('.'); - if (dotPos != std::string::npos) { - extension = name.substr(dotPos + 1, filename.size() - (dotPos + 1)); - name = name.substr(0, dotPos); - } - - return ResourcePathData{ - util::utf8ToUtf16(dirStr), - util::utf8ToUtf16(name), - extension.toString(), - config - }; -} - -bool writeResourceTable(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, - const TableFlattener::Options& flattenerOptions, ZipFile* outApk) { - if (table->begin() != table->end()) { - BigBuffer buffer(1024); - TableFlattener flattener(flattenerOptions); - if (!flattener.flatten(&buffer, *table)) { - Logger::error() << "failed to flatten resource table." << std::endl; - return false; - } - - if (options.verbose) { - Logger::note() << "Final resource table size=" << util::formatSize(buffer.size()) - << std::endl; - } - - if (outApk->add(buffer, "resources.arsc", ZipEntry::kCompressStored, nullptr) != - android::NO_ERROR) { - Logger::note(options.output) << "failed to store resource table." << std::endl; - return false; - } - } - return true; -} - -/** - * For each FileReference in the table, adds a LinkItem to the link queue for processing. - */ -static void addApkFilesToLinkQueue(const std::u16string& package, const Source& source, - const std::shared_ptr<ResourceTable>& table, - const std::unique_ptr<ZipFile>& apk, - std::queue<LinkItem>* outLinkQueue) { - bool mangle = package != table->getPackage(); - for (auto& type : *table) { - for (auto& entry : type->entries) { - ResourceName name = { package, type->type, entry->name }; - if (mangle) { - NameMangler::mangle(table->getPackage(), &name.entry); - } - - for (auto& value : entry->values) { - visitFunc<FileReference>(*value.value, [&](FileReference& ref) { - std::string pathUtf8 = util::utf16ToUtf8(*ref.path); - Source newSource = source; - newSource.path += "/"; - newSource.path += pathUtf8; - outLinkQueue->push(LinkItem{ - name, value.config, newSource, pathUtf8, apk.get(), - table->getPackage() }); - // Now rewrite the file path. - if (mangle) { - ref.path = table->getValueStringPool().makeRef(util::utf8ToUtf16( - buildFileReference(name, value.config, - getExtension<char>(pathUtf8)))); - } - }); - } - } - } -} - -static constexpr int kOpenFlags = ZipFile::kOpenCreate | ZipFile::kOpenTruncate | - ZipFile::kOpenReadWrite; - -bool link(const AaptOptions& options, const std::shared_ptr<ResourceTable>& outTable, - const std::shared_ptr<IResolver>& resolver) { - std::map<std::shared_ptr<ResourceTable>, StaticLibraryData> apkFiles; - std::unordered_set<std::u16string> linkedPackages; - - // Populate the linkedPackages with our own. - linkedPackages.insert(options.appInfo.package); - - // Load all APK files. - for (const Source& source : options.input) { - std::unique_ptr<ZipFile> zipFile = util::make_unique<ZipFile>(); - if (zipFile->open(source.path.data(), ZipFile::kOpenReadOnly) != android::NO_ERROR) { - Logger::error(source) << "failed to open: " << strerror(errno) << std::endl; - return false; - } - - std::shared_ptr<ResourceTable> table = std::make_shared<ResourceTable>(); - - ZipEntry* entry = zipFile->getEntryByName("resources.arsc"); - if (!entry) { - Logger::error(source) << "missing 'resources.arsc'." << std::endl; - return false; - } - - std::unique_ptr<void, DeleteMalloc> uncompressedData = std::unique_ptr<void, DeleteMalloc>( - zipFile->uncompress(entry)); - assert(uncompressedData); - - BinaryResourceParser parser(table, resolver, source, options.appInfo.package, - uncompressedData.get(), entry->getUncompressedLen()); - if (!parser.parse()) { - return false; - } - - // Keep track of where this table came from. - apkFiles[table] = StaticLibraryData{ source, std::move(zipFile) }; - - // Add the package to the set of linked packages. - linkedPackages.insert(table->getPackage()); - } - - std::queue<LinkItem> linkQueue; - for (auto& p : apkFiles) { - const std::shared_ptr<ResourceTable>& inTable = p.first; - - // Collect all FileReferences and add them to the queue for processing. - addApkFilesToLinkQueue(options.appInfo.package, p.second.source, inTable, p.second.apk, - &linkQueue); - - // Merge the tables. - if (!outTable->merge(std::move(*inTable))) { - return false; - } - } - - // Version all styles referencing attributes outside of their specified SDK version. - if (options.versionStylesAndLayouts) { - versionStylesForCompat(outTable); - } - - { - // Now that everything is merged, let's link it. - Linker::Options linkerOptions; - if (options.packageType == AaptOptions::PackageType::StaticLibrary) { - linkerOptions.linkResourceIds = false; - } - Linker linker(outTable, resolver, linkerOptions); - if (!linker.linkAndValidate()) { - return false; - } - - // Verify that all symbols exist. - const auto& unresolvedRefs = linker.getUnresolvedReferences(); - if (!unresolvedRefs.empty()) { - for (const auto& entry : unresolvedRefs) { - for (const auto& source : entry.second) { - Logger::error(source) << "unresolved symbol '" << entry.first << "'." - << std::endl; - } - } - return false; - } - } - - // Open the output APK file for writing. - ZipFile outApk; - if (outApk.open(options.output.path.data(), kOpenFlags) != android::NO_ERROR) { - Logger::error(options.output) << "failed to open: " << strerror(errno) << std::endl; - return false; - } - - proguard::KeepSet keepSet; - - android::ResTable binTable; - if (!compileManifest(options, resolver, apkFiles, binTable, &outApk, &keepSet)) { - return false; - } - - for (; !linkQueue.empty(); linkQueue.pop()) { - const LinkItem& item = linkQueue.front(); - - assert(!item.originalPackage.empty()); - ZipEntry* entry = item.apk->getEntryByName(item.originalPath.data()); - if (!entry) { - Logger::error(item.source) << "failed to find '" << item.originalPath << "'." - << std::endl; - return false; - } - - if (util::stringEndsWith<char>(item.originalPath, ".xml")) { - void* uncompressedData = item.apk->uncompress(entry); - assert(uncompressedData); - - if (!linkXml(options, outTable, resolver, item, uncompressedData, - entry->getUncompressedLen(), &outApk, &linkQueue, &keepSet)) { - Logger::error(options.output) << "failed to link '" << item.originalPath << "'." - << std::endl; - return false; - } - } else { - if (outApk.add(item.apk, entry, buildFileReference(item).data(), 0, nullptr) != - android::NO_ERROR) { - Logger::error(options.output) << "failed to copy '" << item.originalPath << "'." - << std::endl; - return false; - } - } - } - - // Generate the Java class file. - if (options.generateJavaClass) { - JavaClassGenerator::Options javaOptions; - if (options.packageType == AaptOptions::PackageType::StaticLibrary) { - javaOptions.useFinal = false; - } - JavaClassGenerator generator(outTable, javaOptions); - - for (const std::u16string& package : linkedPackages) { - Source outPath = options.generateJavaClass.value(); - - // Build the output directory from the package name. - // Eg. com.android.app -> com/android/app - const std::string packageUtf8 = util::utf16ToUtf8(package); - for (StringPiece part : util::tokenize<char>(packageUtf8, '.')) { - appendPath(&outPath.path, part); - } - - if (!mkdirs(outPath.path)) { - Logger::error(outPath) << strerror(errno) << std::endl; - return false; - } - - appendPath(&outPath.path, "R.java"); - - if (options.verbose) { - Logger::note(outPath) << "writing Java symbols." << std::endl; - } - - std::ofstream fout(outPath.path); - if (!fout) { - Logger::error(outPath) << strerror(errno) << std::endl; - return false; - } - - if (!generator.generate(package, fout)) { - Logger::error(outPath) << generator.getError() << "." << std::endl; - return false; - } - } - } - - // Generate the Proguard rules file. - if (options.generateProguardRules) { - const Source& outPath = options.generateProguardRules.value(); - - if (options.verbose) { - Logger::note(outPath) << "writing proguard rules." << std::endl; - } - - std::ofstream fout(outPath.path); - if (!fout) { - Logger::error(outPath) << strerror(errno) << std::endl; - return false; - } - - if (!proguard::writeKeepSet(&fout, keepSet)) { - Logger::error(outPath) << "failed to write proguard rules." << std::endl; - return false; - } - } - - outTable->getValueStringPool().prune(); - outTable->getValueStringPool().sort( - [](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { - if (a.context.priority < b.context.priority) { - return true; - } - - if (a.context.priority > b.context.priority) { - return false; - } - return a.value < b.value; - }); - - - // Flatten the resource table. - TableFlattener::Options flattenerOptions; - if (options.packageType != AaptOptions::PackageType::StaticLibrary) { - flattenerOptions.useExtendedChunks = false; - } - - if (!writeResourceTable(options, outTable, flattenerOptions, &outApk)) { - return false; - } - - outApk.flush(); - return true; -} - -bool compile(const AaptOptions& options, const std::shared_ptr<ResourceTable>& table, - const std::shared_ptr<IResolver>& resolver) { - std::queue<CompileItem> compileQueue; - bool error = false; - - // Compile all the resource files passed in on the command line. - for (const Source& source : options.input) { - // Need to parse the resource type/config/filename. - Maybe<ResourcePathData> maybePathData = extractResourcePathData(source); - if (!maybePathData) { - return false; - } - - const ResourcePathData& pathData = maybePathData.value(); - if (pathData.resourceDir == u"values") { - // The file is in the values directory, which means its contents will - // go into the resource table. - if (options.verbose) { - Logger::note(source) << "compiling values." << std::endl; - } - - error |= !compileValues(table, source, pathData.config); - } else { - // The file is in a directory like 'layout' or 'drawable'. Find out - // the type. - const ResourceType* type = parseResourceType(pathData.resourceDir); - if (!type) { - Logger::error(source) << "invalid resource type '" << pathData.resourceDir << "'." - << std::endl; - return false; - } - - compileQueue.push(CompileItem{ - ResourceName{ table->getPackage(), *type, pathData.name }, - pathData.config, - source, - pathData.extension - }); - } - } - - if (error) { - return false; - } - // Open the output APK file for writing. - ZipFile outApk; - if (outApk.open(options.output.path.data(), kOpenFlags) != android::NO_ERROR) { - Logger::error(options.output) << "failed to open: " << strerror(errno) << std::endl; - return false; - } - - // Compile each file. - for (; !compileQueue.empty(); compileQueue.pop()) { - const CompileItem& item = compileQueue.front(); - - // Add the file name to the resource table. - error |= !addFileReference(table, item); - - if (item.extension == "xml") { - error |= !compileXml(options, table, item, &outApk); - } else if (item.extension == "png" || item.extension == "9.png") { - error |= !compilePng(options, item, &outApk); - } else { - error |= !copyFile(options, item, &outApk); - } - } - - if (error) { - return false; - } - - // Link and assign resource IDs. - Linker linker(table, resolver, {}); - if (!linker.linkAndValidate()) { - return false; - } - - // Flatten the resource table. - if (!writeResourceTable(options, table, {}, &outApk)) { - return false; - } - - outApk.flush(); - return true; -} - -bool loadAppInfo(const Source& source, AppInfo* outInfo) { - std::ifstream ifs(source.path, std::ifstream::in | std::ifstream::binary); - if (!ifs) { - Logger::error(source) << strerror(errno) << std::endl; - return false; - } - - ManifestParser parser; - std::shared_ptr<XmlPullParser> pullParser = std::make_shared<SourceXmlPullParser>(ifs); - return parser.parse(source, pullParser, outInfo); -} - -static void printCommandsAndDie() { - std::cerr << "The following commands are supported:" << std::endl << std::endl; - std::cerr << "compile compiles a subset of resources" << std::endl; - std::cerr << "link links together compiled resources and libraries" << std::endl; - std::cerr << "dump dumps resource contents to to standard out" << std::endl; - std::cerr << std::endl; - std::cerr << "run aapt2 with one of the commands and the -h flag for extra details." - << std::endl; - exit(1); -} - -static AaptOptions prepareArgs(int argc, char** argv) { - if (argc < 2) { - std::cerr << "no command specified." << std::endl << std::endl; - printCommandsAndDie(); - } - - const StringPiece command(argv[1]); - argc -= 2; - argv += 2; - - AaptOptions options; - - if (command == "--version" || command == "version") { - std::cout << kAaptVersionStr << std::endl; - exit(0); - } else if (command == "link") { - options.phase = AaptOptions::Phase::Link; - } else if (command == "compile") { - options.phase = AaptOptions::Phase::Compile; - } else if (command == "dump") { - options.phase = AaptOptions::Phase::Dump; - } else if (command == "dump-style-graph") { - options.phase = AaptOptions::Phase::DumpStyleGraph; - } else { - std::cerr << "invalid command '" << command << "'." << std::endl << std::endl; - printCommandsAndDie(); - } - - bool isStaticLib = false; - if (options.phase == AaptOptions::Phase::Link) { - flag::requiredFlag("--manifest", "AndroidManifest.xml of your app", - [&options](const StringPiece& arg) { - options.manifest = Source{ arg.toString() }; - }); - - flag::optionalFlag("-I", "add an Android APK to link against", - [&options](const StringPiece& arg) { - options.libraries.push_back(Source{ arg.toString() }); - }); - - flag::optionalFlag("--java", "directory in which to generate R.java", - [&options](const StringPiece& arg) { - options.generateJavaClass = Source{ arg.toString() }; - }); - - flag::optionalFlag("--proguard", "file in which to output proguard rules", - [&options](const StringPiece& arg) { - options.generateProguardRules = Source{ arg.toString() }; - }); - - flag::optionalSwitch("--static-lib", "generate a static Android library", true, - &isStaticLib); - - flag::optionalFlag("--binding", "Output directory for binding XML files", - [&options](const StringPiece& arg) { - options.bindingOutput = Source{ arg.toString() }; - }); - flag::optionalSwitch("--no-version", "Disables automatic style and layout versioning", - false, &options.versionStylesAndLayouts); - } - - if (options.phase == AaptOptions::Phase::Compile || - options.phase == AaptOptions::Phase::Link) { - // Common flags for all steps. - flag::requiredFlag("-o", "Output path", [&options](const StringPiece& arg) { - options.output = Source{ arg.toString() }; - }); - } - - if (options.phase == AaptOptions::Phase::DumpStyleGraph) { - flag::requiredFlag("--style", "Name of the style to dump", - [&options](const StringPiece& arg, std::string* outError) -> bool { - Reference styleReference; - if (!ResourceParser::parseStyleParentReference(util::utf8ToUtf16(arg), - &styleReference, outError)) { - return false; - } - options.dumpStyleTarget = styleReference.name; - return true; - }); - } - - bool help = false; - flag::optionalSwitch("-v", "enables verbose logging", true, &options.verbose); - flag::optionalSwitch("-h", "displays this help menu", true, &help); - - // Build the command string for output (eg. "aapt2 compile"). - std::string fullCommand = "aapt2"; - fullCommand += " "; - fullCommand += command.toString(); - - // Actually read the command line flags. - flag::parse(argc, argv, fullCommand); - - if (help) { - flag::usageAndDie(fullCommand); - } - - if (isStaticLib) { - options.packageType = AaptOptions::PackageType::StaticLibrary; - } - - // Copy all the remaining arguments. - for (const std::string& arg : flag::getArgs()) { - options.input.push_back(Source{ arg }); - } - return options; -} - -static bool doDump(const AaptOptions& options) { - for (const Source& source : options.input) { - std::unique_ptr<ZipFile> zipFile = util::make_unique<ZipFile>(); - if (zipFile->open(source.path.data(), ZipFile::kOpenReadOnly) != android::NO_ERROR) { - Logger::error(source) << "failed to open: " << strerror(errno) << std::endl; - return false; - } - - std::shared_ptr<ResourceTable> table = std::make_shared<ResourceTable>(); - std::shared_ptr<ResourceTableResolver> resolver = - std::make_shared<ResourceTableResolver>( - table, std::vector<std::shared_ptr<const android::AssetManager>>()); - - ZipEntry* entry = zipFile->getEntryByName("resources.arsc"); - if (!entry) { - Logger::error(source) << "missing 'resources.arsc'." << std::endl; - return false; - } - - std::unique_ptr<void, DeleteMalloc> uncompressedData = std::unique_ptr<void, DeleteMalloc>( - zipFile->uncompress(entry)); - assert(uncompressedData); - - BinaryResourceParser parser(table, resolver, source, {}, uncompressedData.get(), - entry->getUncompressedLen()); - if (!parser.parse()) { - return false; - } - - if (options.phase == AaptOptions::Phase::Dump) { - Debug::printTable(table); - } else if (options.phase == AaptOptions::Phase::DumpStyleGraph) { - Debug::printStyleGraph(table, options.dumpStyleTarget); - } - } - return true; -} +} // namespace aapt int main(int argc, char** argv) { - Logger::setLog(std::make_shared<Log>(std::cerr, std::cerr)); - AaptOptions options = prepareArgs(argc, argv); + if (argc >= 2) { + argv += 1; + argc -= 1; - if (options.phase == AaptOptions::Phase::Dump || - options.phase == AaptOptions::Phase::DumpStyleGraph) { - if (!doDump(options)) { - return 1; - } - return 0; - } - - // If we specified a manifest, go ahead and load the package name from the manifest. - if (!options.manifest.path.empty()) { - if (!loadAppInfo(options.manifest, &options.appInfo)) { - return false; + std::vector<aapt::StringPiece> args; + for (int i = 1; i < argc; i++) { + args.push_back(argv[i]); } - if (options.appInfo.package.empty()) { - Logger::error() << "no package name specified." << std::endl; - return false; + aapt::StringPiece command(argv[0]); + if (command == "compile" || command == "c") { + return aapt::compile(args); + } else if (command == "link" || command == "l") { + return aapt::link(args); } - } - - // Every phase needs a resource table. - std::shared_ptr<ResourceTable> table = std::make_shared<ResourceTable>(); - - // The package name is empty when in the compile phase. - table->setPackage(options.appInfo.package); - if (options.appInfo.package == u"android") { - table->setPackageId(0x01); + std::cerr << "unknown command '" << command << "'\n"; } else { - table->setPackageId(0x7f); - } - - // Load the included libraries. - std::vector<std::shared_ptr<const android::AssetManager>> sources; - for (const Source& source : options.libraries) { - std::shared_ptr<android::AssetManager> assetManager = - std::make_shared<android::AssetManager>(); - int32_t cookie; - if (!assetManager->addAssetPath(android::String8(source.path.data()), &cookie)) { - Logger::error(source) << "failed to load library." << std::endl; - return false; - } - - if (cookie == 0) { - Logger::error(source) << "failed to load library." << std::endl; - return false; - } - sources.push_back(assetManager); + std::cerr << "no command specified\n"; } - // Make the resolver that will cache IDs for us. - std::shared_ptr<ResourceTableResolver> resolver = std::make_shared<ResourceTableResolver>( - table, sources); - - if (options.phase == AaptOptions::Phase::Compile) { - if (!compile(options, table, resolver)) { - Logger::error() << "aapt exiting with failures." << std::endl; - return 1; - } - } else if (options.phase == AaptOptions::Phase::Link) { - if (!link(options, table, resolver)) { - Logger::error() << "aapt exiting with failures." << std::endl; - return 1; - } - } - return 0; + std::cerr << "\nusage: aapt2 [compile|link] ..." << std::endl; + return 1; } diff --git a/tools/aapt2/ManifestMerger.cpp b/tools/aapt2/ManifestMerger.cpp deleted file mode 100644 index 71d3424c6ad8..000000000000 --- a/tools/aapt2/ManifestMerger.cpp +++ /dev/null @@ -1,376 +0,0 @@ -#include "ManifestMerger.h" -#include "Maybe.h" -#include "ResourceParser.h" -#include "Source.h" -#include "Util.h" -#include "XmlPullParser.h" - -#include <iostream> -#include <memory> -#include <set> -#include <string> - -namespace aapt { - -constexpr const char16_t* kSchemaAndroid = u"http://schemas.android.com/apk/res/android"; - -static xml::Element* findManifest(xml::Node* root) { - if (!root) { - return nullptr; - } - - while (root->type == xml::NodeType::kNamespace) { - if (root->children.empty()) { - break; - } - root = root->children[0].get(); - } - - if (root && root->type == xml::NodeType::kElement) { - xml::Element* el = static_cast<xml::Element*>(root); - if (el->namespaceUri.empty() && el->name == u"manifest") { - return el; - } - } - return nullptr; -} - -static xml::Element* findChildWithSameName(xml::Element* parent, xml::Element* src) { - xml::Attribute* attrKey = src->findAttribute(kSchemaAndroid, u"name"); - if (!attrKey) { - return nullptr; - } - return parent->findChildWithAttribute(src->namespaceUri, src->name, attrKey); -} - -static bool attrLess(const xml::Attribute& lhs, const xml::Attribute& rhs) { - return std::tie(lhs.namespaceUri, lhs.name, lhs.value) - < std::tie(rhs.namespaceUri, rhs.name, rhs.value); -} - -static int compare(xml::Element* lhs, xml::Element* rhs) { - int diff = lhs->attributes.size() - rhs->attributes.size(); - if (diff != 0) { - return diff; - } - - std::set<xml::Attribute, decltype(&attrLess)> lhsAttrs(&attrLess); - lhsAttrs.insert(lhs->attributes.begin(), lhs->attributes.end()); - for (auto& attr : rhs->attributes) { - if (lhsAttrs.erase(attr) == 0) { - // The rhs attribute is not in the left. - return -1; - } - } - - if (!lhsAttrs.empty()) { - // The lhs has attributes not in the rhs. - return 1; - } - return 0; -} - -ManifestMerger::ManifestMerger(const Options& options) : - mOptions(options), mAppLogger({}), mLogger({}) { -} - -bool ManifestMerger::setAppManifest(const Source& source, const std::u16string& package, - std::unique_ptr<xml::Node> root) { - - mAppLogger = SourceLogger{ source }; - mRoot = std::move(root); - return true; -} - -bool ManifestMerger::checkEqual(xml::Element* elA, xml::Element* elB) { - if (compare(elA, elB) != 0) { - mLogger.error(elB->lineNumber) - << "library tag '" << elB->name << "' conflicts with app tag." - << std::endl; - mAppLogger.note(elA->lineNumber) - << "app tag '" << elA->name << "' defined here." - << std::endl; - return false; - } - - std::vector<xml::Element*> childrenA = elA->getChildElements(); - std::vector<xml::Element*> childrenB = elB->getChildElements(); - - if (childrenA.size() != childrenB.size()) { - mLogger.error(elB->lineNumber) - << "library tag '" << elB->name << "' children conflict with app tag." - << std::endl; - mAppLogger.note(elA->lineNumber) - << "app tag '" << elA->name << "' defined here." - << std::endl; - return false; - } - - auto cmp = [](xml::Element* lhs, xml::Element* rhs) -> bool { - return compare(lhs, rhs) < 0; - }; - - std::sort(childrenA.begin(), childrenA.end(), cmp); - std::sort(childrenB.begin(), childrenB.end(), cmp); - - for (size_t i = 0; i < childrenA.size(); i++) { - if (!checkEqual(childrenA[i], childrenB[i])) { - return false; - } - } - return true; -} - -bool ManifestMerger::mergeNewOrEqual(xml::Element* parentA, xml::Element* elA, xml::Element* elB) { - if (!elA) { - parentA->addChild(elB->clone()); - return true; - } - return checkEqual(elA, elB); -} - -bool ManifestMerger::mergePreferRequired(xml::Element* parentA, xml::Element* elA, - xml::Element* elB) { - if (!elA) { - parentA->addChild(elB->clone()); - return true; - } - - xml::Attribute* reqA = elA->findAttribute(kSchemaAndroid, u"required"); - xml::Attribute* reqB = elB->findAttribute(kSchemaAndroid, u"required"); - bool requiredA = !reqA || (reqA->value != u"false" && reqA->value != u"FALSE"); - bool requiredB = !reqB || (reqB->value != u"false" && reqB->value != u"FALSE"); - if (!requiredA && requiredB) { - if (reqA) { - *reqA = xml::Attribute{ kSchemaAndroid, u"required", u"true" }; - } else { - elA->attributes.push_back(xml::Attribute{ kSchemaAndroid, u"required", u"true" }); - } - } - return true; -} - -static int findIntegerValue(xml::Attribute* attr, int defaultValue) { - if (attr) { - std::unique_ptr<BinaryPrimitive> integer = ResourceParser::tryParseInt(attr->value); - if (integer) { - return integer->value.data; - } - } - return defaultValue; -} - -bool ManifestMerger::mergeUsesSdk(xml::Element* elA, xml::Element* elB) { - bool error = false; - xml::Attribute* minAttrA = nullptr; - xml::Attribute* minAttrB = nullptr; - if (elA) { - minAttrA = elA->findAttribute(kSchemaAndroid, u"minSdkVersion"); - } - - if (elB) { - minAttrB = elB->findAttribute(kSchemaAndroid, u"minSdkVersion"); - } - - int minSdkA = findIntegerValue(minAttrA, 1); - int minSdkB = findIntegerValue(minAttrB, 1); - - if (minSdkA < minSdkB) { - std::ostream* out; - if (minAttrA) { - out = &(mAppLogger.error(elA->lineNumber) << "app declares "); - } else if (elA) { - out = &(mAppLogger.error(elA->lineNumber) << "app has implied "); - } else { - out = &(mAppLogger.error() << "app has implied "); - } - - *out << "minSdkVersion=" << minSdkA << " but library expects a higher SDK version." - << std::endl; - - // elB is valid because minSdkB wouldn't be greater than minSdkA if it wasn't. - mLogger.note(elB->lineNumber) - << "library declares minSdkVersion=" << minSdkB << "." - << std::endl; - error = true; - } - - xml::Attribute* targetAttrA = nullptr; - xml::Attribute* targetAttrB = nullptr; - - if (elA) { - targetAttrA = elA->findAttribute(kSchemaAndroid, u"targetSdkVersion"); - } - - if (elB) { - targetAttrB = elB->findAttribute(kSchemaAndroid, u"targetSdkVersion"); - } - - int targetSdkA = findIntegerValue(targetAttrA, minSdkA); - int targetSdkB = findIntegerValue(targetAttrB, minSdkB); - - if (targetSdkA < targetSdkB) { - std::ostream* out; - if (targetAttrA) { - out = &(mAppLogger.warn(elA->lineNumber) << "app declares "); - } else if (elA) { - out = &(mAppLogger.warn(elA->lineNumber) << "app has implied "); - } else { - out = &(mAppLogger.warn() << "app has implied "); - } - - *out << "targetSdkVerion=" << targetSdkA << " but library expects target SDK " - << targetSdkB << "." << std::endl; - - mLogger.note(elB->lineNumber) - << "library declares targetSdkVersion=" << targetSdkB << "." - << std::endl; - error = true; - } - return !error; -} - -bool ManifestMerger::mergeApplication(xml::Element* applicationA, xml::Element* applicationB) { - if (!applicationA || !applicationB) { - return true; - } - - bool error = false; - - // First make sure that the names are identical. - xml::Attribute* nameA = applicationA->findAttribute(kSchemaAndroid, u"name"); - xml::Attribute* nameB = applicationB->findAttribute(kSchemaAndroid, u"name"); - if (nameB) { - if (!nameA) { - applicationA->attributes.push_back(*nameB); - } else if (nameA->value != nameB->value) { - mLogger.error(applicationB->lineNumber) - << "conflicting application name '" - << nameB->value - << "'." << std::endl; - mAppLogger.note(applicationA->lineNumber) - << "application defines application name '" - << nameA->value - << "'." << std::endl; - error = true; - } - } - - // Now we descend into the activity/receiver/service/provider tags - for (xml::Element* elB : applicationB->getChildElements()) { - if (!elB->namespaceUri.empty()) { - continue; - } - - if (elB->name == u"activity" || elB->name == u"activity-alias" - || elB->name == u"service" || elB->name == u"receiver" - || elB->name == u"provider" || elB->name == u"meta-data") { - xml::Element* elA = findChildWithSameName(applicationA, elB); - error |= !mergeNewOrEqual(applicationA, elA, elB); - } else if (elB->name == u"uses-library") { - xml::Element* elA = findChildWithSameName(applicationA, elB); - error |= !mergePreferRequired(applicationA, elA, elB); - } - } - return !error; -} - -bool ManifestMerger::mergeLibraryManifest(const Source& source, const std::u16string& package, - std::unique_ptr<xml::Node> libRoot) { - mLogger = SourceLogger{ source }; - xml::Element* manifestA = findManifest(mRoot.get()); - xml::Element* manifestB = findManifest(libRoot.get()); - if (!manifestA) { - mAppLogger.error() << "missing manifest tag." << std::endl; - return false; - } - - if (!manifestB) { - mLogger.error() << "library missing manifest tag." << std::endl; - return false; - } - - bool error = false; - - // Do <application> first. - xml::Element* applicationA = manifestA->findChild({}, u"application"); - xml::Element* applicationB = manifestB->findChild({}, u"application"); - error |= !mergeApplication(applicationA, applicationB); - - // Do <uses-sdk> next. - xml::Element* usesSdkA = manifestA->findChild({}, u"uses-sdk"); - xml::Element* usesSdkB = manifestB->findChild({}, u"uses-sdk"); - error |= !mergeUsesSdk(usesSdkA, usesSdkB); - - for (xml::Element* elB : manifestB->getChildElements()) { - if (!elB->namespaceUri.empty()) { - continue; - } - - if (elB->name == u"uses-permission" || elB->name == u"permission" - || elB->name == u"permission-group" || elB->name == u"permission-tree") { - xml::Element* elA = findChildWithSameName(manifestA, elB); - error |= !mergeNewOrEqual(manifestA, elA, elB); - } else if (elB->name == u"uses-feature") { - xml::Element* elA = findChildWithSameName(manifestA, elB); - error |= !mergePreferRequired(manifestA, elA, elB); - } else if (elB->name == u"uses-configuration" || elB->name == u"supports-screen" - || elB->name == u"compatible-screens" || elB->name == u"supports-gl-texture") { - xml::Element* elA = findChildWithSameName(manifestA, elB); - error |= !checkEqual(elA, elB); - } - } - return !error; -} - -static void printMerged(xml::Node* node, int depth) { - std::string indent; - for (int i = 0; i < depth; i++) { - indent += " "; - } - - switch (node->type) { - case xml::NodeType::kNamespace: - std::cerr << indent << "N: " - << "xmlns:" << static_cast<xml::Namespace*>(node)->namespacePrefix - << "=\"" << static_cast<xml::Namespace*>(node)->namespaceUri - << "\"\n"; - break; - - case xml::NodeType::kElement: - std::cerr << indent << "E: " - << static_cast<xml::Element*>(node)->namespaceUri - << ":" << static_cast<xml::Element*>(node)->name - << "\n"; - for (const auto& attr : static_cast<xml::Element*>(node)->attributes) { - std::cerr << indent << " A: " - << attr.namespaceUri - << ":" << attr.name - << "=\"" << attr.value << "\"\n"; - } - break; - - case xml::NodeType::kText: - std::cerr << indent << "T: \"" << static_cast<xml::Text*>(node)->text << "\"\n"; - break; - } - - for (auto& child : node->children) { - printMerged(child.get(), depth + 1); - } -} - -xml::Node* ManifestMerger::getMergedXml() { - return mRoot.get(); -} - -bool ManifestMerger::printMerged() { - if (!mRoot) { - return false; - } - - ::aapt::printMerged(mRoot.get(), 0); - return true; -} - -} // namespace aapt diff --git a/tools/aapt2/ManifestMerger.h b/tools/aapt2/ManifestMerger.h deleted file mode 100644 index c6219dbba65e..000000000000 --- a/tools/aapt2/ManifestMerger.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef AAPT_MANIFEST_MERGER_H -#define AAPT_MANIFEST_MERGER_H - -#include "Logger.h" -#include "Source.h" -#include "XmlDom.h" - -#include <memory> -#include <string> - -namespace aapt { - -class ManifestMerger { -public: - struct Options { - }; - - ManifestMerger(const Options& options); - - bool setAppManifest(const Source& source, const std::u16string& package, - std::unique_ptr<xml::Node> root); - - bool mergeLibraryManifest(const Source& source, const std::u16string& package, - std::unique_ptr<xml::Node> libRoot); - - xml::Node* getMergedXml(); - - bool printMerged(); - -private: - bool mergeNewOrEqual(xml::Element* parentA, xml::Element* elA, xml::Element* elB); - bool mergePreferRequired(xml::Element* parentA, xml::Element* elA, xml::Element* elB); - bool checkEqual(xml::Element* elA, xml::Element* elB); - bool mergeApplication(xml::Element* applicationA, xml::Element* applicationB); - bool mergeUsesSdk(xml::Element* elA, xml::Element* elB); - - Options mOptions; - std::unique_ptr<xml::Node> mRoot; - SourceLogger mAppLogger; - SourceLogger mLogger; -}; - -} // namespace aapt - -#endif // AAPT_MANIFEST_MERGER_H diff --git a/tools/aapt2/ManifestMerger_test.cpp b/tools/aapt2/ManifestMerger_test.cpp deleted file mode 100644 index 6838253dad20..000000000000 --- a/tools/aapt2/ManifestMerger_test.cpp +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "ManifestMerger.h" -#include "SourceXmlPullParser.h" - -#include <gtest/gtest.h> -#include <sstream> -#include <string> - -namespace aapt { - -constexpr const char* kAppManifest = R"EOF(<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android"> - <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="21" /> - <uses-permission android:name="android.permission.INTERNET"/> - <uses-feature android:name="android.hardware.GPS" android:required="false" /> - <application android:name="com.android.library.Application"> - <activity android:name="com.android.example.MainActivity"> - <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.DEFAULT" /> - </intent-filter> - </activity> - <service android:name="com.android.library.Service"> - <intent-filter> - <action android:name="com.android.library.intent.action.SYNC" /> - </intent-filter> - </service> - </application> -</manifest> -)EOF"; - -constexpr const char* kLibManifest = R"EOF(<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android"> - <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="21" /> - <uses-permission android:name="android.permission.INTERNET" /> - <uses-feature android:name="android.hardware.GPS" /> - <uses-permission android:name="android.permission.GPS" /> - <application android:name="com.android.library.Application"> - <service android:name="com.android.library.Service"> - <intent-filter> - <action android:name="com.android.library.intent.action.SYNC" /> - </intent-filter> - </service> - <provider android:name="com.android.library.DocumentProvider" - android:authorities="com.android.library.documents" - android:grantUriPermission="true" - android:exported="true" - android:permission="android.permission.MANAGE_DOCUMENTS" - android:enabled="@bool/atLeastKitKat"> - <intent-filter> - <action android:name="android.content.action.DOCUMENTS_PROVIDER" /> - </intent-filter> - </provider> - </application> -</manifest> -)EOF"; - -constexpr const char* kBadLibManifest = R"EOF(<?xml version="1.0" encoding="utf-8"?> -<manifest xmlns:android="http://schemas.android.com/apk/res/android"> - <uses-sdk android:minSdkVersion="17" android:targetSdkVersion="22" /> - <uses-permission android:name="android.permission.INTERNET" /> - <uses-feature android:name="android.hardware.GPS" /> - <uses-permission android:name="android.permission.GPS" /> - <application android:name="com.android.library.Application2"> - <service android:name="com.android.library.Service"> - <intent-filter> - <action android:name="com.android.library.intent.action.SYNC_ACTION" /> - </intent-filter> - </service> - </application> -</manifest> -)EOF"; - -TEST(ManifestMergerTest, MergeManifestsSuccess) { - std::stringstream inA(kAppManifest); - std::stringstream inB(kLibManifest); - - const Source sourceA = { "AndroidManifest.xml" }; - const Source sourceB = { "lib.apk/AndroidManifest.xml" }; - SourceLogger loggerA(sourceA); - SourceLogger loggerB(sourceB); - - ManifestMerger merger({}); - EXPECT_TRUE(merger.setAppManifest(sourceA, u"com.android.example", - xml::inflate(&inA, &loggerA))); - EXPECT_TRUE(merger.mergeLibraryManifest(sourceB, u"com.android.library", - xml::inflate(&inB, &loggerB))); -} - -TEST(ManifestMergerTest, MergeManifestFail) { - std::stringstream inA(kAppManifest); - std::stringstream inB(kBadLibManifest); - - const Source sourceA = { "AndroidManifest.xml" }; - const Source sourceB = { "lib.apk/AndroidManifest.xml" }; - SourceLogger loggerA(sourceA); - SourceLogger loggerB(sourceB); - - ManifestMerger merger({}); - EXPECT_TRUE(merger.setAppManifest(sourceA, u"com.android.example", - xml::inflate(&inA, &loggerA))); - EXPECT_FALSE(merger.mergeLibraryManifest(sourceB, u"com.android.library", - xml::inflate(&inB, &loggerB))); -} - -} // namespace aapt diff --git a/tools/aapt2/ManifestParser.cpp b/tools/aapt2/ManifestParser.cpp deleted file mode 100644 index b8f0a430bcee..000000000000 --- a/tools/aapt2/ManifestParser.cpp +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "AppInfo.h" -#include "Logger.h" -#include "ManifestParser.h" -#include "Source.h" -#include "XmlPullParser.h" - -#include <string> - -namespace aapt { - -bool ManifestParser::parse(const Source& source, std::shared_ptr<XmlPullParser> parser, - AppInfo* outInfo) { - SourceLogger logger = { source }; - - int depth = 0; - while (XmlPullParser::isGoodEvent(parser->next())) { - XmlPullParser::Event event = parser->getEvent(); - if (event == XmlPullParser::Event::kEndElement) { - depth--; - continue; - } else if (event != XmlPullParser::Event::kStartElement) { - continue; - } - - depth++; - - const std::u16string& element = parser->getElementName(); - if (depth == 1) { - if (element == u"manifest") { - if (!parseManifest(logger, parser, outInfo)) { - return false; - } - } else { - logger.error() - << "unexpected top-level element '" - << element - << "'." - << std::endl; - return false; - } - } else { - XmlPullParser::skipCurrentElement(parser.get()); - } - } - - if (parser->getEvent() == XmlPullParser::Event::kBadDocument) { - logger.error(parser->getLineNumber()) - << "failed to parse manifest: " - << parser->getLastError() - << "." - << std::endl; - return false; - } - return true; -} - -bool ManifestParser::parseManifest(SourceLogger& logger, std::shared_ptr<XmlPullParser> parser, - AppInfo* outInfo) { - auto attrIter = parser->findAttribute(u"", u"package"); - if (attrIter == parser->endAttributes() || attrIter->value.empty()) { - logger.error() << "no 'package' attribute found for element <manifest>." << std::endl; - return false; - } - outInfo->package = attrIter->value; - return true; -} - -} // namespace aapt diff --git a/tools/aapt2/ManifestParser_test.cpp b/tools/aapt2/ManifestParser_test.cpp deleted file mode 100644 index be3a6fbe614a..000000000000 --- a/tools/aapt2/ManifestParser_test.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "AppInfo.h" -#include "ManifestParser.h" -#include "SourceXmlPullParser.h" - -#include <gtest/gtest.h> -#include <sstream> -#include <string> - -namespace aapt { - -TEST(ManifestParserTest, FindPackage) { - std::stringstream input; - input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" - "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" - "package=\"android\">\n" - "</manifest>\n"; - - ManifestParser parser; - AppInfo info; - std::shared_ptr<XmlPullParser> xmlParser = std::make_shared<SourceXmlPullParser>(input); - ASSERT_TRUE(parser.parse(Source{ "AndroidManifest.xml" }, xmlParser, &info)); - - EXPECT_EQ(std::u16string(u"android"), info.package); -} - -} // namespace aapt diff --git a/tools/aapt2/ManifestValidator.cpp b/tools/aapt2/ManifestValidator.cpp deleted file mode 100644 index 123b9fae2fba..000000000000 --- a/tools/aapt2/ManifestValidator.cpp +++ /dev/null @@ -1,217 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Logger.h" -#include "ManifestValidator.h" -#include "Maybe.h" -#include "Source.h" -#include "Util.h" - -#include <androidfw/ResourceTypes.h> - -namespace aapt { - -ManifestValidator::ManifestValidator(const android::ResTable& table) -: mTable(table) { -} - -bool ManifestValidator::validate(const Source& source, android::ResXMLParser* parser) { - SourceLogger logger(source); - - android::ResXMLParser::event_code_t code; - while ((code = parser->next()) != android::ResXMLParser::END_DOCUMENT && - code != android::ResXMLParser::BAD_DOCUMENT) { - if (code != android::ResXMLParser::START_TAG) { - continue; - } - - size_t len = 0; - const StringPiece16 namespaceUri(parser->getElementNamespace(&len), len); - if (!namespaceUri.empty()) { - continue; - } - - const StringPiece16 name(parser->getElementName(&len), len); - if (name.empty()) { - logger.error(parser->getLineNumber()) - << "failed to get the element name." - << std::endl; - return false; - } - - if (name == u"manifest") { - if (!validateManifest(source, parser)) { - return false; - } - } - } - return true; -} - -Maybe<StringPiece16> ManifestValidator::getAttributeValue(android::ResXMLParser* parser, - size_t idx) { - android::Res_value value; - if (parser->getAttributeValue(idx, &value) < 0) { - return StringPiece16(); - } - - const android::ResStringPool* pool = &parser->getStrings(); - if (value.dataType == android::Res_value::TYPE_REFERENCE) { - ssize_t strIdx = mTable.resolveReference(&value, 0x10000000u); - if (strIdx < 0) { - return {}; - } - pool = mTable.getTableStringBlock(strIdx); - } - - if (value.dataType != android::Res_value::TYPE_STRING || !pool) { - return {}; - } - return util::getString(*pool, value.data); -} - -Maybe<StringPiece16> ManifestValidator::getAttributeInlineValue(android::ResXMLParser* parser, - size_t idx) { - android::Res_value value; - if (parser->getAttributeValue(idx, &value) < 0) { - return StringPiece16(); - } - - if (value.dataType != android::Res_value::TYPE_STRING) { - return {}; - } - return util::getString(parser->getStrings(), value.data); -} - -bool ManifestValidator::validateInlineAttribute(android::ResXMLParser* parser, size_t idx, - SourceLogger& logger, - const StringPiece16& charSet) { - size_t len = 0; - StringPiece16 element(parser->getElementName(&len), len); - StringPiece16 attributeName(parser->getAttributeName(idx, &len), len); - Maybe<StringPiece16> result = getAttributeInlineValue(parser, idx); - if (!result) { - logger.error(parser->getLineNumber()) - << "<" - << element - << "> must have a '" - << attributeName - << "' attribute with a string literal value." - << std::endl; - return false; - } - return validateAttributeImpl(element, attributeName, result.value(), charSet, - parser->getLineNumber(), logger); -} - -bool ManifestValidator::validateAttribute(android::ResXMLParser* parser, size_t idx, - SourceLogger& logger, const StringPiece16& charSet) { - size_t len = 0; - StringPiece16 element(parser->getElementName(&len), len); - StringPiece16 attributeName(parser->getAttributeName(idx, &len), len); - Maybe<StringPiece16> result = getAttributeValue(parser, idx); - if (!result) { - logger.error(parser->getLineNumber()) - << "<" - << element - << "> must have a '" - << attributeName - << "' attribute that points to a string." - << std::endl; - return false; - } - return validateAttributeImpl(element, attributeName, result.value(), charSet, - parser->getLineNumber(), logger); -} - -bool ManifestValidator::validateAttributeImpl(const StringPiece16& element, - const StringPiece16& attributeName, - const StringPiece16& attributeValue, - const StringPiece16& charSet, size_t lineNumber, - SourceLogger& logger) { - StringPiece16::const_iterator badIter = - util::findNonAlphaNumericAndNotInSet(attributeValue, charSet); - if (badIter != attributeValue.end()) { - logger.error(lineNumber) - << "tag <" - << element - << "> attribute '" - << attributeName - << "' has invalid character '" - << StringPiece16(badIter, 1) - << "'." - << std::endl; - return false; - } - - if (!attributeValue.empty()) { - StringPiece16 trimmed = util::trimWhitespace(attributeValue); - if (attributeValue.begin() != trimmed.begin()) { - logger.error(lineNumber) - << "tag <" - << element - << "> attribute '" - << attributeName - << "' can not start with whitespace." - << std::endl; - return false; - } - - if (attributeValue.end() != trimmed.end()) { - logger.error(lineNumber) - << "tag <" - << element - << "> attribute '" - << attributeName - << "' can not end with whitespace." - << std::endl; - return false; - } - } - return true; -} - -constexpr const char16_t* kPackageIdentSet = u"._"; - -bool ManifestValidator::validateManifest(const Source& source, android::ResXMLParser* parser) { - bool error = false; - SourceLogger logger(source); - - const StringPiece16 kAndroid = u"android"; - const StringPiece16 kPackage = u"package"; - const StringPiece16 kSharedUserId = u"sharedUserId"; - - ssize_t idx; - - idx = parser->indexOfAttribute(nullptr, 0, kPackage.data(), kPackage.size()); - if (idx < 0) { - logger.error(parser->getLineNumber()) - << "missing package attribute." - << std::endl; - error = true; - } else { - error |= !validateInlineAttribute(parser, idx, logger, kPackageIdentSet); - } - - idx = parser->indexOfAttribute(kAndroid.data(), kAndroid.size(), - kSharedUserId.data(), kSharedUserId.size()); - if (idx >= 0) { - error |= !validateInlineAttribute(parser, idx, logger, kPackageIdentSet); - } - return !error; -} - -} // namespace aapt diff --git a/tools/aapt2/ManifestValidator.h b/tools/aapt2/ManifestValidator.h deleted file mode 100644 index 318878499cfa..000000000000 --- a/tools/aapt2/ManifestValidator.h +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_MANIFEST_VALIDATOR_H -#define AAPT_MANIFEST_VALIDATOR_H - -#include "Logger.h" -#include "Maybe.h" -#include "Source.h" -#include "StringPiece.h" - -#include <androidfw/ResourceTypes.h> - -namespace aapt { - -class ManifestValidator { -public: - ManifestValidator(const android::ResTable& table); - ManifestValidator(const ManifestValidator&) = delete; - - bool validate(const Source& source, android::ResXMLParser* parser); - -private: - bool validateManifest(const Source& source, android::ResXMLParser* parser); - - Maybe<StringPiece16> getAttributeInlineValue(android::ResXMLParser* parser, size_t idx); - Maybe<StringPiece16> getAttributeValue(android::ResXMLParser* parser, size_t idx); - - bool validateInlineAttribute(android::ResXMLParser* parser, size_t idx, - SourceLogger& logger, const StringPiece16& charSet); - bool validateAttribute(android::ResXMLParser* parser, size_t idx, SourceLogger& logger, - const StringPiece16& charSet); - bool validateAttributeImpl(const StringPiece16& element, const StringPiece16& attributeName, - const StringPiece16& attributeValue, const StringPiece16& charSet, - size_t lineNumber, SourceLogger& logger); - - const android::ResTable& mTable; -}; - -} // namespace aapt - -#endif // AAPT_MANIFEST_VALIDATOR_H diff --git a/tools/aapt2/MockResolver.h b/tools/aapt2/MockResolver.h deleted file mode 100644 index 0c9b95464186..000000000000 --- a/tools/aapt2/MockResolver.h +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_MOCK_RESOLVER_H -#define AAPT_MOCK_RESOLVER_H - -#include "Maybe.h" -#include "Resolver.h" -#include "Resource.h" -#include "ResourceTable.h" -#include "ResourceTableResolver.h" -#include "ResourceValues.h" -#include "StringPiece.h" - -#include <map> -#include <string> - -namespace aapt { - -struct MockResolver : public IResolver { - MockResolver(const std::shared_ptr<ResourceTable>& table, - const std::map<ResourceName, ResourceId>& items) : - mResolver(std::make_shared<ResourceTableResolver>( - table, std::vector<std::shared_ptr<const android::AssetManager>>())), - mAttr(false, android::ResTable_map::TYPE_ANY), mItems(items) { - } - - virtual Maybe<ResourceId> findId(const ResourceName& name) override { - Maybe<ResourceId> result = mResolver->findId(name); - if (result) { - return result; - } - - const auto iter = mItems.find(name); - if (iter != mItems.end()) { - return iter->second; - } - return {}; - } - - virtual Maybe<Entry> findAttribute(const ResourceName& name) override { - Maybe<Entry> tableResult = mResolver->findAttribute(name); - if (tableResult) { - return tableResult; - } - - Maybe<ResourceId> result = findId(name); - if (result) { - if (name.type == ResourceType::kAttr) { - return Entry{ result.value(), &mAttr }; - } else { - return Entry{ result.value() }; - } - } - return {}; - } - - virtual Maybe<ResourceName> findName(ResourceId resId) override { - Maybe<ResourceName> result = mResolver->findName(resId); - if (result) { - return result; - } - - for (auto& p : mItems) { - if (p.second == resId) { - return p.first; - } - } - return {}; - } - -private: - std::shared_ptr<ResourceTableResolver> mResolver; - Attribute mAttr; - std::map<ResourceName, ResourceId> mItems; -}; - -} // namespace aapt - -#endif // AAPT_MOCK_RESOLVER_H diff --git a/tools/aapt2/NameMangler.h b/tools/aapt2/NameMangler.h index 1e15e2071e65..054b9ee116f4 100644 --- a/tools/aapt2/NameMangler.h +++ b/tools/aapt2/NameMangler.h @@ -17,19 +17,63 @@ #ifndef AAPT_NAME_MANGLER_H #define AAPT_NAME_MANGLER_H +#include "Resource.h" + +#include "util/Maybe.h" + +#include <set> #include <string> namespace aapt { -struct NameMangler { +struct NameManglerPolicy { + /** + * Represents the package we are trying to build. References pointing + * to this package are not mangled, and mangled references inherit this package name. + */ + std::u16string targetPackageName; + + /** + * We must know which references to mangle, and which to keep (android vs. com.android.support). + */ + std::set<std::u16string> packagesToMangle; +}; + +class NameMangler { +private: + NameManglerPolicy mPolicy; + +public: + NameMangler(NameManglerPolicy policy) : mPolicy(policy) { + } + + Maybe<ResourceName> mangleName(const ResourceName& name) { + if (mPolicy.targetPackageName == name.package || + mPolicy.packagesToMangle.count(name.package) == 0) { + return {}; + } + + return ResourceName{ + mPolicy.targetPackageName, + name.type, + mangleEntry(name.package, name.entry) + }; + } + + bool shouldMangle(const std::u16string& package) const { + if (package.empty() || mPolicy.targetPackageName == package) { + return false; + } + return mPolicy.packagesToMangle.count(package) != 0; + } + /** - * Mangles the name in `outName` with the `package` and stores the mangled - * result in `outName`. The mangled name should contain symbols that are - * illegal to define in XML, so that there will never be name mangling - * collisions. + * Returns a mangled name that is a combination of `name` and `package`. + * The mangled name should contain symbols that are illegal to define in XML, + * so that there will never be name mangling collisions. */ - static void mangle(const std::u16string& package, std::u16string* outName) { - *outName = package + u"$" + *outName; + static std::u16string mangleEntry(const std::u16string& package, const std::u16string& name) { + return package + u"$" + name; } /** diff --git a/tools/aapt2/Resolver.h b/tools/aapt2/Resolver.h deleted file mode 100644 index cb9318e24917..000000000000 --- a/tools/aapt2/Resolver.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_RESOLVER_H -#define AAPT_RESOLVER_H - -#include "Maybe.h" -#include "Resource.h" -#include "ResourceValues.h" - -#include <androidfw/ResourceTypes.h> - -namespace aapt { - -/** - * Resolves symbolic references (package:type/entry) into resource IDs/objects. - */ -class IResolver { -public: - virtual ~IResolver() {}; - - /** - * Holds the result of a resource name lookup. - */ - struct Entry { - /** - * The ID of the resource. ResourceId::isValid() may - * return false if the resource has not been assigned - * an ID. - */ - ResourceId id; - - /** - * If the resource is an attribute, this will point - * to a valid Attribute object, or else it will be - * nullptr. - */ - const Attribute* attr; - }; - - /** - * Returns a ResourceID if the name is found. The ResourceID - * may not be valid if the resource was not assigned an ID. - */ - virtual Maybe<ResourceId> findId(const ResourceName& name) = 0; - - /** - * Returns an Entry if the name is found. Entry::attr - * may be nullptr if the resource is not an attribute. - */ - virtual Maybe<Entry> findAttribute(const ResourceName& name) = 0; - - /** - * Find a resource by ID. Resolvers may contain resources without - * resource IDs assigned to them. - */ - virtual Maybe<ResourceName> findName(ResourceId resId) = 0; -}; - -} // namespace aapt - -#endif // AAPT_RESOLVER_H diff --git a/tools/aapt2/Resource.cpp b/tools/aapt2/Resource.cpp index 287d8de1b767..9328b697719d 100644 --- a/tools/aapt2/Resource.cpp +++ b/tools/aapt2/Resource.cpp @@ -15,7 +15,7 @@ */ #include "Resource.h" -#include "StringPiece.h" +#include "util/StringPiece.h" #include <map> #include <string> @@ -28,7 +28,7 @@ StringPiece16 toString(ResourceType type) { case ResourceType::kAnimator: return u"animator"; case ResourceType::kArray: return u"array"; case ResourceType::kAttr: return u"attr"; - case ResourceType::kAttrPrivate: return u"attr"; + case ResourceType::kAttrPrivate: return u"^attr-private"; case ResourceType::kBool: return u"bool"; case ResourceType::kColor: return u"color"; case ResourceType::kDimen: return u"dimen"; @@ -36,7 +36,6 @@ StringPiece16 toString(ResourceType type) { case ResourceType::kFraction: return u"fraction"; case ResourceType::kId: return u"id"; case ResourceType::kInteger: return u"integer"; - case ResourceType::kIntegerArray: return u"integer-array"; case ResourceType::kInterpolator: return u"interpolator"; case ResourceType::kLayout: return u"layout"; case ResourceType::kMenu: return u"menu"; @@ -65,7 +64,6 @@ static const std::map<StringPiece16, ResourceType> sResourceTypeMap { { u"fraction", ResourceType::kFraction }, { u"id", ResourceType::kId }, { u"integer", ResourceType::kInteger }, - { u"integer-array", ResourceType::kIntegerArray }, { u"interpolator", ResourceType::kInterpolator }, { u"layout", ResourceType::kLayout }, { u"menu", ResourceType::kMenu }, @@ -87,4 +85,12 @@ const ResourceType* parseResourceType(const StringPiece16& str) { return &iter->second; } +bool operator<(const ResourceKey& a, const ResourceKey& b) { + return std::tie(a.name, a.config) < std::tie(b.name, b.config); +} + +bool operator<(const ResourceKeyRef& a, const ResourceKeyRef& b) { + return std::tie(a.name, a.config) < std::tie(b.name, b.config); +} + } // namespace aapt diff --git a/tools/aapt2/Resource.h b/tools/aapt2/Resource.h index fa9ac07b1779..c71e249d71f2 100644 --- a/tools/aapt2/Resource.h +++ b/tools/aapt2/Resource.h @@ -17,12 +17,16 @@ #ifndef AAPT_RESOURCE_H #define AAPT_RESOURCE_H -#include "StringPiece.h" +#include "ConfigDescription.h" +#include "Source.h" + +#include "util/StringPiece.h" #include <iomanip> #include <limits> #include <string> #include <tuple> +#include <vector> namespace aapt { @@ -43,7 +47,6 @@ enum class ResourceType { kFraction, kId, kInteger, - kIntegerArray, kInterpolator, kLayout, kMenu, @@ -74,10 +77,14 @@ struct ResourceName { ResourceType type; std::u16string entry; + ResourceName() = default; + ResourceName(const StringPiece16& p, ResourceType t, const StringPiece16& e); + bool isValid() const; bool operator<(const ResourceName& rhs) const; bool operator==(const ResourceName& rhs) const; bool operator!=(const ResourceName& rhs) const; + std::u16string toString() const; }; /** @@ -125,7 +132,7 @@ struct ResourceId { ResourceId(); ResourceId(const ResourceId& rhs); ResourceId(uint32_t resId); - ResourceId(size_t p, size_t t, size_t e); + ResourceId(uint8_t p, uint8_t t, uint16_t e); bool isValid() const; uint8_t packageId() const; @@ -135,6 +142,59 @@ struct ResourceId { bool operator==(const ResourceId& rhs) const; }; +struct SourcedResourceName { + ResourceName name; + size_t line; + + inline bool operator==(const SourcedResourceName& rhs) const { + return name == rhs.name && line == rhs.line; + } +}; + +struct ResourceFile { + // Name + ResourceName name; + + // Configuration + ConfigDescription config; + + // Source + Source source; + + // Exported symbols + std::vector<SourcedResourceName> exportedSymbols; +}; + +/** + * Useful struct used as a key to represent a unique resource in associative containers. + */ +struct ResourceKey { + ResourceName name; + ConfigDescription config; +}; + +bool operator<(const ResourceKey& a, const ResourceKey& b); + +/** + * Useful struct used as a key to represent a unique resource in associative containers. + * Holds a reference to the name, so that name better live longer than this key! + */ +struct ResourceKeyRef { + ResourceNameRef name; + ConfigDescription config; + + ResourceKeyRef() = default; + ResourceKeyRef(const ResourceNameRef& n, const ConfigDescription& c) : name(n), config(c) { + } + + /** + * Prevent taking a reference to a temporary. This is bad. + */ + ResourceKeyRef(ResourceName&& n, const ConfigDescription& c) = delete; +}; + +bool operator<(const ResourceKeyRef& a, const ResourceKeyRef& b); + // // ResourceId implementation. // @@ -148,17 +208,7 @@ inline ResourceId::ResourceId(const ResourceId& rhs) : id(rhs.id) { inline ResourceId::ResourceId(uint32_t resId) : id(resId) { } -inline ResourceId::ResourceId(size_t p, size_t t, size_t e) : id(0) { - if (p > std::numeric_limits<uint8_t>::max() || - t > std::numeric_limits<uint8_t>::max() || - e > std::numeric_limits<uint16_t>::max()) { - // This will leave the ResourceId in an invalid state. - return; - } - - id = (static_cast<uint8_t>(p) << 24) | - (static_cast<uint8_t>(t) << 16) | - static_cast<uint16_t>(e); +inline ResourceId::ResourceId(uint8_t p, uint8_t t, uint16_t e) : id((p << 24) | (t << 16) | e) { } inline bool ResourceId::isValid() const { @@ -208,6 +258,10 @@ inline ::std::ostream& operator<<(::std::ostream& out, const ResourceType& val) // ResourceName implementation. // +inline ResourceName::ResourceName(const StringPiece16& p, ResourceType t, const StringPiece16& e) : + package(p.toString()), type(t), entry(e.toString()) { +} + inline bool ResourceName::isValid() const { return !package.empty() && !entry.empty(); } @@ -217,6 +271,10 @@ inline bool ResourceName::operator<(const ResourceName& rhs) const { < std::tie(rhs.package, rhs.type, rhs.entry); } +inline bool operator<(const ResourceName& lhs, const ResourceNameRef& b) { + return ResourceNameRef(lhs) < b; +} + inline bool ResourceName::operator==(const ResourceName& rhs) const { return std::tie(package, type, entry) == std::tie(rhs.package, rhs.type, rhs.entry); @@ -227,6 +285,18 @@ inline bool ResourceName::operator!=(const ResourceName& rhs) const { != std::tie(rhs.package, rhs.type, rhs.entry); } +inline bool operator!=(const ResourceName& lhs, const ResourceNameRef& rhs) { + return ResourceNameRef(lhs) != rhs; +} + +inline std::u16string ResourceName::toString() const { + std::u16string result; + if (!package.empty()) { + result = package + u":"; + } + return result + aapt::toString(type).toString() + u"/" + entry; +} + inline ::std::ostream& operator<<(::std::ostream& out, const ResourceName& name) { if (!name.package.empty()) { out << name.package << ":"; diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp index 13f916bfc8f3..b37d366a7887 100644 --- a/tools/aapt2/ResourceParser.cpp +++ b/tools/aapt2/ResourceParser.cpp @@ -14,489 +14,161 @@ * limitations under the License. */ -#include "Logger.h" #include "ResourceParser.h" +#include "ResourceTable.h" +#include "ResourceUtils.h" #include "ResourceValues.h" -#include "ScopedXmlPullParser.h" -#include "SourceXmlPullParser.h" -#include "Util.h" -#include "XliffXmlPullParser.h" +#include "ValueVisitor.h" +#include "util/Comparators.h" +#include "util/ImmutableMap.h" +#include "util/Util.h" +#include "xml/XmlPullParser.h" +#include <functional> #include <sstream> namespace aapt { -void ResourceParser::extractResourceName(const StringPiece16& str, StringPiece16* outPackage, - StringPiece16* outType, StringPiece16* outEntry) { - const char16_t* start = str.data(); - const char16_t* end = start + str.size(); - const char16_t* current = start; - while (current != end) { - if (outType->size() == 0 && *current == u'/') { - outType->assign(start, current - start); - start = current + 1; - } else if (outPackage->size() == 0 && *current == u':') { - outPackage->assign(start, current - start); - start = current + 1; - } - current++; - } - outEntry->assign(start, end - start); -} - -bool ResourceParser::tryParseReference(const StringPiece16& str, ResourceNameRef* outRef, - bool* outCreate, bool* outPrivate) { - StringPiece16 trimmedStr(util::trimWhitespace(str)); - if (trimmedStr.empty()) { - return false; - } - - if (trimmedStr.data()[0] == u'@') { - size_t offset = 1; - *outCreate = false; - if (trimmedStr.data()[1] == u'+') { - *outCreate = true; - offset += 1; - } else if (trimmedStr.data()[1] == u'*') { - *outPrivate = true; - offset += 1; - } - StringPiece16 package; - StringPiece16 type; - StringPiece16 entry; - extractResourceName(trimmedStr.substr(offset, trimmedStr.size() - offset), - &package, &type, &entry); - - const ResourceType* parsedType = parseResourceType(type); - if (!parsedType) { - return false; - } - - if (*outCreate && *parsedType != ResourceType::kId) { - return false; - } +constexpr const char16_t* sXliffNamespaceUri = u"urn:oasis:names:tc:xliff:document:1.2"; - outRef->package = package; - outRef->type = *parsedType; - outRef->entry = entry; - return true; - } - return false; +/** + * Returns true if the element is <skip> or <eat-comment> and can be safely ignored. + */ +static bool shouldIgnoreElement(const StringPiece16& ns, const StringPiece16& name) { + return ns.empty() && (name == u"skip" || name == u"eat-comment"); } -bool ResourceParser::tryParseAttributeReference(const StringPiece16& str, - ResourceNameRef* outRef) { - StringPiece16 trimmedStr(util::trimWhitespace(str)); - if (trimmedStr.empty()) { - return false; - } - - if (*trimmedStr.data() == u'?') { - StringPiece16 package; - StringPiece16 type; - StringPiece16 entry; - extractResourceName(trimmedStr.substr(1, trimmedStr.size() - 1), &package, &type, &entry); +static uint32_t parseFormatType(const StringPiece16& piece) { + if (piece == u"reference") return android::ResTable_map::TYPE_REFERENCE; + else if (piece == u"string") return android::ResTable_map::TYPE_STRING; + else if (piece == u"integer") return android::ResTable_map::TYPE_INTEGER; + else if (piece == u"boolean") return android::ResTable_map::TYPE_BOOLEAN; + else if (piece == u"color") return android::ResTable_map::TYPE_COLOR; + else if (piece == u"float") return android::ResTable_map::TYPE_FLOAT; + else if (piece == u"dimension") return android::ResTable_map::TYPE_DIMENSION; + else if (piece == u"fraction") return android::ResTable_map::TYPE_FRACTION; + else if (piece == u"enum") return android::ResTable_map::TYPE_ENUM; + else if (piece == u"flags") return android::ResTable_map::TYPE_FLAGS; + return 0; +} - if (!type.empty() && type != u"attr") { - return false; +static uint32_t parseFormatAttribute(const StringPiece16& str) { + uint32_t mask = 0; + for (StringPiece16 part : util::tokenize(str, u'|')) { + StringPiece16 trimmedPart = util::trimWhitespace(part); + uint32_t type = parseFormatType(trimmedPart); + if (type == 0) { + return 0; } - - outRef->package = package; - outRef->type = ResourceType::kAttr; - outRef->entry = entry; - return true; + mask |= type; } - return false; + return mask; } -/* - * Style parent's are a bit different. We accept the following formats: - * - * @[package:]style/<entry> - * ?[package:]style/<entry> - * <package>:[style/]<entry> - * [package:style/]<entry> +/** + * A parsed resource ready to be added to the ResourceTable. */ -bool ResourceParser::parseStyleParentReference(const StringPiece16& str, Reference* outReference, - std::string* outError) { - if (str.empty()) { - return true; - } - - StringPiece16 name = str; - - bool hasLeadingIdentifiers = false; - bool privateRef = false; +struct ParsedResource { + ResourceName name; + ConfigDescription config; + Source source; + ResourceId id; + Maybe<SymbolState> symbolState; + std::u16string comment; + std::unique_ptr<Value> value; + std::list<ParsedResource> childResources; +}; - // Skip over these identifiers. A style's parent is a normal reference. - if (name.data()[0] == u'@' || name.data()[0] == u'?') { - hasLeadingIdentifiers = true; - name = name.substr(1, name.size() - 1); - if (name.data()[0] == u'*') { - privateRef = true; - name = name.substr(1, name.size() - 1); +bool ResourceParser::shouldStripResource(const ResourceNameRef& name, + const Maybe<std::u16string>& product) const { + if (product) { + for (const std::u16string& productToMatch : mOptions.products) { + if (product.value() == productToMatch) { + // We specified a product, and it is in the list, so don't strip. + return false; + } } } - ResourceNameRef ref; - ref.type = ResourceType::kStyle; - - StringPiece16 typeStr; - extractResourceName(name, &ref.package, &typeStr, &ref.entry); - if (!typeStr.empty()) { - // If we have a type, make sure it is a Style. - const ResourceType* parsedType = parseResourceType(typeStr); - if (!parsedType || *parsedType != ResourceType::kStyle) { - std::stringstream err; - err << "invalid resource type '" << typeStr << "' for parent of style"; - *outError = err.str(); - return false; - } - } else { - // No type was defined, this should not have a leading identifier. - if (hasLeadingIdentifiers) { - std::stringstream err; - err << "invalid parent reference '" << str << "'"; - *outError = err.str(); - return false; + // Nothing matched, try 'default'. Default only matches if we didn't already use another + // product variant. + if (!product || product.value() == u"default") { + if (Maybe<ResourceTable::SearchResult> result = mTable->findResource(name)) { + const ResourceEntry* entry = result.value().entry; + auto iter = std::lower_bound(entry->values.begin(), entry->values.end(), mConfig, + cmp::lessThanConfig); + if (iter != entry->values.end() && iter->config == mConfig && !iter->value->isWeak()) { + // We have a value for this config already, and it is not weak, + // so filter out this default. + return true; + } } - } - - if (!hasLeadingIdentifiers && ref.package.empty() && !typeStr.empty()) { - std::stringstream err; - err << "invalid parent reference '" << str << "'"; - *outError = err.str(); return false; } - - outReference->name = ref.toResourceName(); - outReference->privateReference = privateRef; return true; } -std::unique_ptr<Reference> ResourceParser::tryParseReference(const StringPiece16& str, - bool* outCreate) { - ResourceNameRef ref; - bool privateRef = false; - if (tryParseReference(str, &ref, outCreate, &privateRef)) { - std::unique_ptr<Reference> value = util::make_unique<Reference>(ref); - value->privateReference = privateRef; - return value; - } - - if (tryParseAttributeReference(str, &ref)) { - *outCreate = false; - return util::make_unique<Reference>(ref, Reference::Type::kAttribute); - } - return {}; -} - -std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseNullOrEmpty(const StringPiece16& str) { - StringPiece16 trimmedStr(util::trimWhitespace(str)); - android::Res_value value = {}; - if (trimmedStr == u"@null") { - // TYPE_NULL with data set to 0 is interpreted by the runtime as an error. - // Instead we set the data type to TYPE_REFERENCE with a value of 0. - value.dataType = android::Res_value::TYPE_REFERENCE; - } else if (trimmedStr == u"@empty") { - // TYPE_NULL with value of DATA_NULL_EMPTY is handled fine by the runtime. - value.dataType = android::Res_value::TYPE_NULL; - value.data = android::Res_value::DATA_NULL_EMPTY; - } else { - return {}; - } - return util::make_unique<BinaryPrimitive>(value); -} - -std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseEnumSymbol(const Attribute& enumAttr, - const StringPiece16& str) { - StringPiece16 trimmedStr(util::trimWhitespace(str)); - for (const auto& entry : enumAttr.symbols) { - // Enum symbols are stored as @package:id/symbol resources, - // so we need to match against the 'entry' part of the identifier. - const ResourceName& enumSymbolResourceName = entry.symbol.name; - if (trimmedStr == enumSymbolResourceName.entry) { - android::Res_value value = {}; - value.dataType = android::Res_value::TYPE_INT_DEC; - value.data = entry.value; - return util::make_unique<BinaryPrimitive>(value); +// Recursively adds resources to the ResourceTable. +static bool addResourcesToTable(ResourceTable* table, IDiagnostics* diag, ParsedResource* res) { + if (res->symbolState) { + Symbol symbol; + symbol.state = res->symbolState.value(); + symbol.source = res->source; + symbol.comment = res->comment; + if (!table->setSymbolState(res->name, res->id, symbol, diag)) { + return false; } } - return {}; -} - -std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseFlagSymbol(const Attribute& flagAttr, - const StringPiece16& str) { - android::Res_value flags = {}; - flags.dataType = android::Res_value::TYPE_INT_DEC; - for (StringPiece16 part : util::tokenize(str, u'|')) { - StringPiece16 trimmedPart = util::trimWhitespace(part); + if (res->value) { + // Attach the comment, source and config to the value. + res->value->setComment(std::move(res->comment)); + res->value->setSource(std::move(res->source)); - bool flagSet = false; - for (const auto& entry : flagAttr.symbols) { - // Flag symbols are stored as @package:id/symbol resources, - // so we need to match against the 'entry' part of the identifier. - const ResourceName& flagSymbolResourceName = entry.symbol.name; - if (trimmedPart == flagSymbolResourceName.entry) { - flags.data |= entry.value; - flagSet = true; - break; - } - } - - if (!flagSet) { - return {}; + if (!table->addResource(res->name, res->id, res->config, std::move(res->value), diag)) { + return false; } } - return util::make_unique<BinaryPrimitive>(flags); -} -static uint32_t parseHex(char16_t c, bool* outError) { - if (c >= u'0' && c <= u'9') { - return c - u'0'; - } else if (c >= u'a' && c <= u'f') { - return c - u'a' + 0xa; - } else if (c >= u'A' && c <= u'F') { - return c - u'A' + 0xa; - } else { - *outError = true; - return 0xffffffffu; - } -} - -std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseColor(const StringPiece16& str) { - StringPiece16 colorStr(util::trimWhitespace(str)); - const char16_t* start = colorStr.data(); - const size_t len = colorStr.size(); - if (len == 0 || start[0] != u'#') { - return {}; - } - - android::Res_value value = {}; bool error = false; - if (len == 4) { - value.dataType = android::Res_value::TYPE_INT_COLOR_RGB4; - value.data = 0xff000000u; - value.data |= parseHex(start[1], &error) << 20; - value.data |= parseHex(start[1], &error) << 16; - value.data |= parseHex(start[2], &error) << 12; - value.data |= parseHex(start[2], &error) << 8; - value.data |= parseHex(start[3], &error) << 4; - value.data |= parseHex(start[3], &error); - } else if (len == 5) { - value.dataType = android::Res_value::TYPE_INT_COLOR_ARGB4; - value.data |= parseHex(start[1], &error) << 28; - value.data |= parseHex(start[1], &error) << 24; - value.data |= parseHex(start[2], &error) << 20; - value.data |= parseHex(start[2], &error) << 16; - value.data |= parseHex(start[3], &error) << 12; - value.data |= parseHex(start[3], &error) << 8; - value.data |= parseHex(start[4], &error) << 4; - value.data |= parseHex(start[4], &error); - } else if (len == 7) { - value.dataType = android::Res_value::TYPE_INT_COLOR_RGB8; - value.data = 0xff000000u; - value.data |= parseHex(start[1], &error) << 20; - value.data |= parseHex(start[2], &error) << 16; - value.data |= parseHex(start[3], &error) << 12; - value.data |= parseHex(start[4], &error) << 8; - value.data |= parseHex(start[5], &error) << 4; - value.data |= parseHex(start[6], &error); - } else if (len == 9) { - value.dataType = android::Res_value::TYPE_INT_COLOR_ARGB8; - value.data |= parseHex(start[1], &error) << 28; - value.data |= parseHex(start[2], &error) << 24; - value.data |= parseHex(start[3], &error) << 20; - value.data |= parseHex(start[4], &error) << 16; - value.data |= parseHex(start[5], &error) << 12; - value.data |= parseHex(start[6], &error) << 8; - value.data |= parseHex(start[7], &error) << 4; - value.data |= parseHex(start[8], &error); - } else { - return {}; - } - return error ? std::unique_ptr<BinaryPrimitive>() : util::make_unique<BinaryPrimitive>(value); -} - -std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseBool(const StringPiece16& str) { - StringPiece16 trimmedStr(util::trimWhitespace(str)); - uint32_t data = 0; - if (trimmedStr == u"true" || trimmedStr == u"TRUE") { - data = 0xffffffffu; - } else if (trimmedStr != u"false" && trimmedStr != u"FALSE") { - return {}; - } - android::Res_value value = {}; - value.dataType = android::Res_value::TYPE_INT_BOOLEAN; - value.data = data; - return util::make_unique<BinaryPrimitive>(value); -} - -std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseInt(const StringPiece16& str) { - android::Res_value value; - if (!android::ResTable::stringToInt(str.data(), str.size(), &value)) { - return {}; - } - return util::make_unique<BinaryPrimitive>(value); -} - -std::unique_ptr<BinaryPrimitive> ResourceParser::tryParseFloat(const StringPiece16& str) { - android::Res_value value; - if (!android::ResTable::stringToFloat(str.data(), str.size(), &value)) { - return {}; + for (ParsedResource& child : res->childResources) { + error |= !addResourcesToTable(table, diag, &child); } - return util::make_unique<BinaryPrimitive>(value); + return !error; } -uint32_t ResourceParser::androidTypeToAttributeTypeMask(uint16_t type) { - switch (type) { - case android::Res_value::TYPE_NULL: - case android::Res_value::TYPE_REFERENCE: - case android::Res_value::TYPE_ATTRIBUTE: - case android::Res_value::TYPE_DYNAMIC_REFERENCE: - return android::ResTable_map::TYPE_REFERENCE; - - case android::Res_value::TYPE_STRING: - return android::ResTable_map::TYPE_STRING; - - case android::Res_value::TYPE_FLOAT: - return android::ResTable_map::TYPE_FLOAT; - - case android::Res_value::TYPE_DIMENSION: - return android::ResTable_map::TYPE_DIMENSION; - - case android::Res_value::TYPE_FRACTION: - return android::ResTable_map::TYPE_FRACTION; - - case android::Res_value::TYPE_INT_DEC: - case android::Res_value::TYPE_INT_HEX: - return android::ResTable_map::TYPE_INTEGER | - android::ResTable_map::TYPE_ENUM | - android::ResTable_map::TYPE_FLAGS; - - case android::Res_value::TYPE_INT_BOOLEAN: - return android::ResTable_map::TYPE_BOOLEAN; - - case android::Res_value::TYPE_INT_COLOR_ARGB8: - case android::Res_value::TYPE_INT_COLOR_RGB8: - case android::Res_value::TYPE_INT_COLOR_ARGB4: - case android::Res_value::TYPE_INT_COLOR_RGB4: - return android::ResTable_map::TYPE_COLOR; - - default: - return 0; - }; -} - -std::unique_ptr<Item> ResourceParser::parseItemForAttribute( - const StringPiece16& value, uint32_t typeMask, - std::function<void(const ResourceName&)> onCreateReference) { - std::unique_ptr<BinaryPrimitive> nullOrEmpty = tryParseNullOrEmpty(value); - if (nullOrEmpty) { - return std::move(nullOrEmpty); - } - - bool create = false; - std::unique_ptr<Reference> reference = tryParseReference(value, &create); - if (reference) { - if (create && onCreateReference) { - onCreateReference(reference->name); - } - return std::move(reference); - } - - if (typeMask & android::ResTable_map::TYPE_COLOR) { - // Try parsing this as a color. - std::unique_ptr<BinaryPrimitive> color = tryParseColor(value); - if (color) { - return std::move(color); - } - } - - if (typeMask & android::ResTable_map::TYPE_BOOLEAN) { - // Try parsing this as a boolean. - std::unique_ptr<BinaryPrimitive> boolean = tryParseBool(value); - if (boolean) { - return std::move(boolean); - } - } - - if (typeMask & android::ResTable_map::TYPE_INTEGER) { - // Try parsing this as an integer. - std::unique_ptr<BinaryPrimitive> integer = tryParseInt(value); - if (integer) { - return std::move(integer); - } - } - - const uint32_t floatMask = android::ResTable_map::TYPE_FLOAT | - android::ResTable_map::TYPE_DIMENSION | - android::ResTable_map::TYPE_FRACTION; - if (typeMask & floatMask) { - // Try parsing this as a float. - std::unique_ptr<BinaryPrimitive> floatingPoint = tryParseFloat(value); - if (floatingPoint) { - if (typeMask & androidTypeToAttributeTypeMask(floatingPoint->value.dataType)) { - return std::move(floatingPoint); - } - } - } - return {}; -} - -/** - * We successively try to parse the string as a resource type that the Attribute - * allows. - */ -std::unique_ptr<Item> ResourceParser::parseItemForAttribute( - const StringPiece16& str, const Attribute& attr, - std::function<void(const ResourceName&)> onCreateReference) { - const uint32_t typeMask = attr.typeMask; - std::unique_ptr<Item> value = parseItemForAttribute(str, typeMask, onCreateReference); - if (value) { - return value; - } - - if (typeMask & android::ResTable_map::TYPE_ENUM) { - // Try parsing this as an enum. - std::unique_ptr<BinaryPrimitive> enumValue = tryParseEnumSymbol(attr, str); - if (enumValue) { - return std::move(enumValue); - } - } - - if (typeMask & android::ResTable_map::TYPE_FLAGS) { - // Try parsing this as a flag. - std::unique_ptr<BinaryPrimitive> flagValue = tryParseFlagSymbol(attr, str); - if (flagValue) { - return std::move(flagValue); - } - } - return {}; -} +// Convenient aliases for more readable function calls. +enum { + kAllowRawString = true, + kNoRawString = false +}; -ResourceParser::ResourceParser(const std::shared_ptr<ResourceTable>& table, const Source& source, +ResourceParser::ResourceParser(IDiagnostics* diag, ResourceTable* table, const Source& source, const ConfigDescription& config, - const std::shared_ptr<XmlPullParser>& parser) : - mTable(table), mSource(source), mConfig(config), mLogger(source), - mParser(std::make_shared<XliffXmlPullParser>(parser)) { + const ResourceParserOptions& options) : + mDiag(diag), mTable(table), mSource(source), mConfig(config), mOptions(options) { } /** * Build a string from XML that converts nested elements into Span objects. */ -bool ResourceParser::flattenXmlSubtree(XmlPullParser* parser, std::u16string* outRawString, +bool ResourceParser::flattenXmlSubtree(xml::XmlPullParser* parser, std::u16string* outRawString, StyleString* outStyleString) { std::vector<Span> spanStack; + bool error = false; outRawString->clear(); outStyleString->spans.clear(); util::StringBuilder builder; size_t depth = 1; - while (XmlPullParser::isGoodEvent(parser->next())) { - const XmlPullParser::Event event = parser->getEvent(); - if (event == XmlPullParser::Event::kEndElement) { + while (xml::XmlPullParser::isGoodEvent(parser->next())) { + const xml::XmlPullParser::Event event = parser->getEvent(); + if (event == xml::XmlPullParser::Event::kEndElement) { + if (!parser->getElementNamespace().empty()) { + // We already warned and skipped the start element, so just skip here too + continue; + } + depth--; if (depth == 0) { break; @@ -506,21 +178,21 @@ bool ResourceParser::flattenXmlSubtree(XmlPullParser* parser, std::u16string* ou outStyleString->spans.push_back(spanStack.back()); spanStack.pop_back(); - } else if (event == XmlPullParser::Event::kText) { - // TODO(adamlesinski): Verify format strings. + } else if (event == xml::XmlPullParser::Event::kText) { outRawString->append(parser->getText()); builder.append(parser->getText()); - } else if (event == XmlPullParser::Event::kStartElement) { - if (parser->getElementNamespace().size() > 0) { - mLogger.warn(parser->getLineNumber()) - << "skipping element '" - << parser->getElementName() - << "' with unknown namespace '" - << parser->getElementNamespace() - << "'." - << std::endl; - XmlPullParser::skipCurrentElement(parser); + } else if (event == xml::XmlPullParser::Event::kStartElement) { + if (!parser->getElementNamespace().empty()) { + if (parser->getElementNamespace() != sXliffNamespaceUri) { + // Only warn if this isn't an xliff namespace. + mDiag->warn(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "skipping element '" + << parser->getElementName() + << "' with unknown namespace '" + << parser->getElementNamespace() + << "'"); + } continue; } depth++; @@ -536,205 +208,294 @@ bool ResourceParser::flattenXmlSubtree(XmlPullParser* parser, std::u16string* ou } if (builder.str().size() > std::numeric_limits<uint32_t>::max()) { - mLogger.error(parser->getLineNumber()) - << "style string '" - << builder.str() - << "' is too long." - << std::endl; - return false; + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "style string '" << builder.str() << "' is too long"); + error = true; + } else { + spanStack.push_back(Span{ spanName, static_cast<uint32_t>(builder.str().size()) }); } - spanStack.push_back(Span{ spanName, static_cast<uint32_t>(builder.str().size()) }); - } else if (event == XmlPullParser::Event::kComment) { + } else if (event == xml::XmlPullParser::Event::kComment) { // Skip } else { - mLogger.warn(parser->getLineNumber()) - << "unknown event " - << event - << "." - << std::endl; + assert(false); } } assert(spanStack.empty() && "spans haven't been fully processed"); outStyleString->str = builder.str(); - return true; + return !error; } -bool ResourceParser::parse() { - while (XmlPullParser::isGoodEvent(mParser->next())) { - if (mParser->getEvent() != XmlPullParser::Event::kStartElement) { +bool ResourceParser::parse(xml::XmlPullParser* parser) { + bool error = false; + const size_t depth = parser->getDepth(); + while (xml::XmlPullParser::nextChildNode(parser, depth)) { + if (parser->getEvent() != xml::XmlPullParser::Event::kStartElement) { + // Skip comments and text. continue; } - ScopedXmlPullParser parser(mParser.get()); - if (!parser.getElementNamespace().empty() || - parser.getElementName() != u"resources") { - mLogger.error(parser.getLineNumber()) - << "root element must be <resources> in the global namespace." - << std::endl; + if (!parser->getElementNamespace().empty() || parser->getElementName() != u"resources") { + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "root element must be <resources>"); return false; } - if (!parseResources(&parser)) { - return false; - } - } + error |= !parseResources(parser); + break; + }; - if (mParser->getEvent() == XmlPullParser::Event::kBadDocument) { - mLogger.error(mParser->getLineNumber()) - << mParser->getLastError() - << std::endl; + if (parser->getEvent() == xml::XmlPullParser::Event::kBadDocument) { + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "xml parser error: " << parser->getLastError()); return false; } - return true; + return !error; } -bool ResourceParser::parseResources(XmlPullParser* parser) { - bool success = true; +bool ResourceParser::parseResources(xml::XmlPullParser* parser) { + std::set<ResourceName> strippedResources; + bool error = false; std::u16string comment; - while (XmlPullParser::isGoodEvent(parser->next())) { - const XmlPullParser::Event event = parser->getEvent(); - if (event == XmlPullParser::Event::kComment) { + const size_t depth = parser->getDepth(); + while (xml::XmlPullParser::nextChildNode(parser, depth)) { + const xml::XmlPullParser::Event event = parser->getEvent(); + if (event == xml::XmlPullParser::Event::kComment) { comment = parser->getComment(); continue; } - if (event == XmlPullParser::Event::kText) { + if (event == xml::XmlPullParser::Event::kText) { if (!util::trimWhitespace(parser->getText()).empty()) { - comment = u""; + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "plain text not allowed here"); + error = true; } continue; } - if (event != XmlPullParser::Event::kStartElement) { - continue; - } + assert(event == xml::XmlPullParser::Event::kStartElement); - ScopedXmlPullParser childParser(parser); - - if (!childParser.getElementNamespace().empty()) { + if (!parser->getElementNamespace().empty()) { // Skip unknown namespace. continue; } - StringPiece16 name = childParser.getElementName(); - if (name == u"skip" || name == u"eat-comment") { + std::u16string elementName = parser->getElementName(); + if (elementName == u"skip" || elementName == u"eat-comment") { + comment = u""; continue; } - if (name == u"private-symbols") { - // Handle differently. - mLogger.note(childParser.getLineNumber()) - << "got a <private-symbols> tag." - << std::endl; - continue; + ParsedResource parsedResource; + parsedResource.config = mConfig; + parsedResource.source = mSource.withLine(parser->getLineNumber()); + parsedResource.comment = std::move(comment); + + // Extract the product name if it exists. + Maybe<std::u16string> product; + if (Maybe<StringPiece16> maybeProduct = xml::findNonEmptyAttribute(parser, u"product")) { + product = maybeProduct.value().toString(); } - const auto endAttrIter = childParser.endAttributes(); - auto attrIter = childParser.findAttribute(u"", u"name"); - if (attrIter == endAttrIter || attrIter->value.empty()) { - mLogger.error(childParser.getLineNumber()) - << "<" << name << "> tag must have a 'name' attribute." - << std::endl; - success = false; + // Parse the resource regardless of product. + if (!parseResource(parser, &parsedResource)) { + error = true; continue; } - // Copy because our iterator will go out of scope when - // we parse more XML. - std::u16string attributeName = attrIter->value; - - if (name == u"item") { - // Items simply have their type encoded in the type attribute. - auto typeIter = childParser.findAttribute(u"", u"type"); - if (typeIter == endAttrIter || typeIter->value.empty()) { - mLogger.error(childParser.getLineNumber()) - << "<item> must have a 'type' attribute." - << std::endl; - success = false; - continue; - } - name = typeIter->value; + // We successfully parsed the resource. Check if we should include it or strip it. + if (shouldStripResource(parsedResource.name, product)) { + // Record that we stripped out this resource name. + // We will check that at least one variant of this resource was included. + strippedResources.insert(parsedResource.name); + } else if (!addResourcesToTable(mTable, mDiag, &parsedResource)) { + error = true; } + } - if (name == u"id") { - success &= mTable->addResource(ResourceNameRef{ {}, ResourceType::kId, attributeName }, - {}, mSource.line(childParser.getLineNumber()), - util::make_unique<Id>()); - } else if (name == u"string") { - success &= parseString(&childParser, - ResourceNameRef{ {}, ResourceType::kString, attributeName }); - } else if (name == u"color") { - success &= parseColor(&childParser, - ResourceNameRef{ {}, ResourceType::kColor, attributeName }); - } else if (name == u"drawable") { - success &= parseColor(&childParser, - ResourceNameRef{ {}, ResourceType::kDrawable, attributeName }); - } else if (name == u"bool") { - success &= parsePrimitive(&childParser, - ResourceNameRef{ {}, ResourceType::kBool, attributeName }); - } else if (name == u"integer") { - success &= parsePrimitive( - &childParser, - ResourceNameRef{ {}, ResourceType::kInteger, attributeName }); - } else if (name == u"dimen") { - success &= parsePrimitive(&childParser, - ResourceNameRef{ {}, ResourceType::kDimen, attributeName }); - } else if (name == u"fraction") { -// success &= parsePrimitive( -// &childParser, -// ResourceNameRef{ {}, ResourceType::kFraction, attributeName }); - } else if (name == u"style") { - success &= parseStyle(&childParser, - ResourceNameRef{ {}, ResourceType::kStyle, attributeName }); - } else if (name == u"plurals") { - success &= parsePlural(&childParser, - ResourceNameRef{ {}, ResourceType::kPlurals, attributeName }); - } else if (name == u"array") { - success &= parseArray(&childParser, - ResourceNameRef{ {}, ResourceType::kArray, attributeName }, - android::ResTable_map::TYPE_ANY); - } else if (name == u"string-array") { - success &= parseArray(&childParser, - ResourceNameRef{ {}, ResourceType::kArray, attributeName }, - android::ResTable_map::TYPE_STRING); - } else if (name == u"integer-array") { - success &= parseArray(&childParser, - ResourceNameRef{ {}, ResourceType::kArray, attributeName }, - android::ResTable_map::TYPE_INTEGER); - } else if (name == u"public") { - success &= parsePublic(&childParser, attributeName); - } else if (name == u"declare-styleable") { - success &= parseDeclareStyleable( - &childParser, - ResourceNameRef{ {}, ResourceType::kStyleable, attributeName }); - } else if (name == u"attr") { - success &= parseAttr(&childParser, - ResourceNameRef{ {}, ResourceType::kAttr, attributeName }); - } else if (name == u"bag") { - } else if (name == u"public-padding") { - } else if (name == u"java-symbol") { - } else if (name == u"add-resource") { - } - } - - if (parser->getEvent() == XmlPullParser::Event::kBadDocument) { - mLogger.error(parser->getLineNumber()) - << parser->getLastError() - << std::endl; - return false; + // Check that we included at least one variant of each stripped resource. + for (const ResourceName& strippedResource : strippedResources) { + if (!mTable->findResource(strippedResource)) { + // Failed to find the resource. + mDiag->error(DiagMessage(mSource) << "resource '" << strippedResource << "' " + "was filtered out but no product variant remains"); + error = true; + } } - return success; + + return !error; } +bool ResourceParser::parseResource(xml::XmlPullParser* parser, ParsedResource* outResource) { + struct ItemTypeFormat { + ResourceType type; + uint32_t format; + }; -enum { - kAllowRawString = true, - kNoRawString = false -}; + using BagParseFunc = std::function<bool(ResourceParser*, xml::XmlPullParser*, ParsedResource*)>; + + static const auto elToItemMap = ImmutableMap<std::u16string, ItemTypeFormat>::createPreSorted({ + { u"bool", { ResourceType::kBool, android::ResTable_map::TYPE_BOOLEAN } }, + { u"color", { ResourceType::kColor, android::ResTable_map::TYPE_COLOR } }, + { u"dimen", { ResourceType::kDimen, android::ResTable_map::TYPE_FLOAT + | android::ResTable_map::TYPE_FRACTION + | android::ResTable_map::TYPE_DIMENSION } }, + { u"drawable", { ResourceType::kDrawable, android::ResTable_map::TYPE_COLOR } }, + { u"fraction", { ResourceType::kFraction, android::ResTable_map::TYPE_FLOAT + | android::ResTable_map::TYPE_FRACTION + | android::ResTable_map::TYPE_DIMENSION } }, + { u"integer", { ResourceType::kInteger, android::ResTable_map::TYPE_INTEGER } }, + { u"string", { ResourceType::kString, android::ResTable_map::TYPE_STRING } }, + }); + + static const auto elToBagMap = ImmutableMap<std::u16string, BagParseFunc>::createPreSorted({ + { u"add-resource", std::mem_fn(&ResourceParser::parseAddResource) }, + { u"array", std::mem_fn(&ResourceParser::parseArray) }, + { u"attr", std::mem_fn(&ResourceParser::parseAttr) }, + { u"declare-styleable", std::mem_fn(&ResourceParser::parseDeclareStyleable) }, + { u"integer-array", std::mem_fn(&ResourceParser::parseIntegerArray) }, + { u"java-symbol", std::mem_fn(&ResourceParser::parseSymbol) }, + { u"plurals", std::mem_fn(&ResourceParser::parsePlural) }, + { u"public", std::mem_fn(&ResourceParser::parsePublic) }, + { u"public-group", std::mem_fn(&ResourceParser::parsePublicGroup) }, + { u"string-array", std::mem_fn(&ResourceParser::parseStringArray) }, + { u"style", std::mem_fn(&ResourceParser::parseStyle) }, + { u"symbol", std::mem_fn(&ResourceParser::parseSymbol) }, + }); + + std::u16string resourceType = parser->getElementName(); + + // The value format accepted for this resource. + uint32_t resourceFormat = 0u; + + if (resourceType == u"item") { + // Items have their type encoded in the type attribute. + if (Maybe<StringPiece16> maybeType = xml::findNonEmptyAttribute(parser, u"type")) { + resourceType = maybeType.value().toString(); + } else { + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "<item> must have a 'type' attribute"); + return false; + } + + if (Maybe<StringPiece16> maybeFormat = xml::findNonEmptyAttribute(parser, u"format")) { + // An explicit format for this resource was specified. The resource will retain + // its type in its name, but the accepted value for this type is overridden. + resourceFormat = parseFormatType(maybeFormat.value()); + if (!resourceFormat) { + mDiag->error(DiagMessage(outResource->source) + << "'" << maybeFormat.value() << "' is an invalid format"); + return false; + } + } + } + + // Get the name of the resource. This will be checked later, because not all + // XML elements require a name. + Maybe<StringPiece16> maybeName = xml::findNonEmptyAttribute(parser, u"name"); + + if (resourceType == u"id") { + if (!maybeName) { + mDiag->error(DiagMessage(outResource->source) + << "<" << parser->getElementName() << "> missing 'name' attribute"); + return false; + } + + outResource->name.type = ResourceType::kId; + outResource->name.entry = maybeName.value().toString(); + outResource->value = util::make_unique<Id>(); + return true; + } + + const auto itemIter = elToItemMap.find(resourceType); + if (itemIter != elToItemMap.end()) { + // This is an item, record its type and format and start parsing. + + if (!maybeName) { + mDiag->error(DiagMessage(outResource->source) + << "<" << parser->getElementName() << "> missing 'name' attribute"); + return false; + } + + outResource->name.type = itemIter->second.type; + outResource->name.entry = maybeName.value().toString(); + + // Only use the implicit format for this type if it wasn't overridden. + if (!resourceFormat) { + resourceFormat = itemIter->second.format; + } + + if (!parseItem(parser, outResource, resourceFormat)) { + return false; + } + return true; + } + + // This might be a bag or something. + const auto bagIter = elToBagMap.find(resourceType); + if (bagIter != elToBagMap.end()) { + // Ensure we have a name (unless this is a <public-group>). + if (resourceType != u"public-group") { + if (!maybeName) { + mDiag->error(DiagMessage(outResource->source) + << "<" << parser->getElementName() << "> missing 'name' attribute"); + return false; + } + + outResource->name.entry = maybeName.value().toString(); + } + + // Call the associated parse method. The type will be filled in by the + // parse func. + if (!bagIter->second(this, parser, outResource)) { + return false; + } + return true; + } + + // Try parsing the elementName (or type) as a resource. These shall only be + // resources like 'layout' or 'xml' and they can only be references. + const ResourceType* parsedType = parseResourceType(resourceType); + if (parsedType) { + if (!maybeName) { + mDiag->error(DiagMessage(outResource->source) + << "<" << parser->getElementName() << "> missing 'name' attribute"); + return false; + } + + outResource->name.type = *parsedType; + outResource->name.entry = maybeName.value().toString(); + outResource->value = parseXml(parser, android::ResTable_map::TYPE_REFERENCE, kNoRawString); + if (!outResource->value) { + mDiag->error(DiagMessage(outResource->source) + << "invalid value for type '" << *parsedType << "'. Expected a reference"); + return false; + } + return true; + } + + mDiag->warn(DiagMessage(outResource->source) + << "unknown resource type '" << parser->getElementName() << "'"); + return false; +} + +bool ResourceParser::parseItem(xml::XmlPullParser* parser, ParsedResource* outResource, + const uint32_t format) { + if (format == android::ResTable_map::TYPE_STRING) { + return parseString(parser, outResource); + } + + outResource->value = parseXml(parser, format, kNoRawString); + if (!outResource->value) { + mDiag->error(DiagMessage(outResource->source) << "invalid " << outResource->name.type); + return false; + } + return true; +} /** * Reads the entire XML subtree and attempts to parse it as some Item, @@ -743,8 +504,8 @@ enum { * an Item. If allowRawValue is false, nullptr is returned in this * case. */ -std::unique_ptr<Item> ResourceParser::parseXml(XmlPullParser* parser, uint32_t typeMask, - bool allowRawValue) { +std::unique_ptr<Item> ResourceParser::parseXml(xml::XmlPullParser* parser, const uint32_t typeMask, + const bool allowRawValue) { const size_t beginXmlLine = parser->getLineNumber(); std::u16string rawValue; @@ -753,34 +514,27 @@ std::unique_ptr<Item> ResourceParser::parseXml(XmlPullParser* parser, uint32_t t return {}; } - StringPool& pool = mTable->getValueStringPool(); - if (!styleString.spans.empty()) { // This can only be a StyledString. return util::make_unique<StyledString>( - pool.makeRef(styleString, StringPool::Context{ 1, mConfig })); + mTable->stringPool.makeRef(styleString, StringPool::Context{ 1, mConfig })); } auto onCreateReference = [&](const ResourceName& name) { // name.package can be empty here, as it will assume the package name of the table. - mTable->addResource(name, {}, mSource.line(beginXmlLine), util::make_unique<Id>()); + std::unique_ptr<Id> id = util::make_unique<Id>(); + id->setSource(mSource.withLine(beginXmlLine)); + mTable->addResource(name, {}, std::move(id), mDiag); }; // Process the raw value. - std::unique_ptr<Item> processedItem = parseItemForAttribute(rawValue, typeMask, - onCreateReference); + std::unique_ptr<Item> processedItem = ResourceUtils::parseItemForAttribute(rawValue, typeMask, + onCreateReference); if (processedItem) { // Fix up the reference. - visitFunc<Reference>(*processedItem, [&](Reference& ref) { - if (!ref.name.package.empty()) { - // The package name was set, so lookup its alias. - parser->applyPackageAlias(&ref.name.package, mTable->getPackage()); - } else { - // The package name was left empty, so it assumes the default package - // without alias lookup. - ref.name.package = mTable->getPackage(); - } - }); + if (Reference* ref = valueCast<Reference>(processedItem.get())) { + transformReferenceFromNamespace(parser, u"", ref); + } return processedItem; } @@ -788,320 +542,428 @@ std::unique_ptr<Item> ResourceParser::parseXml(XmlPullParser* parser, uint32_t t if (typeMask & android::ResTable_map::TYPE_STRING) { // Use the trimmed, escaped string. return util::make_unique<String>( - pool.makeRef(styleString.str, StringPool::Context{ 1, mConfig })); + mTable->stringPool.makeRef(styleString.str, StringPool::Context{ 1, mConfig })); } - // We can't parse this so return a RawString if we are allowed. if (allowRawValue) { + // We can't parse this so return a RawString if we are allowed. return util::make_unique<RawString>( - pool.makeRef(rawValue, StringPool::Context{ 1, mConfig })); + mTable->stringPool.makeRef(rawValue, StringPool::Context{ 1, mConfig })); } return {}; } -bool ResourceParser::parseString(XmlPullParser* parser, const ResourceNameRef& resourceName) { - const SourceLine source = mSource.line(parser->getLineNumber()); - - // Mark the string as untranslateable if needed. - const auto endAttrIter = parser->endAttributes(); - auto attrIter = parser->findAttribute(u"", u"untranslateable"); - // bool untranslateable = attrIter != endAttrIter; - // TODO(adamlesinski): Do something with this (mark the string). - - // Deal with the product. - attrIter = parser->findAttribute(u"", u"product"); - if (attrIter != endAttrIter) { - if (attrIter->value != u"default" && attrIter->value != u"phone") { - // TODO(adamlesinski): Match products. - return true; +bool ResourceParser::parseString(xml::XmlPullParser* parser, ParsedResource* outResource) { + bool formatted = true; + if (Maybe<StringPiece16> formattedAttr = xml::findAttribute(parser, u"formatted")) { + if (!ResourceUtils::tryParseBool(formattedAttr.value(), &formatted)) { + mDiag->error(DiagMessage(outResource->source) + << "invalid value for 'formatted'. Must be a boolean"); + return false; } } - std::unique_ptr<Item> processedItem = parseXml(parser, android::ResTable_map::TYPE_STRING, - kNoRawString); - if (!processedItem) { - mLogger.error(source.line) - << "not a valid string." - << std::endl; - return false; + bool translateable = mOptions.translatable; + if (Maybe<StringPiece16> translateableAttr = xml::findAttribute(parser, u"translatable")) { + if (!ResourceUtils::tryParseBool(translateableAttr.value(), &translateable)) { + mDiag->error(DiagMessage(outResource->source) + << "invalid value for 'translatable'. Must be a boolean"); + return false; + } } - return mTable->addResource(resourceName, mConfig, source, std::move(processedItem)); -} - -bool ResourceParser::parseColor(XmlPullParser* parser, const ResourceNameRef& resourceName) { - const SourceLine source = mSource.line(parser->getLineNumber()); - - std::unique_ptr<Item> item = parseXml(parser, android::ResTable_map::TYPE_COLOR, kNoRawString); - if (!item) { - mLogger.error(source.line) << "invalid color." << std::endl; + outResource->value = parseXml(parser, android::ResTable_map::TYPE_STRING, kNoRawString); + if (!outResource->value) { + mDiag->error(DiagMessage(outResource->source) << "not a valid string"); return false; } - return mTable->addResource(resourceName, mConfig, source, std::move(item)); -} - -bool ResourceParser::parsePrimitive(XmlPullParser* parser, const ResourceNameRef& resourceName) { - const SourceLine source = mSource.line(parser->getLineNumber()); - - uint32_t typeMask = 0; - switch (resourceName.type) { - case ResourceType::kInteger: - typeMask |= android::ResTable_map::TYPE_INTEGER; - break; - case ResourceType::kDimen: - typeMask |= android::ResTable_map::TYPE_DIMENSION - | android::ResTable_map::TYPE_FLOAT - | android::ResTable_map::TYPE_FRACTION; - break; + if (String* stringValue = valueCast<String>(outResource->value.get())) { + stringValue->setTranslateable(translateable); - case ResourceType::kBool: - typeMask |= android::ResTable_map::TYPE_BOOLEAN; - break; + if (formatted && translateable) { + if (!util::verifyJavaStringFormat(*stringValue->value)) { + DiagMessage msg(outResource->source); + msg << "multiple substitutions specified in non-positional format; " + "did you mean to add the formatted=\"false\" attribute?"; + if (mOptions.errorOnPositionalArguments) { + mDiag->error(msg); + return false; + } - default: - assert(false); - break; - } + mDiag->warn(msg); + } + } - std::unique_ptr<Item> item = parseXml(parser, typeMask, kNoRawString); - if (!item) { - mLogger.error(source.line) - << "invalid " - << resourceName.type - << "." - << std::endl; - return false; + } else if (StyledString* stringValue = valueCast<StyledString>(outResource->value.get())) { + stringValue->setTranslateable(translateable); } - - return mTable->addResource(resourceName, mConfig, source, std::move(item)); + return true; } -bool ResourceParser::parsePublic(XmlPullParser* parser, const StringPiece16& name) { - const SourceLine source = mSource.line(parser->getLineNumber()); - - const auto endAttrIter = parser->endAttributes(); - const auto typeAttrIter = parser->findAttribute(u"", u"type"); - if (typeAttrIter == endAttrIter || typeAttrIter->value.empty()) { - mLogger.error(source.line) - << "<public> must have a 'type' attribute." - << std::endl; +bool ResourceParser::parsePublic(xml::XmlPullParser* parser, ParsedResource* outResource) { + Maybe<StringPiece16> maybeType = xml::findNonEmptyAttribute(parser, u"type"); + if (!maybeType) { + mDiag->error(DiagMessage(outResource->source) << "<public> must have a 'type' attribute"); return false; } - const ResourceType* parsedType = parseResourceType(typeAttrIter->value); + const ResourceType* parsedType = parseResourceType(maybeType.value()); if (!parsedType) { - mLogger.error(source.line) - << "invalid resource type '" - << typeAttrIter->value - << "' in <public>." - << std::endl; + mDiag->error(DiagMessage(outResource->source) + << "invalid resource type '" << maybeType.value() << "' in <public>"); return false; } - ResourceNameRef resourceName { {}, *parsedType, name }; - ResourceId resourceId; + outResource->name.type = *parsedType; - const auto idAttrIter = parser->findAttribute(u"", u"id"); - if (idAttrIter != endAttrIter && !idAttrIter->value.empty()) { + if (Maybe<StringPiece16> maybeId = xml::findNonEmptyAttribute(parser, u"id")) { android::Res_value val; - bool result = android::ResTable::stringToInt(idAttrIter->value.data(), - idAttrIter->value.size(), &val); - resourceId.id = val.data; + bool result = android::ResTable::stringToInt(maybeId.value().data(), + maybeId.value().size(), &val); + ResourceId resourceId(val.data); if (!result || !resourceId.isValid()) { - mLogger.error(source.line) - << "invalid resource ID '" - << idAttrIter->value - << "' in <public>." - << std::endl; + mDiag->error(DiagMessage(outResource->source) + << "invalid resource ID '" << maybeId.value() << "' in <public>"); return false; } + outResource->id = resourceId; } if (*parsedType == ResourceType::kId) { // An ID marked as public is also the definition of an ID. - mTable->addResource(resourceName, {}, source, util::make_unique<Id>()); + outResource->value = util::make_unique<Id>(); } - return mTable->markPublic(resourceName, resourceId, source); + outResource->symbolState = SymbolState::kPublic; + return true; } -static uint32_t parseFormatType(const StringPiece16& piece) { - if (piece == u"reference") return android::ResTable_map::TYPE_REFERENCE; - else if (piece == u"string") return android::ResTable_map::TYPE_STRING; - else if (piece == u"integer") return android::ResTable_map::TYPE_INTEGER; - else if (piece == u"boolean") return android::ResTable_map::TYPE_BOOLEAN; - else if (piece == u"color") return android::ResTable_map::TYPE_COLOR; - else if (piece == u"float") return android::ResTable_map::TYPE_FLOAT; - else if (piece == u"dimension") return android::ResTable_map::TYPE_DIMENSION; - else if (piece == u"fraction") return android::ResTable_map::TYPE_FRACTION; - else if (piece == u"enum") return android::ResTable_map::TYPE_ENUM; - else if (piece == u"flags") return android::ResTable_map::TYPE_FLAGS; - return 0; -} +bool ResourceParser::parsePublicGroup(xml::XmlPullParser* parser, ParsedResource* outResource) { + Maybe<StringPiece16> maybeType = xml::findNonEmptyAttribute(parser, u"type"); + if (!maybeType) { + mDiag->error(DiagMessage(outResource->source) + << "<public-group> must have a 'type' attribute"); + return false; + } -static uint32_t parseFormatAttribute(const StringPiece16& str) { - uint32_t mask = 0; - for (StringPiece16 part : util::tokenize(str, u'|')) { - StringPiece16 trimmedPart = util::trimWhitespace(part); - uint32_t type = parseFormatType(trimmedPart); - if (type == 0) { - return 0; + const ResourceType* parsedType = parseResourceType(maybeType.value()); + if (!parsedType) { + mDiag->error(DiagMessage(outResource->source) + << "invalid resource type '" << maybeType.value() << "' in <public-group>"); + return false; + } + + Maybe<StringPiece16> maybeId = xml::findNonEmptyAttribute(parser, u"first-id"); + if (!maybeId) { + mDiag->error(DiagMessage(outResource->source) + << "<public-group> must have a 'first-id' attribute"); + return false; + } + + android::Res_value val; + bool result = android::ResTable::stringToInt(maybeId.value().data(), + maybeId.value().size(), &val); + ResourceId nextId(val.data); + if (!result || !nextId.isValid()) { + mDiag->error(DiagMessage(outResource->source) + << "invalid resource ID '" << maybeId.value() << "' in <public-group>"); + return false; + } + + std::u16string comment; + bool error = false; + const size_t depth = parser->getDepth(); + while (xml::XmlPullParser::nextChildNode(parser, depth)) { + if (parser->getEvent() == xml::XmlPullParser::Event::kComment) { + comment = util::trimWhitespace(parser->getComment()).toString(); + continue; + } else if (parser->getEvent() != xml::XmlPullParser::Event::kStartElement) { + // Skip text. + continue; + } + + const Source itemSource = mSource.withLine(parser->getLineNumber()); + const std::u16string& elementNamespace = parser->getElementNamespace(); + const std::u16string& elementName = parser->getElementName(); + if (elementNamespace.empty() && elementName == u"public") { + Maybe<StringPiece16> maybeName = xml::findNonEmptyAttribute(parser, u"name"); + if (!maybeName) { + mDiag->error(DiagMessage(itemSource) << "<public> must have a 'name' attribute"); + error = true; + continue; + } + + if (xml::findNonEmptyAttribute(parser, u"id")) { + mDiag->error(DiagMessage(itemSource) << "'id' is ignored within <public-group>"); + error = true; + continue; + } + + if (xml::findNonEmptyAttribute(parser, u"type")) { + mDiag->error(DiagMessage(itemSource) << "'type' is ignored within <public-group>"); + error = true; + continue; + } + + ParsedResource childResource; + childResource.name.type = *parsedType; + childResource.name.entry = maybeName.value().toString(); + childResource.id = nextId; + childResource.comment = std::move(comment); + childResource.source = itemSource; + childResource.symbolState = SymbolState::kPublic; + outResource->childResources.push_back(std::move(childResource)); + + nextId.id += 1; + + } else if (!shouldIgnoreElement(elementNamespace, elementName)) { + mDiag->error(DiagMessage(itemSource) << ":" << elementName << ">"); + error = true; } - mask |= type; } - return mask; + return !error; } -bool ResourceParser::parseAttr(XmlPullParser* parser, const ResourceNameRef& resourceName) { - const SourceLine source = mSource.line(parser->getLineNumber()); - ResourceName actualName = resourceName.toResourceName(); - std::unique_ptr<Attribute> attr = parseAttrImpl(parser, &actualName, false); - if (!attr) { +bool ResourceParser::parseSymbolImpl(xml::XmlPullParser* parser, ParsedResource* outResource) { + Maybe<StringPiece16> maybeType = xml::findNonEmptyAttribute(parser, u"type"); + if (!maybeType) { + mDiag->error(DiagMessage(outResource->source) + << "<" << parser->getElementName() << "> must have a 'type' attribute"); + return false; + } + + const ResourceType* parsedType = parseResourceType(maybeType.value()); + if (!parsedType) { + mDiag->error(DiagMessage(outResource->source) + << "invalid resource type '" << maybeType.value() + << "' in <" << parser->getElementName() << ">"); return false; } - return mTable->addResource(actualName, mConfig, source, std::move(attr)); + + outResource->name.type = *parsedType; + return true; +} + +bool ResourceParser::parseSymbol(xml::XmlPullParser* parser, ParsedResource* outResource) { + if (parseSymbolImpl(parser, outResource)) { + outResource->symbolState = SymbolState::kPrivate; + return true; + } + return false; +} + +bool ResourceParser::parseAddResource(xml::XmlPullParser* parser, ParsedResource* outResource) { + if (parseSymbolImpl(parser, outResource)) { + outResource->symbolState = SymbolState::kUndefined; + return true; + } + return false; +} + + +bool ResourceParser::parseAttr(xml::XmlPullParser* parser, ParsedResource* outResource) { + return parseAttrImpl(parser, outResource, false); } -std::unique_ptr<Attribute> ResourceParser::parseAttrImpl(XmlPullParser* parser, - ResourceName* resourceName, - bool weak) { +bool ResourceParser::parseAttrImpl(xml::XmlPullParser* parser, ParsedResource* outResource, + bool weak) { + outResource->name.type = ResourceType::kAttr; + + // Attributes only end up in default configuration. + if (outResource->config != ConfigDescription::defaultConfig()) { + mDiag->warn(DiagMessage(outResource->source) << "ignoring configuration '" + << outResource->config << "' for attribute " << outResource->name); + outResource->config = ConfigDescription::defaultConfig(); + } + uint32_t typeMask = 0; - const auto endAttrIter = parser->endAttributes(); - const auto formatAttrIter = parser->findAttribute(u"", u"format"); - if (formatAttrIter != endAttrIter) { - typeMask = parseFormatAttribute(formatAttrIter->value); + Maybe<StringPiece16> maybeFormat = xml::findAttribute(parser, u"format"); + if (maybeFormat) { + typeMask = parseFormatAttribute(maybeFormat.value()); if (typeMask == 0) { - mLogger.error(parser->getLineNumber()) - << "invalid attribute format '" - << formatAttrIter->value - << "'." - << std::endl; - return {}; + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "invalid attribute format '" << maybeFormat.value() << "'"); + return false; } } - // If this is a declaration, the package name may be in the name. Separate these out. - // Eg. <attr name="android:text" /> - // No format attribute is allowed. - if (weak && formatAttrIter == endAttrIter) { - StringPiece16 package, type, name; - extractResourceName(resourceName->entry, &package, &type, &name); - if (type.empty() && !package.empty()) { - resourceName->package = package.toString(); - resourceName->entry = name.toString(); + Maybe<int32_t> maybeMin, maybeMax; + + if (Maybe<StringPiece16> maybeMinStr = xml::findAttribute(parser, u"min")) { + StringPiece16 minStr = util::trimWhitespace(maybeMinStr.value()); + if (!minStr.empty()) { + android::Res_value value; + if (android::ResTable::stringToInt(minStr.data(), minStr.size(), &value)) { + maybeMin = static_cast<int32_t>(value.data); + } + } + + if (!maybeMin) { + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "invalid 'min' value '" << minStr << "'"); + return false; } } - std::vector<Attribute::Symbol> items; + if (Maybe<StringPiece16> maybeMaxStr = xml::findAttribute(parser, u"max")) { + StringPiece16 maxStr = util::trimWhitespace(maybeMaxStr.value()); + if (!maxStr.empty()) { + android::Res_value value; + if (android::ResTable::stringToInt(maxStr.data(), maxStr.size(), &value)) { + maybeMax = static_cast<int32_t>(value.data); + } + } - bool error = false; - while (XmlPullParser::isGoodEvent(parser->next())) { - if (parser->getEvent() != XmlPullParser::Event::kStartElement) { - continue; + if (!maybeMax) { + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "invalid 'max' value '" << maxStr << "'"); + return false; } + } + + if ((maybeMin || maybeMax) && (typeMask & android::ResTable_map::TYPE_INTEGER) == 0) { + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "'min' and 'max' can only be used when format='integer'"); + return false; + } - ScopedXmlPullParser childParser(parser); + struct SymbolComparator { + bool operator()(const Attribute::Symbol& a, const Attribute::Symbol& b) { + return a.symbol.name.value() < b.symbol.name.value(); + } + }; - const std::u16string& name = childParser.getElementName(); - if (!childParser.getElementNamespace().empty() - || (name != u"flag" && name != u"enum")) { - mLogger.error(childParser.getLineNumber()) - << "unexpected tag <" - << name - << "> in <attr>." - << std::endl; - error = true; + std::set<Attribute::Symbol, SymbolComparator> items; + + std::u16string comment; + bool error = false; + const size_t depth = parser->getDepth(); + while (xml::XmlPullParser::nextChildNode(parser, depth)) { + if (parser->getEvent() == xml::XmlPullParser::Event::kComment) { + comment = util::trimWhitespace(parser->getComment()).toString(); + continue; + } else if (parser->getEvent() != xml::XmlPullParser::Event::kStartElement) { + // Skip text. continue; } - if (name == u"enum") { - if (typeMask & android::ResTable_map::TYPE_FLAGS) { - mLogger.error(childParser.getLineNumber()) - << "can not define an <enum>; already defined a <flag>." - << std::endl; - error = true; - continue; + const Source itemSource = mSource.withLine(parser->getLineNumber()); + const std::u16string& elementNamespace = parser->getElementNamespace(); + const std::u16string& elementName = parser->getElementName(); + if (elementNamespace.empty() && (elementName == u"flag" || elementName == u"enum")) { + if (elementName == u"enum") { + if (typeMask & android::ResTable_map::TYPE_FLAGS) { + mDiag->error(DiagMessage(itemSource) + << "can not define an <enum>; already defined a <flag>"); + error = true; + continue; + } + typeMask |= android::ResTable_map::TYPE_ENUM; + + } else if (elementName == u"flag") { + if (typeMask & android::ResTable_map::TYPE_ENUM) { + mDiag->error(DiagMessage(itemSource) + << "can not define a <flag>; already defined an <enum>"); + error = true; + continue; + } + typeMask |= android::ResTable_map::TYPE_FLAGS; } - typeMask |= android::ResTable_map::TYPE_ENUM; - } else if (name == u"flag") { - if (typeMask & android::ResTable_map::TYPE_ENUM) { - mLogger.error(childParser.getLineNumber()) - << "can not define a <flag>; already defined an <enum>." - << std::endl; - error = true; - continue; - } - typeMask |= android::ResTable_map::TYPE_FLAGS; - } - Attribute::Symbol item; - if (parseEnumOrFlagItem(&childParser, name, &item)) { - if (!mTable->addResource(item.symbol.name, mConfig, - mSource.line(childParser.getLineNumber()), - util::make_unique<Id>())) { - error = true; + if (Maybe<Attribute::Symbol> s = parseEnumOrFlagItem(parser, elementName)) { + Attribute::Symbol& symbol = s.value(); + ParsedResource childResource; + childResource.name = symbol.symbol.name.value(); + childResource.source = itemSource; + childResource.value = util::make_unique<Id>(); + outResource->childResources.push_back(std::move(childResource)); + + symbol.symbol.setComment(std::move(comment)); + symbol.symbol.setSource(itemSource); + + auto insertResult = items.insert(std::move(symbol)); + if (!insertResult.second) { + const Attribute::Symbol& existingSymbol = *insertResult.first; + mDiag->error(DiagMessage(itemSource) + << "duplicate symbol '" << existingSymbol.symbol.name.value().entry + << "'"); + + mDiag->note(DiagMessage(existingSymbol.symbol.getSource()) + << "first defined here"); + error = true; + } } else { - items.push_back(std::move(item)); + error = true; } - } else { + } else if (!shouldIgnoreElement(elementNamespace, elementName)) { + mDiag->error(DiagMessage(itemSource) << ":" << elementName << ">"); error = true; } + + comment = {}; } if (error) { - return {}; + return false; } std::unique_ptr<Attribute> attr = util::make_unique<Attribute>(weak); - attr->symbols.swap(items); + attr->symbols = std::vector<Attribute::Symbol>(items.begin(), items.end()); attr->typeMask = typeMask ? typeMask : uint32_t(android::ResTable_map::TYPE_ANY); - return attr; + if (maybeMin) { + attr->minInt = maybeMin.value(); + } + + if (maybeMax) { + attr->maxInt = maybeMax.value(); + } + outResource->value = std::move(attr); + return true; } -bool ResourceParser::parseEnumOrFlagItem(XmlPullParser* parser, const StringPiece16& tag, - Attribute::Symbol* outSymbol) { - const auto attrIterEnd = parser->endAttributes(); - const auto nameAttrIter = parser->findAttribute(u"", u"name"); - if (nameAttrIter == attrIterEnd || nameAttrIter->value.empty()) { - mLogger.error(parser->getLineNumber()) - << "no attribute 'name' found for tag <" << tag << ">." - << std::endl; - return false; +Maybe<Attribute::Symbol> ResourceParser::parseEnumOrFlagItem(xml::XmlPullParser* parser, + const StringPiece16& tag) { + const Source source = mSource.withLine(parser->getLineNumber()); + + Maybe<StringPiece16> maybeName = xml::findNonEmptyAttribute(parser, u"name"); + if (!maybeName) { + mDiag->error(DiagMessage(source) << "no attribute 'name' found for tag <" << tag << ">"); + return {}; } - const auto valueAttrIter = parser->findAttribute(u"", u"value"); - if (valueAttrIter == attrIterEnd || valueAttrIter->value.empty()) { - mLogger.error(parser->getLineNumber()) - << "no attribute 'value' found for tag <" << tag << ">." - << std::endl; - return false; + Maybe<StringPiece16> maybeValue = xml::findNonEmptyAttribute(parser, u"value"); + if (!maybeValue) { + mDiag->error(DiagMessage(source) << "no attribute 'value' found for tag <" << tag << ">"); + return {}; } android::Res_value val; - if (!android::ResTable::stringToInt(valueAttrIter->value.data(), - valueAttrIter->value.size(), &val)) { - mLogger.error(parser->getLineNumber()) - << "invalid value '" - << valueAttrIter->value - << "' for <" << tag << ">; must be an integer." - << std::endl; - return false; + if (!android::ResTable::stringToInt(maybeValue.value().data(), + maybeValue.value().size(), &val)) { + mDiag->error(DiagMessage(source) << "invalid value '" << maybeValue.value() + << "' for <" << tag << ">; must be an integer"); + return {}; } - outSymbol->symbol.name = ResourceName { - mTable->getPackage(), ResourceType::kId, nameAttrIter->value }; - outSymbol->value = val.data; - return true; + return Attribute::Symbol{ + Reference(ResourceNameRef({}, ResourceType::kId, maybeName.value())), val.data }; } -static bool parseXmlAttributeName(StringPiece16 str, ResourceName* outName) { +static Maybe<Reference> parseXmlAttributeName(StringPiece16 str) { str = util::trimWhitespace(str); - const char16_t* const start = str.data(); + const char16_t* start = str.data(); const char16_t* const end = start + str.size(); const char16_t* p = start; + Reference ref; + if (p != end && *p == u'*') { + ref.privateReference = true; + start++; + p++; + } + StringPiece16 package; StringPiece16 name; while (p != end) { @@ -1113,289 +975,314 @@ static bool parseXmlAttributeName(StringPiece16 str, ResourceName* outName) { p++; } - outName->package = package.toString(); - outName->type = ResourceType::kAttr; - if (name.size() == 0) { - outName->entry = str.toString(); - } else { - outName->entry = name.toString(); - } - return true; + ref.name = ResourceName(package.toString(), ResourceType::kAttr, + name.empty() ? str.toString() : name.toString()); + return Maybe<Reference>(std::move(ref)); } -bool ResourceParser::parseUntypedItem(XmlPullParser* parser, Style& style) { - const auto endAttrIter = parser->endAttributes(); - const auto nameAttrIter = parser->findAttribute(u"", u"name"); - if (nameAttrIter == endAttrIter || nameAttrIter->value.empty()) { - mLogger.error(parser->getLineNumber()) - << "<item> must have a 'name' attribute." - << std::endl; +bool ResourceParser::parseStyleItem(xml::XmlPullParser* parser, Style* style) { + const Source source = mSource.withLine(parser->getLineNumber()); + + Maybe<StringPiece16> maybeName = xml::findNonEmptyAttribute(parser, u"name"); + if (!maybeName) { + mDiag->error(DiagMessage(source) << "<item> must have a 'name' attribute"); return false; } - ResourceName key; - if (!parseXmlAttributeName(nameAttrIter->value, &key)) { - mLogger.error(parser->getLineNumber()) - << "invalid attribute name '" - << nameAttrIter->value - << "'." - << std::endl; + Maybe<Reference> maybeKey = parseXmlAttributeName(maybeName.value()); + if (!maybeKey) { + mDiag->error(DiagMessage(source) << "invalid attribute name '" << maybeName.value() << "'"); return false; } - if (!key.package.empty()) { - // We have a package name set, so lookup its alias. - parser->applyPackageAlias(&key.package, mTable->getPackage()); - } else { - // The package name was omitted, so use the default package name with - // no alias lookup. - key.package = mTable->getPackage(); - } + transformReferenceFromNamespace(parser, u"", &maybeKey.value()); + maybeKey.value().setSource(source); std::unique_ptr<Item> value = parseXml(parser, 0, kAllowRawString); if (!value) { + mDiag->error(DiagMessage(source) << "could not parse style item"); return false; } - style.entries.push_back(Style::Entry{ Reference(key), std::move(value) }); + style->entries.push_back(Style::Entry{ std::move(maybeKey.value()), std::move(value) }); return true; } -bool ResourceParser::parseStyle(XmlPullParser* parser, const ResourceNameRef& resourceName) { - const SourceLine source = mSource.line(parser->getLineNumber()); +bool ResourceParser::parseStyle(xml::XmlPullParser* parser, ParsedResource* outResource) { + outResource->name.type = ResourceType::kStyle; + std::unique_ptr<Style> style = util::make_unique<Style>(); - const auto endAttrIter = parser->endAttributes(); - const auto parentAttrIter = parser->findAttribute(u"", u"parent"); - if (parentAttrIter != endAttrIter) { - std::string errStr; - if (!parseStyleParentReference(parentAttrIter->value, &style->parent, &errStr)) { - mLogger.error(source.line) << errStr << "." << std::endl; - return false; - } + Maybe<StringPiece16> maybeParent = xml::findAttribute(parser, u"parent"); + if (maybeParent) { + // If the parent is empty, we don't have a parent, but we also don't infer either. + if (!maybeParent.value().empty()) { + std::string errStr; + style->parent = ResourceUtils::parseStyleParentReference(maybeParent.value(), &errStr); + if (!style->parent) { + mDiag->error(DiagMessage(outResource->source) << errStr); + return false; + } - if (!style->parent.name.package.empty()) { - // Try to interpret the package name as an alias. These take precedence. - parser->applyPackageAlias(&style->parent.name.package, mTable->getPackage()); - } else { - // If no package is specified, this can not be an alias and is the local package. - style->parent.name.package = mTable->getPackage(); + // Transform the namespace prefix to the actual package name, and mark the reference as + // private if appropriate. + transformReferenceFromNamespace(parser, u"", &style->parent.value()); } + } else { // No parent was specified, so try inferring it from the style name. - std::u16string styleName = resourceName.entry.toString(); + std::u16string styleName = outResource->name.entry; size_t pos = styleName.find_last_of(u'.'); if (pos != std::string::npos) { style->parentInferred = true; - style->parent.name.package = mTable->getPackage(); - style->parent.name.type = ResourceType::kStyle; - style->parent.name.entry = styleName.substr(0, pos); + style->parent = Reference(ResourceName({}, ResourceType::kStyle, + styleName.substr(0, pos))); } } - bool success = true; - while (XmlPullParser::isGoodEvent(parser->next())) { - if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + bool error = false; + const size_t depth = parser->getDepth(); + while (xml::XmlPullParser::nextChildNode(parser, depth)) { + if (parser->getEvent() != xml::XmlPullParser::Event::kStartElement) { + // Skip text and comments. continue; } - ScopedXmlPullParser childParser(parser); - const std::u16string& name = childParser.getElementName(); - if (name == u"item") { - success &= parseUntypedItem(&childParser, *style); - } else { - mLogger.error(childParser.getLineNumber()) - << "unexpected tag <" - << name - << "> in <style> resource." - << std::endl; - success = false; + const std::u16string& elementNamespace = parser->getElementNamespace(); + const std::u16string& elementName = parser->getElementName(); + if (elementNamespace == u"" && elementName == u"item") { + error |= !parseStyleItem(parser, style.get()); + + } else if (!shouldIgnoreElement(elementNamespace, elementName)) { + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << ":" << elementName << ">"); + error = true; } } - if (!success) { + if (error) { return false; } - return mTable->addResource(resourceName, mConfig, source, std::move(style)); + outResource->value = std::move(style); + return true; +} + +bool ResourceParser::parseArray(xml::XmlPullParser* parser, ParsedResource* outResource) { + return parseArrayImpl(parser, outResource, android::ResTable_map::TYPE_ANY); } -bool ResourceParser::parseArray(XmlPullParser* parser, const ResourceNameRef& resourceName, - uint32_t typeMask) { - const SourceLine source = mSource.line(parser->getLineNumber()); +bool ResourceParser::parseIntegerArray(xml::XmlPullParser* parser, ParsedResource* outResource) { + return parseArrayImpl(parser, outResource, android::ResTable_map::TYPE_INTEGER); +} + +bool ResourceParser::parseStringArray(xml::XmlPullParser* parser, ParsedResource* outResource) { + return parseArrayImpl(parser, outResource, android::ResTable_map::TYPE_STRING); +} + +bool ResourceParser::parseArrayImpl(xml::XmlPullParser* parser, ParsedResource* outResource, + const uint32_t typeMask) { + outResource->name.type = ResourceType::kArray; + std::unique_ptr<Array> array = util::make_unique<Array>(); bool error = false; - while (XmlPullParser::isGoodEvent(parser->next())) { - if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + const size_t depth = parser->getDepth(); + while (xml::XmlPullParser::nextChildNode(parser, depth)) { + if (parser->getEvent() != xml::XmlPullParser::Event::kStartElement) { + // Skip text and comments. continue; } - ScopedXmlPullParser childParser(parser); - - if (childParser.getElementName() != u"item") { - mLogger.error(childParser.getLineNumber()) - << "unexpected tag <" - << childParser.getElementName() - << "> in <array> resource." - << std::endl; - error = true; - continue; - } + const Source itemSource = mSource.withLine(parser->getLineNumber()); + const std::u16string& elementNamespace = parser->getElementNamespace(); + const std::u16string& elementName = parser->getElementName(); + if (elementNamespace.empty() && elementName == u"item") { + std::unique_ptr<Item> item = parseXml(parser, typeMask, kNoRawString); + if (!item) { + mDiag->error(DiagMessage(itemSource) << "could not parse array item"); + error = true; + continue; + } + item->setSource(itemSource); + array->items.emplace_back(std::move(item)); - std::unique_ptr<Item> item = parseXml(&childParser, typeMask, kNoRawString); - if (!item) { + } else if (!shouldIgnoreElement(elementNamespace, elementName)) { + mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber())) + << "unknown tag <" << elementNamespace << ":" << elementName << ">"); error = true; - continue; } - array->items.emplace_back(std::move(item)); } if (error) { return false; } - return mTable->addResource(resourceName, mConfig, source, std::move(array)); + outResource->value = std::move(array); + return true; } -bool ResourceParser::parsePlural(XmlPullParser* parser, const ResourceNameRef& resourceName) { - const SourceLine source = mSource.line(parser->getLineNumber()); +bool ResourceParser::parsePlural(xml::XmlPullParser* parser, ParsedResource* outResource) { + outResource->name.type = ResourceType::kPlurals; + std::unique_ptr<Plural> plural = util::make_unique<Plural>(); - bool success = true; - while (XmlPullParser::isGoodEvent(parser->next())) { - if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + bool error = false; + const size_t depth = parser->getDepth(); + while (xml::XmlPullParser::nextChildNode(parser, depth)) { + if (parser->getEvent() != xml::XmlPullParser::Event::kStartElement) { + // Skip text and comments. continue; } - ScopedXmlPullParser childParser(parser); - - if (!childParser.getElementNamespace().empty() || - childParser.getElementName() != u"item") { - success = false; - continue; - } + const Source itemSource = mSource.withLine(parser->getLineNumber()); + const std::u16string& elementNamespace = parser->getElementNamespace(); + const std::u16string& elementName = parser->getElementName(); + if (elementNamespace.empty() && elementName == u"item") { + Maybe<StringPiece16> maybeQuantity = xml::findNonEmptyAttribute(parser, u"quantity"); + if (!maybeQuantity) { + mDiag->error(DiagMessage(itemSource) << "<item> in <plurals> requires attribute " + << "'quantity'"); + error = true; + continue; + } - const auto endAttrIter = childParser.endAttributes(); - auto attrIter = childParser.findAttribute(u"", u"quantity"); - if (attrIter == endAttrIter || attrIter->value.empty()) { - mLogger.error(childParser.getLineNumber()) - << "<item> in <plurals> requires attribute 'quantity'." - << std::endl; - success = false; - continue; - } + StringPiece16 trimmedQuantity = util::trimWhitespace(maybeQuantity.value()); + size_t index = 0; + if (trimmedQuantity == u"zero") { + index = Plural::Zero; + } else if (trimmedQuantity == u"one") { + index = Plural::One; + } else if (trimmedQuantity == u"two") { + index = Plural::Two; + } else if (trimmedQuantity == u"few") { + index = Plural::Few; + } else if (trimmedQuantity == u"many") { + index = Plural::Many; + } else if (trimmedQuantity == u"other") { + index = Plural::Other; + } else { + mDiag->error(DiagMessage(itemSource) + << "<item> in <plural> has invalid value '" << trimmedQuantity + << "' for attribute 'quantity'"); + error = true; + continue; + } - StringPiece16 trimmedQuantity = util::trimWhitespace(attrIter->value); - size_t index = 0; - if (trimmedQuantity == u"zero") { - index = Plural::Zero; - } else if (trimmedQuantity == u"one") { - index = Plural::One; - } else if (trimmedQuantity == u"two") { - index = Plural::Two; - } else if (trimmedQuantity == u"few") { - index = Plural::Few; - } else if (trimmedQuantity == u"many") { - index = Plural::Many; - } else if (trimmedQuantity == u"other") { - index = Plural::Other; - } else { - mLogger.error(childParser.getLineNumber()) - << "<item> in <plural> has invalid value '" - << trimmedQuantity - << "' for attribute 'quantity'." - << std::endl; - success = false; - continue; - } + if (plural->values[index]) { + mDiag->error(DiagMessage(itemSource) + << "duplicate quantity '" << trimmedQuantity << "'"); + error = true; + continue; + } - if (plural->values[index]) { - mLogger.error(childParser.getLineNumber()) - << "duplicate quantity '" - << trimmedQuantity - << "'." - << std::endl; - success = false; - continue; - } + if (!(plural->values[index] = parseXml(parser, android::ResTable_map::TYPE_STRING, + kNoRawString))) { + error = true; + } + plural->values[index]->setSource(itemSource); - if (!(plural->values[index] = parseXml(&childParser, android::ResTable_map::TYPE_STRING, - kNoRawString))) { - success = false; + } else if (!shouldIgnoreElement(elementNamespace, elementName)) { + mDiag->error(DiagMessage(itemSource) << "unknown tag <" << elementNamespace << ":" + << elementName << ">"); + error = true; } } - if (!success) { + if (error) { return false; } - return mTable->addResource(resourceName, mConfig, source, std::move(plural)); + outResource->value = std::move(plural); + return true; } -bool ResourceParser::parseDeclareStyleable(XmlPullParser* parser, - const ResourceNameRef& resourceName) { - const SourceLine source = mSource.line(parser->getLineNumber()); +bool ResourceParser::parseDeclareStyleable(xml::XmlPullParser* parser, + ParsedResource* outResource) { + outResource->name.type = ResourceType::kStyleable; + + // Declare-styleable is kPrivate by default, because it technically only exists in R.java. + outResource->symbolState = SymbolState::kPublic; + + // Declare-styleable only ends up in default config; + if (outResource->config != ConfigDescription::defaultConfig()) { + mDiag->warn(DiagMessage(outResource->source) << "ignoring configuration '" + << outResource->config << "' for styleable " + << outResource->name.entry); + outResource->config = ConfigDescription::defaultConfig(); + } + std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); - bool success = true; - while (XmlPullParser::isGoodEvent(parser->next())) { - if (parser->getEvent() != XmlPullParser::Event::kStartElement) { + std::u16string comment; + bool error = false; + const size_t depth = parser->getDepth(); + while (xml::XmlPullParser::nextChildNode(parser, depth)) { + if (parser->getEvent() == xml::XmlPullParser::Event::kComment) { + comment = util::trimWhitespace(parser->getComment()).toString(); + continue; + } else if (parser->getEvent() != xml::XmlPullParser::Event::kStartElement) { + // Ignore text. continue; } - ScopedXmlPullParser childParser(parser); - - const std::u16string& elementName = childParser.getElementName(); - if (elementName == u"attr") { - const auto endAttrIter = childParser.endAttributes(); - auto attrIter = childParser.findAttribute(u"", u"name"); - if (attrIter == endAttrIter || attrIter->value.empty()) { - mLogger.error(childParser.getLineNumber()) - << "<attr> tag must have a 'name' attribute." - << std::endl; - success = false; + const Source itemSource = mSource.withLine(parser->getLineNumber()); + const std::u16string& elementNamespace = parser->getElementNamespace(); + const std::u16string& elementName = parser->getElementName(); + if (elementNamespace.empty() && elementName == u"attr") { + Maybe<StringPiece16> maybeName = xml::findNonEmptyAttribute(parser, u"name"); + if (!maybeName) { + mDiag->error(DiagMessage(itemSource) << "<attr> tag must have a 'name' attribute"); + error = true; continue; } - // Copy because our iterator will be invalidated. - ResourceName attrResourceName = { - mTable->getPackage(), - ResourceType::kAttr, - attrIter->value - }; - - std::unique_ptr<Attribute> attr = parseAttrImpl(&childParser, &attrResourceName, true); - if (!attr) { - success = false; + // If this is a declaration, the package name may be in the name. Separate these out. + // Eg. <attr name="android:text" /> + Maybe<Reference> maybeRef = parseXmlAttributeName(maybeName.value()); + if (!maybeRef) { + mDiag->error(DiagMessage(itemSource) << "<attr> tag has invalid name '" + << maybeName.value() << "'"); + error = true; continue; } - styleable->entries.emplace_back(attrResourceName); + Reference& childRef = maybeRef.value(); + xml::transformReferenceFromNamespace(parser, u"", &childRef); - // The package may have been corrected to another package. If that is so, - // we don't add the declaration. - if (attrResourceName.package == mTable->getPackage()) { - success &= mTable->addResource(attrResourceName, mConfig, - mSource.line(childParser.getLineNumber()), - std::move(attr)); + // Create the ParsedResource that will add the attribute to the table. + ParsedResource childResource; + childResource.name = childRef.name.value(); + childResource.source = itemSource; + childResource.comment = std::move(comment); + + if (!parseAttrImpl(parser, &childResource, true)) { + error = true; + continue; } - } else if (elementName != u"eat-comment" && elementName != u"skip") { - mLogger.error(childParser.getLineNumber()) - << "<" - << elementName - << "> is not allowed inside <declare-styleable>." - << std::endl; - success = false; + // Create the reference to this attribute. + childRef.setComment(childResource.comment); + childRef.setSource(itemSource); + styleable->entries.push_back(std::move(childRef)); + + outResource->childResources.push_back(std::move(childResource)); + + } else if (!shouldIgnoreElement(elementNamespace, elementName)) { + mDiag->error(DiagMessage(itemSource) << "unknown tag <" << elementNamespace << ":" + << elementName << ">"); + error = true; } + + comment = {}; } - if (!success) { + if (error) { return false; } - return mTable->addResource(resourceName, mConfig, source, std::move(styleable)); + outResource->value = std::move(styleable); + return true; } } // namespace aapt diff --git a/tools/aapt2/ResourceParser.h b/tools/aapt2/ResourceParser.h index 7618999f0023..51cbbe19384e 100644 --- a/tools/aapt2/ResourceParser.h +++ b/tools/aapt2/ResourceParser.h @@ -18,135 +18,50 @@ #define AAPT_RESOURCE_PARSER_H #include "ConfigDescription.h" -#include "Logger.h" +#include "Diagnostics.h" #include "ResourceTable.h" #include "ResourceValues.h" -#include "StringPiece.h" #include "StringPool.h" -#include "XmlPullParser.h" +#include "util/Maybe.h" +#include "util/StringPiece.h" +#include "xml/XmlPullParser.h" -#include <istream> #include <memory> namespace aapt { -/* - * Parses an XML file for resources and adds them to a ResourceTable. - */ -class ResourceParser { -public: - /* - * Extracts the package, type, and name from a string of the format: - * - * [package:]type/name - * - * where the package can be empty. Validation must be performed on each - * individual extracted piece to verify that the pieces are valid. - */ - static void extractResourceName(const StringPiece16& str, StringPiece16* outPackage, - StringPiece16* outType, StringPiece16* outEntry); - - /* - * Returns true if the string was parsed as a reference (@[+][package:]type/name), with - * `outReference` set to the parsed reference. - * - * If '+' was present in the reference, `outCreate` is set to true. - * If '*' was present in the reference, `outPrivate` is set to true. - */ - static bool tryParseReference(const StringPiece16& str, ResourceNameRef* outReference, - bool* outCreate, bool* outPrivate); - - /* - * Returns true if the string was parsed as an attribute reference (?[package:]type/name), - * with `outReference` set to the parsed reference. - */ - static bool tryParseAttributeReference(const StringPiece16& str, - ResourceNameRef* outReference); - - /* - * Returns true if the string `str` was parsed as a valid reference to a style. - * The format for a style parent is slightly more flexible than a normal reference: - * - * @[package:]style/<entry> or - * ?[package:]style/<entry> or - * <package>:[style/]<entry> - */ - static bool parseStyleParentReference(const StringPiece16& str, Reference* outReference, - std::string* outError); - - /* - * Returns a Reference object if the string was parsed as a resource or attribute reference, - * ( @[+][package:]type/name | ?[package:]type/name ) setting outCreate to true if - * the '+' was present in the string. - */ - static std::unique_ptr<Reference> tryParseReference(const StringPiece16& str, - bool* outCreate); - - /* - * Returns a BinaryPrimitve object representing @null or @empty if the string was parsed - * as one. - */ - static std::unique_ptr<BinaryPrimitive> tryParseNullOrEmpty(const StringPiece16& str); +struct ParsedResource; - /* - * Returns a BinaryPrimitve object representing a color if the string was parsed - * as one. - */ - static std::unique_ptr<BinaryPrimitive> tryParseColor(const StringPiece16& str); - - /* - * Returns a BinaryPrimitve object representing a boolean if the string was parsed - * as one. +struct ResourceParserOptions { + /** + * Optional product names by which to filter resources. + * This is like a preprocessor definition in that we strip out resources + * that don't match before we compile them. */ - static std::unique_ptr<BinaryPrimitive> tryParseBool(const StringPiece16& str); + std::vector<std::u16string> products; - /* - * Returns a BinaryPrimitve object representing an integer if the string was parsed - * as one. - */ - static std::unique_ptr<BinaryPrimitive> tryParseInt(const StringPiece16& str); - - /* - * Returns a BinaryPrimitve object representing a floating point number - * (float, dimension, etc) if the string was parsed as one. - */ - static std::unique_ptr<BinaryPrimitive> tryParseFloat(const StringPiece16& str); - - /* - * Returns a BinaryPrimitve object representing an enum symbol if the string was parsed - * as one. + /** + * Whether the default setting for this parser is to allow translation. */ - static std::unique_ptr<BinaryPrimitive> tryParseEnumSymbol(const Attribute& enumAttr, - const StringPiece16& str); + bool translatable = true; - /* - * Returns a BinaryPrimitve object representing a flag symbol if the string was parsed - * as one. - */ - static std::unique_ptr<BinaryPrimitive> tryParseFlagSymbol(const Attribute& enumAttr, - const StringPiece16& str); - /* - * Try to convert a string to an Item for the given attribute. The attribute will - * restrict what values the string can be converted to. - * The callback function onCreateReference is called when the parsed item is a - * reference to an ID that must be created (@+id/foo). + /** + * Whether positional arguments in formatted strings are treated as errors or warnings. */ - static std::unique_ptr<Item> parseItemForAttribute( - const StringPiece16& value, const Attribute& attr, - std::function<void(const ResourceName&)> onCreateReference = {}); - - static std::unique_ptr<Item> parseItemForAttribute( - const StringPiece16& value, uint32_t typeMask, - std::function<void(const ResourceName&)> onCreateReference = {}); - - static uint32_t androidTypeToAttributeTypeMask(uint16_t type); + bool errorOnPositionalArguments = true; +}; - ResourceParser(const std::shared_ptr<ResourceTable>& table, const Source& source, - const ConfigDescription& config, const std::shared_ptr<XmlPullParser>& parser); +/* + * Parses an XML file for resources and adds them to a ResourceTable. + */ +class ResourceParser { +public: + ResourceParser(IDiagnostics* diag, ResourceTable* table, const Source& source, + const ConfigDescription& config, const ResourceParserOptions& options = {}); ResourceParser(const ResourceParser&) = delete; // No copy. - bool parse(); + bool parse(xml::XmlPullParser* parser); private: /* @@ -155,39 +70,50 @@ private: * contains the escaped and whitespace trimmed text, while `outRawString` * contains the unescaped text. Returns true on success. */ - bool flattenXmlSubtree(XmlPullParser* parser, std::u16string* outRawString,\ + bool flattenXmlSubtree(xml::XmlPullParser* parser, std::u16string* outRawString, StyleString* outStyleString); /* - * Parses the XML subtree and converts it to an Item. The type of Item that can be - * parsed is denoted by the `typeMask`. If `allowRawValue` is true and the subtree - * can not be parsed as a regular Item, then a RawString is returned. Otherwise - * this returns nullptr. + * Parses the XML subtree and returns an Item. + * The type of Item that can be parsed is denoted by the `typeMask`. + * If `allowRawValue` is true and the subtree can not be parsed as a regular Item, then a + * RawString is returned. Otherwise this returns false; */ - std::unique_ptr<Item> parseXml(XmlPullParser* parser, uint32_t typeMask, bool allowRawValue); - - bool parseResources(XmlPullParser* parser); - bool parseString(XmlPullParser* parser, const ResourceNameRef& resourceName); - bool parseColor(XmlPullParser* parser, const ResourceNameRef& resourceName); - bool parsePrimitive(XmlPullParser* parser, const ResourceNameRef& resourceName); - bool parsePublic(XmlPullParser* parser, const StringPiece16& name); - bool parseAttr(XmlPullParser* parser, const ResourceNameRef& resourceName); - std::unique_ptr<Attribute> parseAttrImpl(XmlPullParser* parser, - ResourceName* resourceName, - bool weak); - bool parseEnumOrFlagItem(XmlPullParser* parser, const StringPiece16& tag, - Attribute::Symbol* outSymbol); - bool parseStyle(XmlPullParser* parser, const ResourceNameRef& resourceName); - bool parseUntypedItem(XmlPullParser* parser, Style& style); - bool parseDeclareStyleable(XmlPullParser* parser, const ResourceNameRef& resourceName); - bool parseArray(XmlPullParser* parser, const ResourceNameRef& resourceName, uint32_t typeMask); - bool parsePlural(XmlPullParser* parser, const ResourceNameRef& resourceName); - - std::shared_ptr<ResourceTable> mTable; + std::unique_ptr<Item> parseXml(xml::XmlPullParser* parser, const uint32_t typeMask, + const bool allowRawValue); + + bool parseResources(xml::XmlPullParser* parser); + bool parseResource(xml::XmlPullParser* parser, ParsedResource* outResource); + + bool parseItem(xml::XmlPullParser* parser, ParsedResource* outResource, uint32_t format); + bool parseString(xml::XmlPullParser* parser, ParsedResource* outResource); + + bool parsePublic(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parsePublicGroup(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parseSymbolImpl(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parseSymbol(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parseAddResource(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parseAttr(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parseAttrImpl(xml::XmlPullParser* parser, ParsedResource* outResource, bool weak); + Maybe<Attribute::Symbol> parseEnumOrFlagItem(xml::XmlPullParser* parser, + const StringPiece16& tag); + bool parseStyle(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parseStyleItem(xml::XmlPullParser* parser, Style* style); + bool parseDeclareStyleable(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parseArray(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parseIntegerArray(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parseStringArray(xml::XmlPullParser* parser, ParsedResource* outResource); + bool parseArrayImpl(xml::XmlPullParser* parser, ParsedResource* outResource, uint32_t typeMask); + bool parsePlural(xml::XmlPullParser* parser, ParsedResource* outResource); + + bool shouldStripResource(const ResourceNameRef& name, + const Maybe<std::u16string>& product) const; + + IDiagnostics* mDiag; + ResourceTable* mTable; Source mSource; ConfigDescription mConfig; - SourceLogger mLogger; - std::shared_ptr<XmlPullParser> mParser; + ResourceParserOptions mOptions; }; } // namespace aapt diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp index a93d0ff7a835..cf0fcd11b903 100644 --- a/tools/aapt2/ResourceParser_test.cpp +++ b/tools/aapt2/ResourceParser_test.cpp @@ -16,8 +16,10 @@ #include "ResourceParser.h" #include "ResourceTable.h" +#include "ResourceUtils.h" #include "ResourceValues.h" -#include "SourceXmlPullParser.h" +#include "test/Context.h" +#include "xml/XmlPullParser.h" #include <gtest/gtest.h> #include <sstream> @@ -27,156 +29,58 @@ namespace aapt { constexpr const char* kXmlPreamble = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; -TEST(ResourceParserReferenceTest, ParseReferenceWithNoPackage) { - ResourceNameRef expected = { {}, ResourceType::kColor, u"foo" }; - ResourceNameRef actual; - bool create = false; - bool privateRef = false; - EXPECT_TRUE(ResourceParser::tryParseReference(u"@color/foo", &actual, &create, &privateRef)); - EXPECT_EQ(expected, actual); - EXPECT_FALSE(create); - EXPECT_FALSE(privateRef); -} - -TEST(ResourceParserReferenceTest, ParseReferenceWithPackage) { - ResourceNameRef expected = { u"android", ResourceType::kColor, u"foo" }; - ResourceNameRef actual; - bool create = false; - bool privateRef = false; - EXPECT_TRUE(ResourceParser::tryParseReference(u"@android:color/foo", &actual, &create, - &privateRef)); - EXPECT_EQ(expected, actual); - EXPECT_FALSE(create); - EXPECT_FALSE(privateRef); -} - -TEST(ResourceParserReferenceTest, ParseReferenceWithSurroundingWhitespace) { - ResourceNameRef expected = { u"android", ResourceType::kColor, u"foo" }; - ResourceNameRef actual; - bool create = false; - bool privateRef = false; - EXPECT_TRUE(ResourceParser::tryParseReference(u"\t @android:color/foo\n \n\t", &actual, - &create, &privateRef)); - EXPECT_EQ(expected, actual); - EXPECT_FALSE(create); - EXPECT_FALSE(privateRef); -} - -TEST(ResourceParserReferenceTest, ParseAutoCreateIdReference) { - ResourceNameRef expected = { u"android", ResourceType::kId, u"foo" }; - ResourceNameRef actual; - bool create = false; - bool privateRef = false; - EXPECT_TRUE(ResourceParser::tryParseReference(u"@+android:id/foo", &actual, &create, - &privateRef)); - EXPECT_EQ(expected, actual); - EXPECT_TRUE(create); - EXPECT_FALSE(privateRef); -} - -TEST(ResourceParserReferenceTest, ParsePrivateReference) { - ResourceNameRef expected = { u"android", ResourceType::kId, u"foo" }; - ResourceNameRef actual; - bool create = false; - bool privateRef = false; - EXPECT_TRUE(ResourceParser::tryParseReference(u"@*android:id/foo", &actual, &create, - &privateRef)); - EXPECT_EQ(expected, actual); - EXPECT_FALSE(create); - EXPECT_TRUE(privateRef); -} - -TEST(ResourceParserReferenceTest, FailToParseAutoCreateNonIdReference) { - bool create = false; - bool privateRef = false; - ResourceNameRef actual; - EXPECT_FALSE(ResourceParser::tryParseReference(u"@+android:color/foo", &actual, &create, - &privateRef)); -} - -TEST(ResourceParserReferenceTest, ParseStyleParentReference) { - Reference ref; - std::string errStr; - EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"@android:style/foo", &ref, &errStr)); - EXPECT_EQ(ref.name, (ResourceName{ u"android", ResourceType::kStyle, u"foo" })); - - EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"@style/foo", &ref, &errStr)); - EXPECT_EQ(ref.name, (ResourceName{ {}, ResourceType::kStyle, u"foo" })); - - EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"?android:style/foo", &ref, &errStr)); - EXPECT_EQ(ref.name, (ResourceName{ u"android", ResourceType::kStyle, u"foo" })); - - EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"?style/foo", &ref, &errStr)); - EXPECT_EQ(ref.name, (ResourceName{ {}, ResourceType::kStyle, u"foo" })); - - EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"android:style/foo", &ref, &errStr)); - EXPECT_EQ(ref.name, (ResourceName{ u"android", ResourceType::kStyle, u"foo" })); - - EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"android:foo", &ref, &errStr)); - EXPECT_EQ(ref.name, (ResourceName{ u"android", ResourceType::kStyle, u"foo" })); - - EXPECT_TRUE(ResourceParser::parseStyleParentReference(u"foo", &ref, &errStr)); - EXPECT_EQ(ref.name, (ResourceName{ {}, ResourceType::kStyle, u"foo" })); +TEST(ResourceParserSingleTest, FailToParseWithNoRootResourcesElement) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + std::stringstream input(kXmlPreamble); + input << "<attr name=\"foo\"/>" << std::endl; + ResourceTable table; + ResourceParser parser(context->getDiagnostics(), &table, Source{ "test" }, {}); + xml::XmlPullParser xmlParser(input); + ASSERT_FALSE(parser.parse(&xmlParser)); } struct ResourceParserTest : public ::testing::Test { - virtual void SetUp() override { - mTable = std::make_shared<ResourceTable>(); - mTable->setPackage(u"android"); + ResourceTable mTable; + std::unique_ptr<IAaptContext> mContext; + + void SetUp() override { + mContext = test::ContextBuilder().build(); } ::testing::AssertionResult testParse(const StringPiece& str) { - std::stringstream input(kXmlPreamble); - input << "<resources>\n" << str << "\n</resources>" << std::endl; - ResourceParser parser(mTable, Source{ "test" }, {}, - std::make_shared<SourceXmlPullParser>(input)); - if (parser.parse()) { - return ::testing::AssertionSuccess(); - } - return ::testing::AssertionFailure(); + return testParse(str, ConfigDescription{}, {}); } - template <typename T> - const T* findResource(const ResourceNameRef& name, const ConfigDescription& config) { - using std::begin; - using std::end; - - const ResourceTableType* type; - const ResourceEntry* entry; - std::tie(type, entry) = mTable->findResource(name); - if (!type || !entry) { - return nullptr; - } - - for (const auto& configValue : entry->values) { - if (configValue.config == config) { - return dynamic_cast<const T*>(configValue.value.get()); - } - } - return nullptr; + ::testing::AssertionResult testParse(const StringPiece& str, const ConfigDescription& config) { + return testParse(str, config, {}); } - template <typename T> - const T* findResource(const ResourceNameRef& name) { - return findResource<T>(name, {}); + ::testing::AssertionResult testParse(const StringPiece& str, + std::initializer_list<std::u16string> products) { + return testParse(str, {}, std::move(products)); } - std::shared_ptr<ResourceTable> mTable; + ::testing::AssertionResult testParse(const StringPiece& str, const ConfigDescription& config, + std::initializer_list<std::u16string> products) { + std::stringstream input(kXmlPreamble); + input << "<resources>\n" << str << "\n</resources>" << std::endl; + ResourceParserOptions parserOptions; + parserOptions.products = products; + ResourceParser parser(mContext->getDiagnostics(), &mTable, Source{ "test" }, config, + parserOptions); + xml::XmlPullParser xmlParser(input); + if (parser.parse(&xmlParser)) { + return ::testing::AssertionSuccess(); + } + return ::testing::AssertionFailure(); + } }; -TEST_F(ResourceParserTest, FailToParseWithNoRootResourcesElement) { - std::stringstream input(kXmlPreamble); - input << "<attr name=\"foo\"/>" << std::endl; - ResourceParser parser(mTable, {}, {}, std::make_shared<SourceXmlPullParser>(input)); - ASSERT_FALSE(parser.parse()); -} - TEST_F(ResourceParserTest, ParseQuotedString) { std::string input = "<string name=\"foo\"> \" hey there \" </string>"; ASSERT_TRUE(testParse(input)); - const String* str = findResource<String>(ResourceName{ - u"android", ResourceType::kString, u"foo"}); + String* str = test::getValue<String>(&mTable, u"@string/foo"); ASSERT_NE(nullptr, str); EXPECT_EQ(std::u16string(u" hey there "), *str->value); } @@ -185,12 +89,30 @@ TEST_F(ResourceParserTest, ParseEscapedString) { std::string input = "<string name=\"foo\">\\?123</string>"; ASSERT_TRUE(testParse(input)); - const String* str = findResource<String>(ResourceName{ - u"android", ResourceType::kString, u"foo" }); + String* str = test::getValue<String>(&mTable, u"@string/foo"); ASSERT_NE(nullptr, str); EXPECT_EQ(std::u16string(u"?123"), *str->value); } +TEST_F(ResourceParserTest, ParseFormattedString) { + std::string input = "<string name=\"foo\">%d %s</string>"; + ASSERT_FALSE(testParse(input)); + + input = "<string name=\"foo\">%1$d %2$s</string>"; + ASSERT_TRUE(testParse(input)); +} + +TEST_F(ResourceParserTest, IgnoreXliffTags) { + std::string input = "<string name=\"foo\" \n" + " xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">\n" + " There are <xliff:g id=\"count\">%1$d</xliff:g> apples</string>"; + ASSERT_TRUE(testParse(input)); + + String* str = test::getValue<String>(&mTable, u"@string/foo"); + ASSERT_NE(nullptr, str); + EXPECT_EQ(StringPiece16(u"There are %1$d apples"), StringPiece16(*str->value)); +} + TEST_F(ResourceParserTest, ParseNull) { std::string input = "<integer name=\"foo\">@null</integer>"; ASSERT_TRUE(testParse(input)); @@ -199,8 +121,7 @@ TEST_F(ResourceParserTest, ParseNull) { // a non-existing value, and this causes problems in styles when trying to resolve // an attribute. Null values must be encoded as android::Res_value::TYPE_REFERENCE // with a data value of 0. - const BinaryPrimitive* integer = findResource<BinaryPrimitive>(ResourceName{ - u"android", ResourceType::kInteger, u"foo" }); + BinaryPrimitive* integer = test::getValue<BinaryPrimitive>(&mTable, u"@integer/foo"); ASSERT_NE(nullptr, integer); EXPECT_EQ(uint16_t(android::Res_value::TYPE_REFERENCE), integer->value.dataType); EXPECT_EQ(0u, integer->value.data); @@ -210,8 +131,7 @@ TEST_F(ResourceParserTest, ParseEmpty) { std::string input = "<integer name=\"foo\">@empty</integer>"; ASSERT_TRUE(testParse(input)); - const BinaryPrimitive* integer = findResource<BinaryPrimitive>(ResourceName{ - u"android", ResourceType::kInteger, u"foo" }); + BinaryPrimitive* integer = test::getValue<BinaryPrimitive>(&mTable, u"@integer/foo"); ASSERT_NE(nullptr, integer); EXPECT_EQ(uint16_t(android::Res_value::TYPE_NULL), integer->value.dataType); EXPECT_EQ(uint32_t(android::Res_value::DATA_NULL_EMPTY), integer->value.data); @@ -222,17 +142,51 @@ TEST_F(ResourceParserTest, ParseAttr) { "<attr name=\"bar\"/>"; ASSERT_TRUE(testParse(input)); - const Attribute* attr = findResource<Attribute>(ResourceName{ - u"android", ResourceType::kAttr, u"foo"}); - EXPECT_NE(nullptr, attr); + Attribute* attr = test::getValue<Attribute>(&mTable, u"@attr/foo"); + ASSERT_NE(nullptr, attr); EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_STRING), attr->typeMask); - attr = findResource<Attribute>(ResourceName{ - u"android", ResourceType::kAttr, u"bar"}); - EXPECT_NE(nullptr, attr); + attr = test::getValue<Attribute>(&mTable, u"@attr/bar"); + ASSERT_NE(nullptr, attr); EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_ANY), attr->typeMask); } +// Old AAPT allowed attributes to be defined under different configurations, but ultimately +// stored them with the default configuration. Check that we have the same behavior. +TEST_F(ResourceParserTest, ParseAttrAndDeclareStyleableUnderConfigButRecordAsNoConfig) { + const ConfigDescription watchConfig = test::parseConfigOrDie("watch"); + std::string input = R"EOF( + <attr name="foo" /> + <declare-styleable name="bar"> + <attr name="baz" /> + </declare-styleable>)EOF"; + ASSERT_TRUE(testParse(input, watchConfig)); + + EXPECT_EQ(nullptr, test::getValueForConfig<Attribute>(&mTable, u"@attr/foo", watchConfig)); + EXPECT_EQ(nullptr, test::getValueForConfig<Attribute>(&mTable, u"@attr/baz", watchConfig)); + EXPECT_EQ(nullptr, test::getValueForConfig<Styleable>(&mTable, u"@styleable/bar", watchConfig)); + + EXPECT_NE(nullptr, test::getValue<Attribute>(&mTable, u"@attr/foo")); + EXPECT_NE(nullptr, test::getValue<Attribute>(&mTable, u"@attr/baz")); + EXPECT_NE(nullptr, test::getValue<Styleable>(&mTable, u"@styleable/bar")); +} + +TEST_F(ResourceParserTest, ParseAttrWithMinMax) { + std::string input = "<attr name=\"foo\" min=\"10\" max=\"23\" format=\"integer\"/>"; + ASSERT_TRUE(testParse(input)); + + Attribute* attr = test::getValue<Attribute>(&mTable, u"@attr/foo"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_INTEGER), attr->typeMask); + EXPECT_EQ(10, attr->minInt); + EXPECT_EQ(23, attr->maxInt); +} + +TEST_F(ResourceParserTest, FailParseAttrWithMinMaxButNotInteger) { + std::string input = "<attr name=\"foo\" min=\"10\" max=\"23\" format=\"string\"/>"; + ASSERT_FALSE(testParse(input)); +} + TEST_F(ResourceParserTest, ParseUseAndDeclOfAttr) { std::string input = "<declare-styleable name=\"Styleable\">\n" " <attr name=\"foo\" />\n" @@ -240,8 +194,7 @@ TEST_F(ResourceParserTest, ParseUseAndDeclOfAttr) { "<attr name=\"foo\" format=\"string\"/>"; ASSERT_TRUE(testParse(input)); - const Attribute* attr = findResource<Attribute>(ResourceName{ - u"android", ResourceType::kAttr, u"foo"}); + Attribute* attr = test::getValue<Attribute>(&mTable, u"@attr/foo"); ASSERT_NE(nullptr, attr); EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_STRING), attr->typeMask); } @@ -255,8 +208,7 @@ TEST_F(ResourceParserTest, ParseDoubleUseOfAttr) { "</declare-styleable>"; ASSERT_TRUE(testParse(input)); - const Attribute* attr = findResource<Attribute>(ResourceName{ - u"android", ResourceType::kAttr, u"foo"}); + Attribute* attr = test::getValue<Attribute>(&mTable, u"@attr/foo"); ASSERT_NE(nullptr, attr); EXPECT_EQ(uint32_t(android::ResTable_map::TYPE_BOOLEAN), attr->typeMask); } @@ -269,19 +221,21 @@ TEST_F(ResourceParserTest, ParseEnumAttr) { "</attr>"; ASSERT_TRUE(testParse(input)); - const Attribute* enumAttr = findResource<Attribute>(ResourceName{ - u"android", ResourceType::kAttr, u"foo"}); + Attribute* enumAttr = test::getValue<Attribute>(&mTable, u"@attr/foo"); ASSERT_NE(enumAttr, nullptr); EXPECT_EQ(enumAttr->typeMask, android::ResTable_map::TYPE_ENUM); ASSERT_EQ(enumAttr->symbols.size(), 3u); - EXPECT_EQ(enumAttr->symbols[0].symbol.name.entry, u"bar"); + AAPT_ASSERT_TRUE(enumAttr->symbols[0].symbol.name); + EXPECT_EQ(enumAttr->symbols[0].symbol.name.value().entry, u"bar"); EXPECT_EQ(enumAttr->symbols[0].value, 0u); - EXPECT_EQ(enumAttr->symbols[1].symbol.name.entry, u"bat"); + AAPT_ASSERT_TRUE(enumAttr->symbols[1].symbol.name); + EXPECT_EQ(enumAttr->symbols[1].symbol.name.value().entry, u"bat"); EXPECT_EQ(enumAttr->symbols[1].value, 1u); - EXPECT_EQ(enumAttr->symbols[2].symbol.name.entry, u"baz"); + AAPT_ASSERT_TRUE(enumAttr->symbols[2].symbol.name); + EXPECT_EQ(enumAttr->symbols[2].symbol.name.value().entry, u"baz"); EXPECT_EQ(enumAttr->symbols[2].value, 2u); } @@ -293,24 +247,26 @@ TEST_F(ResourceParserTest, ParseFlagAttr) { "</attr>"; ASSERT_TRUE(testParse(input)); - const Attribute* flagAttr = findResource<Attribute>(ResourceName{ - u"android", ResourceType::kAttr, u"foo"}); - ASSERT_NE(flagAttr, nullptr); + Attribute* flagAttr = test::getValue<Attribute>(&mTable, u"@attr/foo"); + ASSERT_NE(nullptr, flagAttr); EXPECT_EQ(flagAttr->typeMask, android::ResTable_map::TYPE_FLAGS); ASSERT_EQ(flagAttr->symbols.size(), 3u); - EXPECT_EQ(flagAttr->symbols[0].symbol.name.entry, u"bar"); + AAPT_ASSERT_TRUE(flagAttr->symbols[0].symbol.name); + EXPECT_EQ(flagAttr->symbols[0].symbol.name.value().entry, u"bar"); EXPECT_EQ(flagAttr->symbols[0].value, 0u); - EXPECT_EQ(flagAttr->symbols[1].symbol.name.entry, u"bat"); + AAPT_ASSERT_TRUE(flagAttr->symbols[1].symbol.name); + EXPECT_EQ(flagAttr->symbols[1].symbol.name.value().entry, u"bat"); EXPECT_EQ(flagAttr->symbols[1].value, 1u); - EXPECT_EQ(flagAttr->symbols[2].symbol.name.entry, u"baz"); + AAPT_ASSERT_TRUE(flagAttr->symbols[2].symbol.name); + EXPECT_EQ(flagAttr->symbols[2].symbol.name.value().entry, u"baz"); EXPECT_EQ(flagAttr->symbols[2].value, 2u); - std::unique_ptr<BinaryPrimitive> flagValue = - ResourceParser::tryParseFlagSymbol(*flagAttr, u"baz|bat"); - ASSERT_NE(flagValue, nullptr); + std::unique_ptr<BinaryPrimitive> flagValue = ResourceUtils::tryParseFlagSymbol(flagAttr, + u"baz|bat"); + ASSERT_NE(nullptr, flagValue); EXPECT_EQ(flagValue->value.data, 1u | 2u); } @@ -331,28 +287,32 @@ TEST_F(ResourceParserTest, ParseStyle) { "</style>"; ASSERT_TRUE(testParse(input)); - const Style* style = findResource<Style>(ResourceName{ - u"android", ResourceType::kStyle, u"foo"}); - ASSERT_NE(style, nullptr); - EXPECT_EQ(ResourceNameRef(u"android", ResourceType::kStyle, u"fu"), style->parent.name); - ASSERT_EQ(style->entries.size(), 3u); + Style* style = test::getValue<Style>(&mTable, u"@style/foo"); + ASSERT_NE(nullptr, style); + AAPT_ASSERT_TRUE(style->parent); + AAPT_ASSERT_TRUE(style->parent.value().name); + EXPECT_EQ(test::parseNameOrDie(u"@style/fu"), style->parent.value().name.value()); + ASSERT_EQ(3u, style->entries.size()); + + AAPT_ASSERT_TRUE(style->entries[0].key.name); + EXPECT_EQ(test::parseNameOrDie(u"@attr/bar"), style->entries[0].key.name.value()); - EXPECT_EQ(style->entries[0].key.name, - (ResourceName{ u"android", ResourceType::kAttr, u"bar" })); - EXPECT_EQ(style->entries[1].key.name, - (ResourceName{ u"android", ResourceType::kAttr, u"bat" })); - EXPECT_EQ(style->entries[2].key.name, - (ResourceName{ u"android", ResourceType::kAttr, u"baz" })); + AAPT_ASSERT_TRUE(style->entries[1].key.name); + EXPECT_EQ(test::parseNameOrDie(u"@attr/bat"), style->entries[1].key.name.value()); + + AAPT_ASSERT_TRUE(style->entries[2].key.name); + EXPECT_EQ(test::parseNameOrDie(u"@attr/baz"), style->entries[2].key.name.value()); } TEST_F(ResourceParserTest, ParseStyleWithShorthandParent) { std::string input = "<style name=\"foo\" parent=\"com.app:Theme\"/>"; ASSERT_TRUE(testParse(input)); - const Style* style = findResource<Style>( - ResourceName{ u"android", ResourceType::kStyle, u"foo" }); - ASSERT_NE(style, nullptr); - EXPECT_EQ(ResourceNameRef(u"com.app", ResourceType::kStyle, u"Theme"), style->parent.name); + Style* style = test::getValue<Style>(&mTable, u"@style/foo"); + ASSERT_NE(nullptr, style); + AAPT_ASSERT_TRUE(style->parent); + AAPT_ASSERT_TRUE(style->parent.value().name); + EXPECT_EQ(test::parseNameOrDie(u"@com.app:style/Theme"), style->parent.value().name.value()); } TEST_F(ResourceParserTest, ParseStyleWithPackageAliasedParent) { @@ -360,10 +320,11 @@ TEST_F(ResourceParserTest, ParseStyleWithPackageAliasedParent) { " name=\"foo\" parent=\"app:Theme\"/>"; ASSERT_TRUE(testParse(input)); - const Style* style = findResource<Style>(ResourceName{ - u"android", ResourceType::kStyle, u"foo" }); - ASSERT_NE(style, nullptr); - EXPECT_EQ(ResourceNameRef(u"android", ResourceType::kStyle, u"Theme"), style->parent.name); + Style* style = test::getValue<Style>(&mTable, u"@style/foo"); + ASSERT_NE(nullptr, style); + AAPT_ASSERT_TRUE(style->parent); + AAPT_ASSERT_TRUE(style->parent.value().name); + EXPECT_EQ(test::parseNameOrDie(u"@android:style/Theme"), style->parent.value().name.value()); } TEST_F(ResourceParserTest, ParseStyleWithPackageAliasedItems) { @@ -373,22 +334,21 @@ TEST_F(ResourceParserTest, ParseStyleWithPackageAliasedItems) { "</style>"; ASSERT_TRUE(testParse(input)); - const Style* style = findResource<Style>(ResourceName{ - u"android", ResourceType::kStyle, u"foo" }); - ASSERT_NE(style, nullptr); + Style* style = test::getValue<Style>(&mTable, u"@style/foo"); + ASSERT_NE(nullptr, style); ASSERT_EQ(1u, style->entries.size()); - EXPECT_EQ(ResourceNameRef(u"android", ResourceType::kAttr, u"bar"), - style->entries[0].key.name); + EXPECT_EQ(test::parseNameOrDie(u"@android:attr/bar"), style->entries[0].key.name.value()); } TEST_F(ResourceParserTest, ParseStyleWithInferredParent) { std::string input = "<style name=\"foo.bar\"/>"; ASSERT_TRUE(testParse(input)); - const Style* style = findResource<Style>(ResourceName{ - u"android", ResourceType::kStyle, u"foo.bar" }); - ASSERT_NE(style, nullptr); - EXPECT_EQ(style->parent.name, (ResourceName{ u"android", ResourceType::kStyle, u"foo" })); + Style* style = test::getValue<Style>(&mTable, u"@style/foo.bar"); + ASSERT_NE(nullptr, style); + AAPT_ASSERT_TRUE(style->parent); + AAPT_ASSERT_TRUE(style->parent.value().name); + EXPECT_EQ(style->parent.value().name.value(), test::parseNameOrDie(u"@style/foo")); EXPECT_TRUE(style->parentInferred); } @@ -396,18 +356,27 @@ TEST_F(ResourceParserTest, ParseStyleWithInferredParentOverridenByEmptyParentAtt std::string input = "<style name=\"foo.bar\" parent=\"\"/>"; ASSERT_TRUE(testParse(input)); - const Style* style = findResource<Style>(ResourceName{ - u"android", ResourceType::kStyle, u"foo.bar" }); - ASSERT_NE(style, nullptr); - EXPECT_FALSE(style->parent.name.isValid()); + Style* style = test::getValue<Style>(&mTable, u"@style/foo.bar"); + ASSERT_NE(nullptr, style); + AAPT_EXPECT_FALSE(style->parent); EXPECT_FALSE(style->parentInferred); } +TEST_F(ResourceParserTest, ParseStyleWithPrivateParentReference) { + std::string input = R"EOF(<style name="foo" parent="*android:style/bar" />)EOF"; + ASSERT_TRUE(testParse(input)); + + Style* style = test::getValue<Style>(&mTable, u"@style/foo"); + ASSERT_NE(nullptr, style); + AAPT_ASSERT_TRUE(style->parent); + EXPECT_TRUE(style->parent.value().privateReference); +} + TEST_F(ResourceParserTest, ParseAutoGeneratedIdReference) { std::string input = "<string name=\"foo\">@+id/bar</string>"; ASSERT_TRUE(testParse(input)); - const Id* id = findResource<Id>(ResourceName{ u"android", ResourceType::kId, u"bar"}); + Id* id = test::getValue<Id>(&mTable, u"@id/bar"); ASSERT_NE(id, nullptr); } @@ -415,25 +384,57 @@ TEST_F(ResourceParserTest, ParseAttributesDeclareStyleable) { std::string input = "<declare-styleable name=\"foo\">\n" " <attr name=\"bar\" />\n" " <attr name=\"bat\" format=\"string|reference\"/>\n" + " <attr name=\"baz\">\n" + " <enum name=\"foo\" value=\"1\"/>\n" + " </attr>\n" "</declare-styleable>"; ASSERT_TRUE(testParse(input)); - const Attribute* attr = findResource<Attribute>(ResourceName{ - u"android", ResourceType::kAttr, u"bar"}); + Maybe<ResourceTable::SearchResult> result = + mTable.findResource(test::parseNameOrDie(u"@styleable/foo")); + AAPT_ASSERT_TRUE(result); + EXPECT_EQ(SymbolState::kPublic, result.value().entry->symbolStatus.state); + + Attribute* attr = test::getValue<Attribute>(&mTable, u"@attr/bar"); + ASSERT_NE(attr, nullptr); + EXPECT_TRUE(attr->isWeak()); + + attr = test::getValue<Attribute>(&mTable, u"@attr/bat"); ASSERT_NE(attr, nullptr); EXPECT_TRUE(attr->isWeak()); - attr = findResource<Attribute>(ResourceName{ u"android", ResourceType::kAttr, u"bat"}); + attr = test::getValue<Attribute>(&mTable, u"@attr/baz"); ASSERT_NE(attr, nullptr); EXPECT_TRUE(attr->isWeak()); + EXPECT_EQ(1u, attr->symbols.size()); - const Styleable* styleable = findResource<Styleable>(ResourceName{ - u"android", ResourceType::kStyleable, u"foo" }); + EXPECT_NE(nullptr, test::getValue<Id>(&mTable, u"@id/foo")); + + Styleable* styleable = test::getValue<Styleable>(&mTable, u"@styleable/foo"); ASSERT_NE(styleable, nullptr); + ASSERT_EQ(3u, styleable->entries.size()); + + EXPECT_EQ(test::parseNameOrDie(u"@attr/bar"), styleable->entries[0].name.value()); + EXPECT_EQ(test::parseNameOrDie(u"@attr/bat"), styleable->entries[1].name.value()); +} + +TEST_F(ResourceParserTest, ParsePrivateAttributesDeclareStyleable) { + std::string input = "<declare-styleable name=\"foo\" xmlns:privAndroid=\"http://schemas.android.com/apk/prv/res/android\">\n" + " <attr name=\"*android:bar\" />\n" + " <attr name=\"privAndroid:bat\" />\n" + "</declare-styleable>"; + ASSERT_TRUE(testParse(input)); + Styleable* styleable = test::getValue<Styleable>(&mTable, u"@styleable/foo"); + ASSERT_NE(nullptr, styleable); ASSERT_EQ(2u, styleable->entries.size()); - EXPECT_EQ((ResourceName{u"android", ResourceType::kAttr, u"bar"}), styleable->entries[0].name); - EXPECT_EQ((ResourceName{u"android", ResourceType::kAttr, u"bat"}), styleable->entries[1].name); + EXPECT_TRUE(styleable->entries[0].privateReference); + AAPT_ASSERT_TRUE(styleable->entries[0].name); + EXPECT_EQ(std::u16string(u"android"), styleable->entries[0].name.value().package); + + EXPECT_TRUE(styleable->entries[1].privateReference); + AAPT_ASSERT_TRUE(styleable->entries[1].name); + EXPECT_EQ(std::u16string(u"android"), styleable->entries[1].name.value().package); } TEST_F(ResourceParserTest, ParseArray) { @@ -444,14 +445,21 @@ TEST_F(ResourceParserTest, ParseArray) { "</array>"; ASSERT_TRUE(testParse(input)); - const Array* array = findResource<Array>(ResourceName{ - u"android", ResourceType::kArray, u"foo" }); + Array* array = test::getValue<Array>(&mTable, u"@array/foo"); ASSERT_NE(array, nullptr); ASSERT_EQ(3u, array->items.size()); - EXPECT_NE(nullptr, dynamic_cast<const Reference*>(array->items[0].get())); - EXPECT_NE(nullptr, dynamic_cast<const String*>(array->items[1].get())); - EXPECT_NE(nullptr, dynamic_cast<const BinaryPrimitive*>(array->items[2].get())); + EXPECT_NE(nullptr, valueCast<Reference>(array->items[0].get())); + EXPECT_NE(nullptr, valueCast<String>(array->items[1].get())); + EXPECT_NE(nullptr, valueCast<BinaryPrimitive>(array->items[2].get())); +} + +TEST_F(ResourceParserTest, ParseStringArray) { + std::string input = "<string-array name=\"foo\">\n" + " <item>\"Werk\"</item>\n" + "</string-array>\n"; + ASSERT_TRUE(testParse(input)); + EXPECT_NE(nullptr, test::getValue<Array>(&mTable, u"@array/foo")); } TEST_F(ResourceParserTest, ParsePlural) { @@ -463,18 +471,67 @@ TEST_F(ResourceParserTest, ParsePlural) { } TEST_F(ResourceParserTest, ParseCommentsWithResource) { - std::string input = "<!-- This is a comment -->\n" + std::string input = "<!--This is a comment-->\n" "<string name=\"foo\">Hi</string>"; ASSERT_TRUE(testParse(input)); - const ResourceTableType* type; - const ResourceEntry* entry; - std::tie(type, entry) = mTable->findResource(ResourceName{ - u"android", ResourceType::kString, u"foo"}); - ASSERT_NE(type, nullptr); - ASSERT_NE(entry, nullptr); - ASSERT_FALSE(entry->values.empty()); - EXPECT_EQ(entry->values.front().comment, u"This is a comment"); + String* value = test::getValue<String>(&mTable, u"@string/foo"); + ASSERT_NE(nullptr, value); + EXPECT_EQ(value->getComment(), u"This is a comment"); +} + +TEST_F(ResourceParserTest, DoNotCombineMultipleComments) { + std::string input = "<!--One-->\n" + "<!--Two-->\n" + "<string name=\"foo\">Hi</string>"; + + ASSERT_TRUE(testParse(input)); + + String* value = test::getValue<String>(&mTable, u"@string/foo"); + ASSERT_NE(nullptr, value); + EXPECT_EQ(value->getComment(), u"Two"); +} + +TEST_F(ResourceParserTest, IgnoreCommentBeforeEndTag) { + std::string input = "<!--One-->\n" + "<string name=\"foo\">\n" + " Hi\n" + "<!--Two-->\n" + "</string>"; + + ASSERT_TRUE(testParse(input)); + + String* value = test::getValue<String>(&mTable, u"@string/foo"); + ASSERT_NE(nullptr, value); + EXPECT_EQ(value->getComment(), u"One"); +} + +TEST_F(ResourceParserTest, ParseNestedComments) { + // We only care about declare-styleable and enum/flag attributes because comments + // from those end up in R.java + std::string input = R"EOF( + <declare-styleable name="foo"> + <!-- The name of the bar --> + <attr name="barName" format="string|reference" /> + </declare-styleable> + + <attr name="foo"> + <!-- The very first --> + <enum name="one" value="1" /> + </attr>)EOF"; + ASSERT_TRUE(testParse(input)); + + Styleable* styleable = test::getValue<Styleable>(&mTable, u"@styleable/foo"); + ASSERT_NE(nullptr, styleable); + ASSERT_EQ(1u, styleable->entries.size()); + + EXPECT_EQ(StringPiece16(u"The name of the bar"), styleable->entries.front().getComment()); + + Attribute* attr = test::getValue<Attribute>(&mTable, u"@attr/foo"); + ASSERT_NE(nullptr, attr); + ASSERT_EQ(1u, attr->symbols.size()); + + EXPECT_EQ(StringPiece16(u"The very first"), attr->symbols.front().symbol.getComment()); } /* @@ -485,8 +542,108 @@ TEST_F(ResourceParserTest, ParsePublicIdAsDefinition) { std::string input = "<public type=\"id\" name=\"foo\"/>"; ASSERT_TRUE(testParse(input)); - const Id* id = findResource<Id>(ResourceName{ u"android", ResourceType::kId, u"foo" }); + Id* id = test::getValue<Id>(&mTable, u"@id/foo"); ASSERT_NE(nullptr, id); } +TEST_F(ResourceParserTest, FilterProductsThatDontMatch) { + std::string input = R"EOF( + <string name="foo" product="phone">hi</string> + <string name="foo" product="no-sdcard">ho</string> + <string name="bar" product="">wee</string> + <string name="baz">woo</string> + <string name="bit" product="phablet">hoot</string> + <string name="bot" product="default">yes</string> + )EOF"; + ASSERT_TRUE(testParse(input, { std::u16string(u"no-sdcard"), std::u16string(u"phablet") })); + + String* fooStr = test::getValue<String>(&mTable, u"@string/foo"); + ASSERT_NE(nullptr, fooStr); + EXPECT_EQ(StringPiece16(u"ho"), *fooStr->value); + + EXPECT_NE(nullptr, test::getValue<String>(&mTable, u"@string/bar")); + EXPECT_NE(nullptr, test::getValue<String>(&mTable, u"@string/baz")); + EXPECT_NE(nullptr, test::getValue<String>(&mTable, u"@string/bit")); + EXPECT_NE(nullptr, test::getValue<String>(&mTable, u"@string/bot")); +} + +TEST_F(ResourceParserTest, FilterProductsThatBothMatchInOrder) { + std::string input = R"EOF( + <string name="foo" product="phone">phone</string> + <string name="foo" product="default">default</string> + )EOF"; + ASSERT_TRUE(testParse(input, { std::u16string(u"phone") })); + + String* foo = test::getValue<String>(&mTable, u"@string/foo"); + ASSERT_NE(nullptr, foo); + EXPECT_EQ(std::u16string(u"phone"), *foo->value); +} + +TEST_F(ResourceParserTest, FailWhenProductFilterStripsOutAllVersionsOfResource) { + std::string input = "<string name=\"foo\" product=\"tablet\">hello</string>\n"; + ASSERT_FALSE(testParse(input, { std::u16string(u"phone") })); +} + +TEST_F(ResourceParserTest, AutoIncrementIdsInPublicGroup) { + std::string input = R"EOF( + <public-group type="attr" first-id="0x01010040"> + <public name="foo" /> + <public name="bar" /> + </public-group>)EOF"; + ASSERT_TRUE(testParse(input)); + + Maybe<ResourceTable::SearchResult> result = mTable.findResource( + test::parseNameOrDie(u"@attr/foo")); + AAPT_ASSERT_TRUE(result); + + AAPT_ASSERT_TRUE(result.value().package->id); + AAPT_ASSERT_TRUE(result.value().type->id); + AAPT_ASSERT_TRUE(result.value().entry->id); + ResourceId actualId(result.value().package->id.value(), + result.value().type->id.value(), + result.value().entry->id.value()); + EXPECT_EQ(ResourceId(0x01010040), actualId); + + result = mTable.findResource(test::parseNameOrDie(u"@attr/bar")); + AAPT_ASSERT_TRUE(result); + + AAPT_ASSERT_TRUE(result.value().package->id); + AAPT_ASSERT_TRUE(result.value().type->id); + AAPT_ASSERT_TRUE(result.value().entry->id); + actualId = ResourceId(result.value().package->id.value(), + result.value().type->id.value(), + result.value().entry->id.value()); + EXPECT_EQ(ResourceId(0x01010041), actualId); +} + +TEST_F(ResourceParserTest, ExternalTypesShouldOnlyBeReferences) { + std::string input = R"EOF(<item type="layout" name="foo">@layout/bar</item>)EOF"; + ASSERT_TRUE(testParse(input)); + + input = R"EOF(<item type="layout" name="bar">"this is a string"</item>)EOF"; + ASSERT_FALSE(testParse(input)); +} + +TEST_F(ResourceParserTest, AddResourcesElementShouldAddEntryWithUndefinedSymbol) { + std::string input = R"EOF(<add-resource name="bar" type="string" />)EOF"; + ASSERT_TRUE(testParse(input)); + + Maybe<ResourceTable::SearchResult> result = mTable.findResource( + test::parseNameOrDie(u"@string/bar")); + AAPT_ASSERT_TRUE(result); + const ResourceEntry* entry = result.value().entry; + ASSERT_NE(nullptr, entry); + EXPECT_EQ(SymbolState::kUndefined, entry->symbolStatus.state); +} + +TEST_F(ResourceParserTest, ParseItemElementWithFormat) { + std::string input = R"EOF(<item name="foo" type="integer" format="float">0.3</item>)EOF"; + ASSERT_TRUE(testParse(input)); + + BinaryPrimitive* val = test::getValue<BinaryPrimitive>(&mTable, u"@integer/foo"); + ASSERT_NE(nullptr, val); + + EXPECT_EQ(uint32_t(android::Res_value::TYPE_FLOAT), val->value.dataType); +} + } // namespace aapt diff --git a/tools/aapt2/ResourceTable.cpp b/tools/aapt2/ResourceTable.cpp index c93ecc768022..8a3d047f6e8d 100644 --- a/tools/aapt2/ResourceTable.cpp +++ b/tools/aapt2/ResourceTable.cpp @@ -15,11 +15,13 @@ */ #include "ConfigDescription.h" -#include "Logger.h" #include "NameMangler.h" #include "ResourceTable.h" #include "ResourceValues.h" -#include "Util.h" +#include "ValueVisitor.h" + +#include "util/Comparators.h" +#include "util/Util.h" #include <algorithm> #include <androidfw/ResourceTypes.h> @@ -29,73 +31,113 @@ namespace aapt { -static bool compareConfigs(const ResourceConfigValue& lhs, const ConfigDescription& rhs) { - return lhs.config < rhs; -} - static bool lessThanType(const std::unique_ptr<ResourceTableType>& lhs, ResourceType rhs) { return lhs->type < rhs; } -static bool lessThanEntry(const std::unique_ptr<ResourceEntry>& lhs, const StringPiece16& rhs) { +template <typename T> +static bool lessThanStructWithName(const std::unique_ptr<T>& lhs, + const StringPiece16& rhs) { return lhs->name.compare(0, lhs->name.size(), rhs.data(), rhs.size()) < 0; } -ResourceTable::ResourceTable() : mPackageId(kUnsetPackageId) { - // Make sure attrs always have type ID 1. - findOrCreateType(ResourceType::kAttr)->typeId = 1; +ResourceTablePackage* ResourceTable::findPackage(const StringPiece16& name) { + const auto last = packages.end(); + auto iter = std::lower_bound(packages.begin(), last, name, + lessThanStructWithName<ResourceTablePackage>); + if (iter != last && name == (*iter)->name) { + return iter->get(); + } + return nullptr; } -std::unique_ptr<ResourceTableType>& ResourceTable::findOrCreateType(ResourceType type) { - auto last = mTypes.end(); - auto iter = std::lower_bound(mTypes.begin(), last, type, lessThanType); - if (iter != last) { - if ((*iter)->type == type) { - return *iter; +ResourceTablePackage* ResourceTable::findPackageById(uint8_t id) { + for (auto& package : packages) { + if (package->id && package->id.value() == id) { + return package.get(); } } - return *mTypes.emplace(iter, new ResourceTableType{ type }); + return nullptr; } -std::unique_ptr<ResourceEntry>& ResourceTable::findOrCreateEntry( - std::unique_ptr<ResourceTableType>& type, const StringPiece16& name) { - auto last = type->entries.end(); - auto iter = std::lower_bound(type->entries.begin(), last, name, lessThanEntry); - if (iter != last) { - if (name == (*iter)->name) { - return *iter; - } +ResourceTablePackage* ResourceTable::createPackage(const StringPiece16& name, Maybe<uint8_t> id) { + ResourceTablePackage* package = findOrCreatePackage(name); + if (id && !package->id) { + package->id = id; + return package; + } + + if (id && package->id && package->id.value() != id.value()) { + return nullptr; + } + return package; +} + +ResourceTablePackage* ResourceTable::findOrCreatePackage(const StringPiece16& name) { + const auto last = packages.end(); + auto iter = std::lower_bound(packages.begin(), last, name, + lessThanStructWithName<ResourceTablePackage>); + if (iter != last && name == (*iter)->name) { + return iter->get(); + } + + std::unique_ptr<ResourceTablePackage> newPackage = util::make_unique<ResourceTablePackage>(); + newPackage->name = name.toString(); + return packages.emplace(iter, std::move(newPackage))->get(); +} + +ResourceTableType* ResourceTablePackage::findType(ResourceType type) { + const auto last = types.end(); + auto iter = std::lower_bound(types.begin(), last, type, lessThanType); + if (iter != last && (*iter)->type == type) { + return iter->get(); } - return *type->entries.emplace(iter, new ResourceEntry{ name }); + return nullptr; } -struct IsAttributeVisitor : ConstValueVisitor { - bool isAttribute = false; +ResourceTableType* ResourceTablePackage::findOrCreateType(ResourceType type) { + const auto last = types.end(); + auto iter = std::lower_bound(types.begin(), last, type, lessThanType); + if (iter != last && (*iter)->type == type) { + return iter->get(); + } + return types.emplace(iter, new ResourceTableType(type))->get(); +} - void visit(const Attribute&, ValueVisitorArgs&) override { - isAttribute = true; +ResourceEntry* ResourceTableType::findEntry(const StringPiece16& name) { + const auto last = entries.end(); + auto iter = std::lower_bound(entries.begin(), last, name, + lessThanStructWithName<ResourceEntry>); + if (iter != last && name == (*iter)->name) { + return iter->get(); } + return nullptr; +} - operator bool() { - return isAttribute; +ResourceEntry* ResourceTableType::findOrCreateEntry(const StringPiece16& name) { + auto last = entries.end(); + auto iter = std::lower_bound(entries.begin(), last, name, + lessThanStructWithName<ResourceEntry>); + if (iter != last && name == (*iter)->name) { + return iter->get(); } -}; + return entries.emplace(iter, new ResourceEntry(name))->get(); +} /** * The default handler for collisions. A return value of -1 means keep the * existing value, 0 means fail, and +1 means take the incoming value. */ -static int defaultCollisionHandler(const Value& existing, const Value& incoming) { - IsAttributeVisitor existingIsAttr, incomingIsAttr; - existing.accept(existingIsAttr, {}); - incoming.accept(incomingIsAttr, {}); +int ResourceTable::resolveValueCollision(Value* existing, Value* incoming) { + Attribute* existingAttr = valueCast<Attribute>(existing); + Attribute* incomingAttr = valueCast<Attribute>(incoming); - if (!incomingIsAttr) { - if (incoming.isWeak()) { + if (!incomingAttr) { + if (incoming->isWeak()) { // We're trying to add a weak resource but a resource // already exists. Keep the existing. return -1; - } else if (existing.isWeak()) { + } else if (existing->isWeak()) { // Override the weak resource with the new strong resource. return 1; } @@ -104,8 +146,8 @@ static int defaultCollisionHandler(const Value& existing, const Value& incoming) return 0; } - if (!existingIsAttr) { - if (existing.isWeak()) { + if (!existingAttr) { + if (existing->isWeak()) { // The existing value is not an attribute and it is weak, // so take the incoming attribute value. return 1; @@ -115,27 +157,27 @@ static int defaultCollisionHandler(const Value& existing, const Value& incoming) return 0; } + assert(incomingAttr && existingAttr); + // // Attribute specific handling. At this point we know both // values are attributes. Since we can declare and define // attributes all-over, we do special handling to see // which definition sticks. // - const Attribute& existingAttr = static_cast<const Attribute&>(existing); - const Attribute& incomingAttr = static_cast<const Attribute&>(incoming); - if (existingAttr.typeMask == incomingAttr.typeMask) { + if (existingAttr->typeMask == incomingAttr->typeMask) { // The two attributes are both DECLs, but they are plain attributes // with the same formats. // Keep the strongest one. - return existingAttr.isWeak() ? 1 : -1; + return existingAttr->isWeak() ? 1 : -1; } - if (existingAttr.isWeak() && existingAttr.typeMask == android::ResTable_map::TYPE_ANY) { + if (existingAttr->isWeak() && existingAttr->typeMask == android::ResTable_map::TYPE_ANY) { // Any incoming attribute is better than this. return 1; } - if (incomingAttr.isWeak() && incomingAttr.typeMask == android::ResTable_map::TYPE_ANY) { + if (incomingAttr->isWeak() && incomingAttr->typeMask == android::ResTable_map::TYPE_ANY) { // The incoming attribute may be a USE instead of a DECL. // Keep the existing attribute. return -1; @@ -147,284 +189,256 @@ static constexpr const char16_t* kValidNameChars = u"._-"; static constexpr const char16_t* kValidNameMangledChars = u"._-$"; bool ResourceTable::addResource(const ResourceNameRef& name, const ConfigDescription& config, - const SourceLine& source, std::unique_ptr<Value> value) { - return addResourceImpl(name, ResourceId{}, config, source, std::move(value), kValidNameChars); + std::unique_ptr<Value> value, IDiagnostics* diag) { + return addResourceImpl(name, {}, config, std::move(value), kValidNameChars, + resolveValueCollision, diag); } bool ResourceTable::addResource(const ResourceNameRef& name, const ResourceId resId, - const ConfigDescription& config, const SourceLine& source, - std::unique_ptr<Value> value) { - return addResourceImpl(name, resId, config, source, std::move(value), kValidNameChars); + const ConfigDescription& config, std::unique_ptr<Value> value, + IDiagnostics* diag) { + return addResourceImpl(name, resId, config, std::move(value), kValidNameChars, + resolveValueCollision, diag); +} + +bool ResourceTable::addFileReference(const ResourceNameRef& name, const ConfigDescription& config, + const Source& source, const StringPiece16& path, + IDiagnostics* diag) { + return addFileReference(name, config, source, path, resolveValueCollision, diag); +} + +bool ResourceTable::addFileReference(const ResourceNameRef& name, const ConfigDescription& config, + const Source& source, const StringPiece16& path, + std::function<int(Value*,Value*)> conflictResolver, + IDiagnostics* diag) { + std::unique_ptr<FileReference> fileRef = util::make_unique<FileReference>( + stringPool.makeRef(path)); + fileRef->setSource(source); + return addResourceImpl(name, ResourceId{}, config, std::move(fileRef), kValidNameChars, + conflictResolver, diag); } bool ResourceTable::addResourceAllowMangled(const ResourceNameRef& name, const ConfigDescription& config, - const SourceLine& source, - std::unique_ptr<Value> value) { - return addResourceImpl(name, ResourceId{}, config, source, std::move(value), - kValidNameMangledChars); + std::unique_ptr<Value> value, + IDiagnostics* diag) { + return addResourceImpl(name, ResourceId{}, config, std::move(value), kValidNameMangledChars, + resolveValueCollision, diag); } -bool ResourceTable::addResourceImpl(const ResourceNameRef& name, const ResourceId resId, - const ConfigDescription& config, const SourceLine& source, - std::unique_ptr<Value> value, const char16_t* validChars) { - if (!name.package.empty() && name.package != mPackage) { - Logger::error(source) - << "resource '" - << name - << "' has incompatible package. Must be '" - << mPackage - << "'." - << std::endl; - return false; - } +bool ResourceTable::addResourceAllowMangled(const ResourceNameRef& name, + const ResourceId id, + const ConfigDescription& config, + std::unique_ptr<Value> value, + IDiagnostics* diag) { + return addResourceImpl(name, id, config, std::move(value), kValidNameMangledChars, + resolveValueCollision, diag); +} + +bool ResourceTable::addResourceImpl(const ResourceNameRef& name, + const ResourceId resId, + const ConfigDescription& config, + std::unique_ptr<Value> value, + const char16_t* validChars, + std::function<int(Value*,Value*)> conflictResolver, + IDiagnostics* diag) { + assert(value && "value can't be nullptr"); + assert(diag && "diagnostics can't be nullptr"); auto badCharIter = util::findNonAlphaNumericAndNotInSet(name.entry, validChars); if (badCharIter != name.entry.end()) { - Logger::error(source) - << "resource '" - << name - << "' has invalid entry name '" - << name.entry - << "'. Invalid character '" - << StringPiece16(badCharIter, 1) - << "'." - << std::endl; + diag->error(DiagMessage(value->getSource()) + << "resource '" + << name + << "' has invalid entry name '" + << name.entry + << "'. Invalid character '" + << StringPiece16(badCharIter, 1) + << "'"); return false; } - std::unique_ptr<ResourceTableType>& type = findOrCreateType(name.type); - if (resId.isValid() && type->typeId != ResourceTableType::kUnsetTypeId && - type->typeId != resId.typeId()) { - Logger::error(source) - << "trying to add resource '" - << name - << "' with ID " - << resId - << " but type '" - << type->type - << "' already has ID " - << std::hex << type->typeId << std::dec - << "." - << std::endl; + ResourceTablePackage* package = findOrCreatePackage(name.package); + if (resId.isValid() && package->id && package->id.value() != resId.packageId()) { + diag->error(DiagMessage(value->getSource()) + << "trying to add resource '" + << name + << "' with ID " + << resId + << " but package '" + << package->name + << "' already has ID " + << std::hex << (int) package->id.value() << std::dec); return false; } - std::unique_ptr<ResourceEntry>& entry = findOrCreateEntry(type, name.entry); - if (resId.isValid() && entry->entryId != ResourceEntry::kUnsetEntryId && - entry->entryId != resId.entryId()) { - Logger::error(source) - << "trying to add resource '" - << name - << "' with ID " - << resId - << " but resource already has ID " - << ResourceId(mPackageId, type->typeId, entry->entryId) - << "." - << std::endl; + ResourceTableType* type = package->findOrCreateType(name.type); + if (resId.isValid() && type->id && type->id.value() != resId.typeId()) { + diag->error(DiagMessage(value->getSource()) + << "trying to add resource '" + << name + << "' with ID " + << resId + << " but type '" + << type->type + << "' already has ID " + << std::hex << (int) type->id.value() << std::dec); return false; } - const auto endIter = std::end(entry->values); - auto iter = std::lower_bound(std::begin(entry->values), endIter, config, compareConfigs); + ResourceEntry* entry = type->findOrCreateEntry(name.entry); + if (resId.isValid() && entry->id && entry->id.value() != resId.entryId()) { + diag->error(DiagMessage(value->getSource()) + << "trying to add resource '" + << name + << "' with ID " + << resId + << " but resource already has ID " + << ResourceId(package->id.value(), type->id.value(), entry->id.value())); + return false; + } + + const auto endIter = entry->values.end(); + auto iter = std::lower_bound(entry->values.begin(), endIter, config, cmp::lessThanConfig); if (iter == endIter || iter->config != config) { // This resource did not exist before, add it. - entry->values.insert(iter, ResourceConfigValue{ config, source, {}, std::move(value) }); + entry->values.insert(iter, ResourceConfigValue{ config, std::move(value) }); } else { - int collisionResult = defaultCollisionHandler(*iter->value, *value); + int collisionResult = conflictResolver(iter->value.get(), value.get()); if (collisionResult > 0) { // Take the incoming value. - *iter = ResourceConfigValue{ config, source, {}, std::move(value) }; + iter->value = std::move(value); } else if (collisionResult == 0) { - Logger::error(source) - << "duplicate value for resource '" << name << "' " - << "with config '" << iter->config << "'." - << std::endl; - - Logger::error(iter->source) - << "resource previously defined here." - << std::endl; + diag->error(DiagMessage(value->getSource()) + << "duplicate value for resource '" << name << "' " + << "with config '" << config << "'"); + diag->error(DiagMessage(iter->value->getSource()) + << "resource previously defined here"); return false; } } if (resId.isValid()) { - type->typeId = resId.typeId(); - entry->entryId = resId.entryId(); + package->id = resId.packageId(); + type->id = resId.typeId(); + entry->id = resId.entryId(); } return true; } -bool ResourceTable::markPublic(const ResourceNameRef& name, const ResourceId resId, - const SourceLine& source) { - return markPublicImpl(name, resId, source, kValidNameChars); +bool ResourceTable::setSymbolState(const ResourceNameRef& name, const ResourceId resId, + const Symbol& symbol, IDiagnostics* diag) { + return setSymbolStateImpl(name, resId, symbol, kValidNameChars, diag); } -bool ResourceTable::markPublicAllowMangled(const ResourceNameRef& name, const ResourceId resId, - const SourceLine& source) { - return markPublicImpl(name, resId, source, kValidNameMangledChars); +bool ResourceTable::setSymbolStateAllowMangled(const ResourceNameRef& name, + const ResourceId resId, + const Symbol& symbol, IDiagnostics* diag) { + return setSymbolStateImpl(name, resId, symbol, kValidNameMangledChars, diag); } -bool ResourceTable::markPublicImpl(const ResourceNameRef& name, const ResourceId resId, - const SourceLine& source, const char16_t* validChars) { - if (!name.package.empty() && name.package != mPackage) { - Logger::error(source) - << "resource '" - << name - << "' has incompatible package. Must be '" - << mPackage - << "'." - << std::endl; - return false; - } +bool ResourceTable::setSymbolStateImpl(const ResourceNameRef& name, const ResourceId resId, + const Symbol& symbol, const char16_t* validChars, + IDiagnostics* diag) { + assert(diag && "diagnostics can't be nullptr"); auto badCharIter = util::findNonAlphaNumericAndNotInSet(name.entry, validChars); if (badCharIter != name.entry.end()) { - Logger::error(source) - << "resource '" - << name - << "' has invalid entry name '" - << name.entry - << "'. Invalid character '" - << StringPiece16(badCharIter, 1) - << "'." - << std::endl; + diag->error(DiagMessage(symbol.source) + << "resource '" + << name + << "' has invalid entry name '" + << name.entry + << "'. Invalid character '" + << StringPiece16(badCharIter, 1) + << "'"); return false; } - std::unique_ptr<ResourceTableType>& type = findOrCreateType(name.type); - if (resId.isValid() && type->typeId != ResourceTableType::kUnsetTypeId && - type->typeId != resId.typeId()) { - Logger::error(source) - << "trying to make resource '" - << name - << "' public with ID " - << resId - << " but type '" - << type->type - << "' already has ID " - << std::hex << type->typeId << std::dec - << "." - << std::endl; + ResourceTablePackage* package = findOrCreatePackage(name.package); + if (resId.isValid() && package->id && package->id.value() != resId.packageId()) { + diag->error(DiagMessage(symbol.source) + << "trying to add resource '" + << name + << "' with ID " + << resId + << " but package '" + << package->name + << "' already has ID " + << std::hex << (int) package->id.value() << std::dec); return false; } - std::unique_ptr<ResourceEntry>& entry = findOrCreateEntry(type, name.entry); - if (resId.isValid() && entry->entryId != ResourceEntry::kUnsetEntryId && - entry->entryId != resId.entryId()) { - Logger::error(source) - << "trying to make resource '" - << name - << "' public with ID " - << resId - << " but resource already has ID " - << ResourceId(mPackageId, type->typeId, entry->entryId) - << "." - << std::endl; + ResourceTableType* type = package->findOrCreateType(name.type); + if (resId.isValid() && type->id && type->id.value() != resId.typeId()) { + diag->error(DiagMessage(symbol.source) + << "trying to add resource '" + << name + << "' with ID " + << resId + << " but type '" + << type->type + << "' already has ID " + << std::hex << (int) type->id.value() << std::dec); return false; } - type->publicStatus.isPublic = true; - entry->publicStatus.isPublic = true; - entry->publicStatus.source = source; + ResourceEntry* entry = type->findOrCreateEntry(name.entry); + if (resId.isValid() && entry->id && entry->id.value() != resId.entryId()) { + diag->error(DiagMessage(symbol.source) + << "trying to add resource '" + << name + << "' with ID " + << resId + << " but resource already has ID " + << ResourceId(package->id.value(), type->id.value(), entry->id.value())); + return false; + } if (resId.isValid()) { - type->typeId = resId.typeId(); - entry->entryId = resId.entryId(); + package->id = resId.packageId(); + type->id = resId.typeId(); + entry->id = resId.entryId(); } - return true; -} -bool ResourceTable::merge(ResourceTable&& other) { - const bool mangleNames = mPackage != other.getPackage(); - std::u16string mangledName; - - for (auto& otherType : other) { - std::unique_ptr<ResourceTableType>& type = findOrCreateType(otherType->type); - if (otherType->publicStatus.isPublic) { - if (type->publicStatus.isPublic && type->typeId != otherType->typeId) { - Logger::error() << "can not merge type '" << type->type - << "': conflicting public IDs " - << "(" << type->typeId << " vs " << otherType->typeId << ")." - << std::endl; - return false; - } - type->publicStatus = std::move(otherType->publicStatus); - type->typeId = otherType->typeId; - } + // Only mark the type state as public, it doesn't care about being private. + if (symbol.state == SymbolState::kPublic) { + type->symbolStatus.state = SymbolState::kPublic; + } - for (auto& otherEntry : otherType->entries) { - const std::u16string* nameToAdd = &otherEntry->name; - if (mangleNames) { - mangledName = otherEntry->name; - NameMangler::mangle(other.getPackage(), &mangledName); - nameToAdd = &mangledName; - } - - std::unique_ptr<ResourceEntry>& entry = findOrCreateEntry(type, *nameToAdd); - if (otherEntry->publicStatus.isPublic) { - if (entry->publicStatus.isPublic && entry->entryId != otherEntry->entryId) { - Logger::error() << "can not merge entry '" << type->type << "/" << entry->name - << "': conflicting public IDs " - << "(" << entry->entryId << " vs " << entry->entryId << ")." - << std::endl; - return false; - } - entry->publicStatus = std::move(otherEntry->publicStatus); - entry->entryId = otherEntry->entryId; - } - - for (ResourceConfigValue& otherValue : otherEntry->values) { - auto iter = std::lower_bound(entry->values.begin(), entry->values.end(), - otherValue.config, compareConfigs); - if (iter != entry->values.end() && iter->config == otherValue.config) { - int collisionResult = defaultCollisionHandler(*iter->value, *otherValue.value); - if (collisionResult > 0) { - // Take the incoming value. - iter->source = std::move(otherValue.source); - iter->comment = std::move(otherValue.comment); - iter->value = std::unique_ptr<Value>(otherValue.value->clone(&mValuePool)); - } else if (collisionResult == 0) { - ResourceNameRef resourceName = { mPackage, type->type, entry->name }; - Logger::error(otherValue.source) - << "resource '" << resourceName << "' has a conflicting value for " - << "configuration (" << otherValue.config << ")." - << std::endl; - Logger::note(iter->source) << "originally defined here." << std::endl; - return false; - } - } else { - entry->values.insert(iter, ResourceConfigValue{ - otherValue.config, - std::move(otherValue.source), - std::move(otherValue.comment), - std::unique_ptr<Value>(otherValue.value->clone(&mValuePool)), - }); - } - } - } + if (symbol.state == SymbolState::kUndefined && + entry->symbolStatus.state != SymbolState::kUndefined) { + // We can't undefine a symbol (remove its visibility). Ignore. + return true; } + + if (symbol.state == SymbolState::kPrivate && + entry->symbolStatus.state == SymbolState::kPublic) { + // We can't downgrade public to private. Ignore. + return true; + } + + entry->symbolStatus = std::move(symbol); return true; } -std::tuple<const ResourceTableType*, const ResourceEntry*> -ResourceTable::findResource(const ResourceNameRef& name) const { - if (name.package != mPackage) { +Maybe<ResourceTable::SearchResult> +ResourceTable::findResource(const ResourceNameRef& name) { + ResourceTablePackage* package = findPackage(name.package); + if (!package) { return {}; } - auto iter = std::lower_bound(mTypes.begin(), mTypes.end(), name.type, lessThanType); - if (iter == mTypes.end() || (*iter)->type != name.type) { + ResourceTableType* type = package->findType(name.type); + if (!type) { return {}; } - const std::unique_ptr<ResourceTableType>& type = *iter; - auto iter2 = std::lower_bound(type->entries.begin(), type->entries.end(), name.entry, - lessThanEntry); - if (iter2 == type->entries.end() || name.entry != (*iter2)->name) { + ResourceEntry* entry = type->findEntry(name.entry); + if (!entry) { return {}; } - return std::make_tuple(iter->get(), iter2->get()); + return SearchResult{ package, type, entry }; } } // namespace aapt diff --git a/tools/aapt2/ResourceTable.h b/tools/aapt2/ResourceTable.h index 706f56a2776f..6b7b07ea3a93 100644 --- a/tools/aapt2/ResourceTable.h +++ b/tools/aapt2/ResourceTable.h @@ -18,6 +18,7 @@ #define AAPT_RESOURCE_TABLE_H #include "ConfigDescription.h" +#include "Diagnostics.h" #include "Resource.h" #include "ResourceValues.h" #include "Source.h" @@ -30,22 +31,26 @@ namespace aapt { +enum class SymbolState { + kUndefined, + kPublic, + kPrivate +}; + /** * The Public status of a resource. */ -struct Public { - bool isPublic = false; - SourceLine source; +struct Symbol { + SymbolState state = SymbolState::kUndefined; + Source source; std::u16string comment; }; /** - * The resource value for a specific configuration. + * Represents a value defined for a given configuration. */ struct ResourceConfigValue { ConfigDescription config; - SourceLine source; - std::u16string comment; std::unique_ptr<Value> value; }; @@ -54,10 +59,6 @@ struct ResourceConfigValue { * varying values for each defined configuration. */ struct ResourceEntry { - enum { - kUnsetEntryId = 0xffffffffu - }; - /** * The name of the resource. Immutable, as * this determines the order of this resource @@ -68,21 +69,20 @@ struct ResourceEntry { /** * The entry ID for this resource. */ - size_t entryId; + Maybe<uint16_t> id; /** * Whether this resource is public (and must maintain the same * entry ID across builds). */ - Public publicStatus; + Symbol symbolStatus; /** * The resource's values for each configuration. */ std::vector<ResourceConfigValue> values; - inline ResourceEntry(const StringPiece16& _name); - inline ResourceEntry(const ResourceEntry* rhs); + ResourceEntry(const StringPiece16& name) : name(name.toString()) { } }; /** @@ -90,10 +90,6 @@ struct ResourceEntry { * for this type. */ struct ResourceTableType { - enum { - kUnsetTypeId = 0xffffffffu - }; - /** * The logical type of resource (string, drawable, layout, etc.). */ @@ -102,21 +98,43 @@ struct ResourceTableType { /** * The type ID for this resource. */ - size_t typeId; + Maybe<uint8_t> id; /** * Whether this type is public (and must maintain the same * type ID across builds). */ - Public publicStatus; + Symbol symbolStatus; /** * List of resources for this type. */ std::vector<std::unique_ptr<ResourceEntry>> entries; - ResourceTableType(const ResourceType _type); - ResourceTableType(const ResourceTableType* rhs); + explicit ResourceTableType(const ResourceType type) : type(type) { } + + ResourceEntry* findEntry(const StringPiece16& name); + + ResourceEntry* findOrCreateEntry(const StringPiece16& name); +}; + +enum class PackageType { + System, + Vendor, + App, + Dynamic +}; + +struct ResourceTablePackage { + PackageType type = PackageType::App; + Maybe<uint8_t> id; + std::u16string name; + + std::vector<std::unique_ptr<ResourceTableType>> types; + + ResourceTableType* findType(ResourceType type); + + ResourceTableType* findOrCreateType(const ResourceType type); }; /** @@ -125,23 +143,32 @@ struct ResourceTableType { */ class ResourceTable { public: - using iterator = std::vector<std::unique_ptr<ResourceTableType>>::iterator; - using const_iterator = std::vector<std::unique_ptr<ResourceTableType>>::const_iterator; + ResourceTable() = default; + ResourceTable(const ResourceTable&) = delete; + ResourceTable& operator=(const ResourceTable&) = delete; - enum { - kUnsetPackageId = 0xffffffff - }; + /** + * When a collision of resources occurs, this method decides which value to keep. + * Returns -1 if the existing value should be chosen. + * Returns 0 if the collision can not be resolved (error). + * Returns 1 if the incoming value should be chosen. + */ + static int resolveValueCollision(Value* existing, Value* incoming); - ResourceTable(); + bool addResource(const ResourceNameRef& name, const ConfigDescription& config, + std::unique_ptr<Value> value, IDiagnostics* diag); - size_t getPackageId() const; - void setPackageId(size_t packageId); + bool addResource(const ResourceNameRef& name, const ResourceId resId, + const ConfigDescription& config, std::unique_ptr<Value> value, + IDiagnostics* diag); - const std::u16string& getPackage() const; - void setPackage(const StringPiece16& package); + bool addFileReference(const ResourceNameRef& name, const ConfigDescription& config, + const Source& source, const StringPiece16& path, + IDiagnostics* diag); - bool addResource(const ResourceNameRef& name, const ConfigDescription& config, - const SourceLine& source, std::unique_ptr<Value> value); + bool addFileReference(const ResourceNameRef& name, const ConfigDescription& config, + const Source& source, const StringPiece16& path, + std::function<int(Value*,Value*)> conflictResolver, IDiagnostics* diag); /** * Same as addResource, but doesn't verify the validity of the name. This is used @@ -149,129 +176,68 @@ public: * names. */ bool addResourceAllowMangled(const ResourceNameRef& name, const ConfigDescription& config, - const SourceLine& source, std::unique_ptr<Value> value); + std::unique_ptr<Value> value, IDiagnostics* diag); - bool addResource(const ResourceNameRef& name, const ResourceId resId, - const ConfigDescription& config, const SourceLine& source, - std::unique_ptr<Value> value); + bool addResourceAllowMangled(const ResourceNameRef& name, const ResourceId id, + const ConfigDescription& config, std::unique_ptr<Value> value, + IDiagnostics* diag); + + bool setSymbolState(const ResourceNameRef& name, const ResourceId resId, + const Symbol& symbol, IDiagnostics* diag); - bool markPublic(const ResourceNameRef& name, const ResourceId resId, const SourceLine& source); - bool markPublicAllowMangled(const ResourceNameRef& name, const ResourceId resId, - const SourceLine& source); + bool setSymbolStateAllowMangled(const ResourceNameRef& name, const ResourceId resId, + const Symbol& symbol, IDiagnostics* diag); - /* - * Merges the resources from `other` into this table, mangling the names of the resources - * if `other` has a different package name. + struct SearchResult { + ResourceTablePackage* package; + ResourceTableType* type; + ResourceEntry* entry; + }; + + Maybe<SearchResult> findResource(const ResourceNameRef& name); + + /** + * The string pool used by this resource table. Values that reference strings must use + * this pool to create their strings. + * + * NOTE: `stringPool` must come before `packages` so that it is destroyed after. + * When `string pool` references are destroyed (as they will be when `packages` + * is destroyed), they decrement a refCount, which would cause invalid + * memory access if the pool was already destroyed. + */ + StringPool stringPool; + + /** + * The list of packages in this table, sorted alphabetically by package name. */ - bool merge(ResourceTable&& other); + std::vector<std::unique_ptr<ResourceTablePackage>> packages; /** - * Returns the string pool used by this ResourceTable. - * Values that reference strings should use this pool to create - * their strings. + * Returns the package struct with the given name, or nullptr if such a package does not + * exist. The empty string is a valid package and typically is used to represent the + * 'current' package before it is known to the ResourceTable. */ - StringPool& getValueStringPool(); - const StringPool& getValueStringPool() const; + ResourceTablePackage* findPackage(const StringPiece16& name); - std::tuple<const ResourceTableType*, const ResourceEntry*> - findResource(const ResourceNameRef& name) const; + ResourceTablePackage* findPackageById(uint8_t id); - iterator begin(); - iterator end(); - const_iterator begin() const; - const_iterator end() const; + ResourceTablePackage* createPackage(const StringPiece16& name, Maybe<uint8_t> id = {}); private: - std::unique_ptr<ResourceTableType>& findOrCreateType(ResourceType type); - std::unique_ptr<ResourceEntry>& findOrCreateEntry(std::unique_ptr<ResourceTableType>& type, - const StringPiece16& name); - - bool addResourceImpl(const ResourceNameRef& name, const ResourceId resId, - const ConfigDescription& config, const SourceLine& source, - std::unique_ptr<Value> value, const char16_t* validChars); - bool markPublicImpl(const ResourceNameRef& name, const ResourceId resId, - const SourceLine& source, const char16_t* validChars); - - std::u16string mPackage; - size_t mPackageId; - - // StringPool must come before mTypes so that it is destroyed after. - // When StringPool references are destroyed (as they will be when mTypes - // is destroyed), they decrement a refCount, which would cause invalid - // memory access if the pool was already destroyed. - StringPool mValuePool; - - std::vector<std::unique_ptr<ResourceTableType>> mTypes; + ResourceTablePackage* findOrCreatePackage(const StringPiece16& name); + + bool addResourceImpl(const ResourceNameRef& name, + ResourceId resId, + const ConfigDescription& config, + std::unique_ptr<Value> value, + const char16_t* validChars, + std::function<int(Value*,Value*)> conflictResolver, + IDiagnostics* diag); + + bool setSymbolStateImpl(const ResourceNameRef& name, ResourceId resId, + const Symbol& symbol, const char16_t* validChars, IDiagnostics* diag); }; -// -// ResourceEntry implementation. -// - -inline ResourceEntry::ResourceEntry(const StringPiece16& _name) : - name(_name.toString()), entryId(kUnsetEntryId) { -} - -inline ResourceEntry::ResourceEntry(const ResourceEntry* rhs) : - name(rhs->name), entryId(rhs->entryId), publicStatus(rhs->publicStatus) { -} - -// -// ResourceTableType implementation. -// - -inline ResourceTableType::ResourceTableType(const ResourceType _type) : - type(_type), typeId(kUnsetTypeId) { -} - -inline ResourceTableType::ResourceTableType(const ResourceTableType* rhs) : - type(rhs->type), typeId(rhs->typeId), publicStatus(rhs->publicStatus) { -} - -// -// ResourceTable implementation. -// - -inline StringPool& ResourceTable::getValueStringPool() { - return mValuePool; -} - -inline const StringPool& ResourceTable::getValueStringPool() const { - return mValuePool; -} - -inline ResourceTable::iterator ResourceTable::begin() { - return mTypes.begin(); -} - -inline ResourceTable::iterator ResourceTable::end() { - return mTypes.end(); -} - -inline ResourceTable::const_iterator ResourceTable::begin() const { - return mTypes.begin(); -} - -inline ResourceTable::const_iterator ResourceTable::end() const { - return mTypes.end(); -} - -inline const std::u16string& ResourceTable::getPackage() const { - return mPackage; -} - -inline size_t ResourceTable::getPackageId() const { - return mPackageId; -} - -inline void ResourceTable::setPackage(const StringPiece16& package) { - mPackage = package.toString(); -} - -inline void ResourceTable::setPackageId(size_t packageId) { - mPackageId = packageId; -} - } // namespace aapt #endif // AAPT_RESOURCE_TABLE_H diff --git a/tools/aapt2/ResourceTableResolver.cpp b/tools/aapt2/ResourceTableResolver.cpp deleted file mode 100644 index 910c2c07fb84..000000000000 --- a/tools/aapt2/ResourceTableResolver.cpp +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "Maybe.h" -#include "NameMangler.h" -#include "Resource.h" -#include "ResourceTable.h" -#include "ResourceTableResolver.h" -#include "ResourceValues.h" -#include "Util.h" - -#include <androidfw/AssetManager.h> -#include <androidfw/ResourceTypes.h> -#include <memory> -#include <vector> - -namespace aapt { - -ResourceTableResolver::ResourceTableResolver( - std::shared_ptr<const ResourceTable> table, - const std::vector<std::shared_ptr<const android::AssetManager>>& sources) : - mTable(table), mSources(sources) { - for (const auto& assetManager : mSources) { - const android::ResTable& resTable = assetManager->getResources(false); - const size_t packageCount = resTable.getBasePackageCount(); - for (size_t i = 0; i < packageCount; i++) { - std::u16string packageName = resTable.getBasePackageName(i).string(); - mIncludedPackages.insert(std::move(packageName)); - } - } -} - -Maybe<ResourceId> ResourceTableResolver::findId(const ResourceName& name) { - Maybe<Entry> result = findAttribute(name); - if (result) { - return result.value().id; - } - return {}; -} - -Maybe<IResolver::Entry> ResourceTableResolver::findAttribute(const ResourceName& name) { - auto cacheIter = mCache.find(name); - if (cacheIter != std::end(mCache)) { - return Entry{ cacheIter->second.id, cacheIter->second.attr.get() }; - } - - ResourceName mangledName; - const ResourceName* nameToSearch = &name; - if (name.package != mTable->getPackage()) { - // This may be a reference to an included resource or - // to a mangled resource. - if (mIncludedPackages.find(name.package) == mIncludedPackages.end()) { - // This is not in our included set, so mangle the name and - // check for that. - mangledName.entry = name.entry; - NameMangler::mangle(name.package, &mangledName.entry); - mangledName.package = mTable->getPackage(); - mangledName.type = name.type; - nameToSearch = &mangledName; - } else { - const CacheEntry* cacheEntry = buildCacheEntry(name); - if (cacheEntry) { - return Entry{ cacheEntry->id, cacheEntry->attr.get() }; - } - return {}; - } - } - - const ResourceTableType* type; - const ResourceEntry* entry; - std::tie(type, entry) = mTable->findResource(*nameToSearch); - if (type && entry) { - Entry result = {}; - if (mTable->getPackageId() != ResourceTable::kUnsetPackageId && - type->typeId != ResourceTableType::kUnsetTypeId && - entry->entryId != ResourceEntry::kUnsetEntryId) { - result.id = ResourceId(mTable->getPackageId(), type->typeId, entry->entryId); - } - - if (!entry->values.empty()) { - visitFunc<Attribute>(*entry->values.front().value, [&result](Attribute& attr) { - result.attr = &attr; - }); - } - return result; - } - return {}; -} - -Maybe<ResourceName> ResourceTableResolver::findName(ResourceId resId) { - for (const auto& assetManager : mSources) { - const android::ResTable& table = assetManager->getResources(false); - - android::ResTable::resource_name resourceName; - if (!table.getResourceName(resId.id, false, &resourceName)) { - continue; - } - - const ResourceType* type = parseResourceType(StringPiece16(resourceName.type, - resourceName.typeLen)); - assert(type); - return ResourceName{ - { resourceName.package, resourceName.packageLen }, - *type, - { resourceName.name, resourceName.nameLen } }; - } - return {}; -} - -/** - * This is called when we need to lookup a resource name in the AssetManager. - * Since the values in the AssetManager are not parsed like in a ResourceTable, - * we must create Attribute objects here if we find them. - */ -const ResourceTableResolver::CacheEntry* ResourceTableResolver::buildCacheEntry( - const ResourceName& name) { - for (const auto& assetManager : mSources) { - const android::ResTable& table = assetManager->getResources(false); - - const StringPiece16 type16 = toString(name.type); - ResourceId resId { - table.identifierForName( - name.entry.data(), name.entry.size(), - type16.data(), type16.size(), - name.package.data(), name.package.size()) - }; - - if (!resId.isValid()) { - continue; - } - - CacheEntry& entry = mCache[name]; - entry.id = resId; - - // - // Now check to see if this resource is an Attribute. - // - - const android::ResTable::bag_entry* bagBegin; - ssize_t bags = table.lockBag(resId.id, &bagBegin); - if (bags < 1) { - table.unlockBag(bagBegin); - return &entry; - } - - // Look for the ATTR_TYPE key in the bag and check the types it supports. - uint32_t attrTypeMask = 0; - for (ssize_t i = 0; i < bags; i++) { - if (bagBegin[i].map.name.ident == android::ResTable_map::ATTR_TYPE) { - attrTypeMask = bagBegin[i].map.value.data; - } - } - - entry.attr = util::make_unique<Attribute>(false); - - if (attrTypeMask & android::ResTable_map::TYPE_ENUM || - attrTypeMask & android::ResTable_map::TYPE_FLAGS) { - for (ssize_t i = 0; i < bags; i++) { - if (Res_INTERNALID(bagBegin[i].map.name.ident)) { - // Internal IDs are special keys, which are not enum/flag symbols, so skip. - continue; - } - - android::ResTable::resource_name symbolName; - bool result = table.getResourceName(bagBegin[i].map.name.ident, false, - &symbolName); - assert(result); - const ResourceType* type = parseResourceType( - StringPiece16(symbolName.type, symbolName.typeLen)); - assert(type); - - entry.attr->symbols.push_back(Attribute::Symbol{ - Reference(ResourceNameRef( - StringPiece16(symbolName.package, symbolName.packageLen), - *type, - StringPiece16(symbolName.name, symbolName.nameLen))), - bagBegin[i].map.value.data - }); - } - } - - entry.attr->typeMask |= attrTypeMask; - table.unlockBag(bagBegin); - return &entry; - } - return nullptr; -} - -} // namespace aapt diff --git a/tools/aapt2/ResourceTableResolver.h b/tools/aapt2/ResourceTableResolver.h deleted file mode 100644 index 8f6b0b5993e4..000000000000 --- a/tools/aapt2/ResourceTableResolver.h +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_RESOURCE_TABLE_RESOLVER_H -#define AAPT_RESOURCE_TABLE_RESOLVER_H - -#include "Maybe.h" -#include "Resolver.h" -#include "Resource.h" -#include "ResourceTable.h" -#include "ResourceValues.h" - -#include <androidfw/AssetManager.h> -#include <memory> -#include <vector> -#include <unordered_set> - -namespace aapt { - -/** - * Encapsulates the search of library sources as well as the local ResourceTable. - */ -class ResourceTableResolver : public IResolver { -public: - /** - * Creates a resolver with a local ResourceTable and an AssetManager - * loaded with library packages. - */ - ResourceTableResolver( - std::shared_ptr<const ResourceTable> table, - const std::vector<std::shared_ptr<const android::AssetManager>>& sources); - - ResourceTableResolver(const ResourceTableResolver&) = delete; // Not copyable. - - virtual Maybe<ResourceId> findId(const ResourceName& name) override; - - virtual Maybe<Entry> findAttribute(const ResourceName& name) override; - - virtual Maybe<ResourceName> findName(ResourceId resId) override; - -private: - struct CacheEntry { - ResourceId id; - std::unique_ptr<Attribute> attr; - }; - - const CacheEntry* buildCacheEntry(const ResourceName& name); - - std::shared_ptr<const ResourceTable> mTable; - std::vector<std::shared_ptr<const android::AssetManager>> mSources; - std::map<ResourceName, CacheEntry> mCache; - std::unordered_set<std::u16string> mIncludedPackages; -}; - -} // namespace aapt - -#endif // AAPT_RESOURCE_TABLE_RESOLVER_H diff --git a/tools/aapt2/ResourceTable_test.cpp b/tools/aapt2/ResourceTable_test.cpp index 06d8699730de..42508fe154b8 100644 --- a/tools/aapt2/ResourceTable_test.cpp +++ b/tools/aapt2/ResourceTable_test.cpp @@ -14,9 +14,12 @@ * limitations under the License. */ +#include "Diagnostics.h" #include "ResourceTable.h" #include "ResourceValues.h" -#include "Util.h" +#include "util/Util.h" + +#include "test/Builders.h" #include <algorithm> #include <gtest/gtest.h> @@ -25,204 +28,100 @@ namespace aapt { -struct TestValue : public Value { - std::u16string value; - - TestValue(StringPiece16 str) : value(str.toString()) { - } - - TestValue* clone(StringPool* /*newPool*/) const override { - return new TestValue(value); - } - - void print(std::ostream& out) const override { - out << "(test) " << value; - } - - virtual void accept(ValueVisitor&, ValueVisitorArgs&&) override {} - virtual void accept(ConstValueVisitor&, ValueVisitorArgs&&) const override {} -}; - -struct TestWeakValue : public Value { - bool isWeak() const override { - return true; - } - - TestWeakValue* clone(StringPool* /*newPool*/) const override { - return new TestWeakValue(); - } - - void print(std::ostream& out) const override { - out << "(test) [weak]"; - } +struct ResourceTableTest : public ::testing::Test { + struct EmptyDiagnostics : public IDiagnostics { + void error(const DiagMessage& msg) override {} + void warn(const DiagMessage& msg) override {} + void note(const DiagMessage& msg) override {} + }; - virtual void accept(ValueVisitor&, ValueVisitorArgs&&) override {} - virtual void accept(ConstValueVisitor&, ValueVisitorArgs&&) const override {} + EmptyDiagnostics mDiagnostics; }; -TEST(ResourceTableTest, FailToAddResourceWithBadName) { +TEST_F(ResourceTableTest, FailToAddResourceWithBadName) { ResourceTable table; - table.setPackage(u"android"); EXPECT_FALSE(table.addResource( - ResourceNameRef{ u"android", ResourceType::kId, u"hey,there" }, - {}, SourceLine{ "test.xml", 21 }, - util::make_unique<TestValue>(u"rawValue"))); + ResourceNameRef(u"android", ResourceType::kId, u"hey,there"), + ConfigDescription{}, + test::ValueBuilder<Id>().setSource("test.xml", 21u).build(), + &mDiagnostics)); EXPECT_FALSE(table.addResource( - ResourceNameRef{ u"android", ResourceType::kId, u"hey:there" }, - {}, SourceLine{ "test.xml", 21 }, - util::make_unique<TestValue>(u"rawValue"))); + ResourceNameRef(u"android", ResourceType::kId, u"hey:there"), + ConfigDescription{}, + test::ValueBuilder<Id>().setSource("test.xml", 21u).build(), + &mDiagnostics)); } -TEST(ResourceTableTest, AddOneResource) { - const std::u16string kAndroidPackage = u"android"; - +TEST_F(ResourceTableTest, AddOneResource) { ResourceTable table; - table.setPackage(kAndroidPackage); - - const ResourceName name = { kAndroidPackage, ResourceType::kAttr, u"id" }; - EXPECT_TRUE(table.addResource(name, {}, SourceLine{ "test/path/file.xml", 23 }, - util::make_unique<TestValue>(u"rawValue"))); + EXPECT_TRUE(table.addResource(test::parseNameOrDie(u"@android:attr/id"), + ConfigDescription{}, + test::ValueBuilder<Id>() + .setSource("test/path/file.xml", 23u).build(), + &mDiagnostics)); - const ResourceTableType* type; - const ResourceEntry* entry; - std::tie(type, entry) = table.findResource(name); - ASSERT_NE(nullptr, type); - ASSERT_NE(nullptr, entry); - EXPECT_EQ(name.entry, entry->name); - - ASSERT_NE(std::end(entry->values), - std::find_if(std::begin(entry->values), std::end(entry->values), - [](const ResourceConfigValue& val) -> bool { - return val.config == ConfigDescription{}; - })); + ASSERT_NE(nullptr, test::getValue<Id>(&table, u"@android:attr/id")); } -TEST(ResourceTableTest, AddMultipleResources) { - const std::u16string kAndroidPackage = u"android"; +TEST_F(ResourceTableTest, AddMultipleResources) { ResourceTable table; - table.setPackage(kAndroidPackage); ConfigDescription config; ConfigDescription languageConfig; memcpy(languageConfig.language, "pl", sizeof(languageConfig.language)); EXPECT_TRUE(table.addResource( - ResourceName{ kAndroidPackage, ResourceType::kAttr, u"layout_width" }, - config, SourceLine{ "test/path/file.xml", 10 }, - util::make_unique<TestValue>(u"rawValue"))); + test::parseNameOrDie(u"@android:attr/layout_width"), + config, + test::ValueBuilder<Id>().setSource("test/path/file.xml", 10u).build(), + &mDiagnostics)); EXPECT_TRUE(table.addResource( - ResourceName{ kAndroidPackage, ResourceType::kAttr, u"id" }, - config, SourceLine{ "test/path/file.xml", 12 }, - util::make_unique<TestValue>(u"rawValue"))); + test::parseNameOrDie(u"@android:attr/id"), + config, + test::ValueBuilder<Id>().setSource("test/path/file.xml", 12u).build(), + &mDiagnostics)); EXPECT_TRUE(table.addResource( - ResourceName{ kAndroidPackage, ResourceType::kString, u"ok" }, - config, SourceLine{ "test/path/file.xml", 14 }, - util::make_unique<TestValue>(u"Ok"))); + test::parseNameOrDie(u"@android:string/ok"), + config, + test::ValueBuilder<Id>().setSource("test/path/file.xml", 14u).build(), + &mDiagnostics)); EXPECT_TRUE(table.addResource( - ResourceName{ kAndroidPackage, ResourceType::kString, u"ok" }, - languageConfig, SourceLine{ "test/path/file.xml", 20 }, - util::make_unique<TestValue>(u"Tak"))); - - const auto endTypeIter = std::end(table); - auto typeIter = std::begin(table); - - ASSERT_NE(endTypeIter, typeIter); - EXPECT_EQ(ResourceType::kAttr, (*typeIter)->type); - - { - const std::unique_ptr<ResourceTableType>& type = *typeIter; - const auto endEntryIter = std::end(type->entries); - auto entryIter = std::begin(type->entries); - ASSERT_NE(endEntryIter, entryIter); - EXPECT_EQ(std::u16string(u"id"), (*entryIter)->name); - - ++entryIter; - ASSERT_NE(endEntryIter, entryIter); - EXPECT_EQ(std::u16string(u"layout_width"), (*entryIter)->name); - - ++entryIter; - ASSERT_EQ(endEntryIter, entryIter); - } - - ++typeIter; - ASSERT_NE(endTypeIter, typeIter); - EXPECT_EQ(ResourceType::kString, (*typeIter)->type); - - { - const std::unique_ptr<ResourceTableType>& type = *typeIter; - const auto endEntryIter = std::end(type->entries); - auto entryIter = std::begin(type->entries); - ASSERT_NE(endEntryIter, entryIter); - EXPECT_EQ(std::u16string(u"ok"), (*entryIter)->name); - - { - const std::unique_ptr<ResourceEntry>& entry = *entryIter; - const auto endConfigIter = std::end(entry->values); - auto configIter = std::begin(entry->values); - - ASSERT_NE(endConfigIter, configIter); - EXPECT_EQ(config, configIter->config); - const TestValue* value = - dynamic_cast<const TestValue*>(configIter->value.get()); - ASSERT_NE(nullptr, value); - EXPECT_EQ(std::u16string(u"Ok"), value->value); - - ++configIter; - ASSERT_NE(endConfigIter, configIter); - EXPECT_EQ(languageConfig, configIter->config); - EXPECT_NE(nullptr, configIter->value); - - value = dynamic_cast<const TestValue*>(configIter->value.get()); - ASSERT_NE(nullptr, value); - EXPECT_EQ(std::u16string(u"Tak"), value->value); - - ++configIter; - EXPECT_EQ(endConfigIter, configIter); - } - - ++entryIter; - ASSERT_EQ(endEntryIter, entryIter); - } - - ++typeIter; - EXPECT_EQ(endTypeIter, typeIter); + test::parseNameOrDie(u"@android:string/ok"), + languageConfig, + test::ValueBuilder<BinaryPrimitive>(android::Res_value{}) + .setSource("test/path/file.xml", 20u) + .build(), + &mDiagnostics)); + + ASSERT_NE(nullptr, test::getValue<Id>(&table, u"@android:attr/layout_width")); + ASSERT_NE(nullptr, test::getValue<Id>(&table, u"@android:attr/id")); + ASSERT_NE(nullptr, test::getValue<Id>(&table, u"@android:string/ok")); + ASSERT_NE(nullptr, test::getValueForConfig<BinaryPrimitive>(&table, u"@android:string/ok", + languageConfig)); } -TEST(ResourceTableTest, OverrideWeakResourceValue) { - const std::u16string kAndroid = u"android"; - +TEST_F(ResourceTableTest, OverrideWeakResourceValue) { ResourceTable table; - table.setPackage(kAndroid); - table.setPackageId(0x01); - - ASSERT_TRUE(table.addResource( - ResourceName{ kAndroid, ResourceType::kAttr, u"foo" }, - {}, {}, util::make_unique<TestWeakValue>())); - - const ResourceTableType* type; - const ResourceEntry* entry; - std::tie(type, entry) = table.findResource( - ResourceNameRef{ kAndroid, ResourceType::kAttr, u"foo" }); - ASSERT_NE(nullptr, type); - ASSERT_NE(nullptr, entry); - ASSERT_EQ(entry->values.size(), 1u); - EXPECT_TRUE(entry->values.front().value->isWeak()); - - ASSERT_TRUE(table.addResource(ResourceName{ kAndroid, ResourceType::kAttr, u"foo" }, {}, {}, - util::make_unique<TestValue>(u"bar"))); - - std::tie(type, entry) = table.findResource( - ResourceNameRef{ kAndroid, ResourceType::kAttr, u"foo" }); - ASSERT_NE(nullptr, type); - ASSERT_NE(nullptr, entry); - ASSERT_EQ(entry->values.size(), 1u); - EXPECT_FALSE(entry->values.front().value->isWeak()); + + ASSERT_TRUE(table.addResource(test::parseNameOrDie(u"@android:attr/foo"), ConfigDescription{}, + util::make_unique<Attribute>(true), &mDiagnostics)); + + Attribute* attr = test::getValue<Attribute>(&table, u"@android:attr/foo"); + ASSERT_NE(nullptr, attr); + EXPECT_TRUE(attr->isWeak()); + + ASSERT_TRUE(table.addResource(test::parseNameOrDie(u"@android:attr/foo"), ConfigDescription{}, + util::make_unique<Attribute>(false), &mDiagnostics)); + + attr = test::getValue<Attribute>(&table, u"@android:attr/foo"); + ASSERT_NE(nullptr, attr); + EXPECT_FALSE(attr->isWeak()); } } // namespace aapt diff --git a/tools/aapt2/ResourceUtils.cpp b/tools/aapt2/ResourceUtils.cpp new file mode 100644 index 000000000000..07f62afe05b9 --- /dev/null +++ b/tools/aapt2/ResourceUtils.cpp @@ -0,0 +1,576 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NameMangler.h" +#include "ResourceUtils.h" +#include "flatten/ResourceTypeExtensions.h" +#include "util/Files.h" +#include "util/Util.h" + +#include <androidfw/ResourceTypes.h> +#include <sstream> + +namespace aapt { +namespace ResourceUtils { + +bool extractResourceName(const StringPiece16& str, StringPiece16* outPackage, + StringPiece16* outType, StringPiece16* outEntry) { + bool hasPackageSeparator = false; + bool hasTypeSeparator = false; + const char16_t* start = str.data(); + const char16_t* end = start + str.size(); + const char16_t* current = start; + while (current != end) { + if (outType->size() == 0 && *current == u'/') { + hasTypeSeparator = true; + outType->assign(start, current - start); + start = current + 1; + } else if (outPackage->size() == 0 && *current == u':') { + hasPackageSeparator = true; + outPackage->assign(start, current - start); + start = current + 1; + } + current++; + } + outEntry->assign(start, end - start); + + return !(hasPackageSeparator && outPackage->empty()) && !(hasTypeSeparator && outType->empty()); +} + +bool parseResourceName(const StringPiece16& str, ResourceNameRef* outRef, bool* outPrivate) { + size_t offset = 0; + bool priv = false; + if (str.data()[0] == u'*') { + priv = true; + offset = 1; + } + + StringPiece16 package; + StringPiece16 type; + StringPiece16 entry; + if (!extractResourceName(str.substr(offset, str.size() - offset), &package, &type, &entry)) { + return false; + } + + const ResourceType* parsedType = parseResourceType(type); + if (!parsedType) { + return false; + } + + if (entry.empty()) { + return false; + } + + if (outRef) { + outRef->package = package; + outRef->type = *parsedType; + outRef->entry = entry; + } + + if (outPrivate) { + *outPrivate = priv; + } + return true; +} + +bool tryParseReference(const StringPiece16& str, ResourceNameRef* outRef, bool* outCreate, + bool* outPrivate) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + if (trimmedStr.empty()) { + return false; + } + + bool create = false; + bool priv = false; + if (trimmedStr.data()[0] == u'@') { + size_t offset = 1; + if (trimmedStr.data()[1] == u'+') { + create = true; + offset += 1; + } + + ResourceNameRef name; + if (!parseResourceName(trimmedStr.substr(offset, trimmedStr.size() - offset), + &name, &priv)) { + return false; + } + + if (create && priv) { + return false; + } + + if (create && name.type != ResourceType::kId) { + return false; + } + + if (outRef) { + *outRef = name; + } + + if (outCreate) { + *outCreate = create; + } + + if (outPrivate) { + *outPrivate = priv; + } + return true; + } + return false; +} + +bool isReference(const StringPiece16& str) { + return tryParseReference(str, nullptr, nullptr, nullptr); +} + +bool tryParseAttributeReference(const StringPiece16& str, ResourceNameRef* outRef) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + if (trimmedStr.empty()) { + return false; + } + + if (*trimmedStr.data() == u'?') { + StringPiece16 package; + StringPiece16 type; + StringPiece16 entry; + if (!extractResourceName(trimmedStr.substr(1, trimmedStr.size() - 1), + &package, &type, &entry)) { + return false; + } + + if (!type.empty() && type != u"attr") { + return false; + } + + if (entry.empty()) { + return false; + } + + if (outRef) { + outRef->package = package; + outRef->type = ResourceType::kAttr; + outRef->entry = entry; + } + return true; + } + return false; +} + +bool isAttributeReference(const StringPiece16& str) { + return tryParseAttributeReference(str, nullptr); +} + +/* + * Style parent's are a bit different. We accept the following formats: + * + * @[[*]package:][style/]<entry> + * ?[[*]package:]style/<entry> + * <[*]package>:[style/]<entry> + * [[*]package:style/]<entry> + */ +Maybe<Reference> parseStyleParentReference(const StringPiece16& str, std::string* outError) { + if (str.empty()) { + return {}; + } + + StringPiece16 name = str; + + bool hasLeadingIdentifiers = false; + bool privateRef = false; + + // Skip over these identifiers. A style's parent is a normal reference. + if (name.data()[0] == u'@' || name.data()[0] == u'?') { + hasLeadingIdentifiers = true; + name = name.substr(1, name.size() - 1); + } + + if (name.data()[0] == u'*') { + privateRef = true; + name = name.substr(1, name.size() - 1); + } + + ResourceNameRef ref; + ref.type = ResourceType::kStyle; + + StringPiece16 typeStr; + extractResourceName(name, &ref.package, &typeStr, &ref.entry); + if (!typeStr.empty()) { + // If we have a type, make sure it is a Style. + const ResourceType* parsedType = parseResourceType(typeStr); + if (!parsedType || *parsedType != ResourceType::kStyle) { + std::stringstream err; + err << "invalid resource type '" << typeStr << "' for parent of style"; + *outError = err.str(); + return {}; + } + } + + if (!hasLeadingIdentifiers && ref.package.empty() && !typeStr.empty()) { + std::stringstream err; + err << "invalid parent reference '" << str << "'"; + *outError = err.str(); + return {}; + } + + Reference result(ref); + result.privateReference = privateRef; + return result; +} + +std::unique_ptr<Reference> tryParseReference(const StringPiece16& str, bool* outCreate) { + ResourceNameRef ref; + bool privateRef = false; + if (tryParseReference(str, &ref, outCreate, &privateRef)) { + std::unique_ptr<Reference> value = util::make_unique<Reference>(ref); + value->privateReference = privateRef; + return value; + } + + if (tryParseAttributeReference(str, &ref)) { + if (outCreate) { + *outCreate = false; + } + return util::make_unique<Reference>(ref, Reference::Type::kAttribute); + } + return {}; +} + +std::unique_ptr<BinaryPrimitive> tryParseNullOrEmpty(const StringPiece16& str) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + android::Res_value value = { }; + if (trimmedStr == u"@null") { + // TYPE_NULL with data set to 0 is interpreted by the runtime as an error. + // Instead we set the data type to TYPE_REFERENCE with a value of 0. + value.dataType = android::Res_value::TYPE_REFERENCE; + } else if (trimmedStr == u"@empty") { + // TYPE_NULL with value of DATA_NULL_EMPTY is handled fine by the runtime. + value.dataType = android::Res_value::TYPE_NULL; + value.data = android::Res_value::DATA_NULL_EMPTY; + } else { + return {}; + } + return util::make_unique<BinaryPrimitive>(value); +} + +std::unique_ptr<BinaryPrimitive> tryParseEnumSymbol(const Attribute* enumAttr, + const StringPiece16& str) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + for (const Attribute::Symbol& symbol : enumAttr->symbols) { + // Enum symbols are stored as @package:id/symbol resources, + // so we need to match against the 'entry' part of the identifier. + const ResourceName& enumSymbolResourceName = symbol.symbol.name.value(); + if (trimmedStr == enumSymbolResourceName.entry) { + android::Res_value value = { }; + value.dataType = android::Res_value::TYPE_INT_DEC; + value.data = symbol.value; + return util::make_unique<BinaryPrimitive>(value); + } + } + return {}; +} + +std::unique_ptr<BinaryPrimitive> tryParseFlagSymbol(const Attribute* flagAttr, + const StringPiece16& str) { + android::Res_value flags = { }; + flags.dataType = android::Res_value::TYPE_INT_DEC; + flags.data = 0u; + + if (util::trimWhitespace(str).empty()) { + // Empty string is a valid flag (0). + return util::make_unique<BinaryPrimitive>(flags); + } + + for (StringPiece16 part : util::tokenize(str, u'|')) { + StringPiece16 trimmedPart = util::trimWhitespace(part); + + bool flagSet = false; + for (const Attribute::Symbol& symbol : flagAttr->symbols) { + // Flag symbols are stored as @package:id/symbol resources, + // so we need to match against the 'entry' part of the identifier. + const ResourceName& flagSymbolResourceName = symbol.symbol.name.value(); + if (trimmedPart == flagSymbolResourceName.entry) { + flags.data |= symbol.value; + flagSet = true; + break; + } + } + + if (!flagSet) { + return {}; + } + } + return util::make_unique<BinaryPrimitive>(flags); +} + +static uint32_t parseHex(char16_t c, bool* outError) { + if (c >= u'0' && c <= u'9') { + return c - u'0'; + } else if (c >= u'a' && c <= u'f') { + return c - u'a' + 0xa; + } else if (c >= u'A' && c <= u'F') { + return c - u'A' + 0xa; + } else { + *outError = true; + return 0xffffffffu; + } +} + +std::unique_ptr<BinaryPrimitive> tryParseColor(const StringPiece16& str) { + StringPiece16 colorStr(util::trimWhitespace(str)); + const char16_t* start = colorStr.data(); + const size_t len = colorStr.size(); + if (len == 0 || start[0] != u'#') { + return {}; + } + + android::Res_value value = { }; + bool error = false; + if (len == 4) { + value.dataType = android::Res_value::TYPE_INT_COLOR_RGB4; + value.data = 0xff000000u; + value.data |= parseHex(start[1], &error) << 20; + value.data |= parseHex(start[1], &error) << 16; + value.data |= parseHex(start[2], &error) << 12; + value.data |= parseHex(start[2], &error) << 8; + value.data |= parseHex(start[3], &error) << 4; + value.data |= parseHex(start[3], &error); + } else if (len == 5) { + value.dataType = android::Res_value::TYPE_INT_COLOR_ARGB4; + value.data |= parseHex(start[1], &error) << 28; + value.data |= parseHex(start[1], &error) << 24; + value.data |= parseHex(start[2], &error) << 20; + value.data |= parseHex(start[2], &error) << 16; + value.data |= parseHex(start[3], &error) << 12; + value.data |= parseHex(start[3], &error) << 8; + value.data |= parseHex(start[4], &error) << 4; + value.data |= parseHex(start[4], &error); + } else if (len == 7) { + value.dataType = android::Res_value::TYPE_INT_COLOR_RGB8; + value.data = 0xff000000u; + value.data |= parseHex(start[1], &error) << 20; + value.data |= parseHex(start[2], &error) << 16; + value.data |= parseHex(start[3], &error) << 12; + value.data |= parseHex(start[4], &error) << 8; + value.data |= parseHex(start[5], &error) << 4; + value.data |= parseHex(start[6], &error); + } else if (len == 9) { + value.dataType = android::Res_value::TYPE_INT_COLOR_ARGB8; + value.data |= parseHex(start[1], &error) << 28; + value.data |= parseHex(start[2], &error) << 24; + value.data |= parseHex(start[3], &error) << 20; + value.data |= parseHex(start[4], &error) << 16; + value.data |= parseHex(start[5], &error) << 12; + value.data |= parseHex(start[6], &error) << 8; + value.data |= parseHex(start[7], &error) << 4; + value.data |= parseHex(start[8], &error); + } else { + return {}; + } + return error ? std::unique_ptr<BinaryPrimitive>() : util::make_unique<BinaryPrimitive>(value); +} + +bool tryParseBool(const StringPiece16& str, bool* outValue) { + StringPiece16 trimmedStr(util::trimWhitespace(str)); + if (trimmedStr == u"true" || trimmedStr == u"TRUE" || trimmedStr == u"True") { + if (outValue) { + *outValue = true; + } + return true; + } else if (trimmedStr == u"false" || trimmedStr == u"FALSE" || trimmedStr == u"False") { + if (outValue) { + *outValue = false; + } + return true; + } + return false; +} + +std::unique_ptr<BinaryPrimitive> tryParseBool(const StringPiece16& str) { + bool result = false; + if (tryParseBool(str, &result)) { + android::Res_value value = {}; + value.dataType = android::Res_value::TYPE_INT_BOOLEAN; + + if (result) { + value.data = 0xffffffffu; + } else { + value.data = 0; + } + return util::make_unique<BinaryPrimitive>(value); + } + return {}; +} + +std::unique_ptr<BinaryPrimitive> tryParseInt(const StringPiece16& str) { + android::Res_value value; + if (!android::ResTable::stringToInt(str.data(), str.size(), &value)) { + return {}; + } + return util::make_unique<BinaryPrimitive>(value); +} + +std::unique_ptr<BinaryPrimitive> tryParseFloat(const StringPiece16& str) { + android::Res_value value; + if (!android::ResTable::stringToFloat(str.data(), str.size(), &value)) { + return {}; + } + return util::make_unique<BinaryPrimitive>(value); +} + +uint32_t androidTypeToAttributeTypeMask(uint16_t type) { + switch (type) { + case android::Res_value::TYPE_NULL: + case android::Res_value::TYPE_REFERENCE: + case android::Res_value::TYPE_ATTRIBUTE: + case android::Res_value::TYPE_DYNAMIC_REFERENCE: + return android::ResTable_map::TYPE_REFERENCE; + + case android::Res_value::TYPE_STRING: + return android::ResTable_map::TYPE_STRING; + + case android::Res_value::TYPE_FLOAT: + return android::ResTable_map::TYPE_FLOAT; + + case android::Res_value::TYPE_DIMENSION: + return android::ResTable_map::TYPE_DIMENSION; + + case android::Res_value::TYPE_FRACTION: + return android::ResTable_map::TYPE_FRACTION; + + case android::Res_value::TYPE_INT_DEC: + case android::Res_value::TYPE_INT_HEX: + return android::ResTable_map::TYPE_INTEGER | android::ResTable_map::TYPE_ENUM + | android::ResTable_map::TYPE_FLAGS; + + case android::Res_value::TYPE_INT_BOOLEAN: + return android::ResTable_map::TYPE_BOOLEAN; + + case android::Res_value::TYPE_INT_COLOR_ARGB8: + case android::Res_value::TYPE_INT_COLOR_RGB8: + case android::Res_value::TYPE_INT_COLOR_ARGB4: + case android::Res_value::TYPE_INT_COLOR_RGB4: + return android::ResTable_map::TYPE_COLOR; + + default: + return 0; + }; +} + +std::unique_ptr<Item> parseItemForAttribute( + const StringPiece16& value, uint32_t typeMask, + std::function<void(const ResourceName&)> onCreateReference) { + std::unique_ptr<BinaryPrimitive> nullOrEmpty = tryParseNullOrEmpty(value); + if (nullOrEmpty) { + return std::move(nullOrEmpty); + } + + bool create = false; + std::unique_ptr<Reference> reference = tryParseReference(value, &create); + if (reference) { + if (create && onCreateReference) { + onCreateReference(reference->name.value()); + } + return std::move(reference); + } + + if (typeMask & android::ResTable_map::TYPE_COLOR) { + // Try parsing this as a color. + std::unique_ptr<BinaryPrimitive> color = tryParseColor(value); + if (color) { + return std::move(color); + } + } + + if (typeMask & android::ResTable_map::TYPE_BOOLEAN) { + // Try parsing this as a boolean. + std::unique_ptr<BinaryPrimitive> boolean = tryParseBool(value); + if (boolean) { + return std::move(boolean); + } + } + + if (typeMask & android::ResTable_map::TYPE_INTEGER) { + // Try parsing this as an integer. + std::unique_ptr<BinaryPrimitive> integer = tryParseInt(value); + if (integer) { + return std::move(integer); + } + } + + const uint32_t floatMask = android::ResTable_map::TYPE_FLOAT + | android::ResTable_map::TYPE_DIMENSION | android::ResTable_map::TYPE_FRACTION; + if (typeMask & floatMask) { + // Try parsing this as a float. + std::unique_ptr<BinaryPrimitive> floatingPoint = tryParseFloat(value); + if (floatingPoint) { + if (typeMask & androidTypeToAttributeTypeMask(floatingPoint->value.dataType)) { + return std::move(floatingPoint); + } + } + } + return {}; +} + +/** + * We successively try to parse the string as a resource type that the Attribute + * allows. + */ +std::unique_ptr<Item> parseItemForAttribute( + const StringPiece16& str, const Attribute* attr, + std::function<void(const ResourceName&)> onCreateReference) { + const uint32_t typeMask = attr->typeMask; + std::unique_ptr<Item> value = parseItemForAttribute(str, typeMask, onCreateReference); + if (value) { + return value; + } + + if (typeMask & android::ResTable_map::TYPE_ENUM) { + // Try parsing this as an enum. + std::unique_ptr<BinaryPrimitive> enumValue = tryParseEnumSymbol(attr, str); + if (enumValue) { + return std::move(enumValue); + } + } + + if (typeMask & android::ResTable_map::TYPE_FLAGS) { + // Try parsing this as a flag. + std::unique_ptr<BinaryPrimitive> flagValue = tryParseFlagSymbol(attr, str); + if (flagValue) { + return std::move(flagValue); + } + } + return {}; +} + +std::string buildResourceFileName(const ResourceFile& resFile, const NameMangler* mangler) { + std::stringstream out; + out << "res/" << resFile.name.type; + if (resFile.config != ConfigDescription{}) { + out << "-" << resFile.config; + } + out << "/"; + + if (mangler && mangler->shouldMangle(resFile.name.package)) { + out << NameMangler::mangleEntry(resFile.name.package, resFile.name.entry); + } else { + out << resFile.name.entry; + } + out << file::getExtension(resFile.source.path); + return out.str(); +} + +} // namespace ResourceUtils +} // namespace aapt diff --git a/tools/aapt2/ResourceUtils.h b/tools/aapt2/ResourceUtils.h new file mode 100644 index 000000000000..64ca97185153 --- /dev/null +++ b/tools/aapt2/ResourceUtils.h @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_RESOURCEUTILS_H +#define AAPT_RESOURCEUTILS_H + +#include "NameMangler.h" +#include "Resource.h" +#include "ResourceValues.h" +#include "util/StringPiece.h" + +#include <functional> +#include <memory> + +namespace aapt { +namespace ResourceUtils { + +/* + * Extracts the package, type, and name from a string of the format: + * + * [package:]type/name + * + * where the package can be empty. Validation must be performed on each + * individual extracted piece to verify that the pieces are valid. + * Returns false if there was no package but a ':' was present. + */ +bool extractResourceName(const StringPiece16& str, StringPiece16* outPackage, + StringPiece16* outType, StringPiece16* outEntry); + +/** + * Returns true if the string was parsed as a resource name ([*][package:]type/name), with + * `outResource` set to the parsed resource name and `outPrivate` set to true if a '*' prefix + * was present. + */ +bool parseResourceName(const StringPiece16& str, ResourceNameRef* outResource, bool* outPrivate); + +/* + * Returns true if the string was parsed as a reference (@[+][package:]type/name), with + * `outReference` set to the parsed reference. + * + * If '+' was present in the reference, `outCreate` is set to true. + * If '*' was present in the reference, `outPrivate` is set to true. + */ +bool tryParseReference(const StringPiece16& str, ResourceNameRef* outReference, + bool* outCreate = nullptr, bool* outPrivate = nullptr); + +/* + * Returns true if the string is in the form of a resource reference (@[+][package:]type/name). + */ +bool isReference(const StringPiece16& str); + +/* + * Returns true if the string was parsed as an attribute reference (?[package:][type/]name), + * with `outReference` set to the parsed reference. + */ +bool tryParseAttributeReference(const StringPiece16& str, ResourceNameRef* outReference); + +/** + * Returns true if the string is in the form of an attribute reference(?[package:][type/]name). + */ +bool isAttributeReference(const StringPiece16& str); + +/** + * Returns true if the value is a boolean, putting the result in `outValue`. + */ +bool tryParseBool(const StringPiece16& str, bool* outValue); + +/* + * Returns a Reference, or None Maybe instance if the string `str` was parsed as a + * valid reference to a style. + * The format for a style parent is slightly more flexible than a normal reference: + * + * @[package:]style/<entry> or + * ?[package:]style/<entry> or + * <package>:[style/]<entry> + */ +Maybe<Reference> parseStyleParentReference(const StringPiece16& str, std::string* outError); + +/* + * Returns a Reference object if the string was parsed as a resource or attribute reference, + * ( @[+][package:]type/name | ?[package:]type/name ) setting outCreate to true if + * the '+' was present in the string. + */ +std::unique_ptr<Reference> tryParseReference(const StringPiece16& str, bool* outCreate = nullptr); + +/* + * Returns a BinaryPrimitve object representing @null or @empty if the string was parsed + * as one. + */ +std::unique_ptr<BinaryPrimitive> tryParseNullOrEmpty(const StringPiece16& str); + +/* + * Returns a BinaryPrimitve object representing a color if the string was parsed + * as one. + */ +std::unique_ptr<BinaryPrimitive> tryParseColor(const StringPiece16& str); + +/* + * Returns a BinaryPrimitve object representing a boolean if the string was parsed + * as one. + */ +std::unique_ptr<BinaryPrimitive> tryParseBool(const StringPiece16& str); + +/* + * Returns a BinaryPrimitve object representing an integer if the string was parsed + * as one. + */ +std::unique_ptr<BinaryPrimitive> tryParseInt(const StringPiece16& str); + +/* + * Returns a BinaryPrimitve object representing a floating point number + * (float, dimension, etc) if the string was parsed as one. + */ +std::unique_ptr<BinaryPrimitive> tryParseFloat(const StringPiece16& str); + +/* + * Returns a BinaryPrimitve object representing an enum symbol if the string was parsed + * as one. + */ +std::unique_ptr<BinaryPrimitive> tryParseEnumSymbol(const Attribute* enumAttr, + const StringPiece16& str); + +/* + * Returns a BinaryPrimitve object representing a flag symbol if the string was parsed + * as one. + */ +std::unique_ptr<BinaryPrimitive> tryParseFlagSymbol(const Attribute* enumAttr, + const StringPiece16& str); +/* + * Try to convert a string to an Item for the given attribute. The attribute will + * restrict what values the string can be converted to. + * The callback function onCreateReference is called when the parsed item is a + * reference to an ID that must be created (@+id/foo). + */ +std::unique_ptr<Item> parseItemForAttribute( + const StringPiece16& value, const Attribute* attr, + std::function<void(const ResourceName&)> onCreateReference = {}); + +std::unique_ptr<Item> parseItemForAttribute( + const StringPiece16& value, uint32_t typeMask, + std::function<void(const ResourceName&)> onCreateReference = {}); + +uint32_t androidTypeToAttributeTypeMask(uint16_t type); + +/** + * Returns a string path suitable for use within an APK. The path will look like: + * + * res/type[-config]/<name>.<ext> + * + * Then name may be mangled if a NameMangler is supplied (can be nullptr) and the package + * requires mangling. + */ +std::string buildResourceFileName(const ResourceFile& resFile, const NameMangler* mangler); + +} // namespace ResourceUtils +} // namespace aapt + +#endif /* AAPT_RESOURCEUTILS_H */ diff --git a/tools/aapt2/ResourceUtils_test.cpp b/tools/aapt2/ResourceUtils_test.cpp new file mode 100644 index 000000000000..c9f93e1dd7c2 --- /dev/null +++ b/tools/aapt2/ResourceUtils_test.cpp @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Resource.h" +#include "ResourceUtils.h" +#include "test/Builders.h" +#include "test/Common.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(ResourceUtilsTest, ParseBool) { + bool val = false; + EXPECT_TRUE(ResourceUtils::tryParseBool(u"true", &val)); + EXPECT_TRUE(val); + + EXPECT_TRUE(ResourceUtils::tryParseBool(u"TRUE", &val)); + EXPECT_TRUE(val); + + EXPECT_TRUE(ResourceUtils::tryParseBool(u"True", &val)); + EXPECT_TRUE(val); + + EXPECT_TRUE(ResourceUtils::tryParseBool(u"false", &val)); + EXPECT_FALSE(val); + + EXPECT_TRUE(ResourceUtils::tryParseBool(u"FALSE", &val)); + EXPECT_FALSE(val); + + EXPECT_TRUE(ResourceUtils::tryParseBool(u"False", &val)); + EXPECT_FALSE(val); +} + +TEST(ResourceUtilsTest, ParseResourceName) { + ResourceNameRef actual; + bool actualPriv = false; + EXPECT_TRUE(ResourceUtils::parseResourceName(u"android:color/foo", &actual, &actualPriv)); + EXPECT_EQ(ResourceNameRef(u"android", ResourceType::kColor, u"foo"), actual); + EXPECT_FALSE(actualPriv); + + EXPECT_TRUE(ResourceUtils::parseResourceName(u"color/foo", &actual, &actualPriv)); + EXPECT_EQ(ResourceNameRef({}, ResourceType::kColor, u"foo"), actual); + EXPECT_FALSE(actualPriv); + + EXPECT_TRUE(ResourceUtils::parseResourceName(u"*android:color/foo", &actual, &actualPriv)); + EXPECT_EQ(ResourceNameRef(u"android", ResourceType::kColor, u"foo"), actual); + EXPECT_TRUE(actualPriv); +} + +TEST(ResourceUtilsTest, ParseReferenceWithNoPackage) { + ResourceNameRef expected({}, ResourceType::kColor, u"foo"); + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceUtils::tryParseReference(u"@color/foo", &actual, &create, &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceUtilsTest, ParseReferenceWithPackage) { + ResourceNameRef expected(u"android", ResourceType::kColor, u"foo"); + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceUtils::tryParseReference(u"@android:color/foo", &actual, &create, + &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceUtilsTest, ParseReferenceWithSurroundingWhitespace) { + ResourceNameRef expected(u"android", ResourceType::kColor, u"foo"); + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceUtils::tryParseReference(u"\t @android:color/foo\n \n\t", &actual, + &create, &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceUtilsTest, ParseAutoCreateIdReference) { + ResourceNameRef expected(u"android", ResourceType::kId, u"foo"); + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceUtils::tryParseReference(u"@+android:id/foo", &actual, &create, + &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_TRUE(create); + EXPECT_FALSE(privateRef); +} + +TEST(ResourceUtilsTest, ParsePrivateReference) { + ResourceNameRef expected(u"android", ResourceType::kId, u"foo"); + ResourceNameRef actual; + bool create = false; + bool privateRef = false; + EXPECT_TRUE(ResourceUtils::tryParseReference(u"@*android:id/foo", &actual, &create, + &privateRef)); + EXPECT_EQ(expected, actual); + EXPECT_FALSE(create); + EXPECT_TRUE(privateRef); +} + +TEST(ResourceUtilsTest, FailToParseAutoCreateNonIdReference) { + bool create = false; + bool privateRef = false; + ResourceNameRef actual; + EXPECT_FALSE(ResourceUtils::tryParseReference(u"@+android:color/foo", &actual, &create, + &privateRef)); +} + +TEST(ResourceUtilsTest, ParseAttributeReferences) { + EXPECT_TRUE(ResourceUtils::isAttributeReference(u"?android")); + EXPECT_TRUE(ResourceUtils::isAttributeReference(u"?android:foo")); + EXPECT_TRUE(ResourceUtils::isAttributeReference(u"?attr/foo")); + EXPECT_TRUE(ResourceUtils::isAttributeReference(u"?android:attr/foo")); +} + +TEST(ResourceUtilsTest, FailParseIncompleteReference) { + EXPECT_FALSE(ResourceUtils::isAttributeReference(u"?style/foo")); + EXPECT_FALSE(ResourceUtils::isAttributeReference(u"?android:style/foo")); + EXPECT_FALSE(ResourceUtils::isAttributeReference(u"?android:")); + EXPECT_FALSE(ResourceUtils::isAttributeReference(u"?android:attr/")); + EXPECT_FALSE(ResourceUtils::isAttributeReference(u"?:attr/")); + EXPECT_FALSE(ResourceUtils::isAttributeReference(u"?:attr/foo")); + EXPECT_FALSE(ResourceUtils::isAttributeReference(u"?:/")); + EXPECT_FALSE(ResourceUtils::isAttributeReference(u"?:/foo")); + EXPECT_FALSE(ResourceUtils::isAttributeReference(u"?attr/")); + EXPECT_FALSE(ResourceUtils::isAttributeReference(u"?/foo")); +} + +TEST(ResourceUtilsTest, ParseStyleParentReference) { + const ResourceName kAndroidStyleFooName(u"android", ResourceType::kStyle, u"foo"); + const ResourceName kStyleFooName({}, ResourceType::kStyle, u"foo"); + + std::string errStr; + Maybe<Reference> ref = ResourceUtils::parseStyleParentReference(u"@android:style/foo", &errStr); + AAPT_ASSERT_TRUE(ref); + EXPECT_EQ(ref.value().name.value(), kAndroidStyleFooName); + + ref = ResourceUtils::parseStyleParentReference(u"@style/foo", &errStr); + AAPT_ASSERT_TRUE(ref); + EXPECT_EQ(ref.value().name.value(), kStyleFooName); + + ref = ResourceUtils::parseStyleParentReference(u"?android:style/foo", &errStr); + AAPT_ASSERT_TRUE(ref); + EXPECT_EQ(ref.value().name.value(), kAndroidStyleFooName); + + ref = ResourceUtils::parseStyleParentReference(u"?style/foo", &errStr); + AAPT_ASSERT_TRUE(ref); + EXPECT_EQ(ref.value().name.value(), kStyleFooName); + + ref = ResourceUtils::parseStyleParentReference(u"android:style/foo", &errStr); + AAPT_ASSERT_TRUE(ref); + EXPECT_EQ(ref.value().name.value(), kAndroidStyleFooName); + + ref = ResourceUtils::parseStyleParentReference(u"android:foo", &errStr); + AAPT_ASSERT_TRUE(ref); + EXPECT_EQ(ref.value().name.value(), kAndroidStyleFooName); + + ref = ResourceUtils::parseStyleParentReference(u"@android:foo", &errStr); + AAPT_ASSERT_TRUE(ref); + EXPECT_EQ(ref.value().name.value(), kAndroidStyleFooName); + + ref = ResourceUtils::parseStyleParentReference(u"foo", &errStr); + AAPT_ASSERT_TRUE(ref); + EXPECT_EQ(ref.value().name.value(), kStyleFooName); + + ref = ResourceUtils::parseStyleParentReference(u"*android:style/foo", &errStr); + AAPT_ASSERT_TRUE(ref); + EXPECT_EQ(ref.value().name.value(), kAndroidStyleFooName); + EXPECT_TRUE(ref.value().privateReference); +} + +TEST(ResourceUtilsTest, ParseEmptyFlag) { + std::unique_ptr<Attribute> attr = test::AttributeBuilder(false) + .setTypeMask(android::ResTable_map::TYPE_FLAGS) + .addItem(u"one", 0x01) + .addItem(u"two", 0x02) + .build(); + + std::unique_ptr<BinaryPrimitive> result = ResourceUtils::tryParseFlagSymbol(attr.get(), u""); + ASSERT_NE(nullptr, result); + EXPECT_EQ(0u, result->value.data); +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceValues.cpp b/tools/aapt2/ResourceValues.cpp index aabb375e6c5e..b93e6d889ad0 100644 --- a/tools/aapt2/ResourceValues.cpp +++ b/tools/aapt2/ResourceValues.cpp @@ -15,42 +15,45 @@ */ #include "Resource.h" -#include "ResourceTypeExtensions.h" +#include "ResourceUtils.h" #include "ResourceValues.h" -#include "Util.h" +#include "ValueVisitor.h" +#include "util/Util.h" +#include "flatten/ResourceTypeExtensions.h" #include <androidfw/ResourceTypes.h> #include <limits> namespace aapt { -bool Value::isItem() const { - return false; +template <typename Derived> +void BaseValue<Derived>::accept(RawValueVisitor* visitor) { + visitor->visit(static_cast<Derived*>(this)); } -bool Value::isWeak() const { - return false; -} - -bool Item::isItem() const { - return true; +template <typename Derived> +void BaseItem<Derived>::accept(RawValueVisitor* visitor) { + visitor->visit(static_cast<Derived*>(this)); } RawString::RawString(const StringPool::Ref& ref) : value(ref) { } RawString* RawString::clone(StringPool* newPool) const { - return new RawString(newPool->makeRef(*value)); + RawString* rs = new RawString(newPool->makeRef(*value)); + rs->mComment = mComment; + rs->mSource = mSource; + return rs; } -bool RawString::flatten(android::Res_value& outValue) const { - outValue.dataType = ExtendedTypes::TYPE_RAW_STRING; - outValue.data = static_cast<uint32_t>(value.getIndex()); +bool RawString::flatten(android::Res_value* outValue) const { + outValue->dataType = ExtendedTypes::TYPE_RAW_STRING; + outValue->data = util::hostToDevice32(static_cast<uint32_t>(value.getIndex())); return true; } -void RawString::print(std::ostream& out) const { - out << "(raw string) " << *value; +void RawString::print(std::ostream* out) const { + *out << "(raw string) " << *value; } Reference::Reference() : referenceType(Reference::Type::kResource) { @@ -63,177 +66,199 @@ Reference::Reference(const ResourceNameRef& n, Type t) : Reference::Reference(const ResourceId& i, Type type) : id(i), referenceType(type) { } -bool Reference::flatten(android::Res_value& outValue) const { - outValue.dataType = (referenceType == Reference::Type::kResource) - ? android::Res_value::TYPE_REFERENCE - : android::Res_value::TYPE_ATTRIBUTE; - outValue.data = id.id; +bool Reference::flatten(android::Res_value* outValue) const { + outValue->dataType = (referenceType == Reference::Type::kResource) ? + android::Res_value::TYPE_REFERENCE : android::Res_value::TYPE_ATTRIBUTE; + outValue->data = util::hostToDevice32(id ? id.value().id : 0); return true; } Reference* Reference::clone(StringPool* /*newPool*/) const { - Reference* ref = new Reference(); - ref->referenceType = referenceType; - ref->name = name; - ref->id = id; - return ref; + return new Reference(*this); } -void Reference::print(std::ostream& out) const { - out << "(reference) "; +void Reference::print(std::ostream* out) const { + *out << "(reference) "; if (referenceType == Reference::Type::kResource) { - out << "@"; + *out << "@"; + if (privateReference) { + *out << "*"; + } } else { - out << "?"; + *out << "?"; } - if (name.isValid()) { - out << name; + if (name) { + *out << name.value(); } - if (id.isValid() || Res_INTERNALID(id.id)) { - out << " " << id; + if (id && !Res_INTERNALID(id.value().id)) { + *out << " " << id.value(); } } -bool Id::isWeak() const { +bool Id::flatten(android::Res_value* out) const { + out->dataType = android::Res_value::TYPE_INT_BOOLEAN; + out->data = util::hostToDevice32(0); return true; } -bool Id::flatten(android::Res_value& out) const { - out.dataType = android::Res_value::TYPE_INT_BOOLEAN; - out.data = 0; - return true; +Id* Id::clone(StringPool* /*newPool*/) const { + return new Id(*this); } -Id* Id::clone(StringPool* /*newPool*/) const { - return new Id(); +void Id::print(std::ostream* out) const { + *out << "(id)"; } -void Id::print(std::ostream& out) const { - out << "(id)"; +String::String(const StringPool::Ref& ref) : value(ref), mTranslateable(true) { } -String::String(const StringPool::Ref& ref) : value(ref) { +void String::setTranslateable(bool val) { + mTranslateable = val; } -bool String::flatten(android::Res_value& outValue) const { - // Verify that our StringPool index is within encodeable limits. +bool String::isTranslateable() const { + return mTranslateable; +} + +bool String::flatten(android::Res_value* outValue) const { + // Verify that our StringPool index is within encode-able limits. if (value.getIndex() > std::numeric_limits<uint32_t>::max()) { return false; } - outValue.dataType = android::Res_value::TYPE_STRING; - outValue.data = static_cast<uint32_t>(value.getIndex()); + outValue->dataType = android::Res_value::TYPE_STRING; + outValue->data = util::hostToDevice32(static_cast<uint32_t>(value.getIndex())); return true; } String* String::clone(StringPool* newPool) const { - return new String(newPool->makeRef(*value)); + String* str = new String(newPool->makeRef(*value)); + str->mComment = mComment; + str->mSource = mSource; + return str; +} + +void String::print(std::ostream* out) const { + *out << "(string) \"" << *value << "\""; } -void String::print(std::ostream& out) const { - out << "(string) \"" << *value << "\""; +StyledString::StyledString(const StringPool::StyleRef& ref) : value(ref), mTranslateable(true) { } -StyledString::StyledString(const StringPool::StyleRef& ref) : value(ref) { +void StyledString::setTranslateable(bool val) { + mTranslateable = val; } -bool StyledString::flatten(android::Res_value& outValue) const { +bool StyledString::isTranslateable() const { + return mTranslateable; +} + +bool StyledString::flatten(android::Res_value* outValue) const { if (value.getIndex() > std::numeric_limits<uint32_t>::max()) { return false; } - outValue.dataType = android::Res_value::TYPE_STRING; - outValue.data = static_cast<uint32_t>(value.getIndex()); + outValue->dataType = android::Res_value::TYPE_STRING; + outValue->data = util::hostToDevice32(static_cast<uint32_t>(value.getIndex())); return true; } StyledString* StyledString::clone(StringPool* newPool) const { - return new StyledString(newPool->makeRef(value)); + StyledString* str = new StyledString(newPool->makeRef(value)); + str->mComment = mComment; + str->mSource = mSource; + return str; } -void StyledString::print(std::ostream& out) const { - out << "(styled string) \"" << *value->str << "\""; +void StyledString::print(std::ostream* out) const { + *out << "(styled string) \"" << *value->str << "\""; } FileReference::FileReference(const StringPool::Ref& _path) : path(_path) { } -bool FileReference::flatten(android::Res_value& outValue) const { +bool FileReference::flatten(android::Res_value* outValue) const { if (path.getIndex() > std::numeric_limits<uint32_t>::max()) { return false; } - outValue.dataType = android::Res_value::TYPE_STRING; - outValue.data = static_cast<uint32_t>(path.getIndex()); + outValue->dataType = android::Res_value::TYPE_STRING; + outValue->data = util::hostToDevice32(static_cast<uint32_t>(path.getIndex())); return true; } FileReference* FileReference::clone(StringPool* newPool) const { - return new FileReference(newPool->makeRef(*path)); + FileReference* fr = new FileReference(newPool->makeRef(*path)); + fr->mComment = mComment; + fr->mSource = mSource; + return fr; } -void FileReference::print(std::ostream& out) const { - out << "(file) " << *path; +void FileReference::print(std::ostream* out) const { + *out << "(file) " << *path; } BinaryPrimitive::BinaryPrimitive(const android::Res_value& val) : value(val) { } -bool BinaryPrimitive::flatten(android::Res_value& outValue) const { - outValue = value; +BinaryPrimitive::BinaryPrimitive(uint8_t dataType, uint32_t data) { + value.dataType = dataType; + value.data = data; +} + +bool BinaryPrimitive::flatten(android::Res_value* outValue) const { + outValue->dataType = value.dataType; + outValue->data = util::hostToDevice32(value.data); return true; } BinaryPrimitive* BinaryPrimitive::clone(StringPool* /*newPool*/) const { - return new BinaryPrimitive(value); + return new BinaryPrimitive(*this); } -void BinaryPrimitive::print(std::ostream& out) const { +void BinaryPrimitive::print(std::ostream* out) const { switch (value.dataType) { case android::Res_value::TYPE_NULL: - out << "(null)"; + *out << "(null)"; break; case android::Res_value::TYPE_INT_DEC: - out << "(integer) " << value.data; + *out << "(integer) " << static_cast<int32_t>(value.data); break; case android::Res_value::TYPE_INT_HEX: - out << "(integer) " << std::hex << value.data << std::dec; + *out << "(integer) " << std::hex << value.data << std::dec; break; case android::Res_value::TYPE_INT_BOOLEAN: - out << "(boolean) " << (value.data != 0 ? "true" : "false"); + *out << "(boolean) " << (value.data != 0 ? "true" : "false"); break; case android::Res_value::TYPE_INT_COLOR_ARGB8: case android::Res_value::TYPE_INT_COLOR_RGB8: case android::Res_value::TYPE_INT_COLOR_ARGB4: case android::Res_value::TYPE_INT_COLOR_RGB4: - out << "(color) #" << std::hex << value.data << std::dec; + *out << "(color) #" << std::hex << value.data << std::dec; break; default: - out << "(unknown 0x" << std::hex << (int) value.dataType << ") 0x" - << std::hex << value.data << std::dec; + *out << "(unknown 0x" << std::hex << (int) value.dataType << ") 0x" + << std::hex << value.data << std::dec; break; } } -Attribute::Attribute(bool w, uint32_t t) : weak(w), typeMask(t) { -} - -bool Attribute::isWeak() const { - return weak; +Attribute::Attribute(bool w, uint32_t t) : + typeMask(t), + minInt(std::numeric_limits<int32_t>::min()), + maxInt(std::numeric_limits<int32_t>::max()) { + mWeak = w; } Attribute* Attribute::clone(StringPool* /*newPool*/) const { - Attribute* attr = new Attribute(weak); - attr->typeMask = typeMask; - std::copy(symbols.begin(), symbols.end(), std::back_inserter(attr->symbols)); - return attr; + return new Attribute(*this); } -void Attribute::printMask(std::ostream& out) const { +void Attribute::printMask(std::ostream* out) const { if (typeMask == android::ResTable_map::TYPE_ANY) { - out << "any"; + *out << "any"; return; } @@ -242,110 +267,189 @@ void Attribute::printMask(std::ostream& out) const { if (!set) { set = true; } else { - out << "|"; + *out << "|"; } - out << "reference"; + *out << "reference"; } if ((typeMask & android::ResTable_map::TYPE_STRING) != 0) { if (!set) { set = true; } else { - out << "|"; + *out << "|"; } - out << "string"; + *out << "string"; } if ((typeMask & android::ResTable_map::TYPE_INTEGER) != 0) { if (!set) { set = true; } else { - out << "|"; + *out << "|"; } - out << "integer"; + *out << "integer"; } if ((typeMask & android::ResTable_map::TYPE_BOOLEAN) != 0) { if (!set) { set = true; } else { - out << "|"; + *out << "|"; } - out << "boolean"; + *out << "boolean"; } if ((typeMask & android::ResTable_map::TYPE_COLOR) != 0) { if (!set) { set = true; } else { - out << "|"; + *out << "|"; } - out << "color"; + *out << "color"; } if ((typeMask & android::ResTable_map::TYPE_FLOAT) != 0) { if (!set) { set = true; } else { - out << "|"; + *out << "|"; } - out << "float"; + *out << "float"; } if ((typeMask & android::ResTable_map::TYPE_DIMENSION) != 0) { if (!set) { set = true; } else { - out << "|"; + *out << "|"; } - out << "dimension"; + *out << "dimension"; } if ((typeMask & android::ResTable_map::TYPE_FRACTION) != 0) { if (!set) { set = true; } else { - out << "|"; + *out << "|"; } - out << "fraction"; + *out << "fraction"; } if ((typeMask & android::ResTable_map::TYPE_ENUM) != 0) { if (!set) { set = true; } else { - out << "|"; + *out << "|"; } - out << "enum"; + *out << "enum"; } if ((typeMask & android::ResTable_map::TYPE_FLAGS) != 0) { if (!set) { set = true; } else { - out << "|"; + *out << "|"; } - out << "flags"; + *out << "flags"; } } -void Attribute::print(std::ostream& out) const { - out << "(attr) "; +void Attribute::print(std::ostream* out) const { + *out << "(attr) "; printMask(out); - out << " [" - << util::joiner(symbols.begin(), symbols.end(), ", ") - << "]"; + if (!symbols.empty()) { + *out << " [" + << util::joiner(symbols.begin(), symbols.end(), ", ") + << "]"; + } - if (weak) { - out << " [weak]"; + if (isWeak()) { + *out << " [weak]"; } } +static void buildAttributeMismatchMessage(DiagMessage* msg, const Attribute* attr, + const Item* value) { + *msg << "expected"; + if (attr->typeMask & android::ResTable_map::TYPE_BOOLEAN) { + *msg << " boolean"; + } + + if (attr->typeMask & android::ResTable_map::TYPE_COLOR) { + *msg << " color"; + } + + if (attr->typeMask & android::ResTable_map::TYPE_DIMENSION) { + *msg << " dimension"; + } + + if (attr->typeMask & android::ResTable_map::TYPE_ENUM) { + *msg << " enum"; + } + + if (attr->typeMask & android::ResTable_map::TYPE_FLAGS) { + *msg << " flags"; + } + + if (attr->typeMask & android::ResTable_map::TYPE_FLOAT) { + *msg << " float"; + } + + if (attr->typeMask & android::ResTable_map::TYPE_FRACTION) { + *msg << " fraction"; + } + + if (attr->typeMask & android::ResTable_map::TYPE_INTEGER) { + *msg << " integer"; + } + + if (attr->typeMask & android::ResTable_map::TYPE_REFERENCE) { + *msg << " reference"; + } + + if (attr->typeMask & android::ResTable_map::TYPE_STRING) { + *msg << " string"; + } + + *msg << " but got " << *value; +} + +bool Attribute::matches(const Item* item, DiagMessage* outMsg) const { + android::Res_value val = {}; + item->flatten(&val); + + // Always allow references. + const uint32_t mask = typeMask | android::ResTable_map::TYPE_REFERENCE; + if (!(mask & ResourceUtils::androidTypeToAttributeTypeMask(val.dataType))) { + if (outMsg) { + buildAttributeMismatchMessage(outMsg, this, item); + } + return false; + + } else if (ResourceUtils::androidTypeToAttributeTypeMask(val.dataType) & + android::ResTable_map::TYPE_INTEGER) { + if (static_cast<int32_t>(util::deviceToHost32(val.data)) < minInt) { + if (outMsg) { + *outMsg << *item << " is less than minimum integer " << minInt; + } + return false; + } else if (static_cast<int32_t>(util::deviceToHost32(val.data)) > maxInt) { + if (outMsg) { + *outMsg << *item << " is greater than maximum integer " << maxInt; + } + return false; + } + } + return true; +} + Style* Style::clone(StringPool* newPool) const { Style* style = new Style(); style->parent = parent; style->parentInferred = parentInferred; + style->mComment = mComment; + style->mSource = mSource; for (auto& entry : entries) { style->entries.push_back(Entry{ entry.key, @@ -355,38 +459,50 @@ Style* Style::clone(StringPool* newPool) const { return style; } -void Style::print(std::ostream& out) const { - out << "(style) "; - if (!parent.name.entry.empty()) { - out << parent.name; +void Style::print(std::ostream* out) const { + *out << "(style) "; + if (parent && parent.value().name) { + if (parent.value().privateReference) { + *out << "*"; + } + *out << parent.value().name.value(); } - out << " [" + *out << " [" << util::joiner(entries.begin(), entries.end(), ", ") << "]"; } static ::std::ostream& operator<<(::std::ostream& out, const Style::Entry& value) { - out << value.key.name << " = "; - value.value->print(out); + if (value.key.name) { + out << value.key.name.value(); + } else { + out << "???"; + } + out << " = "; + value.value->print(&out); return out; } Array* Array::clone(StringPool* newPool) const { Array* array = new Array(); + array->mComment = mComment; + array->mSource = mSource; for (auto& item : items) { array->items.emplace_back(std::unique_ptr<Item>(item->clone(newPool))); } return array; } -void Array::print(std::ostream& out) const { - out << "(array) [" +void Array::print(std::ostream* out) const { + *out << "(array) [" << util::joiner(items.begin(), items.end(), ", ") << "]"; } Plural* Plural::clone(StringPool* newPool) const { Plural* p = new Plural(); + p->mComment = mComment; + p->mSource = mSource; const size_t count = values.size(); for (size_t i = 0; i < count; i++) { if (values[i]) { @@ -396,8 +512,8 @@ Plural* Plural::clone(StringPool* newPool) const { return p; } -void Plural::print(std::ostream& out) const { - out << "(plural)"; +void Plural::print(std::ostream* out) const { + *out << "(plural)"; } static ::std::ostream& operator<<(::std::ostream& out, const std::unique_ptr<Item>& item) { @@ -405,13 +521,11 @@ static ::std::ostream& operator<<(::std::ostream& out, const std::unique_ptr<Ite } Styleable* Styleable::clone(StringPool* /*newPool*/) const { - Styleable* styleable = new Styleable(); - std::copy(entries.begin(), entries.end(), std::back_inserter(styleable->entries)); - return styleable; + return new Styleable(*this); } -void Styleable::print(std::ostream& out) const { - out << "(styleable) " << " [" +void Styleable::print(std::ostream* out) const { + *out << "(styleable) " << " [" << util::joiner(entries.begin(), entries.end(), ", ") << "]"; } diff --git a/tools/aapt2/ResourceValues.h b/tools/aapt2/ResourceValues.h index ef6594e6f231..8e317dbcd1b1 100644 --- a/tools/aapt2/ResourceValues.h +++ b/tools/aapt2/ResourceValues.h @@ -17,8 +17,10 @@ #ifndef AAPT_RESOURCE_VALUES_H #define AAPT_RESOURCE_VALUES_H +#include "Diagnostics.h" #include "Resource.h" #include "StringPool.h" +#include "util/Maybe.h" #include <array> #include <androidfw/ResourceTypes.h> @@ -27,9 +29,7 @@ namespace aapt { -struct ValueVisitor; -struct ConstValueVisitor; -struct ValueVisitorArgs; +struct RawValueVisitor; /** * A resource value. This is an all-encompassing representation @@ -39,26 +39,54 @@ struct ValueVisitorArgs; * but it is the simplest strategy. */ struct Value { + virtual ~Value() = default; + /** - * Whether or not this is an Item. + * Whether this value is weak and can be overridden without + * warning or error. Default is false. */ - virtual bool isItem() const; + bool isWeak() const { + return mWeak; + } + + void setWeak(bool val) { + mWeak = val; + } /** - * Whether this value is weak and can be overriden without - * warning or error. Default for base class is false. + * Returns the source where this value was defined. */ - virtual bool isWeak() const; + const Source& getSource() const { + return mSource; + } + + void setSource(const Source& source) { + mSource = source; + } + + void setSource(Source&& source) { + mSource = std::move(source); + } /** - * Calls the appropriate overload of ValueVisitor. + * Returns the comment that was associated with this resource. */ - virtual void accept(ValueVisitor& visitor, ValueVisitorArgs&& args) = 0; + StringPiece16 getComment() const { + return mComment; + } + + void setComment(const StringPiece16& str) { + mComment = str.toString(); + } + + void setComment(std::u16string&& str) { + mComment = std::move(str); + } /** - * Const version of accept(). + * Calls the appropriate overload of ValueVisitor. */ - virtual void accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const = 0; + virtual void accept(RawValueVisitor* visitor) = 0; /** * Clone the value. @@ -68,7 +96,12 @@ struct Value { /** * Human readable printout of this value. */ - virtual void print(std::ostream& out) const = 0; + virtual void print(std::ostream* out) const = 0; + +protected: + Source mSource; + std::u16string mComment; + bool mWeak = false; }; /** @@ -76,8 +109,7 @@ struct Value { */ template <typename Derived> struct BaseValue : public Value { - virtual void accept(ValueVisitor& visitor, ValueVisitorArgs&& args) override; - virtual void accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const override; + void accept(RawValueVisitor* visitor) override; }; /** @@ -85,20 +117,15 @@ struct BaseValue : public Value { */ struct Item : public Value { /** - * An Item is, of course, an Item. - */ - virtual bool isItem() const override; - - /** * Clone the Item. */ virtual Item* clone(StringPool* newPool) const override = 0; /** * Fills in an android::Res_value structure with this Item's binary representation. - * Returns false if an error ocurred. + * Returns false if an error occurred. */ - virtual bool flatten(android::Res_value& outValue) const = 0; + virtual bool flatten(android::Res_value* outValue) const = 0; }; /** @@ -106,8 +133,7 @@ struct Item : public Value { */ template <typename Derived> struct BaseItem : public Item { - virtual void accept(ValueVisitor& visitor, ValueVisitorArgs&& args) override; - virtual void accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const override; + void accept(RawValueVisitor* visitor) override; }; /** @@ -122,8 +148,8 @@ struct Reference : public BaseItem<Reference> { kAttribute, }; - ResourceName name; - ResourceId id; + Maybe<ResourceName> name; + Maybe<ResourceId> id; Reference::Type referenceType; bool privateReference = false; @@ -131,19 +157,19 @@ struct Reference : public BaseItem<Reference> { Reference(const ResourceNameRef& n, Type type = Type::kResource); Reference(const ResourceId& i, Type type = Type::kResource); - bool flatten(android::Res_value& outValue) const override; + bool flatten(android::Res_value* outValue) const override; Reference* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; }; /** * An ID resource. Has no real value, just a place holder. */ struct Id : public BaseItem<Id> { - bool isWeak() const override; - bool flatten(android::Res_value& out) const override; + Id() { mWeak = true; } + bool flatten(android::Res_value* out) const override; Id* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; }; /** @@ -156,9 +182,9 @@ struct RawString : public BaseItem<RawString> { RawString(const StringPool::Ref& ref); - bool flatten(android::Res_value& outValue) const override; + bool flatten(android::Res_value* outValue) const override; RawString* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; }; struct String : public BaseItem<String> { @@ -166,9 +192,17 @@ struct String : public BaseItem<String> { String(const StringPool::Ref& ref); - bool flatten(android::Res_value& outValue) const override; + // Whether the string is marked as translateable. This does not persist when flattened. + // It is only used during compilation phase. + void setTranslateable(bool val); + bool isTranslateable() const; + + bool flatten(android::Res_value* outValue) const override; String* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; + +private: + bool mTranslateable; }; struct StyledString : public BaseItem<StyledString> { @@ -176,9 +210,17 @@ struct StyledString : public BaseItem<StyledString> { StyledString(const StringPool::StyleRef& ref); - bool flatten(android::Res_value& outValue) const override; + // Whether the string is marked as translateable. This does not persist when flattened. + // It is only used during compilation phase. + void setTranslateable(bool val); + bool isTranslateable() const; + + bool flatten(android::Res_value* outValue) const override; StyledString* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; + +private: + bool mTranslateable; }; struct FileReference : public BaseItem<FileReference> { @@ -187,9 +229,9 @@ struct FileReference : public BaseItem<FileReference> { FileReference() = default; FileReference(const StringPool::Ref& path); - bool flatten(android::Res_value& outValue) const override; + bool flatten(android::Res_value* outValue) const override; FileReference* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; }; /** @@ -200,10 +242,11 @@ struct BinaryPrimitive : public BaseItem<BinaryPrimitive> { BinaryPrimitive() = default; BinaryPrimitive(const android::Res_value& val); + BinaryPrimitive(uint8_t dataType, uint32_t data); - bool flatten(android::Res_value& outValue) const override; + bool flatten(android::Res_value* outValue) const override; BinaryPrimitive* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; }; struct Attribute : public BaseValue<Attribute> { @@ -212,18 +255,17 @@ struct Attribute : public BaseValue<Attribute> { uint32_t value; }; - bool weak; uint32_t typeMask; - uint32_t minInt; - uint32_t maxInt; + int32_t minInt; + int32_t maxInt; std::vector<Symbol> symbols; Attribute(bool w, uint32_t t = 0u); - bool isWeak() const override; - virtual Attribute* clone(StringPool* newPool) const override; - void printMask(std::ostream& out) const; - virtual void print(std::ostream& out) const override; + Attribute* clone(StringPool* newPool) const override; + void printMask(std::ostream* out) const; + void print(std::ostream* out) const override; + bool matches(const Item* item, DiagMessage* outMsg) const; }; struct Style : public BaseValue<Style> { @@ -232,7 +274,7 @@ struct Style : public BaseValue<Style> { std::unique_ptr<Item> value; }; - Reference parent; + Maybe<Reference> parent; /** * If set to true, the parent was auto inferred from the @@ -243,14 +285,14 @@ struct Style : public BaseValue<Style> { std::vector<Entry> entries; Style* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; }; struct Array : public BaseValue<Array> { std::vector<std::unique_ptr<Item>> items; Array* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; }; struct Plural : public BaseValue<Plural> { @@ -267,180 +309,31 @@ struct Plural : public BaseValue<Plural> { std::array<std::unique_ptr<Item>, Count> values; Plural* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; }; struct Styleable : public BaseValue<Styleable> { std::vector<Reference> entries; Styleable* clone(StringPool* newPool) const override; - void print(std::ostream& out) const override; + void print(std::ostream* out) const override; }; /** * Stream operator for printing Value objects. */ inline ::std::ostream& operator<<(::std::ostream& out, const Value& value) { - value.print(out); + value.print(&out); return out; } inline ::std::ostream& operator<<(::std::ostream& out, const Attribute::Symbol& s) { - return out << s.symbol.name.entry << "=" << s.value; -} - -/** - * The argument object that gets passed through the value - * back to the ValueVisitor. Subclasses of ValueVisitor should - * subclass ValueVisitorArgs to contain the data they need - * to operate. - */ -struct ValueVisitorArgs {}; - -/** - * Visits a value and runs the appropriate method based on its type. - */ -struct ValueVisitor { - virtual void visit(Reference& reference, ValueVisitorArgs& args) { - visitItem(reference, args); - } - - virtual void visit(RawString& string, ValueVisitorArgs& args) { - visitItem(string, args); - } - - virtual void visit(String& string, ValueVisitorArgs& args) { - visitItem(string, args); - } - - virtual void visit(StyledString& string, ValueVisitorArgs& args) { - visitItem(string, args); - } - - virtual void visit(FileReference& file, ValueVisitorArgs& args) { - visitItem(file, args); - } - - virtual void visit(Id& id, ValueVisitorArgs& args) { - visitItem(id, args); - } - - virtual void visit(BinaryPrimitive& primitive, ValueVisitorArgs& args) { - visitItem(primitive, args); - } - - virtual void visit(Attribute& attr, ValueVisitorArgs& args) {} - virtual void visit(Style& style, ValueVisitorArgs& args) {} - virtual void visit(Array& array, ValueVisitorArgs& args) {} - virtual void visit(Plural& array, ValueVisitorArgs& args) {} - virtual void visit(Styleable& styleable, ValueVisitorArgs& args) {} - - virtual void visitItem(Item& item, ValueVisitorArgs& args) {} -}; - -/** - * Const version of ValueVisitor. - */ -struct ConstValueVisitor { - virtual void visit(const Reference& reference, ValueVisitorArgs& args) { - visitItem(reference, args); - } - - virtual void visit(const RawString& string, ValueVisitorArgs& args) { - visitItem(string, args); - } - - virtual void visit(const String& string, ValueVisitorArgs& args) { - visitItem(string, args); - } - - virtual void visit(const StyledString& string, ValueVisitorArgs& args) { - visitItem(string, args); - } - - virtual void visit(const FileReference& file, ValueVisitorArgs& args) { - visitItem(file, args); - } - - virtual void visit(const Id& id, ValueVisitorArgs& args) { - visitItem(id, args); - } - - virtual void visit(const BinaryPrimitive& primitive, ValueVisitorArgs& args) { - visitItem(primitive, args); - } - - virtual void visit(const Attribute& attr, ValueVisitorArgs& args) {} - virtual void visit(const Style& style, ValueVisitorArgs& args) {} - virtual void visit(const Array& array, ValueVisitorArgs& args) {} - virtual void visit(const Plural& array, ValueVisitorArgs& args) {} - virtual void visit(const Styleable& styleable, ValueVisitorArgs& args) {} - - virtual void visitItem(const Item& item, ValueVisitorArgs& args) {} -}; - -/** - * Convenience Visitor that forwards a specific type to a function. - * Args are not used as the function can bind variables. Do not use - * directly, use the wrapper visitFunc() method. - */ -template <typename T, typename TFunc> -struct ValueVisitorFunc : ValueVisitor { - TFunc func; - - ValueVisitorFunc(TFunc f) : func(f) { - } - - void visit(T& value, ValueVisitorArgs&) override { - func(value); + if (s.symbol.name) { + out << s.symbol.name.value().entry; + } else { + out << "???"; } -}; - -/** - * Const version of ValueVisitorFunc. - */ -template <typename T, typename TFunc> -struct ConstValueVisitorFunc : ConstValueVisitor { - TFunc func; - - ConstValueVisitorFunc(TFunc f) : func(f) { - } - - void visit(const T& value, ValueVisitorArgs&) override { - func(value); - } -}; - -template <typename T, typename TFunc> -void visitFunc(Value& value, TFunc f) { - ValueVisitorFunc<T, TFunc> visitor(f); - value.accept(visitor, ValueVisitorArgs{}); -} - -template <typename T, typename TFunc> -void visitFunc(const Value& value, TFunc f) { - ConstValueVisitorFunc<T, TFunc> visitor(f); - value.accept(visitor, ValueVisitorArgs{}); -} - -template <typename Derived> -void BaseValue<Derived>::accept(ValueVisitor& visitor, ValueVisitorArgs&& args) { - visitor.visit(static_cast<Derived&>(*this), args); -} - -template <typename Derived> -void BaseValue<Derived>::accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const { - visitor.visit(static_cast<const Derived&>(*this), args); -} - -template <typename Derived> -void BaseItem<Derived>::accept(ValueVisitor& visitor, ValueVisitorArgs&& args) { - visitor.visit(static_cast<Derived&>(*this), args); -} - -template <typename Derived> -void BaseItem<Derived>::accept(ConstValueVisitor& visitor, ValueVisitorArgs&& args) const { - visitor.visit(static_cast<const Derived&>(*this), args); + return out << "=" << s.value; } } // namespace aapt diff --git a/tools/aapt2/Resource_test.cpp b/tools/aapt2/Resource_test.cpp index d957999f492b..48dc521d843c 100644 --- a/tools/aapt2/Resource_test.cpp +++ b/tools/aapt2/Resource_test.cpp @@ -69,10 +69,6 @@ TEST(ResourceTypeTest, ParseResourceTypes) { ASSERT_NE(type, nullptr); EXPECT_EQ(*type, ResourceType::kInteger); - type = parseResourceType(u"integer-array"); - ASSERT_NE(type, nullptr); - EXPECT_EQ(*type, ResourceType::kIntegerArray); - type = parseResourceType(u"interpolator"); ASSERT_NE(type, nullptr); EXPECT_EQ(*type, ResourceType::kInterpolator); diff --git a/tools/aapt2/ScopedXmlPullParser.cpp b/tools/aapt2/ScopedXmlPullParser.cpp deleted file mode 100644 index 48da93edaa02..000000000000 --- a/tools/aapt2/ScopedXmlPullParser.cpp +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "ScopedXmlPullParser.h" - -#include <string> - -namespace aapt { - -ScopedXmlPullParser::ScopedXmlPullParser(XmlPullParser* parser) : - mParser(parser), mDepth(parser->getDepth()), mDone(false) { -} - -ScopedXmlPullParser::~ScopedXmlPullParser() { - while (isGoodEvent(next())); -} - -XmlPullParser::Event ScopedXmlPullParser::next() { - if (mDone) { - return Event::kEndDocument; - } - - const Event event = mParser->next(); - if (mParser->getDepth() <= mDepth) { - mDone = true; - } - return event; -} - -XmlPullParser::Event ScopedXmlPullParser::getEvent() const { - return mParser->getEvent(); -} - -const std::string& ScopedXmlPullParser::getLastError() const { - return mParser->getLastError(); -} - -const std::u16string& ScopedXmlPullParser::getComment() const { - return mParser->getComment(); -} - -size_t ScopedXmlPullParser::getLineNumber() const { - return mParser->getLineNumber(); -} - -size_t ScopedXmlPullParser::getDepth() const { - const size_t depth = mParser->getDepth(); - if (depth < mDepth) { - return 0; - } - return depth - mDepth; -} - -const std::u16string& ScopedXmlPullParser::getText() const { - return mParser->getText(); -} - -const std::u16string& ScopedXmlPullParser::getNamespacePrefix() const { - return mParser->getNamespacePrefix(); -} - -const std::u16string& ScopedXmlPullParser::getNamespaceUri() const { - return mParser->getNamespaceUri(); -} - -bool ScopedXmlPullParser::applyPackageAlias(std::u16string* package, - const std::u16string& defaultPackage) const { - return mParser->applyPackageAlias(package, defaultPackage); -} - -const std::u16string& ScopedXmlPullParser::getElementNamespace() const { - return mParser->getElementNamespace(); -} - -const std::u16string& ScopedXmlPullParser::getElementName() const { - return mParser->getElementName(); -} - -size_t ScopedXmlPullParser::getAttributeCount() const { - return mParser->getAttributeCount(); -} - -XmlPullParser::const_iterator ScopedXmlPullParser::beginAttributes() const { - return mParser->beginAttributes(); -} - -XmlPullParser::const_iterator ScopedXmlPullParser::endAttributes() const { - return mParser->endAttributes(); -} - -} // namespace aapt diff --git a/tools/aapt2/ScopedXmlPullParser.h b/tools/aapt2/ScopedXmlPullParser.h deleted file mode 100644 index a040f6097fc3..000000000000 --- a/tools/aapt2/ScopedXmlPullParser.h +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_SCOPED_XML_PULL_PARSER_H -#define AAPT_SCOPED_XML_PULL_PARSER_H - -#include "XmlPullParser.h" - -#include <string> - -namespace aapt { - -/** - * An XmlPullParser that will not read past the depth - * of the underlying parser. When this parser is destroyed, - * it moves the underlying parser to the same depth it - * started with. - * - * You can write code like this: - * - * while (XmlPullParser::isGoodEvent(parser.next())) { - * if (parser.getEvent() != XmlPullParser::Event::StartElement) { - * continue; - * } - * - * ScopedXmlPullParser scoped(parser); - * if (parser.getElementName() == u"id") { - * // do work. - * } else { - * // do nothing, as all the sub elements will be skipped - * // when scoped goes out of scope. - * } - * } - */ -class ScopedXmlPullParser : public XmlPullParser { -public: - ScopedXmlPullParser(XmlPullParser* parser); - ScopedXmlPullParser(const ScopedXmlPullParser&) = delete; - ScopedXmlPullParser& operator=(const ScopedXmlPullParser&) = delete; - ~ScopedXmlPullParser(); - - Event getEvent() const override; - const std::string& getLastError() const override; - Event next() override; - - const std::u16string& getComment() const override; - size_t getLineNumber() const override; - size_t getDepth() const override; - - const std::u16string& getText() const override; - - const std::u16string& getNamespacePrefix() const override; - const std::u16string& getNamespaceUri() const override; - bool applyPackageAlias(std::u16string* package, const std::u16string& defaultPackage) - const override; - - const std::u16string& getElementNamespace() const override; - const std::u16string& getElementName() const override; - - const_iterator beginAttributes() const override; - const_iterator endAttributes() const override; - size_t getAttributeCount() const override; - -private: - XmlPullParser* mParser; - size_t mDepth; - bool mDone; -}; - -} // namespace aapt - -#endif // AAPT_SCOPED_XML_PULL_PARSER_H diff --git a/tools/aapt2/ScopedXmlPullParser_test.cpp b/tools/aapt2/ScopedXmlPullParser_test.cpp deleted file mode 100644 index 342f305bb11d..000000000000 --- a/tools/aapt2/ScopedXmlPullParser_test.cpp +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "ScopedXmlPullParser.h" -#include "SourceXmlPullParser.h" - -#include <gtest/gtest.h> -#include <sstream> -#include <string> - -namespace aapt { - -TEST(ScopedXmlPullParserTest, StopIteratingAtNoNZeroDepth) { - std::stringstream input; - input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl - << "<resources><string></string></resources>" << std::endl; - - SourceXmlPullParser sourceParser(input); - EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); - EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); - - EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); - EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); - - { - ScopedXmlPullParser scopedParser(&sourceParser); - EXPECT_EQ(XmlPullParser::Event::kEndElement, scopedParser.next()); - EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); - - EXPECT_EQ(XmlPullParser::Event::kEndDocument, scopedParser.next()); - } - - EXPECT_EQ(XmlPullParser::Event::kEndElement, sourceParser.next()); - EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); - - EXPECT_EQ(XmlPullParser::Event::kEndDocument, sourceParser.next()); -} - -TEST(ScopedXmlPullParserTest, FinishCurrentElementOnDestruction) { - std::stringstream input; - input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl - << "<resources><string></string></resources>" << std::endl; - - SourceXmlPullParser sourceParser(input); - EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); - EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); - - EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); - EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); - - { - ScopedXmlPullParser scopedParser(&sourceParser); - EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); - } - - EXPECT_EQ(XmlPullParser::Event::kEndElement, sourceParser.next()); - EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); - - EXPECT_EQ(XmlPullParser::Event::kEndDocument, sourceParser.next()); -} - -TEST(ScopedXmlPullParserTest, NestedParsersOperateCorrectly) { - std::stringstream input; - input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl - << "<resources><string><foo></foo></string></resources>" << std::endl; - - SourceXmlPullParser sourceParser(input); - EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); - EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); - - EXPECT_EQ(XmlPullParser::Event::kStartElement, sourceParser.next()); - EXPECT_EQ(std::u16string(u"string"), sourceParser.getElementName()); - - { - ScopedXmlPullParser scopedParser(&sourceParser); - EXPECT_EQ(std::u16string(u"string"), scopedParser.getElementName()); - while (XmlPullParser::isGoodEvent(scopedParser.next())) { - if (scopedParser.getEvent() != XmlPullParser::Event::kStartElement) { - continue; - } - - ScopedXmlPullParser subScopedParser(&scopedParser); - EXPECT_EQ(std::u16string(u"foo"), subScopedParser.getElementName()); - } - } - - EXPECT_EQ(XmlPullParser::Event::kEndElement, sourceParser.next()); - EXPECT_EQ(std::u16string(u"resources"), sourceParser.getElementName()); - - EXPECT_EQ(XmlPullParser::Event::kEndDocument, sourceParser.next()); -} - -} // namespace aapt diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp index 9bdae490412f..c2a22bf2a373 100644 --- a/tools/aapt2/SdkConstants.cpp +++ b/tools/aapt2/SdkConstants.cpp @@ -34,8 +34,9 @@ static const std::vector<std::pair<uint16_t, size_t>> sAttrIdMap = { { 0x02bd, SDK_FROYO }, { 0x02cb, SDK_GINGERBREAD }, { 0x0361, SDK_HONEYCOMB }, - { 0x0366, SDK_HONEYCOMB_MR1 }, - { 0x03a6, SDK_HONEYCOMB_MR2 }, + { 0x0363, SDK_HONEYCOMB_MR1 }, + { 0x0366, SDK_HONEYCOMB_MR2 }, + { 0x03a6, SDK_ICE_CREAM_SANDWICH }, { 0x03ae, SDK_JELLY_BEAN }, { 0x03cc, SDK_JELLY_BEAN_MR1 }, { 0x03da, SDK_JELLY_BEAN_MR2 }, diff --git a/tools/aapt2/SdkConstants.h b/tools/aapt2/SdkConstants.h index 803da03743c5..282ed9a56f5c 100644 --- a/tools/aapt2/SdkConstants.h +++ b/tools/aapt2/SdkConstants.h @@ -42,6 +42,7 @@ enum { SDK_KITKAT_WATCH = 20, SDK_LOLLIPOP = 21, SDK_LOLLIPOP_MR1 = 22, + SDK_MARSHMALLOW = 23, }; size_t findAttributeSdkLevel(ResourceId id); diff --git a/tools/aapt2/Source.h b/tools/aapt2/Source.h index 3606488591ba..319528e0ea1b 100644 --- a/tools/aapt2/Source.h +++ b/tools/aapt2/Source.h @@ -17,72 +17,62 @@ #ifndef AAPT_SOURCE_H #define AAPT_SOURCE_H +#include "util/Maybe.h" +#include "util/StringPiece.h" + #include <ostream> #include <string> -#include <tuple> namespace aapt { -struct SourceLineColumn; -struct SourceLine; - /** * Represents a file on disk. Used for logging and * showing errors. */ struct Source { std::string path; + Maybe<size_t> line; - inline SourceLine line(size_t line) const; -}; + Source() = default; -/** - * Represents a file on disk and a line number in that file. - * Used for logging and showing errors. - */ -struct SourceLine { - std::string path; - size_t line; + inline Source(const StringPiece& path) : path(path.toString()) { + } - inline SourceLineColumn column(size_t column) const; -}; + inline Source(const StringPiece& path, size_t line) : path(path.toString()), line(line) { + } -/** - * Represents a file on disk and a line:column number in that file. - * Used for logging and showing errors. - */ -struct SourceLineColumn { - std::string path; - size_t line; - size_t column; + inline Source withLine(size_t line) const { + return Source(path, line); + } }; // // Implementations // -SourceLine Source::line(size_t line) const { - return SourceLine{ path, line }; -} - -SourceLineColumn SourceLine::column(size_t column) const { - return SourceLineColumn{ path, line, column }; -} - inline ::std::ostream& operator<<(::std::ostream& out, const Source& source) { - return out << source.path; -} - -inline ::std::ostream& operator<<(::std::ostream& out, const SourceLine& source) { - return out << source.path << ":" << source.line; + out << source.path; + if (source.line) { + out << ":" << source.line.value(); + } + return out; } -inline ::std::ostream& operator<<(::std::ostream& out, const SourceLineColumn& source) { - return out << source.path << ":" << source.line << ":" << source.column; +inline bool operator==(const Source& lhs, const Source& rhs) { + return lhs.path == rhs.path && lhs.line == rhs.line; } -inline bool operator<(const SourceLine& lhs, const SourceLine& rhs) { - return std::tie(lhs.path, lhs.line) < std::tie(rhs.path, rhs.line); +inline bool operator<(const Source& lhs, const Source& rhs) { + int cmp = lhs.path.compare(rhs.path); + if (cmp < 0) return true; + if (cmp > 0) return false; + if (lhs.line) { + if (rhs.line) { + return lhs.line.value() < rhs.line.value(); + } + return false; + } + return bool(rhs.line); } } // namespace aapt diff --git a/tools/aapt2/SourceXmlPullParser.h b/tools/aapt2/SourceXmlPullParser.h deleted file mode 100644 index d8ed45952b31..000000000000 --- a/tools/aapt2/SourceXmlPullParser.h +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_SOURCE_XML_PULL_PARSER_H -#define AAPT_SOURCE_XML_PULL_PARSER_H - -#include "XmlPullParser.h" - -#include <istream> -#include <expat.h> -#include <queue> -#include <stack> -#include <string> -#include <vector> - -namespace aapt { - -class SourceXmlPullParser : public XmlPullParser { -public: - SourceXmlPullParser(std::istream& in); - SourceXmlPullParser(const SourceXmlPullParser& rhs) = delete; - ~SourceXmlPullParser(); - - Event getEvent() const override; - const std::string& getLastError() const override ; - Event next() override ; - - const std::u16string& getComment() const override; - size_t getLineNumber() const override; - size_t getDepth() const override; - - const std::u16string& getText() const override; - - const std::u16string& getNamespacePrefix() const override; - const std::u16string& getNamespaceUri() const override; - bool applyPackageAlias(std::u16string* package, - const std::u16string& defaultPackage) const override; - - - const std::u16string& getElementNamespace() const override; - const std::u16string& getElementName() const override; - - const_iterator beginAttributes() const override; - const_iterator endAttributes() const override; - size_t getAttributeCount() const override; - -private: - static void XMLCALL startNamespaceHandler(void* userData, const char* prefix, const char* uri); - static void XMLCALL startElementHandler(void* userData, const char* name, const char** attrs); - static void XMLCALL characterDataHandler(void* userData, const char* s, int len); - static void XMLCALL endElementHandler(void* userData, const char* name); - static void XMLCALL endNamespaceHandler(void* userData, const char* prefix); - static void XMLCALL commentDataHandler(void* userData, const char* comment); - - struct EventData { - Event event; - size_t lineNumber; - size_t depth; - std::u16string data1; - std::u16string data2; - std::u16string comment; - std::vector<Attribute> attributes; - }; - - std::istream& mIn; - XML_Parser mParser; - char mBuffer[16384]; - std::queue<EventData> mEventQueue; - std::string mLastError; - const std::u16string mEmpty; - size_t mDepth; - std::stack<std::u16string> mNamespaceUris; - std::vector<std::pair<std::u16string, std::u16string>> mPackageAliases; -}; - -} // namespace aapt - -#endif // AAPT_SOURCE_XML_PULL_PARSER_H diff --git a/tools/aapt2/StringPool.cpp b/tools/aapt2/StringPool.cpp index c19aa98a70ac..aadb00b6be2a 100644 --- a/tools/aapt2/StringPool.cpp +++ b/tools/aapt2/StringPool.cpp @@ -14,10 +14,10 @@ * limitations under the License. */ -#include "BigBuffer.h" -#include "StringPiece.h" #include "StringPool.h" -#include "Util.h" +#include "util/BigBuffer.h" +#include "util/StringPiece.h" +#include "util/Util.h" #include <algorithm> #include <androidfw/ResourceTypes.h> @@ -219,7 +219,7 @@ void StringPool::prune() { auto indexIter = std::begin(mIndexedStrings); while (indexIter != iterEnd) { if (indexIter->second->ref <= 0) { - mIndexedStrings.erase(indexIter++); + indexIter = mIndexedStrings.erase(indexIter); } else { ++indexIter; } @@ -241,6 +241,12 @@ void StringPool::prune() { // a deleted string from the StyleEntry. mStrings.erase(endIter2, std::end(mStrings)); mStyles.erase(endIter3, std::end(mStyles)); + + // Reassign the indices. + const size_t len = mStrings.size(); + for (size_t index = 0; index < len; index++) { + mStrings[index]->index = index; + } } void StringPool::sort(const std::function<bool(const Entry&, const Entry&)>& cmp) { @@ -336,7 +342,14 @@ bool StringPool::flatten(BigBuffer* out, const StringPool& pool, bool utf8) { // Encode the actual UTF16 string length. data = encodeLength(data, entry->value.size()); - strncpy16(data, entry->value.data(), entry->value.size()); + const size_t byteLength = entry->value.size() * sizeof(char16_t); + + // NOTE: For some reason, strncpy16(data, entry->value.data(), entry->value.size()) + // truncates the string. + memcpy(data, entry->value.data(), byteLength); + + // The null-terminating character is already here due to the block of data being set + // to 0s on allocation. } } diff --git a/tools/aapt2/StringPool.h b/tools/aapt2/StringPool.h index 14304a6e6b1a..509e3041e081 100644 --- a/tools/aapt2/StringPool.h +++ b/tools/aapt2/StringPool.h @@ -17,9 +17,9 @@ #ifndef AAPT_STRING_POOL_H #define AAPT_STRING_POOL_H -#include "BigBuffer.h" +#include "util/BigBuffer.h" #include "ConfigDescription.h" -#include "StringPiece.h" +#include "util/StringPiece.h" #include <functional> #include <map> diff --git a/tools/aapt2/StringPool_test.cpp b/tools/aapt2/StringPool_test.cpp index 9552937d4ad4..e93c2fba7f3c 100644 --- a/tools/aapt2/StringPool_test.cpp +++ b/tools/aapt2/StringPool_test.cpp @@ -15,7 +15,7 @@ */ #include "StringPool.h" -#include "Util.h" +#include "util/Util.h" #include <gtest/gtest.h> #include <string> @@ -67,15 +67,23 @@ TEST(StringPoolTest, MaintainInsertionOrderIndex) { TEST(StringPoolTest, PruneStringsWithNoReferences) { StringPool pool; + StringPool::Ref refA = pool.makeRef(u"foo"); { StringPool::Ref ref = pool.makeRef(u"wut"); EXPECT_EQ(*ref, u"wut"); - EXPECT_EQ(1u, pool.size()); + EXPECT_EQ(2u, pool.size()); } + StringPool::Ref refB = pool.makeRef(u"bar"); - EXPECT_EQ(1u, pool.size()); + EXPECT_EQ(3u, pool.size()); pool.prune(); - EXPECT_EQ(0u, pool.size()); + EXPECT_EQ(2u, pool.size()); + StringPool::const_iterator iter = begin(pool); + EXPECT_EQ((*iter)->value, u"foo"); + EXPECT_LT((*iter)->index, 2u); + ++iter; + EXPECT_EQ((*iter)->value, u"bar"); + EXPECT_LT((*iter)->index, 2u); } TEST(StringPoolTest, SortAndMaintainIndexesInReferences) { @@ -172,6 +180,22 @@ TEST(StringPoolTest, FlattenEmptyStringPoolUtf8) { ASSERT_EQ(test.setTo(data.get(), buffer.size()), android::NO_ERROR); } +TEST(StringPoolTest, FlattenOddCharactersUtf16) { + StringPool pool; + pool.makeRef(u"\u093f"); + BigBuffer buffer(1024); + StringPool::flattenUtf16(&buffer, pool); + + std::unique_ptr<uint8_t[]> data = util::copy(buffer); + android::ResStringPool test; + ASSERT_EQ(test.setTo(data.get(), buffer.size()), android::NO_ERROR); + size_t len = 0; + const char16_t* str = test.stringAt(0, &len); + EXPECT_EQ(1u, len); + EXPECT_EQ(u'\u093f', *str); + EXPECT_EQ(0u, str[1]); +} + constexpr const char16_t* sLongString = u"バッテリーを長持ちさせるため、バッテリーセーバーは端末のパフォーマンスを抑え、バイブレーション、位置情報サービス、大半のバックグラウンドデータを制限します。メール、SMSや、同期を使 用するその他のアプリは、起動しても更新されないことがあります。バッテリーセーバーは端末の充電中は自動的にOFFになります。"; TEST(StringPoolTest, FlattenUtf8) { diff --git a/tools/aapt2/TableFlattener.cpp b/tools/aapt2/TableFlattener.cpp deleted file mode 100644 index b7c04f06cff5..000000000000 --- a/tools/aapt2/TableFlattener.cpp +++ /dev/null @@ -1,570 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "BigBuffer.h" -#include "ConfigDescription.h" -#include "Logger.h" -#include "ResourceTable.h" -#include "ResourceTypeExtensions.h" -#include "ResourceValues.h" -#include "StringPool.h" -#include "TableFlattener.h" -#include "Util.h" - -#include <algorithm> -#include <androidfw/ResourceTypes.h> -#include <sstream> - -namespace aapt { - -struct FlatEntry { - const ResourceEntry* entry; - const Value* value; - uint32_t entryKey; - uint32_t sourcePathKey; - uint32_t sourceLine; -}; - -/** - * Visitor that knows how to encode Map values. - */ -class MapFlattener : public ConstValueVisitor { -public: - MapFlattener(BigBuffer* out, const FlatEntry& flatEntry, SymbolEntryVector* symbols) : - mOut(out), mSymbols(symbols) { - mMap = mOut->nextBlock<android::ResTable_map_entry>(); - mMap->key.index = flatEntry.entryKey; - mMap->flags = android::ResTable_entry::FLAG_COMPLEX; - if (flatEntry.entry->publicStatus.isPublic) { - mMap->flags |= android::ResTable_entry::FLAG_PUBLIC; - } - if (flatEntry.value->isWeak()) { - mMap->flags |= android::ResTable_entry::FLAG_WEAK; - } - - ResTable_entry_source* sourceBlock = mOut->nextBlock<ResTable_entry_source>(); - sourceBlock->pathIndex = flatEntry.sourcePathKey; - sourceBlock->line = flatEntry.sourceLine; - - mMap->size = sizeof(*mMap) + sizeof(*sourceBlock); - } - - void flattenParent(const Reference& ref) { - if (!ref.id.isValid()) { - mSymbols->push_back({ - ResourceNameRef(ref.name), - (mOut->size() - mMap->size) + sizeof(*mMap) - sizeof(android::ResTable_entry) - }); - } - mMap->parent.ident = ref.id.id; - } - - void flattenEntry(const Reference& key, const Item& value) { - mMap->count++; - - android::ResTable_map* outMapEntry = mOut->nextBlock<android::ResTable_map>(); - - // Write the key. - if (!Res_INTERNALID(key.id.id) && !key.id.isValid()) { - assert(!key.name.entry.empty()); - mSymbols->push_back(std::make_pair(ResourceNameRef(key.name), - mOut->size() - sizeof(*outMapEntry))); - } - outMapEntry->name.ident = key.id.id; - - // Write the value. - value.flatten(outMapEntry->value); - - if (outMapEntry->value.data == 0x0) { - visitFunc<Reference>(value, [&](const Reference& reference) { - mSymbols->push_back(std::make_pair(ResourceNameRef(reference.name), - mOut->size() - sizeof(outMapEntry->value.data))); - }); - } - outMapEntry->value.size = sizeof(outMapEntry->value); - } - - void flattenValueOnly(const Item& value) { - mMap->count++; - - android::ResTable_map* outMapEntry = mOut->nextBlock<android::ResTable_map>(); - - // Write the value. - value.flatten(outMapEntry->value); - - if (outMapEntry->value.data == 0x0) { - visitFunc<Reference>(value, [&](const Reference& reference) { - mSymbols->push_back(std::make_pair(ResourceNameRef(reference.name), - mOut->size() - sizeof(outMapEntry->value.data))); - }); - } - outMapEntry->value.size = sizeof(outMapEntry->value); - } - - static bool compareStyleEntries(const Style::Entry* lhs, const Style::Entry* rhs) { - return lhs->key.id < rhs->key.id; - } - - void visit(const Style& style, ValueVisitorArgs&) override { - if (style.parent.name.isValid()) { - flattenParent(style.parent); - } - - // First sort the entries by ID. - std::vector<const Style::Entry*> sortedEntries; - for (const auto& styleEntry : style.entries) { - auto iter = std::lower_bound(sortedEntries.begin(), sortedEntries.end(), - &styleEntry, compareStyleEntries); - sortedEntries.insert(iter, &styleEntry); - } - - for (const Style::Entry* styleEntry : sortedEntries) { - flattenEntry(styleEntry->key, *styleEntry->value); - } - } - - void visit(const Attribute& attr, ValueVisitorArgs&) override { - android::Res_value tempVal; - tempVal.dataType = android::Res_value::TYPE_INT_DEC; - tempVal.data = attr.typeMask; - flattenEntry(Reference(ResourceId{android::ResTable_map::ATTR_TYPE}), - BinaryPrimitive(tempVal)); - - for (const auto& symbol : attr.symbols) { - tempVal.data = symbol.value; - flattenEntry(symbol.symbol, BinaryPrimitive(tempVal)); - } - } - - void visit(const Styleable& styleable, ValueVisitorArgs&) override { - for (const auto& attr : styleable.entries) { - flattenEntry(attr, BinaryPrimitive(android::Res_value{})); - } - } - - void visit(const Array& array, ValueVisitorArgs&) override { - for (const auto& item : array.items) { - flattenValueOnly(*item); - } - } - - void visit(const Plural& plural, ValueVisitorArgs&) override { - const size_t count = plural.values.size(); - for (size_t i = 0; i < count; i++) { - if (!plural.values[i]) { - continue; - } - - ResourceId q; - switch (i) { - case Plural::Zero: - q.id = android::ResTable_map::ATTR_ZERO; - break; - - case Plural::One: - q.id = android::ResTable_map::ATTR_ONE; - break; - - case Plural::Two: - q.id = android::ResTable_map::ATTR_TWO; - break; - - case Plural::Few: - q.id = android::ResTable_map::ATTR_FEW; - break; - - case Plural::Many: - q.id = android::ResTable_map::ATTR_MANY; - break; - - case Plural::Other: - q.id = android::ResTable_map::ATTR_OTHER; - break; - - default: - assert(false); - break; - } - - flattenEntry(Reference(q), *plural.values[i]); - } - } - -private: - BigBuffer* mOut; - SymbolEntryVector* mSymbols; - android::ResTable_map_entry* mMap; -}; - -/** - * Flattens a value, with special handling for References. - */ -struct ValueFlattener : ConstValueVisitor { - ValueFlattener(BigBuffer* out, SymbolEntryVector* symbols) : - result(false), mOut(out), mOutValue(nullptr), mSymbols(symbols) { - mOutValue = mOut->nextBlock<android::Res_value>(); - } - - virtual void visit(const Reference& ref, ValueVisitorArgs& a) override { - visitItem(ref, a); - if (mOutValue->data == 0x0) { - mSymbols->push_back({ - ResourceNameRef(ref.name), - mOut->size() - sizeof(mOutValue->data)}); - } - } - - virtual void visitItem(const Item& item, ValueVisitorArgs&) override { - result = item.flatten(*mOutValue); - mOutValue->res0 = 0; - mOutValue->size = sizeof(*mOutValue); - } - - bool result; - -private: - BigBuffer* mOut; - android::Res_value* mOutValue; - SymbolEntryVector* mSymbols; -}; - -TableFlattener::TableFlattener(Options options) -: mOptions(options) { -} - -bool TableFlattener::flattenValue(BigBuffer* out, const FlatEntry& flatEntry, - SymbolEntryVector* symbols) { - if (flatEntry.value->isItem()) { - android::ResTable_entry* entry = out->nextBlock<android::ResTable_entry>(); - - if (flatEntry.entry->publicStatus.isPublic) { - entry->flags |= android::ResTable_entry::FLAG_PUBLIC; - } - - if (flatEntry.value->isWeak()) { - entry->flags |= android::ResTable_entry::FLAG_WEAK; - } - - entry->key.index = flatEntry.entryKey; - entry->size = sizeof(*entry); - - if (mOptions.useExtendedChunks) { - // Write the extra source block. This will be ignored by - // the Android runtime. - ResTable_entry_source* sourceBlock = out->nextBlock<ResTable_entry_source>(); - sourceBlock->pathIndex = flatEntry.sourcePathKey; - sourceBlock->line = flatEntry.sourceLine; - entry->size += sizeof(*sourceBlock); - } - - const Item* item = static_cast<const Item*>(flatEntry.value); - ValueFlattener flattener(out, symbols); - item->accept(flattener, {}); - return flattener.result; - } - - MapFlattener flattener(out, flatEntry, symbols); - flatEntry.value->accept(flattener, {}); - return true; -} - -bool TableFlattener::flatten(BigBuffer* out, const ResourceTable& table) { - const size_t beginning = out->size(); - - if (table.getPackageId() == ResourceTable::kUnsetPackageId) { - Logger::error() - << "ResourceTable has no package ID set." - << std::endl; - return false; - } - - SymbolEntryVector symbolEntries; - - StringPool typePool; - StringPool keyPool; - StringPool sourcePool; - - // Sort the types by their IDs. They will be inserted into the StringPool - // in this order. - std::vector<ResourceTableType*> sortedTypes; - for (const auto& type : table) { - if (type->type == ResourceType::kStyleable && !mOptions.useExtendedChunks) { - continue; - } - - auto iter = std::lower_bound(std::begin(sortedTypes), std::end(sortedTypes), type.get(), - [](const ResourceTableType* lhs, const ResourceTableType* rhs) -> bool { - return lhs->typeId < rhs->typeId; - }); - sortedTypes.insert(iter, type.get()); - } - - BigBuffer typeBlock(1024); - size_t expectedTypeId = 1; - for (const ResourceTableType* type : sortedTypes) { - if (type->typeId == ResourceTableType::kUnsetTypeId - || type->typeId == 0) { - Logger::error() - << "resource type '" - << type->type - << "' from package '" - << table.getPackage() - << "' has no ID." - << std::endl; - return false; - } - - // If there is a gap in the type IDs, fill in the StringPool - // with empty values until we reach the ID we expect. - while (type->typeId > expectedTypeId) { - std::u16string typeName(u"?"); - typeName += expectedTypeId; - typePool.makeRef(typeName); - expectedTypeId++; - } - expectedTypeId++; - typePool.makeRef(toString(type->type)); - - android::ResTable_typeSpec* spec = typeBlock.nextBlock<android::ResTable_typeSpec>(); - spec->header.type = android::RES_TABLE_TYPE_SPEC_TYPE; - spec->header.headerSize = sizeof(*spec); - spec->header.size = spec->header.headerSize + (type->entries.size() * sizeof(uint32_t)); - spec->id = type->typeId; - spec->entryCount = type->entries.size(); - - if (type->entries.empty()) { - continue; - } - - // Reserve space for the masks of each resource in this type. These - // show for which configuration axis the resource changes. - uint32_t* configMasks = typeBlock.nextBlock<uint32_t>(type->entries.size()); - - // Sort the entries by entry ID and write their configuration masks. - std::vector<ResourceEntry*> entries; - const size_t entryCount = type->entries.size(); - for (size_t entryIndex = 0; entryIndex < entryCount; entryIndex++) { - const auto& entry = type->entries[entryIndex]; - - if (entry->entryId == ResourceEntry::kUnsetEntryId) { - Logger::error() - << "resource '" - << ResourceName{ table.getPackage(), type->type, entry->name } - << "' has no ID." - << std::endl; - return false; - } - - auto iter = std::lower_bound(std::begin(entries), std::end(entries), entry.get(), - [](const ResourceEntry* lhs, const ResourceEntry* rhs) -> bool { - return lhs->entryId < rhs->entryId; - }); - entries.insert(iter, entry.get()); - - // Populate the config masks for this entry. - if (entry->publicStatus.isPublic) { - configMasks[entry->entryId] |= android::ResTable_typeSpec::SPEC_PUBLIC; - } - - const size_t configCount = entry->values.size(); - for (size_t i = 0; i < configCount; i++) { - const ConfigDescription& config = entry->values[i].config; - for (size_t j = i + 1; j < configCount; j++) { - configMasks[entry->entryId] |= config.diff(entry->values[j].config); - } - } - } - - const size_t beforePublicHeader = typeBlock.size(); - Public_header* publicHeader = nullptr; - if (mOptions.useExtendedChunks) { - publicHeader = typeBlock.nextBlock<Public_header>(); - publicHeader->header.type = RES_TABLE_PUBLIC_TYPE; - publicHeader->header.headerSize = sizeof(*publicHeader); - publicHeader->typeId = type->typeId; - } - - // The binary resource table lists resource entries for each configuration. - // We store them inverted, where a resource entry lists the values for each - // configuration available. Here we reverse this to match the binary table. - std::map<ConfigDescription, std::vector<FlatEntry>> data; - for (const ResourceEntry* entry : entries) { - size_t keyIndex = keyPool.makeRef(entry->name).getIndex(); - - if (keyIndex > std::numeric_limits<uint32_t>::max()) { - Logger::error() - << "resource key string pool exceeded max size." - << std::endl; - return false; - } - - if (publicHeader && entry->publicStatus.isPublic) { - // Write the public status of this entry. - Public_entry* publicEntry = typeBlock.nextBlock<Public_entry>(); - publicEntry->entryId = static_cast<uint32_t>(entry->entryId); - publicEntry->key.index = static_cast<uint32_t>(keyIndex); - publicEntry->source.index = static_cast<uint32_t>(sourcePool.makeRef( - util::utf8ToUtf16(entry->publicStatus.source.path)).getIndex()); - publicEntry->sourceLine = static_cast<uint32_t>(entry->publicStatus.source.line); - publicHeader->count += 1; - } - - for (const auto& configValue : entry->values) { - data[configValue.config].push_back(FlatEntry{ - entry, - configValue.value.get(), - static_cast<uint32_t>(keyIndex), - static_cast<uint32_t>(sourcePool.makeRef(util::utf8ToUtf16( - configValue.source.path)).getIndex()), - static_cast<uint32_t>(configValue.source.line) - }); - } - } - - if (publicHeader) { - typeBlock.align4(); - publicHeader->header.size = - static_cast<uint32_t>(typeBlock.size() - beforePublicHeader); - } - - // Begin flattening a configuration for the current type. - for (const auto& entry : data) { - const size_t typeHeaderStart = typeBlock.size(); - android::ResTable_type* typeHeader = typeBlock.nextBlock<android::ResTable_type>(); - typeHeader->header.type = android::RES_TABLE_TYPE_TYPE; - typeHeader->header.headerSize = sizeof(*typeHeader); - typeHeader->id = type->typeId; - typeHeader->entryCount = type->entries.size(); - typeHeader->entriesStart = typeHeader->header.headerSize - + (sizeof(uint32_t) * type->entries.size()); - typeHeader->config = entry.first; - - uint32_t* indices = typeBlock.nextBlock<uint32_t>(type->entries.size()); - memset(indices, 0xff, type->entries.size() * sizeof(uint32_t)); - - const size_t entryStart = typeBlock.size(); - for (const FlatEntry& flatEntry : entry.second) { - assert(flatEntry.entry->entryId < type->entries.size()); - indices[flatEntry.entry->entryId] = typeBlock.size() - entryStart; - if (!flattenValue(&typeBlock, flatEntry, &symbolEntries)) { - Logger::error() - << "failed to flatten resource '" - << ResourceNameRef { - table.getPackage(), type->type, flatEntry.entry->name } - << "' for configuration '" - << entry.first - << "'." - << std::endl; - return false; - } - } - - typeBlock.align4(); - typeHeader->header.size = typeBlock.size() - typeHeaderStart; - } - } - - const size_t beforeTable = out->size(); - android::ResTable_header* header = out->nextBlock<android::ResTable_header>(); - header->header.type = android::RES_TABLE_TYPE; - header->header.headerSize = sizeof(*header); - header->packageCount = 1; - - SymbolTable_entry* symbolEntryData = nullptr; - if (!symbolEntries.empty() && mOptions.useExtendedChunks) { - const size_t beforeSymbolTable = out->size(); - StringPool symbolPool; - SymbolTable_header* symbolHeader = out->nextBlock<SymbolTable_header>(); - symbolHeader->header.type = RES_TABLE_SYMBOL_TABLE_TYPE; - symbolHeader->header.headerSize = sizeof(*symbolHeader); - symbolHeader->count = symbolEntries.size(); - - symbolEntryData = out->nextBlock<SymbolTable_entry>(symbolHeader->count); - - size_t i = 0; - for (const auto& entry : symbolEntries) { - symbolEntryData[i].offset = entry.second; - StringPool::Ref ref = symbolPool.makeRef( - entry.first.package.toString() + u":" + - toString(entry.first.type).toString() + u"/" + - entry.first.entry.toString()); - symbolEntryData[i].stringIndex = ref.getIndex(); - i++; - } - - StringPool::flattenUtf8(out, symbolPool); - out->align4(); - symbolHeader->header.size = out->size() - beforeSymbolTable; - } - - if (sourcePool.size() > 0 && mOptions.useExtendedChunks) { - const size_t beforeSourcePool = out->size(); - android::ResChunk_header* sourceHeader = out->nextBlock<android::ResChunk_header>(); - sourceHeader->type = RES_TABLE_SOURCE_POOL_TYPE; - sourceHeader->headerSize = sizeof(*sourceHeader); - StringPool::flattenUtf8(out, sourcePool); - out->align4(); - sourceHeader->size = out->size() - beforeSourcePool; - } - - StringPool::flattenUtf8(out, table.getValueStringPool()); - - const size_t beforePackageIndex = out->size(); - android::ResTable_package* package = out->nextBlock<android::ResTable_package>(); - package->header.type = android::RES_TABLE_PACKAGE_TYPE; - package->header.headerSize = sizeof(*package); - - if (table.getPackageId() > std::numeric_limits<uint8_t>::max()) { - Logger::error() - << "package ID 0x'" - << std::hex << table.getPackageId() << std::dec - << "' is invalid." - << std::endl; - return false; - } - package->id = table.getPackageId(); - - if (table.getPackage().size() >= sizeof(package->name) / sizeof(package->name[0])) { - Logger::error() - << "package name '" - << table.getPackage() - << "' is too long." - << std::endl; - return false; - } - memcpy(package->name, reinterpret_cast<const uint16_t*>(table.getPackage().data()), - table.getPackage().length() * sizeof(char16_t)); - package->name[table.getPackage().length()] = 0; - - package->typeStrings = package->header.headerSize; - StringPool::flattenUtf16(out, typePool); - package->keyStrings = out->size() - beforePackageIndex; - StringPool::flattenUtf16(out, keyPool); - - if (symbolEntryData != nullptr) { - for (size_t i = 0; i < symbolEntries.size(); i++) { - symbolEntryData[i].offset += out->size() - beginning; - } - } - - out->appendBuffer(std::move(typeBlock)); - - package->header.size = out->size() - beforePackageIndex; - header->header.size = out->size() - beforeTable; - return true; -} - -} // namespace aapt diff --git a/tools/aapt2/TableFlattener.h b/tools/aapt2/TableFlattener.h deleted file mode 100644 index ccbb737059f9..000000000000 --- a/tools/aapt2/TableFlattener.h +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_TABLE_FLATTENER_H -#define AAPT_TABLE_FLATTENER_H - -#include "BigBuffer.h" -#include "ResourceTable.h" - -namespace aapt { - -using SymbolEntryVector = std::vector<std::pair<ResourceNameRef, uint32_t>>; - -struct FlatEntry; - -/** - * Flattens a ResourceTable into a binary format suitable - * for loading into a ResTable on the host or device. - */ -struct TableFlattener { - /** - * A set of options for this TableFlattener. - */ - struct Options { - /** - * Specifies whether to output extended chunks, like - * source information and mising symbol entries. Default - * is true. - * - * Set this to false when emitting the final table to be used - * on device. - */ - bool useExtendedChunks = true; - }; - - TableFlattener(Options options); - - bool flatten(BigBuffer* out, const ResourceTable& table); - -private: - bool flattenValue(BigBuffer* out, const FlatEntry& flatEntry, SymbolEntryVector* symbols); - - Options mOptions; -}; - -} // namespace aapt - -#endif // AAPT_TABLE_FLATTENER_H diff --git a/tools/aapt2/Util_test.cpp b/tools/aapt2/Util_test.cpp deleted file mode 100644 index 0b08d240cad3..000000000000 --- a/tools/aapt2/Util_test.cpp +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include <gtest/gtest.h> -#include <string> - -#include "StringPiece.h" -#include "Util.h" - -namespace aapt { - -TEST(UtilTest, TrimOnlyWhitespace) { - const std::u16string full = u"\n "; - - StringPiece16 trimmed = util::trimWhitespace(full); - EXPECT_TRUE(trimmed.empty()); - EXPECT_EQ(0u, trimmed.size()); -} - -TEST(UtilTest, StringEndsWith) { - EXPECT_TRUE(util::stringEndsWith<char>("hello.xml", ".xml")); -} - -TEST(UtilTest, StringStartsWith) { - EXPECT_TRUE(util::stringStartsWith<char>("hello.xml", "he")); -} - -TEST(UtilTest, StringBuilderWhitespaceRemoval) { - EXPECT_EQ(StringPiece16(u"hey guys this is so cool"), - util::StringBuilder().append(u" hey guys ") - .append(u" this is so cool ") - .str()); - - EXPECT_EQ(StringPiece16(u" wow, so many \t spaces. what?"), - util::StringBuilder().append(u" \" wow, so many \t ") - .append(u"spaces. \"what? ") - .str()); - - EXPECT_EQ(StringPiece16(u"where is the pie?"), - util::StringBuilder().append(u" where \t ") - .append(u" \nis the "" pie?") - .str()); -} - -TEST(UtilTest, StringBuilderEscaping) { - EXPECT_EQ(StringPiece16(u"hey guys\n this \t is so\\ cool"), - util::StringBuilder().append(u" hey guys\\n ") - .append(u" this \\t is so\\\\ cool ") - .str()); - - EXPECT_EQ(StringPiece16(u"@?#\\\'"), - util::StringBuilder().append(u"\\@\\?\\#\\\\\\'") - .str()); -} - -TEST(UtilTest, StringBuilderMisplacedQuote) { - util::StringBuilder builder{}; - EXPECT_FALSE(builder.append(u"they're coming!")); -} - -TEST(UtilTest, StringBuilderUnicodeCodes) { - EXPECT_EQ(StringPiece16(u"\u00AF\u0AF0 woah"), - util::StringBuilder().append(u"\\u00AF\\u0AF0 woah") - .str()); - - EXPECT_FALSE(util::StringBuilder().append(u"\\u00 yo")); -} - -TEST(UtilTest, TokenizeInput) { - auto tokenizer = util::tokenize(StringPiece16(u"this| is|the|end"), u'|'); - auto iter = tokenizer.begin(); - ASSERT_EQ(*iter, StringPiece16(u"this")); - ++iter; - ASSERT_EQ(*iter, StringPiece16(u" is")); - ++iter; - ASSERT_EQ(*iter, StringPiece16(u"the")); - ++iter; - ASSERT_EQ(*iter, StringPiece16(u"end")); - ++iter; - ASSERT_EQ(tokenizer.end(), iter); -} - -TEST(UtilTest, IsJavaClassName) { - EXPECT_TRUE(util::isJavaClassName(u"android.test.Class")); - EXPECT_TRUE(util::isJavaClassName(u"android.test.Class$Inner")); - EXPECT_TRUE(util::isJavaClassName(u"android_test.test.Class")); - EXPECT_TRUE(util::isJavaClassName(u"_android_.test._Class_")); - EXPECT_FALSE(util::isJavaClassName(u"android.test.$Inner")); - EXPECT_FALSE(util::isJavaClassName(u"android.test.Inner$")); - EXPECT_FALSE(util::isJavaClassName(u".test.Class")); - EXPECT_FALSE(util::isJavaClassName(u"android")); -} - -TEST(UtilTest, FullyQualifiedClassName) { - Maybe<std::u16string> res = util::getFullyQualifiedClassName(u"android", u"asdf"); - ASSERT_TRUE(res); - EXPECT_EQ(res.value(), u"android.asdf"); - - res = util::getFullyQualifiedClassName(u"android", u".asdf"); - ASSERT_TRUE(res); - EXPECT_EQ(res.value(), u"android.asdf"); - - res = util::getFullyQualifiedClassName(u"android", u".a.b"); - ASSERT_TRUE(res); - EXPECT_EQ(res.value(), u"android.a.b"); - - res = util::getFullyQualifiedClassName(u"android", u"a.b"); - ASSERT_TRUE(res); - EXPECT_EQ(res.value(), u"a.b"); - - res = util::getFullyQualifiedClassName(u"", u"a.b"); - ASSERT_TRUE(res); - EXPECT_EQ(res.value(), u"a.b"); - - res = util::getFullyQualifiedClassName(u"", u""); - ASSERT_FALSE(res); - - res = util::getFullyQualifiedClassName(u"android", u"./Apple"); - ASSERT_FALSE(res); -} - - -} // namespace aapt diff --git a/tools/aapt2/ValueVisitor.h b/tools/aapt2/ValueVisitor.h new file mode 100644 index 000000000000..94042e3c2618 --- /dev/null +++ b/tools/aapt2/ValueVisitor.h @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_VALUE_VISITOR_H +#define AAPT_VALUE_VISITOR_H + +#include "ResourceValues.h" + +namespace aapt { + +/** + * Visits a value and invokes the appropriate method based on its type. Does not traverse + * into compound types. Use ValueVisitor for that. + */ +struct RawValueVisitor { + virtual ~RawValueVisitor() = default; + + virtual void visitItem(Item* value) {} + virtual void visit(Reference* value) { visitItem(value); } + virtual void visit(RawString* value) { visitItem(value); } + virtual void visit(String* value) { visitItem(value); } + virtual void visit(StyledString* value) { visitItem(value); } + virtual void visit(FileReference* value) { visitItem(value); } + virtual void visit(Id* value) { visitItem(value); } + virtual void visit(BinaryPrimitive* value) { visitItem(value); } + + virtual void visit(Attribute* value) {} + virtual void visit(Style* value) {} + virtual void visit(Array* value) {} + virtual void visit(Plural* value) {} + virtual void visit(Styleable* value) {} +}; + +#define DECL_VISIT_COMPOUND_VALUE(T) \ + virtual void visit(T* value) { \ + visitSubValues(value); \ + } + +/** + * Visits values, and if they are compound values, visits the components as well. + */ +struct ValueVisitor : public RawValueVisitor { + // The compiler will think we're hiding an overload, when we actually intend + // to call into RawValueVisitor. This will expose the visit methods in the super + // class so the compiler knows we are trying to call them. + using RawValueVisitor::visit; + + void visitSubValues(Attribute* attribute) { + for (Attribute::Symbol& symbol : attribute->symbols) { + visit(&symbol.symbol); + } + } + + void visitSubValues(Style* style) { + if (style->parent) { + visit(&style->parent.value()); + } + + for (Style::Entry& entry : style->entries) { + visit(&entry.key); + entry.value->accept(this); + } + } + + void visitSubValues(Array* array) { + for (std::unique_ptr<Item>& item : array->items) { + item->accept(this); + } + } + + void visitSubValues(Plural* plural) { + for (std::unique_ptr<Item>& item : plural->values) { + if (item) { + item->accept(this); + } + } + } + + void visitSubValues(Styleable* styleable) { + for (Reference& reference : styleable->entries) { + visit(&reference); + } + } + + DECL_VISIT_COMPOUND_VALUE(Attribute); + DECL_VISIT_COMPOUND_VALUE(Style); + DECL_VISIT_COMPOUND_VALUE(Array); + DECL_VISIT_COMPOUND_VALUE(Plural); + DECL_VISIT_COMPOUND_VALUE(Styleable); +}; + +/** + * Do not use directly. Helper struct for dyn_cast. + */ +template <typename T> +struct DynCastVisitor : public RawValueVisitor { + T* value = nullptr; + + void visit(T* v) override { + value = v; + } +}; + +/** + * Specialization that checks if the value is an Item. + */ +template <> +struct DynCastVisitor<Item> : public RawValueVisitor { + Item* value = nullptr; + + void visitItem(Item* item) override { + value = item; + } +}; + +/** + * Returns a valid pointer to T if the Value is of subtype T. + * Otherwise, returns nullptr. + */ +template <typename T> +T* valueCast(Value* value) { + if (!value) { + return nullptr; + } + DynCastVisitor<T> visitor; + value->accept(&visitor); + return visitor.value; +} + +} // namespace aapt + +#endif // AAPT_VALUE_VISITOR_H diff --git a/tools/aapt2/ValueVisitor_test.cpp b/tools/aapt2/ValueVisitor_test.cpp new file mode 100644 index 000000000000..1624079727bb --- /dev/null +++ b/tools/aapt2/ValueVisitor_test.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <gtest/gtest.h> +#include <string> + +#include "ResourceValues.h" +#include "util/Util.h" +#include "ValueVisitor.h" +#include "test/Builders.h" + +namespace aapt { + +struct SingleReferenceVisitor : public ValueVisitor { + using ValueVisitor::visit; + + Reference* visited = nullptr; + + void visit(Reference* ref) override { + visited = ref; + } +}; + +struct StyleVisitor : public ValueVisitor { + using ValueVisitor::visit; + + std::list<Reference*> visitedRefs; + Style* visitedStyle = nullptr; + + void visit(Reference* ref) override { + visitedRefs.push_back(ref); + } + + void visit(Style* style) override { + visitedStyle = style; + ValueVisitor::visit(style); + } +}; + +TEST(ValueVisitorTest, VisitsReference) { + Reference ref(ResourceName{u"android", ResourceType::kAttr, u"foo"}); + SingleReferenceVisitor visitor; + ref.accept(&visitor); + + EXPECT_EQ(visitor.visited, &ref); +} + +TEST(ValueVisitorTest, VisitsReferencesInStyle) { + std::unique_ptr<Style> style = test::StyleBuilder() + .setParent(u"@android:style/foo") + .addItem(u"@android:attr/one", test::buildReference(u"@android:id/foo")) + .build(); + + StyleVisitor visitor; + style->accept(&visitor); + + ASSERT_EQ(style.get(), visitor.visitedStyle); + + // Entry attribute references, plus the parent reference, plus one value reference. + ASSERT_EQ(style->entries.size() + 2, visitor.visitedRefs.size()); +} + +TEST(ValueVisitorTest, ValueCast) { + std::unique_ptr<Reference> ref = test::buildReference(u"@android:color/white"); + EXPECT_NE(valueCast<Reference>(ref.get()), nullptr); + + std::unique_ptr<Style> style = test::StyleBuilder() + .addItem(u"@android:attr/foo", test::buildReference(u"@android:color/black")) + .build(); + EXPECT_NE(valueCast<Style>(style.get()), nullptr); + EXPECT_EQ(valueCast<Reference>(style.get()), nullptr); +} + +} // namespace aapt diff --git a/tools/aapt2/XliffXmlPullParser.cpp b/tools/aapt2/XliffXmlPullParser.cpp deleted file mode 100644 index 31115f28f58f..000000000000 --- a/tools/aapt2/XliffXmlPullParser.cpp +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "XliffXmlPullParser.h" - -#include <string> - -namespace aapt { - -XliffXmlPullParser::XliffXmlPullParser(const std::shared_ptr<XmlPullParser>& parser) : - mParser(parser) { -} - -XmlPullParser::Event XliffXmlPullParser::next() { - while (XmlPullParser::isGoodEvent(mParser->next())) { - Event event = mParser->getEvent(); - if (event != Event::kStartElement && event != Event::kEndElement) { - break; - } - - if (mParser->getElementNamespace() != - u"urn:oasis:names:tc:xliff:document:1.2") { - break; - } - - const std::u16string& name = mParser->getElementName(); - if (name != u"bpt" - && name != u"ept" - && name != u"it" - && name != u"ph" - && name != u"g" - && name != u"bx" - && name != u"ex" - && name != u"x") { - break; - } - - // We hit a tag that was ignored, so get the next event. - } - return mParser->getEvent(); -} - -XmlPullParser::Event XliffXmlPullParser::getEvent() const { - return mParser->getEvent(); -} - -const std::string& XliffXmlPullParser::getLastError() const { - return mParser->getLastError(); -} - -const std::u16string& XliffXmlPullParser::getComment() const { - return mParser->getComment(); -} - -size_t XliffXmlPullParser::getLineNumber() const { - return mParser->getLineNumber(); -} - -size_t XliffXmlPullParser::getDepth() const { - return mParser->getDepth(); -} - -const std::u16string& XliffXmlPullParser::getText() const { - return mParser->getText(); -} - -const std::u16string& XliffXmlPullParser::getNamespacePrefix() const { - return mParser->getNamespacePrefix(); -} - -const std::u16string& XliffXmlPullParser::getNamespaceUri() const { - return mParser->getNamespaceUri(); -} - -bool XliffXmlPullParser::applyPackageAlias(std::u16string* package, - const std::u16string& defaultPackage) const { - return mParser->applyPackageAlias(package, defaultPackage); -} - -const std::u16string& XliffXmlPullParser::getElementNamespace() const { - return mParser->getElementNamespace(); -} - -const std::u16string& XliffXmlPullParser::getElementName() const { - return mParser->getElementName(); -} - -size_t XliffXmlPullParser::getAttributeCount() const { - return mParser->getAttributeCount(); -} - -XmlPullParser::const_iterator XliffXmlPullParser::beginAttributes() const { - return mParser->beginAttributes(); -} - -XmlPullParser::const_iterator XliffXmlPullParser::endAttributes() const { - return mParser->endAttributes(); -} - -} // namespace aapt diff --git a/tools/aapt2/XliffXmlPullParser.h b/tools/aapt2/XliffXmlPullParser.h deleted file mode 100644 index 77912277b31e..000000000000 --- a/tools/aapt2/XliffXmlPullParser.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_XLIFF_XML_PULL_PARSER_H -#define AAPT_XLIFF_XML_PULL_PARSER_H - -#include "XmlPullParser.h" - -#include <memory> -#include <string> - -namespace aapt { - -/** - * Strips xliff elements and provides the caller with a view of the - * underlying XML without xliff. - */ -class XliffXmlPullParser : public XmlPullParser { -public: - XliffXmlPullParser(const std::shared_ptr<XmlPullParser>& parser); - XliffXmlPullParser(const XliffXmlPullParser& rhs) = delete; - - Event getEvent() const override; - const std::string& getLastError() const override; - Event next() override; - - const std::u16string& getComment() const override; - size_t getLineNumber() const override; - size_t getDepth() const override; - - const std::u16string& getText() const override; - - const std::u16string& getNamespacePrefix() const override; - const std::u16string& getNamespaceUri() const override; - bool applyPackageAlias(std::u16string* package, const std::u16string& defaultPackage) - const override; - - const std::u16string& getElementNamespace() const override; - const std::u16string& getElementName() const override; - - const_iterator beginAttributes() const override; - const_iterator endAttributes() const override; - size_t getAttributeCount() const override; - -private: - std::shared_ptr<XmlPullParser> mParser; -}; - -} // namespace aapt - -#endif // AAPT_XLIFF_XML_PULL_PARSER_H diff --git a/tools/aapt2/XliffXmlPullParser_test.cpp b/tools/aapt2/XliffXmlPullParser_test.cpp deleted file mode 100644 index f9030724b80b..000000000000 --- a/tools/aapt2/XliffXmlPullParser_test.cpp +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "SourceXmlPullParser.h" -#include "XliffXmlPullParser.h" - -#include <gtest/gtest.h> -#include <sstream> -#include <string> - -namespace aapt { - -TEST(XliffXmlPullParserTest, IgnoreXliffTags) { - std::stringstream input; - input << "<?xml version=\"1.0\" encoding=\"utf-8\"?>" << std::endl - << "<resources xmlns:xliff=\"urn:oasis:names:tc:xliff:document:1.2\">" << std::endl - << "<string name=\"foo\">" - << "Hey <xliff:g><xliff:it>there</xliff:it></xliff:g> world</string>" << std::endl - << "</resources>" << std::endl; - std::shared_ptr<XmlPullParser> sourceParser = std::make_shared<SourceXmlPullParser>(input); - XliffXmlPullParser parser(sourceParser); - EXPECT_EQ(XmlPullParser::Event::kStartDocument, parser.getEvent()); - - EXPECT_EQ(XmlPullParser::Event::kStartNamespace, parser.next()); - EXPECT_EQ(parser.getNamespaceUri(), u"urn:oasis:names:tc:xliff:document:1.2"); - EXPECT_EQ(parser.getNamespacePrefix(), u"xliff"); - - EXPECT_EQ(XmlPullParser::Event::kStartElement, parser.next()); - EXPECT_EQ(parser.getElementNamespace(), u""); - EXPECT_EQ(parser.getElementName(), u"resources"); - EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); // Account for newline/whitespace. - - EXPECT_EQ(XmlPullParser::Event::kStartElement, parser.next()); - EXPECT_EQ(parser.getElementNamespace(), u""); - EXPECT_EQ(parser.getElementName(), u"string"); - - EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); - EXPECT_EQ(parser.getText(), u"Hey "); - - EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); - EXPECT_EQ(parser.getText(), u"there"); - - EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); - EXPECT_EQ(parser.getText(), u" world"); - - EXPECT_EQ(XmlPullParser::Event::kEndElement, parser.next()); - EXPECT_EQ(parser.getElementNamespace(), u""); - EXPECT_EQ(parser.getElementName(), u"string"); - EXPECT_EQ(XmlPullParser::Event::kText, parser.next()); // Account for newline/whitespace. - - EXPECT_EQ(XmlPullParser::Event::kEndElement, parser.next()); - EXPECT_EQ(parser.getElementNamespace(), u""); - EXPECT_EQ(parser.getElementName(), u"resources"); - - EXPECT_EQ(XmlPullParser::Event::kEndNamespace, parser.next()); - EXPECT_EQ(parser.getNamespacePrefix(), u"xliff"); - EXPECT_EQ(parser.getNamespaceUri(), u"urn:oasis:names:tc:xliff:document:1.2"); - - EXPECT_EQ(XmlPullParser::Event::kEndDocument, parser.next()); -} - -} // namespace aapt diff --git a/tools/aapt2/XmlDom.h b/tools/aapt2/XmlDom.h deleted file mode 100644 index 035e7c46d1b5..000000000000 --- a/tools/aapt2/XmlDom.h +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_XML_DOM_H -#define AAPT_XML_DOM_H - -#include "Logger.h" -#include "StringPiece.h" - -#include <istream> -#include <expat.h> -#include <memory> -#include <string> -#include <vector> - -namespace aapt { -namespace xml { - -struct Visitor; - -/** - * The type of node. Can be used to downcast to the concrete XML node - * class. - */ -enum class NodeType { - kNamespace, - kElement, - kText, -}; - -/** - * Base class for all XML nodes. - */ -struct Node { - NodeType type; - Node* parent; - size_t lineNumber; - size_t columnNumber; - std::u16string comment; - std::vector<std::unique_ptr<Node>> children; - - Node(NodeType type); - void addChild(std::unique_ptr<Node> child); - virtual std::unique_ptr<Node> clone() const = 0; - virtual void accept(Visitor* visitor) = 0; - virtual ~Node() {} -}; - -/** - * Base class that implements the visitor methods for a - * subclass of Node. - */ -template <typename Derived> -struct BaseNode : public Node { - BaseNode(NodeType t); - virtual void accept(Visitor* visitor) override; -}; - -/** - * A Namespace XML node. Can only have one child. - */ -struct Namespace : public BaseNode<Namespace> { - std::u16string namespacePrefix; - std::u16string namespaceUri; - - Namespace(); - virtual std::unique_ptr<Node> clone() const override; -}; - -/** - * An XML attribute. - */ -struct Attribute { - std::u16string namespaceUri; - std::u16string name; - std::u16string value; -}; - -/** - * An Element XML node. - */ -struct Element : public BaseNode<Element> { - std::u16string namespaceUri; - std::u16string name; - std::vector<Attribute> attributes; - - Element(); - virtual std::unique_ptr<Node> clone() const override; - Attribute* findAttribute(const StringPiece16& ns, const StringPiece16& name); - xml::Element* findChild(const StringPiece16& ns, const StringPiece16& name); - xml::Element* findChildWithAttribute(const StringPiece16& ns, const StringPiece16& name, - const xml::Attribute* reqAttr); - std::vector<xml::Element*> getChildElements(); -}; - -/** - * A Text (CDATA) XML node. Can not have any children. - */ -struct Text : public BaseNode<Text> { - std::u16string text; - - Text(); - virtual std::unique_ptr<Node> clone() const override; -}; - -/** - * Inflates an XML DOM from a text stream, logging errors to the logger. - * Returns the root node on success, or nullptr on failure. - */ -std::unique_ptr<Node> inflate(std::istream* in, SourceLogger* logger); - -/** - * Inflates an XML DOM from a binary ResXMLTree, logging errors to the logger. - * Returns the root node on success, or nullptr on failure. - */ -std::unique_ptr<Node> inflate(const void* data, size_t dataLen, SourceLogger* logger); - -/** - * A visitor interface for the different XML Node subtypes. - */ -struct Visitor { - virtual void visit(Namespace* node) = 0; - virtual void visit(Element* node) = 0; - virtual void visit(Text* text) = 0; -}; - -// Implementations - -template <typename Derived> -BaseNode<Derived>::BaseNode(NodeType type) : Node(type) { -} - -template <typename Derived> -void BaseNode<Derived>::accept(Visitor* visitor) { - visitor->visit(static_cast<Derived*>(this)); -} - -} // namespace xml -} // namespace aapt - -#endif // AAPT_XML_DOM_H diff --git a/tools/aapt2/XmlFlattener.cpp b/tools/aapt2/XmlFlattener.cpp deleted file mode 100644 index 56b5613d4264..000000000000 --- a/tools/aapt2/XmlFlattener.cpp +++ /dev/null @@ -1,574 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "BigBuffer.h" -#include "Logger.h" -#include "Maybe.h" -#include "Resolver.h" -#include "Resource.h" -#include "ResourceParser.h" -#include "ResourceValues.h" -#include "SdkConstants.h" -#include "Source.h" -#include "StringPool.h" -#include "Util.h" -#include "XmlFlattener.h" - -#include <androidfw/ResourceTypes.h> -#include <limits> -#include <map> -#include <string> -#include <vector> - -namespace aapt { -namespace xml { - -constexpr uint32_t kLowPriority = 0xffffffffu; - -// A vector that maps String refs to their final destination in the out buffer. -using FlatStringRefList = std::vector<std::pair<StringPool::Ref, android::ResStringPool_ref*>>; - -struct XmlFlattener : public Visitor { - XmlFlattener(BigBuffer* outBuffer, StringPool* pool, FlatStringRefList* stringRefs, - const std::u16string& defaultPackage) : - mOut(outBuffer), mPool(pool), mStringRefs(stringRefs), - mDefaultPackage(defaultPackage) { - } - - // No copying. - XmlFlattener(const XmlFlattener&) = delete; - XmlFlattener& operator=(const XmlFlattener&) = delete; - - void writeNamespace(Namespace* node, uint16_t type) { - const size_t startIndex = mOut->size(); - android::ResXMLTree_node* flatNode = mOut->nextBlock<android::ResXMLTree_node>(); - android::ResXMLTree_namespaceExt* flatNs = - mOut->nextBlock<android::ResXMLTree_namespaceExt>(); - mOut->align4(); - - flatNode->header = { type, sizeof(*flatNode), (uint32_t)(mOut->size() - startIndex) }; - flatNode->lineNumber = node->lineNumber; - flatNode->comment.index = -1; - addString(node->namespacePrefix, kLowPriority, &flatNs->prefix); - addString(node->namespaceUri, kLowPriority, &flatNs->uri); - } - - virtual void visit(Namespace* node) override { - // Extract the package/prefix from this namespace node. - Maybe<std::u16string> package = util::extractPackageFromNamespace(node->namespaceUri); - if (package) { - mPackageAliases.emplace_back( - node->namespacePrefix, - package.value().empty() ? mDefaultPackage : package.value()); - } - - writeNamespace(node, android::RES_XML_START_NAMESPACE_TYPE); - for (const auto& child : node->children) { - child->accept(this); - } - writeNamespace(node, android::RES_XML_END_NAMESPACE_TYPE); - - if (package) { - mPackageAliases.pop_back(); - } - } - - virtual void visit(Text* node) override { - if (util::trimWhitespace(node->text).empty()) { - return; - } - - const size_t startIndex = mOut->size(); - android::ResXMLTree_node* flatNode = mOut->nextBlock<android::ResXMLTree_node>(); - android::ResXMLTree_cdataExt* flatText = mOut->nextBlock<android::ResXMLTree_cdataExt>(); - mOut->align4(); - - const uint16_t type = android::RES_XML_CDATA_TYPE; - flatNode->header = { type, sizeof(*flatNode), (uint32_t)(mOut->size() - startIndex) }; - flatNode->lineNumber = node->lineNumber; - flatNode->comment.index = -1; - addString(node->text, kLowPriority, &flatText->data); - } - - virtual void visit(Element* node) override { - const size_t startIndex = mOut->size(); - android::ResXMLTree_node* flatNode = mOut->nextBlock<android::ResXMLTree_node>(); - android::ResXMLTree_attrExt* flatElem = mOut->nextBlock<android::ResXMLTree_attrExt>(); - - const uint16_t type = android::RES_XML_START_ELEMENT_TYPE; - flatNode->header = { type, sizeof(*flatNode), 0 }; - flatNode->lineNumber = node->lineNumber; - flatNode->comment.index = -1; - - addString(node->namespaceUri, kLowPriority, &flatElem->ns); - addString(node->name, kLowPriority, &flatElem->name); - flatElem->attributeStart = sizeof(*flatElem); - flatElem->attributeSize = sizeof(android::ResXMLTree_attribute); - flatElem->attributeCount = node->attributes.size(); - - if (!writeAttributes(mOut, node, flatElem)) { - mError = true; - } - - mOut->align4(); - flatNode->header.size = (uint32_t)(mOut->size() - startIndex); - - for (const auto& child : node->children) { - child->accept(this); - } - - const size_t startEndIndex = mOut->size(); - android::ResXMLTree_node* flatEndNode = mOut->nextBlock<android::ResXMLTree_node>(); - android::ResXMLTree_endElementExt* flatEndElem = - mOut->nextBlock<android::ResXMLTree_endElementExt>(); - mOut->align4(); - - const uint16_t endType = android::RES_XML_END_ELEMENT_TYPE; - flatEndNode->header = { endType, sizeof(*flatEndNode), - (uint32_t)(mOut->size() - startEndIndex) }; - flatEndNode->lineNumber = node->lineNumber; - flatEndNode->comment.index = -1; - - addString(node->namespaceUri, kLowPriority, &flatEndElem->ns); - addString(node->name, kLowPriority, &flatEndElem->name); - } - - bool success() const { - return !mError; - } - -protected: - void addString(const StringPiece16& str, uint32_t priority, android::ResStringPool_ref* dest) { - if (!str.empty()) { - mStringRefs->emplace_back(mPool->makeRef(str, StringPool::Context{ priority }), dest); - } else { - // The device doesn't think a string of size 0 is the same as null. - dest->index = -1; - } - } - - void addString(const StringPool::Ref& ref, android::ResStringPool_ref* dest) { - mStringRefs->emplace_back(ref, dest); - } - - Maybe<std::u16string> getPackageAlias(const std::u16string& prefix) { - const auto endIter = mPackageAliases.rend(); - for (auto iter = mPackageAliases.rbegin(); iter != endIter; ++iter) { - if (iter->first == prefix) { - return iter->second; - } - } - return {}; - } - - const std::u16string& getDefaultPackage() const { - return mDefaultPackage; - } - - /** - * Subclasses override this to deal with attributes. Attributes can be flattened as - * raw values or as resources. - */ - virtual bool writeAttributes(BigBuffer* out, Element* node, - android::ResXMLTree_attrExt* flatElem) = 0; - -private: - BigBuffer* mOut; - StringPool* mPool; - FlatStringRefList* mStringRefs; - std::u16string mDefaultPackage; - bool mError = false; - std::vector<std::pair<std::u16string, std::u16string>> mPackageAliases; -}; - -/** - * Flattens XML, encoding the attributes as raw strings. This is used in the compile phase. - */ -struct CompileXmlFlattener : public XmlFlattener { - CompileXmlFlattener(BigBuffer* outBuffer, StringPool* pool, FlatStringRefList* stringRefs, - const std::u16string& defaultPackage) : - XmlFlattener(outBuffer, pool, stringRefs, defaultPackage) { - } - - virtual bool writeAttributes(BigBuffer* out, Element* node, - android::ResXMLTree_attrExt* flatElem) override { - flatElem->attributeCount = node->attributes.size(); - if (node->attributes.empty()) { - return true; - } - - android::ResXMLTree_attribute* flatAttrs = out->nextBlock<android::ResXMLTree_attribute>( - node->attributes.size()); - for (const Attribute& attr : node->attributes) { - addString(attr.namespaceUri, kLowPriority, &flatAttrs->ns); - addString(attr.name, kLowPriority, &flatAttrs->name); - addString(attr.value, kLowPriority, &flatAttrs->rawValue); - flatAttrs++; - } - return true; - } -}; - -struct AttributeToFlatten { - uint32_t resourceId = 0; - const Attribute* xmlAttr = nullptr; - const ::aapt::Attribute* resourceAttr = nullptr; -}; - -static bool lessAttributeId(const AttributeToFlatten& a, uint32_t id) { - return a.resourceId < id; -} - -/** - * Flattens XML, encoding the attributes as resources. - */ -struct LinkedXmlFlattener : public XmlFlattener { - LinkedXmlFlattener(BigBuffer* outBuffer, StringPool* pool, - std::map<std::u16string, StringPool>* packagePools, - FlatStringRefList* stringRefs, - const std::u16string& defaultPackage, - const std::shared_ptr<IResolver>& resolver, - SourceLogger* logger, - const FlattenOptions& options) : - XmlFlattener(outBuffer, pool, stringRefs, defaultPackage), mResolver(resolver), - mLogger(logger), mPackagePools(packagePools), mOptions(options) { - } - - virtual bool writeAttributes(BigBuffer* out, Element* node, - android::ResXMLTree_attrExt* flatElem) override { - bool error = false; - std::vector<AttributeToFlatten> sortedAttributes; - uint32_t nextAttributeId = 0x80000000u; - - // Sort and filter attributes by their resource ID. - for (const Attribute& attr : node->attributes) { - AttributeToFlatten attrToFlatten; - attrToFlatten.xmlAttr = &attr; - - Maybe<std::u16string> package = util::extractPackageFromNamespace(attr.namespaceUri); - if (package) { - // Find the Attribute object via our Resolver. - ResourceName attrName = { package.value(), ResourceType::kAttr, attr.name }; - if (attrName.package.empty()) { - attrName.package = getDefaultPackage(); - } - - Maybe<IResolver::Entry> result = mResolver->findAttribute(attrName); - if (!result || !result.value().id.isValid() || !result.value().attr) { - error = true; - mLogger->error(node->lineNumber) - << "unresolved attribute '" << attrName << "'." - << std::endl; - } else { - attrToFlatten.resourceId = result.value().id.id; - attrToFlatten.resourceAttr = result.value().attr; - - size_t sdk = findAttributeSdkLevel(attrToFlatten.resourceId); - if (mOptions.maxSdkAttribute && sdk > mOptions.maxSdkAttribute.value()) { - // We need to filter this attribute out. - mSmallestFilteredSdk = std::min(mSmallestFilteredSdk, sdk); - continue; - } - } - } - - if (attrToFlatten.resourceId == 0) { - // Attributes that have no resource ID (because they don't belong to a - // package) should appear after those that do have resource IDs. Assign - // them some integer value that will appear after. - attrToFlatten.resourceId = nextAttributeId++; - } - - // Insert the attribute into the sorted vector. - auto iter = std::lower_bound(sortedAttributes.begin(), sortedAttributes.end(), - attrToFlatten.resourceId, lessAttributeId); - sortedAttributes.insert(iter, std::move(attrToFlatten)); - } - - flatElem->attributeCount = sortedAttributes.size(); - if (sortedAttributes.empty()) { - return true; - } - - android::ResXMLTree_attribute* flatAttr = out->nextBlock<android::ResXMLTree_attribute>( - sortedAttributes.size()); - - // Now that we have sorted the attributes into their final encoded order, it's time - // to actually write them out. - uint16_t attributeIndex = 1; - for (const AttributeToFlatten& attrToFlatten : sortedAttributes) { - Maybe<std::u16string> package = util::extractPackageFromNamespace( - attrToFlatten.xmlAttr->namespaceUri); - - // Assign the indices for specific attributes. - if (package && package.value() == u"android" && attrToFlatten.xmlAttr->name == u"id") { - flatElem->idIndex = attributeIndex; - } else if (attrToFlatten.xmlAttr->namespaceUri.empty()) { - if (attrToFlatten.xmlAttr->name == u"class") { - flatElem->classIndex = attributeIndex; - } else if (attrToFlatten.xmlAttr->name == u"style") { - flatElem->styleIndex = attributeIndex; - } - } - attributeIndex++; - - // Add the namespaceUri and name to the list of StringRefs to encode. - addString(attrToFlatten.xmlAttr->namespaceUri, kLowPriority, &flatAttr->ns); - flatAttr->rawValue.index = -1; - - if (!attrToFlatten.resourceAttr) { - addString(attrToFlatten.xmlAttr->name, kLowPriority, &flatAttr->name); - } else { - // We've already extracted the package successfully before. - assert(package); - - // Attribute names are stored without packages, but we use - // their StringPool index to lookup their resource IDs. - // This will cause collisions, so we can't dedupe - // attribute names from different packages. We use separate - // pools that we later combine. - // - // Lookup the StringPool for this package and make the reference there. - StringPool::Ref nameRef = (*mPackagePools)[package.value()].makeRef( - attrToFlatten.xmlAttr->name, - StringPool::Context{ attrToFlatten.resourceId }); - - // Add it to the list of strings to flatten. - addString(nameRef, &flatAttr->name); - - if (mOptions.keepRawValues) { - // Keep raw values (this is for static libraries). - // TODO(with a smarter inflater for binary XML, we can do without this). - addString(attrToFlatten.xmlAttr->value, kLowPriority, &flatAttr->rawValue); - } - } - - error |= !flattenItem(node, attrToFlatten.xmlAttr->value, attrToFlatten.resourceAttr, - flatAttr); - flatAttr->typedValue.size = sizeof(flatAttr->typedValue); - flatAttr++; - } - return !error; - } - - Maybe<size_t> getSmallestFilteredSdk() const { - if (mSmallestFilteredSdk == std::numeric_limits<size_t>::max()) { - return {}; - } - return mSmallestFilteredSdk; - } - -private: - bool flattenItem(const Node* el, const std::u16string& value, const ::aapt::Attribute* attr, - android::ResXMLTree_attribute* flatAttr) { - std::unique_ptr<Item> item; - if (!attr) { - bool create = false; - item = ResourceParser::tryParseReference(value, &create); - if (!item) { - flatAttr->typedValue.dataType = android::Res_value::TYPE_STRING; - addString(value, kLowPriority, &flatAttr->rawValue); - addString(value, kLowPriority, reinterpret_cast<android::ResStringPool_ref*>( - &flatAttr->typedValue.data)); - return true; - } - } else { - item = ResourceParser::parseItemForAttribute(value, *attr); - if (!item) { - if (!(attr->typeMask & android::ResTable_map::TYPE_STRING)) { - mLogger->error(el->lineNumber) - << "'" - << value - << "' is not compatible with attribute '" - << *attr - << "'." - << std::endl; - return false; - } - - flatAttr->typedValue.dataType = android::Res_value::TYPE_STRING; - addString(value, kLowPriority, &flatAttr->rawValue); - addString(value, kLowPriority, reinterpret_cast<android::ResStringPool_ref*>( - &flatAttr->typedValue.data)); - return true; - } - } - - assert(item); - - bool error = false; - - // If this is a reference, resolve the name into an ID. - visitFunc<Reference>(*item, [&](Reference& reference) { - // First see if we can convert the package name from a prefix to a real - // package name. - ResourceName realName = reference.name; - if (!realName.package.empty()) { - Maybe<std::u16string> package = getPackageAlias(realName.package); - if (package) { - realName.package = package.value(); - } - } else { - realName.package = getDefaultPackage(); - } - - Maybe<ResourceId> result = mResolver->findId(realName); - if (!result || !result.value().isValid()) { - std::ostream& out = mLogger->error(el->lineNumber) - << "unresolved reference '" - << reference.name - << "'"; - if (realName != reference.name) { - out << " (aka '" << realName << "')"; - } - out << "'." << std::endl; - error = true; - } else { - reference.id = result.value(); - } - }); - - if (error) { - return false; - } - - item->flatten(flatAttr->typedValue); - return true; - } - - std::shared_ptr<IResolver> mResolver; - SourceLogger* mLogger; - std::map<std::u16string, StringPool>* mPackagePools; - FlattenOptions mOptions; - size_t mSmallestFilteredSdk = std::numeric_limits<size_t>::max(); -}; - -/** - * The binary XML file expects the StringPool to appear first, but we haven't collected the - * strings yet. We write to a temporary BigBuffer while parsing the input, adding strings - * we encounter to the StringPool. At the end, we write the StringPool to the given BigBuffer and - * then move the data from the temporary BigBuffer into the given one. This incurs no - * copies as the given BigBuffer simply takes ownership of the data. - */ -static void flattenXml(StringPool* pool, FlatStringRefList* stringRefs, BigBuffer* outBuffer, - BigBuffer&& xmlTreeBuffer) { - // Sort the string pool so that attribute resource IDs show up first. - pool->sort([](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { - return a.context.priority < b.context.priority; - }); - - // Now we flatten the string pool references into the correct places. - for (const auto& refEntry : *stringRefs) { - refEntry.second->index = refEntry.first.getIndex(); - } - - // Write the XML header. - const size_t beforeXmlTreeIndex = outBuffer->size(); - android::ResXMLTree_header* header = outBuffer->nextBlock<android::ResXMLTree_header>(); - header->header.type = android::RES_XML_TYPE; - header->header.headerSize = sizeof(*header); - - // Flatten the StringPool. - StringPool::flattenUtf16(outBuffer, *pool); - - // Write the array of resource IDs, indexed by StringPool order. - const size_t beforeResIdMapIndex = outBuffer->size(); - android::ResChunk_header* resIdMapChunk = outBuffer->nextBlock<android::ResChunk_header>(); - resIdMapChunk->type = android::RES_XML_RESOURCE_MAP_TYPE; - resIdMapChunk->headerSize = sizeof(*resIdMapChunk); - for (const auto& str : *pool) { - ResourceId id { str->context.priority }; - if (id.id == kLowPriority || !id.isValid()) { - // When we see the first non-resource ID, - // we're done. - break; - } - - *outBuffer->nextBlock<uint32_t>() = id.id; - } - resIdMapChunk->size = outBuffer->size() - beforeResIdMapIndex; - - // Move the temporary BigBuffer into outBuffer. - outBuffer->appendBuffer(std::move(xmlTreeBuffer)); - header->header.size = outBuffer->size() - beforeXmlTreeIndex; -} - -bool flatten(Node* root, const std::u16string& defaultPackage, BigBuffer* outBuffer) { - StringPool pool; - - // This will hold the StringRefs and the location in which to write the index. - // Once we sort the StringPool, we can assign the updated indices - // to the correct data locations. - FlatStringRefList stringRefs; - - // Since we don't know the size of the final StringPool, we write to this - // temporary BigBuffer, which we will append to outBuffer later. - BigBuffer out(1024); - - CompileXmlFlattener flattener(&out, &pool, &stringRefs, defaultPackage); - root->accept(&flattener); - - if (!flattener.success()) { - return false; - } - - flattenXml(&pool, &stringRefs, outBuffer, std::move(out)); - return true; -}; - -Maybe<size_t> flattenAndLink(const Source& source, Node* root, - const std::u16string& defaultPackage, - const std::shared_ptr<IResolver>& resolver, - const FlattenOptions& options, BigBuffer* outBuffer) { - SourceLogger logger(source); - StringPool pool; - - // Attribute names are stored without packages, but we use - // their StringPool index to lookup their resource IDs. - // This will cause collisions, so we can't dedupe - // attribute names from different packages. We use separate - // pools that we later combine. - std::map<std::u16string, StringPool> packagePools; - - FlatStringRefList stringRefs; - - // Since we don't know the size of the final StringPool, we write to this - // temporary BigBuffer, which we will append to outBuffer later. - BigBuffer out(1024); - - LinkedXmlFlattener flattener(&out, &pool, &packagePools, &stringRefs, defaultPackage, resolver, - &logger, options); - root->accept(&flattener); - - if (!flattener.success()) { - return {}; - } - - // Merge the package pools into the main pool. - for (auto& packagePoolEntry : packagePools) { - pool.merge(std::move(packagePoolEntry.second)); - } - - flattenXml(&pool, &stringRefs, outBuffer, std::move(out)); - - if (flattener.getSmallestFilteredSdk()) { - return flattener.getSmallestFilteredSdk(); - } - return 0; -} - -} // namespace xml -} // namespace aapt diff --git a/tools/aapt2/XmlFlattener.h b/tools/aapt2/XmlFlattener.h deleted file mode 100644 index 4ece0a37869d..000000000000 --- a/tools/aapt2/XmlFlattener.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef AAPT_XML_FLATTENER_H -#define AAPT_XML_FLATTENER_H - -#include "BigBuffer.h" -#include "Maybe.h" -#include "Resolver.h" -#include "Source.h" -#include "XmlDom.h" - -#include <string> - -namespace aapt { -namespace xml { - -/** - * Flattens an XML file into a binary representation parseable by - * the Android resource system. - */ -bool flatten(Node* root, const std::u16string& defaultPackage, BigBuffer* outBuffer); - -/** - * Options for flattenAndLink. - */ -struct FlattenOptions { - /** - * Keep attribute raw string values along with typed values. - */ - bool keepRawValues = false; - - /** - * If set, any attribute introduced in a later SDK will not be encoded. - */ - Maybe<size_t> maxSdkAttribute; -}; - -/** - * Like flatten(Node*,BigBuffer*), but references to resources are checked - * and string values are transformed to typed data where possible. - * - * `defaultPackage` is used when a reference has no package or the namespace URI - * "http://schemas.android.com/apk/res-auto" is used. - * - * `resolver` is used to resolve references to resources. - */ -Maybe<size_t> flattenAndLink(const Source& source, Node* root, - const std::u16string& defaultPackage, - const std::shared_ptr<IResolver>& resolver, - const FlattenOptions& options, BigBuffer* outBuffer); - -} // namespace xml -} // namespace aapt - -#endif // AAPT_XML_FLATTENER_H diff --git a/tools/aapt2/XmlFlattener_test.cpp b/tools/aapt2/XmlFlattener_test.cpp deleted file mode 100644 index 8915d2478b64..000000000000 --- a/tools/aapt2/XmlFlattener_test.cpp +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "MockResolver.h" -#include "ResourceTable.h" -#include "ResourceValues.h" -#include "Util.h" -#include "XmlFlattener.h" - -#include <androidfw/AssetManager.h> -#include <androidfw/ResourceTypes.h> -#include <gtest/gtest.h> -#include <sstream> -#include <string> - -using namespace android; - -namespace aapt { -namespace xml { - -constexpr const char* kXmlPreamble = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; - -class XmlFlattenerTest : public ::testing::Test { -public: - virtual void SetUp() override { - mResolver = std::make_shared<MockResolver>( - std::make_shared<ResourceTable>(), - std::map<ResourceName, ResourceId>({ - { ResourceName{ u"android", ResourceType::kAttr, u"attr" }, - ResourceId{ 0x01010000u } }, - { ResourceName{ u"android", ResourceType::kId, u"id" }, - ResourceId{ 0x01020000u } }, - { ResourceName{ u"com.lib", ResourceType::kAttr, u"attr" }, - ResourceId{ 0x01010001u } }, - { ResourceName{ u"com.lib", ResourceType::kId, u"id" }, - ResourceId{ 0x01020001u } }})); - } - - ::testing::AssertionResult testFlatten(const std::string& in, ResXMLTree* outTree) { - std::stringstream input(kXmlPreamble); - input << in << std::endl; - - SourceLogger logger(Source{ "test.xml" }); - std::unique_ptr<Node> root = inflate(&input, &logger); - if (!root) { - return ::testing::AssertionFailure(); - } - - BigBuffer outBuffer(1024); - if (!flattenAndLink(Source{ "test.xml" }, root.get(), std::u16string(u"android"), - mResolver, {}, &outBuffer)) { - return ::testing::AssertionFailure(); - } - - std::unique_ptr<uint8_t[]> data = util::copy(outBuffer); - if (outTree->setTo(data.get(), outBuffer.size(), true) != NO_ERROR) { - return ::testing::AssertionFailure(); - } - return ::testing::AssertionSuccess(); - } - - std::shared_ptr<IResolver> mResolver; -}; - -TEST_F(XmlFlattenerTest, ParseSimpleView) { - std::string input = R"EOF( - <View xmlns:android="http://schemas.android.com/apk/res/android" - android:attr="@id/id" - class="str" - style="@id/id"> - </View> - )EOF"; - ResXMLTree tree; - ASSERT_TRUE(testFlatten(input, &tree)); - - while (tree.next() != ResXMLTree::START_TAG) { - ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); - ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); - } - - const StringPiece16 androidNs = u"http://schemas.android.com/apk/res/android"; - const StringPiece16 attrName = u"attr"; - ssize_t idx = tree.indexOfAttribute(androidNs.data(), androidNs.size(), attrName.data(), - attrName.size()); - ASSERT_GE(idx, 0); - EXPECT_EQ(tree.getAttributeNameResID(idx), 0x01010000u); - EXPECT_EQ(tree.getAttributeDataType(idx), android::Res_value::TYPE_REFERENCE); - - const StringPiece16 class16 = u"class"; - idx = tree.indexOfAttribute(nullptr, 0, class16.data(), class16.size()); - ASSERT_GE(idx, 0); - EXPECT_EQ(tree.getAttributeNameResID(idx), 0u); - EXPECT_EQ(tree.getAttributeDataType(idx), android::Res_value::TYPE_STRING); - EXPECT_EQ(tree.getAttributeData(idx), tree.getAttributeValueStringID(idx)); - - const StringPiece16 style16 = u"style"; - idx = tree.indexOfAttribute(nullptr, 0, style16.data(), style16.size()); - ASSERT_GE(idx, 0); - EXPECT_EQ(tree.getAttributeNameResID(idx), 0u); - EXPECT_EQ(tree.getAttributeDataType(idx), android::Res_value::TYPE_REFERENCE); - EXPECT_EQ((uint32_t) tree.getAttributeData(idx), 0x01020000u); - EXPECT_EQ(tree.getAttributeValueStringID(idx), -1); - - while (tree.next() != ResXMLTree::END_DOCUMENT) { - ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); - } -} - -TEST_F(XmlFlattenerTest, ParseViewWithPackageAlias) { - std::string input = "<View xmlns:ns1=\"http://schemas.android.com/apk/res/android\"\n" - " xmlns:ns2=\"http://schemas.android.com/apk/res/android\"\n" - " ns1:attr=\"@ns2:id/id\">\n" - "</View>"; - ResXMLTree tree; - ASSERT_TRUE(testFlatten(input, &tree)); - - while (tree.next() != ResXMLTree::END_DOCUMENT) { - ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); - } -} - -::testing::AssertionResult attributeNameAndValueEquals(ResXMLTree* tree, size_t index, - ResourceId nameId, ResourceId valueId) { - if (index >= tree->getAttributeCount()) { - return ::testing::AssertionFailure() << "index " << index << " is out of bounds (" - << tree->getAttributeCount() << ")"; - } - - if (tree->getAttributeNameResID(index) != nameId.id) { - return ::testing::AssertionFailure() - << "attribute at index " << index << " has ID " - << ResourceId{ (uint32_t) tree->getAttributeNameResID(index) } - << ". Expected ID " << nameId; - } - - if (tree->getAttributeDataType(index) != Res_value::TYPE_REFERENCE) { - return ::testing::AssertionFailure() << "attribute at index " << index << " has value of " - << "type " << std::hex - << tree->getAttributeDataType(index) << std::dec - << ". Expected reference (" << std::hex - << Res_value::TYPE_REFERENCE << std::dec << ")"; - } - - if ((uint32_t) tree->getAttributeData(index) != valueId.id) { - return ::testing::AssertionFailure() - << "attribute at index " << index << " has value " << "with ID " - << ResourceId{ (uint32_t) tree->getAttributeData(index) } - << ". Expected ID " << valueId; - } - return ::testing::AssertionSuccess(); -} - -TEST_F(XmlFlattenerTest, ParseViewWithShadowedPackageAlias) { - std::string input = "<View xmlns:app=\"http://schemas.android.com/apk/res/android\"\n" - " app:attr=\"@app:id/id\">\n" - " <View xmlns:app=\"http://schemas.android.com/apk/res/com.lib\"\n" - " app:attr=\"@app:id/id\"/>\n" - "</View>"; - ResXMLTree tree; - ASSERT_TRUE(testFlatten(input, &tree)); - - while (tree.next() != ResXMLTree::START_TAG) { - ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); - ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); - } - - ASSERT_TRUE(attributeNameAndValueEquals(&tree, 0u, ResourceId{ 0x01010000u }, - ResourceId{ 0x01020000u })); - - while (tree.next() != ResXMLTree::START_TAG) { - ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); - ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); - } - - ASSERT_TRUE(attributeNameAndValueEquals(&tree, 0u, ResourceId{ 0x01010001u }, - ResourceId{ 0x01020001u })); -} - -TEST_F(XmlFlattenerTest, ParseViewWithLocalPackageAndAliasOfTheSameName) { - std::string input = "<View xmlns:android=\"http://schemas.android.com/apk/res/com.lib\"\n" - " android:attr=\"@id/id\"/>"; - ResXMLTree tree; - ASSERT_TRUE(testFlatten(input, &tree)); - - while (tree.next() != ResXMLTree::START_TAG) { - ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); - ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); - } - - // We expect the 'android:attr' to be converted to 'com.lib:attr' due to the namespace - // assignment. - // However, we didn't give '@id/id' a package, so it should use the default package - // 'android', and not be converted from 'android' to 'com.lib'. - ASSERT_TRUE(attributeNameAndValueEquals(&tree, 0u, ResourceId{ 0x01010001u }, - ResourceId{ 0x01020000u })); -} - -/* - * The device ResXMLParser in libandroidfw differentiates between empty namespace and null - * namespace. - */ -TEST_F(XmlFlattenerTest, NoNamespaceIsNotTheSameAsEmptyNamespace) { - std::string input = "<View xmlns:android=\"http://schemas.android.com/apk/res/android\"\n" - " package=\"android\"/>"; - - ResXMLTree tree; - ASSERT_TRUE(testFlatten(input, &tree)); - - while (tree.next() != ResXMLTree::START_TAG) { - ASSERT_NE(tree.getEventType(), ResXMLTree::BAD_DOCUMENT); - ASSERT_NE(tree.getEventType(), ResXMLTree::END_DOCUMENT); - } - - const StringPiece16 kPackage = u"package"; - EXPECT_GE(tree.indexOfAttribute(nullptr, 0, kPackage.data(), kPackage.size()), 0); -} - -} // namespace xml -} // namespace aapt diff --git a/tools/aapt2/ZipEntry.cpp b/tools/aapt2/ZipEntry.cpp deleted file mode 100644 index 891b4e1e2d3c..000000000000 --- a/tools/aapt2/ZipEntry.cpp +++ /dev/null @@ -1,745 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// -// Access to entries in a Zip archive. -// - -#define LOG_TAG "zip" - -#include "ZipEntry.h" -#include <utils/Log.h> - -#include <stdio.h> -#include <string.h> -#include <assert.h> - -namespace aapt { - -using namespace android; - -/* - * Initialize a new ZipEntry structure from a FILE* positioned at a - * CentralDirectoryEntry. - * - * On exit, the file pointer will be at the start of the next CDE or - * at the EOCD. - */ -status_t ZipEntry::initFromCDE(FILE* fp) -{ - status_t result; - long posn; - bool hasDD; - - //ALOGV("initFromCDE ---\n"); - - /* read the CDE */ - result = mCDE.read(fp); - if (result != NO_ERROR) { - ALOGD("mCDE.read failed\n"); - return result; - } - - //mCDE.dump(); - - /* using the info in the CDE, go load up the LFH */ - posn = ftell(fp); - if (fseek(fp, mCDE.mLocalHeaderRelOffset, SEEK_SET) != 0) { - ALOGD("local header seek failed (%ld)\n", - mCDE.mLocalHeaderRelOffset); - return UNKNOWN_ERROR; - } - - result = mLFH.read(fp); - if (result != NO_ERROR) { - ALOGD("mLFH.read failed\n"); - return result; - } - - if (fseek(fp, posn, SEEK_SET) != 0) - return UNKNOWN_ERROR; - - //mLFH.dump(); - - /* - * We *might* need to read the Data Descriptor at this point and - * integrate it into the LFH. If this bit is set, the CRC-32, - * compressed size, and uncompressed size will be zero. In practice - * these seem to be rare. - */ - hasDD = (mLFH.mGPBitFlag & kUsesDataDescr) != 0; - if (hasDD) { - // do something clever - //ALOGD("+++ has data descriptor\n"); - } - - /* - * Sanity-check the LFH. Note that this will fail if the "kUsesDataDescr" - * flag is set, because the LFH is incomplete. (Not a problem, since we - * prefer the CDE values.) - */ - if (!hasDD && !compareHeaders()) { - ALOGW("warning: header mismatch\n"); - // keep going? - } - - /* - * If the mVersionToExtract is greater than 20, we may have an - * issue unpacking the record -- could be encrypted, compressed - * with something we don't support, or use Zip64 extensions. We - * can defer worrying about that to when we're extracting data. - */ - - return NO_ERROR; -} - -/* - * Initialize a new entry. Pass in the file name and an optional comment. - * - * Initializes the CDE and the LFH. - */ -void ZipEntry::initNew(const char* fileName, const char* comment) -{ - assert(fileName != NULL && *fileName != '\0'); // name required - - /* most fields are properly initialized by constructor */ - mCDE.mVersionMadeBy = kDefaultMadeBy; - mCDE.mVersionToExtract = kDefaultVersion; - mCDE.mCompressionMethod = kCompressStored; - mCDE.mFileNameLength = strlen(fileName); - if (comment != NULL) - mCDE.mFileCommentLength = strlen(comment); - mCDE.mExternalAttrs = 0x81b60020; // matches what WinZip does - - if (mCDE.mFileNameLength > 0) { - mCDE.mFileName = new unsigned char[mCDE.mFileNameLength+1]; - strcpy((char*) mCDE.mFileName, fileName); - } - if (mCDE.mFileCommentLength > 0) { - /* TODO: stop assuming null-terminated ASCII here? */ - mCDE.mFileComment = new unsigned char[mCDE.mFileCommentLength+1]; - strcpy((char*) mCDE.mFileComment, comment); - } - - copyCDEtoLFH(); -} - -/* - * Initialize a new entry, starting with the ZipEntry from a different - * archive. - * - * Initializes the CDE and the LFH. - */ -status_t ZipEntry::initFromExternal(const ZipFile* /* pZipFile */, - const ZipEntry* pEntry, const char* storageName) -{ - mCDE = pEntry->mCDE; - if (storageName && *storageName != 0) { - mCDE.mFileNameLength = strlen(storageName); - mCDE.mFileName = new unsigned char[mCDE.mFileNameLength + 1]; - strcpy((char*) mCDE.mFileName, storageName); - } - - // Check whether we got all the memory needed. - if ((mCDE.mFileNameLength > 0 && mCDE.mFileName == NULL) || - (mCDE.mFileCommentLength > 0 && mCDE.mFileComment == NULL) || - (mCDE.mExtraFieldLength > 0 && mCDE.mExtraField == NULL)) { - return NO_MEMORY; - } - - /* construct the LFH from the CDE */ - copyCDEtoLFH(); - - /* - * The LFH "extra" field is independent of the CDE "extra", so we - * handle it here. - */ - assert(mLFH.mExtraField == NULL); - mLFH.mExtraFieldLength = pEntry->mLFH.mExtraFieldLength; - if (mLFH.mExtraFieldLength > 0) { - mLFH.mExtraField = new unsigned char[mLFH.mExtraFieldLength+1]; - if (mLFH.mExtraField == NULL) - return NO_MEMORY; - memcpy(mLFH.mExtraField, pEntry->mLFH.mExtraField, - mLFH.mExtraFieldLength+1); - } - - return NO_ERROR; -} - -/* - * Insert pad bytes in the LFH by tweaking the "extra" field. This will - * potentially confuse something that put "extra" data in here earlier, - * but I can't find an actual problem. - */ -status_t ZipEntry::addPadding(int padding) -{ - if (padding <= 0) - return INVALID_OPERATION; - - //ALOGI("HEY: adding %d pad bytes to existing %d in %s\n", - // padding, mLFH.mExtraFieldLength, mCDE.mFileName); - - if (mLFH.mExtraFieldLength > 0) { - /* extend existing field */ - unsigned char* newExtra; - - newExtra = new unsigned char[mLFH.mExtraFieldLength + padding]; - if (newExtra == NULL) - return NO_MEMORY; - memset(newExtra + mLFH.mExtraFieldLength, 0, padding); - memcpy(newExtra, mLFH.mExtraField, mLFH.mExtraFieldLength); - - delete[] mLFH.mExtraField; - mLFH.mExtraField = newExtra; - mLFH.mExtraFieldLength += padding; - } else { - /* create new field */ - mLFH.mExtraField = new unsigned char[padding]; - memset(mLFH.mExtraField, 0, padding); - mLFH.mExtraFieldLength = padding; - } - - return NO_ERROR; -} - -/* - * Set the fields in the LFH equal to the corresponding fields in the CDE. - * - * This does not touch the LFH "extra" field. - */ -void ZipEntry::copyCDEtoLFH(void) -{ - mLFH.mVersionToExtract = mCDE.mVersionToExtract; - mLFH.mGPBitFlag = mCDE.mGPBitFlag; - mLFH.mCompressionMethod = mCDE.mCompressionMethod; - mLFH.mLastModFileTime = mCDE.mLastModFileTime; - mLFH.mLastModFileDate = mCDE.mLastModFileDate; - mLFH.mCRC32 = mCDE.mCRC32; - mLFH.mCompressedSize = mCDE.mCompressedSize; - mLFH.mUncompressedSize = mCDE.mUncompressedSize; - mLFH.mFileNameLength = mCDE.mFileNameLength; - // the "extra field" is independent - - delete[] mLFH.mFileName; - if (mLFH.mFileNameLength > 0) { - mLFH.mFileName = new unsigned char[mLFH.mFileNameLength+1]; - strcpy((char*) mLFH.mFileName, (const char*) mCDE.mFileName); - } else { - mLFH.mFileName = NULL; - } -} - -/* - * Set some information about a file after we add it. - */ -void ZipEntry::setDataInfo(long uncompLen, long compLen, unsigned long crc32, - int compressionMethod) -{ - mCDE.mCompressionMethod = compressionMethod; - mCDE.mCRC32 = crc32; - mCDE.mCompressedSize = compLen; - mCDE.mUncompressedSize = uncompLen; - mCDE.mCompressionMethod = compressionMethod; - if (compressionMethod == kCompressDeflated) { - mCDE.mGPBitFlag |= 0x0002; // indicates maximum compression used - } - copyCDEtoLFH(); -} - -/* - * See if the data in mCDE and mLFH match up. This is mostly useful for - * debugging these classes, but it can be used to identify damaged - * archives. - * - * Returns "false" if they differ. - */ -bool ZipEntry::compareHeaders(void) const -{ - if (mCDE.mVersionToExtract != mLFH.mVersionToExtract) { - ALOGV("cmp: VersionToExtract\n"); - return false; - } - if (mCDE.mGPBitFlag != mLFH.mGPBitFlag) { - ALOGV("cmp: GPBitFlag\n"); - return false; - } - if (mCDE.mCompressionMethod != mLFH.mCompressionMethod) { - ALOGV("cmp: CompressionMethod\n"); - return false; - } - if (mCDE.mLastModFileTime != mLFH.mLastModFileTime) { - ALOGV("cmp: LastModFileTime\n"); - return false; - } - if (mCDE.mLastModFileDate != mLFH.mLastModFileDate) { - ALOGV("cmp: LastModFileDate\n"); - return false; - } - if (mCDE.mCRC32 != mLFH.mCRC32) { - ALOGV("cmp: CRC32\n"); - return false; - } - if (mCDE.mCompressedSize != mLFH.mCompressedSize) { - ALOGV("cmp: CompressedSize\n"); - return false; - } - if (mCDE.mUncompressedSize != mLFH.mUncompressedSize) { - ALOGV("cmp: UncompressedSize\n"); - return false; - } - if (mCDE.mFileNameLength != mLFH.mFileNameLength) { - ALOGV("cmp: FileNameLength\n"); - return false; - } -#if 0 // this seems to be used for padding, not real data - if (mCDE.mExtraFieldLength != mLFH.mExtraFieldLength) { - ALOGV("cmp: ExtraFieldLength\n"); - return false; - } -#endif - if (mCDE.mFileName != NULL) { - if (strcmp((char*) mCDE.mFileName, (char*) mLFH.mFileName) != 0) { - ALOGV("cmp: FileName\n"); - return false; - } - } - - return true; -} - - -/* - * Convert the DOS date/time stamp into a UNIX time stamp. - */ -time_t ZipEntry::getModWhen(void) const -{ - struct tm parts; - - parts.tm_sec = (mCDE.mLastModFileTime & 0x001f) << 1; - parts.tm_min = (mCDE.mLastModFileTime & 0x07e0) >> 5; - parts.tm_hour = (mCDE.mLastModFileTime & 0xf800) >> 11; - parts.tm_mday = (mCDE.mLastModFileDate & 0x001f); - parts.tm_mon = ((mCDE.mLastModFileDate & 0x01e0) >> 5) -1; - parts.tm_year = ((mCDE.mLastModFileDate & 0xfe00) >> 9) + 80; - parts.tm_wday = parts.tm_yday = 0; - parts.tm_isdst = -1; // DST info "not available" - - return mktime(&parts); -} - -/* - * Set the CDE/LFH timestamp from UNIX time. - */ -void ZipEntry::setModWhen(time_t when) -{ -#if !defined(_WIN32) - struct tm tmResult; -#endif - time_t even; - unsigned short zdate, ztime; - - struct tm* ptm; - - /* round up to an even number of seconds */ - even = (time_t)(((unsigned long)(when) + 1) & (~1)); - - /* expand */ -#if !defined(_WIN32) - ptm = localtime_r(&even, &tmResult); -#else - ptm = localtime(&even); -#endif - - int year; - year = ptm->tm_year; - if (year < 80) - year = 80; - - zdate = (year - 80) << 9 | (ptm->tm_mon+1) << 5 | ptm->tm_mday; - ztime = ptm->tm_hour << 11 | ptm->tm_min << 5 | ptm->tm_sec >> 1; - - mCDE.mLastModFileTime = mLFH.mLastModFileTime = ztime; - mCDE.mLastModFileDate = mLFH.mLastModFileDate = zdate; -} - - -/* - * =========================================================================== - * ZipEntry::LocalFileHeader - * =========================================================================== - */ - -/* - * Read a local file header. - * - * On entry, "fp" points to the signature at the start of the header. - * On exit, "fp" points to the start of data. - */ -status_t ZipEntry::LocalFileHeader::read(FILE* fp) -{ - status_t result = NO_ERROR; - unsigned char buf[kLFHLen]; - - assert(mFileName == NULL); - assert(mExtraField == NULL); - - if (fread(buf, 1, kLFHLen, fp) != kLFHLen) { - result = UNKNOWN_ERROR; - goto bail; - } - - if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) { - ALOGD("whoops: didn't find expected signature\n"); - result = UNKNOWN_ERROR; - goto bail; - } - - mVersionToExtract = ZipEntry::getShortLE(&buf[0x04]); - mGPBitFlag = ZipEntry::getShortLE(&buf[0x06]); - mCompressionMethod = ZipEntry::getShortLE(&buf[0x08]); - mLastModFileTime = ZipEntry::getShortLE(&buf[0x0a]); - mLastModFileDate = ZipEntry::getShortLE(&buf[0x0c]); - mCRC32 = ZipEntry::getLongLE(&buf[0x0e]); - mCompressedSize = ZipEntry::getLongLE(&buf[0x12]); - mUncompressedSize = ZipEntry::getLongLE(&buf[0x16]); - mFileNameLength = ZipEntry::getShortLE(&buf[0x1a]); - mExtraFieldLength = ZipEntry::getShortLE(&buf[0x1c]); - - // TODO: validate sizes - - /* grab filename */ - if (mFileNameLength != 0) { - mFileName = new unsigned char[mFileNameLength+1]; - if (mFileName == NULL) { - result = NO_MEMORY; - goto bail; - } - if (fread(mFileName, 1, mFileNameLength, fp) != mFileNameLength) { - result = UNKNOWN_ERROR; - goto bail; - } - mFileName[mFileNameLength] = '\0'; - } - - /* grab extra field */ - if (mExtraFieldLength != 0) { - mExtraField = new unsigned char[mExtraFieldLength+1]; - if (mExtraField == NULL) { - result = NO_MEMORY; - goto bail; - } - if (fread(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) { - result = UNKNOWN_ERROR; - goto bail; - } - mExtraField[mExtraFieldLength] = '\0'; - } - -bail: - return result; -} - -/* - * Write a local file header. - */ -status_t ZipEntry::LocalFileHeader::write(FILE* fp) -{ - unsigned char buf[kLFHLen]; - - ZipEntry::putLongLE(&buf[0x00], kSignature); - ZipEntry::putShortLE(&buf[0x04], mVersionToExtract); - ZipEntry::putShortLE(&buf[0x06], mGPBitFlag); - ZipEntry::putShortLE(&buf[0x08], mCompressionMethod); - ZipEntry::putShortLE(&buf[0x0a], mLastModFileTime); - ZipEntry::putShortLE(&buf[0x0c], mLastModFileDate); - ZipEntry::putLongLE(&buf[0x0e], mCRC32); - ZipEntry::putLongLE(&buf[0x12], mCompressedSize); - ZipEntry::putLongLE(&buf[0x16], mUncompressedSize); - ZipEntry::putShortLE(&buf[0x1a], mFileNameLength); - ZipEntry::putShortLE(&buf[0x1c], mExtraFieldLength); - - if (fwrite(buf, 1, kLFHLen, fp) != kLFHLen) - return UNKNOWN_ERROR; - - /* write filename */ - if (mFileNameLength != 0) { - if (fwrite(mFileName, 1, mFileNameLength, fp) != mFileNameLength) - return UNKNOWN_ERROR; - } - - /* write "extra field" */ - if (mExtraFieldLength != 0) { - if (fwrite(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) - return UNKNOWN_ERROR; - } - - return NO_ERROR; -} - - -/* - * Dump the contents of a LocalFileHeader object. - */ -void ZipEntry::LocalFileHeader::dump(void) const -{ - ALOGD(" LocalFileHeader contents:\n"); - ALOGD(" versToExt=%u gpBits=0x%04x compression=%u\n", - mVersionToExtract, mGPBitFlag, mCompressionMethod); - ALOGD(" modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n", - mLastModFileTime, mLastModFileDate, mCRC32); - ALOGD(" compressedSize=%lu uncompressedSize=%lu\n", - mCompressedSize, mUncompressedSize); - ALOGD(" filenameLen=%u extraLen=%u\n", - mFileNameLength, mExtraFieldLength); - if (mFileName != NULL) - ALOGD(" filename: '%s'\n", mFileName); -} - - -/* - * =========================================================================== - * ZipEntry::CentralDirEntry - * =========================================================================== - */ - -/* - * Read the central dir entry that appears next in the file. - * - * On entry, "fp" should be positioned on the signature bytes for the - * entry. On exit, "fp" will point at the signature word for the next - * entry or for the EOCD. - */ -status_t ZipEntry::CentralDirEntry::read(FILE* fp) -{ - status_t result = NO_ERROR; - unsigned char buf[kCDELen]; - - /* no re-use */ - assert(mFileName == NULL); - assert(mExtraField == NULL); - assert(mFileComment == NULL); - - if (fread(buf, 1, kCDELen, fp) != kCDELen) { - result = UNKNOWN_ERROR; - goto bail; - } - - if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) { - ALOGD("Whoops: didn't find expected signature\n"); - result = UNKNOWN_ERROR; - goto bail; - } - - mVersionMadeBy = ZipEntry::getShortLE(&buf[0x04]); - mVersionToExtract = ZipEntry::getShortLE(&buf[0x06]); - mGPBitFlag = ZipEntry::getShortLE(&buf[0x08]); - mCompressionMethod = ZipEntry::getShortLE(&buf[0x0a]); - mLastModFileTime = ZipEntry::getShortLE(&buf[0x0c]); - mLastModFileDate = ZipEntry::getShortLE(&buf[0x0e]); - mCRC32 = ZipEntry::getLongLE(&buf[0x10]); - mCompressedSize = ZipEntry::getLongLE(&buf[0x14]); - mUncompressedSize = ZipEntry::getLongLE(&buf[0x18]); - mFileNameLength = ZipEntry::getShortLE(&buf[0x1c]); - mExtraFieldLength = ZipEntry::getShortLE(&buf[0x1e]); - mFileCommentLength = ZipEntry::getShortLE(&buf[0x20]); - mDiskNumberStart = ZipEntry::getShortLE(&buf[0x22]); - mInternalAttrs = ZipEntry::getShortLE(&buf[0x24]); - mExternalAttrs = ZipEntry::getLongLE(&buf[0x26]); - mLocalHeaderRelOffset = ZipEntry::getLongLE(&buf[0x2a]); - - // TODO: validate sizes and offsets - - /* grab filename */ - if (mFileNameLength != 0) { - mFileName = new unsigned char[mFileNameLength+1]; - if (mFileName == NULL) { - result = NO_MEMORY; - goto bail; - } - if (fread(mFileName, 1, mFileNameLength, fp) != mFileNameLength) { - result = UNKNOWN_ERROR; - goto bail; - } - mFileName[mFileNameLength] = '\0'; - } - - /* read "extra field" */ - if (mExtraFieldLength != 0) { - mExtraField = new unsigned char[mExtraFieldLength+1]; - if (mExtraField == NULL) { - result = NO_MEMORY; - goto bail; - } - if (fread(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) { - result = UNKNOWN_ERROR; - goto bail; - } - mExtraField[mExtraFieldLength] = '\0'; - } - - - /* grab comment, if any */ - if (mFileCommentLength != 0) { - mFileComment = new unsigned char[mFileCommentLength+1]; - if (mFileComment == NULL) { - result = NO_MEMORY; - goto bail; - } - if (fread(mFileComment, 1, mFileCommentLength, fp) != mFileCommentLength) - { - result = UNKNOWN_ERROR; - goto bail; - } - mFileComment[mFileCommentLength] = '\0'; - } - -bail: - return result; -} - -/* - * Write a central dir entry. - */ -status_t ZipEntry::CentralDirEntry::write(FILE* fp) -{ - unsigned char buf[kCDELen]; - - ZipEntry::putLongLE(&buf[0x00], kSignature); - ZipEntry::putShortLE(&buf[0x04], mVersionMadeBy); - ZipEntry::putShortLE(&buf[0x06], mVersionToExtract); - ZipEntry::putShortLE(&buf[0x08], mGPBitFlag); - ZipEntry::putShortLE(&buf[0x0a], mCompressionMethod); - ZipEntry::putShortLE(&buf[0x0c], mLastModFileTime); - ZipEntry::putShortLE(&buf[0x0e], mLastModFileDate); - ZipEntry::putLongLE(&buf[0x10], mCRC32); - ZipEntry::putLongLE(&buf[0x14], mCompressedSize); - ZipEntry::putLongLE(&buf[0x18], mUncompressedSize); - ZipEntry::putShortLE(&buf[0x1c], mFileNameLength); - ZipEntry::putShortLE(&buf[0x1e], mExtraFieldLength); - ZipEntry::putShortLE(&buf[0x20], mFileCommentLength); - ZipEntry::putShortLE(&buf[0x22], mDiskNumberStart); - ZipEntry::putShortLE(&buf[0x24], mInternalAttrs); - ZipEntry::putLongLE(&buf[0x26], mExternalAttrs); - ZipEntry::putLongLE(&buf[0x2a], mLocalHeaderRelOffset); - - if (fwrite(buf, 1, kCDELen, fp) != kCDELen) - return UNKNOWN_ERROR; - - /* write filename */ - if (mFileNameLength != 0) { - if (fwrite(mFileName, 1, mFileNameLength, fp) != mFileNameLength) - return UNKNOWN_ERROR; - } - - /* write "extra field" */ - if (mExtraFieldLength != 0) { - if (fwrite(mExtraField, 1, mExtraFieldLength, fp) != mExtraFieldLength) - return UNKNOWN_ERROR; - } - - /* write comment */ - if (mFileCommentLength != 0) { - if (fwrite(mFileComment, 1, mFileCommentLength, fp) != mFileCommentLength) - return UNKNOWN_ERROR; - } - - return NO_ERROR; -} - -/* - * Dump the contents of a CentralDirEntry object. - */ -void ZipEntry::CentralDirEntry::dump(void) const -{ - ALOGD(" CentralDirEntry contents:\n"); - ALOGD(" versMadeBy=%u versToExt=%u gpBits=0x%04x compression=%u\n", - mVersionMadeBy, mVersionToExtract, mGPBitFlag, mCompressionMethod); - ALOGD(" modTime=0x%04x modDate=0x%04x crc32=0x%08lx\n", - mLastModFileTime, mLastModFileDate, mCRC32); - ALOGD(" compressedSize=%lu uncompressedSize=%lu\n", - mCompressedSize, mUncompressedSize); - ALOGD(" filenameLen=%u extraLen=%u commentLen=%u\n", - mFileNameLength, mExtraFieldLength, mFileCommentLength); - ALOGD(" diskNumStart=%u intAttr=0x%04x extAttr=0x%08lx relOffset=%lu\n", - mDiskNumberStart, mInternalAttrs, mExternalAttrs, - mLocalHeaderRelOffset); - - if (mFileName != NULL) - ALOGD(" filename: '%s'\n", mFileName); - if (mFileComment != NULL) - ALOGD(" comment: '%s'\n", mFileComment); -} - -/* - * Copy-assignment operator for CentralDirEntry. - */ -ZipEntry::CentralDirEntry& ZipEntry::CentralDirEntry::operator=(const ZipEntry::CentralDirEntry& src) { - if (this == &src) { - return *this; - } - - // Free up old data. - delete[] mFileName; - delete[] mExtraField; - delete[] mFileComment; - - // Copy scalars. - mVersionMadeBy = src.mVersionMadeBy; - mVersionToExtract = src.mVersionToExtract; - mGPBitFlag = src.mGPBitFlag; - mCompressionMethod = src.mCompressionMethod; - mLastModFileTime = src.mLastModFileTime; - mLastModFileDate = src.mLastModFileDate; - mCRC32 = src.mCRC32; - mCompressedSize = src.mCompressedSize; - mUncompressedSize = src.mUncompressedSize; - mFileNameLength = src.mFileNameLength; - mExtraFieldLength = src.mExtraFieldLength; - mFileCommentLength = src.mFileCommentLength; - mDiskNumberStart = src.mDiskNumberStart; - mInternalAttrs = src.mInternalAttrs; - mExternalAttrs = src.mExternalAttrs; - mLocalHeaderRelOffset = src.mLocalHeaderRelOffset; - - // Copy strings, if necessary. - if (mFileNameLength > 0) { - mFileName = new unsigned char[mFileNameLength + 1]; - if (mFileName != NULL) - strcpy((char*)mFileName, (char*)src.mFileName); - } else { - mFileName = NULL; - } - if (mFileCommentLength > 0) { - mFileComment = new unsigned char[mFileCommentLength + 1]; - if (mFileComment != NULL) - strcpy((char*)mFileComment, (char*)src.mFileComment); - } else { - mFileComment = NULL; - } - if (mExtraFieldLength > 0) { - /* we null-terminate this, though it may not be a string */ - mExtraField = new unsigned char[mExtraFieldLength + 1]; - if (mExtraField != NULL) - memcpy(mExtraField, src.mExtraField, mExtraFieldLength + 1); - } else { - mExtraField = NULL; - } - - return *this; -} - -} // namespace aapt diff --git a/tools/aapt2/ZipEntry.h b/tools/aapt2/ZipEntry.h deleted file mode 100644 index 2745a4386278..000000000000 --- a/tools/aapt2/ZipEntry.h +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// -// Zip archive entries. -// -// The ZipEntry class is tightly meshed with the ZipFile class. -// -#ifndef __LIBS_ZIPENTRY_H -#define __LIBS_ZIPENTRY_H - -#include <utils/Errors.h> - -#include <stdlib.h> -#include <stdio.h> - -namespace aapt { - -using android::status_t; - -class ZipFile; - -/* - * ZipEntry objects represent a single entry in a Zip archive. - * - * You can use one of these to get or set information about an entry, but - * there are no functions here for accessing the data itself. (We could - * tuck a pointer to the ZipFile in here for convenience, but that raises - * the likelihood of using ZipEntry objects after discarding the ZipFile.) - * - * File information is stored in two places: next to the file data (the Local - * File Header, and possibly a Data Descriptor), and at the end of the file - * (the Central Directory Entry). The two must be kept in sync. - */ -class ZipEntry { -public: - friend class ZipFile; - - ZipEntry(void) - : mDeleted(false), mMarked(false) - {} - ~ZipEntry(void) {} - - /* - * Returns "true" if the data is compressed. - */ - bool isCompressed(void) const { - return mCDE.mCompressionMethod != kCompressStored; - } - int getCompressionMethod(void) const { return mCDE.mCompressionMethod; } - - /* - * Return the uncompressed length. - */ - off_t getUncompressedLen(void) const { return mCDE.mUncompressedSize; } - - /* - * Return the compressed length. For uncompressed data, this returns - * the same thing as getUncompresesdLen(). - */ - off_t getCompressedLen(void) const { return mCDE.mCompressedSize; } - - /* - * Return the offset of the local file header. - */ - off_t getLFHOffset(void) const { return mCDE.mLocalHeaderRelOffset; } - - /* - * Return the absolute file offset of the start of the compressed or - * uncompressed data. - */ - off_t getFileOffset(void) const { - return mCDE.mLocalHeaderRelOffset + - LocalFileHeader::kLFHLen + - mLFH.mFileNameLength + - mLFH.mExtraFieldLength; - } - - /* - * Return the data CRC. - */ - unsigned long getCRC32(void) const { return mCDE.mCRC32; } - - /* - * Return file modification time in UNIX seconds-since-epoch. - */ - time_t getModWhen(void) const; - - /* - * Return the archived file name. - */ - const char* getFileName(void) const { return (const char*) mCDE.mFileName; } - - /* - * Application-defined "mark". Can be useful when synchronizing the - * contents of an archive with contents on disk. - */ - bool getMarked(void) const { return mMarked; } - void setMarked(bool val) { mMarked = val; } - - /* - * Some basic functions for raw data manipulation. "LE" means - * Little Endian. - */ - static inline unsigned short getShortLE(const unsigned char* buf) { - return buf[0] | (buf[1] << 8); - } - static inline unsigned long getLongLE(const unsigned char* buf) { - return buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24); - } - static inline void putShortLE(unsigned char* buf, short val) { - buf[0] = (unsigned char) val; - buf[1] = (unsigned char) (val >> 8); - } - static inline void putLongLE(unsigned char* buf, long val) { - buf[0] = (unsigned char) val; - buf[1] = (unsigned char) (val >> 8); - buf[2] = (unsigned char) (val >> 16); - buf[3] = (unsigned char) (val >> 24); - } - - /* defined for Zip archives */ - enum { - kCompressStored = 0, // no compression - // shrunk = 1, - // reduced 1 = 2, - // reduced 2 = 3, - // reduced 3 = 4, - // reduced 4 = 5, - // imploded = 6, - // tokenized = 7, - kCompressDeflated = 8, // standard deflate - // Deflate64 = 9, - // lib imploded = 10, - // reserved = 11, - // bzip2 = 12, - }; - - /* - * Deletion flag. If set, the entry will be removed on the next - * call to "flush". - */ - bool getDeleted(void) const { return mDeleted; } - -protected: - /* - * Initialize the structure from the file, which is pointing at - * our Central Directory entry. - */ - status_t initFromCDE(FILE* fp); - - /* - * Initialize the structure for a new file. We need the filename - * and comment so that we can properly size the LFH area. The - * filename is mandatory, the comment is optional. - */ - void initNew(const char* fileName, const char* comment); - - /* - * Initialize the structure with the contents of a ZipEntry from - * another file. If fileName is non-NULL, override the name with fileName. - */ - status_t initFromExternal(const ZipFile* pZipFile, const ZipEntry* pEntry, - const char* fileName); - - /* - * Add some pad bytes to the LFH. We do this by adding or resizing - * the "extra" field. - */ - status_t addPadding(int padding); - - /* - * Set information about the data for this entry. - */ - void setDataInfo(long uncompLen, long compLen, unsigned long crc32, - int compressionMethod); - - /* - * Set the modification date. - */ - void setModWhen(time_t when); - - /* - * Set the offset of the local file header, relative to the start of - * the current file. - */ - void setLFHOffset(off_t offset) { - mCDE.mLocalHeaderRelOffset = (long) offset; - } - - /* mark for deletion; used by ZipFile::remove() */ - void setDeleted(void) { mDeleted = true; } - -private: - /* these are private and not defined */ - ZipEntry(const ZipEntry& src); - ZipEntry& operator=(const ZipEntry& src); - - /* returns "true" if the CDE and the LFH agree */ - bool compareHeaders(void) const; - void copyCDEtoLFH(void); - - bool mDeleted; // set if entry is pending deletion - bool mMarked; // app-defined marker - - /* - * Every entry in the Zip archive starts off with one of these. - */ - class LocalFileHeader { - public: - LocalFileHeader(void) : - mVersionToExtract(0), - mGPBitFlag(0), - mCompressionMethod(0), - mLastModFileTime(0), - mLastModFileDate(0), - mCRC32(0), - mCompressedSize(0), - mUncompressedSize(0), - mFileNameLength(0), - mExtraFieldLength(0), - mFileName(NULL), - mExtraField(NULL) - {} - virtual ~LocalFileHeader(void) { - delete[] mFileName; - delete[] mExtraField; - } - - status_t read(FILE* fp); - status_t write(FILE* fp); - - // unsigned long mSignature; - unsigned short mVersionToExtract; - unsigned short mGPBitFlag; - unsigned short mCompressionMethod; - unsigned short mLastModFileTime; - unsigned short mLastModFileDate; - unsigned long mCRC32; - unsigned long mCompressedSize; - unsigned long mUncompressedSize; - unsigned short mFileNameLength; - unsigned short mExtraFieldLength; - unsigned char* mFileName; - unsigned char* mExtraField; - - enum { - kSignature = 0x04034b50, - kLFHLen = 30, // LocalFileHdr len, excl. var fields - }; - - void dump(void) const; - }; - - /* - * Every entry in the Zip archive has one of these in the "central - * directory" at the end of the file. - */ - class CentralDirEntry { - public: - CentralDirEntry(void) : - mVersionMadeBy(0), - mVersionToExtract(0), - mGPBitFlag(0), - mCompressionMethod(0), - mLastModFileTime(0), - mLastModFileDate(0), - mCRC32(0), - mCompressedSize(0), - mUncompressedSize(0), - mFileNameLength(0), - mExtraFieldLength(0), - mFileCommentLength(0), - mDiskNumberStart(0), - mInternalAttrs(0), - mExternalAttrs(0), - mLocalHeaderRelOffset(0), - mFileName(NULL), - mExtraField(NULL), - mFileComment(NULL) - {} - virtual ~CentralDirEntry(void) { - delete[] mFileName; - delete[] mExtraField; - delete[] mFileComment; - } - - status_t read(FILE* fp); - status_t write(FILE* fp); - - CentralDirEntry& operator=(const CentralDirEntry& src); - - // unsigned long mSignature; - unsigned short mVersionMadeBy; - unsigned short mVersionToExtract; - unsigned short mGPBitFlag; - unsigned short mCompressionMethod; - unsigned short mLastModFileTime; - unsigned short mLastModFileDate; - unsigned long mCRC32; - unsigned long mCompressedSize; - unsigned long mUncompressedSize; - unsigned short mFileNameLength; - unsigned short mExtraFieldLength; - unsigned short mFileCommentLength; - unsigned short mDiskNumberStart; - unsigned short mInternalAttrs; - unsigned long mExternalAttrs; - unsigned long mLocalHeaderRelOffset; - unsigned char* mFileName; - unsigned char* mExtraField; - unsigned char* mFileComment; - - void dump(void) const; - - enum { - kSignature = 0x02014b50, - kCDELen = 46, // CentralDirEnt len, excl. var fields - }; - }; - - enum { - //kDataDescriptorSignature = 0x08074b50, // currently unused - kDataDescriptorLen = 16, // four 32-bit fields - - kDefaultVersion = 20, // need deflate, nothing much else - kDefaultMadeBy = 0x0317, // 03=UNIX, 17=spec v2.3 - kUsesDataDescr = 0x0008, // GPBitFlag bit 3 - }; - - LocalFileHeader mLFH; - CentralDirEntry mCDE; -}; - -}; // namespace aapt - -#endif // __LIBS_ZIPENTRY_H diff --git a/tools/aapt2/ZipFile.cpp b/tools/aapt2/ZipFile.cpp deleted file mode 100644 index 268c15efd9b9..000000000000 --- a/tools/aapt2/ZipFile.cpp +++ /dev/null @@ -1,1306 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// -// Access to Zip archives. -// - -#define LOG_TAG "zip" - -#include <androidfw/ZipUtils.h> -#include <utils/Log.h> - -#include "ZipFile.h" -#include "Util.h" - -#include <zlib.h> -#define DEF_MEM_LEVEL 8 // normally in zutil.h? - -#include <memory.h> -#include <sys/stat.h> -#include <errno.h> -#include <assert.h> - -namespace aapt { - -using namespace android; - -/* - * Some environments require the "b", some choke on it. - */ -#define FILE_OPEN_RO "rb" -#define FILE_OPEN_RW "r+b" -#define FILE_OPEN_RW_CREATE "w+b" - -/* should live somewhere else? */ -static status_t errnoToStatus(int err) -{ - if (err == ENOENT) - return NAME_NOT_FOUND; - else if (err == EACCES) - return PERMISSION_DENIED; - else - return UNKNOWN_ERROR; -} - -/* - * Open a file and parse its guts. - */ -status_t ZipFile::open(const char* zipFileName, int flags) -{ - bool newArchive = false; - - assert(mZipFp == NULL); // no reopen - - if ((flags & kOpenTruncate)) - flags |= kOpenCreate; // trunc implies create - - if ((flags & kOpenReadOnly) && (flags & kOpenReadWrite)) - return INVALID_OPERATION; // not both - if (!((flags & kOpenReadOnly) || (flags & kOpenReadWrite))) - return INVALID_OPERATION; // not neither - if ((flags & kOpenCreate) && !(flags & kOpenReadWrite)) - return INVALID_OPERATION; // create requires write - - if (flags & kOpenTruncate) { - newArchive = true; - } else { - newArchive = (access(zipFileName, F_OK) != 0); - if (!(flags & kOpenCreate) && newArchive) { - /* not creating, must already exist */ - ALOGD("File %s does not exist", zipFileName); - return NAME_NOT_FOUND; - } - } - - /* open the file */ - const char* openflags; - if (flags & kOpenReadWrite) { - if (newArchive) - openflags = FILE_OPEN_RW_CREATE; - else - openflags = FILE_OPEN_RW; - } else { - openflags = FILE_OPEN_RO; - } - mZipFp = fopen(zipFileName, openflags); - if (mZipFp == NULL) { - int err = errno; - ALOGD("fopen failed: %d\n", err); - return errnoToStatus(err); - } - - status_t result; - if (!newArchive) { - /* - * Load the central directory. If that fails, then this probably - * isn't a Zip archive. - */ - result = readCentralDir(); - } else { - /* - * Newly-created. The EndOfCentralDir constructor actually - * sets everything to be the way we want it (all zeroes). We - * set mNeedCDRewrite so that we create *something* if the - * caller doesn't add any files. (We could also just unlink - * the file if it's brand new and nothing was added, but that's - * probably doing more than we really should -- the user might - * have a need for empty zip files.) - */ - mNeedCDRewrite = true; - result = NO_ERROR; - } - - if (flags & kOpenReadOnly) - mReadOnly = true; - else - assert(!mReadOnly); - - return result; -} - -/* - * Return the Nth entry in the archive. - */ -ZipEntry* ZipFile::getEntryByIndex(int idx) const -{ - if (idx < 0 || idx >= (int) mEntries.size()) - return NULL; - - return mEntries[idx]; -} - -/* - * Find an entry by name. - */ -ZipEntry* ZipFile::getEntryByName(const char* fileName) const -{ - /* - * Do a stupid linear string-compare search. - * - * There are various ways to speed this up, especially since it's rare - * to intermingle changes to the archive with "get by name" calls. We - * don't want to sort the mEntries vector itself, however, because - * it's used to recreate the Central Directory. - * - * (Hash table works, parallel list of pointers in sorted order is good.) - */ - int idx; - - for (idx = mEntries.size()-1; idx >= 0; idx--) { - ZipEntry* pEntry = mEntries[idx]; - if (!pEntry->getDeleted() && - strcmp(fileName, pEntry->getFileName()) == 0) - { - return pEntry; - } - } - - return NULL; -} - -/* - * Empty the mEntries vector. - */ -void ZipFile::discardEntries(void) -{ - int count = mEntries.size(); - - while (--count >= 0) - delete mEntries[count]; - - mEntries.clear(); -} - - -/* - * Find the central directory and read the contents. - * - * The fun thing about ZIP archives is that they may or may not be - * readable from start to end. In some cases, notably for archives - * that were written to stdout, the only length information is in the - * central directory at the end of the file. - * - * Of course, the central directory can be followed by a variable-length - * comment field, so we have to scan through it backwards. The comment - * is at most 64K, plus we have 18 bytes for the end-of-central-dir stuff - * itself, plus apparently sometimes people throw random junk on the end - * just for the fun of it. - * - * This is all a little wobbly. If the wrong value ends up in the EOCD - * area, we're hosed. This appears to be the way that everbody handles - * it though, so we're in pretty good company if this fails. - */ -status_t ZipFile::readCentralDir(void) -{ - status_t result = NO_ERROR; - unsigned char* buf = NULL; - off_t fileLength, seekStart; - long readAmount; - int i; - - fseek(mZipFp, 0, SEEK_END); - fileLength = ftell(mZipFp); - rewind(mZipFp); - - /* too small to be a ZIP archive? */ - if (fileLength < EndOfCentralDir::kEOCDLen) { - ALOGD("Length is %ld -- too small\n", (long)fileLength); - result = INVALID_OPERATION; - goto bail; - } - - buf = new unsigned char[EndOfCentralDir::kMaxEOCDSearch]; - if (buf == NULL) { - ALOGD("Failure allocating %d bytes for EOCD search", - EndOfCentralDir::kMaxEOCDSearch); - result = NO_MEMORY; - goto bail; - } - - if (fileLength > EndOfCentralDir::kMaxEOCDSearch) { - seekStart = fileLength - EndOfCentralDir::kMaxEOCDSearch; - readAmount = EndOfCentralDir::kMaxEOCDSearch; - } else { - seekStart = 0; - readAmount = (long) fileLength; - } - if (fseek(mZipFp, seekStart, SEEK_SET) != 0) { - ALOGD("Failure seeking to end of zip at %ld", (long) seekStart); - result = UNKNOWN_ERROR; - goto bail; - } - - /* read the last part of the file into the buffer */ - if (fread(buf, 1, readAmount, mZipFp) != (size_t) readAmount) { - ALOGD("short file? wanted %ld\n", readAmount); - result = UNKNOWN_ERROR; - goto bail; - } - - /* find the end-of-central-dir magic */ - for (i = readAmount - 4; i >= 0; i--) { - if (buf[i] == 0x50 && - ZipEntry::getLongLE(&buf[i]) == EndOfCentralDir::kSignature) - { - ALOGV("+++ Found EOCD at buf+%d\n", i); - break; - } - } - if (i < 0) { - ALOGD("EOCD not found, not Zip\n"); - result = INVALID_OPERATION; - goto bail; - } - - /* extract eocd values */ - result = mEOCD.readBuf(buf + i, readAmount - i); - if (result != NO_ERROR) { - ALOGD("Failure reading %ld bytes of EOCD values", readAmount - i); - goto bail; - } - //mEOCD.dump(); - - if (mEOCD.mDiskNumber != 0 || mEOCD.mDiskWithCentralDir != 0 || - mEOCD.mNumEntries != mEOCD.mTotalNumEntries) - { - ALOGD("Archive spanning not supported\n"); - result = INVALID_OPERATION; - goto bail; - } - - /* - * So far so good. "mCentralDirSize" is the size in bytes of the - * central directory, so we can just seek back that far to find it. - * We can also seek forward mCentralDirOffset bytes from the - * start of the file. - * - * We're not guaranteed to have the rest of the central dir in the - * buffer, nor are we guaranteed that the central dir will have any - * sort of convenient size. We need to skip to the start of it and - * read the header, then the other goodies. - * - * The only thing we really need right now is the file comment, which - * we're hoping to preserve. - */ - if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) { - ALOGD("Failure seeking to central dir offset %ld\n", - mEOCD.mCentralDirOffset); - result = UNKNOWN_ERROR; - goto bail; - } - - /* - * Loop through and read the central dir entries. - */ - ALOGV("Scanning %d entries...\n", mEOCD.mTotalNumEntries); - int entry; - for (entry = 0; entry < mEOCD.mTotalNumEntries; entry++) { - ZipEntry* pEntry = new ZipEntry; - - result = pEntry->initFromCDE(mZipFp); - if (result != NO_ERROR) { - ALOGD("initFromCDE failed\n"); - delete pEntry; - goto bail; - } - - mEntries.push_back(pEntry); - } - - - /* - * If all went well, we should now be back at the EOCD. - */ - { - unsigned char checkBuf[4]; - if (fread(checkBuf, 1, 4, mZipFp) != 4) { - ALOGD("EOCD check read failed\n"); - result = INVALID_OPERATION; - goto bail; - } - if (ZipEntry::getLongLE(checkBuf) != EndOfCentralDir::kSignature) { - ALOGD("EOCD read check failed\n"); - result = UNKNOWN_ERROR; - goto bail; - } - ALOGV("+++ EOCD read check passed\n"); - } - -bail: - delete[] buf; - return result; -} - -status_t ZipFile::add(const BigBuffer& buffer, const char* storageName, int compressionMethod, - ZipEntry** ppEntry) { - std::unique_ptr<uint8_t[]> data = util::copy(buffer); - return add(data.get(), buffer.size(), storageName, compressionMethod, ppEntry); -} - - -/* - * Add a new file to the archive. - * - * This requires creating and populating a ZipEntry structure, and copying - * the data into the file at the appropriate position. The "appropriate - * position" is the current location of the central directory, which we - * casually overwrite (we can put it back later). - * - * If we were concerned about safety, we would want to make all changes - * in a temp file and then overwrite the original after everything was - * safely written. Not really a concern for us. - */ -status_t ZipFile::addCommon(const char* fileName, const void* data, size_t size, - const char* storageName, int sourceType, int compressionMethod, - ZipEntry** ppEntry) -{ - ZipEntry* pEntry = NULL; - status_t result = NO_ERROR; - long lfhPosn, startPosn, endPosn, uncompressedLen; - FILE* inputFp = NULL; - unsigned long crc; - time_t modWhen; - - if (mReadOnly) - return INVALID_OPERATION; - - assert(compressionMethod == ZipEntry::kCompressDeflated || - compressionMethod == ZipEntry::kCompressStored); - - /* make sure we're in a reasonable state */ - assert(mZipFp != NULL); - assert(mEntries.size() == mEOCD.mTotalNumEntries); - - /* make sure it doesn't already exist */ - if (getEntryByName(storageName) != NULL) - return ALREADY_EXISTS; - - if (!data) { - inputFp = fopen(fileName, FILE_OPEN_RO); - if (inputFp == NULL) - return errnoToStatus(errno); - } - - if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) { - result = UNKNOWN_ERROR; - goto bail; - } - - pEntry = new ZipEntry; - pEntry->initNew(storageName, NULL); - - /* - * From here on out, failures are more interesting. - */ - mNeedCDRewrite = true; - - /* - * Write the LFH, even though it's still mostly blank. We need it - * as a place-holder. In theory the LFH isn't necessary, but in - * practice some utilities demand it. - */ - lfhPosn = ftell(mZipFp); - pEntry->mLFH.write(mZipFp); - startPosn = ftell(mZipFp); - - /* - * Copy the data in, possibly compressing it as we go. - */ - if (sourceType == ZipEntry::kCompressStored) { - if (compressionMethod == ZipEntry::kCompressDeflated) { - bool failed = false; - result = compressFpToFp(mZipFp, inputFp, data, size, &crc); - if (result != NO_ERROR) { - ALOGD("compression failed, storing\n"); - failed = true; - } else { - /* - * Make sure it has compressed "enough". This probably ought - * to be set through an API call, but I don't expect our - * criteria to change over time. - */ - long src = inputFp ? ftell(inputFp) : size; - long dst = ftell(mZipFp) - startPosn; - if (dst + (dst / 10) > src) { - ALOGD("insufficient compression (src=%ld dst=%ld), storing\n", - src, dst); - failed = true; - } - } - - if (failed) { - compressionMethod = ZipEntry::kCompressStored; - if (inputFp) rewind(inputFp); - fseek(mZipFp, startPosn, SEEK_SET); - /* fall through to kCompressStored case */ - } - } - /* handle "no compression" request, or failed compression from above */ - if (compressionMethod == ZipEntry::kCompressStored) { - if (inputFp) { - result = copyFpToFp(mZipFp, inputFp, &crc); - } else { - result = copyDataToFp(mZipFp, data, size, &crc); - } - if (result != NO_ERROR) { - // don't need to truncate; happens in CDE rewrite - ALOGD("failed copying data in\n"); - goto bail; - } - } - - // currently seeked to end of file - uncompressedLen = inputFp ? ftell(inputFp) : size; - } else if (sourceType == ZipEntry::kCompressDeflated) { - /* we should support uncompressed-from-compressed, but it's not - * important right now */ - assert(compressionMethod == ZipEntry::kCompressDeflated); - - bool scanResult; - int method; - long compressedLen; - - scanResult = ZipUtils::examineGzip(inputFp, &method, &uncompressedLen, - &compressedLen, &crc); - if (!scanResult || method != ZipEntry::kCompressDeflated) { - ALOGD("this isn't a deflated gzip file?"); - result = UNKNOWN_ERROR; - goto bail; - } - - result = copyPartialFpToFp(mZipFp, inputFp, compressedLen, NULL); - if (result != NO_ERROR) { - ALOGD("failed copying gzip data in\n"); - goto bail; - } - } else { - assert(false); - result = UNKNOWN_ERROR; - goto bail; - } - - /* - * We could write the "Data Descriptor", but there doesn't seem to - * be any point since we're going to go back and write the LFH. - * - * Update file offsets. - */ - endPosn = ftell(mZipFp); // seeked to end of compressed data - - /* - * Success! Fill out new values. - */ - pEntry->setDataInfo(uncompressedLen, endPosn - startPosn, crc, - compressionMethod); - modWhen = getModTime(inputFp ? fileno(inputFp) : fileno(mZipFp)); - pEntry->setModWhen(modWhen); - pEntry->setLFHOffset(lfhPosn); - mEOCD.mNumEntries++; - mEOCD.mTotalNumEntries++; - mEOCD.mCentralDirSize = 0; // mark invalid; set by flush() - mEOCD.mCentralDirOffset = endPosn; - - /* - * Go back and write the LFH. - */ - if (fseek(mZipFp, lfhPosn, SEEK_SET) != 0) { - result = UNKNOWN_ERROR; - goto bail; - } - pEntry->mLFH.write(mZipFp); - - /* - * Add pEntry to the list. - */ - mEntries.push_back(pEntry); - if (ppEntry != NULL) - *ppEntry = pEntry; - pEntry = NULL; - -bail: - if (inputFp != NULL) - fclose(inputFp); - delete pEntry; - return result; -} - -/* - * Add an entry by copying it from another zip file. If "padding" is - * nonzero, the specified number of bytes will be added to the "extra" - * field in the header. - * - * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. - */ -status_t ZipFile::add(const ZipFile* pSourceZip, const ZipEntry* pSourceEntry, - const char* storageName, int padding, ZipEntry** ppEntry) -{ - ZipEntry* pEntry = NULL; - status_t result; - long lfhPosn, endPosn; - - if (mReadOnly) - return INVALID_OPERATION; - - /* make sure we're in a reasonable state */ - assert(mZipFp != NULL); - assert(mEntries.size() == mEOCD.mTotalNumEntries); - - if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) { - result = UNKNOWN_ERROR; - goto bail; - } - - pEntry = new ZipEntry; - if (pEntry == NULL) { - result = NO_MEMORY; - goto bail; - } - - result = pEntry->initFromExternal(pSourceZip, pSourceEntry, storageName); - if (result != NO_ERROR) { - goto bail; - } - if (padding != 0) { - result = pEntry->addPadding(padding); - if (result != NO_ERROR) - goto bail; - } - - /* - * From here on out, failures are more interesting. - */ - mNeedCDRewrite = true; - - /* - * Write the LFH. Since we're not recompressing the data, we already - * have all of the fields filled out. - */ - lfhPosn = ftell(mZipFp); - pEntry->mLFH.write(mZipFp); - - /* - * Copy the data over. - * - * If the "has data descriptor" flag is set, we want to copy the DD - * fields as well. This is a fixed-size area immediately following - * the data. - */ - if (fseek(pSourceZip->mZipFp, pSourceEntry->getFileOffset(), SEEK_SET) != 0) - { - result = UNKNOWN_ERROR; - goto bail; - } - - off_t copyLen; - copyLen = pSourceEntry->getCompressedLen(); - if ((pSourceEntry->mLFH.mGPBitFlag & ZipEntry::kUsesDataDescr) != 0) - copyLen += ZipEntry::kDataDescriptorLen; - - if (copyPartialFpToFp(mZipFp, pSourceZip->mZipFp, copyLen, NULL) - != NO_ERROR) - { - ALOGW("copy of '%s' failed\n", pEntry->mCDE.mFileName); - result = UNKNOWN_ERROR; - goto bail; - } - - /* - * Update file offsets. - */ - endPosn = ftell(mZipFp); - - /* - * Success! Fill out new values. - */ - pEntry->setLFHOffset(lfhPosn); // sets mCDE.mLocalHeaderRelOffset - mEOCD.mNumEntries++; - mEOCD.mTotalNumEntries++; - mEOCD.mCentralDirSize = 0; // mark invalid; set by flush() - mEOCD.mCentralDirOffset = endPosn; - - /* - * Add pEntry to the list. - */ - mEntries.push_back(pEntry); - if (ppEntry != NULL) - *ppEntry = pEntry; - pEntry = NULL; - - result = NO_ERROR; - -bail: - delete pEntry; - return result; -} - -/* - * Copy all of the bytes in "src" to "dst". - * - * On exit, "srcFp" will be seeked to the end of the file, and "dstFp" - * will be seeked immediately past the data. - */ -status_t ZipFile::copyFpToFp(FILE* dstFp, FILE* srcFp, unsigned long* pCRC32) -{ - unsigned char tmpBuf[32768]; - size_t count; - - *pCRC32 = crc32(0L, Z_NULL, 0); - - while (1) { - count = fread(tmpBuf, 1, sizeof(tmpBuf), srcFp); - if (ferror(srcFp) || ferror(dstFp)) - return errnoToStatus(errno); - if (count == 0) - break; - - *pCRC32 = crc32(*pCRC32, tmpBuf, count); - - if (fwrite(tmpBuf, 1, count, dstFp) != count) { - ALOGD("fwrite %d bytes failed\n", (int) count); - return UNKNOWN_ERROR; - } - } - - return NO_ERROR; -} - -/* - * Copy all of the bytes in "src" to "dst". - * - * On exit, "dstFp" will be seeked immediately past the data. - */ -status_t ZipFile::copyDataToFp(FILE* dstFp, - const void* data, size_t size, unsigned long* pCRC32) -{ - *pCRC32 = crc32(0L, Z_NULL, 0); - if (size > 0) { - *pCRC32 = crc32(*pCRC32, (const unsigned char*)data, size); - if (fwrite(data, 1, size, dstFp) != size) { - ALOGD("fwrite %d bytes failed\n", (int) size); - return UNKNOWN_ERROR; - } - } - - return NO_ERROR; -} - -/* - * Copy some of the bytes in "src" to "dst". - * - * If "pCRC32" is NULL, the CRC will not be computed. - * - * On exit, "srcFp" will be seeked to the end of the file, and "dstFp" - * will be seeked immediately past the data just written. - */ -status_t ZipFile::copyPartialFpToFp(FILE* dstFp, FILE* srcFp, long length, - unsigned long* pCRC32) -{ - unsigned char tmpBuf[32768]; - size_t count; - - if (pCRC32 != NULL) - *pCRC32 = crc32(0L, Z_NULL, 0); - - while (length) { - long readSize; - - readSize = sizeof(tmpBuf); - if (readSize > length) - readSize = length; - - count = fread(tmpBuf, 1, readSize, srcFp); - if ((long) count != readSize) { // error or unexpected EOF - ALOGD("fread %d bytes failed\n", (int) readSize); - return UNKNOWN_ERROR; - } - - if (pCRC32 != NULL) - *pCRC32 = crc32(*pCRC32, tmpBuf, count); - - if (fwrite(tmpBuf, 1, count, dstFp) != count) { - ALOGD("fwrite %d bytes failed\n", (int) count); - return UNKNOWN_ERROR; - } - - length -= readSize; - } - - return NO_ERROR; -} - -/* - * Compress all of the data in "srcFp" and write it to "dstFp". - * - * On exit, "srcFp" will be seeked to the end of the file, and "dstFp" - * will be seeked immediately past the compressed data. - */ -status_t ZipFile::compressFpToFp(FILE* dstFp, FILE* srcFp, - const void* data, size_t size, unsigned long* pCRC32) -{ - status_t result = NO_ERROR; - const size_t kBufSize = 32768; - unsigned char* inBuf = NULL; - unsigned char* outBuf = NULL; - z_stream zstream; - bool atEof = false; // no feof() aviailable yet - unsigned long crc; - int zerr; - - /* - * Create an input buffer and an output buffer. - */ - inBuf = new unsigned char[kBufSize]; - outBuf = new unsigned char[kBufSize]; - if (inBuf == NULL || outBuf == NULL) { - result = NO_MEMORY; - goto bail; - } - - /* - * Initialize the zlib stream. - */ - memset(&zstream, 0, sizeof(zstream)); - zstream.zalloc = Z_NULL; - zstream.zfree = Z_NULL; - zstream.opaque = Z_NULL; - zstream.next_in = NULL; - zstream.avail_in = 0; - zstream.next_out = outBuf; - zstream.avail_out = kBufSize; - zstream.data_type = Z_UNKNOWN; - - zerr = deflateInit2(&zstream, Z_BEST_COMPRESSION, - Z_DEFLATED, -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY); - if (zerr != Z_OK) { - result = UNKNOWN_ERROR; - if (zerr == Z_VERSION_ERROR) { - ALOGE("Installed zlib is not compatible with linked version (%s)\n", - ZLIB_VERSION); - } else { - ALOGD("Call to deflateInit2 failed (zerr=%d)\n", zerr); - } - goto bail; - } - - crc = crc32(0L, Z_NULL, 0); - - /* - * Loop while we have data. - */ - do { - size_t getSize; - int flush; - - /* only read if the input buffer is empty */ - if (zstream.avail_in == 0 && !atEof) { - ALOGV("+++ reading %d bytes\n", (int)kBufSize); - if (data) { - getSize = size > kBufSize ? kBufSize : size; - memcpy(inBuf, data, getSize); - data = ((const char*)data) + getSize; - size -= getSize; - } else { - getSize = fread(inBuf, 1, kBufSize, srcFp); - if (ferror(srcFp)) { - ALOGD("deflate read failed (errno=%d)\n", errno); - goto z_bail; - } - } - if (getSize < kBufSize) { - ALOGV("+++ got %d bytes, EOF reached\n", - (int)getSize); - atEof = true; - } - - crc = crc32(crc, inBuf, getSize); - - zstream.next_in = inBuf; - zstream.avail_in = getSize; - } - - if (atEof) - flush = Z_FINISH; /* tell zlib that we're done */ - else - flush = Z_NO_FLUSH; /* more to come! */ - - zerr = deflate(&zstream, flush); - if (zerr != Z_OK && zerr != Z_STREAM_END) { - ALOGD("zlib deflate call failed (zerr=%d)\n", zerr); - result = UNKNOWN_ERROR; - goto z_bail; - } - - /* write when we're full or when we're done */ - if (zstream.avail_out == 0 || - (zerr == Z_STREAM_END && zstream.avail_out != (uInt) kBufSize)) - { - ALOGV("+++ writing %d bytes\n", (int) (zstream.next_out - outBuf)); - if (fwrite(outBuf, 1, zstream.next_out - outBuf, dstFp) != - (size_t)(zstream.next_out - outBuf)) - { - ALOGD("write %d failed in deflate\n", - (int) (zstream.next_out - outBuf)); - goto z_bail; - } - - zstream.next_out = outBuf; - zstream.avail_out = kBufSize; - } - } while (zerr == Z_OK); - - assert(zerr == Z_STREAM_END); /* other errors should've been caught */ - - *pCRC32 = crc; - -z_bail: - deflateEnd(&zstream); /* free up any allocated structures */ - -bail: - delete[] inBuf; - delete[] outBuf; - - return result; -} - -/* - * Mark an entry as deleted. - * - * We will eventually need to crunch the file down, but if several files - * are being removed (perhaps as part of an "update" process) we can make - * things considerably faster by deferring the removal to "flush" time. - */ -status_t ZipFile::remove(ZipEntry* pEntry) -{ - /* - * Should verify that pEntry is actually part of this archive, and - * not some stray ZipEntry from a different file. - */ - - /* mark entry as deleted, and mark archive as dirty */ - pEntry->setDeleted(); - mNeedCDRewrite = true; - return NO_ERROR; -} - -/* - * Flush any pending writes. - * - * In particular, this will crunch out deleted entries, and write the - * Central Directory and EOCD if we have stomped on them. - */ -status_t ZipFile::flush(void) -{ - status_t result = NO_ERROR; - long eocdPosn; - int i, count; - - if (mReadOnly) - return INVALID_OPERATION; - if (!mNeedCDRewrite) - return NO_ERROR; - - assert(mZipFp != NULL); - - result = crunchArchive(); - if (result != NO_ERROR) - return result; - - if (fseek(mZipFp, mEOCD.mCentralDirOffset, SEEK_SET) != 0) - return UNKNOWN_ERROR; - - count = mEntries.size(); - for (i = 0; i < count; i++) { - ZipEntry* pEntry = mEntries[i]; - pEntry->mCDE.write(mZipFp); - } - - eocdPosn = ftell(mZipFp); - mEOCD.mCentralDirSize = eocdPosn - mEOCD.mCentralDirOffset; - - mEOCD.write(mZipFp); - - /* - * If we had some stuff bloat up during compression and get replaced - * with plain files, or if we deleted some entries, there's a lot - * of wasted space at the end of the file. Remove it now. - */ - if (ftruncate(fileno(mZipFp), ftell(mZipFp)) != 0) { - ALOGW("ftruncate failed %ld: %s\n", ftell(mZipFp), strerror(errno)); - // not fatal - } - - /* should we clear the "newly added" flag in all entries now? */ - - mNeedCDRewrite = false; - return NO_ERROR; -} - -/* - * Crunch deleted files out of an archive by shifting the later files down. - * - * Because we're not using a temp file, we do the operation inside the - * current file. - */ -status_t ZipFile::crunchArchive(void) -{ - status_t result = NO_ERROR; - int i, count; - long delCount, adjust; - -#if 0 - printf("CONTENTS:\n"); - for (i = 0; i < (int) mEntries.size(); i++) { - printf(" %d: lfhOff=%ld del=%d\n", - i, mEntries[i]->getLFHOffset(), mEntries[i]->getDeleted()); - } - printf(" END is %ld\n", (long) mEOCD.mCentralDirOffset); -#endif - - /* - * Roll through the set of files, shifting them as appropriate. We - * could probably get a slight performance improvement by sliding - * multiple files down at once (because we could use larger reads - * when operating on batches of small files), but it's not that useful. - */ - count = mEntries.size(); - delCount = adjust = 0; - for (i = 0; i < count; i++) { - ZipEntry* pEntry = mEntries[i]; - long span; - - if (pEntry->getLFHOffset() != 0) { - long nextOffset; - - /* Get the length of this entry by finding the offset - * of the next entry. Directory entries don't have - * file offsets, so we need to find the next non-directory - * entry. - */ - nextOffset = 0; - for (int ii = i+1; nextOffset == 0 && ii < count; ii++) - nextOffset = mEntries[ii]->getLFHOffset(); - if (nextOffset == 0) - nextOffset = mEOCD.mCentralDirOffset; - span = nextOffset - pEntry->getLFHOffset(); - - assert(span >= ZipEntry::LocalFileHeader::kLFHLen); - } else { - /* This is a directory entry. It doesn't have - * any actual file contents, so there's no need to - * move anything. - */ - span = 0; - } - - //printf("+++ %d: off=%ld span=%ld del=%d [count=%d]\n", - // i, pEntry->getLFHOffset(), span, pEntry->getDeleted(), count); - - if (pEntry->getDeleted()) { - adjust += span; - delCount++; - - delete pEntry; - mEntries.erase(mEntries.begin() + i); - - /* adjust loop control */ - count--; - i--; - } else if (span != 0 && adjust > 0) { - /* shuffle this entry back */ - //printf("+++ Shuffling '%s' back %ld\n", - // pEntry->getFileName(), adjust); - result = filemove(mZipFp, pEntry->getLFHOffset() - adjust, - pEntry->getLFHOffset(), span); - if (result != NO_ERROR) { - /* this is why you use a temp file */ - ALOGE("error during crunch - archive is toast\n"); - return result; - } - - pEntry->setLFHOffset(pEntry->getLFHOffset() - adjust); - } - } - - /* - * Fix EOCD info. We have to wait until the end to do some of this - * because we use mCentralDirOffset to determine "span" for the - * last entry. - */ - mEOCD.mCentralDirOffset -= adjust; - mEOCD.mNumEntries -= delCount; - mEOCD.mTotalNumEntries -= delCount; - mEOCD.mCentralDirSize = 0; // mark invalid; set by flush() - - assert(mEOCD.mNumEntries == mEOCD.mTotalNumEntries); - assert(mEOCD.mNumEntries == count); - - return result; -} - -/* - * Works like memmove(), but on pieces of a file. - */ -status_t ZipFile::filemove(FILE* fp, off_t dst, off_t src, size_t n) -{ - if (dst == src || n <= 0) - return NO_ERROR; - - unsigned char readBuf[32768]; - - if (dst < src) { - /* shift stuff toward start of file; must read from start */ - while (n != 0) { - size_t getSize = sizeof(readBuf); - if (getSize > n) - getSize = n; - - if (fseek(fp, (long) src, SEEK_SET) != 0) { - ALOGD("filemove src seek %ld failed\n", (long) src); - return UNKNOWN_ERROR; - } - - if (fread(readBuf, 1, getSize, fp) != getSize) { - ALOGD("filemove read %ld off=%ld failed\n", - (long) getSize, (long) src); - return UNKNOWN_ERROR; - } - - if (fseek(fp, (long) dst, SEEK_SET) != 0) { - ALOGD("filemove dst seek %ld failed\n", (long) dst); - return UNKNOWN_ERROR; - } - - if (fwrite(readBuf, 1, getSize, fp) != getSize) { - ALOGD("filemove write %ld off=%ld failed\n", - (long) getSize, (long) dst); - return UNKNOWN_ERROR; - } - - src += getSize; - dst += getSize; - n -= getSize; - } - } else { - /* shift stuff toward end of file; must read from end */ - assert(false); // write this someday, maybe - return UNKNOWN_ERROR; - } - - return NO_ERROR; -} - - -/* - * Get the modification time from a file descriptor. - */ -time_t ZipFile::getModTime(int fd) -{ - struct stat sb; - - if (fstat(fd, &sb) < 0) { - ALOGD("HEY: fstat on fd %d failed\n", fd); - return (time_t) -1; - } - - return sb.st_mtime; -} - - -#if 0 /* this is a bad idea */ -/* - * Get a copy of the Zip file descriptor. - * - * We don't allow this if the file was opened read-write because we tend - * to leave the file contents in an uncertain state between calls to - * flush(). The duplicated file descriptor should only be valid for reads. - */ -int ZipFile::getZipFd(void) const -{ - if (!mReadOnly) - return INVALID_OPERATION; - assert(mZipFp != NULL); - - int fd; - fd = dup(fileno(mZipFp)); - if (fd < 0) { - ALOGD("didn't work, errno=%d\n", errno); - } - - return fd; -} -#endif - - -#if 0 -/* - * Expand data. - */ -bool ZipFile::uncompress(const ZipEntry* pEntry, void* buf) const -{ - return false; -} -#endif - -// free the memory when you're done -void* ZipFile::uncompress(const ZipEntry* entry) -{ - size_t unlen = entry->getUncompressedLen(); - size_t clen = entry->getCompressedLen(); - - void* buf = malloc(unlen); - if (buf == NULL) { - return NULL; - } - - fseek(mZipFp, 0, SEEK_SET); - - off_t offset = entry->getFileOffset(); - if (fseek(mZipFp, offset, SEEK_SET) != 0) { - goto bail; - } - - switch (entry->getCompressionMethod()) - { - case ZipEntry::kCompressStored: { - ssize_t amt = fread(buf, 1, unlen, mZipFp); - if (amt != (ssize_t)unlen) { - goto bail; - } -#if 0 - printf("data...\n"); - const unsigned char* p = (unsigned char*)buf; - const unsigned char* end = p+unlen; - for (int i=0; i<32 && p < end; i++) { - printf("0x%08x ", (int)(offset+(i*0x10))); - for (int j=0; j<0x10 && p < end; j++) { - printf(" %02x", *p); - p++; - } - printf("\n"); - } -#endif - - } - break; - case ZipEntry::kCompressDeflated: { - if (!ZipUtils::inflateToBuffer(mZipFp, buf, unlen, clen)) { - goto bail; - } - } - break; - default: - goto bail; - } - return buf; - -bail: - free(buf); - return NULL; -} - - -/* - * =========================================================================== - * ZipFile::EndOfCentralDir - * =========================================================================== - */ - -/* - * Read the end-of-central-dir fields. - * - * "buf" should be positioned at the EOCD signature, and should contain - * the entire EOCD area including the comment. - */ -status_t ZipFile::EndOfCentralDir::readBuf(const unsigned char* buf, int len) -{ - /* don't allow re-use */ - assert(mComment == NULL); - - if (len < kEOCDLen) { - /* looks like ZIP file got truncated */ - ALOGD(" Zip EOCD: expected >= %d bytes, found %d\n", - kEOCDLen, len); - return INVALID_OPERATION; - } - - /* this should probably be an assert() */ - if (ZipEntry::getLongLE(&buf[0x00]) != kSignature) - return UNKNOWN_ERROR; - - mDiskNumber = ZipEntry::getShortLE(&buf[0x04]); - mDiskWithCentralDir = ZipEntry::getShortLE(&buf[0x06]); - mNumEntries = ZipEntry::getShortLE(&buf[0x08]); - mTotalNumEntries = ZipEntry::getShortLE(&buf[0x0a]); - mCentralDirSize = ZipEntry::getLongLE(&buf[0x0c]); - mCentralDirOffset = ZipEntry::getLongLE(&buf[0x10]); - mCommentLen = ZipEntry::getShortLE(&buf[0x14]); - - // TODO: validate mCentralDirOffset - - if (mCommentLen > 0) { - if (kEOCDLen + mCommentLen > len) { - ALOGD("EOCD(%d) + comment(%d) exceeds len (%d)\n", - kEOCDLen, mCommentLen, len); - return UNKNOWN_ERROR; - } - mComment = new unsigned char[mCommentLen]; - memcpy(mComment, buf + kEOCDLen, mCommentLen); - } - - return NO_ERROR; -} - -/* - * Write an end-of-central-directory section. - */ -status_t ZipFile::EndOfCentralDir::write(FILE* fp) -{ - unsigned char buf[kEOCDLen]; - - ZipEntry::putLongLE(&buf[0x00], kSignature); - ZipEntry::putShortLE(&buf[0x04], mDiskNumber); - ZipEntry::putShortLE(&buf[0x06], mDiskWithCentralDir); - ZipEntry::putShortLE(&buf[0x08], mNumEntries); - ZipEntry::putShortLE(&buf[0x0a], mTotalNumEntries); - ZipEntry::putLongLE(&buf[0x0c], mCentralDirSize); - ZipEntry::putLongLE(&buf[0x10], mCentralDirOffset); - ZipEntry::putShortLE(&buf[0x14], mCommentLen); - - if (fwrite(buf, 1, kEOCDLen, fp) != kEOCDLen) - return UNKNOWN_ERROR; - if (mCommentLen > 0) { - assert(mComment != NULL); - if (fwrite(mComment, mCommentLen, 1, fp) != mCommentLen) - return UNKNOWN_ERROR; - } - - return NO_ERROR; -} - -/* - * Dump the contents of an EndOfCentralDir object. - */ -void ZipFile::EndOfCentralDir::dump(void) const -{ - ALOGD(" EndOfCentralDir contents:\n"); - ALOGD(" diskNum=%u diskWCD=%u numEnt=%u totalNumEnt=%u\n", - mDiskNumber, mDiskWithCentralDir, mNumEntries, mTotalNumEntries); - ALOGD(" centDirSize=%lu centDirOff=%lu commentLen=%u\n", - mCentralDirSize, mCentralDirOffset, mCommentLen); -} - -} // namespace aapt diff --git a/tools/aapt2/ZipFile.h b/tools/aapt2/ZipFile.h deleted file mode 100644 index 9de92ddc0872..000000000000 --- a/tools/aapt2/ZipFile.h +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (C) 2006 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// -// General-purpose Zip archive access. This class allows both reading and -// writing to Zip archives, including deletion of existing entries. -// -#ifndef __LIBS_ZIPFILE_H -#define __LIBS_ZIPFILE_H - -#include "BigBuffer.h" -#include "ZipEntry.h" - -#include <stdio.h> -#include <utils/Errors.h> -#include <vector> - -namespace aapt { - -using android::status_t; - -/* - * Manipulate a Zip archive. - * - * Some changes will not be visible in the until until "flush" is called. - * - * The correct way to update a file archive is to make all changes to a - * copy of the archive in a temporary file, and then unlink/rename over - * the original after everything completes. Because we're only interested - * in using this for packaging, we don't worry about such things. Crashing - * after making changes and before flush() completes could leave us with - * an unusable Zip archive. - */ -class ZipFile { -public: - ZipFile(void) - : mZipFp(NULL), mReadOnly(false), mNeedCDRewrite(false) - {} - ~ZipFile(void) { - if (!mReadOnly) - flush(); - if (mZipFp != NULL) - fclose(mZipFp); - discardEntries(); - } - - /* - * Open a new or existing archive. - */ - enum { - kOpenReadOnly = 0x01, - kOpenReadWrite = 0x02, - kOpenCreate = 0x04, // create if it doesn't exist - kOpenTruncate = 0x08, // if it exists, empty it - }; - status_t open(const char* zipFileName, int flags); - - /* - * Add a file to the end of the archive. Specify whether you want the - * library to try to store it compressed. - * - * If "storageName" is specified, the archive will use that instead - * of "fileName". - * - * If there is already an entry with the same name, the call fails. - * Existing entries with the same name must be removed first. - * - * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. - */ - status_t add(const char* fileName, int compressionMethod, - ZipEntry** ppEntry) - { - return add(fileName, fileName, compressionMethod, ppEntry); - } - status_t add(const char* fileName, const char* storageName, - int compressionMethod, ZipEntry** ppEntry) - { - return addCommon(fileName, NULL, 0, storageName, - ZipEntry::kCompressStored, - compressionMethod, ppEntry); - } - - /* - * Add a file that is already compressed with gzip. - * - * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. - */ - status_t addGzip(const char* fileName, const char* storageName, - ZipEntry** ppEntry) - { - return addCommon(fileName, NULL, 0, storageName, - ZipEntry::kCompressDeflated, - ZipEntry::kCompressDeflated, ppEntry); - } - - /* - * Add a file from an in-memory data buffer. - * - * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. - */ - status_t add(const void* data, size_t size, const char* storageName, - int compressionMethod, ZipEntry** ppEntry) - { - return addCommon(NULL, data, size, storageName, - ZipEntry::kCompressStored, - compressionMethod, ppEntry); - } - - status_t add(const BigBuffer& data, const char* storageName, - int compressionMethod, ZipEntry** ppEntry); - - /* - * Add an entry by copying it from another zip file. If storageName is - * non-NULL, the entry will be inserted with the name storageName, otherwise - * it will have the same name as the source entry. If "padding" is - * nonzero, the specified number of bytes will be added to the "extra" - * field in the header. - * - * If "ppEntry" is non-NULL, a pointer to the new entry will be returned. - */ - status_t add(const ZipFile* pSourceZip, const ZipEntry* pSourceEntry, - const char* storageName, int padding, ZipEntry** ppEntry); - - /* - * Mark an entry as having been removed. It is not actually deleted - * from the archive or our internal data structures until flush() is - * called. - */ - status_t remove(ZipEntry* pEntry); - - /* - * Flush changes. If mNeedCDRewrite is set, this writes the central dir. - */ - status_t flush(void); - - /* - * Expand the data into the buffer provided. The buffer must hold - * at least <uncompressed len> bytes. Variation expands directly - * to a file. - * - * Returns "false" if an error was encountered in the compressed data. - */ - //bool uncompress(const ZipEntry* pEntry, void* buf) const; - //bool uncompress(const ZipEntry* pEntry, FILE* fp) const; - void* uncompress(const ZipEntry* pEntry); - - /* - * Get an entry, by name. Returns NULL if not found. - * - * Does not return entries pending deletion. - */ - ZipEntry* getEntryByName(const char* fileName) const; - - /* - * Get the Nth entry in the archive. - * - * This will return an entry that is pending deletion. - */ - int getNumEntries(void) const { return mEntries.size(); } - ZipEntry* getEntryByIndex(int idx) const; - -private: - /* these are private and not defined */ - ZipFile(const ZipFile& src); - ZipFile& operator=(const ZipFile& src); - - class EndOfCentralDir { - public: - EndOfCentralDir(void) : - mDiskNumber(0), - mDiskWithCentralDir(0), - mNumEntries(0), - mTotalNumEntries(0), - mCentralDirSize(0), - mCentralDirOffset(0), - mCommentLen(0), - mComment(NULL) - {} - virtual ~EndOfCentralDir(void) { - delete[] mComment; - } - - status_t readBuf(const unsigned char* buf, int len); - status_t write(FILE* fp); - - //unsigned long mSignature; - unsigned short mDiskNumber; - unsigned short mDiskWithCentralDir; - unsigned short mNumEntries; - unsigned short mTotalNumEntries; - unsigned long mCentralDirSize; - unsigned long mCentralDirOffset; // offset from first disk - unsigned short mCommentLen; - unsigned char* mComment; - - enum { - kSignature = 0x06054b50, - kEOCDLen = 22, // EndOfCentralDir len, excl. comment - - kMaxCommentLen = 65535, // longest possible in ushort - kMaxEOCDSearch = kMaxCommentLen + EndOfCentralDir::kEOCDLen, - - }; - - void dump(void) const; - }; - - - /* read all entries in the central dir */ - status_t readCentralDir(void); - - /* crunch deleted entries out */ - status_t crunchArchive(void); - - /* clean up mEntries */ - void discardEntries(void); - - /* common handler for all "add" functions */ - status_t addCommon(const char* fileName, const void* data, size_t size, - const char* storageName, int sourceType, int compressionMethod, - ZipEntry** ppEntry); - - /* copy all of "srcFp" into "dstFp" */ - status_t copyFpToFp(FILE* dstFp, FILE* srcFp, unsigned long* pCRC32); - /* copy all of "data" into "dstFp" */ - status_t copyDataToFp(FILE* dstFp, - const void* data, size_t size, unsigned long* pCRC32); - /* copy some of "srcFp" into "dstFp" */ - status_t copyPartialFpToFp(FILE* dstFp, FILE* srcFp, long length, - unsigned long* pCRC32); - /* like memmove(), but on parts of a single file */ - status_t filemove(FILE* fp, off_t dest, off_t src, size_t n); - /* compress all of "srcFp" into "dstFp", using Deflate */ - status_t compressFpToFp(FILE* dstFp, FILE* srcFp, - const void* data, size_t size, unsigned long* pCRC32); - - /* get modification date from a file descriptor */ - time_t getModTime(int fd); - - /* - * We use stdio FILE*, which gives us buffering but makes dealing - * with files >2GB awkward. Until we support Zip64, we're fine. - */ - FILE* mZipFp; // Zip file pointer - - /* one of these per file */ - EndOfCentralDir mEOCD; - - /* did we open this read-only? */ - bool mReadOnly; - - /* set this when we trash the central dir */ - bool mNeedCDRewrite; - - /* - * One ZipEntry per entry in the zip file. I'm using pointers instead - * of objects because it's easier than making operator= work for the - * classes and sub-classes. - */ - std::vector<ZipEntry*> mEntries; -}; - -}; // namespace aapt - -#endif // __LIBS_ZIPFILE_H diff --git a/tools/aapt2/compile/Compile.cpp b/tools/aapt2/compile/Compile.cpp new file mode 100644 index 000000000000..689ace6e6aa1 --- /dev/null +++ b/tools/aapt2/compile/Compile.cpp @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ConfigDescription.h" +#include "Diagnostics.h" +#include "Flags.h" +#include "ResourceParser.h" +#include "ResourceTable.h" +#include "compile/IdAssigner.h" +#include "compile/Png.h" +#include "compile/PseudolocaleGenerator.h" +#include "compile/XmlIdCollector.h" +#include "flatten/Archive.h" +#include "flatten/FileExportWriter.h" +#include "flatten/TableFlattener.h" +#include "flatten/XmlFlattener.h" +#include "util/Files.h" +#include "util/Maybe.h" +#include "util/Util.h" +#include "xml/XmlDom.h" +#include "xml/XmlPullParser.h" + +#include <dirent.h> +#include <fstream> +#include <string> + +namespace aapt { + +struct ResourcePathData { + Source source; + std::u16string resourceDir; + std::u16string name; + std::string extension; + + // Original config str. We keep this because when we parse the config, we may add on + // version qualifiers. We want to preserve the original input so the output is easily + // computed before hand. + std::string configStr; + ConfigDescription config; +}; + +/** + * Resource file paths are expected to look like: + * [--/res/]type[-config]/name + */ +static Maybe<ResourcePathData> extractResourcePathData(const std::string& path, + std::string* outError) { + std::vector<std::string> parts = util::split(path, file::sDirSep); + if (parts.size() < 2) { + if (outError) *outError = "bad resource path"; + return {}; + } + + std::string& dir = parts[parts.size() - 2]; + StringPiece dirStr = dir; + + StringPiece configStr; + ConfigDescription config; + size_t dashPos = dir.find('-'); + if (dashPos != std::string::npos) { + configStr = dirStr.substr(dashPos + 1, dir.size() - (dashPos + 1)); + if (!ConfigDescription::parse(configStr, &config)) { + if (outError) { + std::stringstream errStr; + errStr << "invalid configuration '" << configStr << "'"; + *outError = errStr.str(); + } + return {}; + } + dirStr = dirStr.substr(0, dashPos); + } + + std::string& filename = parts[parts.size() - 1]; + StringPiece name = filename; + StringPiece extension; + size_t dotPos = filename.find('.'); + if (dotPos != std::string::npos) { + extension = name.substr(dotPos + 1, filename.size() - (dotPos + 1)); + name = name.substr(0, dotPos); + } + + return ResourcePathData{ + Source(path), + util::utf8ToUtf16(dirStr), + util::utf8ToUtf16(name), + extension.toString(), + configStr.toString(), + config + }; +} + +struct CompileOptions { + std::string outputPath; + Maybe<std::string> resDir; + std::vector<std::u16string> products; + bool pseudolocalize = false; + bool legacyMode = false; + bool verbose = false; +}; + +static std::string buildIntermediateFilename(const ResourcePathData& data) { + std::stringstream name; + name << data.resourceDir; + if (!data.configStr.empty()) { + name << "-" << data.configStr; + } + name << "_" << data.name; + if (!data.extension.empty()) { + name << "." << data.extension; + } + name << ".flat"; + return name.str(); +} + +static bool isHidden(const StringPiece& filename) { + return util::stringStartsWith<char>(filename, "."); +} + +/** + * Walks the res directory structure, looking for resource files. + */ +static bool loadInputFilesFromDir(IAaptContext* context, const CompileOptions& options, + std::vector<ResourcePathData>* outPathData) { + const std::string& rootDir = options.resDir.value(); + std::unique_ptr<DIR, decltype(closedir)*> d(opendir(rootDir.data()), closedir); + if (!d) { + context->getDiagnostics()->error(DiagMessage() << strerror(errno)); + return false; + } + + while (struct dirent* entry = readdir(d.get())) { + if (isHidden(entry->d_name)) { + continue; + } + + std::string prefixPath = rootDir; + file::appendPath(&prefixPath, entry->d_name); + + if (file::getFileType(prefixPath) != file::FileType::kDirectory) { + continue; + } + + std::unique_ptr<DIR, decltype(closedir)*> subDir(opendir(prefixPath.data()), closedir); + if (!subDir) { + context->getDiagnostics()->error(DiagMessage() << strerror(errno)); + return false; + } + + while (struct dirent* leafEntry = readdir(subDir.get())) { + if (isHidden(leafEntry->d_name)) { + continue; + } + + std::string fullPath = prefixPath; + file::appendPath(&fullPath, leafEntry->d_name); + + std::string errStr; + Maybe<ResourcePathData> pathData = extractResourcePathData(fullPath, &errStr); + if (!pathData) { + context->getDiagnostics()->error(DiagMessage() << errStr); + return false; + } + + outPathData->push_back(std::move(pathData.value())); + } + } + return true; +} + +static bool compileTable(IAaptContext* context, const CompileOptions& options, + const ResourcePathData& pathData, IArchiveWriter* writer, + const std::string& outputPath) { + ResourceTable table; + { + std::ifstream fin(pathData.source.path, std::ifstream::binary); + if (!fin) { + context->getDiagnostics()->error(DiagMessage(pathData.source) << strerror(errno)); + return false; + } + + + // Parse the values file from XML. + xml::XmlPullParser xmlParser(fin); + + ResourceParserOptions parserOptions; + parserOptions.products = options.products; + parserOptions.errorOnPositionalArguments = !options.legacyMode; + + // If the filename includes donottranslate, then the default translatable is false. + parserOptions.translatable = pathData.name.find(u"donottranslate") == std::string::npos; + + ResourceParser resParser(context->getDiagnostics(), &table, pathData.source, + pathData.config, parserOptions); + if (!resParser.parse(&xmlParser)) { + return false; + } + + fin.close(); + } + + if (options.pseudolocalize) { + // Generate pseudo-localized strings (en-XA and ar-XB). + // These are created as weak symbols, and are only generated from default configuration + // strings and plurals. + PseudolocaleGenerator pseudolocaleGenerator; + if (!pseudolocaleGenerator.consume(context, &table)) { + return false; + } + } + + // Ensure we have the compilation package at least. + table.createPackage(context->getCompilationPackage()); + + // Assign an ID to any package that has resources. + for (auto& pkg : table.packages) { + if (!pkg->id) { + // If no package ID was set while parsing (public identifiers), auto assign an ID. + pkg->id = context->getPackageId(); + } + } + + // Assign IDs to prepare the table for flattening. + IdAssigner idAssigner; + if (!idAssigner.consume(context, &table)) { + return false; + } + + // Flatten the table. + BigBuffer buffer(1024); + TableFlattenerOptions tableFlattenerOptions; + tableFlattenerOptions.useExtendedChunks = true; + TableFlattener flattener(&buffer, tableFlattenerOptions); + if (!flattener.consume(context, &table)) { + return false; + } + + if (!writer->startEntry(outputPath, 0)) { + context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to open"); + return false; + } + + if (writer->writeEntry(buffer)) { + if (writer->finishEntry()) { + return true; + } + } + + context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write"); + return false; +} + +static bool compileXml(IAaptContext* context, const CompileOptions& options, + const ResourcePathData& pathData, IArchiveWriter* writer, + const std::string& outputPath) { + + std::unique_ptr<xml::XmlResource> xmlRes; + + { + std::ifstream fin(pathData.source.path, std::ifstream::binary); + if (!fin) { + context->getDiagnostics()->error(DiagMessage(pathData.source) << strerror(errno)); + return false; + } + + xmlRes = xml::inflate(&fin, context->getDiagnostics(), pathData.source); + + fin.close(); + } + + if (!xmlRes) { + return false; + } + + // Collect IDs that are defined here. + XmlIdCollector collector; + if (!collector.consume(context, xmlRes.get())) { + return false; + } + + xmlRes->file.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name); + xmlRes->file.config = pathData.config; + xmlRes->file.source = pathData.source; + + BigBuffer buffer(1024); + ChunkWriter fileExportWriter = wrapBufferWithFileExportHeader(&buffer, &xmlRes->file); + + XmlFlattenerOptions xmlFlattenerOptions; + xmlFlattenerOptions.keepRawValues = true; + XmlFlattener flattener(fileExportWriter.getBuffer(), xmlFlattenerOptions); + if (!flattener.consume(context, xmlRes.get())) { + return false; + } + + fileExportWriter.finish(); + + if (!writer->startEntry(outputPath, 0)) { + context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to open"); + return false; + } + + if (writer->writeEntry(buffer)) { + if (writer->finishEntry()) { + return true; + } + } + + context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write"); + return false; +} + +static bool compilePng(IAaptContext* context, const CompileOptions& options, + const ResourcePathData& pathData, IArchiveWriter* writer, + const std::string& outputPath) { + BigBuffer buffer(4096); + ResourceFile resFile; + resFile.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name); + resFile.config = pathData.config; + resFile.source = pathData.source; + + ChunkWriter fileExportWriter = wrapBufferWithFileExportHeader(&buffer, &resFile); + + { + std::ifstream fin(pathData.source.path, std::ifstream::binary); + if (!fin) { + context->getDiagnostics()->error(DiagMessage(pathData.source) << strerror(errno)); + return false; + } + + Png png(context->getDiagnostics()); + if (!png.process(pathData.source, &fin, fileExportWriter.getBuffer(), {})) { + return false; + } + } + + fileExportWriter.finish(); + + if (!writer->startEntry(outputPath, 0)) { + context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to open"); + return false; + } + + if (writer->writeEntry(buffer)) { + if (writer->finishEntry()) { + return true; + } + } + + context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write"); + return false; +} + +static bool compileFile(IAaptContext* context, const CompileOptions& options, + const ResourcePathData& pathData, IArchiveWriter* writer, + const std::string& outputPath) { + BigBuffer buffer(256); + ResourceFile resFile; + resFile.name = ResourceName({}, *parseResourceType(pathData.resourceDir), pathData.name); + resFile.config = pathData.config; + resFile.source = pathData.source; + + ChunkWriter fileExportWriter = wrapBufferWithFileExportHeader(&buffer, &resFile); + + std::string errorStr; + Maybe<android::FileMap> f = file::mmapPath(pathData.source.path, &errorStr); + if (!f) { + context->getDiagnostics()->error(DiagMessage(pathData.source) << errorStr); + return false; + } + + if (!writer->startEntry(outputPath, 0)) { + context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to open"); + return false; + } + + // Manually set the size and don't call finish(). This is because we are not copying from + // the buffer the entire file. + fileExportWriter.getChunkHeader()->size = + util::hostToDevice32(buffer.size() + f.value().getDataLength()); + + if (!writer->writeEntry(buffer)) { + context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write"); + return false; + } + + // Only write if we have something to write. This is because mmap fails with length of 0, + // but we still want to compile the file to get the resource ID. + if (f.value().getDataPtr() && f.value().getDataLength() > 0) { + if (!writer->writeEntry(f.value().getDataPtr(), f.value().getDataLength())) { + context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write"); + return false; + } + } + + if (!writer->finishEntry()) { + context->getDiagnostics()->error(DiagMessage(outputPath) << "failed to write"); + return false; + } + + return true; +} + +class CompileContext : public IAaptContext { +private: + StdErrDiagnostics mDiagnostics; + +public: + IDiagnostics* getDiagnostics() override { + return &mDiagnostics; + } + + NameMangler* getNameMangler() override { + abort(); + return nullptr; + } + + StringPiece16 getCompilationPackage() override { + return {}; + } + + uint8_t getPackageId() override { + return 0x0; + } + + ISymbolTable* getExternalSymbols() override { + abort(); + return nullptr; + } +}; + +/** + * Entry point for compilation phase. Parses arguments and dispatches to the correct steps. + */ +int compile(const std::vector<StringPiece>& args) { + CompileOptions options; + + Maybe<std::string> productList; + Flags flags = Flags() + .requiredFlag("-o", "Output path", &options.outputPath) + .optionalFlag("--product", "Comma separated list of product types to compile", + &productList) + .optionalFlag("--dir", "Directory to scan for resources", &options.resDir) + .optionalSwitch("--pseudo-localize", "Generate resources for pseudo-locales " + "(en-XA and ar-XB)", &options.pseudolocalize) + .optionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings", + &options.legacyMode) + .optionalSwitch("-v", "Enables verbose logging", &options.verbose); + if (!flags.parse("aapt2 compile", args, &std::cerr)) { + return 1; + } + + if (productList) { + for (StringPiece part : util::tokenize<char>(productList.value(), ',')) { + options.products.push_back(util::utf8ToUtf16(part)); + } + } + + CompileContext context; + std::unique_ptr<IArchiveWriter> archiveWriter; + + std::vector<ResourcePathData> inputData; + if (options.resDir) { + if (!flags.getArgs().empty()) { + // Can't have both files and a resource directory. + context.getDiagnostics()->error(DiagMessage() << "files given but --dir specified"); + flags.usage("aapt2 compile", &std::cerr); + return 1; + } + + if (!loadInputFilesFromDir(&context, options, &inputData)) { + return 1; + } + + archiveWriter = createZipFileArchiveWriter(context.getDiagnostics(), options.outputPath); + + } else { + inputData.reserve(flags.getArgs().size()); + + // Collect data from the path for each input file. + for (const std::string& arg : flags.getArgs()) { + std::string errorStr; + if (Maybe<ResourcePathData> pathData = extractResourcePathData(arg, &errorStr)) { + inputData.push_back(std::move(pathData.value())); + } else { + context.getDiagnostics()->error(DiagMessage() << errorStr << " (" << arg << ")"); + return 1; + } + } + + archiveWriter = createDirectoryArchiveWriter(context.getDiagnostics(), options.outputPath); + } + + if (!archiveWriter) { + return false; + } + + bool error = false; + for (ResourcePathData& pathData : inputData) { + if (options.verbose) { + context.getDiagnostics()->note(DiagMessage(pathData.source) << "processing"); + } + + if (pathData.resourceDir == u"values") { + // Overwrite the extension. + pathData.extension = "arsc"; + + const std::string outputFilename = buildIntermediateFilename(pathData); + if (!compileTable(&context, options, pathData, archiveWriter.get(), outputFilename)) { + error = true; + } + + } else { + const std::string outputFilename = buildIntermediateFilename(pathData); + if (const ResourceType* type = parseResourceType(pathData.resourceDir)) { + if (*type != ResourceType::kRaw) { + if (pathData.extension == "xml") { + if (!compileXml(&context, options, pathData, archiveWriter.get(), + outputFilename)) { + error = true; + } + } else if (pathData.extension == "png" || pathData.extension == "9.png") { + if (!compilePng(&context, options, pathData, archiveWriter.get(), + outputFilename)) { + error = true; + } + } else { + if (!compileFile(&context, options, pathData, archiveWriter.get(), + outputFilename)) { + error = true; + } + } + } else { + if (!compileFile(&context, options, pathData, archiveWriter.get(), + outputFilename)) { + error = true; + } + } + } else { + context.getDiagnostics()->error( + DiagMessage() << "invalid file path '" << pathData.source << "'"); + error = true; + } + } + } + + if (error) { + return 1; + } + return 0; +} + +} // namespace aapt diff --git a/tools/aapt2/compile/IdAssigner.cpp b/tools/aapt2/compile/IdAssigner.cpp new file mode 100644 index 000000000000..80c6bbc1abca --- /dev/null +++ b/tools/aapt2/compile/IdAssigner.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ResourceTable.h" + +#include "compile/IdAssigner.h" +#include "process/IResourceTableConsumer.h" +#include "util/Util.h" + +#include <bitset> +#include <cassert> +#include <set> + +namespace aapt { + +bool IdAssigner::consume(IAaptContext* context, ResourceTable* table) { + std::bitset<256> usedTypeIds; + std::set<uint16_t> usedEntryIds; + + for (auto& package : table->packages) { + assert(package->id && "packages must have manually assigned IDs"); + + usedTypeIds.reset(); + + // Type ID 0 is invalid, reserve it. + usedTypeIds.set(0); + + // Collect used type IDs. + for (auto& type : package->types) { + if (type->id) { + usedEntryIds.clear(); + + if (usedTypeIds[type->id.value()]) { + // This ID is already taken! + context->getDiagnostics()->error(DiagMessage() + << "type '" << type->type << "' in " + << "package '" << package->name << "' has " + << "duplicate ID " + << std::hex << (int) type->id.value() + << std::dec); + return false; + } + + // Mark the type ID as taken. + usedTypeIds.set(type->id.value()); + } + + // Collect used entry IDs. + for (auto& entry : type->entries) { + if (entry->id) { + // Mark entry ID as taken. + if (!usedEntryIds.insert(entry->id.value()).second) { + // This ID existed before! + ResourceNameRef nameRef = + { package->name, type->type, entry->name }; + ResourceId takenId(package->id.value(), type->id.value(), + entry->id.value()); + context->getDiagnostics()->error(DiagMessage() + << "resource '" << nameRef << "' " + << "has duplicate ID '" + << takenId << "'"); + return false; + } + } + } + + // Assign unused entry IDs. + const auto endUsedEntryIter = usedEntryIds.end(); + auto nextUsedEntryIter = usedEntryIds.begin(); + uint16_t nextId = 0; + for (auto& entry : type->entries) { + if (!entry->id) { + // Assign the next available entryID. + while (nextUsedEntryIter != endUsedEntryIter && + nextId == *nextUsedEntryIter) { + nextId++; + ++nextUsedEntryIter; + } + entry->id = nextId++; + } + } + } + + // Assign unused type IDs. + size_t nextTypeId = 0; + for (auto& type : package->types) { + if (!type->id) { + while (nextTypeId < usedTypeIds.size() && usedTypeIds[nextTypeId]) { + nextTypeId++; + } + type->id = static_cast<uint8_t>(nextTypeId); + nextTypeId++; + } + } + } + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/ManifestParser.h b/tools/aapt2/compile/IdAssigner.h index f2e43d4bb695..514df3ad3861 100644 --- a/tools/aapt2/ManifestParser.h +++ b/tools/aapt2/compile/IdAssigner.h @@ -14,32 +14,21 @@ * limitations under the License. */ -#ifndef AAPT_MANIFEST_PARSER_H -#define AAPT_MANIFEST_PARSER_H +#ifndef AAPT_COMPILE_IDASSIGNER_H +#define AAPT_COMPILE_IDASSIGNER_H -#include "AppInfo.h" -#include "Logger.h" -#include "Source.h" -#include "XmlPullParser.h" +#include "process/IResourceTableConsumer.h" namespace aapt { -/* - * Parses an AndroidManifest.xml file and fills in an AppInfo structure with - * app data. +/** + * Assigns IDs to each resource in the table, respecting existing IDs and filling in gaps + * in between fixed ID assignments. */ -class ManifestParser { -public: - ManifestParser() = default; - ManifestParser(const ManifestParser&) = delete; - - bool parse(const Source& source, std::shared_ptr<XmlPullParser> parser, AppInfo* outInfo); - -private: - bool parseManifest(SourceLogger& logger, std::shared_ptr<XmlPullParser> parser, - AppInfo* outInfo); +struct IdAssigner : public IResourceTableConsumer { + bool consume(IAaptContext* context, ResourceTable* table) override; }; } // namespace aapt -#endif // AAPT_MANIFEST_PARSER_H +#endif /* AAPT_COMPILE_IDASSIGNER_H */ diff --git a/tools/aapt2/compile/IdAssigner_test.cpp b/tools/aapt2/compile/IdAssigner_test.cpp new file mode 100644 index 000000000000..e25a17ab125e --- /dev/null +++ b/tools/aapt2/compile/IdAssigner_test.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "compile/IdAssigner.h" + +#include "test/Context.h" +#include "test/Builders.h" + +#include <gtest/gtest.h> + +namespace aapt { + +::testing::AssertionResult verifyIds(ResourceTable* table); + +TEST(IdAssignerTest, AssignIds) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addSimple(u"@android:attr/foo") + .addSimple(u"@android:attr/bar") + .addSimple(u"@android:id/foo") + .setPackageId(u"android", 0x01) + .build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + IdAssigner assigner; + + ASSERT_TRUE(assigner.consume(context.get(), table.get())); + ASSERT_TRUE(verifyIds(table.get())); +} + +TEST(IdAssignerTest, AssignIdsWithReservedIds) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addSimple(u"@android:attr/foo", ResourceId(0x01040006)) + .addSimple(u"@android:attr/bar") + .addSimple(u"@android:id/foo") + .addSimple(u"@app:id/biz") + .setPackageId(u"android", 0x01) + .setPackageId(u"app", 0x7f) + .build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + IdAssigner assigner; + + ASSERT_TRUE(assigner.consume(context.get(), table.get())); + ASSERT_TRUE(verifyIds(table.get())); +} + +TEST(IdAssignerTest, FailWhenNonUniqueIdsAssigned) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addSimple(u"@android:attr/foo", ResourceId(0x01040006)) + .addSimple(u"@android:attr/bar", ResourceId(0x01040006)) + .setPackageId(u"android", 0x01) + .setPackageId(u"app", 0x7f) + .build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + IdAssigner assigner; + + ASSERT_FALSE(assigner.consume(context.get(), table.get())); +} + +::testing::AssertionResult verifyIds(ResourceTable* table) { + std::set<uint8_t> packageIds; + for (auto& package : table->packages) { + if (!package->id) { + return ::testing::AssertionFailure() << "package " << package->name << " has no ID"; + } + + if (!packageIds.insert(package->id.value()).second) { + return ::testing::AssertionFailure() << "package " << package->name + << " has non-unique ID " << std::hex << (int) package->id.value() << std::dec; + } + } + + for (auto& package : table->packages) { + std::set<uint8_t> typeIds; + for (auto& type : package->types) { + if (!type->id) { + return ::testing::AssertionFailure() << "type " << type->type << " of package " + << package->name << " has no ID"; + } + + if (!typeIds.insert(type->id.value()).second) { + return ::testing::AssertionFailure() << "type " << type->type + << " of package " << package->name << " has non-unique ID " + << std::hex << (int) type->id.value() << std::dec; + } + } + + + for (auto& type : package->types) { + std::set<uint16_t> entryIds; + for (auto& entry : type->entries) { + if (!entry->id) { + return ::testing::AssertionFailure() << "entry " << entry->name << " of type " + << type->type << " of package " << package->name << " has no ID"; + } + + if (!entryIds.insert(entry->id.value()).second) { + return ::testing::AssertionFailure() << "entry " << entry->name + << " of type " << type->type << " of package " << package->name + << " has non-unique ID " + << std::hex << (int) entry->id.value() << std::dec; + } + } + } + } + return ::testing::AssertionSuccess() << "all IDs are unique and assigned"; +} + +} // namespace aapt diff --git a/tools/aapt2/Png.cpp b/tools/aapt2/compile/Png.cpp index 4e9b68e8c95f..9837c4efc4d2 100644 --- a/tools/aapt2/Png.cpp +++ b/tools/aapt2/compile/Png.cpp @@ -14,11 +14,10 @@ * limitations under the License. */ -#include "BigBuffer.h" -#include "Logger.h" +#include "util/BigBuffer.h" #include "Png.h" #include "Source.h" -#include "Util.h" +#include "util/Util.h" #include <androidfw/ResourceTypes.h> #include <iostream> @@ -95,15 +94,14 @@ static void flushDataToStream(png_structp /*writePtr*/) { } static void logWarning(png_structp readPtr, png_const_charp warningMessage) { - SourceLogger* logger = reinterpret_cast<SourceLogger*>(png_get_error_ptr(readPtr)); - logger->warn() << warningMessage << "." << std::endl; + IDiagnostics* diag = reinterpret_cast<IDiagnostics*>(png_get_error_ptr(readPtr)); + diag->warn(DiagMessage() << warningMessage); } -static bool readPng(png_structp readPtr, png_infop infoPtr, PngInfo* outInfo, - std::string* outError) { +static bool readPng(IDiagnostics* diag, png_structp readPtr, png_infop infoPtr, PngInfo* outInfo) { if (setjmp(png_jmpbuf(readPtr))) { - *outError = "failed reading png"; + diag->error(DiagMessage() << "failed reading png"); return false; } @@ -229,7 +227,7 @@ static void checkNinePatchSerialization(android::Res_png_9patch* inPatch, void* #define MAX(a,b) ((a)>(b)?(a):(b)) #define ABS(a) ((a)<0?-(a):(a)) -static void analyze_image(SourceLogger* logger, const PngInfo& imageInfo, int grayscaleTolerance, +static void analyze_image(IDiagnostics* diag, const PngInfo& imageInfo, int grayscaleTolerance, png_colorp rgbPalette, png_bytep alphaPalette, int *paletteEntries, bool *hasTransparency, int *colorType, png_bytepp outRows) { @@ -363,9 +361,9 @@ static void analyze_image(SourceLogger* logger, const PngInfo& imageInfo, int gr *colorType = PNG_COLOR_TYPE_PALETTE; } else { if (maxGrayDeviation <= grayscaleTolerance) { - logger->note() << "forcing image to gray (max deviation = " << maxGrayDeviation - << ")." - << std::endl; + diag->note(DiagMessage() + << "forcing image to gray (max deviation = " + << maxGrayDeviation << ")"); *colorType = isOpaque ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_GRAY_ALPHA; } else { *colorType = isOpaque ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGB_ALPHA; @@ -411,10 +409,10 @@ static void analyze_image(SourceLogger* logger, const PngInfo& imageInfo, int gr } } -static bool writePng(png_structp writePtr, png_infop infoPtr, PngInfo* info, - int grayScaleTolerance, SourceLogger* logger, std::string* outError) { +static bool writePng(IDiagnostics* diag, png_structp writePtr, png_infop infoPtr, PngInfo* info, + int grayScaleTolerance) { if (setjmp(png_jmpbuf(writePtr))) { - *outError = "failed to write png"; + diag->error(DiagMessage() << "failed to write png"); return false; } @@ -442,9 +440,9 @@ static bool writePng(png_structp writePtr, png_infop infoPtr, PngInfo* info, png_set_compression_level(writePtr, Z_BEST_COMPRESSION); if (kDebug) { - logger->note() << "writing image: w = " << info->width - << ", h = " << info->height - << std::endl; + diag->note(DiagMessage() + << "writing image: w = " << info->width + << ", h = " << info->height); } png_color rgbPalette[256]; @@ -452,7 +450,7 @@ static bool writePng(png_structp writePtr, png_infop infoPtr, PngInfo* info, bool hasTransparency; int paletteEntries; - analyze_image(logger, *info, grayScaleTolerance, rgbPalette, alphaPalette, + analyze_image(diag, *info, grayScaleTolerance, rgbPalette, alphaPalette, &paletteEntries, &hasTransparency, &colorType, outRows); // If the image is a 9-patch, we need to preserve it as a ARGB file to make @@ -465,22 +463,22 @@ static bool writePng(png_structp writePtr, png_infop infoPtr, PngInfo* info, if (kDebug) { switch (colorType) { case PNG_COLOR_TYPE_PALETTE: - logger->note() << "has " << paletteEntries - << " colors" << (hasTransparency ? " (with alpha)" : "") - << ", using PNG_COLOR_TYPE_PALLETTE." - << std::endl; + diag->note(DiagMessage() + << "has " << paletteEntries + << " colors" << (hasTransparency ? " (with alpha)" : "") + << ", using PNG_COLOR_TYPE_PALLETTE"); break; case PNG_COLOR_TYPE_GRAY: - logger->note() << "is opaque gray, using PNG_COLOR_TYPE_GRAY." << std::endl; + diag->note(DiagMessage() << "is opaque gray, using PNG_COLOR_TYPE_GRAY"); break; case PNG_COLOR_TYPE_GRAY_ALPHA: - logger->note() << "is gray + alpha, using PNG_COLOR_TYPE_GRAY_ALPHA." << std::endl; + diag->note(DiagMessage() << "is gray + alpha, using PNG_COLOR_TYPE_GRAY_ALPHA"); break; case PNG_COLOR_TYPE_RGB: - logger->note() << "is opaque RGB, using PNG_COLOR_TYPE_RGB." << std::endl; + diag->note(DiagMessage() << "is opaque RGB, using PNG_COLOR_TYPE_RGB"); break; case PNG_COLOR_TYPE_RGB_ALPHA: - logger->note() << "is RGB + alpha, using PNG_COLOR_TYPE_RGB_ALPHA." << std::endl; + diag->note(DiagMessage() << "is RGB + alpha, using PNG_COLOR_TYPE_RGB_ALPHA"); break; } } @@ -511,7 +509,7 @@ static bool writePng(png_structp writePtr, png_infop infoPtr, PngInfo* info, // base 9 patch data if (kDebug) { - logger->note() << "adding 9-patch info..." << std::endl; + diag->note(DiagMessage() << "adding 9-patch info.."); } strcpy((char*)unknowns[pIndex].name, "npTc"); unknowns[pIndex].data = (png_byte*) info->serialize9Patch(); @@ -587,10 +585,10 @@ static bool writePng(png_structp writePtr, png_infop infoPtr, PngInfo* info, &compressionType, nullptr); if (kDebug) { - logger->note() << "image written: w = " << width << ", h = " << height - << ", d = " << bitDepth << ", colors = " << colorType - << ", inter = " << interlaceType << ", comp = " << compressionType - << std::endl; + diag->note(DiagMessage() + << "image written: w = " << width << ", h = " << height + << ", d = " << bitDepth << ", colors = " << colorType + << ", inter = " << interlaceType << ", comp = " << compressionType); } return true; } @@ -1192,23 +1190,22 @@ getout: } -bool Png::process(const Source& source, std::istream& input, BigBuffer* outBuffer, - const Options& options, std::string* outError) { +bool Png::process(const Source& source, std::istream* input, BigBuffer* outBuffer, + const PngOptions& options) { png_byte signature[kPngSignatureSize]; // Read the PNG signature first. - if (!input.read(reinterpret_cast<char*>(signature), kPngSignatureSize)) { - *outError = strerror(errno); + if (!input->read(reinterpret_cast<char*>(signature), kPngSignatureSize)) { + mDiag->error(DiagMessage() << strerror(errno)); return false; } // If the PNG signature doesn't match, bail early. if (png_sig_cmp(signature, 0, kPngSignatureSize) != 0) { - *outError = "not a valid png file"; + mDiag->error(DiagMessage() << "not a valid png file"); return false; } - SourceLogger logger(source); bool result = false; png_structp readPtr = nullptr; png_infop infoPtr = nullptr; @@ -1218,40 +1215,42 @@ bool Png::process(const Source& source, std::istream& input, BigBuffer* outBuffe readPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); if (!readPtr) { - *outError = "failed to allocate read ptr"; + mDiag->error(DiagMessage() << "failed to allocate read ptr"); goto bail; } infoPtr = png_create_info_struct(readPtr); if (!infoPtr) { - *outError = "failed to allocate info ptr"; + mDiag->error(DiagMessage() << "failed to allocate info ptr"); goto bail; } - png_set_error_fn(readPtr, reinterpret_cast<png_voidp>(&logger), nullptr, logWarning); + png_set_error_fn(readPtr, reinterpret_cast<png_voidp>(mDiag), nullptr, logWarning); // Set the read function to read from std::istream. - png_set_read_fn(readPtr, (png_voidp)&input, readDataFromStream); + png_set_read_fn(readPtr, (png_voidp) input, readDataFromStream); - if (!readPng(readPtr, infoPtr, &pngInfo, outError)) { + if (!readPng(mDiag, readPtr, infoPtr, &pngInfo)) { goto bail; } if (util::stringEndsWith<char>(source.path, ".9.png")) { - if (!do9Patch(&pngInfo, outError)) { + std::string errorMsg; + if (!do9Patch(&pngInfo, &errorMsg)) { + mDiag->error(DiagMessage() << errorMsg); goto bail; } } writePtr = png_create_write_struct(PNG_LIBPNG_VER_STRING, 0, nullptr, nullptr); if (!writePtr) { - *outError = "failed to allocate write ptr"; + mDiag->error(DiagMessage() << "failed to allocate write ptr"); goto bail; } writeInfoPtr = png_create_info_struct(writePtr); if (!writeInfoPtr) { - *outError = "failed to allocate write info ptr"; + mDiag->error(DiagMessage() << "failed to allocate write info ptr"); goto bail; } @@ -1260,8 +1259,7 @@ bool Png::process(const Source& source, std::istream& input, BigBuffer* outBuffe // Set the write function to write to std::ostream. png_set_write_fn(writePtr, (png_voidp)outBuffer, writeDataToStream, flushDataToStream); - if (!writePng(writePtr, writeInfoPtr, &pngInfo, options.grayScaleTolerance, &logger, - outError)) { + if (!writePng(mDiag, writePtr, writeInfoPtr, &pngInfo, options.grayScaleTolerance)) { goto bail; } diff --git a/tools/aapt2/Png.h b/tools/aapt2/compile/Png.h index 4577ab89d2d9..345ff6c56870 100644 --- a/tools/aapt2/Png.h +++ b/tools/aapt2/compile/Png.h @@ -17,7 +17,8 @@ #ifndef AAPT_PNG_H #define AAPT_PNG_H -#include "BigBuffer.h" +#include "util/BigBuffer.h" +#include "Diagnostics.h" #include "Source.h" #include <iostream> @@ -25,13 +26,20 @@ namespace aapt { -struct Png { - struct Options { - int grayScaleTolerance = 0; - }; +struct PngOptions { + int grayScaleTolerance = 0; +}; + +class Png { +public: + Png(IDiagnostics* diag) : mDiag(diag) { + } + + bool process(const Source& source, std::istream* input, BigBuffer* outBuffer, + const PngOptions& options); - bool process(const Source& source, std::istream& input, BigBuffer* outBuffer, - const Options& options, std::string* outError); +private: + IDiagnostics* mDiag; }; } // namespace aapt diff --git a/tools/aapt2/compile/PseudolocaleGenerator.cpp b/tools/aapt2/compile/PseudolocaleGenerator.cpp new file mode 100644 index 000000000000..2963d135cbca --- /dev/null +++ b/tools/aapt2/compile/PseudolocaleGenerator.cpp @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "ValueVisitor.h" +#include "compile/PseudolocaleGenerator.h" +#include "compile/Pseudolocalizer.h" +#include "util/Comparators.h" + +namespace aapt { + +std::unique_ptr<StyledString> pseudolocalizeStyledString(StyledString* string, + Pseudolocalizer::Method method, + StringPool* pool) { + Pseudolocalizer localizer(method); + + const StringPiece16 originalText = *string->value->str; + + StyleString localized; + + // Copy the spans. We will update their offsets when we localize. + localized.spans.reserve(string->value->spans.size()); + for (const StringPool::Span& span : string->value->spans) { + localized.spans.push_back(Span{ *span.name, span.firstChar, span.lastChar }); + } + + // The ranges are all represented with a single value. This is the start of one range and + // end of another. + struct Range { + size_t start; + + // Once the new string is localized, these are the pointers to the spans to adjust. + // Since this struct represents the start of one range and end of another, we have + // the two pointers respectively. + uint32_t* updateStart; + uint32_t* updateEnd; + }; + + auto cmp = [](const Range& r, size_t index) -> bool { + return r.start < index; + }; + + // Construct the ranges. The ranges are represented like so: [0, 2, 5, 7] + // The ranges are the spaces in between. In this example, with a total string length of 9, + // the vector represents: (0,1], (2,4], (5,6], (7,9] + // + std::vector<Range> ranges; + ranges.push_back(Range{ 0 }); + ranges.push_back(Range{ originalText.size() - 1 }); + for (size_t i = 0; i < string->value->spans.size(); i++) { + const StringPool::Span& span = string->value->spans[i]; + + // Insert or update the Range marker for the start of this span. + auto iter = std::lower_bound(ranges.begin(), ranges.end(), span.firstChar, cmp); + if (iter != ranges.end() && iter->start == span.firstChar) { + iter->updateStart = &localized.spans[i].firstChar; + } else { + ranges.insert(iter, + Range{ span.firstChar, &localized.spans[i].firstChar, nullptr }); + } + + // Insert or update the Range marker for the end of this span. + iter = std::lower_bound(ranges.begin(), ranges.end(), span.lastChar, cmp); + if (iter != ranges.end() && iter->start == span.lastChar) { + iter->updateEnd = &localized.spans[i].lastChar; + } else { + ranges.insert(iter, + Range{ span.lastChar, nullptr, &localized.spans[i].lastChar }); + } + } + + localized.str += localizer.start(); + + // Iterate over the ranges and localize each section. + for (size_t i = 0; i < ranges.size(); i++) { + const size_t start = ranges[i].start; + size_t len = originalText.size() - start; + if (i + 1 < ranges.size()) { + len = ranges[i + 1].start - start; + } + + if (ranges[i].updateStart) { + *ranges[i].updateStart = localized.str.size(); + } + + if (ranges[i].updateEnd) { + *ranges[i].updateEnd = localized.str.size(); + } + + localized.str += localizer.text(originalText.substr(start, len)); + } + + localized.str += localizer.end(); + + std::unique_ptr<StyledString> localizedString = util::make_unique<StyledString>( + pool->makeRef(localized)); + localizedString->setSource(string->getSource()); + return localizedString; +} + +namespace { + +struct Visitor : public RawValueVisitor { + StringPool* mPool; + Pseudolocalizer::Method mMethod; + Pseudolocalizer mLocalizer; + + // Either value or item will be populated upon visiting the value. + std::unique_ptr<Value> mValue; + std::unique_ptr<Item> mItem; + + Visitor(StringPool* pool, Pseudolocalizer::Method method) : + mPool(pool), mMethod(method), mLocalizer(method) { + } + + void visit(Array* array) override { + std::unique_ptr<Array> localized = util::make_unique<Array>(); + localized->items.resize(array->items.size()); + for (size_t i = 0; i < array->items.size(); i++) { + Visitor subVisitor(mPool, mMethod); + array->items[i]->accept(&subVisitor); + if (subVisitor.mItem) { + localized->items[i] = std::move(subVisitor.mItem); + } else { + localized->items[i] = std::unique_ptr<Item>(array->items[i]->clone(mPool)); + } + } + localized->setSource(array->getSource()); + localized->setWeak(true); + mValue = std::move(localized); + } + + void visit(Plural* plural) override { + std::unique_ptr<Plural> localized = util::make_unique<Plural>(); + for (size_t i = 0; i < plural->values.size(); i++) { + Visitor subVisitor(mPool, mMethod); + if (plural->values[i]) { + plural->values[i]->accept(&subVisitor); + if (subVisitor.mValue) { + localized->values[i] = std::move(subVisitor.mItem); + } else { + localized->values[i] = std::unique_ptr<Item>(plural->values[i]->clone(mPool)); + } + } + } + localized->setSource(plural->getSource()); + localized->setWeak(true); + mValue = std::move(localized); + } + + void visit(String* string) override { + if (!string->isTranslateable()) { + return; + } + + std::u16string result = mLocalizer.start() + mLocalizer.text(*string->value) + + mLocalizer.end(); + std::unique_ptr<String> localized = util::make_unique<String>(mPool->makeRef(result)); + localized->setSource(string->getSource()); + localized->setWeak(true); + mItem = std::move(localized); + } + + void visit(StyledString* string) override { + if (!string->isTranslateable()) { + return; + } + + mItem = pseudolocalizeStyledString(string, mMethod, mPool); + mItem->setWeak(true); + } +}; + +ConfigDescription modifyConfigForPseudoLocale(const ConfigDescription& base, + Pseudolocalizer::Method m) { + ConfigDescription modified = base; + switch (m) { + case Pseudolocalizer::Method::kAccent: + modified.language[0] = 'e'; + modified.language[1] = 'n'; + modified.country[0] = 'X'; + modified.country[1] = 'A'; + break; + + case Pseudolocalizer::Method::kBidi: + modified.language[0] = 'a'; + modified.language[1] = 'r'; + modified.country[0] = 'X'; + modified.country[1] = 'B'; + break; + default: + break; + } + return modified; +} + +void pseudolocalizeIfNeeded(std::vector<ResourceConfigValue>* configValues, + Pseudolocalizer::Method method, StringPool* pool, Value* value) { + Visitor visitor(pool, method); + value->accept(&visitor); + + std::unique_ptr<Value> localizedValue; + if (visitor.mValue) { + localizedValue = std::move(visitor.mValue); + } else if (visitor.mItem) { + localizedValue = std::move(visitor.mItem); + } + + if (localizedValue) { + ConfigDescription pseudolocalizedConfig = modifyConfigForPseudoLocale(ConfigDescription{}, + method); + auto iter = std::lower_bound(configValues->begin(), configValues->end(), + pseudolocalizedConfig, cmp::lessThanConfig); + if (iter == configValues->end() || iter->config != pseudolocalizedConfig) { + // The pseudolocalized config doesn't exist, add it. + configValues->insert(iter, ResourceConfigValue{ pseudolocalizedConfig, + std::move(localizedValue) }); + } + } +} + +} // namespace + +bool PseudolocaleGenerator::consume(IAaptContext* context, ResourceTable* table) { + for (auto& package : table->packages) { + for (auto& type : package->types) { + for (auto& entry : type->entries) { + auto iter = std::lower_bound(entry->values.begin(), entry->values.end(), + ConfigDescription{}, cmp::lessThanConfig); + if (iter != entry->values.end() && iter->config == ConfigDescription{}) { + // Only pseudolocalize the default configuration. + + // The iterator will be invalidated, so grab a pointer to the value. + Value* originalValue = iter->value.get(); + + pseudolocalizeIfNeeded(&entry->values, Pseudolocalizer::Method::kAccent, + &table->stringPool, originalValue); + pseudolocalizeIfNeeded(&entry->values, Pseudolocalizer::Method::kBidi, + &table->stringPool, originalValue); + } + } + } + } + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/compile/PseudolocaleGenerator.h b/tools/aapt2/compile/PseudolocaleGenerator.h new file mode 100644 index 000000000000..4fbc51607595 --- /dev/null +++ b/tools/aapt2/compile/PseudolocaleGenerator.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_COMPILE_PSEUDOLOCALEGENERATOR_H +#define AAPT_COMPILE_PSEUDOLOCALEGENERATOR_H + +#include "StringPool.h" +#include "compile/Pseudolocalizer.h" +#include "process/IResourceTableConsumer.h" + +namespace aapt { + +std::unique_ptr<StyledString> pseudolocalizeStyledString(StyledString* string, + Pseudolocalizer::Method method, + StringPool* pool); + +struct PseudolocaleGenerator : public IResourceTableConsumer { + bool consume(IAaptContext* context, ResourceTable* table) override; +}; + +} // namespace aapt + +#endif /* AAPT_COMPILE_PSEUDOLOCALEGENERATOR_H */ diff --git a/tools/aapt2/compile/PseudolocaleGenerator_test.cpp b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp new file mode 100644 index 000000000000..4cb6ea2db565 --- /dev/null +++ b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "compile/PseudolocaleGenerator.h" +#include "test/Builders.h" +#include "test/Common.h" +#include "test/Context.h" +#include "util/Util.h" + +#include <androidfw/ResourceTypes.h> +#include <gtest/gtest.h> + +namespace aapt { + +TEST(PseudolocaleGeneratorTest, PseudolocalizeStyledString) { + StringPool pool; + StyleString originalStyle; + originalStyle.str = u"Hello world!"; + originalStyle.spans = { Span{ u"b", 2, 3 }, Span{ u"b", 6, 7 }, Span{ u"i", 1, 10 } }; + + std::unique_ptr<StyledString> newString = pseudolocalizeStyledString( + util::make_unique<StyledString>(pool.makeRef(originalStyle)).get(), + Pseudolocalizer::Method::kNone, &pool); + + EXPECT_EQ(originalStyle.str, *newString->value->str); + ASSERT_EQ(originalStyle.spans.size(), newString->value->spans.size()); + + EXPECT_EQ(2u, newString->value->spans[0].firstChar); + EXPECT_EQ(3u, newString->value->spans[0].lastChar); + EXPECT_EQ(std::u16string(u"b"), *newString->value->spans[0].name); + + EXPECT_EQ(6u, newString->value->spans[1].firstChar); + EXPECT_EQ(7u, newString->value->spans[1].lastChar); + EXPECT_EQ(std::u16string(u"b"), *newString->value->spans[1].name); + + EXPECT_EQ(1u, newString->value->spans[2].firstChar); + EXPECT_EQ(10u, newString->value->spans[2].lastChar); + EXPECT_EQ(std::u16string(u"i"), *newString->value->spans[2].name); + + originalStyle.spans.push_back(Span{ u"em", 0, 11u }); + + newString = pseudolocalizeStyledString( + util::make_unique<StyledString>(pool.makeRef(originalStyle)).get(), + Pseudolocalizer::Method::kAccent, &pool); + + EXPECT_EQ(std::u16string(u"[Ĥéļļö ŵöŕļð¡ one two]"), *newString->value->str); + ASSERT_EQ(originalStyle.spans.size(), newString->value->spans.size()); + + EXPECT_EQ(3u, newString->value->spans[0].firstChar); + EXPECT_EQ(4u, newString->value->spans[0].lastChar); + + EXPECT_EQ(7u, newString->value->spans[1].firstChar); + EXPECT_EQ(8u, newString->value->spans[1].lastChar); + + EXPECT_EQ(2u, newString->value->spans[2].firstChar); + EXPECT_EQ(11u, newString->value->spans[2].lastChar); + + EXPECT_EQ(1u, newString->value->spans[3].firstChar); + EXPECT_EQ(12u, newString->value->spans[3].lastChar); +} + +TEST(PseudolocaleGeneratorTest, PseudolocalizeOnlyDefaultConfigs) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addString(u"@android:string/one", u"one") + .addString(u"@android:string/two", ResourceId{}, test::parseConfigOrDie("en"), u"two") + .addString(u"@android:string/three", u"three") + .addString(u"@android:string/three", ResourceId{}, test::parseConfigOrDie("en-rXA"), + u"three") + .addString(u"@android:string/four", u"four") + .build(); + + String* val = test::getValue<String>(table.get(), u"@android:string/four"); + val->setTranslateable(false); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + PseudolocaleGenerator generator; + ASSERT_TRUE(generator.consume(context.get(), table.get())); + + // Normal pseudolocalization should take place. + ASSERT_NE(nullptr, test::getValueForConfig<String>(table.get(), u"@android:string/one", + test::parseConfigOrDie("en-rXA"))); + ASSERT_NE(nullptr, test::getValueForConfig<String>(table.get(), u"@android:string/one", + test::parseConfigOrDie("ar-rXB"))); + + // No default config for android:string/two, so no pseudlocales should exist. + ASSERT_EQ(nullptr, test::getValueForConfig<String>(table.get(), u"@android:string/two", + test::parseConfigOrDie("en-rXA"))); + ASSERT_EQ(nullptr, test::getValueForConfig<String>(table.get(), u"@android:string/two", + test::parseConfigOrDie("ar-rXB"))); + + + // Check that we didn't override manual pseudolocalization. + val = test::getValueForConfig<String>(table.get(), u"@android:string/three", + test::parseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, val); + EXPECT_EQ(std::u16string(u"three"), *val->value); + + ASSERT_NE(nullptr, test::getValueForConfig<String>(table.get(), u"@android:string/three", + test::parseConfigOrDie("ar-rXB"))); + + // Check that four's translateable marker was honored. + ASSERT_EQ(nullptr, test::getValueForConfig<String>(table.get(), u"@android:string/four", + test::parseConfigOrDie("en-rXA"))); + ASSERT_EQ(nullptr, test::getValueForConfig<String>(table.get(), u"@android:string/four", + test::parseConfigOrDie("ar-rXB"))); + +} + +} // namespace aapt + diff --git a/tools/aapt2/compile/Pseudolocalizer.cpp b/tools/aapt2/compile/Pseudolocalizer.cpp new file mode 100644 index 000000000000..eae52d778744 --- /dev/null +++ b/tools/aapt2/compile/Pseudolocalizer.cpp @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "compile/Pseudolocalizer.h" +#include "util/Util.h" + +namespace aapt { + +// String basis to generate expansion +static const std::u16string k_expansion_string = u"one two three " + "four five six seven eight nine ten eleven twelve thirteen " + "fourteen fiveteen sixteen seventeen nineteen twenty"; + +// Special unicode characters to override directionality of the words +static const std::u16string k_rlm = u"\u200f"; +static const std::u16string k_rlo = u"\u202e"; +static const std::u16string k_pdf = u"\u202c"; + +// Placeholder marks +static const std::u16string k_placeholder_open = u"\u00bb"; +static const std::u16string k_placeholder_close = u"\u00ab"; + +static const char16_t k_arg_start = u'{'; +static const char16_t k_arg_end = u'}'; + +class PseudoMethodNone : public PseudoMethodImpl { +public: + std::u16string text(const StringPiece16& text) override { return text.toString(); } + std::u16string placeholder(const StringPiece16& text) override { return text.toString(); } +}; + +class PseudoMethodBidi : public PseudoMethodImpl { +public: + std::u16string text(const StringPiece16& text) override; + std::u16string placeholder(const StringPiece16& text) override; +}; + +class PseudoMethodAccent : public PseudoMethodImpl { +public: + PseudoMethodAccent() : mDepth(0), mWordCount(0), mLength(0) {} + std::u16string start() override; + std::u16string end() override; + std::u16string text(const StringPiece16& text) override; + std::u16string placeholder(const StringPiece16& text) override; +private: + size_t mDepth; + size_t mWordCount; + size_t mLength; +}; + +Pseudolocalizer::Pseudolocalizer(Method method) : mLastDepth(0) { + setMethod(method); +} + +void Pseudolocalizer::setMethod(Method method) { + switch (method) { + case Method::kNone: + mImpl = util::make_unique<PseudoMethodNone>(); + break; + case Method::kAccent: + mImpl = util::make_unique<PseudoMethodAccent>(); + break; + case Method::kBidi: + mImpl = util::make_unique<PseudoMethodBidi>(); + break; + } +} + +std::u16string Pseudolocalizer::text(const StringPiece16& text) { + std::u16string out; + size_t depth = mLastDepth; + size_t lastpos, pos; + const size_t length = text.size(); + const char16_t* str = text.data(); + bool escaped = false; + for (lastpos = pos = 0; pos < length; pos++) { + char16_t c = str[pos]; + if (escaped) { + escaped = false; + continue; + } + if (c == '\'') { + escaped = true; + continue; + } + + if (c == k_arg_start) { + depth++; + } else if (c == k_arg_end && depth) { + depth--; + } + + if (mLastDepth != depth || pos == length - 1) { + bool pseudo = ((mLastDepth % 2) == 0); + size_t nextpos = pos; + if (!pseudo || depth == mLastDepth) { + nextpos++; + } + size_t size = nextpos - lastpos; + if (size) { + std::u16string chunk = text.substr(lastpos, size).toString(); + if (pseudo) { + chunk = mImpl->text(chunk); + } else if (str[lastpos] == k_arg_start && str[nextpos - 1] == k_arg_end) { + chunk = mImpl->placeholder(chunk); + } + out.append(chunk); + } + if (pseudo && depth < mLastDepth) { // End of message + out.append(mImpl->end()); + } else if (!pseudo && depth > mLastDepth) { // Start of message + out.append(mImpl->start()); + } + lastpos = nextpos; + mLastDepth = depth; + } + } + return out; +} + +static const char16_t* pseudolocalizeChar(const char16_t c) { + switch (c) { + case 'a': return u"\u00e5"; + case 'b': return u"\u0253"; + case 'c': return u"\u00e7"; + case 'd': return u"\u00f0"; + case 'e': return u"\u00e9"; + case 'f': return u"\u0192"; + case 'g': return u"\u011d"; + case 'h': return u"\u0125"; + case 'i': return u"\u00ee"; + case 'j': return u"\u0135"; + case 'k': return u"\u0137"; + case 'l': return u"\u013c"; + case 'm': return u"\u1e3f"; + case 'n': return u"\u00f1"; + case 'o': return u"\u00f6"; + case 'p': return u"\u00fe"; + case 'q': return u"\u0051"; + case 'r': return u"\u0155"; + case 's': return u"\u0161"; + case 't': return u"\u0163"; + case 'u': return u"\u00fb"; + case 'v': return u"\u0056"; + case 'w': return u"\u0175"; + case 'x': return u"\u0445"; + case 'y': return u"\u00fd"; + case 'z': return u"\u017e"; + case 'A': return u"\u00c5"; + case 'B': return u"\u03b2"; + case 'C': return u"\u00c7"; + case 'D': return u"\u00d0"; + case 'E': return u"\u00c9"; + case 'G': return u"\u011c"; + case 'H': return u"\u0124"; + case 'I': return u"\u00ce"; + case 'J': return u"\u0134"; + case 'K': return u"\u0136"; + case 'L': return u"\u013b"; + case 'M': return u"\u1e3e"; + case 'N': return u"\u00d1"; + case 'O': return u"\u00d6"; + case 'P': return u"\u00de"; + case 'Q': return u"\u0071"; + case 'R': return u"\u0154"; + case 'S': return u"\u0160"; + case 'T': return u"\u0162"; + case 'U': return u"\u00db"; + case 'V': return u"\u03bd"; + case 'W': return u"\u0174"; + case 'X': return u"\u00d7"; + case 'Y': return u"\u00dd"; + case 'Z': return u"\u017d"; + case '!': return u"\u00a1"; + case '?': return u"\u00bf"; + case '$': return u"\u20ac"; + default: return NULL; + } +} + +static bool isPossibleNormalPlaceholderEnd(const char16_t c) { + switch (c) { + case 's': return true; + case 'S': return true; + case 'c': return true; + case 'C': return true; + case 'd': return true; + case 'o': return true; + case 'x': return true; + case 'X': return true; + case 'f': return true; + case 'e': return true; + case 'E': return true; + case 'g': return true; + case 'G': return true; + case 'a': return true; + case 'A': return true; + case 'b': return true; + case 'B': return true; + case 'h': return true; + case 'H': return true; + case '%': return true; + case 'n': return true; + default: return false; + } +} + +static std::u16string pseudoGenerateExpansion(const unsigned int length) { + std::u16string result = k_expansion_string; + const char16_t* s = result.data(); + if (result.size() < length) { + result += u" "; + result += pseudoGenerateExpansion(length - result.size()); + } else { + int ext = 0; + // Should contain only whole words, so looking for a space + for (unsigned int i = length + 1; i < result.size(); ++i) { + ++ext; + if (s[i] == ' ') { + break; + } + } + result = result.substr(0, length + ext); + } + return result; +} + +std::u16string PseudoMethodAccent::start() { + std::u16string result; + if (mDepth == 0) { + result = u"["; + } + mWordCount = mLength = 0; + mDepth++; + return result; +} + +std::u16string PseudoMethodAccent::end() { + std::u16string result; + if (mLength) { + result += u" "; + result += pseudoGenerateExpansion(mWordCount > 3 ? mLength : mLength / 2); + } + mWordCount = mLength = 0; + mDepth--; + if (mDepth == 0) { + result += u"]"; + } + return result; +} + +/** + * Converts characters so they look like they've been localized. + * + * Note: This leaves placeholder syntax untouched. + */ +std::u16string PseudoMethodAccent::text(const StringPiece16& source) +{ + const char16_t* s = source.data(); + std::u16string result; + const size_t I = source.size(); + bool lastspace = true; + for (size_t i = 0; i < I; i++) { + char16_t c = s[i]; + if (c == '%') { + // Placeholder syntax, no need to pseudolocalize + std::u16string chunk; + bool end = false; + chunk.append(&c, 1); + while (!end && i < I) { + ++i; + c = s[i]; + chunk.append(&c, 1); + if (isPossibleNormalPlaceholderEnd(c)) { + end = true; + } else if (c == 't') { + ++i; + c = s[i]; + chunk.append(&c, 1); + end = true; + } + } + // Treat chunk as a placeholder unless it ends with %. + result += ((c == '%') ? chunk : placeholder(chunk)); + } else if (c == '<' || c == '&') { + // html syntax, no need to pseudolocalize + bool tag_closed = false; + while (!tag_closed && i < I) { + if (c == '&') { + std::u16string escapeText; + escapeText.append(&c, 1); + bool end = false; + size_t htmlCodePos = i; + while (!end && htmlCodePos < I) { + ++htmlCodePos; + c = s[htmlCodePos]; + escapeText.append(&c, 1); + // Valid html code + if (c == ';') { + end = true; + i = htmlCodePos; + } + // Wrong html code + else if (!((c == '#' || + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9')))) { + end = true; + } + } + result += escapeText; + if (escapeText != u"<") { + tag_closed = true; + } + continue; + } + if (c == '>') { + tag_closed = true; + result.append(&c, 1); + continue; + } + result.append(&c, 1); + i++; + c = s[i]; + } + } else { + // This is a pure text that should be pseudolocalized + const char16_t* p = pseudolocalizeChar(c); + if (p != nullptr) { + result += p; + } else { + bool space = util::isspace16(c); + if (lastspace && !space) { + mWordCount++; + } + lastspace = space; + result.append(&c, 1); + } + // Count only pseudolocalizable chars and delimiters + mLength++; + } + } + return result; +} + +std::u16string PseudoMethodAccent::placeholder(const StringPiece16& source) { + // Surround a placeholder with brackets + return k_placeholder_open + source.toString() + k_placeholder_close; +} + +std::u16string PseudoMethodBidi::text(const StringPiece16& source) { + const char16_t* s = source.data(); + std::u16string result; + bool lastspace = true; + bool space = true; + for (size_t i = 0; i < source.size(); i++) { + char16_t c = s[i]; + space = util::isspace16(c); + if (lastspace && !space) { + // Word start + result += k_rlm + k_rlo; + } else if (!lastspace && space) { + // Word end + result += k_pdf + k_rlm; + } + lastspace = space; + result.append(&c, 1); + } + if (!lastspace) { + // End of last word + result += k_pdf + k_rlm; + } + return result; +} + +std::u16string PseudoMethodBidi::placeholder(const StringPiece16& source) { + // Surround a placeholder with directionality change sequence + return k_rlm + k_rlo + source.toString() + k_pdf + k_rlm; +} + +} // namespace aapt diff --git a/tools/aapt2/compile/Pseudolocalizer.h b/tools/aapt2/compile/Pseudolocalizer.h new file mode 100644 index 000000000000..8818c1725617 --- /dev/null +++ b/tools/aapt2/compile/Pseudolocalizer.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_COMPILE_PSEUDOLOCALIZE_H +#define AAPT_COMPILE_PSEUDOLOCALIZE_H + +#include "ResourceValues.h" +#include "StringPool.h" +#include "util/StringPiece.h" + +#include <android-base/macros.h> +#include <memory> + +namespace aapt { + +class PseudoMethodImpl { +public: + virtual ~PseudoMethodImpl() {} + virtual std::u16string start() { return {}; } + virtual std::u16string end() { return {}; } + virtual std::u16string text(const StringPiece16& text) = 0; + virtual std::u16string placeholder(const StringPiece16& text) = 0; +}; + +class Pseudolocalizer { +public: + enum class Method { + kNone, + kAccent, + kBidi, + }; + + Pseudolocalizer(Method method); + void setMethod(Method method); + std::u16string start() { return mImpl->start(); } + std::u16string end() { return mImpl->end(); } + std::u16string text(const StringPiece16& text); +private: + std::unique_ptr<PseudoMethodImpl> mImpl; + size_t mLastDepth; +}; + +} // namespace aapt + +#endif /* AAPT_COMPILE_PSEUDOLOCALIZE_H */ diff --git a/tools/aapt2/compile/Pseudolocalizer_test.cpp b/tools/aapt2/compile/Pseudolocalizer_test.cpp new file mode 100644 index 000000000000..b0bc2c10fbe0 --- /dev/null +++ b/tools/aapt2/compile/Pseudolocalizer_test.cpp @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "compile/Pseudolocalizer.h" +#include "util/Util.h" + +#include <androidfw/ResourceTypes.h> +#include <gtest/gtest.h> + +namespace aapt { + +// In this context, 'Axis' represents a particular field in the configuration, +// such as language or density. + +static ::testing::AssertionResult simpleHelper(const char* input, const char* expected, + Pseudolocalizer::Method method) { + Pseudolocalizer pseudo(method); + std::string result = util::utf16ToUtf8( + pseudo.start() + pseudo.text(util::utf8ToUtf16(input)) + pseudo.end()); + if (StringPiece(expected) != result) { + return ::testing::AssertionFailure() << expected << " != " << result; + } + return ::testing::AssertionSuccess(); +} + +static ::testing::AssertionResult compoundHelper(const char* in1, const char* in2, const char *in3, + const char* expected, + Pseudolocalizer::Method method) { + Pseudolocalizer pseudo(method); + std::string result = util::utf16ToUtf8(pseudo.start() + + pseudo.text(util::utf8ToUtf16(in1)) + + pseudo.text(util::utf8ToUtf16(in2)) + + pseudo.text(util::utf8ToUtf16(in3)) + + pseudo.end()); + if (StringPiece(expected) != result) { + return ::testing::AssertionFailure() << expected << " != " << result; + } + return ::testing::AssertionSuccess(); +} + +TEST(PseudolocalizerTest, NoPseudolocalization) { + EXPECT_TRUE(simpleHelper("", "", Pseudolocalizer::Method::kNone)); + EXPECT_TRUE(simpleHelper("Hello, world", "Hello, world", Pseudolocalizer::Method::kNone)); + + EXPECT_TRUE(compoundHelper("Hello,", " world", "", + "Hello, world", Pseudolocalizer::Method::kNone)); +} + +TEST(PseudolocalizerTest, PlaintextAccent) { + EXPECT_TRUE(simpleHelper("", "[]", Pseudolocalizer::Method::kAccent)); + EXPECT_TRUE(simpleHelper("Hello, world", + "[Ĥéļļö, ŵöŕļð one two]", Pseudolocalizer::Method::kAccent)); + + EXPECT_TRUE(simpleHelper("Hello, %1d", + "[Ĥéļļö, »%1d« one two]", Pseudolocalizer::Method::kAccent)); + + EXPECT_TRUE(simpleHelper("Battery %1d%%", + "[βåţţéŕý »%1d«%% one two]", Pseudolocalizer::Method::kAccent)); + + EXPECT_TRUE(compoundHelper("", "", "", "[]", Pseudolocalizer::Method::kAccent)); + EXPECT_TRUE(compoundHelper("Hello,", " world", "", + "[Ĥéļļö, ŵöŕļð one two]", Pseudolocalizer::Method::kAccent)); +} + +TEST(PseudolocalizerTest, PlaintextBidi) { + EXPECT_TRUE(simpleHelper("", "", Pseudolocalizer::Method::kBidi)); + EXPECT_TRUE(simpleHelper("word", + "\xe2\x80\x8f\xE2\x80\xaeword\xE2\x80\xac\xe2\x80\x8f", + Pseudolocalizer::Method::kBidi)); + EXPECT_TRUE(simpleHelper(" word ", + " \xe2\x80\x8f\xE2\x80\xaeword\xE2\x80\xac\xe2\x80\x8f ", + Pseudolocalizer::Method::kBidi)); + EXPECT_TRUE(simpleHelper(" word ", + " \xe2\x80\x8f\xE2\x80\xaeword\xE2\x80\xac\xe2\x80\x8f ", + Pseudolocalizer::Method::kBidi)); + EXPECT_TRUE(simpleHelper("hello\n world\n", + "\xe2\x80\x8f\xE2\x80\xaehello\xE2\x80\xac\xe2\x80\x8f\n" \ + " \xe2\x80\x8f\xE2\x80\xaeworld\xE2\x80\xac\xe2\x80\x8f\n", + Pseudolocalizer::Method::kBidi)); + EXPECT_TRUE(compoundHelper("hello", "\n ", " world\n", + "\xe2\x80\x8f\xE2\x80\xaehello\xE2\x80\xac\xe2\x80\x8f\n" \ + " \xe2\x80\x8f\xE2\x80\xaeworld\xE2\x80\xac\xe2\x80\x8f\n", + Pseudolocalizer::Method::kBidi)); +} + +TEST(PseudolocalizerTest, SimpleICU) { + // Single-fragment messages + EXPECT_TRUE(simpleHelper("{placeholder}", "[»{placeholder}«]", + Pseudolocalizer::Method::kAccent)); + EXPECT_TRUE(simpleHelper("{USER} is offline", + "[»{USER}« îš öƒƒļîñé one two]", Pseudolocalizer::Method::kAccent)); + EXPECT_TRUE(simpleHelper("Copy from {path1} to {path2}", + "[Çöþý ƒŕöḿ »{path1}« ţö »{path2}« one two three]", + Pseudolocalizer::Method::kAccent)); + EXPECT_TRUE(simpleHelper("Today is {1,date} {1,time}", + "[Ţöðåý îš »{1,date}« »{1,time}« one two]", + Pseudolocalizer::Method::kAccent)); + + // Multi-fragment messages + EXPECT_TRUE(compoundHelper("{USER}", " ", "is offline", + "[»{USER}« îš öƒƒļîñé one two]", + Pseudolocalizer::Method::kAccent)); + EXPECT_TRUE(compoundHelper("Copy from ", "{path1}", " to {path2}", + "[Çöþý ƒŕöḿ »{path1}« ţö »{path2}« one two three]", + Pseudolocalizer::Method::kAccent)); +} + +TEST(PseudolocalizerTest, ICUBidi) { + // Single-fragment messages + EXPECT_TRUE(simpleHelper("{placeholder}", + "\xe2\x80\x8f\xE2\x80\xae{placeholder}\xE2\x80\xac\xe2\x80\x8f", + Pseudolocalizer::Method::kBidi)); + EXPECT_TRUE(simpleHelper( + "{COUNT, plural, one {one} other {other}}", + "{COUNT, plural, " \ + "one {\xe2\x80\x8f\xE2\x80\xaeone\xE2\x80\xac\xe2\x80\x8f} " \ + "other {\xe2\x80\x8f\xE2\x80\xaeother\xE2\x80\xac\xe2\x80\x8f}}", + Pseudolocalizer::Method::kBidi)); +} + +TEST(PseudolocalizerTest, Escaping) { + // Single-fragment messages + EXPECT_TRUE(simpleHelper("'{USER'} is offline", + "['{ÛŠÉŔ'} îš öƒƒļîñé one two three]", + Pseudolocalizer::Method::kAccent)); + + // Multi-fragment messages + EXPECT_TRUE(compoundHelper("'{USER}", " ", "''is offline", + "['{ÛŠÉŔ} ''îš öƒƒļîñé one two three]", + Pseudolocalizer::Method::kAccent)); +} + +TEST(PseudolocalizerTest, PluralsAndSelects) { + EXPECT_TRUE(simpleHelper( + "{COUNT, plural, one {Delete a file} other {Delete {COUNT} files}}", + "[{COUNT, plural, one {Ðéļéţé å ƒîļé one two} " \ + "other {Ðéļéţé »{COUNT}« ƒîļéš one two}}]", + Pseudolocalizer::Method::kAccent)); + + EXPECT_TRUE(simpleHelper( + "Distance is {COUNT, plural, one {# mile} other {# miles}}", + "[Ðîšţåñçé îš {COUNT, plural, one {# ḿîļé one two} " \ + "other {# ḿîļéš one two}}]", + Pseudolocalizer::Method::kAccent)); + + EXPECT_TRUE(simpleHelper( + "{1, select, female {{1} added you} " \ + "male {{1} added you} other {{1} added you}}", + "[{1, select, female {»{1}« åððéð ýöû one two} " \ + "male {»{1}« åððéð ýöû one two} other {»{1}« åððéð ýöû one two}}]", + Pseudolocalizer::Method::kAccent)); + + EXPECT_TRUE(compoundHelper( + "{COUNT, plural, one {Delete a file} " \ + "other {Delete ", "{COUNT}", " files}}", + "[{COUNT, plural, one {Ðéļéţé å ƒîļé one two} " \ + "other {Ðéļéţé »{COUNT}« ƒîļéš one two}}]", + Pseudolocalizer::Method::kAccent)); +} + +TEST(PseudolocalizerTest, NestedICU) { + EXPECT_TRUE(simpleHelper( + "{person, select, " \ + "female {" \ + "{num_circles, plural," \ + "=0{{person} didn't add you to any of her circles.}" \ + "=1{{person} added you to one of her circles.}" \ + "other{{person} added you to her # circles.}}}" \ + "male {" \ + "{num_circles, plural," \ + "=0{{person} didn't add you to any of his circles.}" \ + "=1{{person} added you to one of his circles.}" \ + "other{{person} added you to his # circles.}}}" \ + "other {" \ + "{num_circles, plural," \ + "=0{{person} didn't add you to any of their circles.}" \ + "=1{{person} added you to one of their circles.}" \ + "other{{person} added you to their # circles.}}}}", + "[{person, select, " \ + "female {" \ + "{num_circles, plural," \ + "=0{»{person}« ðîðñ'ţ åðð ýöû ţö åñý öƒ ĥéŕ çîŕçļéš." \ + " one two three four five}" \ + "=1{»{person}« åððéð ýöû ţö öñé öƒ ĥéŕ çîŕçļéš." \ + " one two three four}" \ + "other{»{person}« åððéð ýöû ţö ĥéŕ # çîŕçļéš." \ + " one two three four}}}" \ + "male {" \ + "{num_circles, plural," \ + "=0{»{person}« ðîðñ'ţ åðð ýöû ţö åñý öƒ ĥîš çîŕçļéš." \ + " one two three four five}" \ + "=1{»{person}« åððéð ýöû ţö öñé öƒ ĥîš çîŕçļéš." \ + " one two three four}" \ + "other{»{person}« åððéð ýöû ţö ĥîš # çîŕçļéš." \ + " one two three four}}}" \ + "other {{num_circles, plural," \ + "=0{»{person}« ðîðñ'ţ åðð ýöû ţö åñý öƒ ţĥéîŕ çîŕçļéš." \ + " one two three four five}" \ + "=1{»{person}« åððéð ýöû ţö öñé öƒ ţĥéîŕ çîŕçļéš." \ + " one two three four}" \ + "other{»{person}« åððéð ýöû ţö ţĥéîŕ # çîŕçļéš." \ + " one two three four}}}}]", + Pseudolocalizer::Method::kAccent)); +} + +TEST(PseudolocalizerTest, RedefineMethod) { + Pseudolocalizer pseudo(Pseudolocalizer::Method::kAccent); + std::u16string result = pseudo.text(u"Hello, "); + pseudo.setMethod(Pseudolocalizer::Method::kNone); + result += pseudo.text(u"world!"); + ASSERT_EQ(StringPiece("Ĥéļļö, world!"), util::utf16ToUtf8(result)); +} + +} // namespace aapt diff --git a/tools/aapt2/compile/XmlIdCollector.cpp b/tools/aapt2/compile/XmlIdCollector.cpp new file mode 100644 index 000000000000..f40689eaeb47 --- /dev/null +++ b/tools/aapt2/compile/XmlIdCollector.cpp @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ResourceUtils.h" +#include "ResourceValues.h" +#include "compile/XmlIdCollector.h" +#include "xml/XmlDom.h" + +#include <algorithm> +#include <vector> + +namespace aapt { + +namespace { + +static bool cmpName(const SourcedResourceName& a, const ResourceNameRef& b) { + return a.name < b; +} + +struct IdCollector : public xml::Visitor { + using xml::Visitor::visit; + + std::vector<SourcedResourceName>* mOutSymbols; + + IdCollector(std::vector<SourcedResourceName>* outSymbols) : mOutSymbols(outSymbols) { + } + + void visit(xml::Element* element) override { + for (xml::Attribute& attr : element->attributes) { + ResourceNameRef name; + bool create = false; + if (ResourceUtils::tryParseReference(attr.value, &name, &create, nullptr)) { + if (create && name.type == ResourceType::kId) { + auto iter = std::lower_bound(mOutSymbols->begin(), mOutSymbols->end(), + name, cmpName); + if (iter == mOutSymbols->end() || iter->name != name) { + mOutSymbols->insert(iter, SourcedResourceName{ name.toResourceName(), + element->lineNumber }); + } + } + } + } + + xml::Visitor::visit(element); + } +}; + +} // namespace + +bool XmlIdCollector::consume(IAaptContext* context, xml::XmlResource* xmlRes) { + xmlRes->file.exportedSymbols.clear(); + IdCollector collector(&xmlRes->file.exportedSymbols); + xmlRes->root->accept(&collector); + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/Compat_test.cpp b/tools/aapt2/compile/XmlIdCollector.h index 96aee44c6c95..1b149449de2c 100644 --- a/tools/aapt2/Compat_test.cpp +++ b/tools/aapt2/compile/XmlIdCollector.h @@ -14,20 +14,18 @@ * limitations under the License. */ -#include <gtest/gtest.h> +#ifndef AAPT_XMLIDCOLLECTOR_H +#define AAPT_XMLIDCOLLECTOR_H -namespace aapt { - -TEST(CompatTest, VersionAttributesInStyle) { -} - -TEST(CompatTest, VersionAttributesInXML) { -} +#include "process/IResourceTableConsumer.h" +#include "xml/XmlDom.h" -TEST(CompatTest, DoNotOverrideExistingVersionedFiles) { -} +namespace aapt { -TEST(CompatTest, VersionAttributesInStyleWithCorrectPrecedence) { -} +struct XmlIdCollector : public IXmlResourceConsumer { + bool consume(IAaptContext* context, xml::XmlResource* xmlRes) override; +}; } // namespace aapt + +#endif /* AAPT_XMLIDCOLLECTOR_H */ diff --git a/tools/aapt2/compile/XmlIdCollector_test.cpp b/tools/aapt2/compile/XmlIdCollector_test.cpp new file mode 100644 index 000000000000..a37ea86c317f --- /dev/null +++ b/tools/aapt2/compile/XmlIdCollector_test.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "compile/XmlIdCollector.h" +#include "test/Builders.h" +#include "test/Context.h" + +#include <algorithm> +#include <gtest/gtest.h> + +namespace aapt { + +TEST(XmlIdCollectorTest, CollectsIds) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDom(R"EOF( + <View xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/foo" + text="@+id/bar"> + <SubView android:id="@+id/car" + class="@+id/bar"/> + </View>)EOF"); + + XmlIdCollector collector; + ASSERT_TRUE(collector.consume(context.get(), doc.get())); + + EXPECT_EQ(1, std::count(doc->file.exportedSymbols.begin(), doc->file.exportedSymbols.end(), + SourcedResourceName{ test::parseNameOrDie(u"@id/foo"), 3u })); + + EXPECT_EQ(1, std::count(doc->file.exportedSymbols.begin(), doc->file.exportedSymbols.end(), + SourcedResourceName{ test::parseNameOrDie(u"@id/bar"), 3u })); + + EXPECT_EQ(1, std::count(doc->file.exportedSymbols.begin(), doc->file.exportedSymbols.end(), + SourcedResourceName{ test::parseNameOrDie(u"@id/car"), 6u })); +} + +TEST(XmlIdCollectorTest, DontCollectNonIds) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDom("<View foo=\"@+string/foo\"/>"); + + XmlIdCollector collector; + ASSERT_TRUE(collector.consume(context.get(), doc.get())); + + EXPECT_TRUE(doc->file.exportedSymbols.empty()); +} + +} // namespace aapt diff --git a/tools/aapt2/data/AndroidManifest.xml b/tools/aapt2/data/AndroidManifest.xml index 8533c28c24bb..d3b2fbe515ae 100644 --- a/tools/aapt2/data/AndroidManifest.xml +++ b/tools/aapt2/data/AndroidManifest.xml @@ -2,6 +2,6 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.app"> <application - android:name=".Activity"> + android:name=".ActivityMain"> </application> </manifest> diff --git a/tools/aapt2/data/Makefile b/tools/aapt2/data/Makefile index 91ff5fee6477..37012decdf05 100644 --- a/tools/aapt2/data/Makefile +++ b/tools/aapt2/data/Makefile @@ -21,63 +21,41 @@ LOCAL_PROGUARD := out/proguard.rule # AAPT2 custom rules. ## -PRIVATE_APK_UNALIGNED := $(LOCAL_OUT)/package-unaligned.apk -PRIVATE_APK_ALIGNED := $(LOCAL_OUT)/package.apk +PRIVATE_R_FILE := $(LOCAL_GEN)/$(subst .,/,$(LOCAL_PACKAGE))/R.java +$(info PRIVATE_R_FILE = $(PRIVATE_R_FILE)) # Eg: framework.apk, etc. PRIVATE_INCLUDES := $(FRAMEWORK) $(info PRIVATE_INCLUDES = $(PRIVATE_INCLUDES)) -# Eg: gen/com/android/app/R.java -PRIVATE_R_JAVA := $(LOCAL_GEN)/$(subst .,/,$(LOCAL_PACKAGE))/R.java -$(info PRIVATE_R_JAVA = $(PRIVATE_R_JAVA)) - # Eg: res/drawable/icon.png, res/values/styles.xml PRIVATE_RESOURCES := $(shell find $(LOCAL_RESOURCE_DIR) -mindepth 1 -maxdepth 2 -type f) $(info PRIVATE_RESOURCES = $(PRIVATE_RESOURCES)) -# Eg: drawable, values, layouts -PRIVATE_RESOURCE_TYPES := \ - $(patsubst $(LOCAL_RESOURCE_DIR)/%/,%,$(sort $(dir $(PRIVATE_RESOURCES)))) -$(info PRIVATE_RESOURCE_TYPES = $(PRIVATE_RESOURCE_TYPES)) - -# Eg: out/values-v4.apk, out/drawable-xhdpi.apk -PRIVATE_INTERMEDIATE_TABLES := $(patsubst %,$(LOCAL_OUT)/%.apk,$(PRIVATE_RESOURCE_TYPES)) -$(info PRIVATE_INTERMEDIATE_TABLES = $(PRIVATE_INTERMEDIATE_TABLES)) +PRIVATE_RESOURCE_OBJECTS := $(subst /,_,$(patsubst $(LOCAL_RESOURCE_DIR)/%,%,$(filter $(LOCAL_RESOURCE_DIR)/values%,$(PRIVATE_RESOURCES)))) +PRIVATE_RESOURCE_OBJECTS := $(addprefix $(LOCAL_OUT)/,$(PRIVATE_RESOURCE_OBJECTS:.xml=.arsc.flat)) +$(info PRIVATE_RESOURCE_OBJECTS = $(PRIVATE_RESOURCE_OBJECTS)) -# Generates rules for collect phase. -# $1: Resource type (values-v4) -# returns: out/values-v4.apk: res/values-v4/styles.xml res/values-v4/colors.xml -define make-collect-rule -$(LOCAL_OUT)/$1.apk: $(filter $(LOCAL_RESOURCE_DIR)/$1/%,$(PRIVATE_RESOURCES)) - $(AAPT) compile -o $$@ $$^ -endef +PRIVATE_FILE_OBJECTS := $(subst /,_,$(patsubst $(LOCAL_RESOURCE_DIR)/%,%,$(filter-out $(LOCAL_RESOURCE_DIR)/values%,$(PRIVATE_RESOURCES)))) +PRIVATE_FILE_OBJECTS := $(addprefix $(LOCAL_OUT)/,$(addsuffix .flat,$(PRIVATE_FILE_OBJECTS))) +$(info PRIVATE_FILE_OBJECTS = $(PRIVATE_FILE_OBJECTS)) -# Collect: out/values-v4.apk <- res/values-v4/styles.xml res/values-v4/colors.xml -$(foreach d,$(PRIVATE_RESOURCE_TYPES),$(eval $(call make-collect-rule,$d))) +.SECONDEXPANSION: -# Link: out/package-unaligned.apk <- out/values-v4.apk out/drawable-v4.apk -$(PRIVATE_APK_UNALIGNED): $(PRIVATE_INTERMEDIATE_TABLES) $(PRIVATE_INCLUDES) $(LOCAL_LIBS) AndroidManifest.xml - $(AAPT) link --manifest AndroidManifest.xml $(addprefix -I ,$(PRIVATE_INCLUDES)) --java $(LOCAL_GEN) -o $@ $(PRIVATE_INTERMEDIATE_TABLES) $(LOCAL_LIBS) --proguard $(LOCAL_PROGUARD) -v +$(LOCAL_OUT)/%.arsc.flat: $(LOCAL_RESOURCE_DIR)/$$(subst _,/,%).xml + $(AAPT) compile -o $(LOCAL_OUT) $< -# R.java: gen/com/android/app/R.java <- out/resources.arsc -# No action since R.java is generated when out/resources.arsc is. -$(PRIVATE_R_JAVA): $(PRIVATE_APK_UNALIGNED) +$(LOCAL_OUT)/%.flat: $(LOCAL_RESOURCE_DIR)/$$(subst _,/,%) + $(AAPT) compile -o $(LOCAL_OUT) $< -# Assemble: zip out/resources.arsc AndroidManifest.xml and res/**/* -$(PRIVATE_APK_ALIGNED): $(PRIVATE_APK_UNALIGNED) - $(ZIPALIGN) $< $@ +$(LOCAL_PROGUARD) $(LOCAL_OUT)/package.apk: AndroidManifest.xml +$(PRIVATE_R_FILE) $(LOCAL_PROGUARD) $(LOCAL_OUT)/package.apk: $(PRIVATE_FILE_OBJECTS) $(PRIVATE_RESOURCE_OBJECTS) + $(AAPT) link -o $(LOCAL_OUT)/package.apk --manifest AndroidManifest.xml --java $(LOCAL_GEN) --proguard $(LOCAL_PROGUARD) -I $(PRIVATE_INCLUDES) $(filter-out AndroidManifest.xml,$^) -v # Create the out directory if needed. dummy := $(shell test -d $(LOCAL_OUT) || mkdir -p $(LOCAL_OUT)) -.PHONY: java -java: $(PRIVATE_R_JAVA) - -.PHONY: assemble -assemble: $(PRIVATE_APK_ALIGNED) - .PHONY: all -all: assemble java +all: $(LOCAL_OUT)/package.apk $(LOCAL_PROGUARD) $(PRIVATE_R_FILE) .DEFAULT_GOAL := all diff --git a/tools/aapt2/data/res/layout-v21/main.xml b/tools/aapt2/data/res/layout-v21/main.xml new file mode 100644 index 000000000000..959b349bfaff --- /dev/null +++ b/tools/aapt2/data/res/layout-v21/main.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:support="http://schemas.android.com/apk/res/android.appcompat" + android:id="@+id/view" + android:layout_width="match_parent" + android:layout_height="wrap_content"> +</LinearLayout> diff --git a/tools/aapt2/data/res/layout/main.xml b/tools/aapt2/data/res/layout/main.xml index 50a51d99ad0a..8a5e9e8ee58d 100644 --- a/tools/aapt2/data/res/layout/main.xml +++ b/tools/aapt2/data/res/layout/main.xml @@ -14,8 +14,8 @@ android:layout_width="1dp" android:onClick="doClick" android:text="@{user.name}" + android:background="#ffffff" android:layout_height="match_parent" - app:layout_width="@support:bool/allow" app:flags="complex|weak" android:colorAccent="#ffffff"/> </LinearLayout> diff --git a/tools/aapt2/data/res/raw/test.txt b/tools/aapt2/data/res/raw/test.txt new file mode 100644 index 000000000000..b14df6442ea5 --- /dev/null +++ b/tools/aapt2/data/res/raw/test.txt @@ -0,0 +1 @@ +Hi diff --git a/tools/aapt2/data/res/values/styles.xml b/tools/aapt2/data/res/values/styles.xml index d0b19a3a881b..2bbdad1bcbf1 100644 --- a/tools/aapt2/data/res/values/styles.xml +++ b/tools/aapt2/data/res/values/styles.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources xmlns:lib="http://schemas.android.com/apk/res/android.appcompat"> - <style name="App" parent="android.appcompat:Platform.AppCompat"> + <style name="App"> <item name="android:background">@color/primary</item> <item name="android:colorPrimary">@color/primary</item> <item name="android:colorPrimaryDark">@color/primary_dark</item> @@ -8,8 +8,8 @@ </style> <attr name="custom" format="reference" /> <style name="Pop"> - <item name="custom">@drawable/image</item> - <item name="android:focusable">@lib:bool/allow</item> + <item name="custom">@android:drawable/btn_default</item> + <item name="android:focusable">true</item> </style> <string name="yo">@string/wow</string> diff --git a/tools/aapt2/data/res/values/test.xml b/tools/aapt2/data/res/values/test.xml index d3ead34d043c..d7ab1c8ddde9 100644 --- a/tools/aapt2/data/res/values/test.xml +++ b/tools/aapt2/data/res/values/test.xml @@ -3,7 +3,7 @@ <string name="hooha"><font bgcolor="#ffffff">Hey guys!</font> <xliff:g>My</xliff:g> name is <b>Adam</b>. How <b><i>are</i></b> you?</string> <public name="hooha" type="string" id="0x7f020001"/> <string name="wow">@android:string/ok</string> - <public name="image" type="drawable" id="0x7f060000" /> + <public name="layout_width" type="attr" /> <attr name="layout_width" format="boolean" /> <attr name="flags"> <flag name="complex" value="1" /> diff --git a/tools/aapt2/filter/ConfigFilter.cpp b/tools/aapt2/filter/ConfigFilter.cpp new file mode 100644 index 000000000000..68a017d247f6 --- /dev/null +++ b/tools/aapt2/filter/ConfigFilter.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ConfigDescription.h" +#include "filter/ConfigFilter.h" + +#include <androidfw/ResourceTypes.h> + +namespace aapt { + +void AxisConfigFilter::addConfig(ConfigDescription config) { + uint32_t diffMask = ConfigDescription::defaultConfig().diff(config); + + // Ignore the version + diffMask &= ~android::ResTable_config::CONFIG_VERSION; + + // Ignore any densities. Those are best handled in --preferred-density + if ((diffMask & android::ResTable_config::CONFIG_DENSITY) != 0) { + config.density = 0; + diffMask &= ~android::ResTable_config::CONFIG_DENSITY; + } + + mConfigs.insert(std::make_pair(config, diffMask)); + mConfigMask |= diffMask; +} + +bool AxisConfigFilter::match(const ConfigDescription& config) const { + const uint32_t mask = ConfigDescription::defaultConfig().diff(config); + if ((mConfigMask & mask) == 0) { + // The two configurations don't have any common axis. + return true; + } + + uint32_t matchedAxis = 0; + for (const auto& entry : mConfigs) { + const ConfigDescription& target = entry.first; + const uint32_t diffMask = entry.second; + uint32_t diff = target.diff(config); + if ((diff & diffMask) == 0) { + // Mark the axis that was matched. + matchedAxis |= diffMask; + } else if ((diff & diffMask) == android::ResTable_config::CONFIG_LOCALE) { + // If the locales differ, but the languages are the same and + // the locale we are matching only has a language specified, + // we match. + if (config.language[0] && + memcmp(config.language, target.language, sizeof(config.language)) == 0) { + if (config.country[0] == 0) { + matchedAxis |= android::ResTable_config::CONFIG_LOCALE; + } + } + } else if ((diff & diffMask) == android::ResTable_config::CONFIG_SMALLEST_SCREEN_SIZE) { + // Special case if the smallest screen width doesn't match. We check that the + // config being matched has a smaller screen width than the filter specified. + if (config.smallestScreenWidthDp != 0 && + config.smallestScreenWidthDp < target.smallestScreenWidthDp) { + matchedAxis |= android::ResTable_config::CONFIG_SMALLEST_SCREEN_SIZE; + } + } + } + return matchedAxis == (mConfigMask & mask); +} + +} // namespace aapt diff --git a/tools/aapt2/filter/ConfigFilter.h b/tools/aapt2/filter/ConfigFilter.h new file mode 100644 index 000000000000..36e9c44255e4 --- /dev/null +++ b/tools/aapt2/filter/ConfigFilter.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_FILTER_CONFIGFILTER_H +#define AAPT_FILTER_CONFIGFILTER_H + +#include "ConfigDescription.h" + +#include <set> +#include <utility> + +namespace aapt { + +/** + * Matches ConfigDescriptions based on some pattern. + */ +class IConfigFilter { +public: + virtual ~IConfigFilter() = default; + + /** + * Returns true if the filter matches the configuration, false otherwise. + */ + virtual bool match(const ConfigDescription& config) const = 0; +}; + +/** + * Implements config axis matching. An axis is one component of a configuration, like screen + * density or locale. If an axis is specified in the filter, and the axis is specified in + * the configuration to match, they must be compatible. Otherwise the configuration to match is + * accepted. + * + * Used when handling "-c" options. + */ +class AxisConfigFilter : public IConfigFilter { +public: + void addConfig(ConfigDescription config); + + bool match(const ConfigDescription& config) const override; + +private: + std::set<std::pair<ConfigDescription, uint32_t>> mConfigs; + uint32_t mConfigMask = 0; +}; + +} // namespace aapt + +#endif /* AAPT_FILTER_CONFIGFILTER_H */ diff --git a/tools/aapt2/filter/ConfigFilter_test.cpp b/tools/aapt2/filter/ConfigFilter_test.cpp new file mode 100644 index 000000000000..f6b49557306d --- /dev/null +++ b/tools/aapt2/filter/ConfigFilter_test.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "filter/ConfigFilter.h" +#include "test/Common.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(ConfigFilterTest, EmptyFilterMatchesAnything) { + AxisConfigFilter filter; + + EXPECT_TRUE(filter.match(test::parseConfigOrDie("320dpi"))); + EXPECT_TRUE(filter.match(test::parseConfigOrDie("fr"))); +} + +TEST(ConfigFilterTest, MatchesConfigWithUnrelatedAxis) { + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("fr")); + + EXPECT_TRUE(filter.match(test::parseConfigOrDie("320dpi"))); +} + +TEST(ConfigFilterTest, MatchesConfigWithSameValueAxis) { + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("fr")); + + EXPECT_TRUE(filter.match(test::parseConfigOrDie("fr"))); +} + +TEST(ConfigFilterTest, MatchesConfigWithSameValueAxisAndOtherUnrelatedAxis) { + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("fr")); + + EXPECT_TRUE(filter.match(test::parseConfigOrDie("fr-320dpi"))); +} + +TEST(ConfigFilterTest, MatchesConfigWithOneMatchingAxis) { + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("fr-rFR")); + filter.addConfig(test::parseConfigOrDie("sw360dp")); + filter.addConfig(test::parseConfigOrDie("normal")); + filter.addConfig(test::parseConfigOrDie("en-rUS")); + + EXPECT_TRUE(filter.match(test::parseConfigOrDie("en"))); +} + +TEST(ConfigFilterTest, DoesNotMatchConfigWithDifferentValueAxis) { + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("fr")); + + EXPECT_FALSE(filter.match(test::parseConfigOrDie("de"))); +} + +TEST(ConfigFilterTest, DoesNotMatchWhenOneQualifierIsExplicitlyNotMatched) { + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("fr-rFR")); + filter.addConfig(test::parseConfigOrDie("en-rUS")); + filter.addConfig(test::parseConfigOrDie("normal")); + filter.addConfig(test::parseConfigOrDie("large")); + filter.addConfig(test::parseConfigOrDie("xxhdpi")); + filter.addConfig(test::parseConfigOrDie("sw320dp")); + + EXPECT_FALSE(filter.match(test::parseConfigOrDie("fr-sw600dp-v13"))); +} + +TEST(ConfigFilterTest, MatchesSmallestWidthWhenSmaller) { + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("sw600dp")); + + EXPECT_TRUE(filter.match(test::parseConfigOrDie("fr-sw320dp-v13"))); +} + +TEST(ConfigFilterTest, MatchesConfigWithSameLanguageButNoRegionSpecified) { + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("de-rDE")); + + EXPECT_TRUE(filter.match(test::parseConfigOrDie("de"))); +} + +TEST(ConfigFilterTest, IgnoresVersion) { + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("normal-v4")); + + // The configs don't match on any axis besides version, which should be ignored. + EXPECT_TRUE(filter.match(test::parseConfigOrDie("sw600dp-v13"))); +} + +TEST(ConfigFilterTest, MatchesConfigWithRegion) { + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("kok")); + filter.addConfig(test::parseConfigOrDie("kok-rIN")); + filter.addConfig(test::parseConfigOrDie("kok-v419")); + + EXPECT_TRUE(filter.match(test::parseConfigOrDie("kok-rIN"))); +} + +} // namespace aapt diff --git a/tools/aapt2/flatten/Archive.cpp b/tools/aapt2/flatten/Archive.cpp new file mode 100644 index 000000000000..3a244c05efec --- /dev/null +++ b/tools/aapt2/flatten/Archive.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "flatten/Archive.h" +#include "util/Files.h" +#include "util/StringPiece.h" + +#include <cstdio> +#include <memory> +#include <string> +#include <vector> +#include <ziparchive/zip_writer.h> + +namespace aapt { + +namespace { + +struct DirectoryWriter : public IArchiveWriter { + std::string mOutDir; + std::unique_ptr<FILE, decltype(fclose)*> mFile = { nullptr, fclose }; + + bool open(IDiagnostics* diag, const StringPiece& outDir) { + mOutDir = outDir.toString(); + file::FileType type = file::getFileType(mOutDir); + if (type == file::FileType::kNonexistant) { + diag->error(DiagMessage() << "directory " << mOutDir << " does not exist"); + return false; + } else if (type != file::FileType::kDirectory) { + diag->error(DiagMessage() << mOutDir << " is not a directory"); + return false; + } + return true; + } + + bool startEntry(const StringPiece& path, uint32_t flags) override { + if (mFile) { + return false; + } + + std::string fullPath = mOutDir; + file::appendPath(&fullPath, path); + file::mkdirs(file::getStem(fullPath)); + + mFile = { fopen(fullPath.data(), "wb"), fclose }; + if (!mFile) { + return false; + } + return true; + } + + bool writeEntry(const BigBuffer& buffer) override { + if (!mFile) { + return false; + } + + for (const BigBuffer::Block& b : buffer) { + if (fwrite(b.buffer.get(), 1, b.size, mFile.get()) != b.size) { + mFile.reset(nullptr); + return false; + } + } + return true; + } + + bool writeEntry(const void* data, size_t len) override { + if (fwrite(data, 1, len, mFile.get()) != len) { + mFile.reset(nullptr); + return false; + } + return true; + } + + bool finishEntry() override { + if (!mFile) { + return false; + } + mFile.reset(nullptr); + return true; + } +}; + +struct ZipFileWriter : public IArchiveWriter { + std::unique_ptr<FILE, decltype(fclose)*> mFile = { nullptr, fclose }; + std::unique_ptr<ZipWriter> mWriter; + + bool open(IDiagnostics* diag, const StringPiece& path) { + mFile = { fopen(path.data(), "w+b"), fclose }; + if (!mFile) { + diag->error(DiagMessage() << "failed to open " << path << ": " << strerror(errno)); + return false; + } + mWriter = util::make_unique<ZipWriter>(mFile.get()); + return true; + } + + bool startEntry(const StringPiece& path, uint32_t flags) override { + if (!mWriter) { + return false; + } + + size_t zipFlags = 0; + if (flags & ArchiveEntry::kCompress) { + zipFlags |= ZipWriter::kCompress; + } + + if (flags & ArchiveEntry::kAlign) { + zipFlags |= ZipWriter::kAlign32; + } + + int32_t result = mWriter->StartEntry(path.data(), zipFlags); + if (result != 0) { + return false; + } + return true; + } + + bool writeEntry(const void* data, size_t len) override { + int32_t result = mWriter->WriteBytes(data, len); + if (result != 0) { + return false; + } + return true; + } + + bool writeEntry(const BigBuffer& buffer) override { + for (const BigBuffer::Block& b : buffer) { + int32_t result = mWriter->WriteBytes(b.buffer.get(), b.size); + if (result != 0) { + return false; + } + } + return true; + } + + bool finishEntry() override { + int32_t result = mWriter->FinishEntry(); + if (result != 0) { + return false; + } + return true; + } + + virtual ~ZipFileWriter() { + if (mWriter) { + mWriter->Finish(); + } + } +}; + +} // namespace + +std::unique_ptr<IArchiveWriter> createDirectoryArchiveWriter(IDiagnostics* diag, + const StringPiece& path) { + + std::unique_ptr<DirectoryWriter> writer = util::make_unique<DirectoryWriter>(); + if (!writer->open(diag, path)) { + return {}; + } + return std::move(writer); +} + +std::unique_ptr<IArchiveWriter> createZipFileArchiveWriter(IDiagnostics* diag, + const StringPiece& path) { + std::unique_ptr<ZipFileWriter> writer = util::make_unique<ZipFileWriter>(); + if (!writer->open(diag, path)) { + return {}; + } + return std::move(writer); +} + +} // namespace aapt diff --git a/tools/aapt2/flatten/Archive.h b/tools/aapt2/flatten/Archive.h new file mode 100644 index 000000000000..6da1d2ac5620 --- /dev/null +++ b/tools/aapt2/flatten/Archive.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_FLATTEN_ARCHIVE_H +#define AAPT_FLATTEN_ARCHIVE_H + +#include "Diagnostics.h" +#include "util/BigBuffer.h" +#include "util/Files.h" +#include "util/StringPiece.h" + +#include <fstream> +#include <memory> +#include <string> +#include <vector> + +namespace aapt { + +struct ArchiveEntry { + enum : uint32_t { + kCompress = 0x01, + kAlign = 0x02, + }; + + std::string path; + uint32_t flags; + size_t uncompressedSize; +}; + +struct IArchiveWriter { + virtual ~IArchiveWriter() = default; + + virtual bool startEntry(const StringPiece& path, uint32_t flags) = 0; + virtual bool writeEntry(const BigBuffer& buffer) = 0; + virtual bool writeEntry(const void* data, size_t len) = 0; + virtual bool finishEntry() = 0; +}; + +std::unique_ptr<IArchiveWriter> createDirectoryArchiveWriter(IDiagnostics* diag, + const StringPiece& path); + +std::unique_ptr<IArchiveWriter> createZipFileArchiveWriter(IDiagnostics* diag, + const StringPiece& path); + +} // namespace aapt + +#endif /* AAPT_FLATTEN_ARCHIVE_H */ diff --git a/tools/aapt2/flatten/ChunkWriter.h b/tools/aapt2/flatten/ChunkWriter.h new file mode 100644 index 000000000000..de1d87a57e6d --- /dev/null +++ b/tools/aapt2/flatten/ChunkWriter.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_FLATTEN_CHUNKWRITER_H +#define AAPT_FLATTEN_CHUNKWRITER_H + +#include "util/BigBuffer.h" +#include "util/Util.h" + +#include <androidfw/ResourceTypes.h> + +namespace aapt { + +class ChunkWriter { +private: + BigBuffer* mBuffer; + size_t mStartSize = 0; + android::ResChunk_header* mHeader = nullptr; + +public: + explicit inline ChunkWriter(BigBuffer* buffer) : mBuffer(buffer) { + } + + ChunkWriter(const ChunkWriter&) = delete; + ChunkWriter& operator=(const ChunkWriter&) = delete; + ChunkWriter(ChunkWriter&&) = default; + ChunkWriter& operator=(ChunkWriter&&) = default; + + template <typename T> + inline T* startChunk(uint16_t type) { + mStartSize = mBuffer->size(); + T* chunk = mBuffer->nextBlock<T>(); + mHeader = &chunk->header; + mHeader->type = util::hostToDevice16(type); + mHeader->headerSize = util::hostToDevice16(sizeof(T)); + return chunk; + } + + template <typename T> + inline T* nextBlock(size_t count = 1) { + return mBuffer->nextBlock<T>(count); + } + + inline BigBuffer* getBuffer() { + return mBuffer; + } + + inline android::ResChunk_header* getChunkHeader() { + return mHeader; + } + + inline size_t size() { + return mBuffer->size() - mStartSize; + } + + inline android::ResChunk_header* finish() { + mBuffer->align4(); + mHeader->size = util::hostToDevice32(mBuffer->size() - mStartSize); + return mHeader; + } +}; + +template <> +inline android::ResChunk_header* ChunkWriter::startChunk(uint16_t type) { + mStartSize = mBuffer->size(); + mHeader = mBuffer->nextBlock<android::ResChunk_header>(); + mHeader->type = util::hostToDevice16(type); + mHeader->headerSize = util::hostToDevice16(sizeof(android::ResChunk_header)); + return mHeader; +} + +} // namespace aapt + +#endif /* AAPT_FLATTEN_CHUNKWRITER_H */ diff --git a/tools/aapt2/flatten/FileExportWriter.h b/tools/aapt2/flatten/FileExportWriter.h new file mode 100644 index 000000000000..7688fa71246e --- /dev/null +++ b/tools/aapt2/flatten/FileExportWriter.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_FLATTEN_FILEEXPORTWRITER_H +#define AAPT_FLATTEN_FILEEXPORTWRITER_H + +#include "StringPool.h" + +#include "flatten/ResourceTypeExtensions.h" +#include "flatten/ChunkWriter.h" +#include "process/IResourceTableConsumer.h" +#include "util/BigBuffer.h" +#include "util/Util.h" + +#include <androidfw/ResourceTypes.h> +#include <utils/misc.h> + +namespace aapt { + +static ChunkWriter wrapBufferWithFileExportHeader(BigBuffer* buffer, ResourceFile* res) { + ChunkWriter fileExportWriter(buffer); + FileExport_header* fileExport = fileExportWriter.startChunk<FileExport_header>( + RES_FILE_EXPORT_TYPE); + + ExportedSymbol* symbolRefs = nullptr; + if (!res->exportedSymbols.empty()) { + symbolRefs = fileExportWriter.nextBlock<ExportedSymbol>( + res->exportedSymbols.size()); + } + fileExport->exportedSymbolCount = util::hostToDevice32(res->exportedSymbols.size()); + + StringPool symbolExportPool; + memcpy(fileExport->magic, "AAPT", NELEM(fileExport->magic)); + fileExport->config = res->config; + fileExport->config.swapHtoD(); + fileExport->name.index = util::hostToDevice32(symbolExportPool.makeRef(res->name.toString()) + .getIndex()); + fileExport->source.index = util::hostToDevice32(symbolExportPool.makeRef(util::utf8ToUtf16( + res->source.path)).getIndex()); + + for (const SourcedResourceName& name : res->exportedSymbols) { + symbolRefs->name.index = util::hostToDevice32(symbolExportPool.makeRef(name.name.toString()) + .getIndex()); + symbolRefs->line = util::hostToDevice32(name.line); + symbolRefs++; + } + + StringPool::flattenUtf16(fileExportWriter.getBuffer(), symbolExportPool); + return fileExportWriter; +} + +} // namespace aapt + +#endif /* AAPT_FLATTEN_FILEEXPORTWRITER_H */ diff --git a/tools/aapt2/flatten/FileExportWriter_test.cpp b/tools/aapt2/flatten/FileExportWriter_test.cpp new file mode 100644 index 000000000000..32fc203c4dee --- /dev/null +++ b/tools/aapt2/flatten/FileExportWriter_test.cpp @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Resource.h" + +#include "flatten/FileExportWriter.h" +#include "util/BigBuffer.h" +#include "util/Util.h" + +#include "test/Common.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(FileExportWriterTest, FlattenResourceFileDataWithNoExports) { + ResourceFile resFile = { + test::parseNameOrDie(u"@android:layout/main.xml"), + test::parseConfigOrDie("sw600dp-v4"), + Source{ "res/layout/main.xml" }, + }; + + BigBuffer buffer(1024); + ChunkWriter writer = wrapBufferWithFileExportHeader(&buffer, &resFile); + *writer.getBuffer()->nextBlock<uint32_t>() = 42u; + writer.finish(); + + std::unique_ptr<uint8_t[]> data = util::copy(buffer); + + // There should be more data (string pool) besides the header and our data. + ASSERT_GT(buffer.size(), sizeof(FileExport_header) + sizeof(uint32_t)); + + // Write at the end of this chunk is our data. + uint32_t* val = (uint32_t*)(data.get() + buffer.size()) - 1; + EXPECT_EQ(*val, 42u); +} + +} // namespace aapt diff --git a/tools/aapt2/ResourceTypeExtensions.h b/tools/aapt2/flatten/ResourceTypeExtensions.h index dcbe9233f6b0..02bff2c69362 100644 --- a/tools/aapt2/ResourceTypeExtensions.h +++ b/tools/aapt2/flatten/ResourceTypeExtensions.h @@ -30,6 +30,12 @@ namespace aapt { * future collisions. */ enum { + /** + * A chunk that contains an entire file that + * has been compiled. + */ + RES_FILE_EXPORT_TYPE = 0x000c, + RES_TABLE_PUBLIC_TYPE = 0x000d, /** @@ -56,10 +62,74 @@ struct ExtendedTypes { * A raw string value that hasn't had its escape sequences * processed nor whitespace removed. */ - TYPE_RAW_STRING = 0xfe + TYPE_RAW_STRING = 0xfe, }; }; +/** + * New types for a ResTable_map. + */ +struct ExtendedResTableMapTypes { + enum { + /** + * Type that contains the source path of the next item in the map. + */ + ATTR_SOURCE_PATH = Res_MAKEINTERNAL(0xffff), + + /** + * Type that contains the source line of the next item in the map. + */ + ATTR_SOURCE_LINE = Res_MAKEINTERNAL(0xfffe), + + /** + * Type that contains the comment of the next item in the map. + */ + ATTR_COMMENT = Res_MAKEINTERNAL(0xfffd) + }; +}; + +/** + * Followed by exportedSymbolCount ExportedSymbol structs, followed by the string pool. + */ +struct FileExport_header { + android::ResChunk_header header; + + /** + * MAGIC value. Must be 'AAPT' (0x41415054) + */ + uint8_t magic[4]; + + /** + * Version of AAPT that built this file. + */ + uint32_t version; + + /** + * The resource name. + */ + android::ResStringPool_ref name; + + /** + * Configuration of this file. + */ + android::ResTable_config config; + + /** + * Original source path of this file. + */ + android::ResStringPool_ref source; + + /** + * Number of symbols exported by this file. + */ + uint32_t exportedSymbolCount; +}; + +struct ExportedSymbol { + android::ResStringPool_ref name; + uint32_t line; +}; + struct Public_header { android::ResChunk_header header; @@ -84,12 +154,44 @@ struct Public_header { uint32_t count; }; +/** + * A structure representing source data for a resource entry. + * Appears after an android::ResTable_entry or android::ResTable_map_entry. + * + * TODO(adamlesinski): This causes some issues when runtime code checks + * the size of an android::ResTable_entry. It assumes it is an + * android::ResTable_map_entry if the size is bigger than an android::ResTable_entry + * which may not be true if this structure is present. + */ +struct ResTable_entry_source { + /** + * File path reference. + */ + android::ResStringPool_ref path; + + /** + * Line number this resource was defined on. + */ + uint32_t line; + + /** + * Comment string reference. + */ + android::ResStringPool_ref comment; +}; + struct Public_entry { uint16_t entryId; - uint16_t res0; + + enum : uint16_t { + kUndefined = 0, + kPublic = 1, + kPrivate = 2, + }; + + uint16_t state; android::ResStringPool_ref key; - android::ResStringPool_ref source; - uint32_t sourceLine; + ResTable_entry_source source; }; /** @@ -118,28 +220,17 @@ struct SymbolTable_entry { * The index into the string pool where the name of this * symbol exists. */ - uint32_t stringIndex; + android::ResStringPool_ref name; }; /** - * A structure representing the source of a resourc entry. - * Appears after an android::ResTable_entry or android::ResTable_map_entry. - * - * TODO(adamlesinski): This causes some issues when runtime code checks - * the size of an android::ResTable_entry. It assumes it is an - * android::ResTable_map_entry if the size is bigger than an android::ResTable_entry - * which may not be true if this structure is present. + * An alternative struct to use instead of ResTable_map_entry. This one is a standard_layout + * struct. */ -struct ResTable_entry_source { - /** - * Index into the source string pool. - */ - uint32_t pathIndex; - - /** - * Line number this resource was defined on. - */ - uint32_t line; +struct ResTable_entry_ext { + android::ResTable_entry entry; + android::ResTable_ref parent; + uint32_t count; }; } // namespace aapt diff --git a/tools/aapt2/flatten/TableFlattener.cpp b/tools/aapt2/flatten/TableFlattener.cpp new file mode 100644 index 000000000000..26d7c2ca055c --- /dev/null +++ b/tools/aapt2/flatten/TableFlattener.cpp @@ -0,0 +1,779 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "ValueVisitor.h" + +#include "flatten/ChunkWriter.h" +#include "flatten/ResourceTypeExtensions.h" +#include "flatten/TableFlattener.h" +#include "util/BigBuffer.h" + +#include <android-base/macros.h> +#include <type_traits> +#include <numeric> + +using namespace android; + +namespace aapt { + +namespace { + +template <typename T> +static bool cmpIds(const T* a, const T* b) { + return a->id.value() < b->id.value(); +} + +static void strcpy16_htod(uint16_t* dst, size_t len, const StringPiece16& src) { + if (len == 0) { + return; + } + + size_t i; + const char16_t* srcData = src.data(); + for (i = 0; i < len - 1 && i < src.size(); i++) { + dst[i] = util::hostToDevice16((uint16_t) srcData[i]); + } + dst[i] = 0; +} + +struct FlatEntry { + ResourceEntry* entry; + Value* value; + + // The entry string pool index to the entry's name. + uint32_t entryKey; + + // The source string pool index to the source file path. + uint32_t sourcePathKey; + uint32_t sourceLine; + + // The source string pool index to the comment. + uint32_t commentKey; +}; + +class SymbolWriter { +public: + struct Entry { + StringPool::Ref name; + size_t offset; + }; + + std::vector<Entry> symbols; + + explicit SymbolWriter(StringPool* pool) : mPool(pool) { + } + + void addSymbol(const Reference& ref, size_t offset) { + const ResourceName& name = ref.name.value(); + std::u16string fullName; + if (ref.privateReference) { + fullName += u"*"; + } + + if (!name.package.empty()) { + fullName += name.package + u":"; + } + fullName += toString(name.type).toString() + u"/" + name.entry; + symbols.push_back(Entry{ mPool->makeRef(fullName), offset }); + } + + void shiftAllOffsets(size_t offset) { + for (Entry& entry : symbols) { + entry.offset += offset; + } + } + +private: + StringPool* mPool; +}; + +struct MapFlattenVisitor : public RawValueVisitor { + using RawValueVisitor::visit; + + SymbolWriter* mSymbols; + FlatEntry* mEntry; + BigBuffer* mBuffer; + StringPool* mSourcePool; + StringPool* mCommentPool; + bool mUseExtendedChunks; + + size_t mEntryCount = 0; + const Reference* mParent = nullptr; + + MapFlattenVisitor(SymbolWriter* symbols, FlatEntry* entry, BigBuffer* buffer, + StringPool* sourcePool, StringPool* commentPool, + bool useExtendedChunks) : + mSymbols(symbols), mEntry(entry), mBuffer(buffer), mSourcePool(sourcePool), + mCommentPool(commentPool), mUseExtendedChunks(useExtendedChunks) { + } + + void flattenKey(Reference* key, ResTable_map* outEntry) { + if (!key->id || (key->privateReference && mUseExtendedChunks)) { + assert(key->name && "reference must have a name"); + + outEntry->name.ident = util::hostToDevice32(0); + mSymbols->addSymbol(*key, (mBuffer->size() - sizeof(ResTable_map)) + + offsetof(ResTable_map, name)); + } else { + outEntry->name.ident = util::hostToDevice32(key->id.value().id); + } + } + + void flattenValue(Item* value, ResTable_map* outEntry) { + bool privateRef = false; + if (Reference* ref = valueCast<Reference>(value)) { + privateRef = ref->privateReference && mUseExtendedChunks; + if (!ref->id || privateRef) { + assert(ref->name && "reference must have a name"); + + mSymbols->addSymbol(*ref, (mBuffer->size() - sizeof(ResTable_map)) + + offsetof(ResTable_map, value) + offsetof(Res_value, data)); + } + } + + bool result = value->flatten(&outEntry->value); + if (privateRef) { + outEntry->value.data = 0; + } + assert(result && "flatten failed"); + } + + void flattenEntry(Reference* key, Item* value) { + ResTable_map* outEntry = mBuffer->nextBlock<ResTable_map>(); + flattenKey(key, outEntry); + flattenValue(value, outEntry); + outEntry->value.size = util::hostToDevice16(sizeof(outEntry->value)); + mEntryCount++; + } + + void flattenMetaData(Value* value) { + if (!mUseExtendedChunks) { + return; + } + + Reference key(ResourceId{ ExtendedResTableMapTypes::ATTR_SOURCE_PATH }); + StringPool::Ref sourcePathRef = mSourcePool->makeRef( + util::utf8ToUtf16(value->getSource().path)); + BinaryPrimitive val(Res_value::TYPE_INT_DEC, + static_cast<uint32_t>(sourcePathRef.getIndex())); + flattenEntry(&key, &val); + + if (value->getSource().line) { + key.id = ResourceId(ExtendedResTableMapTypes::ATTR_SOURCE_LINE); + val.value.data = static_cast<uint32_t>(value->getSource().line.value()); + flattenEntry(&key, &val); + } + + if (!value->getComment().empty()) { + key.id = ResourceId(ExtendedResTableMapTypes::ATTR_COMMENT); + StringPool::Ref commentRef = mCommentPool->makeRef(value->getComment()); + val.value.data = static_cast<uint32_t>(commentRef.getIndex()); + flattenEntry(&key, &val); + } + } + + void visit(Attribute* attr) override { + { + Reference key(ResourceId{ ResTable_map::ATTR_TYPE }); + BinaryPrimitive val(Res_value::TYPE_INT_DEC, attr->typeMask); + flattenEntry(&key, &val); + } + + if (attr->minInt != std::numeric_limits<int32_t>::min()) { + Reference key(ResourceId{ ResTable_map::ATTR_MIN }); + BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->minInt)); + flattenEntry(&key, &val); + } + + if (attr->maxInt != std::numeric_limits<int32_t>::max()) { + Reference key(ResourceId{ ResTable_map::ATTR_MAX }); + BinaryPrimitive val(Res_value::TYPE_INT_DEC, static_cast<uint32_t>(attr->maxInt)); + flattenEntry(&key, &val); + } + + for (Attribute::Symbol& s : attr->symbols) { + BinaryPrimitive val(Res_value::TYPE_INT_DEC, s.value); + flattenEntry(&s.symbol, &val); + } + } + + static bool cmpStyleEntries(const Style::Entry& a, const Style::Entry& b) { + if (a.key.id) { + if (b.key.id) { + return a.key.id.value() < b.key.id.value(); + } + return true; + } else if (!b.key.id) { + return a.key.name.value() < b.key.name.value(); + } + return false; + } + + void visit(Style* style) override { + if (style->parent) { + // Parents are treated a bit differently, so record the existence and move on. + mParent = &style->parent.value(); + } + + // Sort the style. + std::sort(style->entries.begin(), style->entries.end(), cmpStyleEntries); + + for (Style::Entry& entry : style->entries) { + flattenEntry(&entry.key, entry.value.get()); + flattenMetaData(&entry.key); + } + } + + void visit(Styleable* styleable) override { + for (auto& attrRef : styleable->entries) { + BinaryPrimitive val(Res_value{}); + flattenEntry(&attrRef, &val); + flattenMetaData(&attrRef); + } + } + + void visit(Array* array) override { + for (auto& item : array->items) { + ResTable_map* outEntry = mBuffer->nextBlock<ResTable_map>(); + flattenValue(item.get(), outEntry); + outEntry->value.size = util::hostToDevice16(sizeof(outEntry->value)); + mEntryCount++; + flattenMetaData(item.get()); + } + } + + void visit(Plural* plural) override { + const size_t count = plural->values.size(); + for (size_t i = 0; i < count; i++) { + if (!plural->values[i]) { + continue; + } + + ResourceId q; + switch (i) { + case Plural::Zero: + q.id = android::ResTable_map::ATTR_ZERO; + break; + + case Plural::One: + q.id = android::ResTable_map::ATTR_ONE; + break; + + case Plural::Two: + q.id = android::ResTable_map::ATTR_TWO; + break; + + case Plural::Few: + q.id = android::ResTable_map::ATTR_FEW; + break; + + case Plural::Many: + q.id = android::ResTable_map::ATTR_MANY; + break; + + case Plural::Other: + q.id = android::ResTable_map::ATTR_OTHER; + break; + + default: + assert(false); + break; + } + + Reference key(q); + flattenEntry(&key, plural->values[i].get()); + flattenMetaData(plural->values[i].get()); + } + } +}; + +class PackageFlattener { +public: + PackageFlattener(IDiagnostics* diag, TableFlattenerOptions options, + ResourceTablePackage* package, SymbolWriter* symbolWriter, + StringPool* sourcePool) : + mDiag(diag), mOptions(options), mPackage(package), mSymbols(symbolWriter), + mSourcePool(sourcePool) { + } + + bool flattenPackage(BigBuffer* buffer) { + ChunkWriter pkgWriter(buffer); + ResTable_package* pkgHeader = pkgWriter.startChunk<ResTable_package>( + RES_TABLE_PACKAGE_TYPE); + pkgHeader->id = util::hostToDevice32(mPackage->id.value()); + + if (mPackage->name.size() >= arraysize(pkgHeader->name)) { + mDiag->error(DiagMessage() << + "package name '" << mPackage->name << "' is too long"); + return false; + } + + // Copy the package name in device endianness. + strcpy16_htod(pkgHeader->name, arraysize(pkgHeader->name), mPackage->name); + + // Serialize the types. We do this now so that our type and key strings + // are populated. We write those first. + BigBuffer typeBuffer(1024); + flattenTypes(&typeBuffer); + + pkgHeader->typeStrings = util::hostToDevice32(pkgWriter.size()); + StringPool::flattenUtf16(pkgWriter.getBuffer(), mTypePool); + + pkgHeader->keyStrings = util::hostToDevice32(pkgWriter.size()); + StringPool::flattenUtf16(pkgWriter.getBuffer(), mKeyPool); + + // Add the ResTable_package header/type/key strings to the offset. + mSymbols->shiftAllOffsets(pkgWriter.size()); + + // Append the types. + buffer->appendBuffer(std::move(typeBuffer)); + + pkgWriter.finish(); + return true; + } + +private: + IDiagnostics* mDiag; + TableFlattenerOptions mOptions; + ResourceTablePackage* mPackage; + StringPool mTypePool; + StringPool mKeyPool; + SymbolWriter* mSymbols; + StringPool* mSourcePool; + + template <typename T, bool IsItem> + T* writeEntry(FlatEntry* entry, BigBuffer* buffer) { + static_assert(std::is_same<ResTable_entry, T>::value || + std::is_same<ResTable_entry_ext, T>::value, + "T must be ResTable_entry or ResTable_entry_ext"); + + T* result = buffer->nextBlock<T>(); + ResTable_entry* outEntry = (ResTable_entry*)(result); + if (entry->entry->symbolStatus.state == SymbolState::kPublic) { + outEntry->flags |= ResTable_entry::FLAG_PUBLIC; + } + + if (entry->value->isWeak()) { + outEntry->flags |= ResTable_entry::FLAG_WEAK; + } + + if (!IsItem) { + outEntry->flags |= ResTable_entry::FLAG_COMPLEX; + } + + outEntry->key.index = util::hostToDevice32(entry->entryKey); + outEntry->size = sizeof(T); + + if (mOptions.useExtendedChunks) { + // Write the extra source block. This will be ignored by the Android runtime. + ResTable_entry_source* sourceBlock = buffer->nextBlock<ResTable_entry_source>(); + sourceBlock->path.index = util::hostToDevice32(entry->sourcePathKey); + sourceBlock->line = util::hostToDevice32(entry->sourceLine); + sourceBlock->comment.index = util::hostToDevice32(entry->commentKey); + outEntry->size += sizeof(*sourceBlock); + } + + outEntry->flags = util::hostToDevice16(outEntry->flags); + outEntry->size = util::hostToDevice16(outEntry->size); + return result; + } + + bool flattenValue(FlatEntry* entry, BigBuffer* buffer) { + if (Item* item = valueCast<Item>(entry->value)) { + writeEntry<ResTable_entry, true>(entry, buffer); + bool privateRef = false; + if (Reference* ref = valueCast<Reference>(entry->value)) { + // If there is no ID or the reference is private and we allow extended chunks, + // write out a 0 and mark the symbol table with the name of the reference. + privateRef = (ref->privateReference && mOptions.useExtendedChunks); + if (!ref->id || privateRef) { + assert(ref->name && "reference must have at least a name"); + mSymbols->addSymbol(*ref, buffer->size() + offsetof(Res_value, data)); + } + } + Res_value* outValue = buffer->nextBlock<Res_value>(); + bool result = item->flatten(outValue); + assert(result && "flatten failed"); + if (privateRef) { + // Force the value of 0 so we look up the symbol at unflatten time. + outValue->data = 0; + } + outValue->size = util::hostToDevice16(sizeof(*outValue)); + } else { + const size_t beforeEntry = buffer->size(); + ResTable_entry_ext* outEntry = writeEntry<ResTable_entry_ext, false>(entry, buffer); + MapFlattenVisitor visitor(mSymbols, entry, buffer, mSourcePool, mSourcePool, + mOptions.useExtendedChunks); + entry->value->accept(&visitor); + outEntry->count = util::hostToDevice32(visitor.mEntryCount); + if (visitor.mParent) { + const bool forceSymbol = visitor.mParent->privateReference && + mOptions.useExtendedChunks; + if (!visitor.mParent->id || forceSymbol) { + assert(visitor.mParent->name && "reference must have a name"); + mSymbols->addSymbol(*visitor.mParent, + beforeEntry + offsetof(ResTable_entry_ext, parent)); + } else { + outEntry->parent.ident = util::hostToDevice32(visitor.mParent->id.value().id); + } + } + } + return true; + } + + bool flattenConfig(const ResourceTableType* type, const ConfigDescription& config, + std::vector<FlatEntry>* entries, BigBuffer* buffer) { + ChunkWriter typeWriter(buffer); + ResTable_type* typeHeader = typeWriter.startChunk<ResTable_type>(RES_TABLE_TYPE_TYPE); + typeHeader->id = type->id.value(); + typeHeader->config = config; + typeHeader->config.swapHtoD(); + + auto maxAccum = [](uint32_t max, const std::unique_ptr<ResourceEntry>& a) -> uint32_t { + return std::max(max, (uint32_t) a->id.value()); + }; + + // Find the largest entry ID. That is how many entries we will have. + const uint32_t entryCount = + std::accumulate(type->entries.begin(), type->entries.end(), 0, maxAccum) + 1; + + typeHeader->entryCount = util::hostToDevice32(entryCount); + uint32_t* indices = typeWriter.nextBlock<uint32_t>(entryCount); + + assert((size_t) entryCount <= std::numeric_limits<uint16_t>::max() + 1); + memset(indices, 0xff, entryCount * sizeof(uint32_t)); + + typeHeader->entriesStart = util::hostToDevice32(typeWriter.size()); + + const size_t entryStart = typeWriter.getBuffer()->size(); + for (FlatEntry& flatEntry : *entries) { + assert(flatEntry.entry->id.value() < entryCount); + indices[flatEntry.entry->id.value()] = util::hostToDevice32( + typeWriter.getBuffer()->size() - entryStart); + if (!flattenValue(&flatEntry, typeWriter.getBuffer())) { + mDiag->error(DiagMessage() + << "failed to flatten resource '" + << ResourceNameRef(mPackage->name, type->type, flatEntry.entry->name) + << "' for configuration '" << config << "'"); + return false; + } + } + typeWriter.finish(); + return true; + } + + std::vector<ResourceTableType*> collectAndSortTypes() { + std::vector<ResourceTableType*> sortedTypes; + for (auto& type : mPackage->types) { + if (type->type == ResourceType::kStyleable && !mOptions.useExtendedChunks) { + // Styleables aren't real Resource Types, they are represented in the R.java + // file. + continue; + } + + assert(type->id && "type must have an ID set"); + + sortedTypes.push_back(type.get()); + } + std::sort(sortedTypes.begin(), sortedTypes.end(), cmpIds<ResourceTableType>); + return sortedTypes; + } + + std::vector<ResourceEntry*> collectAndSortEntries(ResourceTableType* type) { + // Sort the entries by entry ID. + std::vector<ResourceEntry*> sortedEntries; + for (auto& entry : type->entries) { + assert(entry->id && "entry must have an ID set"); + sortedEntries.push_back(entry.get()); + } + std::sort(sortedEntries.begin(), sortedEntries.end(), cmpIds<ResourceEntry>); + return sortedEntries; + } + + bool flattenTypeSpec(ResourceTableType* type, std::vector<ResourceEntry*>* sortedEntries, + BigBuffer* buffer) { + ChunkWriter typeSpecWriter(buffer); + ResTable_typeSpec* specHeader = typeSpecWriter.startChunk<ResTable_typeSpec>( + RES_TABLE_TYPE_SPEC_TYPE); + specHeader->id = type->id.value(); + + if (sortedEntries->empty()) { + typeSpecWriter.finish(); + return true; + } + + // We can't just take the size of the vector. There may be holes in the entry ID space. + // Since the entries are sorted by ID, the last one will be the biggest. + const size_t numEntries = sortedEntries->back()->id.value() + 1; + + specHeader->entryCount = util::hostToDevice32(numEntries); + + // Reserve space for the masks of each resource in this type. These + // show for which configuration axis the resource changes. + uint32_t* configMasks = typeSpecWriter.nextBlock<uint32_t>(numEntries); + + const size_t actualNumEntries = sortedEntries->size(); + for (size_t entryIndex = 0; entryIndex < actualNumEntries; entryIndex++) { + ResourceEntry* entry = sortedEntries->at(entryIndex); + + // Populate the config masks for this entry. + + if (entry->symbolStatus.state == SymbolState::kPublic) { + configMasks[entry->id.value()] |= + util::hostToDevice32(ResTable_typeSpec::SPEC_PUBLIC); + } + + const size_t configCount = entry->values.size(); + for (size_t i = 0; i < configCount; i++) { + const ConfigDescription& config = entry->values[i].config; + for (size_t j = i + 1; j < configCount; j++) { + configMasks[entry->id.value()] |= util::hostToDevice32( + config.diff(entry->values[j].config)); + } + } + } + typeSpecWriter.finish(); + return true; + } + + bool flattenPublic(ResourceTableType* type, std::vector<ResourceEntry*>* sortedEntries, + BigBuffer* buffer) { + ChunkWriter publicWriter(buffer); + Public_header* publicHeader = publicWriter.startChunk<Public_header>(RES_TABLE_PUBLIC_TYPE); + publicHeader->typeId = type->id.value(); + + for (ResourceEntry* entry : *sortedEntries) { + if (entry->symbolStatus.state != SymbolState::kUndefined) { + // Write the public status of this entry. + Public_entry* publicEntry = publicWriter.nextBlock<Public_entry>(); + publicEntry->entryId = util::hostToDevice32(entry->id.value()); + publicEntry->key.index = util::hostToDevice32(mKeyPool.makeRef( + entry->name).getIndex()); + publicEntry->source.path.index = util::hostToDevice32(mSourcePool->makeRef( + util::utf8ToUtf16(entry->symbolStatus.source.path)).getIndex()); + if (entry->symbolStatus.source.line) { + publicEntry->source.line = util::hostToDevice32( + entry->symbolStatus.source.line.value()); + } + publicEntry->source.comment.index = util::hostToDevice32(mSourcePool->makeRef( + entry->symbolStatus.comment).getIndex()); + + switch (entry->symbolStatus.state) { + case SymbolState::kPrivate: + publicEntry->state = Public_entry::kPrivate; + break; + + case SymbolState::kPublic: + publicEntry->state = Public_entry::kPublic; + break; + + case SymbolState::kUndefined: + publicEntry->state = Public_entry::kUndefined; + break; + } + + // Don't hostToDevice until the last step. + publicHeader->count += 1; + } + } + + publicHeader->count = util::hostToDevice32(publicHeader->count); + publicWriter.finish(); + return true; + } + + bool flattenTypes(BigBuffer* buffer) { + // Sort the types by their IDs. They will be inserted into the StringPool in this order. + std::vector<ResourceTableType*> sortedTypes = collectAndSortTypes(); + + size_t expectedTypeId = 1; + for (ResourceTableType* type : sortedTypes) { + // If there is a gap in the type IDs, fill in the StringPool + // with empty values until we reach the ID we expect. + while (type->id.value() > expectedTypeId) { + std::u16string typeName(u"?"); + typeName += expectedTypeId; + mTypePool.makeRef(typeName); + expectedTypeId++; + } + expectedTypeId++; + mTypePool.makeRef(toString(type->type)); + + std::vector<ResourceEntry*> sortedEntries = collectAndSortEntries(type); + + if (!flattenTypeSpec(type, &sortedEntries, buffer)) { + return false; + } + + if (mOptions.useExtendedChunks) { + if (!flattenPublic(type, &sortedEntries, buffer)) { + return false; + } + } + + // The binary resource table lists resource entries for each configuration. + // We store them inverted, where a resource entry lists the values for each + // configuration available. Here we reverse this to match the binary table. + std::map<ConfigDescription, std::vector<FlatEntry>> configToEntryListMap; + for (ResourceEntry* entry : sortedEntries) { + const uint32_t keyIndex = (uint32_t) mKeyPool.makeRef(entry->name).getIndex(); + + // Group values by configuration. + for (auto& configValue : entry->values) { + Value* value = configValue.value.get(); + + const StringPool::Ref sourceRef = mSourcePool->makeRef( + util::utf8ToUtf16(value->getSource().path)); + + uint32_t lineNumber = 0; + if (value->getSource().line) { + lineNumber = value->getSource().line.value(); + } + + const StringPool::Ref commentRef = mSourcePool->makeRef(value->getComment()); + + configToEntryListMap[configValue.config] + .push_back(FlatEntry{ + entry, + value, + keyIndex, + (uint32_t) sourceRef.getIndex(), + lineNumber, + (uint32_t) commentRef.getIndex() }); + } + } + + // Flatten a configuration value. + for (auto& entry : configToEntryListMap) { + if (!flattenConfig(type, entry.first, &entry.second, buffer)) { + return false; + } + } + } + return true; + } +}; + +} // namespace + +bool TableFlattener::consume(IAaptContext* context, ResourceTable* table) { + // We must do this before writing the resources, since the string pool IDs may change. + table->stringPool.sort([](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { + int diff = a.context.priority - b.context.priority; + if (diff < 0) return true; + if (diff > 0) return false; + diff = a.context.config.compare(b.context.config); + if (diff < 0) return true; + if (diff > 0) return false; + return a.value < b.value; + }); + table->stringPool.prune(); + + // Write the ResTable header. + ChunkWriter tableWriter(mBuffer); + ResTable_header* tableHeader = tableWriter.startChunk<ResTable_header>(RES_TABLE_TYPE); + tableHeader->packageCount = util::hostToDevice32(table->packages.size()); + + // Flatten the values string pool. + StringPool::flattenUtf8(tableWriter.getBuffer(), table->stringPool); + + // If we have a reference to a symbol that doesn't exist, we don't know its resource ID. + // We encode the name of the symbol along with the offset of where to include the resource ID + // once it is found. + StringPool symbolPool; + std::vector<SymbolWriter::Entry> symbolOffsets; + + // String pool holding the source paths of each value. + StringPool sourcePool; + + BigBuffer packageBuffer(1024); + + // Flatten each package. + for (auto& package : table->packages) { + const size_t beforePackageSize = packageBuffer.size(); + + // All packages will share a single global symbol pool. + SymbolWriter packageSymbolWriter(&symbolPool); + + PackageFlattener flattener(context->getDiagnostics(), mOptions, package.get(), + &packageSymbolWriter, &sourcePool); + if (!flattener.flattenPackage(&packageBuffer)) { + return false; + } + + // The symbols are offset only from their own Package start. Offset them from the + // start of the packageBuffer. + packageSymbolWriter.shiftAllOffsets(beforePackageSize); + + // Extract all the symbols to offset + symbolOffsets.insert(symbolOffsets.end(), + std::make_move_iterator(packageSymbolWriter.symbols.begin()), + std::make_move_iterator(packageSymbolWriter.symbols.end())); + } + + SymbolTable_entry* symbolEntryData = nullptr; + if (mOptions.useExtendedChunks) { + if (!symbolOffsets.empty()) { + // Sort the offsets so we can scan them linearly. + std::sort(symbolOffsets.begin(), symbolOffsets.end(), + [](const SymbolWriter::Entry& a, const SymbolWriter::Entry& b) -> bool { + return a.offset < b.offset; + }); + + // Write the Symbol header. + ChunkWriter symbolWriter(tableWriter.getBuffer()); + SymbolTable_header* symbolHeader = symbolWriter.startChunk<SymbolTable_header>( + RES_TABLE_SYMBOL_TABLE_TYPE); + symbolHeader->count = util::hostToDevice32(symbolOffsets.size()); + + symbolEntryData = symbolWriter.nextBlock<SymbolTable_entry>(symbolOffsets.size()); + StringPool::flattenUtf8(symbolWriter.getBuffer(), symbolPool); + symbolWriter.finish(); + } + + if (sourcePool.size() > 0) { + // Write out source pool. + ChunkWriter srcWriter(tableWriter.getBuffer()); + srcWriter.startChunk<ResChunk_header>(RES_TABLE_SOURCE_POOL_TYPE); + StringPool::flattenUtf8(srcWriter.getBuffer(), sourcePool); + srcWriter.finish(); + } + } + + const size_t beforePackagesSize = tableWriter.size(); + + // Finally merge all the packages into the main buffer. + tableWriter.getBuffer()->appendBuffer(std::move(packageBuffer)); + + // Update the offsets to their final values. + if (symbolEntryData) { + for (SymbolWriter::Entry& entry : symbolOffsets) { + symbolEntryData->name.index = util::hostToDevice32(entry.name.getIndex()); + + // The symbols were all calculated with the packageBuffer offset. We need to + // add the beginning of the output buffer. + symbolEntryData->offset = util::hostToDevice32(entry.offset + beforePackagesSize); + symbolEntryData++; + } + } + + tableWriter.finish(); + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/flatten/TableFlattener.h b/tools/aapt2/flatten/TableFlattener.h new file mode 100644 index 000000000000..901b129725ea --- /dev/null +++ b/tools/aapt2/flatten/TableFlattener.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_FLATTEN_TABLEFLATTENER_H +#define AAPT_FLATTEN_TABLEFLATTENER_H + +#include "process/IResourceTableConsumer.h" + +namespace aapt { + +class BigBuffer; +class ResourceTable; + +struct TableFlattenerOptions { + /** + * Specifies whether to output extended chunks, like + * source information and missing symbol entries. Default + * is false. + * + * Set this to true when emitting intermediate resource table. + */ + bool useExtendedChunks = false; +}; + +class TableFlattener : public IResourceTableConsumer { +public: + TableFlattener(BigBuffer* buffer, TableFlattenerOptions options) : + mBuffer(buffer), mOptions(options) { + } + + bool consume(IAaptContext* context, ResourceTable* table) override; + +private: + BigBuffer* mBuffer; + TableFlattenerOptions mOptions; +}; + +} // namespace aapt + +#endif /* AAPT_FLATTEN_TABLEFLATTENER_H */ diff --git a/tools/aapt2/flatten/TableFlattener_test.cpp b/tools/aapt2/flatten/TableFlattener_test.cpp new file mode 100644 index 000000000000..7030603e5bbd --- /dev/null +++ b/tools/aapt2/flatten/TableFlattener_test.cpp @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "flatten/TableFlattener.h" +#include "test/Builders.h" +#include "test/Context.h" +#include "unflatten/BinaryResourceParser.h" +#include "util/Util.h" + + +#include <gtest/gtest.h> + +using namespace android; + +namespace aapt { + +class TableFlattenerTest : public ::testing::Test { +public: + void SetUp() override { + mContext = test::ContextBuilder() + .setCompilationPackage(u"com.app.test") + .setPackageId(0x7f) + .build(); + } + + ::testing::AssertionResult flatten(ResourceTable* table, ResTable* outTable) { + BigBuffer buffer(1024); + TableFlattenerOptions options = {}; + options.useExtendedChunks = true; + TableFlattener flattener(&buffer, options); + if (!flattener.consume(mContext.get(), table)) { + return ::testing::AssertionFailure() << "failed to flatten ResourceTable"; + } + + std::unique_ptr<uint8_t[]> data = util::copy(buffer); + if (outTable->add(data.get(), buffer.size(), -1, true) != NO_ERROR) { + return ::testing::AssertionFailure() << "flattened ResTable is corrupt"; + } + return ::testing::AssertionSuccess(); + } + + ::testing::AssertionResult flatten(ResourceTable* table, ResourceTable* outTable) { + BigBuffer buffer(1024); + TableFlattenerOptions options = {}; + options.useExtendedChunks = true; + TableFlattener flattener(&buffer, options); + if (!flattener.consume(mContext.get(), table)) { + return ::testing::AssertionFailure() << "failed to flatten ResourceTable"; + } + + std::unique_ptr<uint8_t[]> data = util::copy(buffer); + BinaryResourceParser parser(mContext.get(), outTable, {}, data.get(), buffer.size()); + if (!parser.parse()) { + return ::testing::AssertionFailure() << "flattened ResTable is corrupt"; + } + return ::testing::AssertionSuccess(); + } + + ::testing::AssertionResult exists(ResTable* table, + const StringPiece16& expectedName, + const ResourceId expectedId, + const ConfigDescription& expectedConfig, + const uint8_t expectedDataType, const uint32_t expectedData, + const uint32_t expectedSpecFlags) { + const ResourceName expectedResName = test::parseNameOrDie(expectedName); + + table->setParameters(&expectedConfig); + + ResTable_config config; + Res_value val; + uint32_t specFlags; + if (table->getResource(expectedId.id, &val, false, 0, &specFlags, &config) < 0) { + return ::testing::AssertionFailure() << "could not find resource with"; + } + + if (expectedDataType != val.dataType) { + return ::testing::AssertionFailure() + << "expected data type " + << std::hex << (int) expectedDataType << " but got data type " + << (int) val.dataType << std::dec << " instead"; + } + + if (expectedData != val.data) { + return ::testing::AssertionFailure() + << "expected data " + << std::hex << expectedData << " but got data " + << val.data << std::dec << " instead"; + } + + if (expectedSpecFlags != specFlags) { + return ::testing::AssertionFailure() + << "expected specFlags " + << std::hex << expectedSpecFlags << " but got specFlags " + << specFlags << std::dec << " instead"; + } + + ResTable::resource_name actualName; + if (!table->getResourceName(expectedId.id, false, &actualName)) { + return ::testing::AssertionFailure() << "failed to find resource name"; + } + + StringPiece16 package16(actualName.package, actualName.packageLen); + if (package16 != expectedResName.package) { + return ::testing::AssertionFailure() + << "expected package '" << expectedResName.package << "' but got '" + << package16 << "'"; + } + + StringPiece16 type16(actualName.type, actualName.typeLen); + if (type16 != toString(expectedResName.type)) { + return ::testing::AssertionFailure() + << "expected type '" << expectedResName.type + << "' but got '" << type16 << "'"; + } + + StringPiece16 name16(actualName.name, actualName.nameLen); + if (name16 != expectedResName.entry) { + return ::testing::AssertionFailure() + << "expected name '" << expectedResName.entry + << "' but got '" << name16 << "'"; + } + + if (expectedConfig != config) { + return ::testing::AssertionFailure() + << "expected config '" << expectedConfig << "' but got '" + << ConfigDescription(config) << "'"; + } + return ::testing::AssertionSuccess(); + } + +private: + std::unique_ptr<IAaptContext> mContext; +}; + +TEST_F(TableFlattenerTest, FlattenFullyLinkedTable) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"com.app.test", 0x7f) + .addSimple(u"@com.app.test:id/one", ResourceId(0x7f020000)) + .addSimple(u"@com.app.test:id/two", ResourceId(0x7f020001)) + .addValue(u"@com.app.test:id/three", ResourceId(0x7f020002), + test::buildReference(u"@com.app.test:id/one", ResourceId(0x7f020000))) + .addValue(u"@com.app.test:integer/one", ResourceId(0x7f030000), + util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 1u)) + .addValue(u"@com.app.test:integer/one", ResourceId(0x7f030000), + test::parseConfigOrDie("v1"), + util::make_unique<BinaryPrimitive>(uint8_t(Res_value::TYPE_INT_DEC), 2u)) + .addString(u"@com.app.test:string/test", ResourceId(0x7f040000), u"foo") + .addString(u"@com.app.test:layout/bar", ResourceId(0x7f050000), u"res/layout/bar.xml") + .build(); + + ResTable resTable; + ASSERT_TRUE(flatten(table.get(), &resTable)); + + EXPECT_TRUE(exists(&resTable, u"@com.app.test:id/one", ResourceId(0x7f020000), {}, + Res_value::TYPE_INT_BOOLEAN, 0u, 0u)); + + EXPECT_TRUE(exists(&resTable, u"@com.app.test:id/two", ResourceId(0x7f020001), {}, + Res_value::TYPE_INT_BOOLEAN, 0u, 0u)); + + EXPECT_TRUE(exists(&resTable, u"@com.app.test:id/three", ResourceId(0x7f020002), {}, + Res_value::TYPE_REFERENCE, 0x7f020000u, 0u)); + + EXPECT_TRUE(exists(&resTable, u"@com.app.test:integer/one", ResourceId(0x7f030000), + {}, Res_value::TYPE_INT_DEC, 1u, + ResTable_config::CONFIG_VERSION)); + + EXPECT_TRUE(exists(&resTable, u"@com.app.test:integer/one", ResourceId(0x7f030000), + test::parseConfigOrDie("v1"), Res_value::TYPE_INT_DEC, 2u, + ResTable_config::CONFIG_VERSION)); + + StringPiece16 fooStr = u"foo"; + ssize_t idx = resTable.getTableStringBlock(0)->indexOfString(fooStr.data(), fooStr.size()); + ASSERT_GE(idx, 0); + EXPECT_TRUE(exists(&resTable, u"@com.app.test:string/test", ResourceId(0x7f040000), + {}, Res_value::TYPE_STRING, (uint32_t) idx, 0u)); + + StringPiece16 barPath = u"res/layout/bar.xml"; + idx = resTable.getTableStringBlock(0)->indexOfString(barPath.data(), barPath.size()); + ASSERT_GE(idx, 0); + EXPECT_TRUE(exists(&resTable, u"@com.app.test:layout/bar", ResourceId(0x7f050000), {}, + Res_value::TYPE_STRING, (uint32_t) idx, 0u)); +} + +TEST_F(TableFlattenerTest, FlattenEntriesWithGapsInIds) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"com.app.test", 0x7f) + .addSimple(u"@com.app.test:id/one", ResourceId(0x7f020001)) + .addSimple(u"@com.app.test:id/three", ResourceId(0x7f020003)) + .build(); + + ResTable resTable; + ASSERT_TRUE(flatten(table.get(), &resTable)); + + EXPECT_TRUE(exists(&resTable, u"@com.app.test:id/one", ResourceId(0x7f020001), {}, + Res_value::TYPE_INT_BOOLEAN, 0u, 0u)); + EXPECT_TRUE(exists(&resTable, u"@com.app.test:id/three", ResourceId(0x7f020003), {}, + Res_value::TYPE_INT_BOOLEAN, 0u, 0u)); +} + +TEST_F(TableFlattenerTest, FlattenUnlinkedTable) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"com.app.test", 0x7f) + .addValue(u"@com.app.test:integer/one", ResourceId(0x7f020000), + test::buildReference(u"@android:integer/foo")) + .addValue(u"@com.app.test:style/Theme", ResourceId(0x7f030000), test::StyleBuilder() + .setParent(u"@android:style/Theme.Material") + .addItem(u"@android:attr/background", {}) + .addItem(u"@android:attr/colorAccent", + test::buildReference(u"@com.app.test:color/green")) + .build()) + .build(); + + { + // Need access to stringPool to make RawString. + Style* style = test::getValue<Style>(table.get(), u"@com.app.test:style/Theme"); + style->entries[0].value = util::make_unique<RawString>(table->stringPool.makeRef(u"foo")); + } + + ResourceTable finalTable; + ASSERT_TRUE(flatten(table.get(), &finalTable)); + + Reference* ref = test::getValue<Reference>(&finalTable, u"@com.app.test:integer/one"); + ASSERT_NE(ref, nullptr); + AAPT_ASSERT_TRUE(ref->name); + EXPECT_EQ(ref->name.value(), test::parseNameOrDie(u"@android:integer/foo")); + + Style* style = test::getValue<Style>(&finalTable, u"@com.app.test:style/Theme"); + ASSERT_NE(style, nullptr); + AAPT_ASSERT_TRUE(style->parent); + AAPT_ASSERT_TRUE(style->parent.value().name); + EXPECT_EQ(style->parent.value().name.value(), + test::parseNameOrDie(u"@android:style/Theme.Material")); + + ASSERT_EQ(2u, style->entries.size()); + + AAPT_ASSERT_TRUE(style->entries[0].key.name); + EXPECT_EQ(style->entries[0].key.name.value(), + test::parseNameOrDie(u"@android:attr/background")); + RawString* raw = valueCast<RawString>(style->entries[0].value.get()); + ASSERT_NE(raw, nullptr); + EXPECT_EQ(*raw->value, u"foo"); + + AAPT_ASSERT_TRUE(style->entries[1].key.name); + EXPECT_EQ(style->entries[1].key.name.value(), + test::parseNameOrDie(u"@android:attr/colorAccent")); + ref = valueCast<Reference>(style->entries[1].value.get()); + ASSERT_NE(ref, nullptr); + AAPT_ASSERT_TRUE(ref->name); + EXPECT_EQ(ref->name.value(), test::parseNameOrDie(u"@com.app.test:color/green")); +} + +TEST_F(TableFlattenerTest, FlattenMinMaxAttributes) { + Attribute attr(false); + attr.typeMask = android::ResTable_map::TYPE_INTEGER; + attr.minInt = 10; + attr.maxInt = 23; + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"android", 0x01) + .addValue(u"@android:attr/foo", ResourceId(0x01010000), + util::make_unique<Attribute>(attr)) + .build(); + + ResourceTable result; + ASSERT_TRUE(flatten(table.get(), &result)); + + Attribute* actualAttr = test::getValue<Attribute>(&result, u"@android:attr/foo"); + ASSERT_NE(nullptr, actualAttr); + EXPECT_EQ(attr.isWeak(), actualAttr->isWeak()); + EXPECT_EQ(attr.typeMask, actualAttr->typeMask); + EXPECT_EQ(attr.minInt, actualAttr->minInt); + EXPECT_EQ(attr.maxInt, actualAttr->maxInt); +} + +TEST_F(TableFlattenerTest, FlattenSourceAndCommentsForChildrenOfCompoundValues) { + Style style; + Reference key(test::parseNameOrDie(u"@android:attr/foo")); + key.id = ResourceId(0x01010000); + key.setSource(Source("test").withLine(2)); + key.setComment(StringPiece16(u"comment")); + style.entries.push_back(Style::Entry{ key, util::make_unique<Id>() }); + + test::ResourceTableBuilder builder = test::ResourceTableBuilder(); + std::unique_ptr<ResourceTable> table = builder + .setPackageId(u"android", 0x01) + .addValue(u"@android:attr/foo", ResourceId(0x01010000), + test::AttributeBuilder().build()) + .addValue(u"@android:style/foo", ResourceId(0x01020000), + std::unique_ptr<Style>(style.clone(builder.getStringPool()))) + .build(); + + ResourceTable result; + ASSERT_TRUE(flatten(table.get(), &result)); + + Style* actualStyle = test::getValue<Style>(&result, u"@android:style/foo"); + ASSERT_NE(nullptr, actualStyle); + ASSERT_EQ(1u, actualStyle->entries.size()); + + Reference* actualKey = &actualStyle->entries[0].key; + EXPECT_EQ(key.getSource(), actualKey->getSource()); + EXPECT_EQ(key.getComment(), actualKey->getComment()); +} + +} // namespace aapt diff --git a/tools/aapt2/flatten/XmlFlattener.cpp b/tools/aapt2/flatten/XmlFlattener.cpp new file mode 100644 index 000000000000..8219462f8a73 --- /dev/null +++ b/tools/aapt2/flatten/XmlFlattener.cpp @@ -0,0 +1,315 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SdkConstants.h" +#include "flatten/ChunkWriter.h" +#include "flatten/ResourceTypeExtensions.h" +#include "flatten/XmlFlattener.h" +#include "xml/XmlDom.h" + +#include <androidfw/ResourceTypes.h> +#include <utils/misc.h> +#include <vector> + +using namespace android; + +namespace aapt { + +namespace { + +constexpr uint32_t kLowPriority = 0xffffffffu; + +struct XmlFlattenerVisitor : public xml::Visitor { + using xml::Visitor::visit; + + BigBuffer* mBuffer; + XmlFlattenerOptions mOptions; + StringPool mPool; + std::map<uint8_t, StringPool> mPackagePools; + + struct StringFlattenDest { + StringPool::Ref ref; + ResStringPool_ref* dest; + }; + std::vector<StringFlattenDest> mStringRefs; + + // Scratch vector to filter attributes. We avoid allocations + // making this a member. + std::vector<xml::Attribute*> mFilteredAttrs; + + + XmlFlattenerVisitor(BigBuffer* buffer, XmlFlattenerOptions options) : + mBuffer(buffer), mOptions(options) { + } + + void addString(const StringPiece16& str, uint32_t priority, android::ResStringPool_ref* dest) { + if (!str.empty()) { + mStringRefs.push_back(StringFlattenDest{ + mPool.makeRef(str, StringPool::Context{ priority }), + dest }); + } else { + // The device doesn't think a string of size 0 is the same as null. + dest->index = util::deviceToHost32(-1); + } + } + + void addString(const StringPool::Ref& ref, android::ResStringPool_ref* dest) { + mStringRefs.push_back(StringFlattenDest{ ref, dest }); + } + + void writeNamespace(xml::Namespace* node, uint16_t type) { + ChunkWriter writer(mBuffer); + + ResXMLTree_node* flatNode = writer.startChunk<ResXMLTree_node>(type); + flatNode->lineNumber = util::hostToDevice32(node->lineNumber); + flatNode->comment.index = util::hostToDevice32(-1); + + ResXMLTree_namespaceExt* flatNs = writer.nextBlock<ResXMLTree_namespaceExt>(); + addString(node->namespacePrefix, kLowPriority, &flatNs->prefix); + addString(node->namespaceUri, kLowPriority, &flatNs->uri); + + writer.finish(); + } + + void visit(xml::Namespace* node) override { + writeNamespace(node, android::RES_XML_START_NAMESPACE_TYPE); + xml::Visitor::visit(node); + writeNamespace(node, android::RES_XML_END_NAMESPACE_TYPE); + } + + void visit(xml::Text* node) override { + if (util::trimWhitespace(node->text).empty()) { + // Skip whitespace only text nodes. + return; + } + + ChunkWriter writer(mBuffer); + ResXMLTree_node* flatNode = writer.startChunk<ResXMLTree_node>(RES_XML_CDATA_TYPE); + flatNode->lineNumber = util::hostToDevice32(node->lineNumber); + flatNode->comment.index = util::hostToDevice32(-1); + + ResXMLTree_cdataExt* flatText = writer.nextBlock<ResXMLTree_cdataExt>(); + addString(node->text, kLowPriority, &flatText->data); + + writer.finish(); + } + + void visit(xml::Element* node) override { + { + ChunkWriter startWriter(mBuffer); + ResXMLTree_node* flatNode = startWriter.startChunk<ResXMLTree_node>( + RES_XML_START_ELEMENT_TYPE); + flatNode->lineNumber = util::hostToDevice32(node->lineNumber); + flatNode->comment.index = util::hostToDevice32(-1); + + ResXMLTree_attrExt* flatElem = startWriter.nextBlock<ResXMLTree_attrExt>(); + addString(node->namespaceUri, kLowPriority, &flatElem->ns); + addString(node->name, kLowPriority, &flatElem->name); + flatElem->attributeStart = util::hostToDevice16(sizeof(*flatElem)); + flatElem->attributeSize = util::hostToDevice16(sizeof(ResXMLTree_attribute)); + + writeAttributes(node, flatElem, &startWriter); + + startWriter.finish(); + } + + xml::Visitor::visit(node); + + { + ChunkWriter endWriter(mBuffer); + ResXMLTree_node* flatEndNode = endWriter.startChunk<ResXMLTree_node>( + RES_XML_END_ELEMENT_TYPE); + flatEndNode->lineNumber = util::hostToDevice32(node->lineNumber); + flatEndNode->comment.index = util::hostToDevice32(-1); + + ResXMLTree_endElementExt* flatEndElem = endWriter.nextBlock<ResXMLTree_endElementExt>(); + addString(node->namespaceUri, kLowPriority, &flatEndElem->ns); + addString(node->name, kLowPriority, &flatEndElem->name); + + endWriter.finish(); + } + } + + static bool cmpXmlAttributeById(const xml::Attribute* a, const xml::Attribute* b) { + if (a->compiledAttribute) { + if (b->compiledAttribute) { + return a->compiledAttribute.value().id < b->compiledAttribute.value().id; + } + return true; + } else if (!b->compiledAttribute) { + int diff = a->namespaceUri.compare(b->namespaceUri); + if (diff < 0) { + return true; + } else if (diff > 0) { + return false; + } + return a->name < b->name; + } + return false; + } + + void writeAttributes(xml::Element* node, ResXMLTree_attrExt* flatElem, ChunkWriter* writer) { + mFilteredAttrs.clear(); + mFilteredAttrs.reserve(node->attributes.size()); + + // Filter the attributes. + for (xml::Attribute& attr : node->attributes) { + if (mOptions.maxSdkLevel && attr.compiledAttribute) { + size_t sdkLevel = findAttributeSdkLevel(attr.compiledAttribute.value().id); + if (sdkLevel > mOptions.maxSdkLevel.value()) { + continue; + } + } + mFilteredAttrs.push_back(&attr); + } + + if (mFilteredAttrs.empty()) { + return; + } + + const ResourceId kIdAttr(0x010100d0); + + std::sort(mFilteredAttrs.begin(), mFilteredAttrs.end(), cmpXmlAttributeById); + + flatElem->attributeCount = util::hostToDevice16(mFilteredAttrs.size()); + + ResXMLTree_attribute* flatAttr = writer->nextBlock<ResXMLTree_attribute>( + mFilteredAttrs.size()); + uint16_t attributeIndex = 1; + for (const xml::Attribute* xmlAttr : mFilteredAttrs) { + // Assign the indices for specific attributes. + if (xmlAttr->compiledAttribute && + xmlAttr->compiledAttribute.value().id == kIdAttr) { + flatElem->idIndex = util::hostToDevice16(attributeIndex); + } else if (xmlAttr->namespaceUri.empty()) { + if (xmlAttr->name == u"class") { + flatElem->classIndex = util::hostToDevice16(attributeIndex); + } else if (xmlAttr->name == u"style") { + flatElem->styleIndex = util::hostToDevice16(attributeIndex); + } + } + attributeIndex++; + + // Add the namespaceUri to the list of StringRefs to encode. + addString(xmlAttr->namespaceUri, kLowPriority, &flatAttr->ns); + + flatAttr->rawValue.index = util::hostToDevice32(-1); + + if (!xmlAttr->compiledAttribute) { + // The attribute has no associated ResourceID, so the string order doesn't matter. + addString(xmlAttr->name, kLowPriority, &flatAttr->name); + } else { + // Attribute names are stored without packages, but we use + // their StringPool index to lookup their resource IDs. + // This will cause collisions, so we can't dedupe + // attribute names from different packages. We use separate + // pools that we later combine. + // + // Lookup the StringPool for this package and make the reference there. + const xml::AaptAttribute& aaptAttr = xmlAttr->compiledAttribute.value(); + + StringPool::Ref nameRef = mPackagePools[aaptAttr.id.packageId()].makeRef( + xmlAttr->name, StringPool::Context{ aaptAttr.id.id }); + + // Add it to the list of strings to flatten. + addString(nameRef, &flatAttr->name); + + if (mOptions.keepRawValues) { + // Keep raw values (this is for static libraries). + // TODO(with a smarter inflater for binary XML, we can do without this). + addString(xmlAttr->value, kLowPriority, &flatAttr->rawValue); + } + } + + if (xmlAttr->compiledValue) { + bool result = xmlAttr->compiledValue->flatten(&flatAttr->typedValue); + assert(result); + } else { + // Flatten as a regular string type. + flatAttr->typedValue.dataType = android::Res_value::TYPE_STRING; + addString(xmlAttr->value, kLowPriority, &flatAttr->rawValue); + addString(xmlAttr->value, kLowPriority, + (ResStringPool_ref*) &flatAttr->typedValue.data); + } + + flatAttr->typedValue.size = util::hostToDevice16(sizeof(flatAttr->typedValue)); + flatAttr++; + } + } +}; + +} // namespace + +bool XmlFlattener::flatten(IAaptContext* context, xml::Node* node) { + BigBuffer nodeBuffer(1024); + XmlFlattenerVisitor visitor(&nodeBuffer, mOptions); + node->accept(&visitor); + + // Merge the package pools into the main pool. + for (auto& packagePoolEntry : visitor.mPackagePools) { + visitor.mPool.merge(std::move(packagePoolEntry.second)); + } + + // Sort the string pool so that attribute resource IDs show up first. + visitor.mPool.sort([](const StringPool::Entry& a, const StringPool::Entry& b) -> bool { + return a.context.priority < b.context.priority; + }); + + // Now we flatten the string pool references into the correct places. + for (const auto& refEntry : visitor.mStringRefs) { + refEntry.dest->index = util::hostToDevice32(refEntry.ref.getIndex()); + } + + // Write the XML header. + ChunkWriter xmlHeaderWriter(mBuffer); + xmlHeaderWriter.startChunk<ResXMLTree_header>(RES_XML_TYPE); + + // Flatten the StringPool. + StringPool::flattenUtf16(mBuffer, visitor.mPool); + + { + // Write the array of resource IDs, indexed by StringPool order. + ChunkWriter resIdMapWriter(mBuffer); + resIdMapWriter.startChunk<ResChunk_header>(RES_XML_RESOURCE_MAP_TYPE); + for (const auto& str : visitor.mPool) { + ResourceId id = { str->context.priority }; + if (id.id == kLowPriority || !id.isValid()) { + // When we see the first non-resource ID, + // we're done. + break; + } + + *resIdMapWriter.nextBlock<uint32_t>() = id.id; + } + resIdMapWriter.finish(); + } + + // Move the nodeBuffer and append it to the out buffer. + mBuffer->appendBuffer(std::move(nodeBuffer)); + + // Finish the xml header. + xmlHeaderWriter.finish(); + return true; +} + +bool XmlFlattener::consume(IAaptContext* context, xml::XmlResource* resource) { + if (!resource->root) { + return false; + } + return flatten(context, resource->root.get()); +} + +} // namespace aapt diff --git a/tools/aapt2/flatten/XmlFlattener.h b/tools/aapt2/flatten/XmlFlattener.h new file mode 100644 index 000000000000..a688ac965b0d --- /dev/null +++ b/tools/aapt2/flatten/XmlFlattener.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_FLATTEN_XMLFLATTENER_H +#define AAPT_FLATTEN_XMLFLATTENER_H + +#include "process/IResourceTableConsumer.h" +#include "util/BigBuffer.h" +#include "xml/XmlDom.h" + +namespace aapt { + +struct XmlFlattenerOptions { + /** + * Keep attribute raw string values along with typed values. + */ + bool keepRawValues = false; + + /** + * If set, the max SDK level of attribute to flatten. All others are ignored. + */ + Maybe<size_t> maxSdkLevel; +}; + +class XmlFlattener : public IXmlResourceConsumer { +public: + XmlFlattener(BigBuffer* buffer, XmlFlattenerOptions options) : + mBuffer(buffer), mOptions(options) { + } + + bool consume(IAaptContext* context, xml::XmlResource* resource) override; + +private: + BigBuffer* mBuffer; + XmlFlattenerOptions mOptions; + + bool flatten(IAaptContext* context, xml::Node* node); +}; + +} // namespace aapt + +#endif /* AAPT_FLATTEN_XMLFLATTENER_H */ diff --git a/tools/aapt2/flatten/XmlFlattener_test.cpp b/tools/aapt2/flatten/XmlFlattener_test.cpp new file mode 100644 index 000000000000..864887927328 --- /dev/null +++ b/tools/aapt2/flatten/XmlFlattener_test.cpp @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "flatten/XmlFlattener.h" +#include "link/Linkers.h" +#include "test/Builders.h" +#include "test/Context.h" +#include "util/BigBuffer.h" +#include "util/Util.h" + +#include <androidfw/ResourceTypes.h> +#include <gtest/gtest.h> + +namespace aapt { + +class XmlFlattenerTest : public ::testing::Test { +public: + void SetUp() override { + mContext = test::ContextBuilder() + .setCompilationPackage(u"com.app.test") + .setNameManglerPolicy(NameManglerPolicy{ u"com.app.test" }) + .setSymbolTable(test::StaticSymbolTableBuilder() + .addSymbol(u"@android:attr/id", ResourceId(0x010100d0), + test::AttributeBuilder().build()) + .addSymbol(u"@com.app.test:id/id", ResourceId(0x7f020000)) + .addSymbol(u"@android:attr/paddingStart", ResourceId(0x010103b3), + test::AttributeBuilder().build()) + .addSymbol(u"@android:attr/colorAccent", ResourceId(0x01010435), + test::AttributeBuilder().build()) + .build()) + .build(); + } + + ::testing::AssertionResult flatten(xml::XmlResource* doc, android::ResXMLTree* outTree, + XmlFlattenerOptions options = {}) { + BigBuffer buffer(1024); + XmlFlattener flattener(&buffer, options); + if (!flattener.consume(mContext.get(), doc)) { + return ::testing::AssertionFailure() << "failed to flatten XML Tree"; + } + + std::unique_ptr<uint8_t[]> data = util::copy(buffer); + if (outTree->setTo(data.get(), buffer.size(), true) != android::NO_ERROR) { + return ::testing::AssertionFailure() << "flattened XML is corrupt"; + } + return ::testing::AssertionSuccess(); + } + +protected: + std::unique_ptr<IAaptContext> mContext; +}; + +TEST_F(XmlFlattenerTest, FlattenXmlWithNoCompiledAttributes) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDom(R"EOF( + <View xmlns:test="http://com.test" + attr="hey"> + <Layout test:hello="hi" /> + <Layout>Some text</Layout> + </View>)EOF"); + + + android::ResXMLTree tree; + ASSERT_TRUE(flatten(doc.get(), &tree)); + + ASSERT_EQ(tree.next(), android::ResXMLTree::START_NAMESPACE); + + size_t len; + const char16_t* namespacePrefix = tree.getNamespacePrefix(&len); + EXPECT_EQ(StringPiece16(namespacePrefix, len), u"test"); + + const char16_t* namespaceUri = tree.getNamespaceUri(&len); + ASSERT_EQ(StringPiece16(namespaceUri, len), u"http://com.test"); + + ASSERT_EQ(tree.next(), android::ResXMLTree::START_TAG); + + ASSERT_EQ(tree.getElementNamespace(&len), nullptr); + const char16_t* tagName = tree.getElementName(&len); + EXPECT_EQ(StringPiece16(tagName, len), u"View"); + + ASSERT_EQ(1u, tree.getAttributeCount()); + ASSERT_EQ(tree.getAttributeNamespace(0, &len), nullptr); + const char16_t* attrName = tree.getAttributeName(0, &len); + EXPECT_EQ(StringPiece16(attrName, len), u"attr"); + + EXPECT_EQ(0, tree.indexOfAttribute(nullptr, 0, u"attr", StringPiece16(u"attr").size())); + + ASSERT_EQ(tree.next(), android::ResXMLTree::START_TAG); + + ASSERT_EQ(tree.getElementNamespace(&len), nullptr); + tagName = tree.getElementName(&len); + EXPECT_EQ(StringPiece16(tagName, len), u"Layout"); + + ASSERT_EQ(1u, tree.getAttributeCount()); + const char16_t* attrNamespace = tree.getAttributeNamespace(0, &len); + EXPECT_EQ(StringPiece16(attrNamespace, len), u"http://com.test"); + + attrName = tree.getAttributeName(0, &len); + EXPECT_EQ(StringPiece16(attrName, len), u"hello"); + + ASSERT_EQ(tree.next(), android::ResXMLTree::END_TAG); + ASSERT_EQ(tree.next(), android::ResXMLTree::START_TAG); + + ASSERT_EQ(tree.getElementNamespace(&len), nullptr); + tagName = tree.getElementName(&len); + EXPECT_EQ(StringPiece16(tagName, len), u"Layout"); + ASSERT_EQ(0u, tree.getAttributeCount()); + + ASSERT_EQ(tree.next(), android::ResXMLTree::TEXT); + const char16_t* text = tree.getText(&len); + EXPECT_EQ(StringPiece16(text, len), u"Some text"); + + ASSERT_EQ(tree.next(), android::ResXMLTree::END_TAG); + ASSERT_EQ(tree.getElementNamespace(&len), nullptr); + tagName = tree.getElementName(&len); + EXPECT_EQ(StringPiece16(tagName, len), u"Layout"); + + ASSERT_EQ(tree.next(), android::ResXMLTree::END_TAG); + ASSERT_EQ(tree.getElementNamespace(&len), nullptr); + tagName = tree.getElementName(&len); + EXPECT_EQ(StringPiece16(tagName, len), u"View"); + + ASSERT_EQ(tree.next(), android::ResXMLTree::END_NAMESPACE); + namespacePrefix = tree.getNamespacePrefix(&len); + EXPECT_EQ(StringPiece16(namespacePrefix, len), u"test"); + + namespaceUri = tree.getNamespaceUri(&len); + ASSERT_EQ(StringPiece16(namespaceUri, len), u"http://com.test"); + + ASSERT_EQ(tree.next(), android::ResXMLTree::END_DOCUMENT); +} + +TEST_F(XmlFlattenerTest, FlattenCompiledXmlAndStripSdk21) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDom(R"EOF( + <View xmlns:android="http://schemas.android.com/apk/res/android" + android:paddingStart="1dp" + android:colorAccent="#ffffff"/>)EOF"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.consume(mContext.get(), doc.get())); + ASSERT_TRUE(linker.getSdkLevels().count(17) == 1); + ASSERT_TRUE(linker.getSdkLevels().count(21) == 1); + + android::ResXMLTree tree; + XmlFlattenerOptions options; + options.maxSdkLevel = 17; + ASSERT_TRUE(flatten(doc.get(), &tree, options)); + + while (tree.next() != android::ResXMLTree::START_TAG) { + ASSERT_NE(tree.getEventType(), android::ResXMLTree::BAD_DOCUMENT); + ASSERT_NE(tree.getEventType(), android::ResXMLTree::END_DOCUMENT); + } + + ASSERT_EQ(1u, tree.getAttributeCount()); + EXPECT_EQ(uint32_t(0x010103b3), tree.getAttributeNameResID(0)); +} + +TEST_F(XmlFlattenerTest, AssignSpecialAttributeIndices) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDom(R"EOF( + <View xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@id/id" + class="str" + style="@id/id"/>)EOF"); + + android::ResXMLTree tree; + ASSERT_TRUE(flatten(doc.get(), &tree)); + + while (tree.next() != android::ResXMLTree::START_TAG) { + ASSERT_NE(tree.getEventType(), android::ResXMLTree::BAD_DOCUMENT); + ASSERT_NE(tree.getEventType(), android::ResXMLTree::END_DOCUMENT); + } + + EXPECT_EQ(tree.indexOfClass(), 0); + EXPECT_EQ(tree.indexOfStyle(), 1); +} + +/* + * The device ResXMLParser in libandroidfw differentiates between empty namespace and null + * namespace. + */ +TEST_F(XmlFlattenerTest, NoNamespaceIsNotTheSameAsEmptyNamespace) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDom("<View package=\"android\"/>"); + + android::ResXMLTree tree; + ASSERT_TRUE(flatten(doc.get(), &tree)); + + while (tree.next() != android::ResXMLTree::START_TAG) { + ASSERT_NE(tree.getEventType(), android::ResXMLTree::BAD_DOCUMENT); + ASSERT_NE(tree.getEventType(), android::ResXMLTree::END_DOCUMENT); + } + + const StringPiece16 kPackage = u"package"; + EXPECT_GE(tree.indexOfAttribute(nullptr, 0, kPackage.data(), kPackage.size()), 0); +} + +} // namespace aapt diff --git a/tools/aapt2/io/Data.h b/tools/aapt2/io/Data.h new file mode 100644 index 000000000000..467e60464a68 --- /dev/null +++ b/tools/aapt2/io/Data.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_IO_DATA_H +#define AAPT_IO_DATA_H + +#include <utils/FileMap.h> + +#include <memory> + +namespace aapt { +namespace io { + +/** + * Interface for a block of contiguous memory. An instance of this interface owns the data. + */ +class IData { +public: + virtual ~IData() = default; + + virtual const void* data() const = 0; + virtual size_t size() const = 0; +}; + +/** + * Implementation of IData that exposes a memory mapped file. The mmapped file is owned by this + * object. + */ +class MmappedData : public IData { +public: + explicit MmappedData(android::FileMap&& map) : mMap(std::forward<android::FileMap>(map)) { + } + + const void* data() const override { + return mMap.getDataPtr(); + } + + size_t size() const override { + return mMap.getDataLength(); + } + +private: + android::FileMap mMap; +}; + +/** + * Implementation of IData that exposes a block of memory that was malloc'ed (new'ed). The + * memory is owned by this object. + */ +class MallocData : public IData { +public: + MallocData(std::unique_ptr<const uint8_t[]> data, size_t size) : + mData(std::move(data)), mSize(size) { + } + + const void* data() const override { + return mData.get(); + } + + size_t size() const override { + return mSize; + } + +private: + std::unique_ptr<const uint8_t[]> mData; + size_t mSize; +}; + +/** + * When mmap fails because the file has length 0, we use the EmptyData to simulate data of length 0. + */ +class EmptyData : public IData { +public: + const void* data() const override { + static const uint8_t d = 0; + return &d; + } + + size_t size() const override { + return 0u; + } +}; + +} // namespace io +} // namespace aapt + +#endif /* AAPT_IO_DATA_H */ diff --git a/tools/aapt2/io/File.h b/tools/aapt2/io/File.h new file mode 100644 index 000000000000..b4d49719aa3e --- /dev/null +++ b/tools/aapt2/io/File.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_IO_FILE_H +#define AAPT_IO_FILE_H + +#include "Source.h" +#include "io/Data.h" + +#include <memory> +#include <vector> + +namespace aapt { +namespace io { + +/** + * Interface for a file, which could be a real file on the file system, or a file inside + * a ZIP archive. + */ +class IFile { +public: + virtual ~IFile() = default; + + /** + * Open the file and return it as a block of contiguous memory. How this occurs is + * implementation dependent. For example, if this is a file on the file system, it may + * simply mmap the contents. If this file represents a compressed file in a ZIP archive, + * it may need to inflate it to memory, incurring a copy. + * + * Returns nullptr on failure. + */ + virtual std::unique_ptr<IData> openAsData() = 0; + + /** + * Returns the source of this file. This is for presentation to the user and may not be a + * valid file system path (for example, it may contain a '@' sign to separate the files within + * a ZIP archive from the path to the containing ZIP archive. + */ + virtual const Source& getSource() const = 0; +}; + +class IFileCollectionIterator { +public: + virtual ~IFileCollectionIterator() = default; + + virtual bool hasNext() = 0; + virtual IFile* next() = 0; +}; + +/** + * Interface for a collection of files, all of which share a common source. That source may + * simply be the filesystem, or a ZIP archive. + */ +class IFileCollection { +public: + virtual ~IFileCollection() = default; + + virtual IFile* findFile(const StringPiece& path) = 0; + virtual std::unique_ptr<IFileCollectionIterator> iterator() = 0; +}; + +} // namespace io +} // namespace aapt + +#endif /* AAPT_IO_FILE_H */ diff --git a/tools/aapt2/io/FileSystem.cpp b/tools/aapt2/io/FileSystem.cpp new file mode 100644 index 000000000000..e758d8a421e1 --- /dev/null +++ b/tools/aapt2/io/FileSystem.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Source.h" +#include "io/FileSystem.h" +#include "util/Files.h" +#include "util/Maybe.h" +#include "util/StringPiece.h" +#include "util/Util.h" + +#include <utils/FileMap.h> + +namespace aapt { +namespace io { + +RegularFile::RegularFile(const Source& source) : mSource(source) { +} + +std::unique_ptr<IData> RegularFile::openAsData() { + android::FileMap map; + if (Maybe<android::FileMap> map = file::mmapPath(mSource.path, nullptr)) { + if (map.value().getDataPtr() && map.value().getDataLength() > 0) { + return util::make_unique<MmappedData>(std::move(map.value())); + } + return util::make_unique<EmptyData>(); + } + return {}; +} + +const Source& RegularFile::getSource() const { + return mSource; +} + +FileCollectionIterator::FileCollectionIterator(FileCollection* collection) : + mCurrent(collection->mFiles.begin()), mEnd(collection->mFiles.end()) { +} + +bool FileCollectionIterator::hasNext() { + return mCurrent != mEnd; +} + +IFile* FileCollectionIterator::next() { + IFile* result = mCurrent->second.get(); + ++mCurrent; + return result; +} + +IFile* FileCollection::insertFile(const StringPiece& path) { + return (mFiles[path.toString()] = util::make_unique<RegularFile>(Source(path))).get(); +} + +IFile* FileCollection::findFile(const StringPiece& path) { + auto iter = mFiles.find(path.toString()); + if (iter != mFiles.end()) { + return iter->second.get(); + } + return nullptr; +} + +std::unique_ptr<IFileCollectionIterator> FileCollection::iterator() { + return util::make_unique<FileCollectionIterator>(this); +} + +} // namespace io +} // namespace aapt diff --git a/tools/aapt2/io/FileSystem.h b/tools/aapt2/io/FileSystem.h new file mode 100644 index 000000000000..f0559c03a8b8 --- /dev/null +++ b/tools/aapt2/io/FileSystem.h @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_IO_FILESYSTEM_H +#define AAPT_IO_FILESYSTEM_H + +#include "io/File.h" + +#include <map> + +namespace aapt { +namespace io { + +/** + * A regular file from the file system. Uses mmap to open the data. + */ +class RegularFile : public IFile { +public: + RegularFile(const Source& source); + + std::unique_ptr<IData> openAsData() override; + const Source& getSource() const override; + +private: + Source mSource; +}; + +class FileCollection; + +class FileCollectionIterator : public IFileCollectionIterator { +public: + FileCollectionIterator(FileCollection* collection); + + bool hasNext() override; + io::IFile* next() override; + +private: + std::map<std::string, std::unique_ptr<IFile>>::const_iterator mCurrent, mEnd; +}; + +/** + * An IFileCollection representing the file system. + */ +class FileCollection : public IFileCollection { +public: + /** + * Adds a file located at path. Returns the IFile representation of that file. + */ + IFile* insertFile(const StringPiece& path); + IFile* findFile(const StringPiece& path) override; + std::unique_ptr<IFileCollectionIterator> iterator() override; + +private: + friend class FileCollectionIterator; + std::map<std::string, std::unique_ptr<IFile>> mFiles; +}; + +} // namespace io +} // namespace aapt + +#endif // AAPT_IO_FILESYSTEM_H diff --git a/tools/aapt2/io/ZipArchive.cpp b/tools/aapt2/io/ZipArchive.cpp new file mode 100644 index 000000000000..329dac9163ce --- /dev/null +++ b/tools/aapt2/io/ZipArchive.cpp @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Source.h" +#include "io/ZipArchive.h" +#include "util/Util.h" + +#include <utils/FileMap.h> +#include <ziparchive/zip_archive.h> + +namespace aapt { +namespace io { + +ZipFile::ZipFile(ZipArchiveHandle handle, const ZipEntry& entry, const Source& source) : + mZipHandle(handle), mZipEntry(entry), mSource(source) { +} + +std::unique_ptr<IData> ZipFile::openAsData() { + if (mZipEntry.method == kCompressStored) { + int fd = GetFileDescriptor(mZipHandle); + + android::FileMap fileMap; + bool result = fileMap.create(nullptr, fd, mZipEntry.offset, + mZipEntry.uncompressed_length, true); + if (!result) { + return {}; + } + return util::make_unique<MmappedData>(std::move(fileMap)); + + } else { + std::unique_ptr<uint8_t[]> data = std::unique_ptr<uint8_t[]>( + new uint8_t[mZipEntry.uncompressed_length]); + int32_t result = ExtractToMemory(mZipHandle, &mZipEntry, data.get(), + static_cast<uint32_t>(mZipEntry.uncompressed_length)); + if (result != 0) { + return {}; + } + return util::make_unique<MallocData>(std::move(data), mZipEntry.uncompressed_length); + } +} + +const Source& ZipFile::getSource() const { + return mSource; +} + +ZipFileCollectionIterator::ZipFileCollectionIterator(ZipFileCollection* collection) : + mCurrent(collection->mFiles.begin()), mEnd(collection->mFiles.end()) { +} + +bool ZipFileCollectionIterator::hasNext() { + return mCurrent != mEnd; +} + +IFile* ZipFileCollectionIterator::next() { + IFile* result = mCurrent->second.get(); + ++mCurrent; + return result; +} + +ZipFileCollection::ZipFileCollection() : mHandle(nullptr) { +} + +std::unique_ptr<ZipFileCollection> ZipFileCollection::create(const StringPiece& path, + std::string* outError) { + constexpr static const int32_t kEmptyArchive = -6; + + std::unique_ptr<ZipFileCollection> collection = std::unique_ptr<ZipFileCollection>( + new ZipFileCollection()); + + int32_t result = OpenArchive(path.data(), &collection->mHandle); + if (result != 0) { + // If a zip is empty, result will be an error code. This is fine and we should + // return an empty ZipFileCollection. + if (result == kEmptyArchive) { + return collection; + } + + if (outError) *outError = ErrorCodeString(result); + return {}; + } + + ZipString suffix(".flat"); + void* cookie = nullptr; + result = StartIteration(collection->mHandle, &cookie, nullptr, &suffix); + if (result != 0) { + if (outError) *outError = ErrorCodeString(result); + return {}; + } + + using IterationEnder = std::unique_ptr<void, decltype(EndIteration)*>; + IterationEnder iterationEnder(cookie, EndIteration); + + ZipString zipEntryName; + ZipEntry zipData; + while ((result = Next(cookie, &zipData, &zipEntryName)) == 0) { + std::string zipEntryPath = std::string(reinterpret_cast<const char*>(zipEntryName.name), + zipEntryName.name_length); + std::string nestedPath = path.toString() + "@" + zipEntryPath; + collection->mFiles[zipEntryPath] = util::make_unique<ZipFile>(collection->mHandle, + zipData, + Source(nestedPath)); + } + + if (result != -1) { + if (outError) *outError = ErrorCodeString(result); + return {}; + } + return collection; +} + +IFile* ZipFileCollection::findFile(const StringPiece& path) { + auto iter = mFiles.find(path.toString()); + if (iter != mFiles.end()) { + return iter->second.get(); + } + return nullptr; +} + +std::unique_ptr<IFileCollectionIterator> ZipFileCollection::iterator() { + return util::make_unique<ZipFileCollectionIterator>(this); +} + +ZipFileCollection::~ZipFileCollection() { + if (mHandle) { + CloseArchive(mHandle); + } +} + +} // namespace io +} // namespace aapt diff --git a/tools/aapt2/io/ZipArchive.h b/tools/aapt2/io/ZipArchive.h new file mode 100644 index 000000000000..5ad119d1d6d4 --- /dev/null +++ b/tools/aapt2/io/ZipArchive.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_IO_ZIPARCHIVE_H +#define AAPT_IO_ZIPARCHIVE_H + +#include "io/File.h" +#include "util/StringPiece.h" + +#include <map> +#include <ziparchive/zip_archive.h> + +namespace aapt { +namespace io { + +/** + * An IFile representing a file within a ZIP archive. If the file is compressed, it is uncompressed + * and copied into memory when opened. Otherwise it is mmapped from the ZIP archive. + */ +class ZipFile : public IFile { +public: + ZipFile(ZipArchiveHandle handle, const ZipEntry& entry, const Source& source); + + std::unique_ptr<IData> openAsData() override; + const Source& getSource() const override; + +private: + ZipArchiveHandle mZipHandle; + ZipEntry mZipEntry; + Source mSource; +}; + +class ZipFileCollection; + +class ZipFileCollectionIterator : public IFileCollectionIterator { +public: + ZipFileCollectionIterator(ZipFileCollection* collection); + + bool hasNext() override; + io::IFile* next() override; + +private: + std::map<std::string, std::unique_ptr<IFile>>::const_iterator mCurrent, mEnd; +}; + +/** + * An IFileCollection that represents a ZIP archive and the entries within it. + */ +class ZipFileCollection : public IFileCollection { +public: + static std::unique_ptr<ZipFileCollection> create(const StringPiece& path, + std::string* outError); + + io::IFile* findFile(const StringPiece& path) override; + std::unique_ptr<IFileCollectionIterator> iterator() override; + + ~ZipFileCollection() override; + +private: + friend class ZipFileCollectionIterator; + ZipFileCollection(); + + ZipArchiveHandle mHandle; + std::map<std::string, std::unique_ptr<IFile>> mFiles; +}; + +} // namespace io +} // namespace aapt + +#endif /* AAPT_IO_ZIPARCHIVE_H */ diff --git a/tools/aapt2/java/AnnotationProcessor.cpp b/tools/aapt2/java/AnnotationProcessor.cpp new file mode 100644 index 000000000000..9c25d4e35975 --- /dev/null +++ b/tools/aapt2/java/AnnotationProcessor.cpp @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "java/AnnotationProcessor.h" +#include "util/Util.h" + +#include <algorithm> + +namespace aapt { + +void AnnotationProcessor::appendCommentLine(const std::string& comment) { + static const std::string sDeprecated = "@deprecated"; + static const std::string sSystemApi = "@SystemApi"; + + if (comment.find(sDeprecated) != std::string::npos) { + mAnnotationBitMask |= kDeprecated; + } + + if (comment.find(sSystemApi) != std::string::npos) { + mAnnotationBitMask |= kSystemApi; + } + + if (!mHasComments) { + mHasComments = true; + mComment << "/**"; + } + + mComment << "\n" << " * " << std::move(comment); +} + +void AnnotationProcessor::appendComment(const StringPiece16& comment) { + // We need to process line by line to clean-up whitespace and append prefixes. + for (StringPiece16 line : util::tokenize(comment, u'\n')) { + line = util::trimWhitespace(line); + if (!line.empty()) { + appendCommentLine(util::utf16ToUtf8(line)); + } + } +} + +void AnnotationProcessor::appendComment(const StringPiece& comment) { + for (StringPiece line : util::tokenize(comment, '\n')) { + line = util::trimWhitespace(line); + if (!line.empty()) { + appendCommentLine(line.toString()); + } + } +} + +void AnnotationProcessor::writeToStream(std::ostream* out, const StringPiece& prefix) { + if (mHasComments) { + std::string result = mComment.str(); + for (StringPiece line : util::tokenize<char>(result, '\n')) { + *out << prefix << line << "\n"; + } + *out << prefix << " */" << "\n"; + } + + if (mAnnotationBitMask & kDeprecated) { + *out << prefix << "@Deprecated\n"; + } + + if (mAnnotationBitMask & kSystemApi) { + *out << prefix << "@android.annotation.SystemApi\n"; + } +} + +} // namespace aapt diff --git a/tools/aapt2/java/AnnotationProcessor.h b/tools/aapt2/java/AnnotationProcessor.h new file mode 100644 index 000000000000..e7f2be0dc12b --- /dev/null +++ b/tools/aapt2/java/AnnotationProcessor.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_JAVA_ANNOTATIONPROCESSOR_H +#define AAPT_JAVA_ANNOTATIONPROCESSOR_H + +#include "util/StringPiece.h" + +#include <sstream> +#include <string> + +namespace aapt { + +/** + * Builds a JavaDoc comment from a set of XML comments. + * This will also look for instances of @SystemApi and convert them to + * actual Java annotations. + * + * Example: + * + * Input XML: + * + * <!-- This is meant to be hidden because + * It is system api. Also it is @deprecated + * @SystemApi + * --> + * + * Output JavaDoc: + * + * /\* + * * This is meant to be hidden because + * * It is system api. Also it is @deprecated + * * @SystemApi + * *\/ + * + * Output Annotations: + * + * @Deprecated + * @android.annotation.SystemApi + * + */ +class AnnotationProcessor { +public: + /** + * Adds more comments. Since resources can have various values with different configurations, + * we need to collect all the comments. + */ + void appendComment(const StringPiece16& comment); + void appendComment(const StringPiece& comment); + + /** + * Writes the comments and annotations to the stream, with the given prefix before each line. + */ + void writeToStream(std::ostream* out, const StringPiece& prefix); + +private: + enum : uint32_t { + kDeprecated = 0x01, + kSystemApi = 0x02, + }; + + std::stringstream mComment; + std::stringstream mAnnotations; + bool mHasComments = false; + uint32_t mAnnotationBitMask = 0; + + void appendCommentLine(const std::string& line); +}; + +} // namespace aapt + +#endif /* AAPT_JAVA_ANNOTATIONPROCESSOR_H */ diff --git a/tools/aapt2/java/AnnotationProcessor_test.cpp b/tools/aapt2/java/AnnotationProcessor_test.cpp new file mode 100644 index 000000000000..da96b84fd4ea --- /dev/null +++ b/tools/aapt2/java/AnnotationProcessor_test.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ResourceParser.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "java/AnnotationProcessor.h" +#include "test/Builders.h" +#include "test/Context.h" +#include "xml/XmlPullParser.h" + +#include <gtest/gtest.h> + +namespace aapt { + +struct AnnotationProcessorTest : public ::testing::Test { + std::unique_ptr<IAaptContext> mContext; + ResourceTable mTable; + + void SetUp() override { + mContext = test::ContextBuilder().build(); + } + + ::testing::AssertionResult parse(const StringPiece& str) { + ResourceParserOptions options; + ResourceParser parser(mContext->getDiagnostics(), &mTable, Source{}, ConfigDescription{}, + options); + std::stringstream in; + in << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" << str; + xml::XmlPullParser xmlParser(in); + if (parser.parse(&xmlParser)) { + return ::testing::AssertionSuccess(); + } + return ::testing::AssertionFailure(); + } +}; + +TEST_F(AnnotationProcessorTest, EmitsDeprecated) { + ASSERT_TRUE(parse(R"EOF( + <resources> + <declare-styleable name="foo"> + <!-- Some comment, and it should contain + a marker word, something that marks + this resource as nor needed. + {@deprecated That's the marker! } --> + <attr name="autoText" format="boolean" /> + </declare-styleable> + </resources>)EOF")); + + Attribute* attr = test::getValue<Attribute>(&mTable, u"@attr/autoText"); + ASSERT_NE(nullptr, attr); + + AnnotationProcessor processor; + processor.appendComment(attr->getComment()); + + std::stringstream result; + processor.writeToStream(&result, ""); + std::string annotations = result.str(); + + EXPECT_NE(std::string::npos, annotations.find("@Deprecated")); +} + +} // namespace aapt + + diff --git a/tools/aapt2/java/ClassDefinitionWriter.h b/tools/aapt2/java/ClassDefinitionWriter.h new file mode 100644 index 000000000000..04e1274c3a97 --- /dev/null +++ b/tools/aapt2/java/ClassDefinitionWriter.h @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_JAVA_CLASSDEFINITION_H +#define AAPT_JAVA_CLASSDEFINITION_H + +#include "Resource.h" +#include "java/AnnotationProcessor.h" +#include "util/StringPiece.h" +#include "util/Util.h" + +#include <sstream> +#include <string> + +namespace aapt { + +struct ClassDefinitionWriterOptions { + bool useFinalQualifier = false; + bool forceCreationIfEmpty = false; +}; + +/** + * Writes a class for use in R.java or Manifest.java. + */ +class ClassDefinitionWriter { +public: + ClassDefinitionWriter(const StringPiece& name, const ClassDefinitionWriterOptions& options) : + mName(name.toString()), mOptions(options), mStarted(false) { + } + + ClassDefinitionWriter(const StringPiece16& name, const ClassDefinitionWriterOptions& options) : + mName(util::utf16ToUtf8(name)), mOptions(options), mStarted(false) { + } + + void addIntMember(const StringPiece& name, AnnotationProcessor* processor, + const uint32_t val) { + ensureClassDeclaration(); + if (processor) { + processor->writeToStream(&mOut, kIndent); + } + mOut << kIndent << "public static " << (mOptions.useFinalQualifier ? "final " : "") + << "int " << name << "=" << val << ";\n"; + } + + void addStringMember(const StringPiece16& name, AnnotationProcessor* processor, + const StringPiece16& val) { + ensureClassDeclaration(); + if (processor) { + processor->writeToStream(&mOut, kIndent); + } + mOut << kIndent << "public static " << (mOptions.useFinalQualifier ? "final " : "") + << "String " << name << "=\"" << val << "\";\n"; + } + + void addResourceMember(const StringPiece16& name, AnnotationProcessor* processor, + const ResourceId id) { + ensureClassDeclaration(); + if (processor) { + processor->writeToStream(&mOut, kIndent); + } + mOut << kIndent << "public static " << (mOptions.useFinalQualifier ? "final " : "") + << "int " << name << "=" << id <<";\n"; + } + + template <typename Iterator, typename FieldAccessorFunc> + void addArrayMember(const StringPiece16& name, AnnotationProcessor* processor, + const Iterator begin, const Iterator end, FieldAccessorFunc f) { + ensureClassDeclaration(); + if (processor) { + processor->writeToStream(&mOut, kIndent); + } + mOut << kIndent << "public static final int[] " << name << "={"; + + for (Iterator current = begin; current != end; ++current) { + if (std::distance(begin, current) % kAttribsPerLine == 0) { + mOut << "\n" << kIndent << kIndent; + } + + mOut << f(*current); + if (std::distance(current, end) > 1) { + mOut << ", "; + } + } + mOut << "\n" << kIndent <<"};\n"; + } + + void writeToStream(std::ostream* out, const StringPiece& prefix, + AnnotationProcessor* processor=nullptr) { + if (mOptions.forceCreationIfEmpty) { + ensureClassDeclaration(); + } + + if (!mStarted) { + return; + } + + if (processor) { + processor->writeToStream(out, prefix); + } + + std::string result = mOut.str(); + for (StringPiece line : util::tokenize<char>(result, '\n')) { + *out << prefix << line << "\n"; + } + *out << prefix << "}\n"; + } + +private: + constexpr static const char* kIndent = " "; + + // The number of attributes to emit per line in a Styleable array. + constexpr static size_t kAttribsPerLine = 4; + + void ensureClassDeclaration() { + if (!mStarted) { + mStarted = true; + mOut << "public static final class " << mName << " {\n"; + } + } + + std::stringstream mOut; + std::string mName; + ClassDefinitionWriterOptions mOptions; + bool mStarted; +}; + +} // namespace aapt + +#endif /* AAPT_JAVA_CLASSDEFINITION_H */ diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp new file mode 100644 index 000000000000..7280f3a968a0 --- /dev/null +++ b/tools/aapt2/java/JavaClassGenerator.cpp @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NameMangler.h" +#include "Resource.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "ValueVisitor.h" + +#include "java/AnnotationProcessor.h" +#include "java/ClassDefinitionWriter.h" +#include "java/JavaClassGenerator.h" +#include "util/Comparators.h" +#include "util/StringPiece.h" + +#include <algorithm> +#include <ostream> +#include <set> +#include <sstream> +#include <tuple> + +namespace aapt { + +JavaClassGenerator::JavaClassGenerator(ResourceTable* table, JavaClassGeneratorOptions options) : + mTable(table), mOptions(options) { +} + +static void generateHeader(const StringPiece16& packageNameToGenerate, std::ostream* out) { + *out << "/* AUTO-GENERATED FILE. DO NOT MODIFY.\n" + " *\n" + " * This class was automatically generated by the\n" + " * aapt tool from the resource data it found. It\n" + " * should not be modified by hand.\n" + " */\n\n" + "package " << packageNameToGenerate << ";\n\n"; +} + +static const std::set<StringPiece16> sJavaIdentifiers = { + u"abstract", u"assert", u"boolean", u"break", u"byte", + u"case", u"catch", u"char", u"class", u"const", u"continue", + u"default", u"do", u"double", u"else", u"enum", u"extends", + u"final", u"finally", u"float", u"for", u"goto", u"if", + u"implements", u"import", u"instanceof", u"int", u"interface", + u"long", u"native", u"new", u"package", u"private", u"protected", + u"public", u"return", u"short", u"static", u"strictfp", u"super", + u"switch", u"synchronized", u"this", u"throw", u"throws", + u"transient", u"try", u"void", u"volatile", u"while", u"true", + u"false", u"null" +}; + +static bool isValidSymbol(const StringPiece16& symbol) { + return sJavaIdentifiers.find(symbol) == sJavaIdentifiers.end(); +} + +/* + * Java symbols can not contain . or -, but those are valid in a resource name. + * Replace those with '_'. + */ +static std::u16string transform(const StringPiece16& symbol) { + std::u16string output = symbol.toString(); + for (char16_t& c : output) { + if (c == u'.' || c == u'-') { + c = u'_'; + } + } + return output; +} + +bool JavaClassGenerator::skipSymbol(SymbolState state) { + switch (mOptions.types) { + case JavaClassGeneratorOptions::SymbolTypes::kAll: + return false; + case JavaClassGeneratorOptions::SymbolTypes::kPublicPrivate: + return state == SymbolState::kUndefined; + case JavaClassGeneratorOptions::SymbolTypes::kPublic: + return state != SymbolState::kPublic; + } + return true; +} + +void JavaClassGenerator::writeStyleableEntryForClass(ClassDefinitionWriter* outClassDef, + AnnotationProcessor* processor, + const StringPiece16& packageNameToGenerate, + const std::u16string& entryName, + const Styleable* styleable) { + // This must be sorted by resource ID. + std::vector<std::pair<ResourceId, ResourceNameRef>> sortedAttributes; + sortedAttributes.reserve(styleable->entries.size()); + for (const auto& attr : styleable->entries) { + // If we are not encoding final attributes, the styleable entry may have no ID + // if we are building a static library. + assert((!mOptions.useFinal || attr.id) && "no ID set for Styleable entry"); + assert(attr.name && "no name set for Styleable entry"); + sortedAttributes.emplace_back(attr.id ? attr.id.value() : ResourceId(0), attr.name.value()); + } + std::sort(sortedAttributes.begin(), sortedAttributes.end()); + + auto accessorFunc = [](const std::pair<ResourceId, ResourceNameRef>& a) -> ResourceId { + return a.first; + }; + + // First we emit the array containing the IDs of each attribute. + outClassDef->addArrayMember(transform(entryName), processor, + sortedAttributes.begin(), + sortedAttributes.end(), + accessorFunc); + + // Now we emit the indices into the array. + size_t attrCount = sortedAttributes.size(); + for (size_t i = 0; i < attrCount; i++) { + std::stringstream name; + name << transform(entryName); + + // We may reference IDs from other packages, so prefix the entry name with + // the package. + const ResourceNameRef& itemName = sortedAttributes[i].second; + if (!itemName.package.empty() && packageNameToGenerate != itemName.package) { + name << "_" << transform(itemName.package); + } + name << "_" << transform(itemName.entry); + + outClassDef->addIntMember(name.str(), nullptr, i); + } +} + +static void addAttributeFormatDoc(AnnotationProcessor* processor, Attribute* attr) { + const uint32_t typeMask = attr->typeMask; + if (typeMask & android::ResTable_map::TYPE_REFERENCE) { + processor->appendComment( + "<p>May be a reference to another resource, in the form\n" + "\"<code>@[+][<i>package</i>:]<i>type</i>/<i>name</i></code>\" or a theme\n" + "attribute in the form\n" + "\"<code>?[<i>package</i>:]<i>type</i>/<i>name</i></code>\"."); + } + + if (typeMask & android::ResTable_map::TYPE_STRING) { + processor->appendComment( + "<p>May be a string value, using '\\\\;' to escape characters such as\n" + "'\\\\n' or '\\\\uxxxx' for a unicode character;"); + } + + if (typeMask & android::ResTable_map::TYPE_INTEGER) { + processor->appendComment("<p>May be an integer value, such as \"<code>100</code>\"."); + } + + if (typeMask & android::ResTable_map::TYPE_BOOLEAN) { + processor->appendComment( + "<p>May be a boolean value, such as \"<code>true</code>\" or\n" + "\"<code>false</code>\"."); + } + + if (typeMask & android::ResTable_map::TYPE_COLOR) { + processor->appendComment( + "<p>May be a color value, in the form of \"<code>#<i>rgb</i></code>\",\n" + "\"<code>#<i>argb</i></code>\", \"<code>#<i>rrggbb</i></code\", or \n" + "\"<code>#<i>aarrggbb</i></code>\"."); + } + + if (typeMask & android::ResTable_map::TYPE_FLOAT) { + processor->appendComment( + "<p>May be a floating point value, such as \"<code>1.2</code>\"."); + } + + if (typeMask & android::ResTable_map::TYPE_DIMENSION) { + processor->appendComment( + "<p>May be a dimension value, which is a floating point number appended with a\n" + "unit such as \"<code>14.5sp</code>\".\n" + "Available units are: px (pixels), dp (density-independent pixels),\n" + "sp (scaled pixels based on preferred font size), in (inches), and\n" + "mm (millimeters)."); + } + + if (typeMask & android::ResTable_map::TYPE_FRACTION) { + processor->appendComment( + "<p>May be a fractional value, which is a floating point number appended with\n" + "either % or %p, such as \"<code>14.5%</code>\".\n" + "The % suffix always means a percentage of the base size;\n" + "the optional %p suffix provides a size relative to some parent container."); + } + + if (typeMask & (android::ResTable_map::TYPE_FLAGS | android::ResTable_map::TYPE_ENUM)) { + if (typeMask & android::ResTable_map::TYPE_FLAGS) { + processor->appendComment( + "<p>Must be one or more (separated by '|') of the following " + "constant values.</p>"); + } else { + processor->appendComment("<p>Must be one of the following constant values.</p>"); + } + + processor->appendComment("<table>\n<colgroup align=\"left\" />\n" + "<colgroup align=\"left\" />\n" + "<colgroup align=\"left\" />\n" + "<tr><th>Constant</th><th>Value</th><th>Description</th></tr>\n"); + for (const Attribute::Symbol& symbol : attr->symbols) { + std::stringstream line; + line << "<tr><td>" << symbol.symbol.name.value().entry << "</td>" + << "<td>" << std::hex << symbol.value << std::dec << "</td>" + << "<td>" << util::trimWhitespace(symbol.symbol.getComment()) << "</td></tr>"; + processor->appendComment(line.str()); + } + processor->appendComment("</table>"); + } +} + +bool JavaClassGenerator::writeEntriesForClass(ClassDefinitionWriter* outClassDef, + const StringPiece16& packageNameToGenerate, + const ResourceTablePackage* package, + const ResourceTableType* type) { + for (const auto& entry : type->entries) { + if (skipSymbol(entry->symbolStatus.state)) { + continue; + } + + ResourceId id(package->id.value(), type->id.value(), entry->id.value()); + assert(id.isValid()); + + std::u16string unmangledPackage; + std::u16string unmangledName = entry->name; + if (NameMangler::unmangle(&unmangledName, &unmangledPackage)) { + // The entry name was mangled, and we successfully unmangled it. + // Check that we want to emit this symbol. + if (package->name != unmangledPackage) { + // Skip the entry if it doesn't belong to the package we're writing. + continue; + } + } else if (packageNameToGenerate != package->name) { + // We are processing a mangled package name, + // but this is a non-mangled resource. + continue; + } + + if (!isValidSymbol(unmangledName)) { + ResourceNameRef resourceName(packageNameToGenerate, type->type, unmangledName); + std::stringstream err; + err << "invalid symbol name '" << resourceName << "'"; + mError = err.str(); + return false; + } + + // Build the comments and annotations for this entry. + + AnnotationProcessor processor; + if (entry->symbolStatus.state != SymbolState::kUndefined) { + processor.appendComment(entry->symbolStatus.comment); + } + + for (const auto& configValue : entry->values) { + processor.appendComment(configValue.value->getComment()); + } + + // If this is an Attribute, append the format Javadoc. + if (!entry->values.empty()) { + if (Attribute* attr = valueCast<Attribute>(entry->values.front().value.get())) { + // We list out the available values for the given attribute. + addAttributeFormatDoc(&processor, attr); + } + } + + if (type->type == ResourceType::kStyleable) { + assert(!entry->values.empty()); + const Styleable* styleable = static_cast<const Styleable*>( + entry->values.front().value.get()); + writeStyleableEntryForClass(outClassDef, &processor, packageNameToGenerate, + unmangledName, styleable); + } else { + outClassDef->addResourceMember(transform(unmangledName), &processor, id); + } + } + return true; +} + +bool JavaClassGenerator::generate(const StringPiece16& packageNameToGenerate, std::ostream* out) { + return generate(packageNameToGenerate, packageNameToGenerate, out); +} + +bool JavaClassGenerator::generate(const StringPiece16& packageNameToGenerate, + const StringPiece16& outPackageName, std::ostream* out) { + generateHeader(outPackageName, out); + + *out << "public final class R {\n"; + + for (const auto& package : mTable->packages) { + for (const auto& type : package->types) { + if (type->type == ResourceType::kAttrPrivate) { + continue; + } + + ClassDefinitionWriterOptions classOptions; + classOptions.useFinalQualifier = mOptions.useFinal; + classOptions.forceCreationIfEmpty = + (mOptions.types == JavaClassGeneratorOptions::SymbolTypes::kPublic); + ClassDefinitionWriter classDef(toString(type->type), classOptions); + bool result = writeEntriesForClass(&classDef, packageNameToGenerate, + package.get(), type.get()); + if (!result) { + return false; + } + + if (type->type == ResourceType::kAttr) { + // Also include private attributes in this same class. + auto iter = std::lower_bound(package->types.begin(), package->types.end(), + ResourceType::kAttrPrivate, cmp::lessThanType); + if (iter != package->types.end() && (*iter)->type == ResourceType::kAttrPrivate) { + result = writeEntriesForClass(&classDef, packageNameToGenerate, + package.get(), iter->get()); + if (!result) { + return false; + } + } + } + + AnnotationProcessor processor; + if (type->type == ResourceType::kStyleable && + mOptions.types == JavaClassGeneratorOptions::SymbolTypes::kPublic) { + // When generating a public R class, we don't want Styleable to be part of the API. + // It is only emitted for documentation purposes. + processor.appendComment("@doconly"); + } + classDef.writeToStream(out, " ", &processor); + } + } + + *out << "}\n"; + out->flush(); + return true; +} + + + +} // namespace aapt diff --git a/tools/aapt2/JavaClassGenerator.h b/tools/aapt2/java/JavaClassGenerator.h index f8b9ee3f1fc8..023d6d635f8c 100644 --- a/tools/aapt2/JavaClassGenerator.h +++ b/tools/aapt2/java/JavaClassGenerator.h @@ -20,28 +20,38 @@ #include "ResourceTable.h" #include "ResourceValues.h" +#include "util/StringPiece.h" + #include <ostream> #include <string> namespace aapt { -/* - * Generates the R.java file for a resource table. - */ -class JavaClassGenerator : ConstValueVisitor { -public: +class AnnotationProcessor; +class ClassDefinitionWriter; + +struct JavaClassGeneratorOptions { /* - * A set of options for this JavaClassGenerator. + * Specifies whether to use the 'final' modifier + * on resource entries. Default is true. */ - struct Options { - /* - * Specifies whether to use the 'final' modifier - * on resource entries. Default is true. - */ - bool useFinal = true; + bool useFinal = true; + + enum class SymbolTypes { + kAll, + kPublicPrivate, + kPublic, }; - JavaClassGenerator(const std::shared_ptr<const ResourceTable>& table, Options options); + SymbolTypes types = SymbolTypes::kAll; +}; + +/* + * Generates the R.java file for a resource table. + */ +class JavaClassGenerator { +public: + JavaClassGenerator(ResourceTable* table, JavaClassGeneratorOptions options); /* * Writes the R.java file to `out`. Only symbols belonging to `package` are written. @@ -50,21 +60,30 @@ public: * We need to generate these symbols in a separate file. * Returns true on success. */ - bool generate(const std::u16string& package, std::ostream& out); + bool generate(const StringPiece16& packageNameToGenerate, std::ostream* out); - /* - * ConstValueVisitor implementation. - */ - void visit(const Styleable& styleable, ValueVisitorArgs& args); + bool generate(const StringPiece16& packageNameToGenerate, + const StringPiece16& outputPackageName, + std::ostream* out); const std::string& getError() const; private: - bool generateType(const std::u16string& package, size_t packageId, - const ResourceTableType& type, std::ostream& out); + bool writeEntriesForClass(ClassDefinitionWriter* outClassDef, + const StringPiece16& packageNameToGenerate, + const ResourceTablePackage* package, + const ResourceTableType* type); + + void writeStyleableEntryForClass(ClassDefinitionWriter* outClassDef, + AnnotationProcessor* processor, + const StringPiece16& packageNameToGenerate, + const std::u16string& entryName, + const Styleable* styleable); + + bool skipSymbol(SymbolState state); - std::shared_ptr<const ResourceTable> mTable; - Options mOptions; + ResourceTable* mTable; + JavaClassGeneratorOptions mOptions; std::string mError; }; diff --git a/tools/aapt2/java/JavaClassGenerator_test.cpp b/tools/aapt2/java/JavaClassGenerator_test.cpp new file mode 100644 index 000000000000..e9e788167966 --- /dev/null +++ b/tools/aapt2/java/JavaClassGenerator_test.cpp @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "java/JavaClassGenerator.h" +#include "util/Util.h" + +#include "test/Builders.h" + +#include <gtest/gtest.h> +#include <sstream> +#include <string> + +namespace aapt { + +TEST(JavaClassGeneratorTest, FailWhenEntryIsJavaKeyword) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"android", 0x01) + .addSimple(u"@android:id/class", ResourceId(0x01020000)) + .build(); + + JavaClassGenerator generator(table.get(), {}); + + std::stringstream out; + EXPECT_FALSE(generator.generate(u"android", &out)); +} + +TEST(JavaClassGeneratorTest, TransformInvalidJavaIdentifierCharacter) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"android", 0x01) + .addSimple(u"@android:id/hey-man", ResourceId(0x01020000)) + .addSimple(u"@android:attr/cool.attr", ResourceId(0x01010000)) + .addValue(u"@android:styleable/hey.dude", ResourceId(0x01030000), + test::StyleableBuilder() + .addItem(u"@android:attr/cool.attr", ResourceId(0x01010000)) + .build()) + .build(); + + JavaClassGenerator generator(table.get(), {}); + + std::stringstream out; + EXPECT_TRUE(generator.generate(u"android", &out)); + + std::string output = out.str(); + + EXPECT_NE(std::string::npos, + output.find("public static final int hey_man=0x01020000;")); + + EXPECT_NE(std::string::npos, + output.find("public static final int[] hey_dude={")); + + EXPECT_NE(std::string::npos, + output.find("public static final int hey_dude_cool_attr=0;")); +} + +TEST(JavaClassGeneratorTest, CorrectPackageNameIsUsed) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"android", 0x01) + .addSimple(u"@android:id/one", ResourceId(0x01020000)) + .addSimple(u"@android:id/com.foo$two", ResourceId(0x01020001)) + .build(); + + JavaClassGenerator generator(table.get(), {}); + std::stringstream out; + ASSERT_TRUE(generator.generate(u"android", u"com.android.internal", &out)); + + std::string output = out.str(); + EXPECT_NE(std::string::npos, output.find("package com.android.internal;")); + EXPECT_NE(std::string::npos, output.find("public static final int one=0x01020000;")); + EXPECT_EQ(std::string::npos, output.find("two")); + EXPECT_EQ(std::string::npos, output.find("com_foo$two")); +} + +TEST(JavaClassGeneratorTest, AttrPrivateIsWrittenAsAttr) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"android", 0x01) + .addSimple(u"@android:attr/two", ResourceId(0x01010001)) + .addSimple(u"@android:^attr-private/one", ResourceId(0x01010000)) + .build(); + + JavaClassGenerator generator(table.get(), {}); + std::stringstream out; + ASSERT_TRUE(generator.generate(u"android", &out)); + + std::string output = out.str(); + EXPECT_NE(std::string::npos, output.find("public static final class attr")); + EXPECT_EQ(std::string::npos, output.find("public static final class ^attr-private")); +} + +TEST(JavaClassGeneratorTest, OnlyWritePublicResources) { + StdErrDiagnostics diag; + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"android", 0x01) + .addSimple(u"@android:id/one", ResourceId(0x01020000)) + .addSimple(u"@android:id/two", ResourceId(0x01020001)) + .addSimple(u"@android:id/three", ResourceId(0x01020002)) + .setSymbolState(u"@android:id/one", ResourceId(0x01020000), SymbolState::kPublic) + .setSymbolState(u"@android:id/two", ResourceId(0x01020001), SymbolState::kPrivate) + .build(); + + JavaClassGeneratorOptions options; + options.types = JavaClassGeneratorOptions::SymbolTypes::kPublic; + { + JavaClassGenerator generator(table.get(), options); + std::stringstream out; + ASSERT_TRUE(generator.generate(u"android", &out)); + std::string output = out.str(); + EXPECT_NE(std::string::npos, output.find("public static final int one=0x01020000;")); + EXPECT_EQ(std::string::npos, output.find("two")); + EXPECT_EQ(std::string::npos, output.find("three")); + } + + options.types = JavaClassGeneratorOptions::SymbolTypes::kPublicPrivate; + { + JavaClassGenerator generator(table.get(), options); + std::stringstream out; + ASSERT_TRUE(generator.generate(u"android", &out)); + std::string output = out.str(); + EXPECT_NE(std::string::npos, output.find("public static final int one=0x01020000;")); + EXPECT_NE(std::string::npos, output.find("public static final int two=0x01020001;")); + EXPECT_EQ(std::string::npos, output.find("three")); + } + + options.types = JavaClassGeneratorOptions::SymbolTypes::kAll; + { + JavaClassGenerator generator(table.get(), options); + std::stringstream out; + ASSERT_TRUE(generator.generate(u"android", &out)); + std::string output = out.str(); + EXPECT_NE(std::string::npos, output.find("public static final int one=0x01020000;")); + EXPECT_NE(std::string::npos, output.find("public static final int two=0x01020001;")); + EXPECT_NE(std::string::npos, output.find("public static final int three=0x01020002;")); + } +} + +/* + * TODO(adamlesinski): Re-enable this once we get merging working again. + * TEST(JavaClassGeneratorTest, EmitPackageMangledSymbols) { + ASSERT_TRUE(addResource(ResourceName{ {}, ResourceType::kId, u"foo" }, + ResourceId{ 0x01, 0x02, 0x0000 })); + ResourceTable table; + table.setPackage(u"com.lib"); + ASSERT_TRUE(table.addResource(ResourceName{ {}, ResourceType::kId, u"test" }, {}, + Source{ "lib.xml", 33 }, util::make_unique<Id>())); + ASSERT_TRUE(mTable->merge(std::move(table))); + + Linker linker(mTable, + std::make_shared<MockResolver>(mTable, std::map<ResourceName, ResourceId>()), + {}); + ASSERT_TRUE(linker.linkAndValidate()); + + JavaClassGenerator generator(mTable, {}); + + std::stringstream out; + EXPECT_TRUE(generator.generate(mTable->getPackage(), out)); + std::string output = out.str(); + EXPECT_NE(std::string::npos, output.find("int foo =")); + EXPECT_EQ(std::string::npos, output.find("int test =")); + + out.str(""); + EXPECT_TRUE(generator.generate(u"com.lib", out)); + output = out.str(); + EXPECT_NE(std::string::npos, output.find("int test =")); + EXPECT_EQ(std::string::npos, output.find("int foo =")); +}*/ + +TEST(JavaClassGeneratorTest, EmitOtherPackagesAttributesInStyleable) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"android", 0x01) + .setPackageId(u"com.lib", 0x02) + .addSimple(u"@android:attr/bar", ResourceId(0x01010000)) + .addSimple(u"@com.lib:attr/bar", ResourceId(0x02010000)) + .addValue(u"@android:styleable/foo", ResourceId(0x01030000), + test::StyleableBuilder() + .addItem(u"@android:attr/bar", ResourceId(0x01010000)) + .addItem(u"@com.lib:attr/bar", ResourceId(0x02010000)) + .build()) + .build(); + + JavaClassGenerator generator(table.get(), {}); + + std::stringstream out; + EXPECT_TRUE(generator.generate(u"android", &out)); + + std::string output = out.str(); + EXPECT_NE(std::string::npos, output.find("int foo_bar=")); + EXPECT_NE(std::string::npos, output.find("int foo_com_lib_bar=")); +} + +TEST(JavaClassGeneratorTest, CommentsForSimpleResourcesArePresent) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"android", 0x01) + .addSimple(u"@android:id/foo", ResourceId(0x01010000)) + .build(); + test::getValue<Id>(table.get(), u"@android:id/foo") + ->setComment(std::u16string(u"This is a comment\n@deprecated")); + + JavaClassGenerator generator(table.get(), {}); + + std::stringstream out; + ASSERT_TRUE(generator.generate(u"android", &out)); + std::string actual = out.str(); + + EXPECT_NE(std::string::npos, actual.find( + R"EOF(/** + * This is a comment + * @deprecated + */ + @Deprecated + public static final int foo=0x01010000;)EOF")); +} + +TEST(JavaClassGeneratorTest, CommentsForEnumAndFlagAttributesArePresent) { + +} + +TEST(JavaClassGeneratorTest, CommentsForStyleablesAndNestedAttributesArePresent) { + +} + +} // namespace aapt diff --git a/tools/aapt2/java/ManifestClassGenerator.cpp b/tools/aapt2/java/ManifestClassGenerator.cpp new file mode 100644 index 000000000000..a9b4c14337fe --- /dev/null +++ b/tools/aapt2/java/ManifestClassGenerator.cpp @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Source.h" +#include "java/AnnotationProcessor.h" +#include "java/ClassDefinitionWriter.h" +#include "java/ManifestClassGenerator.h" +#include "util/Maybe.h" +#include "xml/XmlDom.h" + +#include <algorithm> + +namespace aapt { + +static Maybe<StringPiece16> extractJavaIdentifier(IDiagnostics* diag, const Source& source, + const StringPiece16& value) { + const StringPiece16 sep = u"."; + auto iter = std::find_end(value.begin(), value.end(), sep.begin(), sep.end()); + + StringPiece16 result; + if (iter != value.end()) { + result.assign(iter + sep.size(), value.end() - (iter + sep.size())); + } else { + result = value; + } + + if (result.empty()) { + diag->error(DiagMessage(source) << "empty symbol"); + return {}; + } + + iter = util::findNonAlphaNumericAndNotInSet(result, u"_"); + if (iter != result.end()) { + diag->error(DiagMessage(source) + << "invalid character '" << StringPiece16(iter, 1) + << "' in '" << result << "'"); + return {}; + } + + if (*result.begin() >= u'0' && *result.begin() <= u'9') { + diag->error(DiagMessage(source) << "symbol can not start with a digit"); + return {}; + } + + return result; +} + +static bool writeSymbol(IDiagnostics* diag, ClassDefinitionWriter* outClassDef, const Source& source, + xml::Element* el) { + xml::Attribute* attr = el->findAttribute(xml::kSchemaAndroid, u"name"); + if (!attr) { + diag->error(DiagMessage(source) << "<" << el->name << "> must define 'android:name'"); + return false; + } + + Maybe<StringPiece16> result = extractJavaIdentifier(diag, source.withLine(el->lineNumber), + attr->value); + if (!result) { + return false; + } + + AnnotationProcessor processor; + processor.appendComment(el->comment); + outClassDef->addStringMember(result.value(), &processor, attr->value); + return true; +} + +bool ManifestClassGenerator::generate(IDiagnostics* diag, const StringPiece16& package, + xml::XmlResource* res, std::ostream* out) { + xml::Element* el = xml::findRootElement(res->root.get()); + if (!el) { + return false; + } + + if (el->name != u"manifest" && !el->namespaceUri.empty()) { + diag->error(DiagMessage(res->file.source) << "no <manifest> root tag defined"); + return false; + } + + *out << "package " << package << ";\n\n" + << "public final class Manifest {\n"; + + bool error = false; + std::vector<xml::Element*> children = el->getChildElements(); + + ClassDefinitionWriterOptions classOptions; + classOptions.useFinalQualifier = true; + classOptions.forceCreationIfEmpty = false; + + // First write out permissions. + ClassDefinitionWriter classDef("permission", classOptions); + for (xml::Element* childEl : children) { + if (childEl->namespaceUri.empty() && childEl->name == u"permission") { + error |= !writeSymbol(diag, &classDef, res->file.source, childEl); + } + } + classDef.writeToStream(out, " "); + + // Next write out permission groups. + classDef = ClassDefinitionWriter("permission_group", classOptions); + for (xml::Element* childEl : children) { + if (childEl->namespaceUri.empty() && childEl->name == u"permission-group") { + error |= !writeSymbol(diag, &classDef, res->file.source, childEl); + } + } + classDef.writeToStream(out, " "); + + *out << "}\n"; + return !error; +} + +} // namespace aapt diff --git a/tools/aapt2/java/ManifestClassGenerator.h b/tools/aapt2/java/ManifestClassGenerator.h new file mode 100644 index 000000000000..226ed23b85f8 --- /dev/null +++ b/tools/aapt2/java/ManifestClassGenerator.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_JAVA_MANIFESTCLASSGENERATOR_H +#define AAPT_JAVA_MANIFESTCLASSGENERATOR_H + +#include "Diagnostics.h" +#include "util/StringPiece.h" +#include "xml/XmlDom.h" + +#include <iostream> + +namespace aapt { + +struct ManifestClassGenerator { + bool generate(IDiagnostics* diag, const StringPiece16& package, xml::XmlResource* res, + std::ostream* out); +}; + +} // namespace aapt + +#endif /* AAPT_JAVA_MANIFESTCLASSGENERATOR_H */ diff --git a/tools/aapt2/java/ManifestClassGenerator_test.cpp b/tools/aapt2/java/ManifestClassGenerator_test.cpp new file mode 100644 index 000000000000..fc57ae6fd8ff --- /dev/null +++ b/tools/aapt2/java/ManifestClassGenerator_test.cpp @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "java/ManifestClassGenerator.h" +#include "test/Builders.h" +#include "test/Context.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(ManifestClassGeneratorTest, NameIsProperlyGeneratedFromSymbol) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + std::unique_ptr<xml::XmlResource> manifest = test::buildXmlDom(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <permission android:name="android.permission.ACCESS_INTERNET" /> + <permission android:name="android.DO_DANGEROUS_THINGS" /> + <permission android:name="com.test.sample.permission.HUH" /> + <permission-group android:name="foo.bar.PERMISSION" /> + </manifest>)EOF"); + + std::stringstream out; + ManifestClassGenerator generator; + ASSERT_TRUE(generator.generate(context->getDiagnostics(), u"android", manifest.get(), &out)); + + std::string actual = out.str(); + + const size_t permissionClassPos = actual.find("public static final class permission {"); + const size_t permissionGroupClassPos = + actual.find("public static final class permission_group {"); + ASSERT_NE(std::string::npos, permissionClassPos); + ASSERT_NE(std::string::npos, permissionGroupClassPos); + + // + // Make sure these permissions are in the permission class. + // + + size_t pos = actual.find("public static final String ACCESS_INTERNET=" + "\"android.permission.ACCESS_INTERNET\";"); + EXPECT_GT(pos, permissionClassPos); + EXPECT_LT(pos, permissionGroupClassPos); + + pos = actual.find("public static final String DO_DANGEROUS_THINGS=" + "\"android.DO_DANGEROUS_THINGS\";"); + EXPECT_GT(pos, permissionClassPos); + EXPECT_LT(pos, permissionGroupClassPos); + + pos = actual.find("public static final String HUH=\"com.test.sample.permission.HUH\";"); + EXPECT_GT(pos, permissionClassPos); + EXPECT_LT(pos, permissionGroupClassPos); + + // + // Make sure these permissions are in the permission_group class + // + + pos = actual.find("public static final String PERMISSION=" + "\"foo.bar.PERMISSION\";"); + EXPECT_GT(pos, permissionGroupClassPos); + EXPECT_LT(pos, std::string::npos); +} + +TEST(ManifestClassGeneratorTest, CommentsAndAnnotationsArePresent) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + std::unique_ptr<xml::XmlResource> manifest = test::buildXmlDom(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- Required to access the internet. + Added in API 1. --> + <permission android:name="android.permission.ACCESS_INTERNET" /> + <!-- @deprecated This permission is for playing outside. --> + <permission android:name="android.permission.PLAY_OUTSIDE" /> + <!-- This is a private permission for system only! + @hide + @SystemApi --> + <permission android:name="android.permission.SECRET" /> + </manifest>)EOF"); + + std::stringstream out; + ManifestClassGenerator generator; + ASSERT_TRUE(generator.generate(context->getDiagnostics(), u"android", manifest.get(), &out)); + + std::string actual = out.str(); + + EXPECT_NE(std::string::npos, actual.find( +R"EOF( /** + * Required to access the internet. + * Added in API 1. + */ + public static final String ACCESS_INTERNET="android.permission.ACCESS_INTERNET";)EOF")); + + EXPECT_NE(std::string::npos, actual.find( +R"EOF( /** + * @deprecated This permission is for playing outside. + */ + @Deprecated + public static final String PLAY_OUTSIDE="android.permission.PLAY_OUTSIDE";)EOF")); + + EXPECT_NE(std::string::npos, actual.find( +R"EOF( /** + * This is a private permission for system only! + * @hide + * @SystemApi + */ + @android.annotation.SystemApi + public static final String SECRET="android.permission.SECRET";)EOF")); +} + +} // namespace aapt diff --git a/tools/aapt2/ProguardRules.cpp b/tools/aapt2/java/ProguardRules.cpp index e89fb7c8bd3d..c610bb0f2ff2 100644 --- a/tools/aapt2/ProguardRules.cpp +++ b/tools/aapt2/java/ProguardRules.cpp @@ -14,9 +14,9 @@ * limitations under the License. */ -#include "ProguardRules.h" -#include "Util.h" -#include "XmlDom.h" +#include "java/ProguardRules.h" +#include "util/Util.h" +#include "xml/XmlDom.h" #include <memory> #include <string> @@ -24,8 +24,6 @@ namespace aapt { namespace proguard { -constexpr const char16_t* kSchemaAndroid = u"http://schemas.android.com/apk/res/android"; - class BaseVisitor : public xml::Visitor { public: BaseVisitor(const Source& source, KeepSet* keepSet) : mSource(source), mKeepSet(keepSet) { @@ -41,11 +39,11 @@ public: virtual void visit(xml::Element* node) override { if (!node->namespaceUri.empty()) { - Maybe<std::u16string> maybePackage = util::extractPackageFromNamespace( + Maybe<xml::ExtractedPackage> maybePackage = xml::extractPackageFromNamespace( node->namespaceUri); if (maybePackage) { // This is a custom view, let's figure out the class name from this. - std::u16string package = maybePackage.value() + u"." + node->name; + std::u16string package = maybePackage.value().package + u"." + node->name; if (util::isJavaClassName(package)) { addClass(node->lineNumber, package); } @@ -61,11 +59,11 @@ public: protected: void addClass(size_t lineNumber, const std::u16string& className) { - mKeepSet->addClass(mSource.line(lineNumber), className); + mKeepSet->addClass(Source(mSource.path, lineNumber), className); } void addMethod(size_t lineNumber, const std::u16string& methodName) { - mKeepSet->addMethod(mSource.line(lineNumber), methodName); + mKeepSet->addMethod(Source(mSource.path, lineNumber), methodName); } private: @@ -82,7 +80,7 @@ struct LayoutVisitor : public BaseVisitor { bool checkName = false; if (node->namespaceUri.empty()) { checkClass = node->name == u"view" || node->name == u"fragment"; - } else if (node->namespaceUri == kSchemaAndroid) { + } else if (node->namespaceUri == xml::kSchemaAndroid) { checkName = node->name == u"fragment"; } @@ -90,10 +88,10 @@ struct LayoutVisitor : public BaseVisitor { if (checkClass && attr.namespaceUri.empty() && attr.name == u"class" && util::isJavaClassName(attr.value)) { addClass(node->lineNumber, attr.value); - } else if (checkName && attr.namespaceUri == kSchemaAndroid && attr.name == u"name" && - util::isJavaClassName(attr.value)) { + } else if (checkName && attr.namespaceUri == xml::kSchemaAndroid && + attr.name == u"name" && util::isJavaClassName(attr.value)) { addClass(node->lineNumber, attr.value); - } else if (attr.namespaceUri == kSchemaAndroid && attr.name == u"onClick") { + } else if (attr.namespaceUri == xml::kSchemaAndroid && attr.name == u"onClick") { addMethod(node->lineNumber, attr.value); } } @@ -113,7 +111,7 @@ struct XmlResourceVisitor : public BaseVisitor { } if (checkFragment) { - xml::Attribute* attr = node->findAttribute(kSchemaAndroid, u"fragment"); + xml::Attribute* attr = node->findAttribute(xml::kSchemaAndroid, u"fragment"); if (attr && util::isJavaClassName(attr->value)) { addClass(node->lineNumber, attr->value); } @@ -155,7 +153,7 @@ struct ManifestVisitor : public BaseVisitor { } } else if (node->name == u"application") { getName = true; - xml::Attribute* attr = node->findAttribute(kSchemaAndroid, u"backupAgent"); + xml::Attribute* attr = node->findAttribute(xml::kSchemaAndroid, u"backupAgent"); if (attr) { Maybe<std::u16string> result = util::getFullyQualifiedClassName(mPackage, attr->value); @@ -170,7 +168,7 @@ struct ManifestVisitor : public BaseVisitor { } if (getName) { - xml::Attribute* attr = node->findAttribute(kSchemaAndroid, u"name"); + xml::Attribute* attr = node->findAttribute(xml::kSchemaAndroid, u"name"); if (attr) { Maybe<std::u16string> result = util::getFullyQualifiedClassName(mPackage, attr->value); @@ -186,30 +184,37 @@ struct ManifestVisitor : public BaseVisitor { std::u16string mPackage; }; -bool collectProguardRulesForManifest(const Source& source, xml::Node* node, KeepSet* keepSet) { +bool collectProguardRulesForManifest(const Source& source, xml::XmlResource* res, + KeepSet* keepSet) { ManifestVisitor visitor(source, keepSet); - node->accept(&visitor); - return true; + if (res->root) { + res->root->accept(&visitor); + return true; + } + return false; } -bool collectProguardRules(ResourceType type, const Source& source, xml::Node* node, - KeepSet* keepSet) { - switch (type) { +bool collectProguardRules(const Source& source, xml::XmlResource* res, KeepSet* keepSet) { + if (!res->root) { + return false; + } + + switch (res->file.name.type) { case ResourceType::kLayout: { LayoutVisitor visitor(source, keepSet); - node->accept(&visitor); + res->root->accept(&visitor); break; } case ResourceType::kXml: { XmlResourceVisitor visitor(source, keepSet); - node->accept(&visitor); + res->root->accept(&visitor); break; } case ResourceType::kTransition: { TransitionVisitor visitor(source, keepSet); - node->accept(&visitor); + res->root->accept(&visitor); break; } @@ -221,15 +226,15 @@ bool collectProguardRules(ResourceType type, const Source& source, xml::Node* no bool writeKeepSet(std::ostream* out, const KeepSet& keepSet) { for (const auto& entry : keepSet.mKeepSet) { - for (const SourceLine& source : entry.second) { - *out << "// Referenced at " << source << "\n"; + for (const Source& source : entry.second) { + *out << "# Referenced at " << source << "\n"; } *out << "-keep class " << entry.first << " { <init>(...); }\n" << std::endl; } for (const auto& entry : keepSet.mKeepMethodSet) { - for (const SourceLine& source : entry.second) { - *out << "// Referenced at " << source << "\n"; + for (const Source& source : entry.second) { + *out << "# Referenced at " << source << "\n"; } *out << "-keepclassmembers class * { *** " << entry.first << "(...); }\n" << std::endl; } diff --git a/tools/aapt2/ProguardRules.h b/tools/aapt2/java/ProguardRules.h index bbb3e64e6758..aafffd39d84e 100644 --- a/tools/aapt2/ProguardRules.h +++ b/tools/aapt2/java/ProguardRules.h @@ -19,7 +19,7 @@ #include "Resource.h" #include "Source.h" -#include "XmlDom.h" +#include "xml/XmlDom.h" #include <map> #include <ostream> @@ -31,24 +31,23 @@ namespace proguard { class KeepSet { public: - inline void addClass(const SourceLine& source, const std::u16string& className) { + inline void addClass(const Source& source, const std::u16string& className) { mKeepSet[className].insert(source); } - inline void addMethod(const SourceLine& source, const std::u16string& methodName) { + inline void addMethod(const Source& source, const std::u16string& methodName) { mKeepMethodSet[methodName].insert(source); } private: friend bool writeKeepSet(std::ostream* out, const KeepSet& keepSet); - std::map<std::u16string, std::set<SourceLine>> mKeepSet; - std::map<std::u16string, std::set<SourceLine>> mKeepMethodSet; + std::map<std::u16string, std::set<Source>> mKeepSet; + std::map<std::u16string, std::set<Source>> mKeepMethodSet; }; -bool collectProguardRulesForManifest(const Source& source, xml::Node* node, KeepSet* keepSet); -bool collectProguardRules(ResourceType type, const Source& source, xml::Node* node, - KeepSet* keepSet); +bool collectProguardRulesForManifest(const Source& source, xml::XmlResource* res, KeepSet* keepSet); +bool collectProguardRules(const Source& source, xml::XmlResource* res, KeepSet* keepSet); bool writeKeepSet(std::ostream* out, const KeepSet& keepSet); diff --git a/tools/aapt2/link/AutoVersioner.cpp b/tools/aapt2/link/AutoVersioner.cpp new file mode 100644 index 000000000000..c7e603ea3774 --- /dev/null +++ b/tools/aapt2/link/AutoVersioner.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ConfigDescription.h" +#include "ResourceTable.h" +#include "SdkConstants.h" +#include "ValueVisitor.h" + +#include "link/Linkers.h" +#include "util/Comparators.h" + +#include <algorithm> +#include <cassert> + +namespace aapt { + +bool shouldGenerateVersionedResource(const ResourceEntry* entry, const ConfigDescription& config, + const int sdkVersionToGenerate) { + assert(sdkVersionToGenerate > config.sdkVersion); + const auto endIter = entry->values.end(); + auto iter = std::lower_bound(entry->values.begin(), endIter, config, cmp::lessThanConfig); + + // The source config came from this list, so it should be here. + assert(iter != entry->values.end()); + ++iter; + + // The next configuration either only varies in sdkVersion, or it is completely different + // and therefore incompatible. If it is incompatible, we must generate the versioned resource. + + // NOTE: The ordering of configurations takes sdkVersion as higher precedence than other + // qualifiers, so we need to iterate through the entire list to be sure there + // are no higher sdk level versions of this resource. + ConfigDescription tempConfig(config); + for (; iter != endIter; ++iter) { + tempConfig.sdkVersion = iter->config.sdkVersion; + if (tempConfig == iter->config) { + // The two configs are the same, check the sdk version. + return sdkVersionToGenerate < iter->config.sdkVersion; + } + } + + // No match was found, so we should generate the versioned resource. + return true; +} + +bool AutoVersioner::consume(IAaptContext* context, ResourceTable* table) { + for (auto& package : table->packages) { + for (auto& type : package->types) { + if (type->type != ResourceType::kStyle) { + continue; + } + + for (auto& entry : type->entries) { + for (size_t i = 0; i < entry->values.size(); i++) { + ResourceConfigValue& configValue = entry->values[i]; + if (configValue.config.sdkVersion >= SDK_LOLLIPOP_MR1) { + // If this configuration is only used on L-MR1 then we don't need + // to do anything since we use private attributes since that version. + continue; + } + + if (Style* style = valueCast<Style>(configValue.value.get())) { + Maybe<size_t> minSdkStripped; + std::vector<Style::Entry> stripped; + + auto iter = style->entries.begin(); + while (iter != style->entries.end()) { + assert(iter->key.id && "IDs must be assigned and linked"); + + // Find the SDK level that is higher than the configuration allows. + const size_t sdkLevel = findAttributeSdkLevel(iter->key.id.value()); + if (sdkLevel > std::max<size_t>(configValue.config.sdkVersion, 1)) { + // Record that we are about to strip this. + stripped.emplace_back(std::move(*iter)); + + // We use the smallest SDK level to generate the new style. + if (minSdkStripped) { + minSdkStripped = std::min(minSdkStripped.value(), sdkLevel); + } else { + minSdkStripped = sdkLevel; + } + + // Erase this from this style. + iter = style->entries.erase(iter); + continue; + } + ++iter; + } + + if (minSdkStripped && !stripped.empty()) { + // We found attributes from a higher SDK level. Check that + // there is no other defined resource for the version we want to + // generate. + if (shouldGenerateVersionedResource(entry.get(), + configValue.config, + minSdkStripped.value())) { + // Let's create a new Style for this versioned resource. + ConfigDescription newConfig(configValue.config); + newConfig.sdkVersion = minSdkStripped.value(); + + std::unique_ptr<Style> newStyle(style->clone(&table->stringPool)); + newStyle->setComment(style->getComment()); + newStyle->setSource(style->getSource()); + + // Move the previously stripped attributes into this style. + newStyle->entries.insert(newStyle->entries.end(), + std::make_move_iterator(stripped.begin()), + std::make_move_iterator(stripped.end())); + + // Insert the new Resource into the correct place. + auto iter = std::lower_bound(entry->values.begin(), + entry->values.end(), + newConfig, + cmp::lessThanConfig); + + entry->values.insert( + iter, + ResourceConfigValue{ newConfig, std::move(newStyle) }); + } + } + } + } + } + } + } + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/link/AutoVersioner_test.cpp b/tools/aapt2/link/AutoVersioner_test.cpp new file mode 100644 index 000000000000..29bcc93518cf --- /dev/null +++ b/tools/aapt2/link/AutoVersioner_test.cpp @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ConfigDescription.h" + +#include "link/Linkers.h" + +#include "test/Builders.h" +#include "test/Context.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(AutoVersionerTest, GenerateVersionedResources) { + const ConfigDescription defaultConfig = {}; + const ConfigDescription landConfig = test::parseConfigOrDie("land"); + const ConfigDescription sw600dpLandConfig = test::parseConfigOrDie("sw600dp-land"); + + ResourceEntry entry(u"foo"); + entry.values.push_back(ResourceConfigValue{ defaultConfig }); + entry.values.push_back(ResourceConfigValue{ landConfig }); + entry.values.push_back(ResourceConfigValue{ sw600dpLandConfig }); + + EXPECT_TRUE(shouldGenerateVersionedResource(&entry, defaultConfig, 17)); + EXPECT_TRUE(shouldGenerateVersionedResource(&entry, landConfig, 17)); +} + +TEST(AutoVersionerTest, GenerateVersionedResourceWhenHigherVersionExists) { + const ConfigDescription defaultConfig = {}; + const ConfigDescription sw600dpV13Config = test::parseConfigOrDie("sw600dp-v13"); + const ConfigDescription v21Config = test::parseConfigOrDie("v21"); + + ResourceEntry entry(u"foo"); + entry.values.push_back(ResourceConfigValue{ defaultConfig }); + entry.values.push_back(ResourceConfigValue{ sw600dpV13Config }); + entry.values.push_back(ResourceConfigValue{ v21Config }); + + EXPECT_TRUE(shouldGenerateVersionedResource(&entry, defaultConfig, 17)); + EXPECT_FALSE(shouldGenerateVersionedResource(&entry, defaultConfig, 22)); +} + +TEST(AutoVersionerTest, VersionStylesForTable) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"app", 0x7f) + .addValue(u"@app:style/Foo", ResourceId(0x7f020000), test::parseConfigOrDie("v4"), + test::StyleBuilder() + .addItem(u"@android:attr/onClick", ResourceId(0x0101026f), + util::make_unique<Id>()) + .addItem(u"@android:attr/paddingStart", ResourceId(0x010103b3), + util::make_unique<Id>()) + .addItem(u"@android:attr/requiresSmallestWidthDp", + ResourceId(0x01010364), util::make_unique<Id>()) + .addItem(u"@android:attr/colorAccent", ResourceId(0x01010435), + util::make_unique<Id>()) + .build()) + .addValue(u"@app:style/Foo", ResourceId(0x7f020000), test::parseConfigOrDie("v21"), + test::StyleBuilder() + .addItem(u"@android:attr/paddingEnd", ResourceId(0x010103b4), + util::make_unique<Id>()) + .build()) + .build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .setCompilationPackage(u"app") + .setPackageId(0x7f) + .build(); + + AutoVersioner versioner; + ASSERT_TRUE(versioner.consume(context.get(), table.get())); + + Style* style = test::getValueForConfig<Style>(table.get(), u"@app:style/Foo", + test::parseConfigOrDie("v4")); + ASSERT_NE(style, nullptr); + ASSERT_EQ(style->entries.size(), 1u); + AAPT_ASSERT_TRUE(style->entries.front().key.name); + EXPECT_EQ(style->entries.front().key.name.value(), + test::parseNameOrDie(u"@android:attr/onClick")); + + style = test::getValueForConfig<Style>(table.get(), u"@app:style/Foo", + test::parseConfigOrDie("v13")); + ASSERT_NE(style, nullptr); + ASSERT_EQ(style->entries.size(), 2u); + AAPT_ASSERT_TRUE(style->entries[0].key.name); + EXPECT_EQ(style->entries[0].key.name.value(), + test::parseNameOrDie(u"@android:attr/onClick")); + AAPT_ASSERT_TRUE(style->entries[1].key.name); + EXPECT_EQ(style->entries[1].key.name.value(), + test::parseNameOrDie(u"@android:attr/requiresSmallestWidthDp")); + + style = test::getValueForConfig<Style>(table.get(), u"@app:style/Foo", + test::parseConfigOrDie("v17")); + ASSERT_NE(style, nullptr); + ASSERT_EQ(style->entries.size(), 3u); + AAPT_ASSERT_TRUE(style->entries[0].key.name); + EXPECT_EQ(style->entries[0].key.name.value(), + test::parseNameOrDie(u"@android:attr/onClick")); + AAPT_ASSERT_TRUE(style->entries[1].key.name); + EXPECT_EQ(style->entries[1].key.name.value(), + test::parseNameOrDie(u"@android:attr/requiresSmallestWidthDp")); + AAPT_ASSERT_TRUE(style->entries[2].key.name); + EXPECT_EQ(style->entries[2].key.name.value(), + test::parseNameOrDie(u"@android:attr/paddingStart")); + + style = test::getValueForConfig<Style>(table.get(), u"@app:style/Foo", + test::parseConfigOrDie("v21")); + ASSERT_NE(style, nullptr); + ASSERT_EQ(style->entries.size(), 1u); + AAPT_ASSERT_TRUE(style->entries.front().key.name); + EXPECT_EQ(style->entries.front().key.name.value(), + test::parseNameOrDie(u"@android:attr/paddingEnd")); +} + +} // namespace aapt diff --git a/tools/aapt2/link/Link.cpp b/tools/aapt2/link/Link.cpp new file mode 100644 index 000000000000..fd76e887ab2a --- /dev/null +++ b/tools/aapt2/link/Link.cpp @@ -0,0 +1,1050 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AppInfo.h" +#include "Debug.h" +#include "Flags.h" +#include "Locale.h" +#include "NameMangler.h" +#include "compile/IdAssigner.h" +#include "filter/ConfigFilter.h" +#include "flatten/Archive.h" +#include "flatten/TableFlattener.h" +#include "flatten/XmlFlattener.h" +#include "io/FileSystem.h" +#include "io/ZipArchive.h" +#include "java/JavaClassGenerator.h" +#include "java/ManifestClassGenerator.h" +#include "java/ProguardRules.h" +#include "link/Linkers.h" +#include "link/ReferenceLinker.h" +#include "link/ManifestFixer.h" +#include "link/TableMerger.h" +#include "process/IResourceTableConsumer.h" +#include "process/SymbolTable.h" +#include "unflatten/BinaryResourceParser.h" +#include "unflatten/FileExportHeaderReader.h" +#include "util/Files.h" +#include "util/StringPiece.h" +#include "xml/XmlDom.h" + +#include <fstream> +#include <sys/stat.h> +#include <vector> + +namespace aapt { + +struct LinkOptions { + std::string outputPath; + std::string manifestPath; + std::vector<std::string> includePaths; + std::vector<std::string> overlayFiles; + Maybe<std::string> generateJavaClassPath; + Maybe<std::u16string> customJavaPackage; + std::set<std::u16string> extraJavaPackages; + Maybe<std::string> generateProguardRulesPath; + bool noAutoVersion = false; + bool staticLib = false; + bool generateNonFinalIds = false; + bool verbose = false; + bool outputToDirectory = false; + bool autoAddOverlay = false; + bool doNotCompressAnything = false; + std::vector<std::string> extensionsToNotCompress; + Maybe<std::u16string> privateSymbols; + ManifestFixerOptions manifestFixerOptions; + IConfigFilter* configFilter = nullptr; +}; + +struct LinkContext : public IAaptContext { + StdErrDiagnostics mDiagnostics; + std::unique_ptr<NameMangler> mNameMangler; + std::u16string mCompilationPackage; + uint8_t mPackageId; + std::unique_ptr<ISymbolTable> mSymbols; + + IDiagnostics* getDiagnostics() override { + return &mDiagnostics; + } + + NameMangler* getNameMangler() override { + return mNameMangler.get(); + } + + StringPiece16 getCompilationPackage() override { + return mCompilationPackage; + } + + uint8_t getPackageId() override { + return mPackageId; + } + + ISymbolTable* getExternalSymbols() override { + return mSymbols.get(); + } +}; + +class LinkCommand { +public: + LinkCommand(LinkContext* context, const LinkOptions& options) : + mOptions(options), mContext(context), mFinalTable(), mFileCollection(nullptr) { + std::unique_ptr<io::FileCollection> fileCollection = + util::make_unique<io::FileCollection>(); + + // Get a pointer to the FileCollection for convenience, but it will be owned by the vector. + mFileCollection = fileCollection.get(); + + // Move it to the collection. + mCollections.push_back(std::move(fileCollection)); + } + + /** + * Creates a SymbolTable that loads symbols from the various APKs and caches the + * results for faster lookup. + */ + std::unique_ptr<ISymbolTable> createSymbolTableFromIncludePaths() { + AssetManagerSymbolTableBuilder builder; + for (const std::string& path : mOptions.includePaths) { + if (mOptions.verbose) { + mContext->getDiagnostics()->note(DiagMessage(path) << "loading include path"); + } + + std::unique_ptr<android::AssetManager> assetManager = + util::make_unique<android::AssetManager>(); + int32_t cookie = 0; + if (!assetManager->addAssetPath(android::String8(path.data(), path.size()), &cookie)) { + mContext->getDiagnostics()->error( + DiagMessage(path) << "failed to load include path"); + return {}; + } + builder.add(std::move(assetManager)); + } + return builder.build(); + } + + std::unique_ptr<ResourceTable> loadTable(const Source& source, const void* data, size_t len) { + std::unique_ptr<ResourceTable> table = util::make_unique<ResourceTable>(); + BinaryResourceParser parser(mContext, table.get(), source, data, len); + if (!parser.parse()) { + return {}; + } + return table; + } + + /** + * Inflates an XML file from the source path. + */ + static std::unique_ptr<xml::XmlResource> loadXml(const std::string& path, IDiagnostics* diag) { + std::ifstream fin(path, std::ifstream::binary); + if (!fin) { + diag->error(DiagMessage(path) << strerror(errno)); + return {}; + } + + return xml::inflate(&fin, diag, Source(path)); + } + + static std::unique_ptr<xml::XmlResource> loadBinaryXmlSkipFileExport( + const Source& source, + const void* data, size_t len, + IDiagnostics* diag) { + std::string errorStr; + ssize_t offset = getWrappedDataOffset(data, len, &errorStr); + if (offset < 0) { + diag->error(DiagMessage(source) << errorStr); + return {}; + } + + std::unique_ptr<xml::XmlResource> xmlRes = xml::inflate( + reinterpret_cast<const uint8_t*>(data) + static_cast<size_t>(offset), + len - static_cast<size_t>(offset), + diag, + source); + if (!xmlRes) { + return {}; + } + return xmlRes; + } + + static std::unique_ptr<ResourceFile> loadFileExportHeader(const Source& source, + const void* data, size_t len, + IDiagnostics* diag) { + std::unique_ptr<ResourceFile> resFile = util::make_unique<ResourceFile>(); + std::string errorStr; + ssize_t offset = unwrapFileExportHeader(data, len, resFile.get(), &errorStr); + if (offset < 0) { + diag->error(DiagMessage(source) << errorStr); + return {}; + } + return resFile; + } + + uint32_t getCompressionFlags(const StringPiece& str) { + if (mOptions.doNotCompressAnything) { + return 0; + } + + for (const std::string& extension : mOptions.extensionsToNotCompress) { + if (util::stringEndsWith<char>(str, extension)) { + return 0; + } + } + return ArchiveEntry::kCompress; + } + + bool copyFileToArchive(io::IFile* file, const std::string& outPath, + IArchiveWriter* writer) { + std::unique_ptr<io::IData> data = file->openAsData(); + if (!data) { + mContext->getDiagnostics()->error(DiagMessage(file->getSource()) + << "failed to open file"); + return false; + } + + std::string errorStr; + ssize_t offset = getWrappedDataOffset(data->data(), data->size(), &errorStr); + if (offset < 0) { + mContext->getDiagnostics()->error(DiagMessage(file->getSource()) << errorStr); + return false; + } + + if (writer->startEntry(outPath, getCompressionFlags(outPath))) { + if (writer->writeEntry(reinterpret_cast<const uint8_t*>(data->data()) + offset, + data->size() - static_cast<size_t>(offset))) { + if (writer->finishEntry()) { + return true; + } + } + } + + mContext->getDiagnostics()->error( + DiagMessage(mOptions.outputPath) << "failed to write file " << outPath); + return false; + } + + Maybe<AppInfo> extractAppInfoFromManifest(xml::XmlResource* xmlRes) { + // Make sure the first element is <manifest> with package attribute. + if (xml::Element* manifestEl = xml::findRootElement(xmlRes->root.get())) { + if (manifestEl->namespaceUri.empty() && manifestEl->name == u"manifest") { + if (xml::Attribute* packageAttr = manifestEl->findAttribute({}, u"package")) { + return AppInfo{ packageAttr->value }; + } + } + } + return {}; + } + + /** + * Precondition: ResourceTable doesn't have any IDs assigned yet, nor is it linked. + * Postcondition: ResourceTable has only one package left. All others are stripped, or there + * is an error and false is returned. + */ + bool verifyNoExternalPackages() { + auto isExtPackageFunc = [&](const std::unique_ptr<ResourceTablePackage>& pkg) -> bool { + return mContext->getCompilationPackage() != pkg->name || + !pkg->id || + pkg->id.value() != mContext->getPackageId(); + }; + + bool error = false; + for (const auto& package : mFinalTable.packages) { + if (isExtPackageFunc(package)) { + // We have a package that is not related to the one we're building! + for (const auto& type : package->types) { + for (const auto& entry : type->entries) { + ResourceNameRef resName(package->name, type->type, entry->name); + + for (const auto& configValue : entry->values) { + // Special case the occurrence of an ID that is being generated for the + // 'android' package. This is due to legacy reasons. + if (valueCast<Id>(configValue.value.get()) && + package->name == u"android") { + mContext->getDiagnostics()->warn( + DiagMessage(configValue.value->getSource()) + << "generated id '" << resName + << "' for external package '" << package->name + << "'"); + } else { + mContext->getDiagnostics()->error( + DiagMessage(configValue.value->getSource()) + << "defined resource '" << resName + << "' for external package '" << package->name + << "'"); + error = true; + } + } + } + } + } + } + + auto newEndIter = std::remove_if(mFinalTable.packages.begin(), mFinalTable.packages.end(), + isExtPackageFunc); + mFinalTable.packages.erase(newEndIter, mFinalTable.packages.end()); + return !error; + } + + std::unique_ptr<IArchiveWriter> makeArchiveWriter() { + if (mOptions.outputToDirectory) { + return createDirectoryArchiveWriter(mContext->getDiagnostics(), mOptions.outputPath); + } else { + return createZipFileArchiveWriter(mContext->getDiagnostics(), mOptions.outputPath); + } + } + + bool flattenTable(ResourceTable* table, IArchiveWriter* writer) { + BigBuffer buffer(1024); + TableFlattenerOptions options = {}; + options.useExtendedChunks = mOptions.staticLib; + TableFlattener flattener(&buffer, options); + if (!flattener.consume(mContext, table)) { + return false; + } + + if (writer->startEntry("resources.arsc", ArchiveEntry::kAlign)) { + if (writer->writeEntry(buffer)) { + if (writer->finishEntry()) { + return true; + } + } + } + + mContext->getDiagnostics()->error( + DiagMessage() << "failed to write resources.arsc to archive"); + return false; + } + + bool flattenXml(xml::XmlResource* xmlRes, const StringPiece& path, Maybe<size_t> maxSdkLevel, + IArchiveWriter* writer) { + BigBuffer buffer(1024); + XmlFlattenerOptions options = {}; + options.keepRawValues = mOptions.staticLib; + options.maxSdkLevel = maxSdkLevel; + XmlFlattener flattener(&buffer, options); + if (!flattener.consume(mContext, xmlRes)) { + return false; + } + + if (writer->startEntry(path, ArchiveEntry::kCompress)) { + if (writer->writeEntry(buffer)) { + if (writer->finishEntry()) { + return true; + } + } + } + mContext->getDiagnostics()->error( + DiagMessage() << "failed to write " << path << " to archive"); + return false; + } + + bool writeJavaFile(ResourceTable* table, const StringPiece16& packageNameToGenerate, + const StringPiece16& outPackage, JavaClassGeneratorOptions javaOptions) { + if (!mOptions.generateJavaClassPath) { + return true; + } + + std::string outPath = mOptions.generateJavaClassPath.value(); + file::appendPath(&outPath, file::packageToPath(util::utf16ToUtf8(outPackage))); + file::mkdirs(outPath); + file::appendPath(&outPath, "R.java"); + + std::ofstream fout(outPath, std::ofstream::binary); + if (!fout) { + mContext->getDiagnostics()->error(DiagMessage() << strerror(errno)); + return false; + } + + JavaClassGenerator generator(table, javaOptions); + if (!generator.generate(packageNameToGenerate, outPackage, &fout)) { + mContext->getDiagnostics()->error(DiagMessage(outPath) << generator.getError()); + return false; + } + return true; + } + + bool writeManifestJavaFile(xml::XmlResource* manifestXml) { + if (!mOptions.generateJavaClassPath) { + return true; + } + + std::string outPath = mOptions.generateJavaClassPath.value(); + file::appendPath(&outPath, + file::packageToPath(util::utf16ToUtf8(mContext->getCompilationPackage()))); + file::mkdirs(outPath); + file::appendPath(&outPath, "Manifest.java"); + + std::ofstream fout(outPath, std::ofstream::binary); + if (!fout) { + mContext->getDiagnostics()->error(DiagMessage() << strerror(errno)); + return false; + } + + ManifestClassGenerator generator; + if (!generator.generate(mContext->getDiagnostics(), mContext->getCompilationPackage(), + manifestXml, &fout)) { + return false; + } + + if (!fout) { + mContext->getDiagnostics()->error(DiagMessage() << strerror(errno)); + return false; + } + return true; + } + + bool writeProguardFile(const proguard::KeepSet& keepSet) { + if (!mOptions.generateProguardRulesPath) { + return true; + } + + std::ofstream fout(mOptions.generateProguardRulesPath.value(), std::ofstream::binary); + if (!fout) { + mContext->getDiagnostics()->error(DiagMessage() << strerror(errno)); + return false; + } + + proguard::writeKeepSet(&fout, keepSet); + if (!fout) { + mContext->getDiagnostics()->error(DiagMessage() << strerror(errno)); + return false; + } + return true; + } + + bool mergeStaticLibrary(const std::string& input) { + // TODO(adamlesinski): Load resources from a static library APK and merge the table into + // TableMerger. + mContext->getDiagnostics()->warn(DiagMessage() + << "linking static libraries not supported yet: " + << input); + return true; + } + + bool mergeResourceTable(io::IFile* file, bool override) { + if (mOptions.verbose) { + mContext->getDiagnostics()->note(DiagMessage() << "linking " << file->getSource()); + } + + std::unique_ptr<io::IData> data = file->openAsData(); + if (!data) { + mContext->getDiagnostics()->error(DiagMessage(file->getSource()) + << "failed to open file"); + return false; + } + + std::unique_ptr<ResourceTable> table = loadTable(file->getSource(), data->data(), + data->size()); + if (!table) { + return false; + } + + bool result = false; + if (override) { + result = mTableMerger->mergeOverlay(file->getSource(), table.get()); + } else { + result = mTableMerger->merge(file->getSource(), table.get()); + } + return result; + } + + bool mergeCompiledFile(io::IFile* file, std::unique_ptr<ResourceFile> fileDesc, bool overlay) { + if (mOptions.verbose) { + mContext->getDiagnostics()->note(DiagMessage() << "adding " << file->getSource()); + } + + bool result = false; + if (overlay) { + result = mTableMerger->mergeFileOverlay(*fileDesc, file); + } else { + result = mTableMerger->mergeFile(*fileDesc, file); + } + + if (!result) { + return false; + } + + // Add the exports of this file to the table. + for (SourcedResourceName& exportedSymbol : fileDesc->exportedSymbols) { + if (exportedSymbol.name.package.empty()) { + exportedSymbol.name.package = mContext->getCompilationPackage().toString(); + } + + ResourceNameRef resName = exportedSymbol.name; + + Maybe<ResourceName> mangledName = mContext->getNameMangler()->mangleName( + exportedSymbol.name); + if (mangledName) { + resName = mangledName.value(); + } + + std::unique_ptr<Id> id = util::make_unique<Id>(); + id->setSource(fileDesc->source.withLine(exportedSymbol.line)); + bool result = mFinalTable.addResourceAllowMangled(resName, {}, std::move(id), + mContext->getDiagnostics()); + if (!result) { + return false; + } + } + return true; + } + + /** + * Creates an io::IFileCollection from the ZIP archive and processes the files within. + */ + bool mergeArchive(const std::string& input, bool override) { + std::string errorStr; + std::unique_ptr<io::ZipFileCollection> collection = io::ZipFileCollection::create( + input, &errorStr); + if (!collection) { + mContext->getDiagnostics()->error(DiagMessage(input) << errorStr); + return false; + } + + bool error = false; + for (auto iter = collection->iterator(); iter->hasNext(); ) { + if (!processFile(iter->next(), override)) { + error = true; + } + } + + // Make sure to move the collection into the set of IFileCollections. + mCollections.push_back(std::move(collection)); + return !error; + } + + bool processFile(const std::string& path, bool override) { + if (util::stringEndsWith<char>(path, ".flata") || + util::stringEndsWith<char>(path, ".jar") || + util::stringEndsWith<char>(path, ".jack") || + util::stringEndsWith<char>(path, ".zip")) { + return mergeArchive(path, override); + } + + io::IFile* file = mFileCollection->insertFile(path); + return processFile(file, override); + } + + bool processFile(io::IFile* file, bool override) { + const Source& src = file->getSource(); + if (util::stringEndsWith<char>(src.path, ".arsc.flat")) { + return mergeResourceTable(file, override); + } else if (util::stringEndsWith<char>(src.path, ".flat")){ + // Try opening the file and looking for an Export header. + std::unique_ptr<io::IData> data = file->openAsData(); + if (!data) { + mContext->getDiagnostics()->error(DiagMessage(src) << "failed to open"); + return false; + } + + std::unique_ptr<ResourceFile> resourceFile = loadFileExportHeader( + src, data->data(), data->size(), mContext->getDiagnostics()); + if (resourceFile) { + return mergeCompiledFile(file, std::move(resourceFile), override); + } + + return false; + } + + // Ignore non .flat files. This could be classes.dex or something else that happens + // to be in an archive. + return true; + } + + int run(const std::vector<std::string>& inputFiles) { + // Load the AndroidManifest.xml + std::unique_ptr<xml::XmlResource> manifestXml = loadXml(mOptions.manifestPath, + mContext->getDiagnostics()); + if (!manifestXml) { + return 1; + } + + if (Maybe<AppInfo> maybeAppInfo = extractAppInfoFromManifest(manifestXml.get())) { + mContext->mCompilationPackage = maybeAppInfo.value().package; + } else { + mContext->getDiagnostics()->error(DiagMessage(mOptions.manifestPath) + << "no package specified in <manifest> tag"); + return 1; + } + + if (!util::isJavaPackageName(mContext->mCompilationPackage)) { + mContext->getDiagnostics()->error(DiagMessage(mOptions.manifestPath) + << "invalid package name '" + << mContext->mCompilationPackage + << "'"); + return 1; + } + + mContext->mNameMangler = util::make_unique<NameMangler>( + NameManglerPolicy{ mContext->mCompilationPackage }); + + if (mContext->mCompilationPackage == u"android") { + mContext->mPackageId = 0x01; + } else { + mContext->mPackageId = 0x7f; + } + + mContext->mSymbols = createSymbolTableFromIncludePaths(); + if (!mContext->mSymbols) { + return 1; + } + + TableMergerOptions tableMergerOptions; + tableMergerOptions.autoAddOverlay = mOptions.autoAddOverlay; + tableMergerOptions.filter = mOptions.configFilter; + mTableMerger = util::make_unique<TableMerger>(mContext, &mFinalTable, tableMergerOptions); + + if (mOptions.verbose) { + mContext->getDiagnostics()->note( + DiagMessage() << "linking package '" << mContext->mCompilationPackage << "' " + << "with package ID " << std::hex << (int) mContext->mPackageId); + } + + + for (const std::string& input : inputFiles) { + if (!processFile(input, false)) { + mContext->getDiagnostics()->error(DiagMessage() << "failed parsing input"); + return 1; + } + } + + for (const std::string& input : mOptions.overlayFiles) { + if (!processFile(input, true)) { + mContext->getDiagnostics()->error(DiagMessage() << "failed parsing overlays"); + return 1; + } + } + + if (!verifyNoExternalPackages()) { + return 1; + } + + if (!mOptions.staticLib) { + PrivateAttributeMover mover; + if (!mover.consume(mContext, &mFinalTable)) { + mContext->getDiagnostics()->error( + DiagMessage() << "failed moving private attributes"); + return 1; + } + } + + { + IdAssigner idAssigner; + if (!idAssigner.consume(mContext, &mFinalTable)) { + mContext->getDiagnostics()->error(DiagMessage() << "failed assigning IDs"); + return 1; + } + } + + mContext->mNameMangler = util::make_unique<NameMangler>(NameManglerPolicy{ + mContext->mCompilationPackage, mTableMerger->getMergedPackages() }); + mContext->mSymbols = JoinedSymbolTableBuilder() + .addSymbolTable(util::make_unique<SymbolTableWrapper>(&mFinalTable)) + .addSymbolTable(std::move(mContext->mSymbols)) + .build(); + + { + ReferenceLinker linker; + if (!linker.consume(mContext, &mFinalTable)) { + mContext->getDiagnostics()->error(DiagMessage() << "failed linking references"); + return 1; + } + } + + proguard::KeepSet proguardKeepSet; + + std::unique_ptr<IArchiveWriter> archiveWriter = makeArchiveWriter(); + if (!archiveWriter) { + mContext->getDiagnostics()->error(DiagMessage() << "failed to create archive"); + return 1; + } + + bool error = false; + { + ManifestFixer manifestFixer(mOptions.manifestFixerOptions); + if (!manifestFixer.consume(mContext, manifestXml.get())) { + error = true; + } + + // AndroidManifest.xml has no resource name, but the CallSite is built from the name + // (aka, which package the AndroidManifest.xml is coming from). + // So we give it a package name so it can see local resources. + manifestXml->file.name.package = mContext->getCompilationPackage().toString(); + + XmlReferenceLinker manifestLinker; + if (manifestLinker.consume(mContext, manifestXml.get())) { + if (!proguard::collectProguardRulesForManifest(Source(mOptions.manifestPath), + manifestXml.get(), + &proguardKeepSet)) { + error = true; + } + + if (mOptions.generateJavaClassPath) { + if (!writeManifestJavaFile(manifestXml.get())) { + error = true; + } + } + + if (!flattenXml(manifestXml.get(), "AndroidManifest.xml", {}, + archiveWriter.get())) { + error = true; + } + } else { + error = true; + } + } + + if (error) { + mContext->getDiagnostics()->error(DiagMessage() << "failed processing manifest"); + return 1; + } + + for (auto& mergeEntry : mTableMerger->getFilesToMerge()) { + const ResourceKeyRef& key = mergeEntry.first; + const FileToMerge& fileToMerge = mergeEntry.second; + + const StringPiece path = fileToMerge.file->getSource().path; + + if (key.name.type != ResourceType::kRaw && + (util::stringEndsWith<char>(path, ".xml.flat") || + util::stringEndsWith<char>(path, ".xml"))) { + if (mOptions.verbose) { + mContext->getDiagnostics()->note(DiagMessage() << "linking " << path); + } + + io::IFile* file = fileToMerge.file; + std::unique_ptr<io::IData> data = file->openAsData(); + if (!data) { + mContext->getDiagnostics()->error(DiagMessage(file->getSource()) + << "failed to open file"); + return 1; + } + + std::unique_ptr<xml::XmlResource> xmlRes; + if (util::stringEndsWith<char>(path, ".flat")) { + xmlRes = loadBinaryXmlSkipFileExport(file->getSource(), + data->data(), data->size(), + mContext->getDiagnostics()); + } else { + xmlRes = xml::inflate(data->data(), data->size(), mContext->getDiagnostics(), + file->getSource()); + } + + if (!xmlRes) { + return 1; + } + + // Create the file description header. + xmlRes->file = ResourceFile{ + key.name.toResourceName(), + key.config, + fileToMerge.originalSource, + }; + + XmlReferenceLinker xmlLinker; + if (xmlLinker.consume(mContext, xmlRes.get())) { + if (!proguard::collectProguardRules(xmlRes->file.source, xmlRes.get(), + &proguardKeepSet)) { + error = true; + } + + Maybe<size_t> maxSdkLevel; + if (!mOptions.noAutoVersion) { + maxSdkLevel = std::max<size_t>(xmlRes->file.config.sdkVersion, 1u); + } + + if (!flattenXml(xmlRes.get(), fileToMerge.dstPath, maxSdkLevel, + archiveWriter.get())) { + error = true; + } + + if (!mOptions.noAutoVersion) { + Maybe<ResourceTable::SearchResult> result = mFinalTable.findResource( + xmlRes->file.name); + for (int sdkLevel : xmlLinker.getSdkLevels()) { + if (sdkLevel > xmlRes->file.config.sdkVersion && + shouldGenerateVersionedResource(result.value().entry, + xmlRes->file.config, + sdkLevel)) { + xmlRes->file.config.sdkVersion = sdkLevel; + + std::string genResourcePath = ResourceUtils::buildResourceFileName( + xmlRes->file, mContext->getNameMangler()); + + bool added = mFinalTable.addFileReference( + xmlRes->file.name, + xmlRes->file.config, + xmlRes->file.source, + util::utf8ToUtf16(genResourcePath), + mContext->getDiagnostics()); + if (!added) { + error = true; + continue; + } + + if (!flattenXml(xmlRes.get(), genResourcePath, sdkLevel, + archiveWriter.get())) { + error = true; + } + } + } + } + + } else { + error = true; + } + } else { + if (mOptions.verbose) { + mContext->getDiagnostics()->note(DiagMessage() << "copying " << path); + } + + if (!copyFileToArchive(fileToMerge.file, fileToMerge.dstPath, + archiveWriter.get())) { + error = true; + } + } + } + + if (error) { + mContext->getDiagnostics()->error(DiagMessage() << "failed linking file resources"); + return 1; + } + + if (!mOptions.noAutoVersion) { + AutoVersioner versioner; + if (!versioner.consume(mContext, &mFinalTable)) { + mContext->getDiagnostics()->error(DiagMessage() << "failed versioning styles"); + return 1; + } + } + + if (!flattenTable(&mFinalTable, archiveWriter.get())) { + mContext->getDiagnostics()->error(DiagMessage() << "failed to write resources.arsc"); + return 1; + } + + if (mOptions.generateJavaClassPath) { + JavaClassGeneratorOptions options; + options.types = JavaClassGeneratorOptions::SymbolTypes::kAll; + + if (mOptions.staticLib || mOptions.generateNonFinalIds) { + options.useFinal = false; + } + + const StringPiece16 actualPackage = mContext->getCompilationPackage(); + StringPiece16 outputPackage = mContext->getCompilationPackage(); + if (mOptions.customJavaPackage) { + // Override the output java package to the custom one. + outputPackage = mOptions.customJavaPackage.value(); + } + + if (mOptions.privateSymbols) { + // If we defined a private symbols package, we only emit Public symbols + // to the original package, and private and public symbols to the private package. + + options.types = JavaClassGeneratorOptions::SymbolTypes::kPublic; + if (!writeJavaFile(&mFinalTable, mContext->getCompilationPackage(), + outputPackage, options)) { + return 1; + } + + options.types = JavaClassGeneratorOptions::SymbolTypes::kPublicPrivate; + outputPackage = mOptions.privateSymbols.value(); + } + + if (!writeJavaFile(&mFinalTable, actualPackage, outputPackage, options)) { + return 1; + } + + for (const std::u16string& extraPackage : mOptions.extraJavaPackages) { + if (!writeJavaFile(&mFinalTable, actualPackage, extraPackage, options)) { + return 1; + } + } + } + + if (mOptions.generateProguardRulesPath) { + if (!writeProguardFile(proguardKeepSet)) { + return 1; + } + } + + if (mOptions.verbose) { + Debug::printTable(&mFinalTable); + } + return 0; + } + +private: + LinkOptions mOptions; + LinkContext* mContext; + ResourceTable mFinalTable; + + ResourceTable mLocalFileTable; + std::unique_ptr<TableMerger> mTableMerger; + + // A pointer to the FileCollection representing the filesystem (not archives). + io::FileCollection* mFileCollection; + + // A vector of IFileCollections. This is mainly here to keep ownership of the collections. + std::vector<std::unique_ptr<io::IFileCollection>> mCollections; +}; + +int link(const std::vector<StringPiece>& args) { + LinkOptions options; + Maybe<std::string> privateSymbolsPackage; + Maybe<std::string> minSdkVersion, targetSdkVersion; + Maybe<std::string> renameManifestPackage, renameInstrumentationTargetPackage; + Maybe<std::string> versionCode, versionName; + Maybe<std::string> customJavaPackage; + std::vector<std::string> extraJavaPackages; + Maybe<std::string> configs; + bool legacyXFlag = false; + bool requireLocalization = false; + Flags flags = Flags() + .requiredFlag("-o", "Output path", &options.outputPath) + .requiredFlag("--manifest", "Path to the Android manifest to build", + &options.manifestPath) + .optionalFlagList("-I", "Adds an Android APK to link against", &options.includePaths) + .optionalFlagList("-R", "Compilation unit to link, using `overlay` semantics.\n" + "The last conflicting resource given takes precedence.", + &options.overlayFiles) + .optionalFlag("--java", "Directory in which to generate R.java", + &options.generateJavaClassPath) + .optionalFlag("--proguard", "Output file for generated Proguard rules", + &options.generateProguardRulesPath) + .optionalSwitch("--no-auto-version", + "Disables automatic style and layout SDK versioning", + &options.noAutoVersion) + .optionalSwitch("-x", "Legacy flag that specifies to use the package identifier 0x01", + &legacyXFlag) + .optionalSwitch("-z", "Require localization of strings marked 'suggested'", + &requireLocalization) + .optionalFlag("-c", "Comma separated list of configurations to include. The default\n" + "is all configurations", &configs) + .optionalSwitch("--output-to-dir", "Outputs the APK contents to a directory specified " + "by -o", + &options.outputToDirectory) + .optionalFlag("--min-sdk-version", "Default minimum SDK version to use for " + "AndroidManifest.xml", &minSdkVersion) + .optionalFlag("--target-sdk-version", "Default target SDK version to use for " + "AndroidManifest.xml", &targetSdkVersion) + .optionalFlag("--version-code", "Version code (integer) to inject into the " + "AndroidManifest.xml if none is present", &versionCode) + .optionalFlag("--version-name", "Version name to inject into the AndroidManifest.xml " + "if none is present", &versionName) + .optionalSwitch("--static-lib", "Generate a static Android library", &options.staticLib) + .optionalSwitch("--non-final-ids", "Generates R.java without the final modifier.\n" + "This is implied when --static-lib is specified.", + &options.generateNonFinalIds) + .optionalFlag("--private-symbols", "Package name to use when generating R.java for " + "private symbols.\n" + "If not specified, public and private symbols will use the application's " + "package name", &privateSymbolsPackage) + .optionalFlag("--custom-package", "Custom Java package under which to generate R.java", + &customJavaPackage) + .optionalFlagList("--extra-packages", "Generate the same R.java but with different " + "package names", &extraJavaPackages) + .optionalSwitch("--auto-add-overlay", "Allows the addition of new resources in " + "overlays without <add-resource> tags", &options.autoAddOverlay) + .optionalFlag("--rename-manifest-package", "Renames the package in AndroidManifest.xml", + &renameManifestPackage) + .optionalFlag("--rename-instrumentation-target-package", + "Changes the name of the target package for instrumentation. Most useful " + "when used\nin conjunction with --rename-manifest-package", + &renameInstrumentationTargetPackage) + .optionalFlagList("-0", "File extensions not to compress", + &options.extensionsToNotCompress) + .optionalSwitch("-v", "Enables verbose logging", &options.verbose); + + if (!flags.parse("aapt2 link", args, &std::cerr)) { + return 1; + } + + LinkContext context; + + if (privateSymbolsPackage) { + options.privateSymbols = util::utf8ToUtf16(privateSymbolsPackage.value()); + } + + if (minSdkVersion) { + options.manifestFixerOptions.minSdkVersionDefault = + util::utf8ToUtf16(minSdkVersion.value()); + } + + if (targetSdkVersion) { + options.manifestFixerOptions.targetSdkVersionDefault = + util::utf8ToUtf16(targetSdkVersion.value()); + } + + if (renameManifestPackage) { + options.manifestFixerOptions.renameManifestPackage = + util::utf8ToUtf16(renameManifestPackage.value()); + } + + if (renameInstrumentationTargetPackage) { + options.manifestFixerOptions.renameInstrumentationTargetPackage = + util::utf8ToUtf16(renameInstrumentationTargetPackage.value()); + } + + if (versionCode) { + options.manifestFixerOptions.versionCodeDefault = util::utf8ToUtf16(versionCode.value()); + } + + if (versionName) { + options.manifestFixerOptions.versionNameDefault = util::utf8ToUtf16(versionName.value()); + } + + if (customJavaPackage) { + options.customJavaPackage = util::utf8ToUtf16(customJavaPackage.value()); + } + + // Populate the set of extra packages for which to generate R.java. + for (std::string& extraPackage : extraJavaPackages) { + // A given package can actually be a colon separated list of packages. + for (StringPiece package : util::split(extraPackage, ':')) { + options.extraJavaPackages.insert(util::utf8ToUtf16(package)); + } + } + + AxisConfigFilter filter; + if (configs) { + for (const StringPiece& configStr : util::tokenize<char>(configs.value(), ',')) { + ConfigDescription config; + LocaleValue lv; + if (lv.initFromFilterString(configStr)) { + lv.writeTo(&config); + } else if (!ConfigDescription::parse(configStr, &config)) { + context.getDiagnostics()->error( + DiagMessage() << "invalid config '" << configStr << "' for -c option"); + return 1; + } + + if (config.density != 0) { + context.getDiagnostics()->warn( + DiagMessage() << "ignoring density '" << config << "' for -c option"); + } else { + filter.addConfig(config); + } + } + + options.configFilter = &filter; + } + + LinkCommand cmd(&context, options); + return cmd.run(flags.getArgs()); +} + +} // namespace aapt diff --git a/tools/aapt2/link/Linkers.h b/tools/aapt2/link/Linkers.h new file mode 100644 index 000000000000..4d3a483c6b82 --- /dev/null +++ b/tools/aapt2/link/Linkers.h @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_LINKER_LINKERS_H +#define AAPT_LINKER_LINKERS_H + +#include "Resource.h" +#include "process/IResourceTableConsumer.h" +#include "xml/XmlDom.h" + +#include <set> + +namespace aapt { + +class ResourceTable; +struct ResourceEntry; +struct ConfigDescription; + +/** + * Defines the location in which a value exists. This determines visibility of other + * package's private symbols. + */ +struct CallSite { + ResourceNameRef resource; +}; + +/** + * Determines whether a versioned resource should be created. If a versioned resource already + * exists, it takes precedence. + */ +bool shouldGenerateVersionedResource(const ResourceEntry* entry, const ConfigDescription& config, + const int sdkVersionToGenerate); + +struct AutoVersioner : public IResourceTableConsumer { + bool consume(IAaptContext* context, ResourceTable* table) override; +}; + +struct XmlAutoVersioner : public IXmlResourceConsumer { + bool consume(IAaptContext* context, xml::XmlResource* resource) override; +}; + +/** + * If any attribute resource values are defined as public, this consumer will move all private + * attribute resource values to a private ^private-attr type, avoiding backwards compatibility + * issues with new apps running on old platforms. + * + * The Android platform ignores resource attributes it doesn't recognize, so an app developer can + * use new attributes in their layout XML files without worrying about versioning. This assumption + * actually breaks on older platforms. OEMs may add private attributes that are used internally. + * AAPT originally assigned all private attributes IDs immediately proceeding the public attributes' + * IDs. + * + * This means that on a newer Android platform, an ID previously assigned to a private attribute + * may end up assigned to a public attribute. + * + * App developers assume using the newer attribute is safe on older platforms because it will + * be ignored. Instead, the platform thinks the new attribute is an older, private attribute and + * will interpret it as such. This leads to unintended styling and exceptions thrown due to + * unexpected types. + * + * By moving the private attributes to a completely different type, this ID conflict will never + * occur. + */ +struct PrivateAttributeMover : public IResourceTableConsumer { + bool consume(IAaptContext* context, ResourceTable* table) override; +}; + +/** + * Resolves attributes in the XmlResource and compiles string values to resource values. + * Once an XmlResource is processed by this linker, it is ready to be flattened. + */ +class XmlReferenceLinker : public IXmlResourceConsumer { +private: + std::set<int> mSdkLevelsFound; + +public: + bool consume(IAaptContext* context, xml::XmlResource* resource) override; + + /** + * Once the XmlResource has been consumed, this returns the various SDK levels in which + * framework attributes used within the XML document were defined. + */ + inline const std::set<int>& getSdkLevels() const { + return mSdkLevelsFound; + } +}; + +} // namespace aapt + +#endif /* AAPT_LINKER_LINKERS_H */ diff --git a/tools/aapt2/link/ManifestFixer.cpp b/tools/aapt2/link/ManifestFixer.cpp new file mode 100644 index 000000000000..9baf1d86795c --- /dev/null +++ b/tools/aapt2/link/ManifestFixer.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ResourceUtils.h" +#include "link/ManifestFixer.h" +#include "util/Util.h" +#include "xml/XmlDom.h" + +namespace aapt { + +static bool verifyManifest(IAaptContext* context, const Source& source, xml::Element* manifestEl) { + xml::Attribute* attr = manifestEl->findAttribute({}, u"package"); + if (!attr) { + context->getDiagnostics()->error(DiagMessage(source.withLine(manifestEl->lineNumber)) + << "missing 'package' attribute"); + } else if (ResourceUtils::isReference(attr->value)) { + context->getDiagnostics()->error(DiagMessage(source.withLine(manifestEl->lineNumber)) + << "value for attribute 'package' must not be a " + "reference"); + } else if (!util::isJavaPackageName(attr->value)) { + context->getDiagnostics()->error(DiagMessage(source.withLine(manifestEl->lineNumber)) + << "invalid package name '" << attr->value << "'"); + } else { + return true; + } + return false; +} + +static bool includeVersionName(IAaptContext* context, const Source& source, + const StringPiece16& versionName, xml::Element* manifestEl) { + if (manifestEl->findAttribute(xml::kSchemaAndroid, u"versionName")) { + return true; + } + + manifestEl->attributes.push_back(xml::Attribute{ + xml::kSchemaAndroid, u"versionName", versionName.toString() }); + return true; +} + +static bool includeVersionCode(IAaptContext* context, const Source& source, + const StringPiece16& versionCode, xml::Element* manifestEl) { + if (manifestEl->findAttribute(xml::kSchemaAndroid, u"versionCode")) { + return true; + } + + manifestEl->attributes.push_back(xml::Attribute{ + xml::kSchemaAndroid, u"versionCode", versionCode.toString() }); + return true; +} + +static bool fixUsesSdk(IAaptContext* context, const Source& source, xml::Element* el, + const ManifestFixerOptions& options) { + if (options.minSdkVersionDefault && + el->findAttribute(xml::kSchemaAndroid, u"minSdkVersion") == nullptr) { + // There was no minSdkVersion defined and we have a default to assign. + el->attributes.push_back(xml::Attribute{ + xml::kSchemaAndroid, u"minSdkVersion", options.minSdkVersionDefault.value() }); + } + + if (options.targetSdkVersionDefault && + el->findAttribute(xml::kSchemaAndroid, u"targetSdkVersion") == nullptr) { + // There was no targetSdkVersion defined and we have a default to assign. + el->attributes.push_back(xml::Attribute{ + xml::kSchemaAndroid, u"targetSdkVersion", + options.targetSdkVersionDefault.value() }); + } + return true; +} + +class FullyQualifiedClassNameVisitor : public xml::Visitor { +public: + using xml::Visitor::visit; + + FullyQualifiedClassNameVisitor(const StringPiece16& package) : mPackage(package) { + } + + void visit(xml::Element* el) override { + for (xml::Attribute& attr : el->attributes) { + if (Maybe<std::u16string> newValue = + util::getFullyQualifiedClassName(mPackage, attr.value)) { + attr.value = std::move(newValue.value()); + } + } + + // Super implementation to iterate over the children. + xml::Visitor::visit(el); + } + +private: + StringPiece16 mPackage; +}; + +static bool renameManifestPackage(IAaptContext* context, const Source& source, + const StringPiece16& packageOverride, xml::Element* manifestEl) { + if (!util::isJavaPackageName(packageOverride)) { + context->getDiagnostics()->error(DiagMessage() << "invalid manifest package override '" + << packageOverride << "'"); + return false; + } + + xml::Attribute* attr = manifestEl->findAttribute({}, u"package"); + + // We've already verified that the manifest element is present, with a package name specified. + assert(attr); + + std::u16string originalPackage = std::move(attr->value); + attr->value = packageOverride.toString(); + + FullyQualifiedClassNameVisitor visitor(originalPackage); + manifestEl->accept(&visitor); + return true; +} + +static bool renameInstrumentationTargetPackage(IAaptContext* context, const Source& source, + const StringPiece16& packageOverride, + xml::Element* manifestEl) { + if (!util::isJavaPackageName(packageOverride)) { + context->getDiagnostics()->error(DiagMessage() + << "invalid instrumentation target package override '" + << packageOverride << "'"); + return false; + } + + xml::Element* instrumentationEl = manifestEl->findChild({}, u"instrumentation"); + if (!instrumentationEl) { + // No error if there is no work to be done. + return true; + } + + xml::Attribute* attr = instrumentationEl->findAttribute(xml::kSchemaAndroid, u"targetPackage"); + if (!attr) { + // No error if there is no work to be done. + return true; + } + + attr->value = packageOverride.toString(); + return true; +} + +bool ManifestFixer::consume(IAaptContext* context, xml::XmlResource* doc) { + xml::Element* root = xml::findRootElement(doc->root.get()); + if (!root || !root->namespaceUri.empty() || root->name != u"manifest") { + context->getDiagnostics()->error(DiagMessage(doc->file.source) + << "root tag must be <manifest>"); + return false; + } + + if (!verifyManifest(context, doc->file.source, root)) { + return false; + } + + if (mOptions.versionCodeDefault) { + if (!includeVersionCode(context, doc->file.source, mOptions.versionCodeDefault.value(), + root)) { + return false; + } + } + + if (mOptions.versionNameDefault) { + if (!includeVersionName(context, doc->file.source, mOptions.versionNameDefault.value(), + root)) { + return false; + } + } + + if (mOptions.renameManifestPackage) { + // Rename manifest package. + if (!renameManifestPackage(context, doc->file.source, + mOptions.renameManifestPackage.value(), root)) { + return false; + } + } + + if (mOptions.renameInstrumentationTargetPackage) { + if (!renameInstrumentationTargetPackage(context, doc->file.source, + mOptions.renameInstrumentationTargetPackage.value(), + root)) { + return false; + } + } + + bool foundUsesSdk = false; + for (xml::Element* el : root->getChildElements()) { + if (!el->namespaceUri.empty()) { + continue; + } + + if (el->name == u"uses-sdk") { + foundUsesSdk = true; + fixUsesSdk(context, doc->file.source, el, mOptions); + } + } + + if (!foundUsesSdk && (mOptions.minSdkVersionDefault || mOptions.targetSdkVersionDefault)) { + std::unique_ptr<xml::Element> usesSdk = util::make_unique<xml::Element>(); + usesSdk->name = u"uses-sdk"; + fixUsesSdk(context, doc->file.source, usesSdk.get(), mOptions); + root->addChild(std::move(usesSdk)); + } + + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/link/ManifestFixer.h b/tools/aapt2/link/ManifestFixer.h new file mode 100644 index 000000000000..b8d9c833ff05 --- /dev/null +++ b/tools/aapt2/link/ManifestFixer.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_LINK_MANIFESTFIXER_H +#define AAPT_LINK_MANIFESTFIXER_H + +#include "process/IResourceTableConsumer.h" +#include "util/Maybe.h" +#include "xml/XmlDom.h" + +#include <string> + +namespace aapt { + +struct ManifestFixerOptions { + Maybe<std::u16string> minSdkVersionDefault; + Maybe<std::u16string> targetSdkVersionDefault; + Maybe<std::u16string> renameManifestPackage; + Maybe<std::u16string> renameInstrumentationTargetPackage; + Maybe<std::u16string> versionNameDefault; + Maybe<std::u16string> versionCodeDefault; +}; + +/** + * Verifies that the manifest is correctly formed and inserts defaults + * where specified with ManifestFixerOptions. + */ +struct ManifestFixer : public IXmlResourceConsumer { + ManifestFixerOptions mOptions; + + ManifestFixer(const ManifestFixerOptions& options) : mOptions(options) { + } + + bool consume(IAaptContext* context, xml::XmlResource* doc) override; +}; + +} // namespace aapt + +#endif /* AAPT_LINK_MANIFESTFIXER_H */ diff --git a/tools/aapt2/link/ManifestFixer_test.cpp b/tools/aapt2/link/ManifestFixer_test.cpp new file mode 100644 index 000000000000..f40fbfb2e81a --- /dev/null +++ b/tools/aapt2/link/ManifestFixer_test.cpp @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "link/ManifestFixer.h" +#include "test/Builders.h" +#include "test/Context.h" + +#include <gtest/gtest.h> + +namespace aapt { + +struct ManifestFixerTest : public ::testing::Test { + std::unique_ptr<IAaptContext> mContext; + + void SetUp() override { + mContext = test::ContextBuilder() + .setCompilationPackage(u"android") + .setPackageId(0x01) + .setNameManglerPolicy(NameManglerPolicy{ u"android" }) + .setSymbolTable(test::StaticSymbolTableBuilder() + .addSymbol(u"@android:attr/package", ResourceId(0x01010000), + test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_STRING) + .build()) + .addSymbol(u"@android:attr/minSdkVersion", ResourceId(0x01010001), + test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_STRING | + android::ResTable_map::TYPE_INTEGER) + .build()) + .addSymbol(u"@android:attr/targetSdkVersion", ResourceId(0x01010002), + test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_STRING | + android::ResTable_map::TYPE_INTEGER) + .build()) + .addSymbol(u"@android:string/str", ResourceId(0x01060000)) + .build()) + .build(); + } + + std::unique_ptr<xml::XmlResource> verify(const StringPiece& str) { + return verifyWithOptions(str, {}); + } + + std::unique_ptr<xml::XmlResource> verifyWithOptions(const StringPiece& str, + const ManifestFixerOptions& options) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDom(str); + ManifestFixer fixer(options); + if (fixer.consume(mContext.get(), doc.get())) { + return doc; + } + return {}; + } +}; + +TEST_F(ManifestFixerTest, EnsureManifestIsRootTag) { + EXPECT_EQ(nullptr, verify("<other-tag />")); + EXPECT_EQ(nullptr, verify("<ns:manifest xmlns:ns=\"com\" />")); + EXPECT_NE(nullptr, verify("<manifest package=\"android\"></manifest>")); +} + +TEST_F(ManifestFixerTest, EnsureManifestHasPackage) { + EXPECT_NE(nullptr, verify("<manifest package=\"android\" />")); + EXPECT_NE(nullptr, verify("<manifest package=\"com.android\" />")); + EXPECT_NE(nullptr, verify("<manifest package=\"com.android.google\" />")); + EXPECT_EQ(nullptr, verify("<manifest package=\"com.android.google.Class$1\" />")); + EXPECT_EQ(nullptr, + verify("<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\" " + "android:package=\"com.android\" />")); + EXPECT_EQ(nullptr, verify("<manifest package=\"@string/str\" />")); +} + +TEST_F(ManifestFixerTest, UseDefaultSdkVersionsIfNonePresent) { + ManifestFixerOptions options = { std::u16string(u"8"), std::u16string(u"22") }; + + std::unique_ptr<xml::XmlResource> doc = verifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <uses-sdk android:minSdkVersion="7" android:targetSdkVersion="21" /> + </manifest>)EOF", options); + ASSERT_NE(nullptr, doc); + + xml::Element* el; + xml::Attribute* attr; + + el = xml::findRootElement(doc.get()); + ASSERT_NE(nullptr, el); + el = el->findChild({}, u"uses-sdk"); + ASSERT_NE(nullptr, el); + attr = el->findAttribute(xml::kSchemaAndroid, u"minSdkVersion"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(u"7", attr->value); + attr = el->findAttribute(xml::kSchemaAndroid, u"targetSdkVersion"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(u"21", attr->value); + + doc = verifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <uses-sdk android:targetSdkVersion="21" /> + </manifest>)EOF", options); + ASSERT_NE(nullptr, doc); + + el = xml::findRootElement(doc.get()); + ASSERT_NE(nullptr, el); + el = el->findChild({}, u"uses-sdk"); + ASSERT_NE(nullptr, el); + attr = el->findAttribute(xml::kSchemaAndroid, u"minSdkVersion"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(u"8", attr->value); + attr = el->findAttribute(xml::kSchemaAndroid, u"targetSdkVersion"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(u"21", attr->value); + + doc = verifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <uses-sdk /> + </manifest>)EOF", options); + ASSERT_NE(nullptr, doc); + + el = xml::findRootElement(doc.get()); + ASSERT_NE(nullptr, el); + el = el->findChild({}, u"uses-sdk"); + ASSERT_NE(nullptr, el); + attr = el->findAttribute(xml::kSchemaAndroid, u"minSdkVersion"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(u"8", attr->value); + attr = el->findAttribute(xml::kSchemaAndroid, u"targetSdkVersion"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(u"22", attr->value); + + doc = verifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android" />)EOF", options); + ASSERT_NE(nullptr, doc); + + el = xml::findRootElement(doc.get()); + ASSERT_NE(nullptr, el); + el = el->findChild({}, u"uses-sdk"); + ASSERT_NE(nullptr, el); + attr = el->findAttribute(xml::kSchemaAndroid, u"minSdkVersion"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(u"8", attr->value); + attr = el->findAttribute(xml::kSchemaAndroid, u"targetSdkVersion"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(u"22", attr->value); +} + +TEST_F(ManifestFixerTest, RenameManifestPackageAndFullyQualifyClasses) { + ManifestFixerOptions options; + options.renameManifestPackage = std::u16string(u"com.android"); + + std::unique_ptr<xml::XmlResource> doc = verifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <application name=".MainApplication" text="hello"> + <activity name=".activity.Start" /> + <receiver name="com.google.android.Receiver" /> + </application> + </manifest>)EOF", options); + ASSERT_NE(nullptr, doc); + + xml::Element* manifestEl = xml::findRootElement(doc.get()); + ASSERT_NE(nullptr, manifestEl); + + xml::Attribute* attr = nullptr; + + attr = manifestEl->findAttribute({}, u"package"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(std::u16string(u"com.android"), attr->value); + + xml::Element* applicationEl = manifestEl->findChild({}, u"application"); + ASSERT_NE(nullptr, applicationEl); + + attr = applicationEl->findAttribute({}, u"name"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(std::u16string(u"android.MainApplication"), attr->value); + + attr = applicationEl->findAttribute({}, u"text"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(std::u16string(u"hello"), attr->value); + + xml::Element* el; + el = applicationEl->findChild({}, u"activity"); + ASSERT_NE(nullptr, el); + + attr = el->findAttribute({}, u"name"); + ASSERT_NE(nullptr, el); + EXPECT_EQ(std::u16string(u"android.activity.Start"), attr->value); + + el = applicationEl->findChild({}, u"receiver"); + ASSERT_NE(nullptr, el); + + attr = el->findAttribute({}, u"name"); + ASSERT_NE(nullptr, el); + EXPECT_EQ(std::u16string(u"com.google.android.Receiver"), attr->value); +} + +TEST_F(ManifestFixerTest, RenameManifestInstrumentationPackageAndFullyQualifyTarget) { + ManifestFixerOptions options; + options.renameInstrumentationTargetPackage = std::u16string(u"com.android"); + + std::unique_ptr<xml::XmlResource> doc = verifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android"> + <instrumentation android:targetPackage="android" /> + </manifest>)EOF", options); + ASSERT_NE(nullptr, doc); + + xml::Element* manifestEl = xml::findRootElement(doc.get()); + ASSERT_NE(nullptr, manifestEl); + + xml::Element* instrumentationEl = manifestEl->findChild({}, u"instrumentation"); + ASSERT_NE(nullptr, instrumentationEl); + + xml::Attribute* attr = instrumentationEl->findAttribute(xml::kSchemaAndroid, u"targetPackage"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(std::u16string(u"com.android"), attr->value); +} + +TEST_F(ManifestFixerTest, UseDefaultVersionNameAndCode) { + ManifestFixerOptions options; + options.versionNameDefault = std::u16string(u"Beta"); + options.versionCodeDefault = std::u16string(u"0x10000000"); + + std::unique_ptr<xml::XmlResource> doc = verifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android" />)EOF", options); + ASSERT_NE(nullptr, doc); + + xml::Element* manifestEl = xml::findRootElement(doc.get()); + ASSERT_NE(nullptr, manifestEl); + + xml::Attribute* attr = manifestEl->findAttribute(xml::kSchemaAndroid, u"versionName"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(std::u16string(u"Beta"), attr->value); + + attr = manifestEl->findAttribute(xml::kSchemaAndroid, u"versionCode"); + ASSERT_NE(nullptr, attr); + EXPECT_EQ(std::u16string(u"0x10000000"), attr->value); +} + +} // namespace aapt diff --git a/tools/aapt2/link/PrivateAttributeMover.cpp b/tools/aapt2/link/PrivateAttributeMover.cpp new file mode 100644 index 000000000000..3c8af4f81ffe --- /dev/null +++ b/tools/aapt2/link/PrivateAttributeMover.cpp @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ResourceTable.h" +#include "link/Linkers.h" + +#include <algorithm> +#include <iterator> + +namespace aapt { + +template <typename InputContainer, typename OutputIterator, typename Predicate> +OutputIterator moveIf(InputContainer& inputContainer, OutputIterator result, + Predicate pred) { + const auto last = inputContainer.end(); + auto newEnd = std::find_if(inputContainer.begin(), inputContainer.end(), pred); + if (newEnd == last) { + return result; + } + + *result = std::move(*newEnd); + + auto first = newEnd; + ++first; + + for (; first != last; ++first) { + if (bool(pred(*first))) { + // We want to move this guy + *result = std::move(*first); + ++result; + } else { + // We want to keep this guy, but we will need to move it up the list to replace + // missing items. + *newEnd = std::move(*first); + ++newEnd; + } + } + + inputContainer.erase(newEnd, last); + return result; +} + +bool PrivateAttributeMover::consume(IAaptContext* context, ResourceTable* table) { + for (auto& package : table->packages) { + ResourceTableType* type = package->findType(ResourceType::kAttr); + if (!type) { + continue; + } + + if (type->symbolStatus.state != SymbolState::kPublic) { + // No public attributes, so we can safely leave these private attributes where they are. + return true; + } + + ResourceTableType* privAttrType = package->findOrCreateType(ResourceType::kAttrPrivate); + assert(privAttrType->entries.empty()); + + moveIf(type->entries, std::back_inserter(privAttrType->entries), + [](const std::unique_ptr<ResourceEntry>& entry) -> bool { + return entry->symbolStatus.state != SymbolState::kPublic; + }); + break; + } + return true; +} + +} // namespace aapt diff --git a/tools/aapt2/link/PrivateAttributeMover_test.cpp b/tools/aapt2/link/PrivateAttributeMover_test.cpp new file mode 100644 index 000000000000..dbe0c92253c1 --- /dev/null +++ b/tools/aapt2/link/PrivateAttributeMover_test.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "link/Linkers.h" +#include "test/Builders.h" +#include "test/Context.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(PrivateAttributeMoverTest, MovePrivateAttributes) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addSimple(u"@android:attr/publicA") + .addSimple(u"@android:attr/privateA") + .addSimple(u"@android:attr/publicB") + .addSimple(u"@android:attr/privateB") + .setSymbolState(u"@android:attr/publicA", ResourceId(0x01010000), SymbolState::kPublic) + .setSymbolState(u"@android:attr/publicB", ResourceId(0x01010000), SymbolState::kPublic) + .build(); + + PrivateAttributeMover mover; + ASSERT_TRUE(mover.consume(context.get(), table.get())); + + ResourceTablePackage* package = table->findPackage(u"android"); + ASSERT_NE(package, nullptr); + + ResourceTableType* type = package->findType(ResourceType::kAttr); + ASSERT_NE(type, nullptr); + ASSERT_EQ(type->entries.size(), 2u); + EXPECT_NE(type->findEntry(u"publicA"), nullptr); + EXPECT_NE(type->findEntry(u"publicB"), nullptr); + + type = package->findType(ResourceType::kAttrPrivate); + ASSERT_NE(type, nullptr); + ASSERT_EQ(type->entries.size(), 2u); + EXPECT_NE(type->findEntry(u"privateA"), nullptr); + EXPECT_NE(type->findEntry(u"privateB"), nullptr); +} + +TEST(PrivateAttributeMoverTest, LeavePrivateAttributesWhenNoPublicAttributesDefined) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().build(); + + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addSimple(u"@android:attr/privateA") + .addSimple(u"@android:attr/privateB") + .build(); + + PrivateAttributeMover mover; + ASSERT_TRUE(mover.consume(context.get(), table.get())); + + ResourceTablePackage* package = table->findPackage(u"android"); + ASSERT_NE(package, nullptr); + + ResourceTableType* type = package->findType(ResourceType::kAttr); + ASSERT_NE(type, nullptr); + ASSERT_EQ(type->entries.size(), 2u); + + type = package->findType(ResourceType::kAttrPrivate); + ASSERT_EQ(type, nullptr); +} + +} // namespace aapt diff --git a/tools/aapt2/link/ReferenceLinker.cpp b/tools/aapt2/link/ReferenceLinker.cpp new file mode 100644 index 000000000000..27435398c408 --- /dev/null +++ b/tools/aapt2/link/ReferenceLinker.cpp @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ReferenceLinker.h" + +#include "Diagnostics.h" +#include "ResourceTable.h" +#include "ResourceUtils.h" +#include "ResourceValues.h" +#include "ValueVisitor.h" +#include "link/Linkers.h" +#include "process/IResourceTableConsumer.h" +#include "process/SymbolTable.h" +#include "util/Util.h" +#include "xml/XmlUtil.h" + +#include <androidfw/ResourceTypes.h> +#include <cassert> + +namespace aapt { + +namespace { + +/** + * The ReferenceLinkerVisitor will follow all references and make sure they point + * to resources that actually exist, either in the local resource table, or as external + * symbols. Once the target resource has been found, the ID of the resource will be assigned + * to the reference object. + * + * NOTE: All of the entries in the ResourceTable must be assigned IDs. + */ +class ReferenceLinkerVisitor : public ValueVisitor { +private: + IAaptContext* mContext; + ISymbolTable* mSymbols; + xml::IPackageDeclStack* mPackageDecls; + StringPool* mStringPool; + CallSite* mCallSite; + bool mError = false; + + /** + * Transform a RawString value into a more specific, appropriate value, based on the + * Attribute. If a non RawString value is passed in, this is an identity transform. + */ + std::unique_ptr<Item> parseValueWithAttribute(std::unique_ptr<Item> value, + const Attribute* attr) { + if (RawString* rawString = valueCast<RawString>(value.get())) { + std::unique_ptr<Item> transformed = + ResourceUtils::parseItemForAttribute(*rawString->value, attr); + + // If we could not parse as any specific type, try a basic STRING. + if (!transformed && (attr->typeMask & android::ResTable_map::TYPE_STRING)) { + util::StringBuilder stringBuilder; + stringBuilder.append(*rawString->value); + if (stringBuilder) { + transformed = util::make_unique<String>( + mStringPool->makeRef(stringBuilder.str())); + } + } + + if (transformed) { + return transformed; + } + }; + return value; + } + +public: + using ValueVisitor::visit; + + ReferenceLinkerVisitor(IAaptContext* context, ISymbolTable* symbols, StringPool* stringPool, + xml::IPackageDeclStack* decl,CallSite* callSite) : + mContext(context), mSymbols(symbols), mPackageDecls(decl), mStringPool(stringPool), + mCallSite(callSite) { + } + + void visit(Reference* ref) override { + if (!ReferenceLinker::linkReference(ref, mContext, mSymbols, mPackageDecls, mCallSite)) { + mError = true; + } + } + + /** + * We visit the Style specially because during this phase, values of attributes are + * all RawString values. Now that we are expected to resolve all symbols, we can + * lookup the attributes to find out which types are allowed for the attributes' values. + */ + void visit(Style* style) override { + if (style->parent) { + visit(&style->parent.value()); + } + + for (Style::Entry& entry : style->entries) { + std::string errStr; + + // Transform the attribute reference so that it is using the fully qualified package + // name. This will also mark the reference as being able to see private resources if + // there was a '*' in the reference or if the package came from the private namespace. + Reference transformedReference = entry.key; + transformReferenceFromNamespace(mPackageDecls, mContext->getCompilationPackage(), + &transformedReference); + + // Find the attribute in the symbol table and check if it is visible from this callsite. + const ISymbolTable::Symbol* symbol = ReferenceLinker::resolveAttributeCheckVisibility( + transformedReference, mContext->getNameMangler(), mSymbols, mCallSite, &errStr); + if (symbol) { + // Assign our style key the correct ID. + entry.key.id = symbol->id; + + // Try to convert the value to a more specific, typed value based on the + // attribute it is set to. + entry.value = parseValueWithAttribute(std::move(entry.value), + symbol->attribute.get()); + + // Link/resolve the final value (mostly if it's a reference). + entry.value->accept(this); + + // Now verify that the type of this item is compatible with the attribute it + // is defined for. We pass `nullptr` as the DiagMessage so that this check is + // fast and we avoid creating a DiagMessage when the match is successful. + if (!symbol->attribute->matches(entry.value.get(), nullptr)) { + // The actual type of this item is incompatible with the attribute. + DiagMessage msg(entry.key.getSource()); + + // Call the matches method again, this time with a DiagMessage so we fill + // in the actual error message. + symbol->attribute->matches(entry.value.get(), &msg); + mContext->getDiagnostics()->error(msg); + mError = true; + } + + } else { + DiagMessage msg(entry.key.getSource()); + msg << "style attribute '"; + ReferenceLinker::writeResourceName(&msg, entry.key, transformedReference); + msg << "' " << errStr; + mContext->getDiagnostics()->error(msg); + mError = true; + } + } + } + + bool hasError() { + return mError; + } +}; + +} // namespace + +/** + * The symbol is visible if it is public, or if the reference to it is requesting private access + * or if the callsite comes from the same package. + */ +bool ReferenceLinker::isSymbolVisible(const ISymbolTable::Symbol& symbol, const Reference& ref, + const CallSite& callSite) { + if (!symbol.isPublic && !ref.privateReference) { + if (ref.name) { + return callSite.resource.package == ref.name.value().package; + } else if (ref.id) { + return ref.id.value().packageId() == symbol.id.packageId(); + } else { + return false; + } + } + return true; +} + +const ISymbolTable::Symbol* ReferenceLinker::resolveSymbol(const Reference& reference, + NameMangler* mangler, + ISymbolTable* symbols) { + if (reference.name) { + Maybe<ResourceName> mangled = mangler->mangleName(reference.name.value()); + return symbols->findByName(mangled ? mangled.value() : reference.name.value()); + } else if (reference.id) { + return symbols->findById(reference.id.value()); + } else { + return nullptr; + } +} + +const ISymbolTable::Symbol* ReferenceLinker::resolveSymbolCheckVisibility( + const Reference& reference, NameMangler* nameMangler, ISymbolTable* symbols, + CallSite* callSite, std::string* outError) { + const ISymbolTable::Symbol* symbol = resolveSymbol(reference, nameMangler, symbols); + if (!symbol) { + if (outError) *outError = "not found"; + return nullptr; + } + + if (!isSymbolVisible(*symbol, reference, *callSite)) { + if (outError) *outError = "is private"; + return nullptr; + } + return symbol; +} + +const ISymbolTable::Symbol* ReferenceLinker::resolveAttributeCheckVisibility( + const Reference& reference, NameMangler* nameMangler, ISymbolTable* symbols, + CallSite* callSite, std::string* outError) { + const ISymbolTable::Symbol* symbol = resolveSymbolCheckVisibility(reference, nameMangler, + symbols, callSite, + outError); + if (!symbol) { + return nullptr; + } + + if (!symbol->attribute) { + if (outError) *outError = "is not an attribute"; + return nullptr; + } + return symbol; +} + +Maybe<xml::AaptAttribute> ReferenceLinker::compileXmlAttribute(const Reference& reference, + NameMangler* nameMangler, + ISymbolTable* symbols, + CallSite* callSite, + std::string* outError) { + const ISymbolTable::Symbol* symbol = resolveSymbol(reference, nameMangler, symbols); + if (!symbol) { + return {}; + } + + if (!symbol->attribute) { + if (outError) *outError = "is not an attribute"; + return {}; + } + return xml::AaptAttribute{ symbol->id, *symbol->attribute }; +} + +void ReferenceLinker::writeResourceName(DiagMessage* outMsg, const Reference& orig, + const Reference& transformed) { + assert(outMsg); + + if (orig.name) { + *outMsg << orig.name.value(); + if (transformed.name.value() != orig.name.value()) { + *outMsg << " (aka " << transformed.name.value() << ")"; + } + } else { + *outMsg << orig.id.value(); + } +} + +bool ReferenceLinker::linkReference(Reference* reference, IAaptContext* context, + ISymbolTable* symbols, xml::IPackageDeclStack* decls, + CallSite* callSite) { + assert(reference); + assert(reference->name || reference->id); + + Reference transformedReference = *reference; + transformReferenceFromNamespace(decls, context->getCompilationPackage(), + &transformedReference); + + std::string errStr; + const ISymbolTable::Symbol* s = resolveSymbolCheckVisibility( + transformedReference, context->getNameMangler(), symbols, callSite, &errStr); + if (s) { + reference->id = s->id; + return true; + } + + DiagMessage errorMsg(reference->getSource()); + errorMsg << "resource "; + writeResourceName(&errorMsg, *reference, transformedReference); + errorMsg << " " << errStr; + context->getDiagnostics()->error(errorMsg); + return false; +} + +namespace { + +struct EmptyDeclStack : public xml::IPackageDeclStack { + Maybe<xml::ExtractedPackage> transformPackageAlias( + const StringPiece16& alias, const StringPiece16& localPackage) const override { + if (alias.empty()) { + return xml::ExtractedPackage{ localPackage.toString(), true /* private */ }; + } + return {}; + } +}; + +} // namespace + +bool ReferenceLinker::consume(IAaptContext* context, ResourceTable* table) { + EmptyDeclStack declStack; + bool error = false; + for (auto& package : table->packages) { + for (auto& type : package->types) { + for (auto& entry : type->entries) { + // Symbol state information may be lost if there is no value for the resource. + if (entry->symbolStatus.state != SymbolState::kUndefined && entry->values.empty()) { + context->getDiagnostics()->error( + DiagMessage(entry->symbolStatus.source) + << "no definition for declared symbol '" + << ResourceNameRef(package->name, type->type, entry->name) + << "'"); + error = true; + } + + CallSite callSite = { ResourceNameRef(package->name, type->type, entry->name) }; + ReferenceLinkerVisitor visitor(context, context->getExternalSymbols(), + &table->stringPool, &declStack, &callSite); + + for (auto& configValue : entry->values) { + configValue.value->accept(&visitor); + } + + if (visitor.hasError()) { + error = true; + } + } + } + } + return !error; +} + +} // namespace aapt diff --git a/tools/aapt2/link/ReferenceLinker.h b/tools/aapt2/link/ReferenceLinker.h new file mode 100644 index 000000000000..a0eb00c85b70 --- /dev/null +++ b/tools/aapt2/link/ReferenceLinker.h @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_LINKER_REFERENCELINKER_H +#define AAPT_LINKER_REFERENCELINKER_H + +#include "Resource.h" +#include "ResourceValues.h" +#include "ValueVisitor.h" +#include "link/Linkers.h" +#include "process/IResourceTableConsumer.h" +#include "process/SymbolTable.h" +#include "xml/XmlDom.h" + +#include <cassert> + +namespace aapt { + +/** + * Resolves all references to resources in the ResourceTable and assigns them IDs. + * The ResourceTable must already have IDs assigned to each resource. + * Once the ResourceTable is processed by this linker, it is ready to be flattened. + */ +struct ReferenceLinker : public IResourceTableConsumer { + /** + * Returns true if the symbol is visible by the reference and from the callsite. + */ + static bool isSymbolVisible(const ISymbolTable::Symbol& symbol, const Reference& ref, + const CallSite& callSite); + + /** + * Performs name mangling and looks up the resource in the symbol table. Returns nullptr + * if the symbol was not found. + */ + static const ISymbolTable::Symbol* resolveSymbol(const Reference& reference, + NameMangler* mangler, ISymbolTable* symbols); + + /** + * Performs name mangling and looks up the resource in the symbol table. If the symbol is + * not visible by the reference at the callsite, nullptr is returned. outError holds + * the error message. + */ + static const ISymbolTable::Symbol* resolveSymbolCheckVisibility(const Reference& reference, + NameMangler* nameMangler, + ISymbolTable* symbols, + CallSite* callSite, + std::string* outError); + + /** + * Same as resolveSymbolCheckVisibility(), but also makes sure the symbol is an attribute. + * That is, the return value will have a non-null value for ISymbolTable::Symbol::attribute. + */ + static const ISymbolTable::Symbol* resolveAttributeCheckVisibility(const Reference& reference, + NameMangler* nameMangler, + ISymbolTable* symbols, + CallSite* callSite, + std::string* outError); + + /** + * Resolves the attribute reference and returns an xml::AaptAttribute if successful. + * If resolution fails, outError holds the error message. + */ + static Maybe<xml::AaptAttribute> compileXmlAttribute(const Reference& reference, + NameMangler* nameMangler, + ISymbolTable* symbols, + CallSite* callSite, + std::string* outError); + + /** + * Writes the resource name to the DiagMessage, using the "orig_name (aka <transformed_name>)" + * syntax. + */ + static void writeResourceName(DiagMessage* outMsg, const Reference& orig, + const Reference& transformed); + + /** + * Transforms the package name of the reference to the fully qualified package name using + * the xml::IPackageDeclStack, then mangles and looks up the symbol. If the symbol is visible + * to the reference at the callsite, the reference is updated with an ID. + * Returns false on failure, and an error message is logged to the IDiagnostics in the context. + */ + static bool linkReference(Reference* reference, IAaptContext* context, ISymbolTable* symbols, + xml::IPackageDeclStack* decls, CallSite* callSite); + + /** + * Links all references in the ResourceTable. + */ + bool consume(IAaptContext* context, ResourceTable* table) override; +}; + +} // namespace aapt + +#endif /* AAPT_LINKER_REFERENCELINKER_H */ diff --git a/tools/aapt2/link/ReferenceLinker_test.cpp b/tools/aapt2/link/ReferenceLinker_test.cpp new file mode 100644 index 000000000000..8d324fe753a1 --- /dev/null +++ b/tools/aapt2/link/ReferenceLinker_test.cpp @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "link/ReferenceLinker.h" +#include "process/SymbolTable.h" + +#include "test/Builders.h" +#include "test/Context.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(ReferenceLinkerTest, LinkSimpleReferences) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"com.app.test", 0x7f) + .addReference(u"@com.app.test:string/foo", ResourceId(0x7f020000), + u"@com.app.test:string/bar") + + // Test use of local reference (w/o package name). + .addReference(u"@com.app.test:string/bar", ResourceId(0x7f020001), u"@string/baz") + + .addReference(u"@com.app.test:string/baz", ResourceId(0x7f020002), + u"@android:string/ok") + .build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .setCompilationPackage(u"com.app.test") + .setPackageId(0x7f) + .setNameManglerPolicy(NameManglerPolicy{ u"com.app.test" }) + .setSymbolTable(JoinedSymbolTableBuilder() + .addSymbolTable(util::make_unique<SymbolTableWrapper>(table.get())) + .addSymbolTable(test::StaticSymbolTableBuilder() + .addPublicSymbol(u"@android:string/ok", ResourceId(0x01040034)) + .build()) + .build()) + .build(); + + ReferenceLinker linker; + ASSERT_TRUE(linker.consume(context.get(), table.get())); + + Reference* ref = test::getValue<Reference>(table.get(), u"@com.app.test:string/foo"); + ASSERT_NE(ref, nullptr); + AAPT_ASSERT_TRUE(ref->id); + EXPECT_EQ(ref->id.value(), ResourceId(0x7f020001)); + + ref = test::getValue<Reference>(table.get(), u"@com.app.test:string/bar"); + ASSERT_NE(ref, nullptr); + AAPT_ASSERT_TRUE(ref->id); + EXPECT_EQ(ref->id.value(), ResourceId(0x7f020002)); + + ref = test::getValue<Reference>(table.get(), u"@com.app.test:string/baz"); + ASSERT_NE(ref, nullptr); + AAPT_ASSERT_TRUE(ref->id); + EXPECT_EQ(ref->id.value(), ResourceId(0x01040034)); +} + +TEST(ReferenceLinkerTest, LinkStyleAttributes) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"com.app.test", 0x7f) + .addValue(u"@com.app.test:style/Theme", test::StyleBuilder() + .setParent(u"@android:style/Theme.Material") + .addItem(u"@android:attr/foo", ResourceUtils::tryParseColor(u"#ff00ff")) + .addItem(u"@android:attr/bar", {} /* placeholder */) + .build()) + .build(); + + { + // We need to fill in the value for the attribute android:attr/bar after we build the + // table, because we need access to the string pool. + Style* style = test::getValue<Style>(table.get(), u"@com.app.test:style/Theme"); + ASSERT_NE(style, nullptr); + style->entries.back().value = util::make_unique<RawString>( + table->stringPool.makeRef(u"one|two")); + } + + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .setCompilationPackage(u"com.app.test") + .setPackageId(0x7f) + .setNameManglerPolicy(NameManglerPolicy{ u"com.app.test" }) + .setSymbolTable(test::StaticSymbolTableBuilder() + .addPublicSymbol(u"@android:style/Theme.Material", ResourceId(0x01060000)) + .addPublicSymbol(u"@android:attr/foo", ResourceId(0x01010001), + test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_COLOR) + .build()) + .addPublicSymbol(u"@android:attr/bar", ResourceId(0x01010002), + test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_FLAGS) + .addItem(u"one", 0x01) + .addItem(u"two", 0x02) + .build()) + .build()) + .build(); + + ReferenceLinker linker; + ASSERT_TRUE(linker.consume(context.get(), table.get())); + + Style* style = test::getValue<Style>(table.get(), u"@com.app.test:style/Theme"); + ASSERT_NE(style, nullptr); + AAPT_ASSERT_TRUE(style->parent); + AAPT_ASSERT_TRUE(style->parent.value().id); + EXPECT_EQ(style->parent.value().id.value(), ResourceId(0x01060000)); + + ASSERT_EQ(2u, style->entries.size()); + + AAPT_ASSERT_TRUE(style->entries[0].key.id); + EXPECT_EQ(style->entries[0].key.id.value(), ResourceId(0x01010001)); + ASSERT_NE(valueCast<BinaryPrimitive>(style->entries[0].value.get()), nullptr); + + AAPT_ASSERT_TRUE(style->entries[1].key.id); + EXPECT_EQ(style->entries[1].key.id.value(), ResourceId(0x01010002)); + ASSERT_NE(valueCast<BinaryPrimitive>(style->entries[1].value.get()), nullptr); +} + +TEST(ReferenceLinkerTest, LinkMangledReferencesAndAttributes) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .setCompilationPackage(u"com.app.test") + .setPackageId(0x7f) + .setNameManglerPolicy(NameManglerPolicy{ u"com.app.test", { u"com.android.support" } }) + .setSymbolTable(test::StaticSymbolTableBuilder() + .addPublicSymbol(u"@com.app.test:attr/com.android.support$foo", + ResourceId(0x7f010000), test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_COLOR).build()) + .build()) + .build(); + + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"com.app.test", 0x7f) + .addValue(u"@com.app.test:style/Theme", ResourceId(0x7f020000), + test::StyleBuilder().addItem(u"@com.android.support:attr/foo", + ResourceUtils::tryParseColor(u"#ff0000")) + .build()) + .build(); + + ReferenceLinker linker; + ASSERT_TRUE(linker.consume(context.get(), table.get())); + + Style* style = test::getValue<Style>(table.get(), u"@com.app.test:style/Theme"); + ASSERT_NE(style, nullptr); + ASSERT_EQ(1u, style->entries.size()); + AAPT_ASSERT_TRUE(style->entries.front().key.id); + EXPECT_EQ(style->entries.front().key.id.value(), ResourceId(0x7f010000)); +} + +TEST(ReferenceLinkerTest, FailToLinkPrivateSymbols) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"com.app.test", 0x7f) + .addReference(u"@com.app.test:string/foo", ResourceId(0x7f020000), + u"@android:string/hidden") + .build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .setCompilationPackage(u"com.app.test") + .setPackageId(0x7f) + .setNameManglerPolicy(NameManglerPolicy{ u"com.app.test" }) + .setSymbolTable(JoinedSymbolTableBuilder() + .addSymbolTable(util::make_unique<SymbolTableWrapper>(table.get())) + .addSymbolTable(test::StaticSymbolTableBuilder() + .addSymbol(u"@android:string/hidden", ResourceId(0x01040034)) + .build()) + .build()) + .build(); + + ReferenceLinker linker; + ASSERT_FALSE(linker.consume(context.get(), table.get())); +} + +TEST(ReferenceLinkerTest, FailToLinkPrivateMangledSymbols) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"com.app.test", 0x7f) + .addReference(u"@com.app.test:string/foo", ResourceId(0x7f020000), + u"@com.app.lib:string/hidden") + .build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .setCompilationPackage(u"com.app.test") + .setPackageId(0x7f) + .setNameManglerPolicy(NameManglerPolicy{ u"com.app.test", { u"com.app.lib" } }) + .setSymbolTable(JoinedSymbolTableBuilder() + .addSymbolTable(util::make_unique<SymbolTableWrapper>(table.get())) + .addSymbolTable(test::StaticSymbolTableBuilder() + .addSymbol(u"@com.app.test:string/com.app.lib$hidden", + ResourceId(0x7f040034)) + .build()) + .build()) + .build(); + + ReferenceLinker linker; + ASSERT_FALSE(linker.consume(context.get(), table.get())); +} + +TEST(ReferenceLinkerTest, FailToLinkPrivateStyleAttributes) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .setPackageId(u"com.app.test", 0x7f) + .addValue(u"@com.app.test:style/Theme", test::StyleBuilder() + .addItem(u"@android:attr/hidden", ResourceUtils::tryParseColor(u"#ff00ff")) + .build()) + .build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder() + .setCompilationPackage(u"com.app.test") + .setPackageId(0x7f) + .setNameManglerPolicy(NameManglerPolicy{ u"com.app.test" }) + .setSymbolTable(JoinedSymbolTableBuilder() + .addSymbolTable(util::make_unique<SymbolTableWrapper>(table.get())) + .addSymbolTable(test::StaticSymbolTableBuilder() + .addSymbol(u"@android:attr/hidden", ResourceId(0x01010001), + test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_COLOR) + .build()) + .build()) + .build()) + .build(); + + ReferenceLinker linker; + ASSERT_FALSE(linker.consume(context.get(), table.get())); +} + +} // namespace aapt diff --git a/tools/aapt2/link/TableMerger.cpp b/tools/aapt2/link/TableMerger.cpp new file mode 100644 index 000000000000..e01a00401133 --- /dev/null +++ b/tools/aapt2/link/TableMerger.cpp @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ResourceTable.h" +#include "ResourceUtils.h" +#include "ResourceValues.h" +#include "ValueVisitor.h" + +#include "link/TableMerger.h" +#include "util/Comparators.h" +#include "util/Util.h" + +#include <cassert> + +namespace aapt { + +TableMerger::TableMerger(IAaptContext* context, ResourceTable* outTable, + const TableMergerOptions& options) : + mContext(context), mMasterTable(outTable), mOptions(options) { + // Create the desired package that all tables will be merged into. + mMasterPackage = mMasterTable->createPackage( + mContext->getCompilationPackage(), mContext->getPackageId()); + assert(mMasterPackage && "package name or ID already taken"); +} + +/** + * This will merge packages with the same package name (or no package name). + */ +bool TableMerger::mergeImpl(const Source& src, ResourceTable* table, + bool overlay, bool allowNew) { + const uint8_t desiredPackageId = mContext->getPackageId(); + + bool error = false; + for (auto& package : table->packages) { + // Warn of packages with an unrelated ID. + if (package->id && package->id.value() != 0x0 && package->id.value() != desiredPackageId) { + mContext->getDiagnostics()->warn(DiagMessage(src) + << "ignoring package " << package->name); + continue; + } + + if (package->name.empty() || mContext->getCompilationPackage() == package->name) { + // Merge here. Once the entries are merged and mangled, any references to + // them are still valid. This is because un-mangled references are + // mangled, then looked up at resolution time. + // Also, when linking, we convert references with no package name to use + // the compilation package name. + error |= !doMerge(src, table, package.get(), + false /* mangle */, overlay, allowNew, {}); + } + } + return !error; +} + +bool TableMerger::merge(const Source& src, ResourceTable* table) { + return mergeImpl(src, table, false /* overlay */, true /* allow new */); +} + +bool TableMerger::mergeOverlay(const Source& src, ResourceTable* table) { + return mergeImpl(src, table, true /* overlay */, mOptions.autoAddOverlay); +} + +/** + * This will merge and mangle resources from a static library. + */ +bool TableMerger::mergeAndMangle(const Source& src, const StringPiece16& packageName, + ResourceTable* table, io::IFileCollection* collection) { + bool error = false; + for (auto& package : table->packages) { + // Warn of packages with an unrelated ID. + if (packageName != package->name) { + mContext->getDiagnostics()->warn(DiagMessage(src) + << "ignoring package " << package->name); + continue; + } + + bool mangle = packageName != mContext->getCompilationPackage(); + mMergedPackages.insert(package->name); + + auto callback = [&](const ResourceNameRef& name, const ConfigDescription& config, + FileReference* newFile, FileReference* oldFile) -> bool { + // The old file's path points inside the APK, so we can use it as is. + io::IFile* f = collection->findFile(util::utf16ToUtf8(*oldFile->path)); + if (!f) { + mContext->getDiagnostics()->error(DiagMessage(src) << "file '" << *oldFile->path + << "' not found"); + return false; + } + + mFilesToMerge[ResourceKeyRef(name, config)] = FileToMerge{ + f, oldFile->getSource(), util::utf16ToUtf8(*newFile->path) }; + return true; + }; + + error |= !doMerge(src, table, package.get(), + mangle, false /* overlay */, true /* allow new */, callback); + } + return !error; +} + +bool TableMerger::doMerge(const Source& src, + ResourceTable* srcTable, + ResourceTablePackage* srcPackage, + const bool manglePackage, + const bool overlay, + const bool allowNewResources, + FileMergeCallback callback) { + bool error = false; + + for (auto& srcType : srcPackage->types) { + ResourceTableType* dstType = mMasterPackage->findOrCreateType(srcType->type); + if (srcType->symbolStatus.state == SymbolState::kPublic) { + if (dstType->symbolStatus.state == SymbolState::kPublic && dstType->id && srcType->id + && dstType->id.value() == srcType->id.value()) { + // Both types are public and have different IDs. + mContext->getDiagnostics()->error(DiagMessage(src) + << "can not merge type '" + << srcType->type + << "': conflicting public IDs"); + error = true; + continue; + } + + dstType->symbolStatus = std::move(srcType->symbolStatus); + dstType->id = srcType->id; + } + + for (auto& srcEntry : srcType->entries) { + ResourceEntry* dstEntry; + if (manglePackage) { + std::u16string mangledName = NameMangler::mangleEntry(srcPackage->name, + srcEntry->name); + if (allowNewResources) { + dstEntry = dstType->findOrCreateEntry(mangledName); + } else { + dstEntry = dstType->findEntry(mangledName); + } + } else { + if (allowNewResources) { + dstEntry = dstType->findOrCreateEntry(srcEntry->name); + } else { + dstEntry = dstType->findEntry(srcEntry->name); + } + } + + if (!dstEntry) { + mContext->getDiagnostics()->error(DiagMessage(src) + << "resource " + << ResourceNameRef(srcPackage->name, + srcType->type, + srcEntry->name) + << " does not override an existing resource"); + mContext->getDiagnostics()->note(DiagMessage(src) + << "define an <add-resource> tag or use " + "--auto-add-overlay"); + error = true; + continue; + } + + if (srcEntry->symbolStatus.state != SymbolState::kUndefined) { + if (srcEntry->symbolStatus.state == SymbolState::kPublic) { + if (dstEntry->symbolStatus.state == SymbolState::kPublic && + dstEntry->id && srcEntry->id && + dstEntry->id.value() != srcEntry->id.value()) { + // Both entries are public and have different IDs. + mContext->getDiagnostics()->error(DiagMessage(src) + << "can not merge entry '" + << srcEntry->name + << "': conflicting public IDs"); + error = true; + continue; + } + + if (srcEntry->id) { + dstEntry->id = srcEntry->id; + } + } + + if (dstEntry->symbolStatus.state != SymbolState::kPublic && + dstEntry->symbolStatus.state != srcEntry->symbolStatus.state) { + dstEntry->symbolStatus = std::move(srcEntry->symbolStatus); + } + } + + ResourceNameRef resName(mMasterPackage->name, dstType->type, dstEntry->name); + + for (ResourceConfigValue& srcValue : srcEntry->values) { + auto iter = std::lower_bound(dstEntry->values.begin(), dstEntry->values.end(), + srcValue.config, cmp::lessThanConfig); + + const bool stripConfig = mOptions.filter ? + !mOptions.filter->match(srcValue.config) : false; + + if (iter != dstEntry->values.end() && iter->config == srcValue.config) { + const int collisionResult = ResourceTable::resolveValueCollision( + iter->value.get(), srcValue.value.get()); + if (collisionResult == 0 && !overlay) { + // Error! + ResourceNameRef resourceName(srcPackage->name, + srcType->type, + srcEntry->name); + + mContext->getDiagnostics()->error(DiagMessage(srcValue.value->getSource()) + << "resource '" << resourceName + << "' has a conflicting value for " + << "configuration (" + << srcValue.config << ")"); + mContext->getDiagnostics()->note(DiagMessage(iter->value->getSource()) + << "originally defined here"); + error = true; + continue; + } else if (collisionResult < 0) { + // Keep our existing value. + continue; + } + + } else if (!stripConfig){ + // Insert a place holder value. We will fill it in below. + iter = dstEntry->values.insert(iter, ResourceConfigValue{ srcValue.config }); + } + + if (stripConfig) { + continue; + } + + if (FileReference* f = valueCast<FileReference>(srcValue.value.get())) { + std::unique_ptr<FileReference> newFileRef; + if (manglePackage) { + newFileRef = cloneAndMangleFile(srcPackage->name, *f); + } else { + newFileRef = std::unique_ptr<FileReference>(f->clone( + &mMasterTable->stringPool)); + } + + if (callback) { + if (!callback(resName, iter->config, newFileRef.get(), f)) { + error = true; + continue; + } + } + iter->value = std::move(newFileRef); + + } else { + iter->value = std::unique_ptr<Value>(srcValue.value->clone( + &mMasterTable->stringPool)); + } + } + } + } + return !error; +} + +std::unique_ptr<FileReference> TableMerger::cloneAndMangleFile(const std::u16string& package, + const FileReference& fileRef) { + + StringPiece16 prefix, entry, suffix; + if (util::extractResFilePathParts(*fileRef.path, &prefix, &entry, &suffix)) { + std::u16string mangledEntry = NameMangler::mangleEntry(package, entry.toString()); + std::u16string newPath = prefix.toString() + mangledEntry + suffix.toString(); + std::unique_ptr<FileReference> newFileRef = util::make_unique<FileReference>( + mMasterTable->stringPool.makeRef(newPath)); + newFileRef->setComment(fileRef.getComment()); + newFileRef->setSource(fileRef.getSource()); + return newFileRef; + } + return std::unique_ptr<FileReference>(fileRef.clone(&mMasterTable->stringPool)); +} + +bool TableMerger::mergeFileImpl(const ResourceFile& fileDesc, io::IFile* file, bool overlay) { + ResourceTable table; + std::u16string path = util::utf8ToUtf16(ResourceUtils::buildResourceFileName(fileDesc, + nullptr)); + std::unique_ptr<FileReference> fileRef = util::make_unique<FileReference>( + table.stringPool.makeRef(path)); + fileRef->setSource(fileDesc.source); + + ResourceTablePackage* pkg = table.createPackage(fileDesc.name.package, 0x0); + pkg->findOrCreateType(fileDesc.name.type) + ->findOrCreateEntry(fileDesc.name.entry) + ->values.push_back(ResourceConfigValue{ fileDesc.config, std::move(fileRef) }); + + auto callback = [&](const ResourceNameRef& name, const ConfigDescription& config, + FileReference* newFile, FileReference* oldFile) -> bool { + mFilesToMerge[ResourceKeyRef(name, config)] = FileToMerge{ + file, oldFile->getSource(), util::utf16ToUtf8(*newFile->path) }; + return true; + }; + + return doMerge(file->getSource(), &table, pkg, + false /* mangle */, overlay /* overlay */, true /* allow new */, callback); +} + +bool TableMerger::mergeFile(const ResourceFile& fileDesc, io::IFile* file) { + return mergeFileImpl(fileDesc, file, false /* overlay */); +} + +bool TableMerger::mergeFileOverlay(const ResourceFile& fileDesc, io::IFile* file) { + return mergeFileImpl(fileDesc, file, true /* overlay */); +} + +} // namespace aapt diff --git a/tools/aapt2/link/TableMerger.h b/tools/aapt2/link/TableMerger.h new file mode 100644 index 000000000000..4539679fa769 --- /dev/null +++ b/tools/aapt2/link/TableMerger.h @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_TABLEMERGER_H +#define AAPT_TABLEMERGER_H + +#include "Resource.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "filter/ConfigFilter.h" +#include "io/File.h" +#include "process/IResourceTableConsumer.h" +#include "util/Util.h" + +#include <functional> +#include <map> + +namespace aapt { + +struct FileToMerge { + /** + * The compiled file from which to read the data. + */ + io::IFile* file; + + /** + * Where the original, uncompiled file came from. + */ + Source originalSource; + + /** + * The destination path within the APK/archive. + */ + std::string dstPath; +}; + +struct TableMergerOptions { + /** + * If true, resources in overlays can be added without previously having existed. + */ + bool autoAddOverlay = false; + + /** + * A filter that removes resources whose configurations don't match. + */ + IConfigFilter* filter = nullptr; +}; + +/** + * TableMerger takes resource tables and merges all packages within the tables that have the same + * package ID. + * + * If a package has a different name, all the entries in that table have their names mangled + * to include the package name. This way there are no collisions. In order to do this correctly, + * the TableMerger needs to also mangle any FileReference paths. Once these are mangled, + * the original source path of the file, along with the new destination path is recorded in the + * queue returned from getFileMergeQueue(). + * + * Once the merging is complete, a separate process can go collect the files from the various + * source APKs and either copy or process their XML and put them in the correct location in + * the final APK. + */ +class TableMerger { +public: + /** + * Note: The outTable ResourceTable must live longer than this TableMerger. References + * are made to this ResourceTable for efficiency reasons. + */ + TableMerger(IAaptContext* context, ResourceTable* outTable, const TableMergerOptions& options); + + const std::map<ResourceKeyRef, FileToMerge>& getFilesToMerge() { + return mFilesToMerge; + } + + const std::set<std::u16string>& getMergedPackages() const { + return mMergedPackages; + } + + /** + * Merges resources from the same or empty package. This is for local sources. + */ + bool merge(const Source& src, ResourceTable* table); + + /** + * Merges resources from an overlay ResourceTable. + */ + bool mergeOverlay(const Source& src, ResourceTable* table); + + /** + * Merges resources from the given package, mangling the name. This is for static libraries. + * An io::IFileCollection is needed in order to find the referenced Files and process them. + */ + bool mergeAndMangle(const Source& src, const StringPiece16& package, ResourceTable* table, + io::IFileCollection* collection); + + /** + * Merges a compiled file that belongs to this same or empty package. This is for local sources. + */ + bool mergeFile(const ResourceFile& fileDesc, io::IFile* file); + + /** + * Merges a compiled file from an overlay, overriding an existing definition. + */ + bool mergeFileOverlay(const ResourceFile& fileDesc, io::IFile* file); + +private: + using FileMergeCallback = std::function<bool(const ResourceNameRef&, + const ConfigDescription& config, + FileReference*, + FileReference*)>; + + IAaptContext* mContext; + ResourceTable* mMasterTable; + TableMergerOptions mOptions; + ResourceTablePackage* mMasterPackage; + + std::set<std::u16string> mMergedPackages; + std::map<ResourceKeyRef, FileToMerge> mFilesToMerge; + + bool mergeFileImpl(const ResourceFile& fileDesc, io::IFile* file, bool overlay); + + bool mergeImpl(const Source& src, ResourceTable* srcTable, + bool overlay, bool allowNew); + + bool doMerge(const Source& src, ResourceTable* srcTable, ResourceTablePackage* srcPackage, + const bool manglePackage, + const bool overlay, + const bool allowNewResources, + FileMergeCallback callback); + + std::unique_ptr<FileReference> cloneAndMangleFile(const std::u16string& package, + const FileReference& value); +}; + +} // namespace aapt + +#endif /* AAPT_TABLEMERGER_H */ diff --git a/tools/aapt2/link/TableMerger_test.cpp b/tools/aapt2/link/TableMerger_test.cpp new file mode 100644 index 000000000000..45c8c98780b8 --- /dev/null +++ b/tools/aapt2/link/TableMerger_test.cpp @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "filter/ConfigFilter.h" +#include "io/FileSystem.h" +#include "link/TableMerger.h" +#include "test/Builders.h" +#include "test/Context.h" + +#include <gtest/gtest.h> + +namespace aapt { + +struct TableMergerTest : public ::testing::Test { + std::unique_ptr<IAaptContext> mContext; + + void SetUp() override { + mContext = test::ContextBuilder() + // We are compiling this package. + .setCompilationPackage(u"com.app.a") + + // Merge all packages that have this package ID. + .setPackageId(0x7f) + + // Mangle all packages that do not have this package name. + .setNameManglerPolicy(NameManglerPolicy{ u"com.app.a", { u"com.app.b" } }) + + .build(); + } +}; + +TEST_F(TableMergerTest, SimpleMerge) { + std::unique_ptr<ResourceTable> tableA = test::ResourceTableBuilder() + .setPackageId(u"com.app.a", 0x7f) + .addReference(u"@com.app.a:id/foo", u"@com.app.a:id/bar") + .addReference(u"@com.app.a:id/bar", u"@com.app.b:id/foo") + .addValue(u"@com.app.a:styleable/view", test::StyleableBuilder() + .addItem(u"@com.app.b:id/foo") + .build()) + .build(); + + std::unique_ptr<ResourceTable> tableB = test::ResourceTableBuilder() + .setPackageId(u"com.app.b", 0x7f) + .addSimple(u"@com.app.b:id/foo") + .build(); + + ResourceTable finalTable; + TableMerger merger(mContext.get(), &finalTable, TableMergerOptions{}); + io::FileCollection collection; + + ASSERT_TRUE(merger.merge({}, tableA.get())); + ASSERT_TRUE(merger.mergeAndMangle({}, u"com.app.b", tableB.get(), &collection)); + + EXPECT_TRUE(merger.getMergedPackages().count(u"com.app.b") != 0); + + // Entries from com.app.a should not be mangled. + AAPT_EXPECT_TRUE(finalTable.findResource(test::parseNameOrDie(u"@com.app.a:id/foo"))); + AAPT_EXPECT_TRUE(finalTable.findResource(test::parseNameOrDie(u"@com.app.a:id/bar"))); + AAPT_EXPECT_TRUE(finalTable.findResource(test::parseNameOrDie(u"@com.app.a:styleable/view"))); + + // The unmangled name should not be present. + AAPT_EXPECT_FALSE(finalTable.findResource(test::parseNameOrDie(u"@com.app.b:id/foo"))); + + // Look for the mangled name. + AAPT_EXPECT_TRUE(finalTable.findResource(test::parseNameOrDie(u"@com.app.a:id/com.app.b$foo"))); +} + +TEST_F(TableMergerTest, MergeFile) { + ResourceTable finalTable; + TableMergerOptions options; + options.autoAddOverlay = false; + TableMerger merger(mContext.get(), &finalTable, options); + + ResourceFile fileDesc; + fileDesc.config = test::parseConfigOrDie("hdpi-v4"); + fileDesc.name = test::parseNameOrDie(u"@layout/main"); + fileDesc.source = Source("res/layout-hdpi/main.xml"); + test::TestFile testFile("path/to/res/layout-hdpi/main.xml.flat"); + + ASSERT_TRUE(merger.mergeFile(fileDesc, &testFile)); + + FileReference* file = test::getValueForConfig<FileReference>(&finalTable, + u"@com.app.a:layout/main", + test::parseConfigOrDie("hdpi-v4")); + ASSERT_NE(nullptr, file); + EXPECT_EQ(std::u16string(u"res/layout-hdpi-v4/main.xml"), *file->path); + + ResourceName name = test::parseNameOrDie(u"@com.app.a:layout/main"); + ResourceKeyRef key = { name, test::parseConfigOrDie("hdpi-v4") }; + + auto iter = merger.getFilesToMerge().find(key); + ASSERT_NE(merger.getFilesToMerge().end(), iter); + + const FileToMerge& actualFileToMerge = iter->second; + EXPECT_EQ(&testFile, actualFileToMerge.file); + EXPECT_EQ(std::string("res/layout-hdpi-v4/main.xml"), actualFileToMerge.dstPath); +} + +TEST_F(TableMergerTest, MergeFileOverlay) { + ResourceTable finalTable; + TableMergerOptions tableMergerOptions; + tableMergerOptions.autoAddOverlay = false; + TableMerger merger(mContext.get(), &finalTable, tableMergerOptions); + + ResourceFile fileDesc; + fileDesc.name = test::parseNameOrDie(u"@xml/foo"); + test::TestFile fileA("path/to/fileA.xml.flat"); + test::TestFile fileB("path/to/fileB.xml.flat"); + + ASSERT_TRUE(merger.mergeFile(fileDesc, &fileA)); + ASSERT_TRUE(merger.mergeFileOverlay(fileDesc, &fileB)); + + ResourceName name = test::parseNameOrDie(u"@com.app.a:xml/foo"); + ResourceKeyRef key = { name, ConfigDescription{} }; + auto iter = merger.getFilesToMerge().find(key); + ASSERT_NE(merger.getFilesToMerge().end(), iter); + + const FileToMerge& actualFileToMerge = iter->second; + EXPECT_EQ(&fileB, actualFileToMerge.file); +} + +TEST_F(TableMergerTest, MergeFileReferences) { + std::unique_ptr<ResourceTable> tableA = test::ResourceTableBuilder() + .setPackageId(u"com.app.a", 0x7f) + .addFileReference(u"@com.app.a:xml/file", u"res/xml/file.xml") + .build(); + std::unique_ptr<ResourceTable> tableB = test::ResourceTableBuilder() + .setPackageId(u"com.app.b", 0x7f) + .addFileReference(u"@com.app.b:xml/file", u"res/xml/file.xml") + .build(); + + ResourceTable finalTable; + TableMerger merger(mContext.get(), &finalTable, TableMergerOptions{}); + io::FileCollection collection; + collection.insertFile("res/xml/file.xml"); + + ASSERT_TRUE(merger.merge({}, tableA.get())); + ASSERT_TRUE(merger.mergeAndMangle({}, u"com.app.b", tableB.get(), &collection)); + + FileReference* f = test::getValue<FileReference>(&finalTable, u"@com.app.a:xml/file"); + ASSERT_NE(f, nullptr); + EXPECT_EQ(std::u16string(u"res/xml/file.xml"), *f->path); + + f = test::getValue<FileReference>(&finalTable, u"@com.app.a:xml/com.app.b$file"); + ASSERT_NE(f, nullptr); + EXPECT_EQ(std::u16string(u"res/xml/com.app.b$file.xml"), *f->path); + + ResourceName name = test::parseNameOrDie(u"@com.app.a:xml/com.app.b$file"); + ResourceKeyRef key = { name, ConfigDescription{} }; + auto iter = merger.getFilesToMerge().find(key); + ASSERT_NE(merger.getFilesToMerge().end(), iter); + + const FileToMerge& actualFileToMerge = iter->second; + EXPECT_EQ(Source("res/xml/file.xml"), actualFileToMerge.file->getSource()); + EXPECT_EQ(std::string("res/xml/com.app.b$file.xml"), actualFileToMerge.dstPath); +} + +TEST_F(TableMergerTest, OverrideResourceWithOverlay) { + std::unique_ptr<ResourceTable> base = test::ResourceTableBuilder() + .setPackageId(u"", 0x00) + .addValue(u"@bool/foo", ResourceUtils::tryParseBool(u"true")) + .build(); + std::unique_ptr<ResourceTable> overlay = test::ResourceTableBuilder() + .setPackageId(u"", 0x00) + .addValue(u"@bool/foo", ResourceUtils::tryParseBool(u"false")) + .build(); + + ResourceTable finalTable; + TableMergerOptions tableMergerOptions; + tableMergerOptions.autoAddOverlay = false; + TableMerger merger(mContext.get(), &finalTable, tableMergerOptions); + + ASSERT_TRUE(merger.merge({}, base.get())); + ASSERT_TRUE(merger.mergeOverlay({}, overlay.get())); + + BinaryPrimitive* foo = test::getValue<BinaryPrimitive>(&finalTable, u"@com.app.a:bool/foo"); + ASSERT_NE(nullptr, foo); + EXPECT_EQ(0x0u, foo->value.data); +} + +TEST_F(TableMergerTest, MergeAddResourceFromOverlay) { + std::unique_ptr<ResourceTable> tableA = test::ResourceTableBuilder() + .setPackageId(u"", 0x7f) + .setSymbolState(u"@bool/foo", {}, SymbolState::kUndefined) + .build(); + std::unique_ptr<ResourceTable> tableB = test::ResourceTableBuilder() + .setPackageId(u"", 0x7f) + .addValue(u"@bool/foo", ResourceUtils::tryParseBool(u"true")) + .build(); + + ResourceTable finalTable; + TableMerger merger(mContext.get(), &finalTable, TableMergerOptions{}); + + ASSERT_TRUE(merger.merge({}, tableA.get())); + ASSERT_TRUE(merger.mergeOverlay({}, tableB.get())); +} + +TEST_F(TableMergerTest, MergeAddResourceFromOverlayWithAutoAddOverlay) { + std::unique_ptr<ResourceTable> tableA = test::ResourceTableBuilder() + .setPackageId(u"", 0x7f) + .build(); + std::unique_ptr<ResourceTable> tableB = test::ResourceTableBuilder() + .setPackageId(u"", 0x7f) + .addValue(u"@bool/foo", ResourceUtils::tryParseBool(u"true")) + .build(); + + ResourceTable finalTable; + TableMergerOptions options; + options.autoAddOverlay = true; + TableMerger merger(mContext.get(), &finalTable, options); + + ASSERT_TRUE(merger.merge({}, tableA.get())); + ASSERT_TRUE(merger.mergeOverlay({}, tableB.get())); +} + +TEST_F(TableMergerTest, FailToMergeNewResourceWithoutAutoAddOverlay) { + std::unique_ptr<ResourceTable> tableA = test::ResourceTableBuilder() + .setPackageId(u"", 0x7f) + .build(); + std::unique_ptr<ResourceTable> tableB = test::ResourceTableBuilder() + .setPackageId(u"", 0x7f) + .addValue(u"@bool/foo", ResourceUtils::tryParseBool(u"true")) + .build(); + + ResourceTable finalTable; + TableMergerOptions options; + options.autoAddOverlay = false; + TableMerger merger(mContext.get(), &finalTable, options); + + ASSERT_TRUE(merger.merge({}, tableA.get())); + ASSERT_FALSE(merger.mergeOverlay({}, tableB.get())); +} + +TEST_F(TableMergerTest, MergeAndStripResourcesNotMatchingFilter) { + ResourceTable finalTable; + TableMergerOptions options; + options.autoAddOverlay = false; + + AxisConfigFilter filter; + filter.addConfig(test::parseConfigOrDie("en")); + options.filter = &filter; + + test::TestFile fileA("res/layout-en/main.xml"), fileB("res/layout-fr-rFR/main.xml"); + const ResourceName name = test::parseNameOrDie(u"@com.app.a:layout/main"); + const ConfigDescription configEn = test::parseConfigOrDie("en"); + const ConfigDescription configFr = test::parseConfigOrDie("fr-rFR"); + + TableMerger merger(mContext.get(), &finalTable, options); + ASSERT_TRUE(merger.mergeFile(ResourceFile{ name, configEn }, &fileA)); + ASSERT_TRUE(merger.mergeFile(ResourceFile{ name, configFr }, &fileB)); + + EXPECT_NE(nullptr, test::getValueForConfig<FileReference>(&finalTable, + u"@com.app.a:layout/main", + configEn)); + EXPECT_EQ(nullptr, test::getValueForConfig<FileReference>(&finalTable, + u"@com.app.a:layout/main", + configFr)); + + EXPECT_NE(merger.getFilesToMerge().end(), + merger.getFilesToMerge().find(ResourceKeyRef(name, configEn))); + + EXPECT_EQ(merger.getFilesToMerge().end(), + merger.getFilesToMerge().find(ResourceKeyRef(name, configFr))); +} + +} // namespace aapt diff --git a/tools/aapt2/link/XmlReferenceLinker.cpp b/tools/aapt2/link/XmlReferenceLinker.cpp new file mode 100644 index 000000000000..a26d7637ab3a --- /dev/null +++ b/tools/aapt2/link/XmlReferenceLinker.cpp @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Diagnostics.h" +#include "ResourceUtils.h" +#include "SdkConstants.h" +#include "link/Linkers.h" +#include "link/ReferenceLinker.h" +#include "process/IResourceTableConsumer.h" +#include "process/SymbolTable.h" +#include "util/Util.h" +#include "xml/XmlDom.h" + +namespace aapt { + +namespace { + +/** + * Visits all references (including parents of styles, references in styles, arrays, etc) and + * links their symbolic name to their Resource ID, performing mangling and package aliasing + * as needed. + */ +class ReferenceVisitor : public ValueVisitor { +private: + IAaptContext* mContext; + ISymbolTable* mSymbols; + xml::IPackageDeclStack* mDecls; + CallSite* mCallSite; + bool mError; + +public: + using ValueVisitor::visit; + + ReferenceVisitor(IAaptContext* context, ISymbolTable* symbols, xml::IPackageDeclStack* decls, + CallSite* callSite) : + mContext(context), mSymbols(symbols), mDecls(decls), mCallSite(callSite), + mError(false) { + } + + void visit(Reference* ref) override { + if (!ReferenceLinker::linkReference(ref, mContext, mSymbols, mDecls, mCallSite)) { + mError = true; + } + } + + bool hasError() const { + return mError; + } +}; + +/** + * Visits each xml Element and compiles the attributes within. + */ +class XmlVisitor : public xml::PackageAwareVisitor { +private: + IAaptContext* mContext; + ISymbolTable* mSymbols; + Source mSource; + std::set<int>* mSdkLevelsFound; + CallSite* mCallSite; + ReferenceVisitor mReferenceVisitor; + bool mError = false; + +public: + using xml::PackageAwareVisitor::visit; + + XmlVisitor(IAaptContext* context, ISymbolTable* symbols, const Source& source, + std::set<int>* sdkLevelsFound, CallSite* callSite) : + mContext(context), mSymbols(symbols), mSource(source), mSdkLevelsFound(sdkLevelsFound), + mCallSite(callSite), mReferenceVisitor(context, symbols, this, callSite) { + } + + void visit(xml::Element* el) override { + const Source source = mSource.withLine(el->lineNumber); + for (xml::Attribute& attr : el->attributes) { + Maybe<xml::ExtractedPackage> maybePackage = + xml::extractPackageFromNamespace(attr.namespaceUri); + if (maybePackage) { + // There is a valid package name for this attribute. We will look this up. + StringPiece16 package = maybePackage.value().package; + if (package.empty()) { + // Empty package means the 'current' or 'local' package. + package = mContext->getCompilationPackage(); + } + + Reference attrRef(ResourceNameRef(package, ResourceType::kAttr, attr.name)); + attrRef.privateReference = maybePackage.value().privateNamespace; + + std::string errStr; + attr.compiledAttribute = ReferenceLinker::compileXmlAttribute( + attrRef, mContext->getNameMangler(), mSymbols, mCallSite, &errStr); + + // Convert the string value into a compiled Value if this is a valid attribute. + if (attr.compiledAttribute) { + // Record all SDK levels from which the attributes were defined. + const int sdkLevel = findAttributeSdkLevel(attr.compiledAttribute.value().id); + if (sdkLevel > 1) { + mSdkLevelsFound->insert(sdkLevel); + } + + const Attribute* attribute = &attr.compiledAttribute.value().attribute; + attr.compiledValue = ResourceUtils::parseItemForAttribute(attr.value, + attribute); + if (!attr.compiledValue && + !(attribute->typeMask & android::ResTable_map::TYPE_STRING)) { + // We won't be able to encode this as a string. + mContext->getDiagnostics()->error( + DiagMessage(source) << "'" << attr.value << "' " + << "is incompatible with attribute " + << package << ":" << attr.name << " " + << *attribute); + mError = true; + } + } else { + mContext->getDiagnostics()->error(DiagMessage(source) + << "attribute '" << package << ":" + << attr.name << "' " << errStr); + mError = true; + + } + } else { + // We still encode references. + attr.compiledValue = ResourceUtils::tryParseReference(attr.value); + } + + if (attr.compiledValue) { + // With a compiledValue, we must resolve the reference and assign it an ID. + attr.compiledValue->setSource(source); + attr.compiledValue->accept(&mReferenceVisitor); + } + } + + // Call the super implementation. + xml::PackageAwareVisitor::visit(el); + } + + bool hasError() { + return mError || mReferenceVisitor.hasError(); + } +}; + +} // namespace + +bool XmlReferenceLinker::consume(IAaptContext* context, xml::XmlResource* resource) { + mSdkLevelsFound.clear(); + CallSite callSite = { resource->file.name }; + XmlVisitor visitor(context, context->getExternalSymbols(), resource->file.source, + &mSdkLevelsFound, &callSite); + if (resource->root) { + resource->root->accept(&visitor); + return !visitor.hasError(); + } + return false; +} + +} // namespace aapt diff --git a/tools/aapt2/link/XmlReferenceLinker_test.cpp b/tools/aapt2/link/XmlReferenceLinker_test.cpp new file mode 100644 index 000000000000..3bfaf91854bb --- /dev/null +++ b/tools/aapt2/link/XmlReferenceLinker_test.cpp @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "link/Linkers.h" + +#include "test/Builders.h" +#include "test/Context.h" + +#include <gtest/gtest.h> + +namespace aapt { + +class XmlReferenceLinkerTest : public ::testing::Test { +public: + void SetUp() override { + mContext = test::ContextBuilder() + .setCompilationPackage(u"com.app.test") + .setNameManglerPolicy( + NameManglerPolicy{ u"com.app.test", { u"com.android.support" } }) + .setSymbolTable(test::StaticSymbolTableBuilder() + .addPublicSymbol(u"@android:attr/layout_width", ResourceId(0x01010000), + test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_ENUM | + android::ResTable_map::TYPE_DIMENSION) + .addItem(u"match_parent", 0xffffffff) + .build()) + .addPublicSymbol(u"@android:attr/background", ResourceId(0x01010001), + test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_COLOR).build()) + .addPublicSymbol(u"@android:attr/attr", ResourceId(0x01010002), + test::AttributeBuilder().build()) + .addPublicSymbol(u"@android:attr/text", ResourceId(0x01010003), + test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_STRING) + .build()) + + // Add one real symbol that was introduces in v21 + .addPublicSymbol(u"@android:attr/colorAccent", ResourceId(0x01010435), + test::AttributeBuilder().build()) + + // Private symbol. + .addSymbol(u"@android:color/hidden", ResourceId(0x01020001)) + + .addPublicSymbol(u"@android:id/id", ResourceId(0x01030000)) + .addSymbol(u"@com.app.test:id/id", ResourceId(0x7f030000)) + .addSymbol(u"@com.app.test:color/green", ResourceId(0x7f020000)) + .addSymbol(u"@com.app.test:color/red", ResourceId(0x7f020001)) + .addSymbol(u"@com.app.test:attr/colorAccent", ResourceId(0x7f010000), + test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_COLOR).build()) + .addPublicSymbol(u"@com.app.test:attr/com.android.support$colorAccent", + ResourceId(0x7f010001), test::AttributeBuilder() + .setTypeMask(android::ResTable_map::TYPE_COLOR).build()) + .addPublicSymbol(u"@com.app.test:attr/attr", ResourceId(0x7f010002), + test::AttributeBuilder().build()) + .build()) + .build(); + } + +protected: + std::unique_ptr<IAaptContext> mContext; +}; + +TEST_F(XmlReferenceLinkerTest, LinkBasicAttributes) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF( + <View xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:background="@color/green" + android:text="hello" + class="hello" />)EOF"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.consume(mContext.get(), doc.get())); + + xml::Element* viewEl = xml::findRootElement(doc.get()); + ASSERT_NE(viewEl, nullptr); + + xml::Attribute* xmlAttr = viewEl->findAttribute(u"http://schemas.android.com/apk/res/android", + u"layout_width"); + ASSERT_NE(xmlAttr, nullptr); + AAPT_ASSERT_TRUE(xmlAttr->compiledAttribute); + EXPECT_EQ(xmlAttr->compiledAttribute.value().id, ResourceId(0x01010000)); + ASSERT_NE(xmlAttr->compiledValue, nullptr); + ASSERT_NE(valueCast<BinaryPrimitive>(xmlAttr->compiledValue.get()), nullptr); + + xmlAttr = viewEl->findAttribute(u"http://schemas.android.com/apk/res/android", u"background"); + ASSERT_NE(xmlAttr, nullptr); + AAPT_ASSERT_TRUE(xmlAttr->compiledAttribute); + EXPECT_EQ(xmlAttr->compiledAttribute.value().id, ResourceId(0x01010001)); + ASSERT_NE(xmlAttr->compiledValue, nullptr); + Reference* ref = valueCast<Reference>(xmlAttr->compiledValue.get()); + ASSERT_NE(ref, nullptr); + AAPT_ASSERT_TRUE(ref->name); + EXPECT_EQ(ref->name.value(), test::parseNameOrDie(u"@color/green")); // Make sure the name + // didn't change. + AAPT_ASSERT_TRUE(ref->id); + EXPECT_EQ(ref->id.value(), ResourceId(0x7f020000)); + + xmlAttr = viewEl->findAttribute(u"http://schemas.android.com/apk/res/android", u"text"); + ASSERT_NE(xmlAttr, nullptr); + AAPT_ASSERT_TRUE(xmlAttr->compiledAttribute); + ASSERT_FALSE(xmlAttr->compiledValue); // Strings don't get compiled for memory sake. + + xmlAttr = viewEl->findAttribute(u"", u"class"); + ASSERT_NE(xmlAttr, nullptr); + AAPT_ASSERT_FALSE(xmlAttr->compiledAttribute); + ASSERT_EQ(xmlAttr->compiledValue, nullptr); +} + +TEST_F(XmlReferenceLinkerTest, PrivateSymbolsAreNotLinked) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF( + <View xmlns:android="http://schemas.android.com/apk/res/android" + android:colorAccent="@android:color/hidden" />)EOF"); + + XmlReferenceLinker linker; + ASSERT_FALSE(linker.consume(mContext.get(), doc.get())); +} + +TEST_F(XmlReferenceLinkerTest, PrivateSymbolsAreLinkedWhenReferenceHasStarPrefix) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF( + <View xmlns:android="http://schemas.android.com/apk/res/android" + android:colorAccent="@*android:color/hidden" />)EOF"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.consume(mContext.get(), doc.get())); +} + +TEST_F(XmlReferenceLinkerTest, SdkLevelsAreRecorded) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF( + <View xmlns:android="http://schemas.android.com/apk/res/android" + android:colorAccent="#ffffff" />)EOF"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.consume(mContext.get(), doc.get())); + EXPECT_TRUE(linker.getSdkLevels().count(21) == 1); +} + +TEST_F(XmlReferenceLinkerTest, LinkMangledAttributes) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF( + <View xmlns:support="http://schemas.android.com/apk/res/com.android.support" + support:colorAccent="#ff0000" />)EOF"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.consume(mContext.get(), doc.get())); + + xml::Element* viewEl = xml::findRootElement(doc.get()); + ASSERT_NE(viewEl, nullptr); + + xml::Attribute* xmlAttr = viewEl->findAttribute( + u"http://schemas.android.com/apk/res/com.android.support", u"colorAccent"); + ASSERT_NE(xmlAttr, nullptr); + AAPT_ASSERT_TRUE(xmlAttr->compiledAttribute); + EXPECT_EQ(xmlAttr->compiledAttribute.value().id, ResourceId(0x7f010001)); + ASSERT_NE(valueCast<BinaryPrimitive>(xmlAttr->compiledValue.get()), nullptr); +} + +TEST_F(XmlReferenceLinkerTest, LinkAutoResReference) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF( + <View xmlns:app="http://schemas.android.com/apk/res-auto" + app:colorAccent="@app:color/red" />)EOF"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.consume(mContext.get(), doc.get())); + + xml::Element* viewEl = xml::findRootElement(doc.get()); + ASSERT_NE(viewEl, nullptr); + + xml::Attribute* xmlAttr = viewEl->findAttribute(u"http://schemas.android.com/apk/res-auto", + u"colorAccent"); + ASSERT_NE(xmlAttr, nullptr); + AAPT_ASSERT_TRUE(xmlAttr->compiledAttribute); + EXPECT_EQ(xmlAttr->compiledAttribute.value().id, ResourceId(0x7f010000)); + Reference* ref = valueCast<Reference>(xmlAttr->compiledValue.get()); + ASSERT_NE(ref, nullptr); + AAPT_ASSERT_TRUE(ref->name); + AAPT_ASSERT_TRUE(ref->id); + EXPECT_EQ(ref->id.value(), ResourceId(0x7f020001)); +} + +TEST_F(XmlReferenceLinkerTest, LinkViewWithShadowedPackageAlias) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF( + <View xmlns:app="http://schemas.android.com/apk/res/android" + app:attr="@app:id/id"> + <View xmlns:app="http://schemas.android.com/apk/res/com.app.test" + app:attr="@app:id/id"/> + </View>)EOF"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.consume(mContext.get(), doc.get())); + + xml::Element* viewEl = xml::findRootElement(doc.get()); + ASSERT_NE(viewEl, nullptr); + + // All attributes and references in this element should be referring to "android" (0x01). + xml::Attribute* xmlAttr = viewEl->findAttribute(u"http://schemas.android.com/apk/res/android", + u"attr"); + ASSERT_NE(xmlAttr, nullptr); + AAPT_ASSERT_TRUE(xmlAttr->compiledAttribute); + EXPECT_EQ(xmlAttr->compiledAttribute.value().id, ResourceId(0x01010002)); + Reference* ref = valueCast<Reference>(xmlAttr->compiledValue.get()); + ASSERT_NE(ref, nullptr); + AAPT_ASSERT_TRUE(ref->id); + EXPECT_EQ(ref->id.value(), ResourceId(0x01030000)); + + ASSERT_FALSE(viewEl->getChildElements().empty()); + viewEl = viewEl->getChildElements().front(); + ASSERT_NE(viewEl, nullptr); + + // All attributes and references in this element should be referring to "com.app.test" (0x7f). + xmlAttr = viewEl->findAttribute(u"http://schemas.android.com/apk/res/com.app.test", u"attr"); + ASSERT_NE(xmlAttr, nullptr); + AAPT_ASSERT_TRUE(xmlAttr->compiledAttribute); + EXPECT_EQ(xmlAttr->compiledAttribute.value().id, ResourceId(0x7f010002)); + ref = valueCast<Reference>(xmlAttr->compiledValue.get()); + ASSERT_NE(ref, nullptr); + AAPT_ASSERT_TRUE(ref->id); + EXPECT_EQ(ref->id.value(), ResourceId(0x7f030000)); +} + +TEST_F(XmlReferenceLinkerTest, LinkViewWithLocalPackageAndAliasOfTheSameName) { + std::unique_ptr<xml::XmlResource> doc = test::buildXmlDomForPackageName(mContext.get(), R"EOF( + <View xmlns:android="http://schemas.android.com/apk/res/com.app.test" + android:attr="@id/id"/>)EOF"); + + XmlReferenceLinker linker; + ASSERT_TRUE(linker.consume(mContext.get(), doc.get())); + + xml::Element* viewEl = xml::findRootElement(doc.get()); + ASSERT_NE(viewEl, nullptr); + + // All attributes and references in this element should be referring to "com.app.test" (0x7f). + xml::Attribute* xmlAttr = viewEl->findAttribute( + u"http://schemas.android.com/apk/res/com.app.test", u"attr"); + ASSERT_NE(xmlAttr, nullptr); + AAPT_ASSERT_TRUE(xmlAttr->compiledAttribute); + EXPECT_EQ(xmlAttr->compiledAttribute.value().id, ResourceId(0x7f010002)); + Reference* ref = valueCast<Reference>(xmlAttr->compiledValue.get()); + ASSERT_NE(ref, nullptr); + AAPT_ASSERT_TRUE(ref->id); + EXPECT_EQ(ref->id.value(), ResourceId(0x7f030000)); +} + +} // namespace aapt diff --git a/tools/aapt2/process.dot b/tools/aapt2/process.dot deleted file mode 100644 index 4741952d2332..000000000000 --- a/tools/aapt2/process.dot +++ /dev/null @@ -1,108 +0,0 @@ -digraph aapt { - out_package [label="out/default/package.apk"]; - out_fr_package [label="out/fr/package.apk"]; - out_table_aligned [label="out/default/resources-aligned.arsc"]; - out_table_fr_aligned [label="out/fr/resources-aligned.arsc"]; - out_res_layout_main_xml [label="out/res/layout/main.xml"]; - out_res_layout_v21_main_xml [color=red,label="out/res/layout-v21/main.xml"]; - out_res_layout_fr_main_xml [label="out/res/layout-fr/main.xml"]; - out_res_layout_fr_v21_main_xml [color=red,label="out/res/layout-fr-v21/main.xml"]; - out_table [label="out/default/resources.arsc"]; - out_fr_table [label="out/fr/resources.arsc"]; - out_values_table [label="out/values/resources.arsc"]; - out_layout_table [label="out/layout/resources.arsc"]; - out_values_fr_table [label="out/values-fr/resources.arsc"]; - out_layout_fr_table [label="out/layout-fr/resources.arsc"]; - res_values_strings_xml [label="res/values/strings.xml"]; - res_values_attrs_xml [label="res/values/attrs.xml"]; - res_layout_main_xml [label="res/layout/main.xml"]; - res_layout_fr_main_xml [label="res/layout-fr/main.xml"]; - res_values_fr_strings_xml [label="res/values-fr/strings.xml"]; - - lib_apk_resources_arsc [label="lib.apk:resources.arsc",color=green]; - lib_apk_res_layout_main_xml [label="lib.apk:res/layout/main.xml",color=green]; - lib_apk_res_drawable_icon_png [label="lib.apk:res/drawable/icon.png",color=green]; - lib_apk_fr_res_layout_main_xml [label="lib.apk:res/layout-fr/main.xml",color=green]; - lib_apk_fr_res_drawable_icon_png [label="lib.apk:res/drawable-fr/icon.png",color=green]; - out_res_layout_lib_main_xml [label="out/res/layout/lib-main.xml"]; - - out_package -> package_default; - out_fr_package -> package_fr; - - package_default [shape=box,label="Assemble",color=blue]; - package_default -> out_table_aligned; - package_default -> out_res_layout_main_xml; - package_default -> out_res_layout_v21_main_xml [color=red]; - package_default -> out_res_layout_lib_main_xml; - - package_fr [shape=box,label="Assemble",color=blue]; - package_fr -> out_table_fr_aligned; - package_fr -> out_res_layout_fr_main_xml; - package_fr -> out_res_layout_fr_v21_main_xml [color=red]; - - out_table_aligned -> align_tables; - out_table_fr_aligned -> align_tables; - - align_tables [shape=box,label="Align",color=blue]; - align_tables -> out_table; - align_tables -> out_fr_table; - - out_table -> link_tables; - - link_tables [shape=box,label="Link",color=blue]; - link_tables -> out_values_table; - link_tables -> out_layout_table; - link_tables -> lib_apk_resources_arsc; - - out_values_table -> compile_values; - - compile_values [shape=box,label="Collect",color=blue]; - compile_values -> res_values_strings_xml; - compile_values -> res_values_attrs_xml; - - out_layout_table -> collect_xml; - - collect_xml [shape=box,label="Collect",color=blue]; - collect_xml -> res_layout_main_xml; - - out_fr_table -> link_fr_tables; - - link_fr_tables [shape=box,label="Link",color=blue]; - link_fr_tables -> out_values_fr_table; - link_fr_tables -> out_layout_fr_table; - link_fr_tables -> lib_apk_resources_arsc; - - out_values_fr_table -> compile_values_fr; - - compile_values_fr [shape=box,label="Collect",color=blue]; - compile_values_fr -> res_values_fr_strings_xml; - - out_layout_fr_table -> collect_xml_fr; - - collect_xml_fr [shape=box,label="Collect",color=blue]; - collect_xml_fr -> res_layout_fr_main_xml; - - compile_res_layout_main_xml [shape=box,label="Compile",color=blue]; - - out_res_layout_main_xml -> compile_res_layout_main_xml; - - out_res_layout_v21_main_xml -> compile_res_layout_main_xml [color=red]; - - compile_res_layout_main_xml -> res_layout_main_xml; - compile_res_layout_main_xml -> out_table_aligned; - - compile_res_layout_fr_main_xml [shape=box,label="Compile",color=blue]; - - out_res_layout_fr_main_xml -> compile_res_layout_fr_main_xml; - - out_res_layout_fr_v21_main_xml -> compile_res_layout_fr_main_xml [color=red]; - - compile_res_layout_fr_main_xml -> res_layout_fr_main_xml; - compile_res_layout_fr_main_xml -> out_table_fr_aligned; - - out_res_layout_lib_main_xml -> compile_res_layout_lib_main_xml; - - compile_res_layout_lib_main_xml [shape=box,label="Compile",color=blue]; - compile_res_layout_lib_main_xml -> out_table_aligned; - compile_res_layout_lib_main_xml -> lib_apk_res_layout_main_xml; -} diff --git a/tools/aapt2/process/IResourceTableConsumer.h b/tools/aapt2/process/IResourceTableConsumer.h new file mode 100644 index 000000000000..a2528d2ac195 --- /dev/null +++ b/tools/aapt2/process/IResourceTableConsumer.h @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_PROCESS_IRESOURCETABLECONSUMER_H +#define AAPT_PROCESS_IRESOURCETABLECONSUMER_H + +#include "Diagnostics.h" +#include "NameMangler.h" +#include "Resource.h" +#include "ResourceValues.h" +#include "Source.h" + +#include <iostream> +#include <list> +#include <sstream> + +namespace aapt { + +class ResourceTable; +struct ISymbolTable; + +struct IAaptContext { + virtual ~IAaptContext() = default; + + virtual ISymbolTable* getExternalSymbols() = 0; + virtual IDiagnostics* getDiagnostics() = 0; + virtual StringPiece16 getCompilationPackage() = 0; + virtual uint8_t getPackageId() = 0; + virtual NameMangler* getNameMangler() = 0; +}; + +struct IResourceTableConsumer { + virtual ~IResourceTableConsumer() = default; + + virtual bool consume(IAaptContext* context, ResourceTable* table) = 0; +}; + +namespace xml { +struct XmlResource; +} + +struct IXmlResourceConsumer { + virtual ~IXmlResourceConsumer() = default; + + virtual bool consume(IAaptContext* context, xml::XmlResource* resource) = 0; +}; + +} // namespace aapt + +#endif /* AAPT_PROCESS_IRESOURCETABLECONSUMER_H */ diff --git a/tools/aapt2/process/SymbolTable.cpp b/tools/aapt2/process/SymbolTable.cpp new file mode 100644 index 000000000000..6ad2f9c10d22 --- /dev/null +++ b/tools/aapt2/process/SymbolTable.cpp @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ConfigDescription.h" +#include "Resource.h" +#include "ValueVisitor.h" + +#include "process/SymbolTable.h" +#include "util/Comparators.h" +#include "util/Util.h" + +#include <androidfw/AssetManager.h> +#include <androidfw/ResourceTypes.h> + +namespace aapt { + +const ISymbolTable::Symbol* SymbolTableWrapper::findByName(const ResourceName& name) { + if (const std::shared_ptr<Symbol>& s = mCache.get(name)) { + return s.get(); + } + + Maybe<ResourceTable::SearchResult> result = mTable->findResource(name); + if (!result) { + if (name.type == ResourceType::kAttr) { + // Recurse and try looking up a private attribute. + return findByName(ResourceName(name.package, ResourceType::kAttrPrivate, name.entry)); + } + return {}; + } + + ResourceTable::SearchResult sr = result.value(); + + // If no ID exists, we treat the symbol as missing. SymbolTables are used to + // find symbols to link. + if (!sr.package->id || !sr.type->id || !sr.entry->id) { + return {}; + } + + std::shared_ptr<Symbol> symbol = std::make_shared<Symbol>(); + symbol->id = ResourceId(sr.package->id.value(), sr.type->id.value(), sr.entry->id.value()); + symbol->isPublic = (sr.entry->symbolStatus.state == SymbolState::kPublic); + + if (name.type == ResourceType::kAttr || name.type == ResourceType::kAttrPrivate) { + const ConfigDescription kDefaultConfig; + auto iter = std::lower_bound(sr.entry->values.begin(), sr.entry->values.end(), + kDefaultConfig, cmp::lessThanConfig); + + if (iter != sr.entry->values.end() && iter->config == kDefaultConfig) { + // This resource has an Attribute. + if (Attribute* attr = valueCast<Attribute>(iter->value.get())) { + symbol->attribute = util::make_unique<Attribute>(*attr); + } else { + return {}; + } + } + } + + if (name.type == ResourceType::kAttrPrivate) { + // Masquerade this entry as kAttr. + mCache.put(ResourceName(name.package, ResourceType::kAttr, name.entry), symbol); + } else { + mCache.put(name, symbol); + } + return symbol.get(); +} + +static std::shared_ptr<ISymbolTable::Symbol> lookupAttributeInTable(const android::ResTable& table, + ResourceId id) { + // Try as a bag. + const android::ResTable::bag_entry* entry; + ssize_t count = table.lockBag(id.id, &entry); + if (count < 0) { + table.unlockBag(entry); + return nullptr; + } + + // We found a resource. + std::shared_ptr<ISymbolTable::Symbol> s = std::make_shared<ISymbolTable::Symbol>(); + s->id = id; + + // Check to see if it is an attribute. + for (size_t i = 0; i < (size_t) count; i++) { + if (entry[i].map.name.ident == android::ResTable_map::ATTR_TYPE) { + s->attribute = util::make_unique<Attribute>(false); + s->attribute->typeMask = entry[i].map.value.data; + break; + } + } + + if (s->attribute) { + for (size_t i = 0; i < (size_t) count; i++) { + const android::ResTable_map& mapEntry = entry[i].map; + if (Res_INTERNALID(mapEntry.name.ident)) { + switch (mapEntry.name.ident) { + case android::ResTable_map::ATTR_MIN: + s->attribute->minInt = static_cast<int32_t>(mapEntry.value.data); + break; + case android::ResTable_map::ATTR_MAX: + s->attribute->maxInt = static_cast<int32_t>(mapEntry.value.data); + break; + } + continue; + } + + android::ResTable::resource_name entryName; + if (!table.getResourceName(mapEntry.name.ident, false, &entryName)) { + table.unlockBag(entry); + return nullptr; + } + + const ResourceType* parsedType = parseResourceType( + StringPiece16(entryName.type, entryName.typeLen)); + if (!parsedType) { + table.unlockBag(entry); + return nullptr; + } + + Attribute::Symbol symbol; + symbol.symbol.name = ResourceName( + StringPiece16(entryName.package, entryName.packageLen), + *parsedType, + StringPiece16(entryName.name, entryName.nameLen)); + symbol.symbol.id = ResourceId(mapEntry.name.ident); + symbol.value = mapEntry.value.data; + s->attribute->symbols.push_back(std::move(symbol)); + } + } + table.unlockBag(entry); + return s; +} + +const ISymbolTable::Symbol* AssetManagerSymbolTableBuilder::AssetManagerSymbolTable::findByName( + const ResourceName& name) { + if (const std::shared_ptr<Symbol>& s = mCache.get(name)) { + return s.get(); + } + + for (const auto& asset : mAssets) { + const android::ResTable& table = asset->getResources(false); + StringPiece16 typeStr = toString(name.type); + uint32_t typeSpecFlags = 0; + ResourceId resId = table.identifierForName(name.entry.data(), name.entry.size(), + typeStr.data(), typeStr.size(), + name.package.data(), name.package.size(), + &typeSpecFlags); + if (!resId.isValid()) { + continue; + } + + std::shared_ptr<Symbol> s; + if (name.type == ResourceType::kAttr) { + s = lookupAttributeInTable(table, resId); + } else { + s = std::make_shared<Symbol>(); + s->id = resId; + } + + if (s) { + s->isPublic = (typeSpecFlags & android::ResTable_typeSpec::SPEC_PUBLIC) != 0; + mCache.put(name, s); + return s.get(); + } + } + return nullptr; +} + +static Maybe<ResourceName> getResourceName(const android::ResTable& table, ResourceId id) { + android::ResTable::resource_name resName; + if (!table.getResourceName(id.id, true, &resName)) { + return {}; + } + + ResourceName name; + if (resName.package) { + name.package = StringPiece16(resName.package, resName.packageLen).toString(); + } + + const ResourceType* type; + if (resName.type) { + type = parseResourceType(StringPiece16(resName.type, resName.typeLen)); + + } else if (resName.type8) { + type = parseResourceType(util::utf8ToUtf16(StringPiece(resName.type8, resName.typeLen))); + } else { + return {}; + } + + if (!type) { + return {}; + } + + name.type = *type; + + if (resName.name) { + name.entry = StringPiece16(resName.name, resName.nameLen).toString(); + } else if (resName.name8) { + name.entry = util::utf8ToUtf16(StringPiece(resName.name8, resName.nameLen)); + } else { + return {}; + } + + return name; +} + +const ISymbolTable::Symbol* AssetManagerSymbolTableBuilder::AssetManagerSymbolTable::findById( + ResourceId id) { + if (const std::shared_ptr<Symbol>& s = mIdCache.get(id)) { + return s.get(); + } + + for (const auto& asset : mAssets) { + const android::ResTable& table = asset->getResources(false); + + Maybe<ResourceName> maybeName = getResourceName(table, id); + if (!maybeName) { + continue; + } + + uint32_t typeSpecFlags = 0; + table.getResourceFlags(id.id, &typeSpecFlags); + + std::shared_ptr<Symbol> s; + if (maybeName.value().type == ResourceType::kAttr) { + s = lookupAttributeInTable(table, id); + } else { + s = std::make_shared<Symbol>(); + s->id = id; + } + + if (s) { + s->isPublic = (typeSpecFlags & android::ResTable_typeSpec::SPEC_PUBLIC) != 0; + mIdCache.put(id, s); + return s.get(); + } + } + return nullptr; +} + +const ISymbolTable::Symbol* JoinedSymbolTableBuilder::JoinedSymbolTable::findByName( + const ResourceName& name) { + for (auto& symbolTable : mSymbolTables) { + if (const Symbol* s = symbolTable->findByName(name)) { + return s; + } + } + return {}; +} + +const ISymbolTable::Symbol* JoinedSymbolTableBuilder::JoinedSymbolTable::findById(ResourceId id) { + for (auto& symbolTable : mSymbolTables) { + if (const Symbol* s = symbolTable->findById(id)) { + return s; + } + } + return {}; +} + +} // namespace aapt diff --git a/tools/aapt2/process/SymbolTable.h b/tools/aapt2/process/SymbolTable.h new file mode 100644 index 000000000000..22096ed82f4e --- /dev/null +++ b/tools/aapt2/process/SymbolTable.h @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_PROCESS_SYMBOLTABLE_H +#define AAPT_PROCESS_SYMBOLTABLE_H + +#include "Resource.h" +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "util/Util.h" + +#include <utils/JenkinsHash.h> +#include <utils/LruCache.h> + +#include <androidfw/AssetManager.h> +#include <algorithm> +#include <map> +#include <memory> +#include <vector> + +namespace aapt { + +struct ISymbolTable { + virtual ~ISymbolTable() = default; + + struct Symbol { + ResourceId id; + std::unique_ptr<Attribute> attribute; + bool isPublic; + }; + + /** + * Never hold on to the result between calls to findByName or findById. The results + * are typically stored in a cache which may evict entries. + */ + virtual const Symbol* findByName(const ResourceName& name) = 0; + virtual const Symbol* findById(ResourceId id) = 0; +}; + +inline android::hash_t hash_type(const ResourceName& name) { + std::hash<std::u16string> strHash; + android::hash_t hash = 0; + hash = android::JenkinsHashMix(hash, strHash(name.package)); + hash = android::JenkinsHashMix(hash, (uint32_t) name.type); + hash = android::JenkinsHashMix(hash, strHash(name.entry)); + return hash; +} + +inline android::hash_t hash_type(const ResourceId& id) { + return android::hash_type(id.id); +} + +/** + * Presents a ResourceTable as an ISymbolTable, caching results. + * Instances of this class must outlive the encompassed ResourceTable. + * Since symbols are cached, the ResourceTable should not change during the + * lifetime of this SymbolTableWrapper. + * + * If a resource in the ResourceTable does not have a ResourceID assigned to it, + * it is ignored. + * + * Lookups by ID are ignored. + */ +class SymbolTableWrapper : public ISymbolTable { +private: + ResourceTable* mTable; + + // We use shared_ptr because unique_ptr is not supported and + // we need automatic deletion. + android::LruCache<ResourceName, std::shared_ptr<Symbol>> mCache; + +public: + SymbolTableWrapper(ResourceTable* table) : mTable(table), mCache(200) { + } + + const Symbol* findByName(const ResourceName& name) override; + + // Unsupported, all queries to ResourceTable should be done by name. + const Symbol* findById(ResourceId id) override { + return {}; + } +}; + +class AssetManagerSymbolTableBuilder { +private: + struct AssetManagerSymbolTable : public ISymbolTable { + std::vector<std::unique_ptr<android::AssetManager>> mAssets; + + // We use shared_ptr because unique_ptr is not supported and + // we need automatic deletion. + android::LruCache<ResourceName, std::shared_ptr<Symbol>> mCache; + android::LruCache<ResourceId, std::shared_ptr<Symbol>> mIdCache; + + AssetManagerSymbolTable() : mCache(200), mIdCache(200) { + } + + const Symbol* findByName(const ResourceName& name) override; + const Symbol* findById(ResourceId id) override; + }; + + std::unique_ptr<AssetManagerSymbolTable> mSymbolTable = + util::make_unique<AssetManagerSymbolTable>(); + +public: + AssetManagerSymbolTableBuilder& add(std::unique_ptr<android::AssetManager> assetManager) { + mSymbolTable->mAssets.push_back(std::move(assetManager)); + return *this; + } + + std::unique_ptr<ISymbolTable> build() { + return std::move(mSymbolTable); + } +}; + +class JoinedSymbolTableBuilder { +private: + struct JoinedSymbolTable : public ISymbolTable { + std::vector<std::unique_ptr<ISymbolTable>> mSymbolTables; + + const Symbol* findByName(const ResourceName& name) override; + const Symbol* findById(ResourceId id) override; + }; + + std::unique_ptr<JoinedSymbolTable> mSymbolTable = util::make_unique<JoinedSymbolTable>(); + +public: + JoinedSymbolTableBuilder& addSymbolTable(std::unique_ptr<ISymbolTable> table) { + mSymbolTable->mSymbolTables.push_back(std::move(table)); + return *this; + } + + std::unique_ptr<ISymbolTable> build() { + return std::move(mSymbolTable); + } +}; + +} // namespace aapt + +#endif /* AAPT_PROCESS_SYMBOLTABLE_H */ diff --git a/tools/aapt2/process/SymbolTable_test.cpp b/tools/aapt2/process/SymbolTable_test.cpp new file mode 100644 index 000000000000..1dc3b4fe4e4a --- /dev/null +++ b/tools/aapt2/process/SymbolTable_test.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "process/SymbolTable.h" +#include "test/Builders.h" +#include "test/Context.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(SymbolTableWrapperTest, FindSymbolsWithIds) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addSimple(u"@android:id/foo", ResourceId(0x01020000)) + .addSimple(u"@android:id/bar") + .addValue(u"@android:attr/foo", ResourceId(0x01010000), + test::AttributeBuilder().build()) + .build(); + + SymbolTableWrapper symbolTable(table.get()); + EXPECT_NE(symbolTable.findByName(test::parseNameOrDie(u"@android:id/foo")), nullptr); + EXPECT_EQ(symbolTable.findByName(test::parseNameOrDie(u"@android:id/bar")), nullptr); + + const ISymbolTable::Symbol* s = symbolTable.findByName( + test::parseNameOrDie(u"@android:attr/foo")); + ASSERT_NE(s, nullptr); + EXPECT_NE(s->attribute, nullptr); +} + +TEST(SymbolTableWrapperTest, FindPrivateAttrSymbol) { + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .addValue(u"@android:^attr-private/foo", ResourceId(0x01010000), + test::AttributeBuilder().build()) + .build(); + + SymbolTableWrapper symbolTable(table.get()); + const ISymbolTable::Symbol* s = symbolTable.findByName( + test::parseNameOrDie(u"@android:attr/foo")); + ASSERT_NE(s, nullptr); + EXPECT_NE(s->attribute, nullptr); +} + +} // namespace aapt diff --git a/tools/aapt2/test/Builders.h b/tools/aapt2/test/Builders.h new file mode 100644 index 000000000000..579a46ec230f --- /dev/null +++ b/tools/aapt2/test/Builders.h @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_TEST_BUILDERS_H +#define AAPT_TEST_BUILDERS_H + +#include "ResourceTable.h" +#include "ResourceValues.h" +#include "test/Common.h" +#include "util/Util.h" +#include "xml/XmlDom.h" + +#include <memory> + +namespace aapt { +namespace test { + +class ResourceTableBuilder { +private: + DummyDiagnosticsImpl mDiagnostics; + std::unique_ptr<ResourceTable> mTable = util::make_unique<ResourceTable>(); + +public: + ResourceTableBuilder() = default; + + StringPool* getStringPool() { + return &mTable->stringPool; + } + + ResourceTableBuilder& setPackageId(const StringPiece16& packageName, uint8_t id) { + ResourceTablePackage* package = mTable->createPackage(packageName, id); + assert(package); + return *this; + } + + ResourceTableBuilder& addSimple(const StringPiece16& name, const ResourceId id = {}) { + return addValue(name, id, util::make_unique<Id>()); + } + + ResourceTableBuilder& addReference(const StringPiece16& name, const StringPiece16& ref) { + return addReference(name, {}, ref); + } + + ResourceTableBuilder& addReference(const StringPiece16& name, const ResourceId id, + const StringPiece16& ref) { + return addValue(name, id, util::make_unique<Reference>(parseNameOrDie(ref))); + } + + ResourceTableBuilder& addString(const StringPiece16& name, const StringPiece16& str) { + return addString(name, {}, str); + } + + ResourceTableBuilder& addString(const StringPiece16& name, const ResourceId id, + const StringPiece16& str) { + return addValue(name, id, util::make_unique<String>(mTable->stringPool.makeRef(str))); + } + + ResourceTableBuilder& addString(const StringPiece16& name, const ResourceId id, + const ConfigDescription& config, const StringPiece16& str) { + return addValue(name, id, config, + util::make_unique<String>(mTable->stringPool.makeRef(str))); + } + + ResourceTableBuilder& addFileReference(const StringPiece16& name, const StringPiece16& path) { + return addFileReference(name, {}, path); + } + + ResourceTableBuilder& addFileReference(const StringPiece16& name, const ResourceId id, + const StringPiece16& path) { + return addValue(name, id, + util::make_unique<FileReference>(mTable->stringPool.makeRef(path))); + } + + ResourceTableBuilder& addFileReference(const StringPiece16& name, const StringPiece16& path, + const ConfigDescription& config) { + return addValue(name, {}, config, + util::make_unique<FileReference>(mTable->stringPool.makeRef(path))); + } + + ResourceTableBuilder& addValue(const StringPiece16& name, + std::unique_ptr<Value> value) { + return addValue(name, {}, std::move(value)); + } + + ResourceTableBuilder& addValue(const StringPiece16& name, const ResourceId id, + std::unique_ptr<Value> value) { + return addValue(name, id, {}, std::move(value)); + } + + ResourceTableBuilder& addValue(const StringPiece16& name, const ResourceId id, + const ConfigDescription& config, + std::unique_ptr<Value> value) { + ResourceName resName = parseNameOrDie(name); + bool result = mTable->addResourceAllowMangled(resName, id, config, std::move(value), + &mDiagnostics); + assert(result); + return *this; + } + + ResourceTableBuilder& setSymbolState(const StringPiece16& name, ResourceId id, + SymbolState state) { + ResourceName resName = parseNameOrDie(name); + Symbol symbol; + symbol.state = state; + bool result = mTable->setSymbolStateAllowMangled(resName, id, symbol, &mDiagnostics); + assert(result); + return *this; + } + + std::unique_ptr<ResourceTable> build() { + return std::move(mTable); + } +}; + +inline std::unique_ptr<Reference> buildReference(const StringPiece16& ref, + Maybe<ResourceId> id = {}) { + std::unique_ptr<Reference> reference = util::make_unique<Reference>(parseNameOrDie(ref)); + reference->id = id; + return reference; +} + +template <typename T> +class ValueBuilder { +private: + std::unique_ptr<Value> mValue; + +public: + template <typename... Args> + ValueBuilder(Args&&... args) : mValue(new T{ std::forward<Args>(args)... }) { + } + + template <typename... Args> + ValueBuilder& setSource(Args&&... args) { + mValue->setSource(Source{ std::forward<Args>(args)... }); + return *this; + } + + ValueBuilder& setComment(const StringPiece16& str) { + mValue->setComment(str); + return *this; + } + + std::unique_ptr<Value> build() { + return std::move(mValue); + } +}; + +class AttributeBuilder { +private: + std::unique_ptr<Attribute> mAttr; + +public: + AttributeBuilder(bool weak = false) : mAttr(util::make_unique<Attribute>(weak)) { + mAttr->typeMask = android::ResTable_map::TYPE_ANY; + } + + AttributeBuilder& setTypeMask(uint32_t typeMask) { + mAttr->typeMask = typeMask; + return *this; + } + + AttributeBuilder& addItem(const StringPiece16& name, uint32_t value) { + mAttr->symbols.push_back(Attribute::Symbol{ + Reference(ResourceName{ {}, ResourceType::kId, name.toString()}), + value}); + return *this; + } + + std::unique_ptr<Attribute> build() { + return std::move(mAttr); + } +}; + +class StyleBuilder { +private: + std::unique_ptr<Style> mStyle = util::make_unique<Style>(); + +public: + StyleBuilder& setParent(const StringPiece16& str) { + mStyle->parent = Reference(parseNameOrDie(str)); + return *this; + } + + StyleBuilder& addItem(const StringPiece16& str, std::unique_ptr<Item> value) { + mStyle->entries.push_back(Style::Entry{ Reference(parseNameOrDie(str)), std::move(value) }); + return *this; + } + + StyleBuilder& addItem(const StringPiece16& str, ResourceId id, std::unique_ptr<Item> value) { + addItem(str, std::move(value)); + mStyle->entries.back().key.id = id; + return *this; + } + + std::unique_ptr<Style> build() { + return std::move(mStyle); + } +}; + +class StyleableBuilder { +private: + std::unique_ptr<Styleable> mStyleable = util::make_unique<Styleable>(); + +public: + StyleableBuilder& addItem(const StringPiece16& str, Maybe<ResourceId> id = {}) { + mStyleable->entries.push_back(Reference(parseNameOrDie(str))); + mStyleable->entries.back().id = id; + return *this; + } + + std::unique_ptr<Styleable> build() { + return std::move(mStyleable); + } +}; + +inline std::unique_ptr<xml::XmlResource> buildXmlDom(const StringPiece& str) { + std::stringstream in; + in << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" << str; + StdErrDiagnostics diag; + std::unique_ptr<xml::XmlResource> doc = xml::inflate(&in, &diag, {}); + assert(doc); + return doc; +} + +inline std::unique_ptr<xml::XmlResource> buildXmlDomForPackageName(IAaptContext* context, + const StringPiece& str) { + std::unique_ptr<xml::XmlResource> doc = buildXmlDom(str); + doc->file.name.package = context->getCompilationPackage().toString(); + return doc; +} + +} // namespace test +} // namespace aapt + +#endif /* AAPT_TEST_BUILDERS_H */ diff --git a/tools/aapt2/test/Common.h b/tools/aapt2/test/Common.h new file mode 100644 index 000000000000..51e2dd44e521 --- /dev/null +++ b/tools/aapt2/test/Common.h @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_TEST_COMMON_H +#define AAPT_TEST_COMMON_H + +#include "ConfigDescription.h" +#include "Debug.h" +#include "ResourceTable.h" +#include "ResourceUtils.h" +#include "ValueVisitor.h" +#include "io/File.h" +#include "process/IResourceTableConsumer.h" +#include "util/StringPiece.h" + +#include <gtest/gtest.h> +#include <iostream> + +// +// GTEST 1.7 doesn't explicitly cast to bool, which causes explicit operators to fail to compile. +// +#define AAPT_ASSERT_TRUE(v) ASSERT_TRUE(bool(v)) +#define AAPT_ASSERT_FALSE(v) ASSERT_FALSE(bool(v)) +#define AAPT_EXPECT_TRUE(v) EXPECT_TRUE(bool(v)) +#define AAPT_EXPECT_FALSE(v) EXPECT_FALSE(bool(v)) + +namespace aapt { +namespace test { + +struct DummyDiagnosticsImpl : public IDiagnostics { + void error(const DiagMessage& message) override { + DiagMessageActual actual = message.build(); + std::cerr << actual.source << ": error: " << actual.message << "." << std::endl; + } + void warn(const DiagMessage& message) override { + DiagMessageActual actual = message.build(); + std::cerr << actual.source << ": warn: " << actual.message << "." << std::endl; + } + void note(const DiagMessage& message) override {} +}; + +inline ResourceName parseNameOrDie(const StringPiece16& str) { + ResourceNameRef ref; + bool result = ResourceUtils::tryParseReference(str, &ref); + assert(result && "invalid resource name"); + return ref.toResourceName(); +} + +inline ConfigDescription parseConfigOrDie(const StringPiece& str) { + ConfigDescription config; + bool result = ConfigDescription::parse(str, &config); + assert(result && "invalid configuration"); + return config; +} + +template <typename T> T* getValueForConfig(ResourceTable* table, const StringPiece16& resName, + const ConfigDescription& config) { + Maybe<ResourceTable::SearchResult> result = table->findResource(parseNameOrDie(resName)); + if (result) { + ResourceEntry* entry = result.value().entry; + auto iter = std::lower_bound(entry->values.begin(), entry->values.end(), config, + [](const ResourceConfigValue& a, const ConfigDescription& b) + -> bool { + return a.config < b; + }); + if (iter != entry->values.end() && iter->config == config) { + return valueCast<T>(iter->value.get()); + } + } + return nullptr; +} + +template <typename T> T* getValue(ResourceTable* table, const StringPiece16& resName) { + return getValueForConfig<T>(table, resName, {}); +} + +class TestFile : public io::IFile { +private: + Source mSource; + +public: + TestFile(const StringPiece& path) : mSource(path) {} + + std::unique_ptr<io::IData> openAsData() override { + return {}; + } + + const Source& getSource() const override { + return mSource; + } +}; + +} // namespace test +} // namespace aapt + +#endif /* AAPT_TEST_COMMON_H */ diff --git a/tools/aapt2/test/Context.h b/tools/aapt2/test/Context.h new file mode 100644 index 000000000000..555a53959737 --- /dev/null +++ b/tools/aapt2/test/Context.h @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_TEST_CONTEXT_H +#define AAPT_TEST_CONTEXT_H + +#include "NameMangler.h" +#include "util/Util.h" + +#include "process/IResourceTableConsumer.h" +#include "process/SymbolTable.h" +#include "test/Common.h" + +#include <cassert> +#include <list> + +namespace aapt { +namespace test { + +class Context : public IAaptContext { +private: + friend class ContextBuilder; + + Context() = default; + + Maybe<std::u16string> mCompilationPackage; + Maybe<uint8_t> mPackageId; + std::unique_ptr<IDiagnostics> mDiagnostics = util::make_unique<StdErrDiagnostics>(); + std::unique_ptr<ISymbolTable> mSymbols; + std::unique_ptr<NameMangler> mNameMangler; + +public: + ISymbolTable* getExternalSymbols() override { + assert(mSymbols && "test symbols not set"); + return mSymbols.get(); + } + + void setSymbolTable(std::unique_ptr<ISymbolTable> symbols) { + mSymbols = std::move(symbols); + } + + IDiagnostics* getDiagnostics() override { + assert(mDiagnostics && "test diagnostics not set"); + return mDiagnostics.get(); + } + + StringPiece16 getCompilationPackage() override { + assert(mCompilationPackage && "package name not set"); + return mCompilationPackage.value(); + } + + uint8_t getPackageId() override { + assert(mPackageId && "package ID not set"); + return mPackageId.value(); + } + + NameMangler* getNameMangler() override { + assert(mNameMangler && "test name mangler not set"); + return mNameMangler.get(); + } +}; + +class ContextBuilder { +private: + std::unique_ptr<Context> mContext = std::unique_ptr<Context>(new Context()); + +public: + ContextBuilder& setCompilationPackage(const StringPiece16& package) { + mContext->mCompilationPackage = package.toString(); + return *this; + } + + ContextBuilder& setPackageId(uint8_t id) { + mContext->mPackageId = id; + return *this; + } + + ContextBuilder& setSymbolTable(std::unique_ptr<ISymbolTable> symbols) { + mContext->mSymbols = std::move(symbols); + return *this; + } + + ContextBuilder& setDiagnostics(std::unique_ptr<IDiagnostics> diag) { + mContext->mDiagnostics = std::move(diag); + return *this; + } + + ContextBuilder& setNameManglerPolicy(NameManglerPolicy policy) { + mContext->mNameMangler = util::make_unique<NameMangler>(policy); + return *this; + } + + std::unique_ptr<Context> build() { + return std::move(mContext); + } +}; + +class StaticSymbolTableBuilder { +private: + struct SymbolTable : public ISymbolTable { + std::list<std::unique_ptr<Symbol>> mSymbols; + std::map<ResourceName, Symbol*> mNameMap; + std::map<ResourceId, Symbol*> mIdMap; + + const Symbol* findByName(const ResourceName& name) override { + auto iter = mNameMap.find(name); + if (iter != mNameMap.end()) { + return iter->second; + } + return nullptr; + } + + const Symbol* findById(ResourceId id) override { + auto iter = mIdMap.find(id); + if (iter != mIdMap.end()) { + return iter->second; + } + return nullptr; + } + }; + + std::unique_ptr<SymbolTable> mSymbolTable = util::make_unique<SymbolTable>(); + +public: + StaticSymbolTableBuilder& addPublicSymbol(const StringPiece16& name, ResourceId id, + std::unique_ptr<Attribute> attr = {}) { + std::unique_ptr<ISymbolTable::Symbol> symbol = util::make_unique<ISymbolTable::Symbol>( + id, std::move(attr)); + symbol->isPublic = true; + mSymbolTable->mNameMap[parseNameOrDie(name)] = symbol.get(); + mSymbolTable->mIdMap[id] = symbol.get(); + mSymbolTable->mSymbols.push_back(std::move(symbol)); + return *this; + } + + StaticSymbolTableBuilder& addSymbol(const StringPiece16& name, ResourceId id, + std::unique_ptr<Attribute> attr = {}) { + std::unique_ptr<ISymbolTable::Symbol> symbol = util::make_unique<ISymbolTable::Symbol>( + id, std::move(attr)); + mSymbolTable->mNameMap[parseNameOrDie(name)] = symbol.get(); + mSymbolTable->mIdMap[id] = symbol.get(); + mSymbolTable->mSymbols.push_back(std::move(symbol)); + return *this; + } + + std::unique_ptr<ISymbolTable> build() { + return std::move(mSymbolTable); + } +}; + +} // namespace test +} // namespace aapt + +#endif /* AAPT_TEST_CONTEXT_H */ diff --git a/tools/aapt2/todo.txt b/tools/aapt2/todo.txt deleted file mode 100644 index acc8bfbcc9e5..000000000000 --- a/tools/aapt2/todo.txt +++ /dev/null @@ -1,29 +0,0 @@ -XML Files -X Collect declared IDs -X Build StringPool -X Flatten - -Resource Table Operations -X Build Resource Table (with StringPool) from XML. -X Modify Resource Table. -X - Copy and transform resources. -X - Pre-17/21 attr correction. -X Perform analysis of types. -X Flatten. -X Assign resource IDs. -X Assign public resource IDs. -X Merge resource tables -- Assign private attributes to different typespace. -- Align resource tables - -Splits -- Collect all resources (ids from layouts). -- Generate resource table from base resources. -- Generate resource table from individual resources of the required type. -- Align resource tables (same type/name = same ID). - -Fat Apk -X Collect all resources (ids from layouts). -X Generate resource tables for all configurations. -- Align individual resource tables. -- Merge resource tables. diff --git a/tools/aapt2/unflatten/BinaryResourceParser.cpp b/tools/aapt2/unflatten/BinaryResourceParser.cpp new file mode 100644 index 000000000000..6b7a63cf7bf2 --- /dev/null +++ b/tools/aapt2/unflatten/BinaryResourceParser.cpp @@ -0,0 +1,967 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ResourceTable.h" +#include "ResourceUtils.h" +#include "ResourceValues.h" +#include "Source.h" +#include "ValueVisitor.h" + +#include "flatten/ResourceTypeExtensions.h" +#include "unflatten/BinaryResourceParser.h" +#include "unflatten/ResChunkPullParser.h" +#include "util/Util.h" + +#include <androidfw/ResourceTypes.h> +#include <androidfw/TypeWrappers.h> +#include <android-base/macros.h> + +#include <map> +#include <string> + +namespace aapt { + +using namespace android; + +/* + * Visitor that converts a reference's resource ID to a resource name, + * given a mapping from resource ID to resource name. + */ +class ReferenceIdToNameVisitor : public ValueVisitor { +private: + const std::map<ResourceId, ResourceName>* mMapping; + +public: + using ValueVisitor::visit; + + ReferenceIdToNameVisitor(const std::map<ResourceId, ResourceName>* mapping) : + mMapping(mapping) { + assert(mMapping); + } + + void visit(Reference* reference) override { + if (!reference->id || !reference->id.value().isValid()) { + return; + } + + ResourceId id = reference->id.value(); + auto cacheIter = mMapping->find(id); + if (cacheIter != mMapping->end()) { + reference->name = cacheIter->second; + reference->id = {}; + } + } +}; + +BinaryResourceParser::BinaryResourceParser(IAaptContext* context, ResourceTable* table, + const Source& source, const void* data, size_t len) : + mContext(context), mTable(table), mSource(source), mData(data), mDataLen(len) { +} + +bool BinaryResourceParser::parse() { + ResChunkPullParser parser(mData, mDataLen); + + bool error = false; + while(ResChunkPullParser::isGoodEvent(parser.next())) { + if (parser.getChunk()->type != android::RES_TABLE_TYPE) { + mContext->getDiagnostics()->warn(DiagMessage(mSource) + << "unknown chunk of type '" + << (int) parser.getChunk()->type << "'"); + continue; + } + + if (!parseTable(parser.getChunk())) { + error = true; + } + } + + if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt resource table: " + << parser.getLastError()); + return false; + } + return !error; +} + +Maybe<Reference> BinaryResourceParser::getSymbol(const void* data) { + if (!mSymbolEntries || mSymbolEntryCount == 0) { + return {}; + } + + if ((uintptr_t) data < (uintptr_t) mData) { + return {}; + } + + // We only support 32 bit offsets right now. + const uintptr_t offset = (uintptr_t) data - (uintptr_t) mData; + if (offset > std::numeric_limits<uint32_t>::max()) { + return {}; + } + + for (size_t i = 0; i < mSymbolEntryCount; i++) { + if (util::deviceToHost32(mSymbolEntries[i].offset) == offset) { + // This offset is a symbol! + const StringPiece16 str = util::getString( + mSymbolPool, util::deviceToHost32(mSymbolEntries[i].name.index)); + + ResourceNameRef nameRef; + bool privateRef = false; + if (!ResourceUtils::parseResourceName(str, &nameRef, &privateRef)) { + return {}; + } + + // Since we scan the symbol table in order, we can start looking for the + // next symbol from this point. + mSymbolEntryCount -= i + 1; + mSymbolEntries += i + 1; + + Reference ref(nameRef); + ref.privateReference = privateRef; + return Maybe<Reference>(std::move(ref)); + } + } + return {}; +} + +/** + * Parses the SymbolTable_header, which is present on non-final resource tables + * after the compile phase. + * + * | SymbolTable_header | + * |--------------------| + * |SymbolTable_entry 0 | + * |SymbolTable_entry 1 | + * | ... | + * |SymbolTable_entry n | + * |--------------------| + * + */ +bool BinaryResourceParser::parseSymbolTable(const ResChunk_header* chunk) { + const SymbolTable_header* header = convertTo<SymbolTable_header>(chunk); + if (!header) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt SymbolTable_header"); + return false; + } + + const uint32_t entrySizeBytes = + util::deviceToHost32(header->count) * sizeof(SymbolTable_entry); + if (entrySizeBytes > getChunkDataLen(&header->header)) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "SymbolTable_header data section too long"); + return false; + } + + mSymbolEntries = (const SymbolTable_entry*) getChunkData(&header->header); + mSymbolEntryCount = util::deviceToHost32(header->count); + + // Skip over the symbol entries and parse the StringPool chunk that should be next. + ResChunkPullParser parser(getChunkData(&header->header) + entrySizeBytes, + getChunkDataLen(&header->header) - entrySizeBytes); + if (!ResChunkPullParser::isGoodEvent(parser.next())) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "failed to parse chunk in SymbolTable: " + << parser.getLastError()); + return false; + } + + const ResChunk_header* nextChunk = parser.getChunk(); + if (util::deviceToHost16(nextChunk->type) != android::RES_STRING_POOL_TYPE) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "expected string pool in SymbolTable but got " + << "chunk of type " + << (int) util::deviceToHost16(nextChunk->type)); + return false; + } + + if (mSymbolPool.setTo(nextChunk, util::deviceToHost32(nextChunk->size)) != NO_ERROR) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt string pool in SymbolTable: " + << mSymbolPool.getError()); + return false; + } + return true; +} + +/** + * Parses the resource table, which contains all the packages, types, and entries. + */ +bool BinaryResourceParser::parseTable(const ResChunk_header* chunk) { + const ResTable_header* tableHeader = convertTo<ResTable_header>(chunk); + if (!tableHeader) { + mContext->getDiagnostics()->error(DiagMessage(mSource) << "corrupt ResTable_header chunk"); + return false; + } + + ResChunkPullParser parser(getChunkData(&tableHeader->header), + getChunkDataLen(&tableHeader->header)); + while (ResChunkPullParser::isGoodEvent(parser.next())) { + switch (util::deviceToHost16(parser.getChunk()->type)) { + case android::RES_STRING_POOL_TYPE: + if (mValuePool.getError() == NO_INIT) { + status_t err = mValuePool.setTo(parser.getChunk(), + util::deviceToHost32(parser.getChunk()->size)); + if (err != NO_ERROR) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt string pool in ResTable: " + << mValuePool.getError()); + return false; + } + + // Reserve some space for the strings we are going to add. + mTable->stringPool.hintWillAdd(mValuePool.size(), mValuePool.styleCount()); + } else { + mContext->getDiagnostics()->warn(DiagMessage(mSource) + << "unexpected string pool in ResTable"); + } + break; + + case RES_TABLE_SYMBOL_TABLE_TYPE: + if (!parseSymbolTable(parser.getChunk())) { + return false; + } + break; + + case RES_TABLE_SOURCE_POOL_TYPE: { + status_t err = mSourcePool.setTo(getChunkData(parser.getChunk()), + getChunkDataLen(parser.getChunk())); + if (err != NO_ERROR) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt source string pool in ResTable: " + << mSourcePool.getError()); + return false; + } + break; + } + + case android::RES_TABLE_PACKAGE_TYPE: + if (!parsePackage(parser.getChunk())) { + return false; + } + break; + + default: + mContext->getDiagnostics() + ->warn(DiagMessage(mSource) + << "unexpected chunk type " + << (int) util::deviceToHost16(parser.getChunk()->type)); + break; + } + } + + if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt resource table: " << parser.getLastError()); + return false; + } + return true; +} + + +bool BinaryResourceParser::parsePackage(const ResChunk_header* chunk) { + const ResTable_package* packageHeader = convertTo<ResTable_package>(chunk); + if (!packageHeader) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt ResTable_package chunk"); + return false; + } + + uint32_t packageId = util::deviceToHost32(packageHeader->id); + if (packageId > std::numeric_limits<uint8_t>::max()) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "package ID is too big (" << packageId << ")"); + return false; + } + + // Extract the package name. + size_t len = strnlen16((const char16_t*) packageHeader->name, arraysize(packageHeader->name)); + std::u16string packageName; + packageName.resize(len); + for (size_t i = 0; i < len; i++) { + packageName[i] = util::deviceToHost16(packageHeader->name[i]); + } + + ResourceTablePackage* package = mTable->createPackage(packageName, (uint8_t) packageId); + if (!package) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "incompatible package '" << packageName + << "' with ID " << packageId); + return false; + } + + // There can be multiple packages in a table, so + // clear the type and key pool in case they were set from a previous package. + mTypePool.uninit(); + mKeyPool.uninit(); + + ResChunkPullParser parser(getChunkData(&packageHeader->header), + getChunkDataLen(&packageHeader->header)); + while (ResChunkPullParser::isGoodEvent(parser.next())) { + switch (util::deviceToHost16(parser.getChunk()->type)) { + case android::RES_STRING_POOL_TYPE: + if (mTypePool.getError() == NO_INIT) { + status_t err = mTypePool.setTo(parser.getChunk(), + util::deviceToHost32(parser.getChunk()->size)); + if (err != NO_ERROR) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt type string pool in " + << "ResTable_package: " + << mTypePool.getError()); + return false; + } + } else if (mKeyPool.getError() == NO_INIT) { + status_t err = mKeyPool.setTo(parser.getChunk(), + util::deviceToHost32(parser.getChunk()->size)); + if (err != NO_ERROR) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt key string pool in " + << "ResTable_package: " + << mKeyPool.getError()); + return false; + } + } else { + mContext->getDiagnostics()->warn(DiagMessage(mSource) << "unexpected string pool"); + } + break; + + case android::RES_TABLE_TYPE_SPEC_TYPE: + if (!parseTypeSpec(parser.getChunk())) { + return false; + } + break; + + case android::RES_TABLE_TYPE_TYPE: + if (!parseType(package, parser.getChunk())) { + return false; + } + break; + + case RES_TABLE_PUBLIC_TYPE: + if (!parsePublic(package, parser.getChunk())) { + return false; + } + break; + + default: + mContext->getDiagnostics() + ->warn(DiagMessage(mSource) + << "unexpected chunk type " + << (int) util::deviceToHost16(parser.getChunk()->type)); + break; + } + } + + if (parser.getEvent() == ResChunkPullParser::Event::BadDocument) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt ResTable_package: " + << parser.getLastError()); + return false; + } + + // Now go through the table and change local resource ID references to + // symbolic references. + ReferenceIdToNameVisitor visitor(&mIdIndex); + for (auto& package : mTable->packages) { + for (auto& type : package->types) { + for (auto& entry : type->entries) { + for (auto& configValue : entry->values) { + configValue.value->accept(&visitor); + } + } + } + } + return true; +} + +bool BinaryResourceParser::parsePublic(const ResourceTablePackage* package, + const ResChunk_header* chunk) { + const Public_header* header = convertTo<Public_header>(chunk); + if (!header) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt Public_header chunk"); + return false; + } + + if (header->typeId == 0) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "invalid type ID " + << (int) header->typeId); + return false; + } + + StringPiece16 typeStr16 = util::getString(mTypePool, header->typeId - 1); + const ResourceType* parsedType = parseResourceType(typeStr16); + if (!parsedType) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "invalid type '" << typeStr16 << "'"); + return false; + } + + const uintptr_t chunkEnd = (uintptr_t) chunk + util::deviceToHost32(chunk->size); + const Public_entry* entry = (const Public_entry*) getChunkData(&header->header); + for (uint32_t i = 0; i < util::deviceToHost32(header->count); i++) { + if ((uintptr_t) entry + sizeof(*entry) > chunkEnd) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "Public_entry data section is too long"); + return false; + } + + const ResourceId resId(package->id.value(), header->typeId, + util::deviceToHost16(entry->entryId)); + + const ResourceName name(package->name, *parsedType, + util::getString(mKeyPool, entry->key.index).toString()); + + Symbol symbol; + if (mSourcePool.getError() == NO_ERROR) { + symbol.source.path = util::utf16ToUtf8(util::getString( + mSourcePool, util::deviceToHost32(entry->source.path.index))); + symbol.source.line = util::deviceToHost32(entry->source.line); + } + + StringPiece16 comment = util::getString(mSourcePool, + util::deviceToHost32(entry->source.comment.index)); + if (!comment.empty()) { + symbol.comment = comment.toString(); + } + + switch (util::deviceToHost16(entry->state)) { + case Public_entry::kPrivate: + symbol.state = SymbolState::kPrivate; + break; + + case Public_entry::kPublic: + symbol.state = SymbolState::kPublic; + break; + + case Public_entry::kUndefined: + symbol.state = SymbolState::kUndefined; + break; + } + + if (!mTable->setSymbolStateAllowMangled(name, resId, symbol, mContext->getDiagnostics())) { + return false; + } + + // Add this resource name->id mapping to the index so + // that we can resolve all ID references to name references. + auto cacheIter = mIdIndex.find(resId); + if (cacheIter == mIdIndex.end()) { + mIdIndex.insert({ resId, name }); + } + + entry++; + } + return true; +} + +bool BinaryResourceParser::parseTypeSpec(const ResChunk_header* chunk) { + if (mTypePool.getError() != NO_ERROR) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "missing type string pool"); + return false; + } + + const ResTable_typeSpec* typeSpec = convertTo<ResTable_typeSpec>(chunk); + if (!typeSpec) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt ResTable_typeSpec chunk"); + return false; + } + + if (typeSpec->id == 0) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "ResTable_typeSpec has invalid id: " << typeSpec->id); + return false; + } + return true; +} + +bool BinaryResourceParser::parseType(const ResourceTablePackage* package, + const ResChunk_header* chunk) { + if (mTypePool.getError() != NO_ERROR) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "missing type string pool"); + return false; + } + + if (mKeyPool.getError() != NO_ERROR) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "missing key string pool"); + return false; + } + + const ResTable_type* type = convertTo<ResTable_type>(chunk); + if (!type) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "corrupt ResTable_type chunk"); + return false; + } + + if (type->id == 0) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "ResTable_type has invalid id: " << (int) type->id); + return false; + } + + ConfigDescription config; + config.copyFromDtoH(type->config); + + StringPiece16 typeStr16 = util::getString(mTypePool, type->id - 1); + + const ResourceType* parsedType = parseResourceType(typeStr16); + if (!parsedType) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "invalid type name '" << typeStr16 + << "' for type with ID " << (int) type->id); + return false; + } + + TypeVariant tv(type); + for (auto it = tv.beginEntries(); it != tv.endEntries(); ++it) { + const ResTable_entry* entry = *it; + if (!entry) { + continue; + } + + const ResourceName name(package->name, *parsedType, + util::getString(mKeyPool, + util::deviceToHost32(entry->key.index)).toString()); + + const ResourceId resId(package->id.value(), type->id, static_cast<uint16_t>(it.index())); + + std::unique_ptr<Value> resourceValue; + const ResTable_entry_source* sourceBlock = nullptr; + + if (entry->flags & ResTable_entry::FLAG_COMPLEX) { + const ResTable_map_entry* mapEntry = static_cast<const ResTable_map_entry*>(entry); + if (util::deviceToHost32(mapEntry->size) - sizeof(*mapEntry) == sizeof(*sourceBlock)) { + const uint8_t* data = (const uint8_t*) mapEntry; + data += util::deviceToHost32(mapEntry->size) - sizeof(*sourceBlock); + sourceBlock = (const ResTable_entry_source*) data; + } + + // TODO(adamlesinski): Check that the entry count is valid. + resourceValue = parseMapEntry(name, config, mapEntry); + } else { + if (util::deviceToHost32(entry->size) - sizeof(*entry) == sizeof(*sourceBlock)) { + const uint8_t* data = (const uint8_t*) entry; + data += util::deviceToHost32(entry->size) - sizeof(*sourceBlock); + sourceBlock = (const ResTable_entry_source*) data; + } + + const Res_value* value = (const Res_value*)( + (const uint8_t*) entry + util::deviceToHost32(entry->size)); + resourceValue = parseValue(name, config, value, entry->flags); + } + + if (!resourceValue) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "failed to parse value for resource " << name + << " (" << resId << ") with configuration '" + << config << "'"); + return false; + } + + Source source = mSource; + if (sourceBlock) { + StringPiece path = util::getString8(mSourcePool, + util::deviceToHost32(sourceBlock->path.index)); + if (!path.empty()) { + source.path = path.toString(); + } + source.line = util::deviceToHost32(sourceBlock->line); + + if (Style* style = valueCast<Style>(resourceValue.get())) { + // The parent's source is the same as the resource itself, set it here. + if (style->parent) { + style->parent.value().setSource(source); + } + } + } + + StringPiece16 comment = util::getString(mSourcePool, + util::deviceToHost32(sourceBlock->comment.index)); + if (!comment.empty()) { + resourceValue->setComment(comment); + } + + resourceValue->setSource(source); + if (!mTable->addResourceAllowMangled(name, config, std::move(resourceValue), + mContext->getDiagnostics())) { + return false; + } + + if ((entry->flags & ResTable_entry::FLAG_PUBLIC) != 0) { + Symbol symbol; + symbol.state = SymbolState::kPublic; + symbol.source = mSource.withLine(0); + if (!mTable->setSymbolStateAllowMangled(name, resId, symbol, + mContext->getDiagnostics())) { + return false; + } + } + + // Add this resource name->id mapping to the index so + // that we can resolve all ID references to name references. + auto cacheIter = mIdIndex.find(resId); + if (cacheIter == mIdIndex.end()) { + mIdIndex.insert({ resId, name }); + } + } + return true; +} + +std::unique_ptr<Item> BinaryResourceParser::parseValue(const ResourceNameRef& name, + const ConfigDescription& config, + const Res_value* value, + uint16_t flags) { + if (name.type == ResourceType::kId) { + return util::make_unique<Id>(); + } + + const uint32_t data = util::deviceToHost32(value->data); + + if (value->dataType == Res_value::TYPE_STRING) { + StringPiece16 str = util::getString(mValuePool, data); + + const ResStringPool_span* spans = mValuePool.styleAt(data); + + // Check if the string has a valid style associated with it. + if (spans != nullptr && spans->name.index != ResStringPool_span::END) { + StyleString styleStr = { str.toString() }; + while (spans->name.index != ResStringPool_span::END) { + styleStr.spans.push_back(Span{ + util::getString(mValuePool, spans->name.index).toString(), + spans->firstChar, + spans->lastChar + }); + spans++; + } + return util::make_unique<StyledString>(mTable->stringPool.makeRef( + styleStr, StringPool::Context{1, config})); + } else { + if (name.type != ResourceType::kString && + util::stringStartsWith<char16_t>(str, u"res/")) { + // This must be a FileReference. + return util::make_unique<FileReference>(mTable->stringPool.makeRef( + str, StringPool::Context{ 0, config })); + } + + // There are no styles associated with this string, so treat it as + // a simple string. + return util::make_unique<String>(mTable->stringPool.makeRef( + str, StringPool::Context{1, config})); + } + } + + if (value->dataType == Res_value::TYPE_REFERENCE || + value->dataType == Res_value::TYPE_ATTRIBUTE) { + const Reference::Type type = (value->dataType == Res_value::TYPE_REFERENCE) ? + Reference::Type::kResource : Reference::Type::kAttribute; + + if (data != 0) { + // This is a normal reference. + return util::make_unique<Reference>(data, type); + } + + // This reference has an invalid ID. Check if it is an unresolved symbol. + if (Maybe<Reference> ref = getSymbol(&value->data)) { + ref.value().referenceType = type; + return util::make_unique<Reference>(std::move(ref.value())); + } + + // This is not an unresolved symbol, so it must be the magic @null reference. + Res_value nullType = {}; + nullType.dataType = Res_value::TYPE_REFERENCE; + return util::make_unique<BinaryPrimitive>(nullType); + } + + if (value->dataType == ExtendedTypes::TYPE_RAW_STRING) { + return util::make_unique<RawString>(mTable->stringPool.makeRef( + util::getString(mValuePool, data), StringPool::Context{ 1, config })); + } + + // Treat this as a raw binary primitive. + return util::make_unique<BinaryPrimitive>(*value); +} + +std::unique_ptr<Value> BinaryResourceParser::parseMapEntry(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + switch (name.type) { + case ResourceType::kStyle: + return parseStyle(name, config, map); + case ResourceType::kAttrPrivate: + // fallthrough + case ResourceType::kAttr: + return parseAttr(name, config, map); + case ResourceType::kArray: + return parseArray(name, config, map); + case ResourceType::kStyleable: + return parseStyleable(name, config, map); + case ResourceType::kPlurals: + return parsePlural(name, config, map); + default: + assert(false && "unknown map type"); + break; + } + return {}; +} + +std::unique_ptr<Style> BinaryResourceParser::parseStyle(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Style> style = util::make_unique<Style>(); + if (util::deviceToHost32(map->parent.ident) == 0) { + // The parent is either not set or it is an unresolved symbol. + // Check to see if it is a symbol. + style->parent = getSymbol(&map->parent.ident); + + } else { + // The parent is a regular reference to a resource. + style->parent = Reference(util::deviceToHost32(map->parent.ident)); + } + + for (const ResTable_map& mapEntry : map) { + if (Res_INTERNALID(util::deviceToHost32(mapEntry.name.ident))) { + if (style->entries.empty()) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "out-of-sequence meta data in style"); + return {}; + } + collectMetaData(mapEntry, &style->entries.back().key); + continue; + } + + style->entries.emplace_back(); + Style::Entry& styleEntry = style->entries.back(); + + if (util::deviceToHost32(mapEntry.name.ident) == 0) { + // The map entry's key (attribute) is not set. This must be + // a symbol reference, so resolve it. + Maybe<Reference> symbol = getSymbol(&mapEntry.name.ident); + if (!symbol) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "unresolved style attribute"); + return {}; + } + styleEntry.key = std::move(symbol.value()); + + } else { + // The map entry's key (attribute) is a regular reference. + styleEntry.key.id = ResourceId(util::deviceToHost32(mapEntry.name.ident)); + } + + // Parse the attribute's value. + styleEntry.value = parseValue(name, config, &mapEntry.value, 0); + if (!styleEntry.value) { + return {}; + } + } + return style; +} + +std::unique_ptr<Attribute> BinaryResourceParser::parseAttr(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + const bool isWeak = (util::deviceToHost16(map->flags) & ResTable_entry::FLAG_WEAK) != 0; + std::unique_ptr<Attribute> attr = util::make_unique<Attribute>(isWeak); + + // First we must discover what type of attribute this is. Find the type mask. + auto typeMaskIter = std::find_if(begin(map), end(map), [](const ResTable_map& entry) -> bool { + return util::deviceToHost32(entry.name.ident) == ResTable_map::ATTR_TYPE; + }); + + if (typeMaskIter != end(map)) { + attr->typeMask = util::deviceToHost32(typeMaskIter->value.data); + } + + for (const ResTable_map& mapEntry : map) { + if (Res_INTERNALID(util::deviceToHost32(mapEntry.name.ident))) { + switch (util::deviceToHost32(mapEntry.name.ident)) { + case ResTable_map::ATTR_MIN: + attr->minInt = static_cast<int32_t>(mapEntry.value.data); + break; + case ResTable_map::ATTR_MAX: + attr->maxInt = static_cast<int32_t>(mapEntry.value.data); + break; + } + continue; + } + + if (attr->typeMask & (ResTable_map::TYPE_ENUM | ResTable_map::TYPE_FLAGS)) { + Attribute::Symbol symbol; + symbol.value = util::deviceToHost32(mapEntry.value.data); + if (util::deviceToHost32(mapEntry.name.ident) == 0) { + // The map entry's key (id) is not set. This must be + // a symbol reference, so resolve it. + Maybe<Reference> ref = getSymbol(&mapEntry.name.ident); + if (!ref) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "unresolved attribute symbol"); + return {}; + } + symbol.symbol = std::move(ref.value()); + + } else { + // The map entry's key (id) is a regular reference. + symbol.symbol.id = ResourceId(util::deviceToHost32(mapEntry.name.ident)); + } + + attr->symbols.push_back(std::move(symbol)); + } + } + + // TODO(adamlesinski): Find i80n, attributes. + return attr; +} + +static bool isMetaDataEntry(const ResTable_map& mapEntry) { + switch (util::deviceToHost32(mapEntry.name.ident)) { + case ExtendedResTableMapTypes::ATTR_SOURCE_PATH: + case ExtendedResTableMapTypes::ATTR_SOURCE_LINE: + case ExtendedResTableMapTypes::ATTR_COMMENT: + return true; + } + return false; +} + +bool BinaryResourceParser::collectMetaData(const ResTable_map& mapEntry, Value* value) { + switch (util::deviceToHost32(mapEntry.name.ident)) { + case ExtendedResTableMapTypes::ATTR_SOURCE_PATH: + value->setSource(Source(util::getString8(mSourcePool, + util::deviceToHost32(mapEntry.value.data)))); + return true; + break; + + case ExtendedResTableMapTypes::ATTR_SOURCE_LINE: + value->setSource(value->getSource().withLine(util::deviceToHost32(mapEntry.value.data))); + return true; + break; + + case ExtendedResTableMapTypes::ATTR_COMMENT: + value->setComment(util::getString(mSourcePool, util::deviceToHost32(mapEntry.value.data))); + return true; + break; + } + return false; +} + +std::unique_ptr<Array> BinaryResourceParser::parseArray(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Array> array = util::make_unique<Array>(); + Source source; + for (const ResTable_map& mapEntry : map) { + if (isMetaDataEntry(mapEntry)) { + if (array->items.empty()) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "out-of-sequence meta data in array"); + return {}; + } + collectMetaData(mapEntry, array->items.back().get()); + continue; + } + + array->items.push_back(parseValue(name, config, &mapEntry.value, 0)); + } + return array; +} + +std::unique_ptr<Styleable> BinaryResourceParser::parseStyleable(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Styleable> styleable = util::make_unique<Styleable>(); + for (const ResTable_map& mapEntry : map) { + if (isMetaDataEntry(mapEntry)) { + if (styleable->entries.empty()) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "out-of-sequence meta data in styleable"); + return {}; + } + collectMetaData(mapEntry, &styleable->entries.back()); + continue; + } + + if (util::deviceToHost32(mapEntry.name.ident) == 0) { + // The map entry's key (attribute) is not set. This must be + // a symbol reference, so resolve it. + Maybe<Reference> ref = getSymbol(&mapEntry.name.ident); + if (!ref) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "unresolved styleable symbol"); + return {}; + } + styleable->entries.emplace_back(std::move(ref.value())); + + } else { + // The map entry's key (attribute) is a regular reference. + styleable->entries.emplace_back(util::deviceToHost32(mapEntry.name.ident)); + } + } + return styleable; +} + +std::unique_ptr<Plural> BinaryResourceParser::parsePlural(const ResourceNameRef& name, + const ConfigDescription& config, + const ResTable_map_entry* map) { + std::unique_ptr<Plural> plural = util::make_unique<Plural>(); + Item* lastEntry = nullptr; + for (const ResTable_map& mapEntry : map) { + if (isMetaDataEntry(mapEntry)) { + if (!lastEntry) { + mContext->getDiagnostics()->error(DiagMessage(mSource) + << "out-of-sequence meta data in plural"); + return {}; + } + collectMetaData(mapEntry, lastEntry); + continue; + } + + std::unique_ptr<Item> item = parseValue(name, config, &mapEntry.value, 0); + if (!item) { + return {}; + } + + lastEntry = item.get(); + + switch (util::deviceToHost32(mapEntry.name.ident)) { + case ResTable_map::ATTR_ZERO: + plural->values[Plural::Zero] = std::move(item); + break; + case ResTable_map::ATTR_ONE: + plural->values[Plural::One] = std::move(item); + break; + case ResTable_map::ATTR_TWO: + plural->values[Plural::Two] = std::move(item); + break; + case ResTable_map::ATTR_FEW: + plural->values[Plural::Few] = std::move(item); + break; + case ResTable_map::ATTR_MANY: + plural->values[Plural::Many] = std::move(item); + break; + case ResTable_map::ATTR_OTHER: + plural->values[Plural::Other] = std::move(item); + break; + } + } + return plural; +} + +} // namespace aapt diff --git a/tools/aapt2/BinaryResourceParser.h b/tools/aapt2/unflatten/BinaryResourceParser.h index 3aab301ec199..0745a592c296 100644 --- a/tools/aapt2/BinaryResourceParser.h +++ b/tools/aapt2/unflatten/BinaryResourceParser.h @@ -17,11 +17,13 @@ #ifndef AAPT_BINARY_RESOURCE_PARSER_H #define AAPT_BINARY_RESOURCE_PARSER_H -#include "Resolver.h" #include "ResourceTable.h" #include "ResourceValues.h" #include "Source.h" +#include "process/IResourceTableConsumer.h" +#include "util/Util.h" + #include <androidfw/ResourceTypes.h> #include <string> @@ -42,11 +44,8 @@ public: * Creates a parser, which will read `len` bytes from `data`, and * add any resources parsed to `table`. `source` is for logging purposes. */ - BinaryResourceParser(const std::shared_ptr<ResourceTable>& table, - const std::shared_ptr<IResolver>& resolver, - const Source& source, - const std::u16string& defaultPackage, - const void* data, size_t len); + BinaryResourceParser(IAaptContext* context, ResourceTable* table, const Source& source, + const void* data, size_t dataLen); BinaryResourceParser(const BinaryResourceParser&) = delete; // No copy. @@ -58,49 +57,52 @@ public: private: // Helper method to retrieve the symbol name for a given table offset specified // as a pointer. - bool getSymbol(const void* data, ResourceNameRef* outSymbol); + Maybe<Reference> getSymbol(const void* data); bool parseTable(const android::ResChunk_header* chunk); bool parseSymbolTable(const android::ResChunk_header* chunk); - - // Looks up the resource ID in the reference and converts it to a name if available. - bool idToName(Reference* reference); - bool parsePackage(const android::ResChunk_header* chunk); - bool parsePublic(const android::ResChunk_header* chunk); + bool parsePublic(const ResourceTablePackage* package, const android::ResChunk_header* chunk); bool parseTypeSpec(const android::ResChunk_header* chunk); - bool parseType(const android::ResChunk_header* chunk); + bool parseType(const ResourceTablePackage* package, const android::ResChunk_header* chunk); - std::unique_ptr<Item> parseValue(const ResourceNameRef& name, - const ConfigDescription& config, const android::Res_value* value, uint16_t flags); + std::unique_ptr<Item> parseValue(const ResourceNameRef& name, const ConfigDescription& config, + const android::Res_value* value, uint16_t flags); std::unique_ptr<Value> parseMapEntry(const ResourceNameRef& name, - const ConfigDescription& config, const android::ResTable_map_entry* map); + const ConfigDescription& config, + const android::ResTable_map_entry* map); - std::unique_ptr<Style> parseStyle(const ResourceNameRef& name, - const ConfigDescription& config, const android::ResTable_map_entry* map); + std::unique_ptr<Style> parseStyle(const ResourceNameRef& name, const ConfigDescription& config, + const android::ResTable_map_entry* map); std::unique_ptr<Attribute> parseAttr(const ResourceNameRef& name, - const ConfigDescription& config, const android::ResTable_map_entry* map); + const ConfigDescription& config, + const android::ResTable_map_entry* map); - std::unique_ptr<Array> parseArray(const ResourceNameRef& name, - const ConfigDescription& config, const android::ResTable_map_entry* map); + std::unique_ptr<Array> parseArray(const ResourceNameRef& name, const ConfigDescription& config, + const android::ResTable_map_entry* map); std::unique_ptr<Plural> parsePlural(const ResourceNameRef& name, - const ConfigDescription& config, const android::ResTable_map_entry* map); + const ConfigDescription& config, + const android::ResTable_map_entry* map); std::unique_ptr<Styleable> parseStyleable(const ResourceNameRef& name, - const ConfigDescription& config, const android::ResTable_map_entry* map); + const ConfigDescription& config, + const android::ResTable_map_entry* map); - std::shared_ptr<ResourceTable> mTable; + /** + * If the mapEntry is a special type that denotes meta data (source, comment), then it is + * read and added to the Value. + * Returns true if the mapEntry was meta data. + */ + bool collectMetaData(const android::ResTable_map& mapEntry, Value* value); - std::shared_ptr<IResolver> mResolver; + IAaptContext* mContext; + ResourceTable* mTable; const Source mSource; - // The package name of the resource table. - std::u16string mDefaultPackage; - const void* mData; const size_t mDataLen; @@ -146,13 +148,11 @@ namespace android { */ inline const ResTable_map* begin(const ResTable_map_entry* map) { - return reinterpret_cast<const ResTable_map*>( - reinterpret_cast<const uint8_t*>(map) + map->size); + return (const ResTable_map*)((const uint8_t*) map + aapt::util::deviceToHost32(map->size)); } inline const ResTable_map* end(const ResTable_map_entry* map) { - return reinterpret_cast<const ResTable_map*>( - reinterpret_cast<const uint8_t*>(map) + map->size) + map->count; + return begin(map) + aapt::util::deviceToHost32(map->count); } } // namespace android diff --git a/tools/aapt2/unflatten/FileExportHeaderReader.h b/tools/aapt2/unflatten/FileExportHeaderReader.h new file mode 100644 index 000000000000..e552ea176417 --- /dev/null +++ b/tools/aapt2/unflatten/FileExportHeaderReader.h @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_UNFLATTEN_FILEEXPORTHEADERREADER_H +#define AAPT_UNFLATTEN_FILEEXPORTHEADERREADER_H + +#include "ResChunkPullParser.h" +#include "Resource.h" +#include "ResourceUtils.h" + +#include "flatten/ResourceTypeExtensions.h" +#include "util/StringPiece.h" +#include "util/Util.h" + +#include <androidfw/ResourceTypes.h> +#include <sstream> +#include <string> + +namespace aapt { + +static ssize_t parseFileExportHeaderImpl(const void* data, const size_t len, + const FileExport_header** outFileExport, + const ExportedSymbol** outExportedSymbolIndices, + android::ResStringPool* outStringPool, + std::string* outError) { + ResChunkPullParser parser(data, len); + if (!ResChunkPullParser::isGoodEvent(parser.next())) { + if (outError) *outError = parser.getLastError(); + return -1; + } + + if (util::deviceToHost16(parser.getChunk()->type) != RES_FILE_EXPORT_TYPE) { + if (outError) *outError = "no FileExport_header found"; + return -1; + } + + const FileExport_header* fileExport = convertTo<FileExport_header>(parser.getChunk()); + if (!fileExport) { + if (outError) *outError = "corrupt FileExport_header"; + return -1; + } + + if (memcmp(fileExport->magic, "AAPT", sizeof(fileExport->magic)) != 0) { + if (outError) *outError = "invalid magic value"; + return -1; + } + + const size_t exportedSymbolCount = util::deviceToHost32(fileExport->exportedSymbolCount); + + // Verify that we have enough space for all those symbols. + size_t dataLen = getChunkDataLen(&fileExport->header); + if (exportedSymbolCount > dataLen / sizeof(ExportedSymbol)) { + if (outError) *outError = "too many symbols"; + return -1; + } + + const size_t symbolIndicesSize = exportedSymbolCount * sizeof(ExportedSymbol); + + const void* strPoolData = getChunkData(&fileExport->header) + symbolIndicesSize; + const size_t strPoolDataLen = dataLen - symbolIndicesSize; + if (outStringPool->setTo(strPoolData, strPoolDataLen, false) != android::NO_ERROR) { + if (outError) *outError = "corrupt string pool"; + return -1; + } + + *outFileExport = fileExport; + *outExportedSymbolIndices = (const ExportedSymbol*) getChunkData( + &fileExport->header); + return util::deviceToHost16(fileExport->header.headerSize) + symbolIndicesSize + + outStringPool->bytes(); +} + +static ssize_t getWrappedDataOffset(const void* data, size_t len, std::string* outError) { + const FileExport_header* header = nullptr; + const ExportedSymbol* entries = nullptr; + android::ResStringPool pool; + return parseFileExportHeaderImpl(data, len, &header, &entries, &pool, outError); +} + +/** + * Reads the FileExport_header and populates outRes with the values in that header. + */ +static ssize_t unwrapFileExportHeader(const void* data, size_t len, ResourceFile* outRes, + std::string* outError) { + + const FileExport_header* fileExport = nullptr; + const ExportedSymbol* entries = nullptr; + android::ResStringPool symbolPool; + const ssize_t offset = parseFileExportHeaderImpl(data, len, &fileExport, &entries, &symbolPool, + outError); + if (offset < 0) { + return offset; + } + + const size_t exportedSymbolCount = util::deviceToHost32(fileExport->exportedSymbolCount); + outRes->exportedSymbols.clear(); + outRes->exportedSymbols.reserve(exportedSymbolCount); + + for (size_t i = 0; i < exportedSymbolCount; i++) { + const StringPiece16 str = util::getString(symbolPool, + util::deviceToHost32(entries[i].name.index)); + StringPiece16 packageStr, typeStr, entryStr; + ResourceUtils::extractResourceName(str, &packageStr, &typeStr, &entryStr); + const ResourceType* resType = parseResourceType(typeStr); + if (!resType || entryStr.empty()) { + if (outError) { + std::stringstream errorStr; + errorStr << "invalid exported symbol at index=" + << util::deviceToHost32(entries[i].name.index) + << " (" << str << ")"; + *outError = errorStr.str(); + } + return -1; + } + + outRes->exportedSymbols.push_back(SourcedResourceName{ + ResourceName{ packageStr.toString(), *resType, entryStr.toString() }, + util::deviceToHost32(entries[i].line) }); + } + + const StringPiece16 str = util::getString(symbolPool, + util::deviceToHost32(fileExport->name.index)); + StringPiece16 packageStr, typeStr, entryStr; + ResourceUtils::extractResourceName(str, &packageStr, &typeStr, &entryStr); + const ResourceType* resType = parseResourceType(typeStr); + if (!resType || entryStr.empty()) { + if (outError) { + std::stringstream errorStr; + errorStr << "invalid resource name at index=" + << util::deviceToHost32(fileExport->name.index) + << " (" << str << ")"; + *outError = errorStr.str(); + } + return -1; + } + + outRes->name = ResourceName{ packageStr.toString(), *resType, entryStr.toString() }; + outRes->source.path = util::utf16ToUtf8( + util::getString(symbolPool, util::deviceToHost32(fileExport->source.index))); + outRes->config.copyFromDtoH(fileExport->config); + return offset; +} + +} // namespace aapt + +#endif /* AAPT_UNFLATTEN_FILEEXPORTHEADERREADER_H */ diff --git a/tools/aapt2/unflatten/FileExportHeaderReader_test.cpp b/tools/aapt2/unflatten/FileExportHeaderReader_test.cpp new file mode 100644 index 000000000000..a76c83bdbd9a --- /dev/null +++ b/tools/aapt2/unflatten/FileExportHeaderReader_test.cpp @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Resource.h" + +#include "flatten/FileExportWriter.h" +#include "unflatten/FileExportHeaderReader.h" +#include "util/BigBuffer.h" +#include "util/Util.h" + +#include "test/Common.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(FileExportHeaderReaderTest, ReadHeaderWithNoSymbolExports) { + ResourceFile resFile = { + test::parseNameOrDie(u"@android:layout/main.xml"), + test::parseConfigOrDie("sw600dp-v4"), + Source{ "res/layout/main.xml" }, + }; + + BigBuffer buffer(1024); + ChunkWriter writer = wrapBufferWithFileExportHeader(&buffer, &resFile); + *writer.getBuffer()->nextBlock<uint32_t>() = 42u; + writer.finish(); + + std::unique_ptr<uint8_t[]> data = util::copy(buffer); + + ResourceFile actualResFile; + + ssize_t offset = unwrapFileExportHeader(data.get(), buffer.size(), &actualResFile, nullptr); + ASSERT_GT(offset, 0); + + EXPECT_EQ(offset, getWrappedDataOffset(data.get(), buffer.size(), nullptr)); + + EXPECT_EQ(actualResFile.config, test::parseConfigOrDie("sw600dp-v4")); + EXPECT_EQ(actualResFile.name, test::parseNameOrDie(u"@android:layout/main.xml")); + EXPECT_EQ(actualResFile.source.path, "res/layout/main.xml"); + + EXPECT_EQ(*(uint32_t*)(data.get() + offset), 42u); +} + +} // namespace aapt diff --git a/tools/aapt2/ResChunkPullParser.cpp b/tools/aapt2/unflatten/ResChunkPullParser.cpp index 78ea60e795fc..6f8bb1b29b62 100644 --- a/tools/aapt2/ResChunkPullParser.cpp +++ b/tools/aapt2/unflatten/ResChunkPullParser.cpp @@ -14,7 +14,8 @@ * limitations under the License. */ -#include "ResChunkPullParser.h" +#include "unflatten/ResChunkPullParser.h" +#include "util/Util.h" #include <androidfw/ResourceTypes.h> #include <cstddef> @@ -31,12 +32,11 @@ ResChunkPullParser::Event ResChunkPullParser::next() { if (mEvent == Event::StartDocument) { mCurrentChunk = mData; } else { - mCurrentChunk = reinterpret_cast<const ResChunk_header*>( - reinterpret_cast<const char*>(mCurrentChunk) + mCurrentChunk->size); + mCurrentChunk = (const ResChunk_header*) + (((const char*) mCurrentChunk) + util::deviceToHost32(mCurrentChunk->size)); } - const std::ptrdiff_t diff = reinterpret_cast<const char*>(mCurrentChunk) - - reinterpret_cast<const char*>(mData); + const std::ptrdiff_t diff = (const char*) mCurrentChunk - (const char*) mData; assert(diff >= 0 && "diff is negative"); const size_t offset = static_cast<const size_t>(diff); @@ -49,15 +49,16 @@ ResChunkPullParser::Event ResChunkPullParser::next() { return (mEvent = Event::BadDocument); } - if (mCurrentChunk->headerSize < sizeof(ResChunk_header)) { + if (util::deviceToHost16(mCurrentChunk->headerSize) < sizeof(ResChunk_header)) { mLastError = "chunk has too small header"; mCurrentChunk = nullptr; return (mEvent = Event::BadDocument); - } else if (mCurrentChunk->size < mCurrentChunk->headerSize) { + } else if (util::deviceToHost32(mCurrentChunk->size) < + util::deviceToHost16(mCurrentChunk->headerSize)) { mLastError = "chunk's total size is smaller than header"; mCurrentChunk = nullptr; return (mEvent = Event::BadDocument); - } else if (offset + mCurrentChunk->size > mLen) { + } else if (offset + util::deviceToHost32(mCurrentChunk->size) > mLen) { mLastError = "chunk's data extends past the end of the document"; mCurrentChunk = nullptr; return (mEvent = Event::BadDocument); diff --git a/tools/aapt2/ResChunkPullParser.h b/tools/aapt2/unflatten/ResChunkPullParser.h index 1426ed23a5c7..a51d5bfdc9b3 100644 --- a/tools/aapt2/ResChunkPullParser.h +++ b/tools/aapt2/unflatten/ResChunkPullParser.h @@ -17,6 +17,8 @@ #ifndef AAPT_RES_CHUNK_PULL_PARSER_H #define AAPT_RES_CHUNK_PULL_PARSER_H +#include "util/Util.h" + #include <androidfw/ResourceTypes.h> #include <string> @@ -76,18 +78,18 @@ private: template <typename T> inline static const T* convertTo(const android::ResChunk_header* chunk) { - if (chunk->headerSize < sizeof(T)) { + if (util::deviceToHost16(chunk->headerSize) < sizeof(T)) { return nullptr; } return reinterpret_cast<const T*>(chunk); } -inline static const uint8_t* getChunkData(const android::ResChunk_header& chunk) { - return reinterpret_cast<const uint8_t*>(&chunk) + chunk.headerSize; +inline static const uint8_t* getChunkData(const android::ResChunk_header* chunk) { + return reinterpret_cast<const uint8_t*>(chunk) + util::deviceToHost16(chunk->headerSize); } -inline static size_t getChunkDataLen(const android::ResChunk_header& chunk) { - return chunk.size - chunk.headerSize; +inline static uint32_t getChunkDataLen(const android::ResChunk_header* chunk) { + return util::deviceToHost32(chunk->size) - util::deviceToHost16(chunk->headerSize); } // diff --git a/tools/aapt2/BigBuffer.cpp b/tools/aapt2/util/BigBuffer.cpp index 8f571728d729..c88e3c102415 100644 --- a/tools/aapt2/BigBuffer.cpp +++ b/tools/aapt2/util/BigBuffer.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "BigBuffer.h" +#include "util/BigBuffer.h" #include <algorithm> #include <memory> diff --git a/tools/aapt2/BigBuffer.h b/tools/aapt2/util/BigBuffer.h index 8b6569c6a8d6..cad2a2e63be1 100644 --- a/tools/aapt2/BigBuffer.h +++ b/tools/aapt2/util/BigBuffer.h @@ -20,6 +20,7 @@ #include <cassert> #include <cstring> #include <memory> +#include <type_traits> #include <vector> namespace aapt { @@ -124,6 +125,7 @@ inline size_t BigBuffer::size() const { template <typename T> inline T* BigBuffer::nextBlock(size_t count) { + static_assert(std::is_standard_layout<T>::value, "T must be standard_layout type"); assert(count != 0); return reinterpret_cast<T*>(nextBlockImpl(sizeof(T) * count)); } diff --git a/tools/aapt2/BigBuffer_test.cpp b/tools/aapt2/util/BigBuffer_test.cpp index 01ee8d7e9ad5..2a24f123e18e 100644 --- a/tools/aapt2/BigBuffer_test.cpp +++ b/tools/aapt2/util/BigBuffer_test.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "BigBuffer.h" +#include "util/BigBuffer.h" #include <gtest/gtest.h> diff --git a/tools/aapt2/util/Comparators.h b/tools/aapt2/util/Comparators.h new file mode 100644 index 000000000000..0ee0bf35457d --- /dev/null +++ b/tools/aapt2/util/Comparators.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_UTIL_COMPARATORS_H +#define AAPT_UTIL_COMPARATORS_H + +#include "ConfigDescription.h" +#include "ResourceTable.h" + +namespace aapt { +namespace cmp { + +inline bool lessThanConfig(const ResourceConfigValue& a, const ConfigDescription& b) { + return a.config < b; +} + +inline bool lessThanType(const std::unique_ptr<ResourceTableType>& a, ResourceType b) { + return a->type < b; +} + +} // namespace cmp +} // namespace aapt + +#endif /* AAPT_UTIL_COMPARATORS_H */ diff --git a/tools/aapt2/Files.cpp b/tools/aapt2/util/Files.cpp index b24ff6bb6291..04e8199d85b4 100644 --- a/tools/aapt2/Files.cpp +++ b/tools/aapt2/util/Files.cpp @@ -14,10 +14,11 @@ * limitations under the License. */ -#include "Files.h" -#include "Util.h" +#include "util/Files.h" +#include "util/Util.h" #include <cerrno> +#include <cstdio> #include <dirent.h> #include <string> #include <sys/stat.h> @@ -28,6 +29,7 @@ #endif namespace aapt { +namespace file { FileType getFileType(const StringPiece& path) { struct stat sb; @@ -61,15 +63,15 @@ FileType getFileType(const StringPiece& path) { } } -std::vector<std::string> listFiles(const StringPiece& root) { +std::vector<std::string> listFiles(const StringPiece& root, std::string* outError) { DIR* dir = opendir(root.data()); if (dir == nullptr) { - Logger::error(Source{ root.toString() }) - << "unable to open file: " - << strerror(errno) - << "." - << std::endl; - return {}; + if (outError) { + std::stringstream errorStr; + errorStr << "unable to open file: " << strerror(errno); + *outError = errorStr.str(); + return {}; + } } std::vector<std::string> files; @@ -105,17 +107,74 @@ bool mkdirs(const StringPiece& path) { return mkdirImpl(path) == 0 || errno == EEXIST; } -std::string getStem(const StringPiece& path) { +StringPiece getStem(const StringPiece& path) { const char* start = path.begin(); const char* end = path.end(); for (const char* current = end - 1; current != start - 1; --current) { if (*current == sDirSep) { - return std::string(start, current - start); + return StringPiece(start, current - start); + } + } + return {}; +} + +StringPiece getFilename(const StringPiece& path) { + const char* end = path.end(); + const char* lastDirSep = path.begin(); + for (const char* c = path.begin(); c != end; ++c) { + if (*c == sDirSep) { + lastDirSep = c + 1; } } + return StringPiece(lastDirSep, end - lastDirSep); +} + +StringPiece getExtension(const StringPiece& path) { + StringPiece filename = getFilename(path); + const char* const end = filename.end(); + const char* c = std::find(filename.begin(), end, '.'); + if (c != end) { + return StringPiece(c, end - c); + } return {}; } +std::string packageToPath(const StringPiece& package) { + std::string outPath; + for (StringPiece part : util::tokenize<char>(package, '.')) { + appendPath(&outPath, part); + } + return outPath; +} + +Maybe<android::FileMap> mmapPath(const StringPiece& path, std::string* outError) { + std::unique_ptr<FILE, decltype(fclose)*> f = { fopen(path.data(), "rb"), fclose }; + if (!f) { + if (outError) *outError = strerror(errno); + return {}; + } + + int fd = fileno(f.get()); + + struct stat fileStats = {}; + if (fstat(fd, &fileStats) != 0) { + if (outError) *outError = strerror(errno); + return {}; + } + + android::FileMap fileMap; + if (fileStats.st_size == 0) { + // mmap doesn't like a length of 0. Instead we return an empty FileMap. + return std::move(fileMap); + } + + if (!fileMap.create(path.data(), fd, 0, fileStats.st_size, true)) { + if (outError) *outError = strerror(errno); + return {}; + } + return std::move(fileMap); +} + bool FileFilter::setPattern(const StringPiece& pattern) { mPatternTokens = util::splitAndLowercase(pattern, ':'); return true; @@ -169,14 +228,10 @@ bool FileFilter::operator()(const std::string& filename, FileType type) const { if (ignore) { if (chatty) { - Logger::warn() - << "skipping " << - (type == FileType::kDirectory ? "dir '" : "file '") - << filename - << "' due to ignore pattern '" - << token - << "'." - << std::endl; + mDiag->warn(DiagMessage() << "skipping " + << (type == FileType::kDirectory ? "dir '" : "file '") + << filename << "' due to ignore pattern '" + << token << "'"); } return false; } @@ -184,5 +239,5 @@ bool FileFilter::operator()(const std::string& filename, FileType type) const { return true; } - +} // namespace file } // namespace aapt diff --git a/tools/aapt2/Files.h b/tools/aapt2/util/Files.h index 844fd2b07189..c58ba5d6d1e3 100644 --- a/tools/aapt2/Files.h +++ b/tools/aapt2/util/Files.h @@ -17,15 +17,20 @@ #ifndef AAPT_FILES_H #define AAPT_FILES_H -#include "Logger.h" +#include "Diagnostics.h" +#include "Maybe.h" #include "Source.h" -#include "StringPiece.h" +#include "util/StringPiece.h" + +#include <utils/FileMap.h> #include <cassert> +#include <memory> #include <string> #include <vector> namespace aapt { +namespace file { #ifdef _WIN32 constexpr const char sDirSep = '\\'; @@ -74,7 +79,28 @@ bool mkdirs(const StringPiece& path); /** * Returns all but the last part of the path. */ -std::string getStem(const StringPiece& path); +StringPiece getStem(const StringPiece& path); + +/** + * Returns the last part of the path with extension. + */ +StringPiece getFilename(const StringPiece& path); + +/** + * Returns the extension of the path. This is the entire string after + * the first '.' of the last part of the path. + */ +StringPiece getExtension(const StringPiece& path); + +/** + * Converts a package name (com.android.app) to a path: com/android/app + */ +std::string packageToPath(const StringPiece& package); + +/** + * Creates a FileMap for the file at path. + */ +Maybe<android::FileMap> mmapPath(const StringPiece& path, std::string* outError); /* * Filter that determines which resource files/directories are @@ -84,6 +110,9 @@ std::string getStem(const StringPiece& path); */ class FileFilter { public: + FileFilter(IDiagnostics* diag) : mDiag(diag) { + } + /* * Patterns syntax: * - Delimiter is : @@ -106,6 +135,7 @@ public: bool operator()(const std::string& filename, FileType type) const; private: + IDiagnostics* mDiag; std::vector<std::string> mPatternTokens; }; @@ -123,6 +153,7 @@ void appendPath(std::string* base, const StringPiece& part, const Ts&... parts) appendPath(base, parts...); } +} // namespace file } // namespace aapt #endif // AAPT_FILES_H diff --git a/tools/aapt2/util/ImmutableMap.h b/tools/aapt2/util/ImmutableMap.h new file mode 100644 index 000000000000..b1f9e9d2fb57 --- /dev/null +++ b/tools/aapt2/util/ImmutableMap.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_UTIL_IMMUTABLEMAP_H +#define AAPT_UTIL_IMMUTABLEMAP_H + +#include "util/TypeTraits.h" + +#include <utility> +#include <vector> + +namespace aapt { + +template <typename TKey, typename TValue> +class ImmutableMap { + static_assert(is_comparable<TKey, TKey>::value, "key is not comparable"); + +private: + std::vector<std::pair<TKey, TValue>> mData; + + explicit ImmutableMap(std::vector<std::pair<TKey, TValue>> data) : mData(std::move(data)) { + } + +public: + using const_iterator = typename decltype(mData)::const_iterator; + + ImmutableMap(ImmutableMap&&) = default; + ImmutableMap& operator=(ImmutableMap&&) = default; + + ImmutableMap(const ImmutableMap&) = delete; + ImmutableMap& operator=(const ImmutableMap&) = delete; + + static ImmutableMap<TKey, TValue> createPreSorted( + std::initializer_list<std::pair<TKey, TValue>> list) { + return ImmutableMap(std::vector<std::pair<TKey, TValue>>(list.begin(), list.end())); + } + + static ImmutableMap<TKey, TValue> createAndSort( + std::initializer_list<std::pair<TKey, TValue>> list) { + std::vector<std::pair<TKey, TValue>> data(list.begin(), list.end()); + std::sort(data.begin(), data.end()); + return ImmutableMap(std::move(data)); + } + + template <typename TKey2, + typename = typename std::enable_if<is_comparable<TKey, TKey2>::value>::type> + const_iterator find(const TKey2& key) const { + auto cmp = [](const std::pair<TKey, TValue>& candidate, const TKey2& target) -> bool { + return candidate.first < target; + }; + + const_iterator endIter = end(); + auto iter = std::lower_bound(mData.begin(), endIter, key, cmp); + if (iter == endIter || iter->first == key) { + return iter; + } + return endIter; + } + + const_iterator begin() const { + return mData.begin(); + } + + const_iterator end() const { + return mData.end(); + } +}; + +} // namespace aapt + +#endif /* AAPT_UTIL_IMMUTABLEMAP_H */ diff --git a/tools/aapt2/Maybe.h b/tools/aapt2/util/Maybe.h index ff6625f4bb5e..10a280347141 100644 --- a/tools/aapt2/Maybe.h +++ b/tools/aapt2/util/Maybe.h @@ -72,7 +72,7 @@ public: * True if this holds a value, false if * it holds Nothing. */ - operator bool() const; + explicit operator bool() const; /** * Gets the value if one exists, or else @@ -275,6 +275,31 @@ inline Maybe<T> make_nothing() { return Maybe<T>(); } +/** + * Define the == operator between Maybe<T> and Maybe<U> if the operator T == U is defined. + * Otherwise this won't be defined and the compiler will yell at the callsite instead of inside + * Maybe.h. + */ +template <typename T, typename U> +auto operator==(const Maybe<T>& a, const Maybe<U>& b) +-> decltype(std::declval<T> == std::declval<U>) { + if (a && b) { + return a.value() == b.value(); + } else if (!a && !b) { + return true; + } + return false; +} + +/** + * Same as operator== but negated. + */ +template <typename T, typename U> +auto operator!=(const Maybe<T>& a, const Maybe<U>& b) +-> decltype(std::declval<T> == std::declval<U>) { + return !(a == b); +} + } // namespace aapt #endif // AAPT_MAYBE_H diff --git a/tools/aapt2/Maybe_test.cpp b/tools/aapt2/util/Maybe_test.cpp index 71bbb940beda..5d42dc3ac3ab 100644 --- a/tools/aapt2/Maybe_test.cpp +++ b/tools/aapt2/util/Maybe_test.cpp @@ -14,11 +14,12 @@ * limitations under the License. */ +#include "test/Common.h" +#include "util/Maybe.h" + #include <gtest/gtest.h> #include <string> -#include "Maybe.h" - namespace aapt { struct Dummy { @@ -85,22 +86,22 @@ struct Dummy { TEST(MaybeTest, MakeNothing) { Maybe<int> val = make_nothing<int>(); - EXPECT_FALSE(val); + AAPT_EXPECT_FALSE(val); Maybe<std::string> val2 = make_nothing<std::string>(); - EXPECT_FALSE(val2); + AAPT_EXPECT_FALSE(val2); val2 = make_nothing<std::string>(); - EXPECT_FALSE(val2); + AAPT_EXPECT_FALSE(val2); } TEST(MaybeTest, MakeSomething) { Maybe<int> val = make_value(23); - ASSERT_TRUE(val); + AAPT_ASSERT_TRUE(val); EXPECT_EQ(23, val.value()); Maybe<std::string> val2 = make_value(std::string("hey")); - ASSERT_TRUE(val2); + AAPT_ASSERT_TRUE(val2); EXPECT_EQ(std::string("hey"), val2.value()); } @@ -118,4 +119,17 @@ TEST(MaybeTest, MoveAssign) { } } +TEST(MaybeTest, Equality) { + Maybe<int> a = 1; + Maybe<int> b = 1; + Maybe<int> c; + + Maybe<int> emptyA, emptyB; + + EXPECT_EQ(a, b); + EXPECT_EQ(b, a); + EXPECT_NE(a, c); + EXPECT_EQ(emptyA, emptyB); +} + } // namespace aapt diff --git a/tools/aapt2/StringPiece.h b/tools/aapt2/util/StringPiece.h index e2a1597caeda..31deb452b53c 100644 --- a/tools/aapt2/StringPiece.h +++ b/tools/aapt2/util/StringPiece.h @@ -36,6 +36,7 @@ template <typename TChar> class BasicStringPiece { public: using const_iterator = const TChar*; + using difference_type = size_t; BasicStringPiece(); BasicStringPiece(const BasicStringPiece<TChar>& str); @@ -229,4 +230,9 @@ inline ::std::ostream& operator<<(::std::ostream& out, const BasicStringPiece<ch } // namespace aapt +inline ::std::ostream& operator<<(::std::ostream& out, const std::u16string& str) { + android::String8 utf8(str.data(), str.size()); + return out.write(utf8.string(), utf8.size()); +} + #endif // AAPT_STRING_PIECE_H diff --git a/tools/aapt2/StringPiece_test.cpp b/tools/aapt2/util/StringPiece_test.cpp index 43f7a370d23c..d49b67ffa88e 100644 --- a/tools/aapt2/StringPiece_test.cpp +++ b/tools/aapt2/util/StringPiece_test.cpp @@ -19,7 +19,7 @@ #include <string> #include <vector> -#include "StringPiece.h" +#include "util/StringPiece.h" namespace aapt { diff --git a/tools/aapt2/util/TypeTraits.h b/tools/aapt2/util/TypeTraits.h new file mode 100644 index 000000000000..76c13d615e41 --- /dev/null +++ b/tools/aapt2/util/TypeTraits.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_UTIL_TYPETRAITS_H +#define AAPT_UTIL_TYPETRAITS_H + +#include <type_traits> + +namespace aapt { + +#define DEFINE_HAS_BINARY_OP_TRAIT(name, op) \ + template <typename T, typename U> \ + struct name { \ + template <typename V, typename W> \ + static constexpr decltype(std::declval<V>() op std::declval<W>(), bool()) test(int) { \ + return true; \ + } \ + template <typename V, typename W> \ + static constexpr bool test(...) { \ + return false; \ + } \ + static constexpr bool value = test<T, U>(int()); \ +} + +DEFINE_HAS_BINARY_OP_TRAIT(has_eq_op, ==); +DEFINE_HAS_BINARY_OP_TRAIT(has_lt_op, <); + +/** + * Type trait that checks if two types can be equated (==) and compared (<). + */ +template <typename T, typename U> +struct is_comparable { + static constexpr bool value = has_eq_op<T, U>::value && has_lt_op<T, U>::value; +}; + +} // namespace aapt + +#endif /* AAPT_UTIL_TYPETRAITS_H */ diff --git a/tools/aapt2/Util.cpp b/tools/aapt2/util/Util.cpp index 03ecd1aca310..7b0c71d93bb5 100644 --- a/tools/aapt2/Util.cpp +++ b/tools/aapt2/util/Util.cpp @@ -14,10 +14,10 @@ * limitations under the License. */ -#include "BigBuffer.h" -#include "Maybe.h" -#include "StringPiece.h" -#include "Util.h" +#include "util/BigBuffer.h" +#include "util/Maybe.h" +#include "util/StringPiece.h" +#include "util/Util.h" #include <algorithm> #include <ostream> @@ -28,9 +28,6 @@ namespace aapt { namespace util { -constexpr const char16_t* kSchemaAuto = u"http://schemas.android.com/apk/res-auto"; -constexpr const char16_t* kSchemaPrefix = u"http://schemas.android.com/apk/res/"; - static std::vector<std::string> splitAndTransform(const StringPiece& str, char sep, const std::function<char(char)>& f) { std::vector<std::string> parts; @@ -76,6 +73,25 @@ StringPiece16 trimWhitespace(const StringPiece16& str) { return StringPiece16(start, end - start); } +StringPiece trimWhitespace(const StringPiece& str) { + if (str.size() == 0 || str.data() == nullptr) { + return str; + } + + const char* start = str.data(); + const char* end = str.data() + str.length(); + + while (start != end && isspace(*start)) { + start++; + } + + while (end != start && isspace(*(end - 1))) { + end--; + } + + return StringPiece(start, end - start); +} + StringPiece16::const_iterator findNonAlphaNumericAndNotInSet(const StringPiece16& str, const StringPiece16& allowedChars) { const auto endIter = str.end(); @@ -122,6 +138,29 @@ bool isJavaClassName(const StringPiece16& str) { return pieces >= 2; } +bool isJavaPackageName(const StringPiece16& str) { + if (str.empty()) { + return false; + } + + size_t pieces = 0; + for (const StringPiece16& piece : tokenize(str, u'.')) { + pieces++; + if (piece.empty()) { + return false; + } + + if (piece.data()[0] == u'_' || piece.data()[piece.size() - 1] == u'_') { + return false; + } + + if (findNonAlphaNumericAndNotInSet(piece, u"_") != piece.end()) { + return false; + } + } + return pieces >= 1; +} + Maybe<std::u16string> getFullyQualifiedClassName(const StringPiece16& package, const StringPiece16& className) { if (className.empty()) { @@ -136,10 +175,11 @@ Maybe<std::u16string> getFullyQualifiedClassName(const StringPiece16& package, return {}; } - std::u16string result(package.data(), package.size()); if (className.data()[0] != u'.') { - result += u'.'; + return {}; } + + std::u16string result(package.data(), package.size()); result.append(className.data(), className.size()); if (!isJavaClassName(result)) { return {}; @@ -147,6 +187,105 @@ Maybe<std::u16string> getFullyQualifiedClassName(const StringPiece16& package, return result; } +static size_t consumeDigits(const char16_t* start, const char16_t* end) { + const char16_t* c = start; + for (; c != end && *c >= u'0' && *c <= u'9'; c++) {} + return static_cast<size_t>(c - start); +} + +bool verifyJavaStringFormat(const StringPiece16& str) { + const char16_t* c = str.begin(); + const char16_t* const end = str.end(); + + size_t argCount = 0; + bool nonpositional = false; + while (c != end) { + if (*c == u'%' && c + 1 < end) { + c++; + + if (*c == u'%') { + c++; + continue; + } + + argCount++; + + size_t numDigits = consumeDigits(c, end); + if (numDigits > 0) { + c += numDigits; + if (c != end && *c != u'$') { + // The digits were a size, but not a positional argument. + nonpositional = true; + } + } else if (*c == u'<') { + // Reusing last argument, bad idea since positions can be moved around + // during translation. + nonpositional = true; + + c++; + + // Optionally we can have a $ after + if (c != end && *c == u'$') { + c++; + } + } else { + nonpositional = true; + } + + // Ignore size, width, flags, etc. + while (c != end && (*c == u'-' || + *c == u'#' || + *c == u'+' || + *c == u' ' || + *c == u',' || + *c == u'(' || + (*c >= u'0' && *c <= '9'))) { + c++; + } + + /* + * This is a shortcut to detect strings that are going to Time.format() + * instead of String.format() + * + * Comparison of String.format() and Time.format() args: + * + * String: ABC E GH ST X abcdefgh nost x + * Time: DEFGHKMS W Za d hkm s w yz + * + * Therefore we know it's definitely Time if we have: + * DFKMWZkmwyz + */ + if (c != end) { + switch (*c) { + case 'D': + case 'F': + case 'K': + case 'M': + case 'W': + case 'Z': + case 'k': + case 'm': + case 'w': + case 'y': + case 'z': + return true; + } + } + } + + if (c != end) { + c++; + } + } + + if (argCount > 1 && nonpositional) { + // Multiple arguments were specified, but some or all were non positional. Translated + // strings may rearrange the order of the arguments, which will break the string. + return false; + } + return true; +} + static Maybe<char16_t> parseUnicodeCodepoint(const char16_t** start, const char16_t* end) { char16_t code = 0; for (size_t i = 0; i < 4 && *start != end; i++, (*start)++) { @@ -175,7 +314,51 @@ StringBuilder& StringBuilder::append(const StringPiece16& str) { const char16_t* start = str.begin(); const char16_t* current = start; while (current != end) { - if (*current == u'"') { + if (mLastCharWasEscape) { + switch (*current) { + case u't': + mStr += u'\t'; + break; + case u'n': + mStr += u'\n'; + break; + case u'#': + mStr += u'#'; + break; + case u'@': + mStr += u'@'; + break; + case u'?': + mStr += u'?'; + break; + case u'"': + mStr += u'"'; + break; + case u'\'': + mStr += u'\''; + break; + case u'\\': + mStr += u'\\'; + break; + case u'u': { + current++; + Maybe<char16_t> c = parseUnicodeCodepoint(¤t, end); + if (!c) { + mError = "invalid unicode escape sequence"; + return *this; + } + mStr += c.value(); + current -= 1; + break; + } + + default: + // Ignore. + break; + } + mLastCharWasEscape = false; + start = current + 1; + } else if (*current == u'"') { if (!mQuote && mTrailingSpace) { // We found an opening quote, and we have // trailing space, so we should append that @@ -208,52 +391,7 @@ StringBuilder& StringBuilder::append(const StringPiece16& str) { } mStr.append(start, current - start); start = current + 1; - - current++; - if (current != end) { - switch (*current) { - case u't': - mStr += u'\t'; - break; - case u'n': - mStr += u'\n'; - break; - case u'#': - mStr += u'#'; - break; - case u'@': - mStr += u'@'; - break; - case u'?': - mStr += u'?'; - break; - case u'"': - mStr += u'"'; - break; - case u'\'': - mStr += u'\''; - break; - case u'\\': - mStr += u'\\'; - break; - case u'u': { - current++; - Maybe<char16_t> c = parseUnicodeCodepoint(¤t, end); - if (!c) { - mError = "invalid unicode escape sequence"; - return *this; - } - mStr += c.value(); - current -= 1; - break; - } - - default: - // Ignore. - break; - } - start = current + 1; - } + mLastCharWasEscape = true; } else if (!mQuote) { // This is not quoted text, so look for whitespace. if (isspace16(*current)) { @@ -327,16 +465,28 @@ std::unique_ptr<uint8_t[]> copy(const BigBuffer& buffer) { return data; } -Maybe<std::u16string> extractPackageFromNamespace(const std::u16string& namespaceUri) { - if (stringStartsWith<char16_t>(namespaceUri, kSchemaPrefix)) { - StringPiece16 schemaPrefix = kSchemaPrefix; - StringPiece16 package = namespaceUri; - return package.substr(schemaPrefix.size(), package.size() - schemaPrefix.size()) - .toString(); - } else if (namespaceUri == kSchemaAuto) { - return std::u16string(); +bool extractResFilePathParts(const StringPiece16& path, StringPiece16* outPrefix, + StringPiece16* outEntry, StringPiece16* outSuffix) { + if (!stringStartsWith<char16_t>(path, u"res/")) { + return false; + } + + StringPiece16::const_iterator lastOccurence = path.end(); + for (auto iter = path.begin() + StringPiece16(u"res/").size(); iter != path.end(); ++iter) { + if (*iter == u'/') { + lastOccurence = iter; + } + } + + if (lastOccurence == path.end()) { + return false; } - return {}; + + auto iter = std::find(lastOccurence, path.end(), u'.'); + *outSuffix = StringPiece16(iter, path.end() - iter); + *outEntry = StringPiece16(lastOccurence + 1, iter - lastOccurence - 1); + *outPrefix = StringPiece16(path.begin(), lastOccurence - path.begin() + 1); + return true; } } // namespace util diff --git a/tools/aapt2/Util.h b/tools/aapt2/util/Util.h index 9cdb152bf41f..0dacbd773488 100644 --- a/tools/aapt2/Util.h +++ b/tools/aapt2/util/Util.h @@ -17,9 +17,9 @@ #ifndef AAPT_UTIL_H #define AAPT_UTIL_H -#include "BigBuffer.h" -#include "Maybe.h" -#include "StringPiece.h" +#include "util/BigBuffer.h" +#include "util/Maybe.h" +#include "util/StringPiece.h" #include <androidfw/ResourceTypes.h> #include <functional> @@ -62,6 +62,8 @@ bool stringEndsWith(const BasicStringPiece<T>& str, const BasicStringPiece<T>& s */ StringPiece16 trimWhitespace(const StringPiece16& str); +StringPiece trimWhitespace(const StringPiece& str); + /** * UTF-16 isspace(). It basically checks for lower range characters that are * whitespace. @@ -83,6 +85,11 @@ StringPiece16::const_iterator findNonAlphaNumericAndNotInSet(const StringPiece16 bool isJavaClassName(const StringPiece16& str); /** + * Tests that the string is a valid Java package name. + */ +bool isJavaPackageName(const StringPiece16& str); + +/** * Converts the class name to a fully qualified class name from the given `package`. Ex: * * asdf --> package.asdf @@ -151,6 +158,23 @@ inline StringPiece16 getString(const android::ResStringPool& pool, size_t idx) { return StringPiece16(); } +inline StringPiece getString8(const android::ResStringPool& pool, size_t idx) { + size_t len; + const char* str = pool.string8At(idx, &len); + if (str != nullptr) { + return StringPiece(str, len); + } + return StringPiece(); +} + +/** + * Checks that the Java string format contains no non-positional arguments (arguments without + * explicitly specifying an index) when there are more than one argument. This is an error + * because translations may rearrange the order of the arguments in the string, which will + * break the string interpolation. + */ +bool verifyJavaStringFormat(const StringPiece16& str); + class StringBuilder { public: StringBuilder& append(const StringPiece16& str); @@ -162,6 +186,7 @@ private: std::u16string mStr; bool mQuote = false; bool mTrailingSpace = false; + bool mLastCharWasEscape = false; std::string mError; }; @@ -213,11 +238,12 @@ public: private: friend class Tokenizer<Char>; - iterator(BasicStringPiece<Char> s, Char sep, BasicStringPiece<Char> tok); + iterator(BasicStringPiece<Char> s, Char sep, BasicStringPiece<Char> tok, bool end); - BasicStringPiece<Char> str; - Char separator; - BasicStringPiece<Char> token; + BasicStringPiece<Char> mStr; + Char mSeparator; + BasicStringPiece<Char> mToken; + bool mEnd; }; Tokenizer(BasicStringPiece<Char> str, Char sep); @@ -236,36 +262,38 @@ inline Tokenizer<Char> tokenize(BasicStringPiece<Char> str, Char sep) { template <typename Char> typename Tokenizer<Char>::iterator& Tokenizer<Char>::iterator::operator++() { - const Char* start = token.end(); - const Char* end = str.end(); + const Char* start = mToken.end(); + const Char* end = mStr.end(); if (start == end) { - token.assign(token.end(), 0); + mEnd = true; + mToken.assign(mToken.end(), 0); return *this; } start += 1; const Char* current = start; while (current != end) { - if (*current == separator) { - token.assign(start, current - start); + if (*current == mSeparator) { + mToken.assign(start, current - start); return *this; } ++current; } - token.assign(start, end - start); + mToken.assign(start, end - start); return *this; } template <typename Char> inline BasicStringPiece<Char> Tokenizer<Char>::iterator::operator*() { - return token; + return mToken; } template <typename Char> inline bool Tokenizer<Char>::iterator::operator==(const iterator& rhs) const { // We check equality here a bit differently. // We need to know that the addresses are the same. - return token.begin() == rhs.token.begin() && token.end() == rhs.token.end(); + return mToken.begin() == rhs.mToken.begin() && mToken.end() == rhs.mToken.end() && + mEnd == rhs.mEnd; } template <typename Char> @@ -275,8 +303,8 @@ inline bool Tokenizer<Char>::iterator::operator!=(const iterator& rhs) const { template <typename Char> inline Tokenizer<Char>::iterator::iterator(BasicStringPiece<Char> s, Char sep, - BasicStringPiece<Char> tok) : - str(s), separator(sep), token(tok) { + BasicStringPiece<Char> tok, bool end) : + mStr(s), mSeparator(sep), mToken(tok), mEnd(end) { } template <typename Char> @@ -291,18 +319,37 @@ inline typename Tokenizer<Char>::iterator Tokenizer<Char>::end() { template <typename Char> inline Tokenizer<Char>::Tokenizer(BasicStringPiece<Char> str, Char sep) : - mBegin(++iterator(str, sep, BasicStringPiece<Char>(str.begin() - 1, 0))), - mEnd(str, sep, BasicStringPiece<Char>(str.end(), 0)) { + mBegin(++iterator(str, sep, BasicStringPiece<Char>(str.begin() - 1, 0), false)), + mEnd(str, sep, BasicStringPiece<Char>(str.end(), 0), true) { +} + +inline uint16_t hostToDevice16(uint16_t value) { + return htods(value); +} + +inline uint32_t hostToDevice32(uint32_t value) { + return htodl(value); +} + +inline uint16_t deviceToHost16(uint16_t value) { + return dtohs(value); +} + +inline uint32_t deviceToHost32(uint32_t value) { + return dtohl(value); } /** - * Returns a package name if the namespace URI is of the form: - * http://schemas.android.com/apk/res/<package> + * Given a path like: res/xml-sw600dp/foo.xml + * + * Extracts "res/xml-sw600dp/" into outPrefix. + * Extracts "foo" into outEntry. + * Extracts ".xml" into outSuffix. * - * Special case: if namespaceUri is http://schemas.android.com/apk/res-auto, - * returns an empty package name. + * Returns true if successful. */ -Maybe<std::u16string> extractPackageFromNamespace(const std::u16string& namespaceUri); +bool extractResFilePathParts(const StringPiece16& path, StringPiece16* outPrefix, + StringPiece16* outEntry, StringPiece16* outSuffix); } // namespace util diff --git a/tools/aapt2/util/Util_test.cpp b/tools/aapt2/util/Util_test.cpp new file mode 100644 index 000000000000..1e0c7fa9152d --- /dev/null +++ b/tools/aapt2/util/Util_test.cpp @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "test/Common.h" +#include "util/StringPiece.h" +#include "util/Util.h" + +#include <gtest/gtest.h> +#include <string> + +namespace aapt { + +TEST(UtilTest, TrimOnlyWhitespace) { + const std::u16string full = u"\n "; + + StringPiece16 trimmed = util::trimWhitespace(full); + EXPECT_TRUE(trimmed.empty()); + EXPECT_EQ(0u, trimmed.size()); +} + +TEST(UtilTest, StringEndsWith) { + EXPECT_TRUE(util::stringEndsWith<char>("hello.xml", ".xml")); +} + +TEST(UtilTest, StringStartsWith) { + EXPECT_TRUE(util::stringStartsWith<char>("hello.xml", "he")); +} + +TEST(UtilTest, StringBuilderSplitEscapeSequence) { + EXPECT_EQ(StringPiece16(u"this is a new\nline."), + util::StringBuilder().append(u"this is a new\\") + .append(u"nline.") + .str()); +} + +TEST(UtilTest, StringBuilderWhitespaceRemoval) { + EXPECT_EQ(StringPiece16(u"hey guys this is so cool"), + util::StringBuilder().append(u" hey guys ") + .append(u" this is so cool ") + .str()); + + EXPECT_EQ(StringPiece16(u" wow, so many \t spaces. what?"), + util::StringBuilder().append(u" \" wow, so many \t ") + .append(u"spaces. \"what? ") + .str()); + + EXPECT_EQ(StringPiece16(u"where is the pie?"), + util::StringBuilder().append(u" where \t ") + .append(u" \nis the "" pie?") + .str()); +} + +TEST(UtilTest, StringBuilderEscaping) { + EXPECT_EQ(StringPiece16(u"hey guys\n this \t is so\\ cool"), + util::StringBuilder().append(u" hey guys\\n ") + .append(u" this \\t is so\\\\ cool ") + .str()); + + EXPECT_EQ(StringPiece16(u"@?#\\\'"), + util::StringBuilder().append(u"\\@\\?\\#\\\\\\'") + .str()); +} + +TEST(UtilTest, StringBuilderMisplacedQuote) { + util::StringBuilder builder{}; + EXPECT_FALSE(builder.append(u"they're coming!")); +} + +TEST(UtilTest, StringBuilderUnicodeCodes) { + EXPECT_EQ(StringPiece16(u"\u00AF\u0AF0 woah"), + util::StringBuilder().append(u"\\u00AF\\u0AF0 woah") + .str()); + + EXPECT_FALSE(util::StringBuilder().append(u"\\u00 yo")); +} + +TEST(UtilTest, TokenizeInput) { + auto tokenizer = util::tokenize(StringPiece16(u"this| is|the|end"), u'|'); + auto iter = tokenizer.begin(); + ASSERT_EQ(*iter, StringPiece16(u"this")); + ++iter; + ASSERT_EQ(*iter, StringPiece16(u" is")); + ++iter; + ASSERT_EQ(*iter, StringPiece16(u"the")); + ++iter; + ASSERT_EQ(*iter, StringPiece16(u"end")); + ++iter; + ASSERT_EQ(tokenizer.end(), iter); +} + +TEST(UtilTest, TokenizeEmptyString) { + auto tokenizer = util::tokenize(StringPiece16(u""), u'|'); + auto iter = tokenizer.begin(); + ASSERT_NE(tokenizer.end(), iter); + ASSERT_EQ(StringPiece16(), *iter); + ++iter; + ASSERT_EQ(tokenizer.end(), iter); +} + +TEST(UtilTest, TokenizeAtEnd) { + auto tokenizer = util::tokenize(StringPiece16(u"one."), u'.'); + auto iter = tokenizer.begin(); + ASSERT_EQ(*iter, StringPiece16(u"one")); + ++iter; + ASSERT_NE(iter, tokenizer.end()); + ASSERT_EQ(*iter, StringPiece16()); +} + +TEST(UtilTest, IsJavaClassName) { + EXPECT_TRUE(util::isJavaClassName(u"android.test.Class")); + EXPECT_TRUE(util::isJavaClassName(u"android.test.Class$Inner")); + EXPECT_TRUE(util::isJavaClassName(u"android_test.test.Class")); + EXPECT_TRUE(util::isJavaClassName(u"_android_.test._Class_")); + EXPECT_FALSE(util::isJavaClassName(u"android.test.$Inner")); + EXPECT_FALSE(util::isJavaClassName(u"android.test.Inner$")); + EXPECT_FALSE(util::isJavaClassName(u".test.Class")); + EXPECT_FALSE(util::isJavaClassName(u"android")); +} + +TEST(UtilTest, IsJavaPackageName) { + EXPECT_TRUE(util::isJavaPackageName(u"android")); + EXPECT_TRUE(util::isJavaPackageName(u"android.test")); + EXPECT_TRUE(util::isJavaPackageName(u"android.test_thing")); + EXPECT_FALSE(util::isJavaPackageName(u"_android")); + EXPECT_FALSE(util::isJavaPackageName(u"android_")); + EXPECT_FALSE(util::isJavaPackageName(u"android.")); + EXPECT_FALSE(util::isJavaPackageName(u".android")); + EXPECT_FALSE(util::isJavaPackageName(u"android._test")); + EXPECT_FALSE(util::isJavaPackageName(u"..")); +} + +TEST(UtilTest, FullyQualifiedClassName) { + Maybe<std::u16string> res = util::getFullyQualifiedClassName(u"android", u"asdf"); + AAPT_ASSERT_FALSE(res); + + res = util::getFullyQualifiedClassName(u"android", u".asdf"); + AAPT_ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"android.asdf"); + + res = util::getFullyQualifiedClassName(u"android", u".a.b"); + AAPT_ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"android.a.b"); + + res = util::getFullyQualifiedClassName(u"android", u"a.b"); + AAPT_ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"a.b"); + + res = util::getFullyQualifiedClassName(u"", u"a.b"); + AAPT_ASSERT_TRUE(res); + EXPECT_EQ(res.value(), u"a.b"); + + res = util::getFullyQualifiedClassName(u"", u""); + AAPT_ASSERT_FALSE(res); + + res = util::getFullyQualifiedClassName(u"android", u"./Apple"); + AAPT_ASSERT_FALSE(res); +} + +TEST(UtilTest, ExtractResourcePathComponents) { + StringPiece16 prefix, entry, suffix; + ASSERT_TRUE(util::extractResFilePathParts(u"res/xml-sw600dp/entry.xml", &prefix, &entry, + &suffix)); + EXPECT_EQ(prefix, u"res/xml-sw600dp/"); + EXPECT_EQ(entry, u"entry"); + EXPECT_EQ(suffix, u".xml"); + + ASSERT_TRUE(util::extractResFilePathParts(u"res/xml-sw600dp/entry.9.png", &prefix, &entry, + &suffix)); + + EXPECT_EQ(prefix, u"res/xml-sw600dp/"); + EXPECT_EQ(entry, u"entry"); + EXPECT_EQ(suffix, u".9.png"); + + EXPECT_FALSE(util::extractResFilePathParts(u"AndroidManifest.xml", &prefix, &entry, &suffix)); + EXPECT_FALSE(util::extractResFilePathParts(u"res/.xml", &prefix, &entry, &suffix)); + + ASSERT_TRUE(util::extractResFilePathParts(u"res//.", &prefix, &entry, &suffix)); + EXPECT_EQ(prefix, u"res//"); + EXPECT_EQ(entry, u""); + EXPECT_EQ(suffix, u"."); +} + +TEST(UtilTest, VerifyJavaStringFormat) { + ASSERT_TRUE(util::verifyJavaStringFormat(u"%09.34f")); + ASSERT_TRUE(util::verifyJavaStringFormat(u"%9$.34f %8$")); + ASSERT_TRUE(util::verifyJavaStringFormat(u"%% %%")); + ASSERT_FALSE(util::verifyJavaStringFormat(u"%09$f %f")); + ASSERT_FALSE(util::verifyJavaStringFormat(u"%09f %08s")); +} + +} // namespace aapt diff --git a/tools/aapt2/XmlDom.cpp b/tools/aapt2/xml/XmlDom.cpp index b8b2d1295067..d27b62fd99fb 100644 --- a/tools/aapt2/XmlDom.cpp +++ b/tools/aapt2/xml/XmlDom.cpp @@ -14,12 +14,12 @@ * limitations under the License. */ -#include "Logger.h" -#include "Util.h" #include "XmlDom.h" #include "XmlPullParser.h" +#include "util/Util.h" #include <cassert> +#include <expat.h> #include <memory> #include <stack> #include <string> @@ -65,7 +65,7 @@ static void addToStack(Stack* stack, XML_Parser parser, std::unique_ptr<Node> no stack->root = std::move(node); } - if (thisNode->type != NodeType::kText) { + if (!nodeCast<Text>(thisNode)) { stack->nodeStack.push(thisNode); } } @@ -126,7 +126,7 @@ static void XMLCALL endElementHandler(void* userData, const char* name) { Stack* stack = reinterpret_cast<Stack*>(XML_GetUserData(parser)); assert(!stack->nodeStack.empty()); - stack->nodeStack.top()->comment = std::move(stack->pendingComment); + //stack->nodeStack.top()->comment = std::move(stack->pendingComment); stack->nodeStack.pop(); } @@ -143,8 +143,7 @@ static void XMLCALL characterDataHandler(void* userData, const char* s, int len) Node* currentParent = stack->nodeStack.top(); if (!currentParent->children.empty()) { Node* lastChild = currentParent->children.back().get(); - if (lastChild->type == NodeType::kText) { - Text* text = static_cast<Text*>(lastChild); + if (Text* text = nodeCast<Text>(lastChild)) { text->text += util::utf8ToUtf16(StringPiece(s, len)); return; } @@ -166,7 +165,7 @@ static void XMLCALL commentDataHandler(void* userData, const char* comment) { stack->pendingComment += util::utf8ToUtf16(comment); } -std::unique_ptr<Node> inflate(std::istream* in, SourceLogger* logger) { +std::unique_ptr<XmlResource> inflate(std::istream* in, IDiagnostics* diag, const Source& source) { Stack stack; XML_Parser parser = XML_ParserCreateNS(nullptr, kXmlNamespaceSep); @@ -182,20 +181,23 @@ std::unique_ptr<Node> inflate(std::istream* in, SourceLogger* logger) { in->read(buffer, sizeof(buffer) / sizeof(buffer[0])); if (in->bad() && !in->eof()) { stack.root = {}; - logger->error() << strerror(errno) << std::endl; + diag->error(DiagMessage(source) << strerror(errno)); break; } if (XML_Parse(parser, buffer, in->gcount(), in->eof()) == XML_STATUS_ERROR) { stack.root = {}; - logger->error(XML_GetCurrentLineNumber(parser)) - << XML_ErrorString(XML_GetErrorCode(parser)) << std::endl; + diag->error(DiagMessage(source.withLine(XML_GetCurrentLineNumber(parser))) + << XML_ErrorString(XML_GetErrorCode(parser))); break; } } XML_ParserFree(parser); - return std::move(stack.root); + if (stack.root) { + return util::make_unique<XmlResource>(ResourceFile{ {}, {}, source }, std::move(stack.root)); + } + return {}; } static void copyAttributes(Element* el, android::ResXMLParser* parser) { @@ -224,7 +226,8 @@ static void copyAttributes(Element* el, android::ResXMLParser* parser) { } } -std::unique_ptr<Node> inflate(const void* data, size_t dataLen, SourceLogger* logger) { +std::unique_ptr<XmlResource> inflate(const void* data, size_t dataLen, IDiagnostics* diag, + const Source& source) { std::unique_ptr<Node> root; std::stack<Node*> nodeStack; @@ -307,53 +310,37 @@ std::unique_ptr<Node> inflate(const void* data, size_t dataLen, SourceLogger* lo nodeStack.top()->addChild(std::move(newNode)); } - if (thisNode->type != NodeType::kText) { + if (!nodeCast<Text>(thisNode)) { nodeStack.push(thisNode); } } } - return root; + return util::make_unique<XmlResource>(ResourceFile{}, std::move(root)); } -Node::Node(NodeType type) : type(type), parent(nullptr), lineNumber(0), columnNumber(0) { +Element* findRootElement(XmlResource* doc) { + return findRootElement(doc->root.get()); } -void Node::addChild(std::unique_ptr<Node> child) { - child->parent = this; - children.push_back(std::move(child)); -} - -Namespace::Namespace() : BaseNode(NodeType::kNamespace) { -} - -std::unique_ptr<Node> Namespace::clone() const { - Namespace* ns = new Namespace(); - ns->lineNumber = lineNumber; - ns->columnNumber = columnNumber; - ns->comment = comment; - ns->namespacePrefix = namespacePrefix; - ns->namespaceUri = namespaceUri; - for (auto& child : children) { - ns->addChild(child->clone()); +Element* findRootElement(Node* node) { + if (!node) { + return nullptr; } - return std::unique_ptr<Node>(ns); -} -Element::Element() : BaseNode(NodeType::kElement) { + Element* el = nullptr; + while ((el = nodeCast<Element>(node)) == nullptr) { + if (node->children.empty()) { + return nullptr; + } + // We are looking for the first element, and namespaces can only have one child. + node = node->children.front().get(); + } + return el; } -std::unique_ptr<Node> Element::clone() const { - Element* el = new Element(); - el->lineNumber = lineNumber; - el->columnNumber = columnNumber; - el->comment = comment; - el->namespaceUri = namespaceUri; - el->name = name; - el->attributes = attributes; - for (auto& child : children) { - el->addChild(child->clone()); - } - return std::unique_ptr<Node>(el); +void Node::addChild(std::unique_ptr<Node> child) { + child->parent = this; + children.push_back(std::move(child)); } Attribute* Element::findAttribute(const StringPiece16& ns, const StringPiece16& name) { @@ -366,29 +353,29 @@ Attribute* Element::findAttribute(const StringPiece16& ns, const StringPiece16& } Element* Element::findChild(const StringPiece16& ns, const StringPiece16& name) { - return findChildWithAttribute(ns, name, nullptr); + return findChildWithAttribute(ns, name, {}, {}, {}); } Element* Element::findChildWithAttribute(const StringPiece16& ns, const StringPiece16& name, - const Attribute* reqAttr) { + const StringPiece16& attrNs, const StringPiece16& attrName, + const StringPiece16& attrValue) { for (auto& childNode : children) { Node* child = childNode.get(); - while (child->type == NodeType::kNamespace) { + while (nodeCast<Namespace>(child)) { if (child->children.empty()) { break; } child = child->children[0].get(); } - if (child->type == NodeType::kElement) { - Element* el = static_cast<Element*>(child); + if (Element* el = nodeCast<Element>(child)) { if (ns == el->namespaceUri && name == el->name) { - if (!reqAttr) { + if (attrNs.empty() && attrName.empty()) { return el; } - Attribute* attrName = el->findAttribute(reqAttr->namespaceUri, reqAttr->name); - if (attrName && attrName->value == reqAttr->value) { + Attribute* attr = el->findAttribute(attrNs, attrName); + if (attr && attrValue == attr->value) { return el; } } @@ -401,30 +388,52 @@ std::vector<Element*> Element::getChildElements() { std::vector<Element*> elements; for (auto& childNode : children) { Node* child = childNode.get(); - while (child->type == NodeType::kNamespace) { + while (nodeCast<Namespace>(child)) { if (child->children.empty()) { break; } child = child->children[0].get(); } - if (child->type == NodeType::kElement) { - elements.push_back(static_cast<Element*>(child)); + if (Element* el = nodeCast<Element>(child)) { + elements.push_back(el); } } return elements; } -Text::Text() : BaseNode(NodeType::kText) { +void PackageAwareVisitor::visit(Namespace* ns) { + bool added = false; + if (Maybe<ExtractedPackage> maybePackage = extractPackageFromNamespace(ns->namespaceUri)) { + ExtractedPackage& package = maybePackage.value(); + mPackageDecls.push_back(PackageDecl{ ns->namespacePrefix, std::move(package) }); + added = true; + } + + Visitor::visit(ns); + + if (added) { + mPackageDecls.pop_back(); + } } -std::unique_ptr<Node> Text::clone() const { - Text* el = new Text(); - el->lineNumber = lineNumber; - el->columnNumber = columnNumber; - el->comment = comment; - el->text = text; - return std::unique_ptr<Node>(el); +Maybe<ExtractedPackage> PackageAwareVisitor::transformPackageAlias( + const StringPiece16& alias, const StringPiece16& localPackage) const { + if (alias.empty()) { + return ExtractedPackage{ localPackage.toString(), false /* private */ }; + } + + const auto rend = mPackageDecls.rend(); + for (auto iter = mPackageDecls.rbegin(); iter != rend; ++iter) { + if (alias == iter->prefix) { + if (iter->package.package.empty()) { + return ExtractedPackage{ localPackage.toString(), + iter->package.privateNamespace }; + } + return iter->package; + } + } + return {}; } } // namespace xml diff --git a/tools/aapt2/xml/XmlDom.h b/tools/aapt2/xml/XmlDom.h new file mode 100644 index 000000000000..033b0a4d031c --- /dev/null +++ b/tools/aapt2/xml/XmlDom.h @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_XML_DOM_H +#define AAPT_XML_DOM_H + +#include "Diagnostics.h" +#include "Resource.h" +#include "ResourceValues.h" +#include "util/StringPiece.h" +#include "util/Util.h" +#include "xml/XmlUtil.h" + +#include <istream> +#include <memory> +#include <string> +#include <vector> + +namespace aapt { +namespace xml { + +struct RawVisitor; + +/** + * Base class for all XML nodes. + */ +struct Node { + Node* parent = nullptr; + size_t lineNumber = 0; + size_t columnNumber = 0; + std::u16string comment; + std::vector<std::unique_ptr<Node>> children; + + virtual ~Node() = default; + + void addChild(std::unique_ptr<Node> child); + virtual void accept(RawVisitor* visitor) = 0; +}; + +/** + * Base class that implements the visitor methods for a + * subclass of Node. + */ +template <typename Derived> +struct BaseNode : public Node { + virtual void accept(RawVisitor* visitor) override; +}; + +/** + * A Namespace XML node. Can only have one child. + */ +struct Namespace : public BaseNode<Namespace> { + std::u16string namespacePrefix; + std::u16string namespaceUri; +}; + +struct AaptAttribute { + ResourceId id; + aapt::Attribute attribute; +}; + +/** + * An XML attribute. + */ +struct Attribute { + std::u16string namespaceUri; + std::u16string name; + std::u16string value; + + Maybe<AaptAttribute> compiledAttribute; + std::unique_ptr<Item> compiledValue; +}; + +/** + * An Element XML node. + */ +struct Element : public BaseNode<Element> { + std::u16string namespaceUri; + std::u16string name; + std::vector<Attribute> attributes; + + Attribute* findAttribute(const StringPiece16& ns, const StringPiece16& name); + xml::Element* findChild(const StringPiece16& ns, const StringPiece16& name); + xml::Element* findChildWithAttribute(const StringPiece16& ns, const StringPiece16& name, + const StringPiece16& attrNs, + const StringPiece16& attrName, + const StringPiece16& attrValue); + std::vector<xml::Element*> getChildElements(); +}; + +/** + * A Text (CDATA) XML node. Can not have any children. + */ +struct Text : public BaseNode<Text> { + std::u16string text; +}; + +/** + * An XML resource with a source, name, and XML tree. + */ +struct XmlResource { + ResourceFile file; + std::unique_ptr<xml::Node> root; +}; + +/** + * Inflates an XML DOM from a text stream, logging errors to the logger. + * Returns the root node on success, or nullptr on failure. + */ +std::unique_ptr<XmlResource> inflate(std::istream* in, IDiagnostics* diag, const Source& source); + +/** + * Inflates an XML DOM from a binary ResXMLTree, logging errors to the logger. + * Returns the root node on success, or nullptr on failure. + */ +std::unique_ptr<XmlResource> inflate(const void* data, size_t dataLen, IDiagnostics* diag, + const Source& source); + +Element* findRootElement(XmlResource* doc); +Element* findRootElement(Node* node); + +/** + * A visitor interface for the different XML Node subtypes. This will not traverse into + * children. Use Visitor for that. + */ +struct RawVisitor { + virtual ~RawVisitor() = default; + + virtual void visit(Namespace* node) {} + virtual void visit(Element* node) {} + virtual void visit(Text* text) {} +}; + +/** + * Visitor whose default implementation visits the children nodes of any node. + */ +struct Visitor : public RawVisitor { + using RawVisitor::visit; + + void visit(Namespace* node) override { + visitChildren(node); + } + + void visit(Element* node) override { + visitChildren(node); + } + + void visit(Text* text) override { + visitChildren(text); + } + + void visitChildren(Node* node) { + for (auto& child : node->children) { + child->accept(this); + } + } +}; + +/** + * An XML DOM visitor that will record the package name for a namespace prefix. + */ +class PackageAwareVisitor : public Visitor, public IPackageDeclStack { +private: + struct PackageDecl { + std::u16string prefix; + ExtractedPackage package; + }; + + std::vector<PackageDecl> mPackageDecls; + +public: + using Visitor::visit; + + void visit(Namespace* ns) override; + Maybe<ExtractedPackage> transformPackageAlias( + const StringPiece16& alias, const StringPiece16& localPackage) const override; +}; + +// Implementations + +template <typename Derived> +void BaseNode<Derived>::accept(RawVisitor* visitor) { + visitor->visit(static_cast<Derived*>(this)); +} + +template <typename T> +struct NodeCastImpl : public RawVisitor { + using RawVisitor::visit; + + T* value = nullptr; + + void visit(T* v) override { + value = v; + } +}; + +template <typename T> +T* nodeCast(Node* node) { + NodeCastImpl<T> visitor; + node->accept(&visitor); + return visitor.value; +} + +} // namespace xml +} // namespace aapt + +#endif // AAPT_XML_DOM_H diff --git a/tools/aapt2/XmlDom_test.cpp b/tools/aapt2/xml/XmlDom_test.cpp index 021714410e75..431ee2c8fb46 100644 --- a/tools/aapt2/XmlDom_test.cpp +++ b/tools/aapt2/xml/XmlDom_test.cpp @@ -14,7 +14,7 @@ * limitations under the License. */ -#include "XmlDom.h" +#include "xml/XmlDom.h" #include <gtest/gtest.h> #include <sstream> @@ -36,12 +36,13 @@ TEST(XmlDomTest, Inflate) { </Layout> )EOF"; - SourceLogger logger(Source{ "/test/path" }); - std::unique_ptr<xml::Node> root = xml::inflate(&in, &logger); - ASSERT_NE(root, nullptr); + const Source source = { "test.xml" }; + StdErrDiagnostics diag; + std::unique_ptr<xml::XmlResource> doc = xml::inflate(&in, &diag, source); + ASSERT_NE(doc, nullptr); - EXPECT_EQ(root->type, xml::NodeType::kNamespace); - xml::Namespace* ns = static_cast<xml::Namespace*>(root.get()); + xml::Namespace* ns = xml::nodeCast<xml::Namespace>(doc->root.get()); + ASSERT_NE(ns, nullptr); EXPECT_EQ(ns->namespaceUri, u"http://schemas.android.com/apk/res/android"); EXPECT_EQ(ns->namespacePrefix, u"android"); } diff --git a/tools/aapt2/SourceXmlPullParser.cpp b/tools/aapt2/xml/XmlPullParser.cpp index 8099044f616d..323ec05b5f2c 100644 --- a/tools/aapt2/SourceXmlPullParser.cpp +++ b/tools/aapt2/xml/XmlPullParser.cpp @@ -14,18 +14,20 @@ * limitations under the License. */ +#include "util/Maybe.h" +#include "util/Util.h" +#include "xml/XmlPullParser.h" +#include "xml/XmlUtil.h" + #include <iostream> #include <string> -#include "Maybe.h" -#include "SourceXmlPullParser.h" -#include "Util.h" - namespace aapt { +namespace xml { constexpr char kXmlNamespaceSep = 1; -SourceXmlPullParser::SourceXmlPullParser(std::istream& in) : mIn(in), mEmpty(), mDepth(0) { +XmlPullParser::XmlPullParser(std::istream& in) : mIn(in), mEmpty(), mDepth(0) { mParser = XML_ParserCreateNS(nullptr, kXmlNamespaceSep); XML_SetUserData(mParser, this); XML_SetElementHandler(mParser, startElementHandler, endElementHandler); @@ -35,11 +37,11 @@ SourceXmlPullParser::SourceXmlPullParser(std::istream& in) : mIn(in), mEmpty(), mEventQueue.push(EventData{ Event::kStartDocument, 0, mDepth++ }); } -SourceXmlPullParser::~SourceXmlPullParser() { +XmlPullParser::~XmlPullParser() { XML_ParserFree(mParser); } -SourceXmlPullParser::Event SourceXmlPullParser::next() { +XmlPullParser::Event XmlPullParser::next() { const Event currentEvent = getEvent(); if (currentEvent == Event::kBadDocument || currentEvent == Event::kEndDocument) { return currentEvent; @@ -72,14 +74,14 @@ SourceXmlPullParser::Event SourceXmlPullParser::next() { // Record namespace prefixes and package names so that we can do our own // handling of references that use namespace aliases. if (event == Event::kStartNamespace || event == Event::kEndNamespace) { - Maybe<std::u16string> result = util::extractPackageFromNamespace(getNamespaceUri()); + Maybe<ExtractedPackage> result = extractPackageFromNamespace(getNamespaceUri()); if (event == Event::kStartNamespace) { if (result) { - mPackageAliases.emplace_back(getNamespacePrefix(), result.value()); + mPackageAliases.emplace_back( + PackageDecl{ getNamespacePrefix(), std::move(result.value()) }); } } else { if (result) { - assert(mPackageAliases.back().second == result.value()); mPackageAliases.pop_back(); } } @@ -88,34 +90,34 @@ SourceXmlPullParser::Event SourceXmlPullParser::next() { return event; } -SourceXmlPullParser::Event SourceXmlPullParser::getEvent() const { +XmlPullParser::Event XmlPullParser::getEvent() const { return mEventQueue.front().event; } -const std::string& SourceXmlPullParser::getLastError() const { +const std::string& XmlPullParser::getLastError() const { return mLastError; } -const std::u16string& SourceXmlPullParser::getComment() const { - return mEventQueue.front().comment; +const std::u16string& XmlPullParser::getComment() const { + return mEventQueue.front().data1; } -size_t SourceXmlPullParser::getLineNumber() const { +size_t XmlPullParser::getLineNumber() const { return mEventQueue.front().lineNumber; } -size_t SourceXmlPullParser::getDepth() const { +size_t XmlPullParser::getDepth() const { return mEventQueue.front().depth; } -const std::u16string& SourceXmlPullParser::getText() const { +const std::u16string& XmlPullParser::getText() const { if (getEvent() != Event::kText) { return mEmpty; } return mEventQueue.front().data1; } -const std::u16string& SourceXmlPullParser::getNamespacePrefix() const { +const std::u16string& XmlPullParser::getNamespacePrefix() const { const Event currentEvent = getEvent(); if (currentEvent != Event::kStartNamespace && currentEvent != Event::kEndNamespace) { return mEmpty; @@ -123,7 +125,7 @@ const std::u16string& SourceXmlPullParser::getNamespacePrefix() const { return mEventQueue.front().data1; } -const std::u16string& SourceXmlPullParser::getNamespaceUri() const { +const std::u16string& XmlPullParser::getNamespaceUri() const { const Event currentEvent = getEvent(); if (currentEvent != Event::kStartNamespace && currentEvent != Event::kEndNamespace) { return mEmpty; @@ -131,23 +133,26 @@ const std::u16string& SourceXmlPullParser::getNamespaceUri() const { return mEventQueue.front().data2; } -bool SourceXmlPullParser::applyPackageAlias(std::u16string* package, - const std::u16string& defaultPackage) const { +Maybe<ExtractedPackage> XmlPullParser::transformPackageAlias( + const StringPiece16& alias, const StringPiece16& localPackage) const { + if (alias.empty()) { + return ExtractedPackage{ localPackage.toString(), false /* private */ }; + } + const auto endIter = mPackageAliases.rend(); for (auto iter = mPackageAliases.rbegin(); iter != endIter; ++iter) { - if (iter->first == *package) { - if (iter->second.empty()) { - *package = defaultPackage; - } else { - *package = iter->second; + if (alias == iter->prefix) { + if (iter->package.package.empty()) { + return ExtractedPackage{ localPackage.toString(), + iter->package.privateNamespace }; } - return true; + return iter->package; } } - return false; + return {}; } -const std::u16string& SourceXmlPullParser::getElementNamespace() const { +const std::u16string& XmlPullParser::getElementNamespace() const { const Event currentEvent = getEvent(); if (currentEvent != Event::kStartElement && currentEvent != Event::kEndElement) { return mEmpty; @@ -155,7 +160,7 @@ const std::u16string& SourceXmlPullParser::getElementNamespace() const { return mEventQueue.front().data1; } -const std::u16string& SourceXmlPullParser::getElementName() const { +const std::u16string& XmlPullParser::getElementName() const { const Event currentEvent = getEvent(); if (currentEvent != Event::kStartElement && currentEvent != Event::kEndElement) { return mEmpty; @@ -163,15 +168,15 @@ const std::u16string& SourceXmlPullParser::getElementName() const { return mEventQueue.front().data2; } -XmlPullParser::const_iterator SourceXmlPullParser::beginAttributes() const { +XmlPullParser::const_iterator XmlPullParser::beginAttributes() const { return mEventQueue.front().attributes.begin(); } -XmlPullParser::const_iterator SourceXmlPullParser::endAttributes() const { +XmlPullParser::const_iterator XmlPullParser::endAttributes() const { return mEventQueue.front().attributes.end(); } -size_t SourceXmlPullParser::getAttributeCount() const { +size_t XmlPullParser::getAttributeCount() const { if (getEvent() != Event::kStartElement) { return 0; } @@ -196,9 +201,9 @@ static void splitName(const char* name, std::u16string& outNs, std::u16string& o } } -void XMLCALL SourceXmlPullParser::startNamespaceHandler(void* userData, const char* prefix, +void XMLCALL XmlPullParser::startNamespaceHandler(void* userData, const char* prefix, const char* uri) { - SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + XmlPullParser* parser = reinterpret_cast<XmlPullParser*>(userData); std::u16string namespaceUri = uri != nullptr ? util::utf8ToUtf16(uri) : std::u16string(); parser->mNamespaceUris.push(namespaceUri); parser->mEventQueue.push(EventData{ @@ -210,9 +215,9 @@ void XMLCALL SourceXmlPullParser::startNamespaceHandler(void* userData, const ch }); } -void XMLCALL SourceXmlPullParser::startElementHandler(void* userData, const char* name, +void XMLCALL XmlPullParser::startElementHandler(void* userData, const char* name, const char** attrs) { - SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); + XmlPullParser* parser = reinterpret_cast<XmlPullParser*>(userData); EventData data = { Event::kStartElement, XML_GetCurrentLineNumber(parser->mParser), parser->mDepth++ @@ -233,8 +238,8 @@ void XMLCALL SourceXmlPullParser::startElementHandler(void* userData, const char parser->mEventQueue.push(std::move(data)); } -void XMLCALL SourceXmlPullParser::characterDataHandler(void* userData, const char* s, int len) { - SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); +void XMLCALL XmlPullParser::characterDataHandler(void* userData, const char* s, int len) { + XmlPullParser* parser = reinterpret_cast<XmlPullParser*>(userData); parser->mEventQueue.push(EventData{ Event::kText, @@ -244,8 +249,8 @@ void XMLCALL SourceXmlPullParser::characterDataHandler(void* userData, const cha }); } -void XMLCALL SourceXmlPullParser::endElementHandler(void* userData, const char* name) { - SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); +void XMLCALL XmlPullParser::endElementHandler(void* userData, const char* name) { + XmlPullParser* parser = reinterpret_cast<XmlPullParser*>(userData); EventData data = { Event::kEndElement, XML_GetCurrentLineNumber(parser->mParser), --(parser->mDepth) @@ -256,8 +261,8 @@ void XMLCALL SourceXmlPullParser::endElementHandler(void* userData, const char* parser->mEventQueue.push(std::move(data)); } -void XMLCALL SourceXmlPullParser::endNamespaceHandler(void* userData, const char* prefix) { - SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); +void XMLCALL XmlPullParser::endNamespaceHandler(void* userData, const char* prefix) { + XmlPullParser* parser = reinterpret_cast<XmlPullParser*>(userData); parser->mEventQueue.push(EventData{ Event::kEndNamespace, @@ -269,8 +274,8 @@ void XMLCALL SourceXmlPullParser::endNamespaceHandler(void* userData, const char parser->mNamespaceUris.pop(); } -void XMLCALL SourceXmlPullParser::commentDataHandler(void* userData, const char* comment) { - SourceXmlPullParser* parser = reinterpret_cast<SourceXmlPullParser*>(userData); +void XMLCALL XmlPullParser::commentDataHandler(void* userData, const char* comment) { + XmlPullParser* parser = reinterpret_cast<XmlPullParser*>(userData); parser->mEventQueue.push(EventData{ Event::kComment, @@ -280,4 +285,24 @@ void XMLCALL SourceXmlPullParser::commentDataHandler(void* userData, const char* }); } +Maybe<StringPiece16> findAttribute(const XmlPullParser* parser, const StringPiece16& name) { + auto iter = parser->findAttribute(u"", name); + if (iter != parser->endAttributes()) { + return StringPiece16(util::trimWhitespace(iter->value)); + } + return {}; +} + +Maybe<StringPiece16> findNonEmptyAttribute(const XmlPullParser* parser, const StringPiece16& name) { + auto iter = parser->findAttribute(u"", name); + if (iter != parser->endAttributes()) { + StringPiece16 trimmed = util::trimWhitespace(iter->value); + if (!trimmed.empty()) { + return trimmed; + } + } + return {}; +} + +} // namespace xml } // namespace aapt diff --git a/tools/aapt2/XmlPullParser.h b/tools/aapt2/xml/XmlPullParser.h index accfd30a4775..7e7070e5e5ea 100644 --- a/tools/aapt2/XmlPullParser.h +++ b/tools/aapt2/xml/XmlPullParser.h @@ -17,16 +17,25 @@ #ifndef AAPT_XML_PULL_PARSER_H #define AAPT_XML_PULL_PARSER_H +#include "Resource.h" +#include "process/IResourceTableConsumer.h" +#include "util/Maybe.h" +#include "util/StringPiece.h" +#include "xml/XmlUtil.h" + #include <algorithm> +#include <expat.h> +#include <istream> #include <ostream> +#include <queue> +#include <stack> #include <string> #include <vector> -#include "StringPiece.h" - namespace aapt { +namespace xml { -class XmlPullParser { +class XmlPullParser : public IPackageDeclStack { public: enum class Event { kBadDocument, @@ -41,43 +50,58 @@ public: kComment, }; - static void skipCurrentElement(XmlPullParser* parser); + /** + * Skips to the next direct descendant node of the given startDepth, + * skipping namespace nodes. + * + * When nextChildNode returns true, you can expect Comments, Text, and StartElement events. + */ + static bool nextChildNode(XmlPullParser* parser, size_t startDepth); + static bool skipCurrentElement(XmlPullParser* parser); static bool isGoodEvent(Event event); - virtual ~XmlPullParser() {} + XmlPullParser(std::istream& in); + ~XmlPullParser(); /** * Returns the current event that is being processed. */ - virtual Event getEvent() const = 0; + Event getEvent() const; - virtual const std::string& getLastError() const = 0; + const std::string& getLastError() const; /** * Note, unlike XmlPullParser, the first call to next() will return * StartElement of the first element. */ - virtual Event next() = 0; + Event next(); // // These are available for all nodes. // - virtual const std::u16string& getComment() const = 0; - virtual size_t getLineNumber() const = 0; - virtual size_t getDepth() const = 0; + const std::u16string& getComment() const; + size_t getLineNumber() const; + size_t getDepth() const; /** * Returns the character data for a Text event. */ - virtual const std::u16string& getText() const = 0; + const std::u16string& getText() const; // // Namespace prefix and URI are available for StartNamespace and EndNamespace. // - virtual const std::u16string& getNamespacePrefix() const = 0; - virtual const std::u16string& getNamespaceUri() const = 0; + const std::u16string& getNamespacePrefix() const; + const std::u16string& getNamespaceUri() const; + + // + // These are available for StartElement and EndElement. + // + + const std::u16string& getElementNamespace() const; + const std::u16string& getElementName() const; /* * Uses the current stack of namespaces to resolve the package. Eg: @@ -90,15 +114,8 @@ public: * If xmlns:app="http://schemas.android.com/apk/res-auto", then * 'package' will be set to 'defaultPackage'. */ - virtual bool applyPackageAlias(std::u16string* package, - const std::u16string& defaultPackage) const = 0; - - // - // These are available for StartElement and EndElement. - // - - virtual const std::u16string& getElementNamespace() const = 0; - virtual const std::u16string& getElementName() const = 0; + Maybe<ExtractedPackage> transformPackageAlias( + const StringPiece16& alias, const StringPiece16& localPackage) const override; // // Remaining methods are for retrieving information about attributes @@ -121,12 +138,55 @@ public: using const_iterator = std::vector<Attribute>::const_iterator; - virtual const_iterator beginAttributes() const = 0; - virtual const_iterator endAttributes() const = 0; - virtual size_t getAttributeCount() const = 0; + const_iterator beginAttributes() const; + const_iterator endAttributes() const; + size_t getAttributeCount() const; const_iterator findAttribute(StringPiece16 namespaceUri, StringPiece16 name) const; + +private: + static void XMLCALL startNamespaceHandler(void* userData, const char* prefix, const char* uri); + static void XMLCALL startElementHandler(void* userData, const char* name, const char** attrs); + static void XMLCALL characterDataHandler(void* userData, const char* s, int len); + static void XMLCALL endElementHandler(void* userData, const char* name); + static void XMLCALL endNamespaceHandler(void* userData, const char* prefix); + static void XMLCALL commentDataHandler(void* userData, const char* comment); + + struct EventData { + Event event; + size_t lineNumber; + size_t depth; + std::u16string data1; + std::u16string data2; + std::vector<Attribute> attributes; + }; + + std::istream& mIn; + XML_Parser mParser; + char mBuffer[16384]; + std::queue<EventData> mEventQueue; + std::string mLastError; + const std::u16string mEmpty; + size_t mDepth; + std::stack<std::u16string> mNamespaceUris; + + struct PackageDecl { + std::u16string prefix; + ExtractedPackage package; + }; + std::vector<PackageDecl> mPackageAliases; }; +/** + * Finds the attribute in the current element within the global namespace. + */ +Maybe<StringPiece16> findAttribute(const XmlPullParser* parser, const StringPiece16& name); + +/** + * Finds the attribute in the current element within the global namespace. The attribute's value + * must not be the empty string. + */ +Maybe<StringPiece16> findNonEmptyAttribute(const XmlPullParser* parser, const StringPiece16& name); + // // Implementation // @@ -146,13 +206,35 @@ inline ::std::ostream& operator<<(::std::ostream& out, XmlPullParser::Event even return out; } -inline void XmlPullParser::skipCurrentElement(XmlPullParser* parser) { +inline bool XmlPullParser::nextChildNode(XmlPullParser* parser, size_t startDepth) { + Event event; + + // First get back to the start depth. + while (isGoodEvent(event = parser->next()) && parser->getDepth() > startDepth + 1) {} + + // Now look for the first good node. + while ((event != Event::kEndElement || parser->getDepth() > startDepth) && isGoodEvent(event)) { + switch (event) { + case Event::kText: + case Event::kComment: + case Event::kStartElement: + return true; + default: + break; + } + event = parser->next(); + } + return false; +} + +inline bool XmlPullParser::skipCurrentElement(XmlPullParser* parser) { int depth = 1; while (depth > 0) { switch (parser->next()) { case Event::kEndDocument: + return true; case Event::kBadDocument: - return; + return false; case Event::kStartElement: depth++; break; @@ -163,6 +245,7 @@ inline void XmlPullParser::skipCurrentElement(XmlPullParser* parser) { break; } } + return true; } inline bool XmlPullParser::isGoodEvent(XmlPullParser::Event event) { @@ -209,6 +292,7 @@ inline XmlPullParser::const_iterator XmlPullParser::findAttribute(StringPiece16 return endIter; } +} // namespace xml } // namespace aapt #endif // AAPT_XML_PULL_PARSER_H diff --git a/tools/aapt2/xml/XmlPullParser_test.cpp b/tools/aapt2/xml/XmlPullParser_test.cpp new file mode 100644 index 000000000000..8fa2c6d274c8 --- /dev/null +++ b/tools/aapt2/xml/XmlPullParser_test.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "util/StringPiece.h" +#include "xml/XmlPullParser.h" + +#include <gtest/gtest.h> +#include <sstream> + +namespace aapt { + +TEST(XmlPullParserTest, NextChildNodeTraversesCorrectly) { + std::stringstream str; + str << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + "<a><b><c xmlns:a=\"http://schema.org\"><d/></c><e/></b></a>"; + xml::XmlPullParser parser(str); + + const size_t depthOuter = parser.getDepth(); + ASSERT_TRUE(xml::XmlPullParser::nextChildNode(&parser, depthOuter)); + + EXPECT_EQ(xml::XmlPullParser::Event::kStartElement, parser.getEvent()); + EXPECT_EQ(StringPiece16(u"a"), StringPiece16(parser.getElementName())); + + const size_t depthA = parser.getDepth(); + ASSERT_TRUE(xml::XmlPullParser::nextChildNode(&parser, depthA)); + EXPECT_EQ(xml::XmlPullParser::Event::kStartElement, parser.getEvent()); + EXPECT_EQ(StringPiece16(u"b"), StringPiece16(parser.getElementName())); + + const size_t depthB = parser.getDepth(); + ASSERT_TRUE(xml::XmlPullParser::nextChildNode(&parser, depthB)); + EXPECT_EQ(xml::XmlPullParser::Event::kStartElement, parser.getEvent()); + EXPECT_EQ(StringPiece16(u"c"), StringPiece16(parser.getElementName())); + + ASSERT_TRUE(xml::XmlPullParser::nextChildNode(&parser, depthB)); + EXPECT_EQ(xml::XmlPullParser::Event::kStartElement, parser.getEvent()); + EXPECT_EQ(StringPiece16(u"e"), StringPiece16(parser.getElementName())); + + ASSERT_FALSE(xml::XmlPullParser::nextChildNode(&parser, depthOuter)); + EXPECT_EQ(xml::XmlPullParser::Event::kEndDocument, parser.getEvent()); +} + +} // namespace aapt diff --git a/tools/aapt2/xml/XmlUtil.cpp b/tools/aapt2/xml/XmlUtil.cpp new file mode 100644 index 000000000000..ab9f544d67ea --- /dev/null +++ b/tools/aapt2/xml/XmlUtil.cpp @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "util/Maybe.h" +#include "util/Util.h" +#include "xml/XmlUtil.h" + +#include <string> + +namespace aapt { +namespace xml { + +Maybe<ExtractedPackage> extractPackageFromNamespace(const std::u16string& namespaceUri) { + if (util::stringStartsWith<char16_t>(namespaceUri, kSchemaPublicPrefix)) { + StringPiece16 schemaPrefix = kSchemaPublicPrefix; + StringPiece16 package = namespaceUri; + package = package.substr(schemaPrefix.size(), package.size() - schemaPrefix.size()); + if (package.empty()) { + return {}; + } + return ExtractedPackage{ package.toString(), false /* isPrivate */ }; + + } else if (util::stringStartsWith<char16_t>(namespaceUri, kSchemaPrivatePrefix)) { + StringPiece16 schemaPrefix = kSchemaPrivatePrefix; + StringPiece16 package = namespaceUri; + package = package.substr(schemaPrefix.size(), package.size() - schemaPrefix.size()); + if (package.empty()) { + return {}; + } + return ExtractedPackage{ package.toString(), true /* isPrivate */ }; + + } else if (namespaceUri == kSchemaAuto) { + return ExtractedPackage{ std::u16string(), true /* isPrivate */ }; + } + return {}; +} + +void transformReferenceFromNamespace(IPackageDeclStack* declStack, + const StringPiece16& localPackage, Reference* inRef) { + if (inRef->name) { + if (Maybe<ExtractedPackage> transformedPackage = + declStack->transformPackageAlias(inRef->name.value().package, localPackage)) { + ExtractedPackage& extractedPackage = transformedPackage.value(); + inRef->name.value().package = std::move(extractedPackage.package); + + // If the reference was already private (with a * prefix) and the namespace is public, + // we keep the reference private. + inRef->privateReference |= extractedPackage.privateNamespace; + } + } +} + +} // namespace xml +} // namespace aapt diff --git a/tools/aapt2/xml/XmlUtil.h b/tools/aapt2/xml/XmlUtil.h new file mode 100644 index 000000000000..98e5520a6ea2 --- /dev/null +++ b/tools/aapt2/xml/XmlUtil.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAPT_XML_XMLUTIL_H +#define AAPT_XML_XMLUTIL_H + +#include "ResourceValues.h" +#include "util/Maybe.h" + +#include <string> + +namespace aapt { +namespace xml { + +constexpr const char16_t* kSchemaAuto = u"http://schemas.android.com/apk/res-auto"; +constexpr const char16_t* kSchemaPublicPrefix = u"http://schemas.android.com/apk/res/"; +constexpr const char16_t* kSchemaPrivatePrefix = u"http://schemas.android.com/apk/prv/res/"; +constexpr const char16_t* kSchemaAndroid = u"http://schemas.android.com/apk/res/android"; + +/** + * Result of extracting a package name from a namespace URI declaration. + */ +struct ExtractedPackage { + /** + * The name of the package. This can be the empty string, which means that the package + * should be assumed to be the package being compiled. + */ + std::u16string package; + + /** + * True if the package's private namespace was declared. This means that private resources + * are made visible. + */ + bool privateNamespace; +}; + +/** + * Returns an ExtractedPackage struct if the namespace URI is of the form: + * http://schemas.android.com/apk/res/<package> or + * http://schemas.android.com/apk/prv/res/<package> + * + * Special case: if namespaceUri is http://schemas.android.com/apk/res-auto, + * returns an empty package name. + */ +Maybe<ExtractedPackage> extractPackageFromNamespace(const std::u16string& namespaceUri); + +/** + * Interface representing a stack of XML namespace declarations. When looking up the package + * for a namespace prefix, the stack is checked from top to bottom. + */ +struct IPackageDeclStack { + virtual ~IPackageDeclStack() = default; + + /** + * Returns an ExtractedPackage struct if the alias given corresponds with a package declaration. + */ + virtual Maybe<ExtractedPackage> transformPackageAlias( + const StringPiece16& alias, const StringPiece16& localPackage) const = 0; +}; + +/** + * Helper function for transforming the original Reference inRef to a fully qualified reference + * via the IPackageDeclStack. This will also mark the Reference as private if the namespace of + * the package declaration was private. + */ +void transformReferenceFromNamespace(IPackageDeclStack* declStack, + const StringPiece16& localPackage, Reference* inRef); + +} // namespace xml +} // namespace aapt + +#endif /* AAPT_XML_XMLUTIL_H */ diff --git a/tools/aapt2/xml/XmlUtil_test.cpp b/tools/aapt2/xml/XmlUtil_test.cpp new file mode 100644 index 000000000000..7796b3ea7691 --- /dev/null +++ b/tools/aapt2/xml/XmlUtil_test.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "test/Common.h" +#include "xml/XmlUtil.h" + +#include <gtest/gtest.h> + +namespace aapt { + +TEST(XmlUtilTest, ExtractPackageFromNamespace) { + AAPT_ASSERT_FALSE(xml::extractPackageFromNamespace(u"com.android")); + AAPT_ASSERT_FALSE(xml::extractPackageFromNamespace(u"http://schemas.android.com/apk")); + AAPT_ASSERT_FALSE(xml::extractPackageFromNamespace(u"http://schemas.android.com/apk/res")); + AAPT_ASSERT_FALSE(xml::extractPackageFromNamespace(u"http://schemas.android.com/apk/res/")); + AAPT_ASSERT_FALSE(xml::extractPackageFromNamespace( + u"http://schemas.android.com/apk/prv/res/")); + + Maybe<xml::ExtractedPackage> p = + xml::extractPackageFromNamespace(u"http://schemas.android.com/apk/res/a"); + AAPT_ASSERT_TRUE(p); + EXPECT_EQ(std::u16string(u"a"), p.value().package); + EXPECT_EQ(false, p.value().privateNamespace); + + p = xml::extractPackageFromNamespace(u"http://schemas.android.com/apk/prv/res/android"); + AAPT_ASSERT_TRUE(p); + EXPECT_EQ(std::u16string(u"android"), p.value().package); + EXPECT_EQ(true, p.value().privateNamespace); + + p = xml::extractPackageFromNamespace(u"http://schemas.android.com/apk/prv/res/com.test"); + AAPT_ASSERT_TRUE(p); + EXPECT_EQ(std::u16string(u"com.test"), p.value().package); + EXPECT_EQ(true, p.value().privateNamespace); + + p = xml::extractPackageFromNamespace(u"http://schemas.android.com/apk/res-auto"); + AAPT_ASSERT_TRUE(p); + EXPECT_EQ(std::u16string(), p.value().package); + EXPECT_EQ(true, p.value().privateNamespace); +} + +} // namespace aapt diff --git a/tools/layoutlib/.idea/compiler.xml b/tools/layoutlib/.idea/compiler.xml index 5aaaf18f9d94..35961a2896ac 100644 --- a/tools/layoutlib/.idea/compiler.xml +++ b/tools/layoutlib/.idea/compiler.xml @@ -21,7 +21,5 @@ <processorPath useClasspath="true" /> </profile> </annotationProcessing> - <bytecodeTargetLevel target="1.6" /> </component> -</project> - +</project>
\ No newline at end of file diff --git a/tools/layoutlib/bridge/src/android/animation/FakeAnimator.java b/tools/layoutlib/bridge/src/android/animation/FakeAnimator.java deleted file mode 100644 index 78aedc5a3102..000000000000 --- a/tools/layoutlib/bridge/src/android/animation/FakeAnimator.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.animation; - -/** - * A fake implementation of Animator which doesn't do anything. - */ -public class FakeAnimator extends Animator { - @Override - public long getStartDelay() { - return 0; - } - - @Override - public void setStartDelay(long startDelay) { - - } - - @Override - public Animator setDuration(long duration) { - return this; - } - - @Override - public long getDuration() { - return 0; - } - - @Override - public void setInterpolator(TimeInterpolator value) { - - } - - @Override - public boolean isRunning() { - return false; - } -} diff --git a/tools/layoutlib/bridge/src/android/animation/PropertyValuesHolder_Delegate.java b/tools/layoutlib/bridge/src/android/animation/PropertyValuesHolder_Delegate.java index 4603b6362b0e..28a489ad6ed7 100644 --- a/tools/layoutlib/bridge/src/android/animation/PropertyValuesHolder_Delegate.java +++ b/tools/layoutlib/bridge/src/android/animation/PropertyValuesHolder_Delegate.java @@ -16,9 +16,16 @@ package android.animation; +import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + /** * Delegate implementing the native methods of android.animation.PropertyValuesHolder * @@ -29,81 +36,162 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate; * around to map int to instance of the delegate. * * The main goal of this class' methods are to provide a native way to access setters and getters - * on some object. In this case we want to default to using Java reflection instead so the native - * methods do nothing. + * on some object. We override these methods to use reflection since the original reflection + * implementation of the PropertyValuesHolder won't be able to access protected methods. * */ -/*package*/ class PropertyValuesHolder_Delegate { +/*package*/ +@SuppressWarnings("unused") +class PropertyValuesHolder_Delegate { + // This code is copied from android.animation.PropertyValuesHolder and must be kept in sync + // We try several different types when searching for appropriate setter/getter functions. + // The caller may have supplied values in a type that does not match the setter/getter + // functions (such as the integers 0 and 1 to represent floating point values for alpha). + // Also, the use of generics in constructors means that we end up with the Object versions + // of primitive types (Float vs. float). But most likely, the setter/getter functions + // will take primitive types instead. + // So we supply an ordered array of other types to try before giving up. + private static Class[] FLOAT_VARIANTS = {float.class, Float.class, double.class, int.class, + Double.class, Integer.class}; + private static Class[] INTEGER_VARIANTS = {int.class, Integer.class, float.class, double.class, + Float.class, Double.class}; + + private static final Object sMethodIndexLock = new Object(); + private static final Map<Long, Method> ID_TO_METHOD = new HashMap<Long, Method>(); + private static final Map<String, Long> METHOD_NAME_TO_ID = new HashMap<String, Long>(); + private static long sNextId = 1; + + private static long registerMethod(Class<?> targetClass, String methodName, Class[] types, + int nArgs) { + // Encode the number of arguments in the method name + String methodIndexName = String.format("%1$s.%2$s#%3$d", targetClass.getSimpleName(), + methodName, nArgs); + synchronized (sMethodIndexLock) { + Long methodId = METHOD_NAME_TO_ID.get(methodIndexName); + + if (methodId != null) { + // The method was already registered + return methodId; + } + + Class[] args = new Class[nArgs]; + Method method = null; + for (Class typeVariant : types) { + for (int i = 0; i < nArgs; i++) { + args[i] = typeVariant; + } + try { + method = targetClass.getDeclaredMethod(methodName, args); + } catch (NoSuchMethodException ignore) { + } + } + + if (method != null) { + methodId = sNextId++; + ID_TO_METHOD.put(methodId, method); + METHOD_NAME_TO_ID.put(methodIndexName, methodId); + + return methodId; + } + } + + // Method not found + return 0; + } + + private static void callMethod(Object target, long methodID, Object... args) { + Method method = ID_TO_METHOD.get(methodID); + assert method != null; + + try { + method.setAccessible(true); + method.invoke(target, args); + } catch (IllegalAccessException e) { + Bridge.getLog().error(null, "Unable to update property during animation", e, null); + } catch (InvocationTargetException e) { + Bridge.getLog().error(null, "Unable to update property during animation", e, null); + } + } @LayoutlibDelegate /*package*/ static long nGetIntMethod(Class<?> targetClass, String methodName) { - // return 0 to force PropertyValuesHolder to use Java reflection. - return 0; + return nGetMultipleIntMethod(targetClass, methodName, 1); } @LayoutlibDelegate /*package*/ static long nGetFloatMethod(Class<?> targetClass, String methodName) { - // return 0 to force PropertyValuesHolder to use Java reflection. - return 0; + return nGetMultipleFloatMethod(targetClass, methodName, 1); } @LayoutlibDelegate /*package*/ static long nGetMultipleIntMethod(Class<?> targetClass, String methodName, int numParams) { - // TODO: return the right thing. - return 0; + return registerMethod(targetClass, methodName, INTEGER_VARIANTS, numParams); } @LayoutlibDelegate /*package*/ static long nGetMultipleFloatMethod(Class<?> targetClass, String methodName, int numParams) { - // TODO: return the right thing. - return 0; + return registerMethod(targetClass, methodName, FLOAT_VARIANTS, numParams); } @LayoutlibDelegate /*package*/ static void nCallIntMethod(Object target, long methodID, int arg) { - // do nothing + callMethod(target, methodID, arg); } @LayoutlibDelegate /*package*/ static void nCallFloatMethod(Object target, long methodID, float arg) { - // do nothing + callMethod(target, methodID, arg); } @LayoutlibDelegate /*package*/ static void nCallTwoIntMethod(Object target, long methodID, int arg1, int arg2) { - // do nothing + callMethod(target, methodID, arg1, arg2); } @LayoutlibDelegate /*package*/ static void nCallFourIntMethod(Object target, long methodID, int arg1, int arg2, int arg3, int arg4) { - // do nothing + callMethod(target, methodID, arg1, arg2, arg3, arg4); } @LayoutlibDelegate /*package*/ static void nCallMultipleIntMethod(Object target, long methodID, int[] args) { - // do nothing + assert args != null; + + // Box parameters + Object[] params = new Object[args.length]; + for (int i = 0; i < args.length; i++) { + params[i] = args; + } + callMethod(target, methodID, params); } @LayoutlibDelegate /*package*/ static void nCallTwoFloatMethod(Object target, long methodID, float arg1, float arg2) { - // do nothing + callMethod(target, methodID, arg1, arg2); } @LayoutlibDelegate /*package*/ static void nCallFourFloatMethod(Object target, long methodID, float arg1, float arg2, float arg3, float arg4) { - // do nothing + callMethod(target, methodID, arg1, arg2, arg3, arg4); } @LayoutlibDelegate /*package*/ static void nCallMultipleFloatMethod(Object target, long methodID, float[] args) { - // do nothing + assert args != null; + + // Box parameters + Object[] params = new Object[args.length]; + for (int i = 0; i < args.length; i++) { + params[i] = args; + } + callMethod(target, methodID, params); } } diff --git a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java index 31dd3d943769..db4c6dc68b77 100644 --- a/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java +++ b/tools/layoutlib/bridge/src/android/content/res/BridgeTypedArray.java @@ -327,12 +327,19 @@ public final class BridgeTypedArray extends TypedArray { return null; } - // let the framework inflate the ColorStateList from the XML file. - File f = new File(value); - if (f.isFile()) { - try { - XmlPullParser parser = ParserFactory.create(f); + try { + // Get the state list file content from callback to parse PSI file + XmlPullParser parser = mContext.getLayoutlibCallback().getXmlFileParser(value); + if (parser == null) { + // If used with a version of Android Studio that does not implement getXmlFileParser + // fall back to reading the file from disk + File f = new File(value); + if (f.isFile()) { + parser = ParserFactory.create(f); + } + } + if (parser != null) { BridgeXmlBlockParser blockParser = new BridgeXmlBlockParser( parser, mContext, resValue.isFramework()); try { @@ -341,18 +348,18 @@ public final class BridgeTypedArray extends TypedArray { } finally { blockParser.ensurePopped(); } - } catch (XmlPullParserException e) { - Bridge.getLog().error(LayoutLog.TAG_BROKEN, - "Failed to configure parser for " + value, e, null); - return null; - } catch (Exception e) { - // this is an error and not warning since the file existence is checked before - // attempting to parse it. - Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, - "Failed to parse file " + value, e, null); - - return null; } + } catch (XmlPullParserException e) { + Bridge.getLog().error(LayoutLog.TAG_BROKEN, + "Failed to configure parser for " + value, e, null); + return null; + } catch (Exception e) { + // this is an error and not warning since the file existence is checked before + // attempting to parse it. + Bridge.getLog().error(LayoutLog.TAG_RESOURCES_READ, + "Failed to parse file " + value, e, null); + + return null; } try { diff --git a/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java index 60514b658649..8d5863be918b 100644 --- a/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/BitmapFactory_Delegate.java @@ -122,4 +122,35 @@ import java.util.Set; /*package*/ static boolean nativeIsSeekable(FileDescriptor fd) { return true; } + + /** + * Set the newly decoded bitmap's density based on the Options. + * + * Copied from {@link BitmapFactory#setDensityFromOptions(Bitmap, Options)}. + */ + @LayoutlibDelegate + /*package*/ static void setDensityFromOptions(Bitmap outputBitmap, Options opts) { + if (outputBitmap == null || opts == null) return; + + final int density = opts.inDensity; + if (density != 0) { + outputBitmap.setDensity(density); + final int targetDensity = opts.inTargetDensity; + if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) { + return; + } + + // --- Change from original implementation begins --- + // LayoutLib doesn't scale the nine patch when decoding it. Hence, don't change the + // density of the source bitmap in case of ninepatch. + + if (opts.inScaled) { + // --- Change from original implementation ends. --- + outputBitmap.setDensity(targetDensity); + } + } else if (opts.inBitmap != null) { + // bitmap was reused, ensure density is reset + outputBitmap.setDensity(Bitmap.getDefaultDensity()); + } + } } diff --git a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java index 64cd5031346e..ba0d399ce52f 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java @@ -35,7 +35,6 @@ import java.awt.RenderingHints; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.Arc2D; -import java.awt.geom.Path2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; @@ -713,8 +712,27 @@ public final class Canvas_Delegate { if (bounds.isEmpty()) { // Apple JRE 1.6 doesn't like drawing empty shapes. // http://b.android.com/178278 - return; + + if (pathDelegate.isEmpty()) { + // This means that the path doesn't have any lines or curves so + // nothing to draw. + return; + } + + // The stroke width is not consider for the size of the bounds so, + // for example, a horizontal line, would be considered as an empty + // rectangle. + // If the strokeWidth is not 0, we use it to consider the size of the + // path as well. + float strokeWidth = paintDelegate.getStrokeWidth(); + if (strokeWidth <= 0.0f) { + return; + } + bounds.setRect(bounds.getX(), bounds.getY(), + Math.max(strokeWidth, bounds.getWidth()), + Math.max(strokeWidth, bounds.getHeight())); } + int style = paintDelegate.getStyle(); if (style == Paint.Style.FILL.nativeInt || diff --git a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java index a545283ea0ca..dbd45c4f68be 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java @@ -152,11 +152,7 @@ public class Paint_Delegate { * returns the value of stroke miter needed by the java api. */ public float getJavaStrokeMiter() { - float miter = mStrokeMiter * mStrokeWidth; - if (miter < 1.f) { - miter = 1.f; - } - return miter; + return mStrokeMiter; } public int getJavaCap() { diff --git a/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java index dd2978f5c414..fc9b4f772883 100644 --- a/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/PathMeasure_Delegate.java @@ -19,10 +19,12 @@ package android.graphics; import com.android.ide.common.rendering.api.LayoutLog; import com.android.layoutlib.bridge.Bridge; import com.android.layoutlib.bridge.impl.DelegateManager; +import com.android.layoutlib.bridge.util.CachedPathIteratorFactory; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import com.android.layoutlib.bridge.util.CachedPathIteratorFactory.CachedPathIterator; + import java.awt.geom.PathIterator; -import java.awt.geom.Point2D; /** * Delegate implementing the native methods of {@link android.graphics.PathMeasure} @@ -38,35 +40,30 @@ import java.awt.geom.Point2D; * @see DelegateManager */ public final class PathMeasure_Delegate { + // ---- delegate manager ---- private static final DelegateManager<PathMeasure_Delegate> sManager = new DelegateManager<PathMeasure_Delegate>(PathMeasure_Delegate.class); // ---- delegate data ---- - // This governs how accurate the approximation of the Path is. - private static final float PRECISION = 0.002f; - - /** - * Array containing the path points components. There are three components for each point: - * <ul> - * <li>Fraction along the length of the path that the point resides</li> - * <li>The x coordinate of the point</li> - * <li>The y coordinate of the point</li> - * </ul> - */ - private float mPathPoints[]; + private CachedPathIteratorFactory mOriginalPathIterator; + private long mNativePath; + private PathMeasure_Delegate(long native_path, boolean forceClosed) { mNativePath = native_path; - if (forceClosed && mNativePath != 0) { - // Copy the path and call close - mNativePath = Path_Delegate.init2(native_path); - Path_Delegate.native_close(mNativePath); - } + if (native_path != 0) { + if (forceClosed) { + // Copy the path and call close + native_path = Path_Delegate.init2(native_path); + Path_Delegate.native_close(native_path); + } - mPathPoints = - mNativePath != 0 ? Path_Delegate.native_approximate(mNativePath, PRECISION) : null; + Path_Delegate pathDelegate = Path_Delegate.getDelegate(native_path); + mOriginalPathIterator = new CachedPathIteratorFactory(pathDelegate.getJavaShape() + .getPathIterator(null)); + } } @LayoutlibDelegate @@ -108,13 +105,19 @@ public final class PathMeasure_Delegate { PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance); assert pathMeasure != null; - if (forceClosed && native_path != 0) { - // Copy the path and call close - native_path = Path_Delegate.init2(native_path); - Path_Delegate.native_close(native_path); + if (native_path != 0) { + if (forceClosed) { + // Copy the path and call close + native_path = Path_Delegate.init2(native_path); + Path_Delegate.native_close(native_path); + } + + Path_Delegate pathDelegate = Path_Delegate.getDelegate(native_path); + pathMeasure.mOriginalPathIterator = new CachedPathIteratorFactory(pathDelegate.getJavaShape() + .getPathIterator(null)); } + pathMeasure.mNativePath = native_path; - pathMeasure.mPathPoints = Path_Delegate.native_approximate(native_path, PRECISION); } @LayoutlibDelegate @@ -122,21 +125,11 @@ public final class PathMeasure_Delegate { PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance); assert pathMeasure != null; - if (pathMeasure.mPathPoints == null) { + if (pathMeasure.mOriginalPathIterator == null) { return 0; } - float length = 0; - int nPoints = pathMeasure.mPathPoints.length / 3; - for (int i = 1; i < nPoints; i++) { - length += Point2D.distance( - pathMeasure.mPathPoints[(i - 1) * 3 + 1], - pathMeasure.mPathPoints[(i - 1) * 3 + 2], - pathMeasure.mPathPoints[i*3 + 1], - pathMeasure.mPathPoints[i*3 + 2]); - } - - return length; + return pathMeasure.mOriginalPathIterator.iterator().getTotalLength(); } @LayoutlibDelegate @@ -149,13 +142,10 @@ public final class PathMeasure_Delegate { return false; } - PathIterator pathIterator = path.getJavaShape().getPathIterator(null); - int type = 0; float segment[] = new float[6]; - while (!pathIterator.isDone()) { - type = pathIterator.currentSegment(segment); - pathIterator.next(); + for (PathIterator pi = path.getJavaShape().getPathIterator(null); !pi.isDone(); pi.next()) { + type = pi.currentSegment(segment); } // A path is a closed path if the last element is SEG_CLOSE @@ -176,33 +166,56 @@ public final class PathMeasure_Delegate { PathMeasure_Delegate pathMeasure = sManager.getDelegate(native_instance); assert pathMeasure != null; - if (pathMeasure.mPathPoints == null) { - return false; - } - - float accLength = 0; + CachedPathIterator iterator = pathMeasure.mOriginalPathIterator.iterator(); + float accLength = startD; boolean isZeroLength = true; // Whether the output has zero length or not - int nPoints = pathMeasure.mPathPoints.length / 3; - for (int i = 0; i < nPoints; i++) { - float x = pathMeasure.mPathPoints[i * 3 + 1]; - float y = pathMeasure.mPathPoints[i * 3 + 2]; - if (accLength >= startD && accLength <= stopD) { + float[] points = new float[6]; + + iterator.jumpToSegment(accLength); + while (!iterator.isDone() && (stopD - accLength > 0.1f)) { + int type = iterator.currentSegment(points, stopD - accLength); + + if (accLength - iterator.getCurrentSegmentLength() <= stopD) { if (startWithMoveTo) { startWithMoveTo = false; - Path_Delegate.native_moveTo(native_dst_path, x, y); - } else { - isZeroLength = false; - Path_Delegate.native_lineTo(native_dst_path, x, y); + + // If this segment is a MOVETO, then we just use that one. If not, then we issue + // a first moveto + if (type != PathIterator.SEG_MOVETO) { + float[] lastPoint = new float[2]; + iterator.getCurrentSegmentEnd(lastPoint); + Path_Delegate.native_moveTo(native_dst_path, lastPoint[0], lastPoint[1]); + } } - } - if (i > 0) { - accLength += Point2D.distance( - pathMeasure.mPathPoints[(i - 1) * 3 + 1], - pathMeasure.mPathPoints[(i - 1) * 3 + 2], - pathMeasure.mPathPoints[i * 3 + 1], - pathMeasure.mPathPoints[i * 3 + 2]); + isZeroLength = isZeroLength && iterator.getCurrentSegmentLength() > 0; + switch (type) { + case PathIterator.SEG_MOVETO: + Path_Delegate.native_moveTo(native_dst_path, points[0], points[1]); + break; + case PathIterator.SEG_LINETO: + Path_Delegate.native_lineTo(native_dst_path, points[0], points[1]); + break; + case PathIterator.SEG_CLOSE: + Path_Delegate.native_close(native_dst_path); + break; + case PathIterator.SEG_CUBICTO: + Path_Delegate.native_cubicTo(native_dst_path, points[0], points[1], + points[2], points[3], + points[4], points[5]); + break; + case PathIterator.SEG_QUADTO: + Path_Delegate.native_quadTo(native_dst_path, points[0], points[1], + points[2], + points[3]); + break; + default: + assert false; + } } + + accLength += iterator.getCurrentSegmentLength(); + iterator.next(); } return !isZeroLength; diff --git a/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java index a2a53fef7f1f..d0dd22f8faad 100644 --- a/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java +++ b/tools/layoutlib/bridge/src/android/graphics/Path_Delegate.java @@ -57,6 +57,8 @@ public final class Path_Delegate { private static final DelegateManager<Path_Delegate> sManager = new DelegateManager<Path_Delegate>(Path_Delegate.class); + private static final float EPSILON = 1e-4f; + // ---- delegate data ---- private FillType mFillType = FillType.WINDING; private Path2D mPath = new Path2D.Double(); @@ -64,6 +66,9 @@ public final class Path_Delegate { private float mLastX = 0; private float mLastY = 0; + // true if the path contains does not contain a curve or line. + private boolean mCachedIsEmpty = true; + // ---- Public Helper methods ---- public static Path_Delegate getDelegate(long nPath) { @@ -75,7 +80,7 @@ public final class Path_Delegate { } public void setJavaShape(Shape shape) { - mPath.reset(); + reset(); mPath.append(shape, false /*connect*/); } @@ -84,7 +89,7 @@ public final class Path_Delegate { } public void setPathIterator(PathIterator iterator) { - mPath.reset(); + reset(); mPath.append(iterator, false /*connect*/); } @@ -591,11 +596,37 @@ public final class Path_Delegate { /** - * Returns whether the path is empty. - * @return true if the path is empty. + * Returns whether the path already contains any points. + * Note that this is different to + * {@link #isEmpty} because if all elements are {@link PathIterator#SEG_MOVETO}, + * {@link #isEmpty} will return true while hasPoints will return false. */ - private boolean isEmpty() { - return mPath.getCurrentPoint() == null; + public boolean hasPoints() { + return !mPath.getPathIterator(null).isDone(); + } + + /** + * Returns whether the path is empty (contains no lines or curves). + * @see Path#isEmpty + */ + public boolean isEmpty() { + if (!mCachedIsEmpty) { + return false; + } + + float[] coords = new float[6]; + mCachedIsEmpty = Boolean.TRUE; + for (PathIterator it = mPath.getPathIterator(null); !it.isDone(); it.next()) { + int type = it.currentSegment(coords); + if (type != PathIterator.SEG_MOVETO) { + // Once we know that the path is not empty, we do not need to check again unless + // Path#reset is called. + mCachedIsEmpty = false; + return false; + } + } + + return true; } /** @@ -645,7 +676,7 @@ public final class Path_Delegate { * @param y The y-coordinate of the end of a line */ private void lineTo(float x, float y) { - if (isEmpty()) { + if (!hasPoints()) { mPath.moveTo(mLastX = 0, mLastY = 0); } mPath.lineTo(mLastX = x, mLastY = y); @@ -662,9 +693,15 @@ public final class Path_Delegate { * this contour, to specify a line */ private void rLineTo(float dx, float dy) { - if (isEmpty()) { + if (!hasPoints()) { mPath.moveTo(mLastX = 0, mLastY = 0); } + + if (Math.abs(dx) < EPSILON && Math.abs(dy) < EPSILON) { + // The delta is so small that this shouldn't generate a line + return; + } + dx += mLastX; dy += mLastY; mPath.lineTo(mLastX = dx, mLastY = dy); @@ -699,7 +736,7 @@ public final class Path_Delegate { * this contour, for the end point of a quadratic curve */ private void rQuadTo(float dx1, float dy1, float dx2, float dy2) { - if (isEmpty()) { + if (!hasPoints()) { mPath.moveTo(mLastX = 0, mLastY = 0); } dx1 += mLastX; @@ -723,7 +760,7 @@ public final class Path_Delegate { */ private void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) { - if (isEmpty()) { + if (!hasPoints()) { mPath.moveTo(0, 0); } mPath.curveTo(x1, y1, x2, y2, mLastX = x3, mLastY = y3); @@ -736,7 +773,7 @@ public final class Path_Delegate { */ private void rCubicTo(float dx1, float dy1, float dx2, float dy2, float dx3, float dy3) { - if (isEmpty()) { + if (!hasPoints()) { mPath.moveTo(mLastX = 0, mLastY = 0); } dx1 += mLastX; diff --git a/tools/layoutlib/bridge/src/android/os/SystemClock_Delegate.java b/tools/layoutlib/bridge/src/android/os/SystemClock_Delegate.java index 5f0d98b35431..9677aaf5d07a 100644 --- a/tools/layoutlib/bridge/src/android/os/SystemClock_Delegate.java +++ b/tools/layoutlib/bridge/src/android/os/SystemClock_Delegate.java @@ -18,6 +18,7 @@ package android.os; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import com.android.tools.layoutlib.java.System_Delegate; /** * Delegate implementing the native methods of android.os.SystemClock @@ -30,9 +31,6 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate; * */ public class SystemClock_Delegate { - private static long sBootTime = System.currentTimeMillis(); - private static long sBootTimeNano = System.nanoTime(); - /** * Returns milliseconds since boot, not counting time spent in deep sleep. * <b>Note:</b> This value may get reset occasionally (before it would @@ -42,7 +40,7 @@ public class SystemClock_Delegate { */ @LayoutlibDelegate /*package*/ static long uptimeMillis() { - return System.currentTimeMillis() - sBootTime; + return System_Delegate.currentTimeMillis() - System_Delegate.bootTimeMillis(); } /** @@ -52,7 +50,7 @@ public class SystemClock_Delegate { */ @LayoutlibDelegate /*package*/ static long elapsedRealtime() { - return System.currentTimeMillis() - sBootTime; + return System_Delegate.currentTimeMillis() - System_Delegate.bootTimeMillis(); } /** @@ -62,7 +60,7 @@ public class SystemClock_Delegate { */ @LayoutlibDelegate /*package*/ static long elapsedRealtimeNanos() { - return System.nanoTime() - sBootTimeNano; + return System_Delegate.nanoTime() - System_Delegate.bootTime(); } /** @@ -72,7 +70,7 @@ public class SystemClock_Delegate { */ @LayoutlibDelegate /*package*/ static long currentThreadTimeMillis() { - return System.currentTimeMillis(); + return System_Delegate.currentTimeMillis(); } /** @@ -84,7 +82,7 @@ public class SystemClock_Delegate { */ @LayoutlibDelegate /*package*/ static long currentThreadTimeMicro() { - return System.currentTimeMillis() * 1000; + return System_Delegate.currentTimeMillis() * 1000; } /** diff --git a/tools/layoutlib/bridge/src/android/view/Choreographer_Delegate.java b/tools/layoutlib/bridge/src/android/view/Choreographer_Delegate.java index f75ee5030674..01af669e39d3 100644 --- a/tools/layoutlib/bridge/src/android/view/Choreographer_Delegate.java +++ b/tools/layoutlib/bridge/src/android/view/Choreographer_Delegate.java @@ -17,6 +17,8 @@ package android.view; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import java.util.concurrent.atomic.AtomicReference; + /** * Delegate used to provide new implementation of a select few methods of {@link Choreographer} * @@ -25,9 +27,41 @@ import com.android.tools.layoutlib.annotations.LayoutlibDelegate; * */ public class Choreographer_Delegate { + static final AtomicReference<Choreographer> mInstance = new AtomicReference<Choreographer>(); + + @LayoutlibDelegate + public static Choreographer getInstance() { + if (mInstance.get() == null) { + mInstance.compareAndSet(null, Choreographer.getInstance_Original()); + } + + return mInstance.get(); + } @LayoutlibDelegate public static float getRefreshRate() { return 60.f; } + + @LayoutlibDelegate + static void scheduleVsyncLocked(Choreographer thisChoreographer) { + // do nothing + } + + public static void doFrame(long frameTimeNanos) { + Choreographer thisChoreographer = Choreographer.getInstance(); + + thisChoreographer.mLastFrameTimeNanos = frameTimeNanos; + + thisChoreographer.mFrameInfo.markInputHandlingStart(); + thisChoreographer.doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); + + thisChoreographer.mFrameInfo.markAnimationsStart(); + thisChoreographer.doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); + + thisChoreographer.mFrameInfo.markPerformTraversalsStart(); + thisChoreographer.doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); + + thisChoreographer.doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos); + } } diff --git a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java index 82012c1e2767..2560c31196f0 100644 --- a/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java +++ b/tools/layoutlib/bridge/src/android/view/IWindowManagerImpl.java @@ -17,7 +17,9 @@ package android.view; import android.graphics.Point; +import android.graphics.Rect; import com.android.internal.app.IAssistScreenshotReceiver; +import com.android.internal.os.IResultReceiver; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodClient; @@ -29,6 +31,7 @@ import android.os.IBinder; import android.os.IRemoteCallback; import android.os.RemoteException; import android.util.DisplayMetrics; +import android.view.AppTransitionAnimationSpec; import java.lang.Override; @@ -73,10 +76,10 @@ public class IWindowManagerImpl implements IWindowManager { @Override public void addAppToken(int arg0, IApplicationToken arg1, int arg2, int arg3, int arg4, - boolean arg5, boolean arg6, int arg7, int arg8, boolean arg9, boolean arg10) + boolean arg5, boolean arg6, int arg7, int arg8, boolean arg9, boolean arg10, + Rect arg11, Configuration arg12, int arg13, boolean arg14, boolean arg15) throws RemoteException { // TODO Auto-generated method stub - } @Override @@ -240,6 +243,19 @@ public class IWindowManagerImpl implements IWindowManager { } @Override + public void overridePendingAppTransitionMultiThumbFuture( + IAppTransitionAnimationSpecsFuture specsFuture, IRemoteCallback startedCallback, + boolean scaleUp) throws RemoteException { + + } + + @Override + public void overridePendingAppTransitionMultiThumb(AppTransitionAnimationSpec[] specs, + IRemoteCallback callback0, IRemoteCallback callback1, boolean scaleUp) { + // TODO Auto-generated method stub + } + + @Override public void pauseKeyDispatching(IBinder arg0) throws RemoteException { // TODO Auto-generated method stub @@ -284,7 +300,7 @@ public class IWindowManagerImpl implements IWindowManager { @Override public Bitmap screenshotApplications(IBinder appToken, int displayId, int maxWidth, - int maxHeight) throws RemoteException { + int maxHeight, float frameScale) throws RemoteException { // TODO Auto-generated method stub return null; } @@ -307,9 +323,10 @@ public class IWindowManagerImpl implements IWindowManager { } @Override - public void setAppTask(IBinder arg0, int arg1) throws RemoteException { + public void setAppTask(IBinder arg0, int arg1, int arg2, Rect arg3, Configuration arg4, + int arg5, boolean arg6) + throws RemoteException { // TODO Auto-generated method stub - } @Override @@ -331,11 +348,6 @@ public class IWindowManagerImpl implements IWindowManager { } @Override - public void setAppWillBeHidden(IBinder arg0) throws RemoteException { - // TODO Auto-generated method stub - } - - @Override public void setEventDispatching(boolean arg0) throws RemoteException { // TODO Auto-generated method stub } @@ -511,4 +523,42 @@ public class IWindowManagerImpl implements IWindowManager { // TODO Auto-generated method stub return null; } + + @Override + public int getDockedStackSide() throws RemoteException { + return 0; + } + + @Override + public void setDockedStackResizing(boolean resizing) throws RemoteException { + } + + @Override + public void cancelTaskWindowTransition(int taskId) { + } + + @Override + public void cancelTaskThumbnailTransition(int taskId) { + } + + @Override + public void endProlongedAnimations() { + } + + @Override + public void registerDockedStackListener(IDockedStackListener listener) throws RemoteException { + } + + @Override + public void setResizeDimLayer(boolean visible, int targetStackId, float alpha) + throws RemoteException { + } + + @Override + public void requestAppKeyboardShortcuts(IResultReceiver receiver) throws RemoteException { + } + + @Override + public void getStableInsets(Rect outInsets) throws RemoteException { + } } diff --git a/tools/layoutlib/bridge/src/android/view/RenderNode_Delegate.java b/tools/layoutlib/bridge/src/android/view/RenderNode_Delegate.java index 6c949d9dcd4e..1465f5089599 100644 --- a/tools/layoutlib/bridge/src/android/view/RenderNode_Delegate.java +++ b/tools/layoutlib/bridge/src/android/view/RenderNode_Delegate.java @@ -19,6 +19,8 @@ package android.view; import com.android.layoutlib.bridge.impl.DelegateManager; import com.android.tools.layoutlib.annotations.LayoutlibDelegate; +import android.graphics.Matrix; + /** * Delegate implementing the native methods of {@link RenderNode} * <p/> @@ -36,6 +38,19 @@ public class RenderNode_Delegate { private float mLift; + private float mTranslationX; + private float mTranslationY; + private float mTranslationZ; + private float mRotation; + private float mScaleX = 1; + private float mScaleY = 1; + private float mPivotX; + private float mPivotY; + private boolean mPivotExplicitlySet; + private int mLeft; + private int mRight; + private int mTop; + private int mBottom; @SuppressWarnings("UnusedDeclaration") private String mName; @@ -69,4 +84,245 @@ public class RenderNode_Delegate { } return 0f; } + + @LayoutlibDelegate + /*package*/ static boolean nSetTranslationX(long renderNode, float translationX) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mTranslationX != translationX) { + delegate.mTranslationX = translationX; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static float nGetTranslationX(long renderNode) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + return delegate.mTranslationX; + } + return 0f; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetTranslationY(long renderNode, float translationY) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mTranslationY != translationY) { + delegate.mTranslationY = translationY; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static float nGetTranslationY(long renderNode) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + return delegate.mTranslationY; + } + return 0f; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetTranslationZ(long renderNode, float translationZ) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mTranslationZ != translationZ) { + delegate.mTranslationZ = translationZ; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static float nGetTranslationZ(long renderNode) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + return delegate.mTranslationZ; + } + return 0f; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetRotation(long renderNode, float rotation) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mRotation != rotation) { + delegate.mRotation = rotation; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static float nGetRotation(long renderNode) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + return delegate.mRotation; + } + return 0f; + } + + @LayoutlibDelegate + /*package*/ static void getMatrix(RenderNode renderNode, Matrix outMatrix) { + outMatrix.reset(); + if (renderNode != null) { + float rotation = renderNode.getRotation(); + float translationX = renderNode.getTranslationX(); + float translationY = renderNode.getTranslationY(); + float pivotX = renderNode.getPivotX(); + float pivotY = renderNode.getPivotY(); + float scaleX = renderNode.getScaleX(); + float scaleY = renderNode.getScaleY(); + + outMatrix.setTranslate(translationX, translationY); + outMatrix.preRotate(rotation, pivotX, pivotY); + outMatrix.preScale(scaleX, scaleY, pivotX, pivotY); + } + } + + @LayoutlibDelegate + /*package*/ static boolean nSetLeft(long renderNode, int left) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mLeft != left) { + delegate.mLeft = left; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetTop(long renderNode, int top) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mTop != top) { + delegate.mTop = top; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetRight(long renderNode, int right) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mRight != right) { + delegate.mRight = right; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetBottom(long renderNode, int bottom) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mBottom != bottom) { + delegate.mBottom = bottom; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetLeftTopRightBottom(long renderNode, int left, int top, int right, + int bottom) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && (delegate.mLeft != left || delegate.mTop != top || delegate + .mRight != right || delegate.mBottom != bottom)) { + delegate.mLeft = left; + delegate.mTop = top; + delegate.mRight = right; + delegate.mBottom = bottom; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static boolean nIsPivotExplicitlySet(long renderNode) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + return delegate != null && delegate.mPivotExplicitlySet; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetPivotX(long renderNode, float pivotX) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + delegate.mPivotX = pivotX; + delegate.mPivotExplicitlySet = true; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static float nGetPivotX(long renderNode) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + if (delegate.mPivotExplicitlySet) { + return delegate.mPivotX; + } else { + return (delegate.mRight - delegate.mLeft) / 2.0f; + } + } + return 0f; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetPivotY(long renderNode, float pivotY) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + delegate.mPivotY = pivotY; + delegate.mPivotExplicitlySet = true; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static float nGetPivotY(long renderNode) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + if (delegate.mPivotExplicitlySet) { + return delegate.mPivotY; + } else { + return (delegate.mBottom - delegate.mTop) / 2.0f; + } + } + return 0f; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetScaleX(long renderNode, float scaleX) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mScaleX != scaleX) { + delegate.mScaleX = scaleX; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static float nGetScaleX(long renderNode) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + return delegate.mScaleX; + } + return 0f; + } + + @LayoutlibDelegate + /*package*/ static boolean nSetScaleY(long renderNode, float scaleY) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null && delegate.mScaleY != scaleY) { + delegate.mScaleY = scaleY; + return true; + } + return false; + } + + @LayoutlibDelegate + /*package*/ static float nGetScaleY(long renderNode) { + RenderNode_Delegate delegate = sManager.getDelegate(renderNode); + if (delegate != null) { + return delegate.mScaleY; + } + return 0f; + } } diff --git a/tools/layoutlib/bridge/src/android/view/WindowCallback.java b/tools/layoutlib/bridge/src/android/view/WindowCallback.java index d691c8ea71a5..411417c883fb 100644 --- a/tools/layoutlib/bridge/src/android/view/WindowCallback.java +++ b/tools/layoutlib/bridge/src/android/view/WindowCallback.java @@ -16,10 +16,13 @@ package android.view; +import android.annotation.Nullable; import android.view.ActionMode.Callback; import android.view.WindowManager.LayoutParams; import android.view.accessibility.AccessibilityEvent; +import java.util.List; + /** * An empty implementation of {@link Window.Callback} that always returns null/false. */ @@ -138,4 +141,9 @@ public class WindowCallback implements Window.Callback { public void onActionModeFinished(ActionMode mode) { } + + @Override + public void onProvideKeyboardShortcuts(List<KeyboardShortcutGroup> data, @Nullable Menu menu) { + + } } diff --git a/tools/layoutlib/bridge/src/android/widget/SimpleMonthView_Delegate.java b/tools/layoutlib/bridge/src/android/widget/SimpleMonthView_Delegate.java deleted file mode 100644 index 8e41e5162513..000000000000 --- a/tools/layoutlib/bridge/src/android/widget/SimpleMonthView_Delegate.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.widget; - -import com.android.tools.layoutlib.annotations.LayoutlibDelegate; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.icu.text.SimpleDateFormat; -import android.text.format.DateFormat; - -import java.util.Calendar; -import java.util.Locale; - -/** - * Delegate that provides implementation for some methods in {@link SimpleMonthView}. - * <p/> - * Through the layoutlib_create tool, selected methods of SimpleMonthView have been replaced by - * calls to methods of the same name in this delegate class. - * <p/> - * The main purpose of this class is to use {@link android.icu.text.SimpleDateFormat} instead of - * {@link java.text.SimpleDateFormat}. - */ -public class SimpleMonthView_Delegate { - - private static final String DEFAULT_TITLE_FORMAT = "MMMMy"; - private static final String DAY_OF_WEEK_FORMAT = "EEEEE"; - - // Maintain a cache of the last view used, so that the formatters can be reused. - @Nullable private static SimpleMonthView sLastView; - @Nullable private static SimpleMonthView_Delegate sLastDelegate; - - private SimpleDateFormat mTitleFormatter; - private SimpleDateFormat mDayOfWeekFormatter; - - private Locale locale; - - @LayoutlibDelegate - /*package*/ static CharSequence getTitle(SimpleMonthView view) { - if (view.mTitle == null) { - SimpleMonthView_Delegate delegate = getDelegate(view); - if (delegate.mTitleFormatter == null) { - delegate.mTitleFormatter = new SimpleDateFormat(DateFormat.getBestDateTimePattern( - getLocale(delegate, view), DEFAULT_TITLE_FORMAT)); - } - view.mTitle = delegate.mTitleFormatter.format(view.mCalendar.getTime()); - } - return view.mTitle; - } - - @LayoutlibDelegate - /*package*/ static String getDayOfWeekLabel(SimpleMonthView view, int dayOfWeek) { - view.mDayOfWeekLabelCalendar.set(Calendar.DAY_OF_WEEK, dayOfWeek); - SimpleMonthView_Delegate delegate = getDelegate(view); - if (delegate.mDayOfWeekFormatter == null) { - delegate.mDayOfWeekFormatter = - new SimpleDateFormat(DAY_OF_WEEK_FORMAT, getLocale(delegate, view)); - } - return delegate.mDayOfWeekFormatter.format(view.mDayOfWeekLabelCalendar.getTime()); - } - - private static Locale getLocale(SimpleMonthView_Delegate delegate, SimpleMonthView view) { - if (delegate.locale == null) { - delegate.locale = view.getContext().getResources().getConfiguration().locale; - } - return delegate.locale; - } - - @NonNull - private static SimpleMonthView_Delegate getDelegate(SimpleMonthView view) { - if (view == sLastView) { - assert sLastDelegate != null; - return sLastDelegate; - } else { - sLastView = view; - sLastDelegate = new SimpleMonthView_Delegate(); - return sLastDelegate; - } - } - - public static void clearCache() { - sLastView = null; - sLastDelegate = null; - } -} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java index 48ca7d8d5fb6..c8e3d03169e8 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java @@ -183,7 +183,7 @@ public final class Bridge extends com.android.ide.common.rendering.api.Bridge { */ private static LayoutLog sCurrentLog = sDefaultLog; - private static final int LAST_SUPPORTED_FEATURE = Features.RECYCLER_VIEW_ADAPTER; + private static final int LAST_SUPPORTED_FEATURE = Features.THEME_PREVIEW_NAVIGATION_BAR; @Override public int getApiLevel() { diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java index feb25905390c..2ac212c312c0 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/BridgeRenderSession.java @@ -23,6 +23,7 @@ import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.common.rendering.api.Result; import com.android.ide.common.rendering.api.ViewInfo; import com.android.layoutlib.bridge.impl.RenderSessionImpl; +import com.android.tools.layoutlib.java.System_Delegate; import android.view.View; import android.view.ViewGroup; @@ -191,6 +192,21 @@ public class BridgeRenderSession extends RenderSession { } @Override + public void setSystemTimeNanos(long nanos) { + System_Delegate.setNanosTime(nanos); + } + + @Override + public void setSystemBootTimeNanos(long nanos) { + System_Delegate.setBootTimeNanos(nanos); + } + + @Override + public void setElapsedFrameTimeNanos(long nanos) { + mSession.setElapsedFrameTimeNanos(nanos); + } + + @Override public void dispose() { } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java index f2d214c43135..4b89217a581e 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java @@ -73,6 +73,7 @@ import android.os.Looper; import android.os.Parcel; import android.os.PowerManager; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.UserHandle; import android.util.AttributeSet; import android.util.DisplayMetrics; @@ -196,7 +197,12 @@ public final class BridgeContext extends Context { mRenderResources = renderResources; mConfig = config; - mAssets = new BridgeAssetManager(); + AssetManager systemAssetManager = AssetManager.getSystem(); + if (systemAssetManager instanceof BridgeAssetManager) { + mAssets = (BridgeAssetManager) systemAssetManager; + } else { + throw new AssertionError("Creating BridgeContext without initializing Bridge"); + } mAssets.setAssetRepository(assets); mApplicationInfo = new ApplicationInfo(); @@ -1130,6 +1136,11 @@ public final class BridgeContext extends Context { public boolean unlinkToDeath(DeathRecipient recipient, int flags) { return false; } + + @Override + public void shellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, + String[] args, ResultReceiver resultReceiver) { + } }; } return mBinder; @@ -1247,6 +1258,12 @@ public final class BridgeContext extends Context { } @Override + public boolean migrateDatabaseFrom(Context sourceContext, String name) { + // pass + return false; + } + + @Override public boolean deleteDatabase(String arg0) { // pass return false; @@ -1359,6 +1376,12 @@ public final class BridgeContext extends Context { } @Override + public File getSharedPreferencesPath(String name) { + // pass + return null; + } + + @Override public File getFilesDir() { // pass return null; @@ -1406,13 +1429,15 @@ public final class BridgeContext extends Context { } @Override - public File getSharedPrefsFile(String name) { - // pass - return null; + public SharedPreferences getSharedPreferences(String arg0, int arg1) { + if (mSharedPreferences == null) { + mSharedPreferences = new BridgeSharedPreferences(); + } + return mSharedPreferences; } @Override - public SharedPreferences getSharedPreferences(String arg0, int arg1) { + public SharedPreferences getSharedPreferences(File arg0, int arg1) { if (mSharedPreferences == null) { mSharedPreferences = new BridgeSharedPreferences(); } @@ -1420,6 +1445,18 @@ public final class BridgeContext extends Context { } @Override + public boolean migrateSharedPreferencesFrom(Context sourceContext, String name) { + // pass + return false; + } + + @Override + public boolean deleteSharedPreferences(String name) { + // pass + return false; + } + + @Override public Drawable getWallpaper() { // pass return null; @@ -1620,6 +1657,11 @@ public final class BridgeContext extends Context { } @Override + public void sendStickyBroadcastAsUser(Intent intent, UserHandle user, Bundle options) { + // pass + } + + @Override public void sendStickyOrderedBroadcastAsUser(Intent intent, UserHandle user, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, @@ -1788,4 +1830,26 @@ public final class BridgeContext extends Context { Integer pos = mScrollYPos.get(view); return pos != null ? pos : 0; } + + @Override + public Context createDeviceEncryptedStorageContext() { + // pass + return null; + } + + @Override + public Context createCredentialEncryptedStorageContext() { + // pass + return null; + } + + @Override + public boolean isDeviceEncryptedStorage() { + return false; + } + + @Override + public boolean isCredentialEncryptedStorage() { + return false; + } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java index 8899e53648d2..01c3c500855d 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java @@ -184,8 +184,10 @@ public class BridgeIInputMethodManager implements IInputMethodManager { } @Override - public InputBindResult startInput(IInputMethodClient client, IInputContext inputContext, - EditorInfo attribute, int controlFlags) throws RemoteException { + public InputBindResult startInput( + /* @InputMethodClient.StartInputReason */ int startInputReason, + IInputMethodClient client, IInputContext inputContext, EditorInfo attribute, + int controlFlags) throws RemoteException { // TODO Auto-generated method stub return null; } @@ -226,9 +228,11 @@ public class BridgeIInputMethodManager implements IInputMethodManager { } @Override - public InputBindResult windowGainedFocus(IInputMethodClient client, IBinder windowToken, - int controlFlags, int softInputMode, int windowFlags, EditorInfo attribute, - IInputContext inputContext) throws RemoteException { + public InputBindResult windowGainedFocus( + /* @InputMethodClient.StartInputReason */ int startInputReason, + IInputMethodClient client, IBinder windowToken, int controlFlags, int softInputMode, + int windowFlags, EditorInfo attribute, IInputContext inputContext) + throws RemoteException { // TODO Auto-generated method stub return null; } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java index f04654eded0f..037ce57ced1b 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePackageManager.java @@ -16,6 +16,7 @@ package com.android.layoutlib.bridge.android; +import android.annotation.NonNull; import android.app.PackageInstallObserver; import android.content.ComponentName; import android.content.Intent; @@ -24,6 +25,7 @@ import android.content.IntentSender; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ContainerEncryptionParams; +import android.content.pm.EphemeralApplicationInfo; import android.content.pm.FeatureInfo; import android.content.pm.IPackageDataObserver; import android.content.pm.IPackageDeleteObserver; @@ -32,7 +34,6 @@ import android.content.pm.IPackageStatsObserver; import android.content.pm.InstrumentationInfo; import android.content.pm.IntentFilterVerificationInfo; import android.content.pm.KeySet; -import android.content.pm.ManifestDigest; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageItemInfo; @@ -65,6 +66,12 @@ public class BridgePackageManager extends PackageManager { } @Override + public PackageInfo getPackageInfoAsUser(String packageName, int flags, int userId) + throws NameNotFoundException { + return null; + } + + @Override public String[] currentToCanonicalPackageNames(String[] names) { return new String[0]; } @@ -90,7 +97,22 @@ public class BridgePackageManager extends PackageManager { } @Override - public int getPackageUid(String packageName, int userHandle) throws NameNotFoundException { + public int[] getPackageGids(String packageName, int flags) throws NameNotFoundException { + return new int[0]; + } + + @Override + public int getPackageUid(String packageName, int flags) throws NameNotFoundException { + return 0; + } + + @Override + public int getPackageUidAsUser(String packageName, int userHandle) throws NameNotFoundException { + return 0; + } + + @Override + public int getPackageUidAsUser(String packageName, int flags, int userHandle) throws NameNotFoundException { return 0; } @@ -123,6 +145,12 @@ public class BridgePackageManager extends PackageManager { } @Override + public ApplicationInfo getApplicationInfoAsUser(String packageName, int flags, int userId) + throws NameNotFoundException { + return null; + } + + @Override public ActivityInfo getActivityInfo(ComponentName component, int flags) throws NameNotFoundException { return null; @@ -157,7 +185,7 @@ public class BridgePackageManager extends PackageManager { } @Override - public List<PackageInfo> getInstalledPackages(int flags, int userId) { + public List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) { return null; } @@ -245,6 +273,36 @@ public class BridgePackageManager extends PackageManager { } @Override + public List<EphemeralApplicationInfo> getEphemeralApplications() { + return null; + } + + @Override + public Drawable getEphemeralApplicationIcon(String packageName) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] getEphemeralCookie() { + return new byte[0]; + } + + @Override + public boolean isEphemeralApplication() { + return false; + } + + @Override + public int getEphemeralCookieMaxSizeBytes() { + return 0; + } + + @Override + public boolean setEphemeralCookie(@NonNull byte[] cookie) { + return false; + } + + @Override public String[] getSystemSharedLibraryNames() { return new String[0]; } @@ -291,7 +349,7 @@ public class BridgePackageManager extends PackageManager { } @Override - public List<ResolveInfo> queryBroadcastReceivers(Intent intent, int flags, int userId) { + public List<ResolveInfo> queryBroadcastReceiversAsUser(Intent intent, int flags, int userId) { return null; } @@ -434,6 +492,11 @@ public class BridgePackageManager extends PackageManager { } @Override + public Drawable getUserBadgeForDensityNoBackground(UserHandle user, int density) { + return null; + } + + @Override public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) { return null; } @@ -484,7 +547,7 @@ public class BridgePackageManager extends PackageManager { @Override public void installPackageWithVerification(Uri packageURI, IPackageInstallObserver observer, int flags, String installerPackageName, Uri verificationURI, - ManifestDigest manifestDigest, ContainerEncryptionParams encryptionParams) { + ContainerEncryptionParams encryptionParams) { } @Override @@ -499,9 +562,14 @@ public class BridgePackageManager extends PackageManager { } @Override + public void installPackageAsUser(Uri packageURI, PackageInstallObserver observer,int flags, + String installerPackageName, int userId) { + } + + @Override public void installPackageWithVerification(Uri packageURI, PackageInstallObserver observer, int flags, String installerPackageName, Uri verificationURI, - ManifestDigest manifestDigest, ContainerEncryptionParams encryptionParams) { + ContainerEncryptionParams encryptionParams) { } @Override @@ -516,6 +584,12 @@ public class BridgePackageManager extends PackageManager { } @Override + public int installExistingPackageAsUser(String packageName, int userId) + throws NameNotFoundException { + return 0; + } + + @Override public void verifyPendingInstall(int id, int verificationCode) { } @@ -530,12 +604,12 @@ public class BridgePackageManager extends PackageManager { } @Override - public int getIntentVerificationStatus(String packageName, int userId) { + public int getIntentVerificationStatusAsUser(String packageName, int userId) { return 0; } @Override - public boolean updateIntentVerificationStatus(String packageName, int status, int userId) { + public boolean updateIntentVerificationStatusAsUser(String packageName, int status, int userId) { return false; } @@ -550,12 +624,12 @@ public class BridgePackageManager extends PackageManager { } @Override - public String getDefaultBrowserPackageName(int userId) { + public String getDefaultBrowserPackageNameAsUser(int userId) { return null; } @Override - public boolean setDefaultBrowserPackageName(String packageName, int userId) { + public boolean setDefaultBrowserPackageNameAsUser(String packageName, int userId) { return false; } @@ -568,6 +642,11 @@ public class BridgePackageManager extends PackageManager { } @Override + public void deletePackageAsUser(String packageName, IPackageDeleteObserver observer, int flags, + int userId) { + } + + @Override public String getInstallerPackageName(String packageName) { return null; } @@ -590,7 +669,7 @@ public class BridgePackageManager extends PackageManager { } @Override - public void getPackageSizeInfo(String packageName, int userHandle, + public void getPackageSizeInfoAsUser(String packageName, int userHandle, IPackageStatsObserver observer) { } @@ -695,6 +774,11 @@ public class BridgePackageManager extends PackageManager { } @Override + public boolean setPackageSuspendedAsUser(String packageName, boolean suspended, int userId) { + return false; + } + + @Override public int getMoveStatus(int moveId) { return 0; } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java index a410c53eb22b..e9b7819e9d8b 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgePowerManager.java @@ -152,6 +152,11 @@ public class BridgePowerManager implements IPowerManager { } @Override + public boolean isLightDeviceIdleMode() throws RemoteException { + return false; + } + + @Override public boolean isScreenBrightnessBoosted() throws RemoteException { return false; } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java index 771c3c85d2f5..2000fbc0fa47 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindow.java @@ -16,6 +16,8 @@ package com.android.layoutlib.bridge.android; +import com.android.internal.os.IResultReceiver; + import android.content.res.Configuration; import android.graphics.Rect; import android.os.Bundle; @@ -48,7 +50,7 @@ public final class BridgeWindow implements IWindow { @Override public void resized(Rect rect, Rect rect2, Rect rect3, Rect rect4, Rect rect5, Rect rect6, - boolean b, Configuration configuration) throws RemoteException { + boolean b, Configuration configuration, Rect rect7, boolean b2) throws RemoteException { // pass for now. } @@ -85,21 +87,22 @@ public final class BridgeWindow implements IWindow { } @Override - public void dispatchSystemUiVisibilityChanged(int seq, int globalUi, - int localValue, int localChanges) { - // pass for now. + public void updatePointerIcon(float x, float y) { + // pass for now } @Override - public void onAnimationStarted(int remainingFrameCount) { + public void dispatchSystemUiVisibilityChanged(int seq, int globalUi, + int localValue, int localChanges) { + // pass for now. } @Override - public void onAnimationStopped() { + public void dispatchWindowShown() { } @Override - public void dispatchWindowShown() { + public void requestAppKeyboardShortcuts(IResultReceiver receiver) throws RemoteException { } @Override @@ -107,4 +110,5 @@ public final class BridgeWindow implements IWindow { // pass for now. return null; } + } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java index bea1f86907e0..7b8e29a03d86 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeWindowSession.java @@ -89,12 +89,20 @@ public final class BridgeWindowSession implements IWindowSession { @Override public int relayout(IWindow iWindow, int i, LayoutParams layoutParams, int i2, int i3, int i4, int i5, Rect rect, Rect rect2, Rect rect3, Rect rect4, Rect rect5, - Rect rect6, Configuration configuration, Surface surface) throws RemoteException { + Rect rect6, Rect rect7, Configuration configuration, Surface surface) + throws RemoteException { // pass for now. return 0; } @Override + public void repositionChild(IWindow childWindow, int x, int y, int width, int height, + long deferTransactionUntilFrame, Rect outFrame) { + // pass for now. + return; + } + + @Override public void performDeferredDestroy(IWindow window) { // pass for now. } @@ -140,7 +148,7 @@ public final class BridgeWindowSession implements IWindowSession { @Override public boolean performDrag(IWindow window, IBinder dragToken, - float touchX, float touchY, float thumbCenterX, float thumbCenterY, + int touchSource, float touchX, float touchY, float thumbCenterX, float thumbCenterY, ClipData data) throws RemoteException { // pass for now @@ -148,11 +156,23 @@ public final class BridgeWindowSession implements IWindowSession { } @Override + public boolean startMovingTask(IWindow window, float startX, float startY) + throws RemoteException { + // pass for now + return false; + } + + @Override public void reportDropResult(IWindow window, boolean consumed) throws RemoteException { // pass for now } @Override + public void cancelDragAndDrop(IBinder dragToken) throws RemoteException { + // pass for now + } + + @Override public void dragRecipientEntered(IWindow window) throws RemoteException { // pass for now } @@ -211,4 +231,9 @@ public final class BridgeWindowSession implements IWindowSession { public void pokeDrawLock(IBinder window) { // pass for now. } + + @Override + public void prepareToReplaceChildren(IBinder appToken) { + // pass for now. + } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/view/WindowManagerImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/view/WindowManagerImpl.java index 7e5ae8d8c756..d417eb76b45f 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/view/WindowManagerImpl.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/view/WindowManagerImpl.java @@ -63,4 +63,9 @@ public class WindowManagerImpl implements WindowManager { public void removeViewImmediate(View arg0) { // pass } + + @Override + public void requestAppKeyboardShortcuts( + KeyboardShortcutsReceiver receiver) { + } } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java index 9c89bfe2a28f..dfbc69bee59c 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java @@ -19,9 +19,6 @@ package com.android.layoutlib.bridge.bars; import com.android.layoutlib.bridge.android.BridgeContext; import com.android.resources.Density; -import android.content.Context; -import android.content.pm.ApplicationInfo; -import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.View; import android.widget.LinearLayout; @@ -41,29 +38,18 @@ public class NavigationBar extends CustomBar { private static final int WIDTH_DEFAULT = 36; private static final int WIDTH_SW360 = 40; private static final int WIDTH_SW600 = 48; - private static final String LAYOUT_XML = "/bars/navigation_bar.xml"; + protected static final String LAYOUT_XML = "/bars/navigation_bar.xml"; private static final String LAYOUT_600DP_XML = "/bars/navigation_bar600dp.xml"; - - /** - * Constructor to be used when creating the {@link NavigationBar} as a regular control. - * This is currently used by the theme editor. - */ - @SuppressWarnings("unused") - public NavigationBar(Context context, AttributeSet attrs) { - this((BridgeContext) context, - Density.getEnum(((BridgeContext) context).getMetrics().densityDpi), - LinearLayout.HORIZONTAL, // In this mode, it doesn't need to be render vertically - ((BridgeContext) context).getConfiguration().getLayoutDirection() == - View.LAYOUT_DIRECTION_RTL, - (context.getApplicationInfo().flags & ApplicationInfo.FLAG_SUPPORTS_RTL) != 0, - 0); + public NavigationBar(BridgeContext context, Density density, int orientation, boolean isRtl, + boolean rtlEnabled, int simulatedPlatformVersion) { + this(context, density, orientation, isRtl, rtlEnabled, simulatedPlatformVersion, + getShortestWidth(context)>= 600 ? LAYOUT_600DP_XML : LAYOUT_XML); } - public NavigationBar(BridgeContext context, Density density, int orientation, boolean isRtl, - boolean rtlEnabled, int simulatedPlatformVersion) { - super(context, orientation, getShortestWidth(context)>= 600 ? LAYOUT_600DP_XML : LAYOUT_XML, - "navigation_bar.xml", simulatedPlatformVersion); + protected NavigationBar(BridgeContext context, Density density, int orientation, boolean isRtl, + boolean rtlEnabled, int simulatedPlatformVersion, String layoutPath) { + super(context, orientation, layoutPath, "navigation_bar.xml", simulatedPlatformVersion); int color = getBarColor(ATTR_COLOR, ATTR_TRANSLUCENT); setBackgroundColor(color == 0 ? 0xFF000000 : color); @@ -117,7 +103,7 @@ public class NavigationBar extends CustomBar { view.setLayoutParams(layoutParams); } - private static int getSidePadding(float sw) { + protected int getSidePadding(float sw) { if (sw >= 400) { return PADDING_WIDTH_SW400; } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/ThemePreviewNavigationBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/ThemePreviewNavigationBar.java new file mode 100644 index 000000000000..043528016c2d --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/ThemePreviewNavigationBar.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.bars; + +import com.android.layoutlib.bridge.android.BridgeContext; +import com.android.resources.Density; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.util.AttributeSet; +import android.view.View; +import android.widget.LinearLayout; + +/** + * Navigation Bar for the Theme Editor preview. + * + * For small bars, it is identical to {@link NavigationBar}. + * But wide bars from {@link NavigationBar} are too wide for the Theme Editor preview. + * To solve that problem, {@link ThemePreviewNavigationBar} use the layout for small bars, + * and have no padding on the sides. That way, they have a similar look as the true ones, + * and they fit in the Theme Editor preview. + */ +public class ThemePreviewNavigationBar extends NavigationBar { + private static final int PADDING_WIDTH_SW600 = 0; + + @SuppressWarnings("unused") + public ThemePreviewNavigationBar(Context context, AttributeSet attrs) { + super((BridgeContext) context, + Density.getEnum(((BridgeContext) context).getMetrics().densityDpi), + LinearLayout.HORIZONTAL, // In this mode, it doesn't need to be render vertically + ((BridgeContext) context).getConfiguration().getLayoutDirection() == + View.LAYOUT_DIRECTION_RTL, + (context.getApplicationInfo().flags & ApplicationInfo.FLAG_SUPPORTS_RTL) != 0, + 0, LAYOUT_XML); + } + + @Override + protected int getSidePadding(float sw) { + if (sw >= 600) { + return PADDING_WIDTH_SW600; + } + return super.getSidePadding(sw); + } +} diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java index 92b39e3f1b78..4e4fcd0aff2d 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java @@ -36,7 +36,6 @@ import android.util.DisplayMetrics; import android.view.ViewConfiguration_Accessor; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager_Accessor; -import android.widget.SimpleMonthView_Delegate; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -278,7 +277,6 @@ public abstract class RenderAction<T extends RenderParams> extends FrameworkReso mContext.getRenderResources().setLogger(null); } ParserFactory.setParserFactory(null); - SimpleMonthView_Delegate.clearCache(); } public static BridgeContext getCurrentContext() { @@ -382,23 +380,17 @@ public abstract class RenderAction<T extends RenderParams> extends FrameworkReso config.orientation = Configuration.ORIENTATION_UNDEFINED; } - try { - ScreenRound roundness = hardwareConfig.getScreenRoundness(); - if (roundness != null) { - switch (roundness) { - case ROUND: - config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_YES; - break; - case NOTROUND: - config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_NO; - } - } else { - config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_UNDEFINED; + ScreenRound roundness = hardwareConfig.getScreenRoundness(); + if (roundness != null) { + switch (roundness) { + case ROUND: + config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_YES; + break; + case NOTROUND: + config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_NO; } - } catch (NoSuchMethodError ignored) { - // getScreenRoundness was added in later stages of API 15. So, it's not present on some - // preview releases of API 15. - // TODO: Remove the try catch around Oct 2015. + } else { + config.screenLayout |= Configuration.SCREENLAYOUT_ROUND_UNDEFINED; } String locale = getParams().getLocale(); if (locale != null && !locale.isEmpty()) config.locale = new Locale(locale); diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java index 26f9000ad945..d797eecad3dd 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderDrawable.java @@ -42,10 +42,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - /** * Action to render a given Drawable provided through {@link DrawableParams#getDrawable()}. * @@ -72,8 +68,12 @@ public class RenderDrawable extends RenderAction<DrawableParams> { BridgeContext context = getContext(); drawableResource = context.getRenderResources().resolveResValue(drawableResource); - if (drawableResource == null || - drawableResource.getResourceType() != ResourceType.DRAWABLE) { + if (drawableResource == null) { + return Status.ERROR_NOT_A_DRAWABLE.createResult(); + } + + ResourceType resourceType = drawableResource.getResourceType(); + if (resourceType != ResourceType.DRAWABLE && resourceType != ResourceType.MIPMAP) { return Status.ERROR_NOT_A_DRAWABLE.createResult(); } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java index 0ffa35733180..ec50cfe55651 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java @@ -46,6 +46,7 @@ import com.android.layoutlib.bridge.android.support.DesignLibUtil; import com.android.layoutlib.bridge.impl.binding.FakeAdapter; import com.android.layoutlib.bridge.impl.binding.FakeExpandableAdapter; import com.android.resources.ResourceType; +import com.android.tools.layoutlib.java.System_Delegate; import com.android.util.Pair; import android.animation.AnimationThread; @@ -62,6 +63,7 @@ import android.graphics.Canvas; import android.preference.Preference_Delegate; import android.view.AttachInfo_Accessor; import android.view.BridgeInflater; +import android.view.Choreographer_Delegate; import android.view.IWindowManager; import android.view.IWindowManagerImpl; import android.view.Surface; @@ -120,6 +122,10 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { private int mMeasuredScreenWidth = -1; private int mMeasuredScreenHeight = -1; private boolean mIsAlphaChannelImage; + /** If >= 0, a frame will be executed */ + private long mElapsedFrameTimeNanos = -1; + /** True if one frame has been already executed to start the animations */ + private boolean mFirstFrameExecuted = false; // information being returned through the API private BufferedImage mImage; @@ -252,6 +258,14 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { } /** + * Sets the time for which the next frame will be selected. The time is the elapsed time from + * the current system nanos time. You + */ + public void setElapsedFrameTimeNanos(long nanos) { + mElapsedFrameTimeNanos = nanos; + } + + /** * Renders the scene. * <p> * {@link #acquire(long)} must have been called before this. @@ -428,6 +442,16 @@ public class RenderSessionImpl extends RenderAction<SessionParams> { gc.dispose(); } + if (mElapsedFrameTimeNanos >= 0) { + long initialTime = System_Delegate.nanoTime(); + if (!mFirstFrameExecuted) { + // The first frame will initialize the animations + Choreographer_Delegate.doFrame(initialTime); + mFirstFrameExecuted = true; + } + // Second frame will move the animations + Choreographer_Delegate.doFrame(initialTime + mElapsedFrameTimeNanos); + } mViewRoot.draw(mCanvas); } diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/AdapterHelper.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/AdapterHelper.java index 9aab340909fd..8c60bae4f11e 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/AdapterHelper.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/AdapterHelper.java @@ -114,7 +114,7 @@ public class AdapterHelper { if (value != null) { if (value.getClass() != ViewAttribute.IS_CHECKED.getAttributeClass()) { Bridge.getLog().error(LayoutLog.TAG_BROKEN, String.format( - "Wrong Adapter Item value class for TEXT. Expected Boolean, got %s", + "Wrong Adapter Item value class for IS_CHECKED. Expected Boolean, got %s", value.getClass().getName()), null); } else { cb.setChecked((Boolean) value); @@ -134,7 +134,7 @@ public class AdapterHelper { if (value != null) { if (value.getClass() != ViewAttribute.SRC.getAttributeClass()) { Bridge.getLog().error(LayoutLog.TAG_BROKEN, String.format( - "Wrong Adapter Item value class for TEXT. Expected Boolean, got %s", + "Wrong Adapter Item value class for SRC. Expected Boolean, got %s", value.getClass().getName()), null); } else { // FIXME diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/CachedPathIteratorFactory.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/CachedPathIteratorFactory.java new file mode 100644 index 000000000000..0a9b9ecde2f8 --- /dev/null +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/util/CachedPathIteratorFactory.java @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.layoutlib.bridge.util; + +import android.annotation.NonNull; + +import java.awt.geom.CubicCurve2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.geom.QuadCurve2D; +import java.util.ArrayList; + +import com.google.android.collect.Lists; + +/** + * Class that returns iterators for a given path. These iterators are lightweight and can be reused + * multiple times to iterate over the path. + */ +public class CachedPathIteratorFactory { + /* + * A few conventions used in the code: + * Coordinates or coords arrays store segment coordinates. They use the same format as + * PathIterator#currentSegment coordinates array. + * float arrays store always points where the first element is X and the second is Y. + */ + + // This governs how accurate the approximation of the Path is. + private static final float PRECISION = 0.002f; + + private final int mWindingRule; + private final int[] mTypes; + private final float[][] mCoordinates; + private final float[] mSegmentsLength; + private final float mTotalLength; + + public CachedPathIteratorFactory(@NonNull PathIterator iterator) { + mWindingRule = iterator.getWindingRule(); + + ArrayList<Integer> typesArray = Lists.newArrayList(); + ArrayList<float[]> pointsArray = Lists.newArrayList(); + float[] points = new float[6]; + while (!iterator.isDone()) { + int type = iterator.currentSegment(points); + int nPoints = getNumberOfPoints(type) * 2; // 2 coordinates per point + + typesArray.add(type); + float[] itemPoints = new float[nPoints]; + System.arraycopy(points, 0, itemPoints, 0, nPoints); + pointsArray.add(itemPoints); + iterator.next(); + } + + mTypes = new int[typesArray.size()]; + mCoordinates = new float[mTypes.length][]; + for (int i = 0; i < typesArray.size(); i++) { + mTypes[i] = typesArray.get(i); + mCoordinates[i] = pointsArray.get(i); + } + + // Do measurement + mSegmentsLength = new float[mTypes.length]; + + // Curves that we can reuse to estimate segments length + CubicCurve2D.Float cubicCurve = new CubicCurve2D.Float(); + QuadCurve2D.Float quadCurve = new QuadCurve2D.Float(); + float lastX = 0; + float lastY = 0; + float totalLength = 0; + for (int i = 0; i < mTypes.length; i++) { + switch (mTypes[i]) { + case PathIterator.SEG_CUBICTO: + cubicCurve.setCurve(lastX, lastY, + mCoordinates[i][0], mCoordinates[i][1], mCoordinates[i][2], + mCoordinates[i][3], lastX = mCoordinates[i][4], + lastY = mCoordinates[i][5]); + mSegmentsLength[i] = + getFlatPathLength(cubicCurve.getPathIterator(null, PRECISION)); + break; + case PathIterator.SEG_QUADTO: + quadCurve.setCurve(lastX, lastY, mCoordinates[i][0], mCoordinates[i][1], + lastX = mCoordinates[i][2], lastY = mCoordinates[i][3]); + mSegmentsLength[i] = + getFlatPathLength(quadCurve.getPathIterator(null, PRECISION)); + break; + case PathIterator.SEG_CLOSE: + mSegmentsLength[i] = (float) Point2D.distance(lastX, lastY, + lastX = mCoordinates[0][0], + lastY = mCoordinates[0][1]); + mCoordinates[i] = new float[2]; + // We convert a SEG_CLOSE segment to a SEG_LINETO so we do not have to worry + // about this special case in the rest of the code. + mTypes[i] = PathIterator.SEG_LINETO; + mCoordinates[i][0] = mCoordinates[0][0]; + mCoordinates[i][1] = mCoordinates[0][1]; + break; + case PathIterator.SEG_MOVETO: + mSegmentsLength[i] = 0; + lastX = mCoordinates[i][0]; + lastY = mCoordinates[i][1]; + break; + case PathIterator.SEG_LINETO: + mSegmentsLength[i] = (float) Point2D.distance(lastX, lastY, mCoordinates[i][0], + mCoordinates[i][1]); + lastX = mCoordinates[i][0]; + lastY = mCoordinates[i][1]; + default: + } + totalLength += mSegmentsLength[i]; + } + + mTotalLength = totalLength; + } + + private static void quadCurveSegment(float[] coords, float t0, float t1) { + // Calculate X and Y at 0.5 (We'll use this to reconstruct the control point later) + float mt = t0 + (t1 - t0) / 2; + float mu = 1 - mt; + float mx = mu * mu * coords[0] + 2 * mu * mt * coords[2] + mt * mt * coords[4]; + float my = mu * mu * coords[1] + 2 * mu * mt * coords[3] + mt * mt * coords[5]; + + float u0 = 1 - t0; + float u1 = 1 - t1; + + // coords at t0 + coords[0] = coords[0] * u0 * u0 + coords[2] * 2 * t0 * u0 + coords[4] * t0 * t0; + coords[1] = coords[1] * u0 * u0 + coords[3] * 2 * t0 * u0 + coords[5] * t0 * t0; + + // coords at t1 + coords[4] = coords[0] * u1 * u1 + coords[2] * 2 * t1 * u1 + coords[4] * t1 * t1; + coords[5] = coords[1] * u1 * u1 + coords[3] * 2 * t1 * u1 + coords[5] * t1 * t1; + + // estimated control point at t'=0.5 + coords[2] = 2 * mx - coords[0] / 2 - coords[4] / 2; + coords[3] = 2 * my - coords[1] / 2 - coords[5] / 2; + } + + private static void cubicCurveSegment(float[] coords, float t0, float t1) { + // http://stackoverflow.com/questions/11703283/cubic-bezier-curve-segment + float u0 = 1 - t0; + float u1 = 1 - t1; + + // Calculate the points at t0 and t1 for the quadratic curves formed by (P0, P1, P2) and + // (P1, P2, P3) + float qxa = coords[0] * u0 * u0 + coords[2] * 2 * t0 * u0 + coords[4] * t0 * t0; + float qxb = coords[0] * u1 * u1 + coords[2] * 2 * t1 * u1 + coords[4] * t1 * t1; + float qxc = coords[2] * u0 * u0 + coords[4] * 2 * t0 * u0 + coords[6] * t0 * t0; + float qxd = coords[2] * u1 * u1 + coords[4] * 2 * t1 * u1 + coords[6] * t1 * t1; + + float qya = coords[1] * u0 * u0 + coords[3] * 2 * t0 * u0 + coords[5] * t0 * t0; + float qyb = coords[1] * u1 * u1 + coords[3] * 2 * t1 * u1 + coords[5] * t1 * t1; + float qyc = coords[3] * u0 * u0 + coords[5] * 2 * t0 * u0 + coords[7] * t0 * t0; + float qyd = coords[3] * u1 * u1 + coords[5] * 2 * t1 * u1 + coords[7] * t1 * t1; + + // Linear interpolation + coords[0] = qxa * u0 + qxc * t0; + coords[1] = qya * u0 + qyc * t0; + + coords[2] = qxa * u1 + qxc * t1; + coords[3] = qya * u1 + qyc * t1; + + coords[4] = qxb * u0 + qxd * t0; + coords[5] = qyb * u0 + qyd * t0; + + coords[6] = qxb * u1 + qxd * t1; + coords[7] = qyb * u1 + qyd * t1; + } + + /** + * Returns the end point of a given segment + * + * @param type the segment type + * @param coords the segment coordinates array + * @param point the return array where the point will be stored + */ + private static void getShapeEndPoint(int type, @NonNull float[] coords, @NonNull float[] + point) { + // start index of the end point for the segment type + int pointIndex = (getNumberOfPoints(type) - 1) * 2; + point[0] = coords[pointIndex]; + point[1] = coords[pointIndex + 1]; + } + + /** + * Returns the number of points stored in a coordinates array for the given segment type. + */ + private static int getNumberOfPoints(int segmentType) { + switch (segmentType) { + case PathIterator.SEG_QUADTO: + return 2; + case PathIterator.SEG_CUBICTO: + return 3; + case PathIterator.SEG_CLOSE: + return 0; + default: + return 1; + } + } + + /** + * Returns the estimated length of a flat path. If the passed path is not flat (i.e. contains a + * segment that is not {@link PathIterator#SEG_CLOSE}, {@link PathIterator#SEG_MOVETO} or {@link + * PathIterator#SEG_LINETO} this method will fail. + */ + private static float getFlatPathLength(@NonNull PathIterator iterator) { + float segment[] = new float[6]; + float totalLength = 0; + float[] previousPoint = new float[2]; + boolean isFirstPoint = true; + + while (!iterator.isDone()) { + int type = iterator.currentSegment(segment); + assert type == PathIterator.SEG_LINETO || type == PathIterator.SEG_CLOSE || type == + PathIterator.SEG_MOVETO; + + // MoveTo shouldn't affect the length + if (!isFirstPoint && type != PathIterator.SEG_MOVETO) { + totalLength += Point2D.distance(previousPoint[0], previousPoint[1], segment[0], + segment[1]); + } else { + isFirstPoint = false; + } + previousPoint[0] = segment[0]; + previousPoint[1] = segment[1]; + iterator.next(); + } + + return totalLength; + } + + /** + * Returns the estimated position along a path of the given length. + */ + private void getPointAtLength(int type, @NonNull float[] coords, float lastX, float + lastY, float t, @NonNull float[] point) { + if (type == PathIterator.SEG_LINETO) { + point[0] = lastX + (coords[0] - lastX) * t; + point[1] = lastY + (coords[1] - lastY) * t; + // Return here, since we do not need a shape to estimate + return; + } + + float[] curve = new float[8]; + int lastPointIndex = (getNumberOfPoints(type) - 1) * 2; + + System.arraycopy(coords, 0, curve, 2, coords.length); + curve[0] = lastX; + curve[1] = lastY; + if (type == PathIterator.SEG_CUBICTO) { + cubicCurveSegment(curve, 0f, t); + } else { + quadCurveSegment(curve, 0f, t); + } + + point[0] = curve[2 + lastPointIndex]; + point[1] = curve[2 + lastPointIndex + 1]; + } + + public CachedPathIterator iterator() { + return new CachedPathIterator(); + } + + /** + * Class that allows us to iterate over a path multiple times + */ + public class CachedPathIterator implements PathIterator { + private int mNextIndex; + + /** + * Current segment type. + * + * @see PathIterator + */ + private int mCurrentType; + + /** + * Stores the coordinates array of the current segment. The number of points stored depends + * on the segment type. + * + * @see PathIterator + */ + private float[] mCurrentCoords = new float[6]; + private float mCurrentSegmentLength; + + /** + * Current segment length offset. When asking for the length of the current segment, the + * length will be reduced by this amount. This is useful when we are only using portions of + * the segment. + * + * @see #jumpToSegment(float) + */ + private float mOffsetLength; + + /** Point where the current segment started */ + private float[] mLastPoint = new float[2]; + private boolean isIteratorDone; + + private CachedPathIterator() { + next(); + } + + public float getCurrentSegmentLength() { + return mCurrentSegmentLength; + } + + @Override + public int getWindingRule() { + return mWindingRule; + } + + @Override + public boolean isDone() { + return isIteratorDone; + } + + @Override + public void next() { + if (mNextIndex >= mTypes.length) { + isIteratorDone = true; + return; + } + + if (mNextIndex >= 1) { + // We've already called next() once so there is a previous segment in this path. + // We want to get the coordinates where the path ends. + getShapeEndPoint(mCurrentType, mCurrentCoords, mLastPoint); + } else { + // This is the first segment, no previous point so initialize to 0, 0 + mLastPoint[0] = mLastPoint[1] = 0f; + } + mCurrentType = mTypes[mNextIndex]; + mCurrentSegmentLength = mSegmentsLength[mNextIndex] - mOffsetLength; + + if (mOffsetLength > 0f && (mCurrentType == SEG_CUBICTO || mCurrentType == SEG_QUADTO)) { + // We need to skip part of the start of the current segment (because + // mOffsetLength > 0) + float[] points = new float[8]; + + if (mNextIndex < 1) { + points[0] = points[1] = 0f; + } else { + getShapeEndPoint(mTypes[mNextIndex - 1], mCoordinates[mNextIndex - 1], points); + } + + System.arraycopy(mCoordinates[mNextIndex], 0, points, 2, + mCoordinates[mNextIndex].length); + float t0 = (mSegmentsLength[mNextIndex] - mCurrentSegmentLength) / + mSegmentsLength[mNextIndex]; + if (mCurrentType == SEG_CUBICTO) { + cubicCurveSegment(points, t0, 1f); + } else { + quadCurveSegment(points, t0, 1f); + } + System.arraycopy(points, 2, mCurrentCoords, 0, mCoordinates[mNextIndex].length); + } else { + System.arraycopy(mCoordinates[mNextIndex], 0, mCurrentCoords, 0, + mCoordinates[mNextIndex].length); + } + + mOffsetLength = 0f; + mNextIndex++; + } + + @Override + public int currentSegment(float[] coords) { + System.arraycopy(mCurrentCoords, 0, coords, 0, getNumberOfPoints(mCurrentType) * 2); + return mCurrentType; + } + + @Override + public int currentSegment(double[] coords) { + throw new UnsupportedOperationException(); + } + + /** + * Returns the point where the current segment ends + */ + public void getCurrentSegmentEnd(float[] point) { + point[0] = mLastPoint[0]; + point[1] = mLastPoint[1]; + } + + /** + * Restarts the iterator and jumps all the segments of this path up to the length value. + */ + public void jumpToSegment(float length) { + isIteratorDone = false; + if (length <= 0f) { + mNextIndex = 0; + return; + } + + float accLength = 0; + float lastPoint[] = new float[2]; + for (mNextIndex = 0; mNextIndex < mTypes.length; mNextIndex++) { + float segmentLength = mSegmentsLength[mNextIndex]; + if (accLength + segmentLength >= length && mTypes[mNextIndex] != SEG_MOVETO) { + float[] estimatedPoint = new float[2]; + getPointAtLength(mTypes[mNextIndex], + mCoordinates[mNextIndex], lastPoint[0], lastPoint[1], + (length - accLength) / segmentLength, + estimatedPoint); + + // This segment makes us go further than length so we go back one step, + // set a moveto and offset the length of the next segment by the length + // of this segment that we've already used. + mCurrentType = PathIterator.SEG_MOVETO; + mCurrentCoords[0] = estimatedPoint[0]; + mCurrentCoords[1] = estimatedPoint[1]; + mCurrentSegmentLength = 0; + + // We need to offset next path length to account for the segment we've just + // skipped. + mOffsetLength = length - accLength; + return; + } + accLength += segmentLength; + getShapeEndPoint(mTypes[mNextIndex], mCoordinates[mNextIndex], lastPoint); + } + } + + /** + * Returns the current segment up to certain length. If the current segment is shorter than + * length, then the whole segment is returned. The segment coordinates are copied into the + * coords array. + * + * @return the segment type + */ + public int currentSegment(@NonNull float[] coords, float length) { + int type = currentSegment(coords); + // If the length is greater than the current segment length, no need to find + // the cut point. Same if this is a SEG_MOVETO. + if (mCurrentSegmentLength <= length || type == SEG_MOVETO) { + return type; + } + + float t = length / getCurrentSegmentLength(); + + // We find at which offset the end point is located within the coords array and set + // a new end point to cut the segment short + switch (type) { + case SEG_CUBICTO: + case SEG_QUADTO: + float[] curve = new float[8]; + curve[0] = mLastPoint[0]; + curve[1] = mLastPoint[1]; + System.arraycopy(coords, 0, curve, 2, coords.length); + if (type == SEG_CUBICTO) { + cubicCurveSegment(curve, 0f, t); + } else { + quadCurveSegment(curve, 0f, t); + } + System.arraycopy(curve, 2, coords, 0, coords.length); + break; + default: + float[] point = new float[2]; + getPointAtLength(type, coords, mLastPoint[0], mLastPoint[1], t, point); + coords[0] = point[0]; + coords[1] = point[1]; + } + + return type; + } + + /** + * Returns the total length of the path + */ + public float getTotalLength() { + return mTotalLength; + } + } +} diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png Binary files differindex 2b86bfba54cc..d8ead233b4ec 100644 --- a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets.png diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets_tab.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets_tab.png Binary files differnew file mode 100644 index 000000000000..65d1dc5b1edb --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/allwidgets_tab.png diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/animated_vector.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/animated_vector.png Binary files differnew file mode 100644 index 000000000000..9f266278c352 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/animated_vector.png diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/animated_vector_1.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/animated_vector_1.png Binary files differnew file mode 100644 index 000000000000..89009be843e7 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/animated_vector_1.png diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/vector_drawable.png b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/vector_drawable.png Binary files differnew file mode 100644 index 000000000000..72b87abfb917 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/golden/vector_drawable.png diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/multi_path.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/multi_path.xml new file mode 100644 index 000000000000..ffc70dc1e519 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/drawable/multi_path.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="76dp" + android:width="76dp" + android:viewportHeight="48" + android:viewportWidth="48"> + + <group + android:name="root" + android:translateX="24.0" + android:translateY="24.0"> + <!-- + This is the same as the material indeterminate progressbar which involves drawing + several cubic segments + --> + <path + android:pathData="M0, 0 m 0, -19 a 19,19 0 1,1 0,38 a 19,19 0 1,1 0,-38" + android:strokeColor="#00FF00" + android:strokeLineCap="square" + android:strokeLineJoin="miter" + android:strokeWidth="1" + android:trimPathEnd="0.8" + android:trimPathStart="0.3" /> + <!-- Same figure with reversed end and start --> + <path + android:pathData="M0, 0 m 0, -12 a 19,19 0 1,1 0,38 a 19,19 0 1,1 0,-38" + android:strokeColor="#FFFF00" + android:strokeLineCap="square" + android:strokeLineJoin="miter" + android:strokeWidth="1" + android:trimPathEnd="0.3" + android:trimPathStart="0.8" /> + + <!-- + Draw a few partial quadratic segments + --> + <path + android:strokeWidth="2" + android:strokeColor="#FFFF00" + android:pathData="M2,2 Q 5 30 15 0" + android:trimPathStart="0.1" + android:trimPathEnd="0.9" + /> + + <!-- + Draw a line + --> + <path + android:strokeWidth="3" + android:strokeColor="#00FFFF" + android:pathData="M-10,-10 L 10, 10" + android:trimPathStart="0.2" + android:trimPathEnd="0.8" + /> + </group> + +</vector>
\ No newline at end of file diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/indeterminate_progressbar.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/indeterminate_progressbar.xml new file mode 100644 index 000000000000..70d739692e29 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/indeterminate_progressbar.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:padding="16dp" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + + <ProgressBar + android:layout_height="fill_parent" + android:layout_width="fill_parent" /> + +</LinearLayout> + diff --git a/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/vector_drawable.xml b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/vector_drawable.xml new file mode 100644 index 000000000000..2ce4f4cce919 --- /dev/null +++ b/tools/layoutlib/bridge/tests/res/testApp/MyApplication/src/main/res/layout/vector_drawable.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:padding="16dp" + android:orientation="horizontal" + android:layout_width="fill_parent" + android:layout_height="fill_parent"> + <ImageView + android:layout_height="fill_parent" + android:layout_width="fill_parent" + android:src="@drawable/multi_path" /> + +</LinearLayout> + diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java index 9ebeebd49c82..fe16a3ed8459 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/Main.java @@ -48,6 +48,8 @@ import java.io.IOException; import java.net.URL; import java.util.Arrays; import java.util.Comparator; +import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.junit.Assert.fail; @@ -303,6 +305,11 @@ public class Main { renderAndVerify("array_check.xml", "array_check.png"); } + @Test + public void testAllWidgetsTablet() throws ClassNotFoundException { + renderAndVerify("allwidgets.xml", "allwidgets_tab.png", ConfigGenerator.NEXUS_7_2012); + } + @AfterClass public static void tearDown() { sLayoutLibLog = null; @@ -348,16 +355,67 @@ public class Main { renderAndVerify(params, "expand_horz_layout.png"); } + /** Test indeterminate_progressbar.xml */ + @Test + public void testVectorAnimation() throws ClassNotFoundException { + // Create the layout pull parser. + LayoutPullParser parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + + "indeterminate_progressbar.xml"); + // Create LayoutLibCallback. + LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); + layoutLibCallback.initResources(); + + SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5, + layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, + RenderingMode.V_SCROLL, 22); + + renderAndVerify(params, "animated_vector.png", TimeUnit.SECONDS.toNanos(2)); + + parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + + "indeterminate_progressbar.xml"); + params = getSessionParams(parser, ConfigGenerator.NEXUS_5, + layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, + RenderingMode.V_SCROLL, 22); + renderAndVerify(params, "animated_vector_1.png", TimeUnit.SECONDS.toNanos(3)); + } + /** - * Create a new rendering session and test that rendering given layout on nexus 5 - * doesn't throw any exceptions and matches the provided image. + * Test a vector drawable that uses trimStart and trimEnd. It also tests all the primitives + * for vector drawables (lines, moves and cubic and quadratic curves). */ - private void renderAndVerify(SessionParams params, String goldenFileName) + @Test + public void testVectorDrawable() throws ClassNotFoundException { + // Create the layout pull parser. + LayoutPullParser parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + + "vector_drawable.xml"); + // Create LayoutLibCallback. + LayoutLibTestCallback layoutLibCallback = new LayoutLibTestCallback(getLogger()); + layoutLibCallback.initResources(); + + SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5, + layoutLibCallback, "Theme.Material.NoActionBar.Fullscreen", false, + RenderingMode.V_SCROLL, 22); + + renderAndVerify(params, "vector_drawable.png", TimeUnit.SECONDS.toNanos(2)); + } + + /** + * Create a new rendering session and test that rendering the given layout doesn't throw any + * exceptions and matches the provided image. + * <p> + * If frameTimeNanos is >= 0 a frame will be executed during the rendering. The time indicates + * how far in the future is. + */ + private void renderAndVerify(SessionParams params, String goldenFileName, long frameTimeNanos) throws ClassNotFoundException { // TODO: Set up action bar handler properly to test menu rendering. // Create session params. RenderSession session = sBridge.createSession(params); + if (frameTimeNanos != -1) { + session.setElapsedFrameTimeNanos(frameTimeNanos); + } + if (!session.getResult().isSuccess()) { getLogger().error(session.getResult().getException(), session.getResult().getErrorMessage()); @@ -377,11 +435,30 @@ public class Main { } /** - * Create a new rendering session and test that rendering given layout on nexus 5 + * Create a new rendering session and test that rendering the given layout doesn't throw any + * exceptions and matches the provided image. + */ + private void renderAndVerify(SessionParams params, String goldenFileName) + throws ClassNotFoundException { + renderAndVerify(params, goldenFileName, -1); + } + + /** + * Create a new rendering session and test that rendering the given layout on nexus 5 * doesn't throw any exceptions and matches the provided image. */ private void renderAndVerify(String layoutFileName, String goldenFileName) throws ClassNotFoundException { + renderAndVerify(layoutFileName, goldenFileName, ConfigGenerator.NEXUS_5); + } + + /** + * Create a new rendering session and test that rendering the given layout on given device + * doesn't throw any exceptions and matches the provided image. + */ + private void renderAndVerify(String layoutFileName, String goldenFileName, + ConfigGenerator deviceConfig) + throws ClassNotFoundException { // Create the layout pull parser. LayoutPullParser parser = new LayoutPullParser(APP_TEST_RES + "/layout/" + layoutFileName); // Create LayoutLibCallback. @@ -389,7 +466,7 @@ public class Main { layoutLibCallback.initResources(); // TODO: Set up action bar handler properly to test menu rendering. // Create session params. - SessionParams params = getSessionParams(parser, ConfigGenerator.NEXUS_5, + SessionParams params = getSessionParams(parser, deviceConfig, layoutLibCallback, "AppTheme", true, RenderingMode.NORMAL, 22); renderAndVerify(params, goldenFileName); } diff --git a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/ConfigGenerator.java b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/ConfigGenerator.java index 8e0cec6b550f..34fc726352cd 100644 --- a/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/ConfigGenerator.java +++ b/tools/layoutlib/bridge/tests/src/com/android/layoutlib/bridge/intensive/setup/ConfigGenerator.java @@ -126,6 +126,21 @@ public class ConfigGenerator { .setSoftButtons(true) .setNavigation(Navigation.NONAV); + public static final ConfigGenerator NEXUS_7_2012 = new ConfigGenerator() + .setScreenHeight(1280) + .setScreenWidth(800) + .setXdpi(195) + .setYdpi(200) + .setOrientation(ScreenOrientation.PORTRAIT) + .setDensity(Density.TV) + .setRatio(ScreenRatio.NOTLONG) + .setSize(ScreenSize.LARGE) + .setKeyboard(Keyboard.NOKEY) + .setTouchScreen(TouchScreen.FINGER) + .setKeyboardState(KeyboardState.SOFT) + .setSoftButtons(true) + .setNavigation(Navigation.NONAV); + private static final String TAG_ATTR = "attr"; private static final String TAG_ENUM = "enum"; private static final String TAG_FLAG = "flag"; diff --git a/tools/layoutlib/create/Android.mk b/tools/layoutlib/create/Android.mk index e6f0bc306b07..c7f2c4137687 100644 --- a/tools/layoutlib/create/Android.mk +++ b/tools/layoutlib/create/Android.mk @@ -20,7 +20,7 @@ LOCAL_SRC_FILES := $(call all-java-files-under,src) LOCAL_JAR_MANIFEST := manifest.txt LOCAL_STATIC_JAVA_LIBRARIES := \ - asm-4.0 + asm-5.0 LOCAL_MODULE := layoutlib_create diff --git a/tools/layoutlib/create/create.iml b/tools/layoutlib/create/create.iml index 9b18e73aae90..b2b14b4e8a91 100644 --- a/tools/layoutlib/create/create.iml +++ b/tools/layoutlib/create/create.iml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <module type="JAVA_MODULE" version="4"> - <component name="NewModuleRootManager" inherit-compiler-output="true"> + <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="true"> <exclude-output /> <content url="file://$MODULE_DIR$"> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> @@ -9,12 +9,12 @@ <sourceFolder url="file://$MODULE_DIR$/tests/mock_data" type="java-test-resource" /> <excludeFolder url="file://$MODULE_DIR$/.settings" /> </content> - <orderEntry type="inheritedJdk" /> + <orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" /> <orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="module-library"> - <library name="asm-4.0"> + <library name="asm-5.0"> <CLASSES> - <root url="jar://$MODULE_DIR$/../../../../../prebuilts/misc/common/asm/asm-4.0.jar!/" /> + <root url="jar://$MODULE_DIR$/../../../../../prebuilts/misc/common/asm/asm-5.0.jar!/" /> </CLASSES> <JAVADOC /> <SOURCES> diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AbstractClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AbstractClassAdapter.java index a6902a40b1a3..758bd48e6067 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AbstractClassAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AbstractClassAdapter.java @@ -21,7 +21,6 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.signature.SignatureReader; import org.objectweb.asm.signature.SignatureVisitor; @@ -44,7 +43,7 @@ public abstract class AbstractClassAdapter extends ClassVisitor { abstract String renameInternalType(String name); public AbstractClassAdapter(ClassVisitor cv) { - super(Opcodes.ASM4, cv); + super(Main.ASM_VERSION, cv); } /** @@ -239,7 +238,7 @@ public abstract class AbstractClassAdapter extends ClassVisitor { * The names must be full qualified internal ASM names (e.g. com/blah/MyClass$InnerClass). */ public RenameMethodAdapter(MethodVisitor mv) { - super(Opcodes.ASM4, mv); + super(Main.ASM_VERSION, mv); } @Override @@ -276,7 +275,8 @@ public abstract class AbstractClassAdapter extends ClassVisitor { } @Override - public void visitMethodInsn(int opcode, String owner, String name, String desc) { + public void visitMethodInsn(int opcode, String owner, String name, String desc, + boolean itf) { // The owner sometimes turns out to be a type descriptor. We try to detect it and fix. if (owner.indexOf(';') > 0) { owner = renameTypeDesc(owner); @@ -285,7 +285,7 @@ public abstract class AbstractClassAdapter extends ClassVisitor { } desc = renameMethodDesc(desc); - super.visitMethodInsn(opcode, owner, name, desc); + super.visitMethodInsn(opcode, owner, name, desc, itf); } @Override @@ -330,7 +330,7 @@ public abstract class AbstractClassAdapter extends ClassVisitor { private final SignatureVisitor mSv; public RenameSignatureAdapter(SignatureVisitor sv) { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); mSv = sv; } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java index c8b2b8448e21..48544cac16ab 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmAnalyzer.java @@ -23,7 +23,6 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.signature.SignatureReader; import org.objectweb.asm.signature.SignatureVisitor; @@ -65,7 +64,7 @@ public class AsmAnalyzer { /** Glob patterns of files to keep as is. */ private final String[] mIncludeFileGlobs; /** Internal names of classes that contain method calls that need to be rewritten. */ - private final Set<String> mReplaceMethodCallClasses = new HashSet<String>(); + private final Set<String> mReplaceMethodCallClasses = new HashSet<>(); /** * Creates a new analyzer. @@ -97,8 +96,8 @@ public class AsmAnalyzer { */ public void analyze() throws IOException, LogAbortException { - TreeMap<String, ClassReader> zipClasses = new TreeMap<String, ClassReader>(); - Map<String, InputStream> filesFound = new TreeMap<String, InputStream>(); + TreeMap<String, ClassReader> zipClasses = new TreeMap<>(); + Map<String, InputStream> filesFound = new TreeMap<>(); parseZip(mOsSourceJar, zipClasses, filesFound); mLog.info("Found %d classes in input JAR%s.", zipClasses.size(), @@ -189,7 +188,7 @@ public class AsmAnalyzer { */ Map<String, ClassReader> findIncludes(Map<String, ClassReader> zipClasses) throws LogAbortException { - TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> found = new TreeMap<>(); mLog.debug("Find classes to include."); @@ -318,10 +317,10 @@ public class AsmAnalyzer { Map<String, ClassReader> findDeps(Map<String, ClassReader> zipClasses, Map<String, ClassReader> inOutKeepClasses) { - TreeMap<String, ClassReader> deps = new TreeMap<String, ClassReader>(); - TreeMap<String, ClassReader> new_deps = new TreeMap<String, ClassReader>(); - TreeMap<String, ClassReader> new_keep = new TreeMap<String, ClassReader>(); - TreeMap<String, ClassReader> temp = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> deps = new TreeMap<>(); + TreeMap<String, ClassReader> new_deps = new TreeMap<>(); + TreeMap<String, ClassReader> new_keep = new TreeMap<>(); + TreeMap<String, ClassReader> temp = new TreeMap<>(); DependencyVisitor visitor = getVisitor(zipClasses, inOutKeepClasses, new_keep, @@ -399,7 +398,7 @@ public class AsmAnalyzer { Map<String, ClassReader> outKeep, Map<String,ClassReader> inDeps, Map<String,ClassReader> outDeps) { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); mZipClasses = zipClasses; mInKeep = inKeep; mOutKeep = outKeep; @@ -557,7 +556,7 @@ public class AsmAnalyzer { private class MyFieldVisitor extends FieldVisitor { public MyFieldVisitor() { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); } @Override @@ -630,7 +629,7 @@ public class AsmAnalyzer { private String mOwnerClass; public MyMethodVisitor(String ownerClass) { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); mOwnerClass = ownerClass; } @@ -719,7 +718,8 @@ public class AsmAnalyzer { // instruction that invokes a method @Override - public void visitMethodInsn(int opcode, String owner, String name, String desc) { + public void visitMethodInsn(int opcode, String owner, String name, String desc, + boolean itf) { // owner is the internal name of the method's owner class considerName(owner); @@ -779,7 +779,7 @@ public class AsmAnalyzer { private class MySignatureVisitor extends SignatureVisitor { public MySignatureVisitor() { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); } // --------------------------------------------------- @@ -878,7 +878,7 @@ public class AsmAnalyzer { private class MyAnnotationVisitor extends AnnotationVisitor { public MyAnnotationVisitor() { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); } // Visits a primitive value of an annotation diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java index 8f0ad01c6dc3..5b99a6bae195 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/AsmGenerator.java @@ -91,7 +91,7 @@ public class AsmGenerator { mLog = log; mOsDestJar = osDestJar; ArrayList<Class<?>> injectedClasses = - new ArrayList<Class<?>>(Arrays.asList(createInfo.getInjectedClasses())); + new ArrayList<>(Arrays.asList(createInfo.getInjectedClasses())); // Search for and add anonymous inner classes also. ListIterator<Class<?>> iter = injectedClasses.listIterator(); while (iter.hasNext()) { @@ -107,25 +107,25 @@ public class AsmGenerator { } } mInjectClasses = injectedClasses.toArray(new Class<?>[0]); - mStubMethods = new HashSet<String>(Arrays.asList(createInfo.getOverriddenMethods())); + mStubMethods = new HashSet<>(Arrays.asList(createInfo.getOverriddenMethods())); // Create the map/set of methods to change to delegates - mDelegateMethods = new HashMap<String, Set<String>>(); + mDelegateMethods = new HashMap<>(); addToMap(createInfo.getDelegateMethods(), mDelegateMethods); for (String className : createInfo.getDelegateClassNatives()) { className = binaryToInternalClassName(className); Set<String> methods = mDelegateMethods.get(className); if (methods == null) { - methods = new HashSet<String>(); + methods = new HashSet<>(); mDelegateMethods.put(className, methods); } methods.add(DelegateClassAdapter.ALL_NATIVES); } // Create the map of classes to rename. - mRenameClasses = new HashMap<String, String>(); - mClassesNotRenamed = new HashSet<String>(); + mRenameClasses = new HashMap<>(); + mClassesNotRenamed = new HashSet<>(); String[] renameClasses = createInfo.getRenamedClasses(); int n = renameClasses.length; for (int i = 0; i < n; i += 2) { @@ -138,7 +138,7 @@ public class AsmGenerator { } // Create a map of classes to be refactored. - mRefactorClasses = new HashMap<String, String>(); + mRefactorClasses = new HashMap<>(); String[] refactorClasses = createInfo.getJavaPkgClasses(); n = refactorClasses.length; for (int i = 0; i < n; i += 2) { @@ -149,7 +149,7 @@ public class AsmGenerator { } // create the map of renamed class -> return type of method to delete. - mDeleteReturns = new HashMap<String, Set<String>>(); + mDeleteReturns = new HashMap<>(); String[] deleteReturns = createInfo.getDeleteReturns(); Set<String> returnTypes = null; String renamedClass = null; @@ -172,12 +172,12 @@ public class AsmGenerator { // just a standard return type, we add it to the list. if (returnTypes == null) { - returnTypes = new HashSet<String>(); + returnTypes = new HashSet<>(); } returnTypes.add(binaryToInternalClassName(className)); } - mPromotedFields = new HashMap<String, Set<String>>(); + mPromotedFields = new HashMap<>(); addToMap(createInfo.getPromotedFields(), mPromotedFields); mInjectedMethodsMap = createInfo.getInjectedMethodsMap(); @@ -197,7 +197,7 @@ public class AsmGenerator { String methodOrFieldName = entry.substring(pos + 1); Set<String> set = map.get(className); if (set == null) { - set = new HashSet<String>(); + set = new HashSet<>(); map.put(className, set); } set.add(methodOrFieldName); @@ -247,7 +247,7 @@ public class AsmGenerator { /** Generates the final JAR */ public void generate() throws IOException { - TreeMap<String, byte[]> all = new TreeMap<String, byte[]>(); + TreeMap<String, byte[]> all = new TreeMap<>(); for (Class<?> clazz : mInjectClasses) { String name = classToEntryPath(clazz); @@ -314,7 +314,7 @@ public class AsmGenerator { * e.g. for the input "android.view.View" it returns "android/view/View.class" */ String classNameToEntryPath(String className) { - return className.replaceAll("\\.", "/").concat(".class"); + return className.replace('.', '/').concat(".class"); } /** diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java index 2c955fd9d9bb..4748a7c7b2f1 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ClassHasNativeVisitor.java @@ -31,7 +31,7 @@ import org.objectweb.asm.Opcodes; */ public class ClassHasNativeVisitor extends ClassVisitor { public ClassHasNativeVisitor() { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); } private boolean mHasNativeMethods = false; diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java index 7c838e97cd63..d106592118c8 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/CreateInfo.java @@ -111,7 +111,7 @@ public final class CreateInfo implements ICreateInfo { public Set<String> getExcludedClasses() { String[] refactoredClasses = getJavaPkgClasses(); int count = refactoredClasses.length / 2 + EXCLUDED_CLASSES.length; - Set<String> excludedClasses = new HashSet<String>(count); + Set<String> excludedClasses = new HashSet<>(count); for (int i = 0; i < refactoredClasses.length; i+=2) { excludedClasses.add(refactoredClasses[i]); } @@ -166,6 +166,7 @@ public final class CreateInfo implements ICreateInfo { "android.content.res.TypedArray#getValueAt", "android.content.res.TypedArray#obtain", "android.graphics.BitmapFactory#finishDecode", + "android.graphics.BitmapFactory#setDensityFromOptions", "android.graphics.drawable.GradientDrawable#buildRing", "android.graphics.Typeface#getSystemFontConfigLocation", "android.os.Handler#sendMessageAtTime", @@ -174,7 +175,9 @@ public final class CreateInfo implements ICreateInfo { "android.text.format.DateFormat#is24HourFormat", "android.text.Hyphenator#getSystemHyphenatorLocation", "android.util.Xml#newPullParser", + "android.view.Choreographer#getInstance", "android.view.Choreographer#getRefreshRate", + "android.view.Choreographer#scheduleVsyncLocked", "android.view.Display#updateDisplayInfoLocked", "android.view.Display#getWindowManager", "android.view.LayoutInflater#rInflate", @@ -189,9 +192,30 @@ public final class CreateInfo implements ICreateInfo { "android.view.RenderNode#nDestroyRenderNode", "android.view.RenderNode#nSetElevation", "android.view.RenderNode#nGetElevation", + "android.view.RenderNode#nSetTranslationX", + "android.view.RenderNode#nGetTranslationX", + "android.view.RenderNode#nSetTranslationY", + "android.view.RenderNode#nGetTranslationY", + "android.view.RenderNode#nSetTranslationZ", + "android.view.RenderNode#nGetTranslationZ", + "android.view.RenderNode#nSetRotation", + "android.view.RenderNode#nGetRotation", + "android.view.RenderNode#getMatrix", + "android.view.RenderNode#nSetLeft", + "android.view.RenderNode#nSetTop", + "android.view.RenderNode#nSetRight", + "android.view.RenderNode#nSetBottom", + "android.view.RenderNode#nSetLeftTopRightBottom", + "android.view.RenderNode#nSetPivotX", + "android.view.RenderNode#nGetPivotX", + "android.view.RenderNode#nSetPivotY", + "android.view.RenderNode#nGetPivotY", + "android.view.RenderNode#nSetScaleX", + "android.view.RenderNode#nGetScaleX", + "android.view.RenderNode#nSetScaleY", + "android.view.RenderNode#nGetScaleY", + "android.view.RenderNode#nIsPivotExplicitlySet", "android.view.ViewGroup#drawChild", - "android.widget.SimpleMonthView#getTitle", - "android.widget.SimpleMonthView#getDayOfWeekLabel", "android.widget.TimePickerClockDelegate#getAmOrPmKeyCode", "com.android.internal.view.menu.MenuBuilder#createNewMenuItem", "com.android.internal.util.XmlUtils#convertValueToInt", @@ -297,9 +321,7 @@ public final class CreateInfo implements ICreateInfo { }; private final static String[] PROMOTED_FIELDS = new String[] { - "android.widget.SimpleMonthView#mTitle", - "android.widget.SimpleMonthView#mCalendar", - "android.widget.SimpleMonthView#mDayOfWeekLabelCalendar" + "android.view.Choreographer#mLastFrameTimeNanos" }; /** diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java index 7ef75662aad4..cbb3a8abd692 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateClassAdapter.java @@ -60,7 +60,7 @@ public class DelegateClassAdapter extends ClassVisitor { ClassVisitor cv, String className, Set<String> delegateMethods) { - super(Opcodes.ASM4, cv); + super(Main.ASM_VERSION, cv); mLog = log; mClassName = className; mDelegateMethods = delegateMethods; diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java index cca9e574b7ea..da8babcbca83 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DelegateMethodAdapter.java @@ -124,7 +124,7 @@ class DelegateMethodAdapter extends MethodVisitor { String desc, boolean isStatic, boolean isStaticClass) { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); mLog = log; mOrgWriter = mvOriginal; mDelWriter = mvDelegate; @@ -188,7 +188,7 @@ class DelegateMethodAdapter extends MethodVisitor { mDelWriter.visitLineNumber((Integer) p[0], (Label) p[1]); } - ArrayList<Type> paramTypes = new ArrayList<Type>(); + ArrayList<Type> paramTypes = new ArrayList<>(); String delegateClassName = mClassName + DELEGATE_SUFFIX; boolean pushedArg0 = false; int maxStack = 0; @@ -253,7 +253,8 @@ class DelegateMethodAdapter extends MethodVisitor { mDelWriter.visitMethodInsn(Opcodes.INVOKESTATIC, delegateClassName, mMethodName, - desc); + desc, + false); Type returnType = Type.getReturnType(mDesc); mDelWriter.visitInsn(returnType.getOpcode(Opcodes.IRETURN)); @@ -371,9 +372,9 @@ class DelegateMethodAdapter extends MethodVisitor { } @Override - public void visitMethodInsn(int opcode, String owner, String name, String desc) { + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if (mOrgWriter != null) { - mOrgWriter.visitMethodInsn(opcode, owner, name, desc); + mOrgWriter.visitMethodInsn(opcode, owner, name, desc, itf); } } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java index 61b64a2e8e38..aa68ea099844 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/DependencyFinder.java @@ -26,7 +26,6 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.signature.SignatureReader; import org.objectweb.asm.signature.SignatureVisitor; @@ -82,7 +81,7 @@ public class DependencyFinder { Map<String, Set<String>> missing = findMissingClasses(deps, zipClasses.keySet()); - List<Map<String, Set<String>>> result = new ArrayList<Map<String,Set<String>>>(2); + List<Map<String, Set<String>>> result = new ArrayList<>(2); result.add(deps); result.add(missing); return result; @@ -151,7 +150,7 @@ public class DependencyFinder { * class name => ASM ClassReader. Class names are in the form "android.view.View". */ Map<String,ClassReader> parseZip(List<String> jarPathList) throws IOException { - TreeMap<String, ClassReader> classes = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> classes = new TreeMap<>(); for (String jarPath : jarPathList) { ZipFile zip = new ZipFile(jarPath); @@ -202,7 +201,7 @@ public class DependencyFinder { // The dependencies that we'll collect. // It's a map Class name => uses class names. - Map<String, Set<String>> dependencyMap = new TreeMap<String, Set<String>>(); + Map<String, Set<String>> dependencyMap = new TreeMap<>(); DependencyVisitor visitor = getVisitor(); @@ -211,7 +210,7 @@ public class DependencyFinder { for (Entry<String, ClassReader> entry : zipClasses.entrySet()) { String name = entry.getKey(); - TreeSet<String> set = new TreeSet<String>(); + TreeSet<String> set = new TreeSet<>(); dependencyMap.put(name, set); visitor.setDependencySet(set); @@ -240,7 +239,7 @@ public class DependencyFinder { private Map<String, Set<String>> findMissingClasses( Map<String, Set<String>> deps, Set<String> zipClasses) { - Map<String, Set<String>> missing = new TreeMap<String, Set<String>>(); + Map<String, Set<String>> missing = new TreeMap<>(); for (Entry<String, Set<String>> entry : deps.entrySet()) { String name = entry.getKey(); @@ -250,7 +249,7 @@ public class DependencyFinder { // This dependency doesn't exist in the zip classes. Set<String> set = missing.get(dep); if (set == null) { - set = new TreeSet<String>(); + set = new TreeSet<>(); missing.put(dep, set); } set.add(name); @@ -284,7 +283,7 @@ public class DependencyFinder { * Creates a new visitor that will find all the dependencies for the visited class. */ public DependencyVisitor() { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); } /** @@ -435,7 +434,7 @@ public class DependencyFinder { private class MyFieldVisitor extends FieldVisitor { public MyFieldVisitor() { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); } @Override @@ -510,7 +509,7 @@ public class DependencyFinder { private class MyMethodVisitor extends MethodVisitor { public MyMethodVisitor() { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); } @@ -598,7 +597,8 @@ public class DependencyFinder { // instruction that invokes a method @Override - public void visitMethodInsn(int opcode, String owner, String name, String desc) { + public void visitMethodInsn(int opcode, String owner, String name, String desc, + boolean itf) { // owner is the internal name of the method's owner class if (!considerDesc(owner) && owner.indexOf('/') != -1) { @@ -654,7 +654,7 @@ public class DependencyFinder { private class MySignatureVisitor extends SignatureVisitor { public MySignatureVisitor() { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); } // --------------------------------------------------- @@ -753,7 +753,7 @@ public class DependencyFinder { private class MyAnnotationVisitor extends AnnotationVisitor { public MyAnnotationVisitor() { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); } // Visits a primitive value of an annotation diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java index 37fc096acb04..1941ab4e78d8 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodRunnables.java @@ -42,9 +42,9 @@ public class InjectMethodRunnables { mv.visitCode(); mv.visitVarInsn(ALOAD, 0); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", - "()Ljava/lang/Class;"); + "()Ljava/lang/Class;", false); mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getClassLoader", - "()Ljava/lang/ClassLoader;"); + "()Ljava/lang/ClassLoader;", false); mv.visitInsn(ARETURN); mv.visitMaxs(1, 1); mv.visitEnd(); diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java index ea2b9c900ad0..c834808d950d 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/InjectMethodsAdapter.java @@ -19,7 +19,6 @@ package com.android.tools.layoutlib.create; import com.android.tools.layoutlib.create.ICreateInfo.InjectMethodRunnable; import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.Opcodes; /** * Injects methods into some classes. @@ -29,7 +28,7 @@ public class InjectMethodsAdapter extends ClassVisitor { private final ICreateInfo.InjectMethodRunnable mRunnable; public InjectMethodsAdapter(ClassVisitor cv, InjectMethodRunnable runnable) { - super(Opcodes.ASM4, cv); + super(Main.ASM_VERSION, cv); mRunnable = runnable; } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java index 383168face86..9bb91e599d77 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/Main.java @@ -16,6 +16,8 @@ package com.android.tools.layoutlib.create; +import org.objectweb.asm.Opcodes; + import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -52,13 +54,15 @@ public class Main { public boolean listOnlyMissingDeps = false; } + public static final int ASM_VERSION = Opcodes.ASM5; + public static final Options sOptions = new Options(); public static void main(String[] args) { Log log = new Log(); - ArrayList<String> osJarPath = new ArrayList<String>(); + ArrayList<String> osJarPath = new ArrayList<>(); String[] osDestJar = { null }; if (!processArgs(log, args, osJarPath, osDestJar)) { diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java index 6fc2b240b84f..faba4d727a06 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/MethodListener.java @@ -36,41 +36,40 @@ public interface MethodListener { * @param isNative True if the method was a native method. * @param caller The calling object. Null for static methods, "this" for instance methods. */ - public void onInvokeV(String signature, boolean isNative, Object caller); + void onInvokeV(String signature, boolean isNative, Object caller); /** * Same as {@link #onInvokeV(String, boolean, Object)} but returns an integer or similar. * @see #onInvokeV(String, boolean, Object) * @return an integer, or a boolean, or a short or a byte. */ - public int onInvokeI(String signature, boolean isNative, Object caller); + int onInvokeI(String signature, boolean isNative, Object caller); /** * Same as {@link #onInvokeV(String, boolean, Object)} but returns a long. * @see #onInvokeV(String, boolean, Object) * @return a long. */ - public long onInvokeL(String signature, boolean isNative, Object caller); + long onInvokeL(String signature, boolean isNative, Object caller); /** * Same as {@link #onInvokeV(String, boolean, Object)} but returns a float. * @see #onInvokeV(String, boolean, Object) * @return a float. */ - public float onInvokeF(String signature, boolean isNative, Object caller); + float onInvokeF(String signature, boolean isNative, Object caller); /** * Same as {@link #onInvokeV(String, boolean, Object)} but returns a double. * @see #onInvokeV(String, boolean, Object) * @return a double. */ - public double onInvokeD(String signature, boolean isNative, Object caller); + double onInvokeD(String signature, boolean isNative, Object caller); /** * Same as {@link #onInvokeV(String, boolean, Object)} but returns an object. * @see #onInvokeV(String, boolean, Object) * @return an object. */ - public Object onInvokeA(String signature, boolean isNative, Object caller); + Object onInvokeA(String signature, boolean isNative, Object caller); } - diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java index 4c87b3c3562d..7ccafc3867e7 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/OverrideMethod.java @@ -28,7 +28,7 @@ import java.util.HashMap; public final class OverrideMethod { /** Map of method overridden. */ - private static HashMap<String, MethodListener> sMethods = new HashMap<String, MethodListener>(); + private static HashMap<String, MethodListener> sMethods = new HashMap<>(); /** Default listener for all method not listed in sMethods. Nothing if null. */ private static MethodListener sDefaultListener = null; diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteFieldClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteFieldClassAdapter.java index e4b70da2504f..05af0337a397 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteFieldClassAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/PromoteFieldClassAdapter.java @@ -24,7 +24,6 @@ import java.util.Set; import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PROTECTED; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; -import static org.objectweb.asm.Opcodes.ASM4; /** * Promotes given fields to public visibility. @@ -35,7 +34,7 @@ public class PromoteFieldClassAdapter extends ClassVisitor { private static final int ACC_NOT_PUBLIC = ~(ACC_PRIVATE | ACC_PROTECTED); public PromoteFieldClassAdapter(ClassVisitor cv, Set<String> fieldNames) { - super(ASM4, cv); + super(Main.ASM_VERSION, cv); mFieldNames = fieldNames; } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java index 0b85c48b4e68..bf94415a4c6f 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/ReplaceMethodCallsAdapter.java @@ -43,11 +43,11 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { * Descriptors for specialized versions {@link System#arraycopy} that are not present on the * Desktop VM. */ - private static Set<String> ARRAYCOPY_DESCRIPTORS = new HashSet<String>(Arrays.asList( + private static Set<String> ARRAYCOPY_DESCRIPTORS = new HashSet<>(Arrays.asList( "([CI[CII)V", "([BI[BII)V", "([SI[SII)V", "([II[III)V", "([JI[JII)V", "([FI[FII)V", "([DI[DII)V", "([ZI[ZII)V")); - private static final List<MethodReplacer> METHOD_REPLACERS = new ArrayList<MethodReplacer>(5); + private static final List<MethodReplacer> METHOD_REPLACERS = new ArrayList<>(5); private static final String ANDROID_LOCALE_CLASS = "com/android/layoutlib/bridge/android/AndroidLocale"; @@ -134,7 +134,33 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { } }); - // Case 5: java.util.LinkedHashMap.eldest() + // Case 5: java.lang.System time calls + METHOD_REPLACERS.add(new MethodReplacer() { + @Override + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { + return JAVA_LANG_SYSTEM.equals(owner) && name.equals("nanoTime"); + } + + @Override + public void replace(MethodInformation mi) { + mi.name = "nanoTime"; + mi.owner = Type.getInternalName(System_Delegate.class); + } + }); + METHOD_REPLACERS.add(new MethodReplacer() { + @Override + public boolean isNeeded(String owner, String name, String desc, String sourceClass) { + return JAVA_LANG_SYSTEM.equals(owner) && name.equals("currentTimeMillis"); + } + + @Override + public void replace(MethodInformation mi) { + mi.name = "currentTimeMillis"; + mi.owner = Type.getInternalName(System_Delegate.class); + } + }); + + // Case 6: java.util.LinkedHashMap.eldest() METHOD_REPLACERS.add(new MethodReplacer() { private final String VOID_TO_MAP_ENTRY = @@ -157,7 +183,7 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { } }); - // Case 6: android.content.Context.getClassLoader() in LayoutInflater + // Case 7: android.content.Context.getClassLoader() in LayoutInflater METHOD_REPLACERS.add(new MethodReplacer() { // When LayoutInflater asks for a class loader, we must return the class loader that // cannot return app's custom views/classes. This is so that in case of any failure @@ -206,7 +232,7 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { private final String mOriginalClassName; public ReplaceMethodCallsAdapter(ClassVisitor cv, String originalClassName) { - super(Opcodes.ASM4, cv); + super(Main.ASM_VERSION, cv); mOriginalClassName = originalClassName; } @@ -219,11 +245,12 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { private class MyMethodVisitor extends MethodVisitor { public MyMethodVisitor(MethodVisitor mv) { - super(Opcodes.ASM4, mv); + super(Main.ASM_VERSION, mv); } @Override - public void visitMethodInsn(int opcode, String owner, String name, String desc) { + public void visitMethodInsn(int opcode, String owner, String name, String desc, + boolean itf) { for (MethodReplacer replacer : METHOD_REPLACERS) { if (replacer.isNeeded(owner, name, desc, mOriginalClassName)) { MethodInformation mi = new MethodInformation(opcode, owner, name, desc); @@ -235,7 +262,7 @@ public class ReplaceMethodCallsAdapter extends ClassVisitor { break; } } - super.visitMethodInsn(opcode, owner, name, desc); + super.visitMethodInsn(opcode, owner, name, desc, itf); } } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java index 416b73a43c11..b5ab73855189 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/StubMethodAdapter.java @@ -50,7 +50,7 @@ class StubMethodAdapter extends MethodVisitor { public StubMethodAdapter(MethodVisitor mv, String methodName, Type returnType, String invokeSignature, boolean isStatic, boolean isNative) { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); mParentVisitor = mv; mReturnType = returnType; mInvokeSignature = invokeSignature; @@ -82,7 +82,8 @@ class StubMethodAdapter extends MethodVisitor { mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "com/android/tools/layoutlib/create/OverrideMethod", "invokeV", - "(Ljava/lang/String;ZLjava/lang/Object;)V"); + "(Ljava/lang/String;ZLjava/lang/Object;)V", + false); mParentVisitor.visitInsn(Opcodes.RETURN); break; case Type.BOOLEAN: @@ -93,7 +94,8 @@ class StubMethodAdapter extends MethodVisitor { mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "com/android/tools/layoutlib/create/OverrideMethod", "invokeI", - "(Ljava/lang/String;ZLjava/lang/Object;)I"); + "(Ljava/lang/String;ZLjava/lang/Object;)I", + false); switch(sort) { case Type.BOOLEAN: Label l1 = new Label(); @@ -119,21 +121,24 @@ class StubMethodAdapter extends MethodVisitor { mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "com/android/tools/layoutlib/create/OverrideMethod", "invokeL", - "(Ljava/lang/String;ZLjava/lang/Object;)J"); + "(Ljava/lang/String;ZLjava/lang/Object;)J", + false); mParentVisitor.visitInsn(Opcodes.LRETURN); break; case Type.FLOAT: mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "com/android/tools/layoutlib/create/OverrideMethod", "invokeF", - "(Ljava/lang/String;ZLjava/lang/Object;)F"); + "(Ljava/lang/String;ZLjava/lang/Object;)F", + false); mParentVisitor.visitInsn(Opcodes.FRETURN); break; case Type.DOUBLE: mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "com/android/tools/layoutlib/create/OverrideMethod", "invokeD", - "(Ljava/lang/String;ZLjava/lang/Object;)D"); + "(Ljava/lang/String;ZLjava/lang/Object;)D", + false); mParentVisitor.visitInsn(Opcodes.DRETURN); break; case Type.ARRAY: @@ -141,7 +146,8 @@ class StubMethodAdapter extends MethodVisitor { mParentVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "com/android/tools/layoutlib/create/OverrideMethod", "invokeA", - "(Ljava/lang/String;ZLjava/lang/Object;)Ljava/lang/Object;"); + "(Ljava/lang/String;ZLjava/lang/Object;)Ljava/lang/Object;", + false); mParentVisitor.visitTypeInsn(Opcodes.CHECKCAST, mReturnType.getInternalName()); mParentVisitor.visitInsn(Opcodes.ARETURN); break; @@ -282,9 +288,9 @@ class StubMethodAdapter extends MethodVisitor { } @Override - public void visitMethodInsn(int opcode, String owner, String name, String desc) { + public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) { if (mIsInitMethod) { - mParentVisitor.visitMethodInsn(opcode, owner, name, desc); + mParentVisitor.visitMethodInsn(opcode, owner, name, desc, itf); } } diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java index d9ecf980658c..a28ae694246d 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/create/TransformClassAdapter.java @@ -49,7 +49,7 @@ class TransformClassAdapter extends ClassVisitor { public TransformClassAdapter(Log logger, Set<String> stubMethods, Set<String> deleteReturns, String className, ClassVisitor cv, boolean stubNativesOnly) { - super(Opcodes.ASM4, cv); + super(Main.ASM_VERSION, cv); mLog = logger; mStubMethods = stubMethods; mClassName = className; diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/java/AutoCloseable.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/java/AutoCloseable.java index ed2c128e1900..7d6c4ec1ad9e 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/java/AutoCloseable.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/java/AutoCloseable.java @@ -28,4 +28,5 @@ public interface AutoCloseable { /** * Closes the object and release any system resources it holds. */ - void close() throws Exception; } + void close() throws Exception; +} diff --git a/tools/layoutlib/create/src/com/android/tools/layoutlib/java/System_Delegate.java b/tools/layoutlib/create/src/com/android/tools/layoutlib/java/System_Delegate.java index 613c8d96b862..be937445c33d 100644 --- a/tools/layoutlib/create/src/com/android/tools/layoutlib/java/System_Delegate.java +++ b/tools/layoutlib/create/src/com/android/tools/layoutlib/java/System_Delegate.java @@ -18,12 +18,22 @@ package com.android.tools.layoutlib.java; import com.android.tools.layoutlib.create.ReplaceMethodCallsAdapter; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + /** * Provides dummy implementation of methods that don't exist on the host VM. + * This also providers a time control that allows to set a specific system time. * * @see ReplaceMethodCallsAdapter */ +@SuppressWarnings("unused") public class System_Delegate { + // Current system time + private static AtomicLong mNanosTime = new AtomicLong(System.nanoTime()); + // Time that the system booted up in nanos + private static AtomicLong mBootNanosTime = new AtomicLong(System.nanoTime()); + public static void log(String message) { // ignore. } @@ -31,4 +41,28 @@ public class System_Delegate { public static void log(String message, Throwable th) { // ignore. } + + public static void setNanosTime(long nanos) { + mNanosTime.set(nanos); + } + + public static void setBootTimeNanos(long nanos) { + mBootNanosTime.set(nanos); + } + + public static long nanoTime() { + return mNanosTime.get(); + } + + public static long currentTimeMillis() { + return TimeUnit.NANOSECONDS.toMillis(mNanosTime.get()); + } + + public static long bootTime() { + return mBootNanosTime.get(); + } + + public static long bootTimeMillis() { + return TimeUnit.NANOSECONDS.toMillis(mBootNanosTime.get()); + } } diff --git a/tools/layoutlib/create/tests/Android.mk b/tools/layoutlib/create/tests/Android.mk index c197d5733658..dafb9c6f9402 100644 --- a/tools/layoutlib/create/tests/Android.mk +++ b/tools/layoutlib/create/tests/Android.mk @@ -24,7 +24,7 @@ LOCAL_MODULE := layoutlib-create-tests LOCAL_MODULE_TAGS := optional LOCAL_JAVA_LIBRARIES := layoutlib_create junit -LOCAL_STATIC_JAVA_LIBRARIES := asm-4.0 +LOCAL_STATIC_JAVA_LIBRARIES := asm-5.0 include $(BUILD_HOST_JAVA_LIBRARY) diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java index 78e2c4899c1b..f86917a8139c 100644 --- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmAnalyzerTest.java @@ -17,13 +17,8 @@ package com.android.tools.layoutlib.create; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - import com.android.tools.layoutlib.create.AsmAnalyzer.DependencyVisitor; -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.objectweb.asm.ClassReader; @@ -32,11 +27,15 @@ import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; -import java.util.HashSet; +import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.TreeMap; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + /** * Unit tests for some methods of {@link AsmAnalyzer}. */ @@ -51,26 +50,22 @@ public class AsmAnalyzerTest { mLog = new MockLog(); URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar"); - mOsJarPath = new ArrayList<String>(); + mOsJarPath = new ArrayList<>(); + //noinspection ConstantConditions mOsJarPath.add(url.getFile()); - Set<String> excludeClasses = new HashSet<String>(1); - excludeClasses.add("java.lang.JavaClass"); + Set<String> excludeClasses = Collections.singleton("java.lang.JavaClass"); String[] includeFiles = new String[]{"mock_android/data/data*"}; mAa = new AsmAnalyzer(mLog, mOsJarPath, null /* gen */, null /* deriveFrom */, null /* includeGlobs */, excludeClasses, includeFiles); } - @After - public void tearDown() throws Exception { - } - @Test public void testParseZip() throws IOException { - Map<String, ClassReader> map = new TreeMap<String, ClassReader>(); - Map<String, InputStream> filesFound = new TreeMap<String, InputStream>(); + Map<String, ClassReader> map = new TreeMap<>(); + Map<String, InputStream> filesFound = new TreeMap<>(); mAa.parseZip(mOsJarPath, map, filesFound); @@ -101,11 +96,11 @@ public class AsmAnalyzerTest { @Test public void testFindClass() throws IOException, LogAbortException { - Map<String, ClassReader> zipClasses = new TreeMap<String, ClassReader>(); - Map<String, InputStream> filesFound = new TreeMap<String, InputStream>(); + Map<String, ClassReader> zipClasses = new TreeMap<>(); + Map<String, InputStream> filesFound = new TreeMap<>(); mAa.parseZip(mOsJarPath, zipClasses, filesFound); - TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> found = new TreeMap<>(); ClassReader cr = mAa.findClass("mock_android.view.ViewGroup$LayoutParams", zipClasses, found); @@ -120,11 +115,11 @@ public class AsmAnalyzerTest { @Test public void testFindGlobs() throws IOException, LogAbortException { - Map<String, ClassReader> zipClasses = new TreeMap<String, ClassReader>(); - Map<String, InputStream> filesFound = new TreeMap<String, InputStream>(); + Map<String, ClassReader> zipClasses = new TreeMap<>(); + Map<String, InputStream> filesFound = new TreeMap<>(); mAa.parseZip(mOsJarPath, zipClasses, filesFound); - TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> found = new TreeMap<>(); // this matches classes, a package match returns nothing found.clear(); @@ -183,11 +178,11 @@ public class AsmAnalyzerTest { @Test public void testFindClassesDerivingFrom() throws LogAbortException, IOException { - Map<String, ClassReader> zipClasses = new TreeMap<String, ClassReader>(); - Map<String, InputStream> filesFound = new TreeMap<String, InputStream>(); + Map<String, ClassReader> zipClasses = new TreeMap<>(); + Map<String, InputStream> filesFound = new TreeMap<>(); mAa.parseZip(mOsJarPath, zipClasses, filesFound); - TreeMap<String, ClassReader> found = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> found = new TreeMap<>(); mAa.findClassesDerivingFrom("mock_android.view.View", zipClasses, found); @@ -209,14 +204,14 @@ public class AsmAnalyzerTest { @Test public void testDependencyVisitor() throws IOException, LogAbortException { - Map<String, ClassReader> zipClasses = new TreeMap<String, ClassReader>(); - Map<String, InputStream> filesFound = new TreeMap<String, InputStream>(); + Map<String, ClassReader> zipClasses = new TreeMap<>(); + Map<String, InputStream> filesFound = new TreeMap<>(); mAa.parseZip(mOsJarPath, zipClasses, filesFound); - TreeMap<String, ClassReader> keep = new TreeMap<String, ClassReader>(); - TreeMap<String, ClassReader> new_keep = new TreeMap<String, ClassReader>(); - TreeMap<String, ClassReader> in_deps = new TreeMap<String, ClassReader>(); - TreeMap<String, ClassReader> out_deps = new TreeMap<String, ClassReader>(); + TreeMap<String, ClassReader> keep = new TreeMap<>(); + TreeMap<String, ClassReader> new_keep = new TreeMap<>(); + TreeMap<String, ClassReader> in_deps = new TreeMap<>(); + TreeMap<String, ClassReader> out_deps = new TreeMap<>(); ClassReader cr = mAa.findClass("mock_android.widget.LinearLayout", zipClasses, keep); DependencyVisitor visitor = mAa.getVisitor(zipClasses, keep, new_keep, in_deps, out_deps); diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java index 8a2235b8526c..c4dd7eeafbba 100644 --- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/AsmGeneratorTest.java @@ -18,12 +18,6 @@ package com.android.tools.layoutlib.create; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -31,7 +25,6 @@ import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import java.io.ByteArrayOutputStream; @@ -44,7 +37,6 @@ import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; -import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -52,11 +44,18 @@ import java.util.TreeMap; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + /** * Unit tests for some methods of {@link AsmGenerator}. */ public class AsmGeneratorTest { + private static final String[] EMPTY_STRING_ARRAY = new String[0]; private MockLog mLog; private ArrayList<String> mOsJarPath; private String mOsDestJar; @@ -70,7 +69,8 @@ public class AsmGeneratorTest { mLog = new MockLog(); URL url = this.getClass().getClassLoader().getResource("data/mock_android.jar"); - mOsJarPath = new ArrayList<String>(); + mOsJarPath = new ArrayList<>(); + //noinspection ConstantConditions mOsJarPath.add(url.getFile()); mTempFile = File.createTempFile("mock", ".jar"); @@ -98,18 +98,18 @@ public class AsmGeneratorTest { @Override public String[] getDelegateMethods() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getDelegateClassNatives() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getOverriddenMethods() { // methods to force override - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override @@ -123,7 +123,7 @@ public class AsmGeneratorTest { @Override public String[] getJavaPkgClasses() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override @@ -134,17 +134,17 @@ public class AsmGeneratorTest { @Override public String[] getDeleteReturns() { // methods deleted from their return type. - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getPromotedFields() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { - return new HashMap<String, InjectMethodRunnable>(0); + return Collections.emptyMap(); } }; @@ -155,7 +155,7 @@ public class AsmGeneratorTest { new String[] { // include classes "**" }, - new HashSet<String>(0) /* excluded classes */, + Collections.<String>emptySet() /* excluded classes */, new String[]{} /* include files */); aa.analyze(); agen.generate(); @@ -178,24 +178,24 @@ public class AsmGeneratorTest { @Override public String[] getDelegateMethods() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getDelegateClassNatives() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getOverriddenMethods() { // methods to force override - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getRenamedClasses() { // classes to rename (so that we can replace them) - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override @@ -214,17 +214,17 @@ public class AsmGeneratorTest { @Override public String[] getDeleteReturns() { // methods deleted from their return type. - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getPromotedFields() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { - return new HashMap<String, InjectMethodRunnable>(0); + return Collections.emptyMap(); } }; @@ -235,14 +235,14 @@ public class AsmGeneratorTest { new String[] { // include classes "**" }, - new HashSet<String>(1), + Collections.<String>emptySet(), new String[] { /* include files */ "mock_android/data/data*" }); aa.analyze(); agen.generate(); - Map<String, ClassReader> output = new TreeMap<String, ClassReader>(); - Map<String, InputStream> filesFound = new TreeMap<String, InputStream>(); + Map<String, ClassReader> output = new TreeMap<>(); + Map<String, InputStream> filesFound = new TreeMap<>(); parseZip(mOsDestJar, output, filesFound); boolean injectedClassFound = false; for (ClassReader cr: output.values()) { @@ -265,35 +265,35 @@ public class AsmGeneratorTest { @Override public String[] getDelegateMethods() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getDelegateClassNatives() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getOverriddenMethods() { // methods to force override - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getRenamedClasses() { // classes to rename (so that we can replace them) - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getJavaPkgClasses() { // classes to refactor (so that we can replace them) - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public Set<String> getExcludedClasses() { - Set<String> set = new HashSet<String>(2); + Set<String> set = new HashSet<>(2); set.add("mock_android.dummy.InnerTest"); set.add("java.lang.JavaClass"); return set; @@ -302,17 +302,17 @@ public class AsmGeneratorTest { @Override public String[] getDeleteReturns() { // methods deleted from their return type. - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getPromotedFields() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { - return new HashMap<String, InjectMethodRunnable>(0); + return Collections.emptyMap(); } }; @@ -329,8 +329,8 @@ public class AsmGeneratorTest { }); aa.analyze(); agen.generate(); - Map<String, ClassReader> output = new TreeMap<String, ClassReader>(); - Map<String, InputStream> filesFound = new TreeMap<String, InputStream>(); + Map<String, ClassReader> output = new TreeMap<>(); + Map<String, InputStream> filesFound = new TreeMap<>(); parseZip(mOsDestJar, output, filesFound); for (String s : output.keySet()) { assertFalse(excludedClasses.contains(s)); @@ -351,55 +351,52 @@ public class AsmGeneratorTest { @Override public String[] getDelegateMethods() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getDelegateClassNatives() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getOverriddenMethods() { // methods to force override - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getRenamedClasses() { // classes to rename (so that we can replace them) - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getJavaPkgClasses() { // classes to refactor (so that we can replace them) - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public Set<String> getExcludedClasses() { - return new HashSet<String>(0); + return Collections.emptySet(); } @Override public String[] getDeleteReturns() { // methods deleted from their return type. - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public String[] getPromotedFields() { - return new String[0]; + return EMPTY_STRING_ARRAY; } @Override public Map<String, InjectMethodRunnable> getInjectedMethodsMap() { - HashMap<String, InjectMethodRunnable> map = - new HashMap<String, InjectMethodRunnable>(1); - map.put("mock_android.util.EmptyArray", + return Collections.singletonMap("mock_android.util.EmptyArray", InjectMethodRunnables.CONTEXT_GET_FRAMEWORK_CLASS_LOADER); - return map; } }; @@ -415,8 +412,8 @@ public class AsmGeneratorTest { }); aa.analyze(); agen.generate(); - Map<String, ClassReader> output = new TreeMap<String, ClassReader>(); - Map<String, InputStream> filesFound = new TreeMap<String, InputStream>(); + Map<String, ClassReader> output = new TreeMap<>(); + Map<String, InputStream> filesFound = new TreeMap<>(); parseZip(mOsDestJar, output, filesFound); final String modifiedClass = "mock_android.util.EmptyArray"; final String modifiedClassPath = modifiedClass.replace('.', '/').concat(".class"); @@ -424,11 +421,8 @@ public class AsmGeneratorTest { ZipEntry entry = zipFile.getEntry(modifiedClassPath); assertNotNull(entry); final byte[] bytes; - final InputStream inputStream = zipFile.getInputStream(entry); - try { + try (InputStream inputStream = zipFile.getInputStream(entry)) { bytes = getByteArray(inputStream); - } finally { - inputStream.close(); } ClassLoader classLoader = new ClassLoader(getClass().getClassLoader()) { @Override @@ -489,7 +483,7 @@ public class AsmGeneratorTest { boolean mInjectedClassFound = false; TestClassVisitor() { - super(Opcodes.ASM4); + super(Main.ASM_VERSION); } @Override @@ -514,7 +508,7 @@ public class AsmGeneratorTest { public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); - return new MethodVisitor(Opcodes.ASM4, mv) { + return new MethodVisitor(Main.ASM_VERSION, mv) { @Override public void visitFieldInsn(int opcode, String owner, String name, @@ -540,10 +534,10 @@ public class AsmGeneratorTest { @Override public void visitMethodInsn(int opcode, String owner, String name, - String desc) { + String desc, boolean itf) { assertTrue(!getBase(owner).equals(JAVA_CLASS_NAME)); assertTrue(testType(Type.getType(desc))); - super.visitMethodInsn(opcode, owner, name, desc); + super.visitMethodInsn(opcode, owner, name, desc, itf); } }; diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java index 0135c40e71ab..0cdcdc0c1235 100644 --- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/ClassHasNativeVisitorTest.java @@ -60,7 +60,7 @@ public class ClassHasNativeVisitorTest { * Overrides {@link ClassHasNativeVisitor} to collec the name of the native methods found. */ private static class MockClassHasNativeVisitor extends ClassHasNativeVisitor { - private ArrayList<String> mMethodsFound = new ArrayList<String>(); + private ArrayList<String> mMethodsFound = new ArrayList<>(); public String[] getMethodsFound() { return mMethodsFound.toArray(new String[mMethodsFound.size()]); diff --git a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java index e37a09b348b8..0912fb1e105c 100644 --- a/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java +++ b/tools/layoutlib/create/tests/com/android/tools/layoutlib/create/DelegateClassAdapterTest.java @@ -88,7 +88,7 @@ public class DelegateClassAdapterTest { // Now process it but tell the delegate to not modify any method ClassWriter cw = new ClassWriter(0 /*flags*/); - HashSet<String> delegateMethods = new HashSet<String>(); + HashSet<String> delegateMethods = new HashSet<>(); String internalClassName = NATIVE_CLASS_NAME.replace('.', '/'); DelegateClassAdapter cv = new DelegateClassAdapter( mLog, cw, internalClassName, delegateMethods); @@ -152,7 +152,7 @@ public class DelegateClassAdapterTest { String internalClassName = NATIVE_CLASS_NAME.replace('.', '/'); - HashSet<String> delegateMethods = new HashSet<String>(); + HashSet<String> delegateMethods = new HashSet<>(); delegateMethods.add("<init>"); DelegateClassAdapter cv = new DelegateClassAdapter( mLog, cw, internalClassName, delegateMethods); @@ -166,7 +166,7 @@ public class DelegateClassAdapterTest { ClassWriter cw = new ClassWriter(0 /*flags*/); String internalClassName = NATIVE_CLASS_NAME.replace('.', '/'); - HashSet<String> delegateMethods = new HashSet<String>(); + HashSet<String> delegateMethods = new HashSet<>(); delegateMethods.add(DelegateClassAdapter.ALL_NATIVES); DelegateClassAdapter cv = new DelegateClassAdapter( mLog, cw, internalClassName, delegateMethods); @@ -217,7 +217,7 @@ public class DelegateClassAdapterTest { @Test public void testDelegateInner() throws Throwable { // We'll delegate the "get" method of both the inner and outer class. - HashSet<String> delegateMethods = new HashSet<String>(); + HashSet<String> delegateMethods = new HashSet<>(); delegateMethods.add("get"); delegateMethods.add("privateMethod"); @@ -300,7 +300,7 @@ public class DelegateClassAdapterTest { @Test public void testDelegateStaticInner() throws Throwable { // We'll delegate the "get" method of both the inner and outer class. - HashSet<String> delegateMethods = new HashSet<String>(); + HashSet<String> delegateMethods = new HashSet<>(); delegateMethods.add("get"); // Generate the delegate for the outer class. @@ -367,7 +367,7 @@ public class DelegateClassAdapterTest { */ private abstract class ClassLoader2 extends ClassLoader { - private final Map<String, byte[]> mClassDefs = new HashMap<String, byte[]>(); + private final Map<String, byte[]> mClassDefs = new HashMap<>(); public ClassLoader2() { super(null); diff --git a/tools/localedata/extract_icu_data.py b/tools/localedata/extract_icu_data.py new file mode 100755 index 000000000000..b071093a5615 --- /dev/null +++ b/tools/localedata/extract_icu_data.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +# +# Copyright 2016 The Android Open Source Project. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +"""Generate a C++ data table containing locale data.""" + +import collections +import glob +import os.path +import sys + + +def get_locale_parts(locale): + """Split a locale into three parts, for langauge, script, and region.""" + parts = locale.split('_') + if len(parts) == 1: + return (parts[0], None, None) + elif len(parts) == 2: + if len(parts[1]) == 4: # parts[1] is a script + return (parts[0], parts[1], None) + else: + return (parts[0], None, parts[1]) + else: + assert len(parts) == 3 + return tuple(parts) + + +def read_likely_subtags(input_file_name): + """Read and parse ICU's likelySubtags.txt.""" + with open(input_file_name) as input_file: + likely_script_dict = { + # Android's additions for pseudo-locales. These internal codes make + # sure that the pseudo-locales would not match other English or + # Arabic locales. (We can't use private-use ISO 15924 codes, since + # they may be used by apps for other purposes.) + "en_XA": "~~~A", + "ar_XB": "~~~B", + } + representative_locales = { + # Android's additions + "en_Latn_GB", # representative for en_Latn_001 + "es_Latn_MX", # representative for es_Latn_419 + "es_Latn_US", # representative for es_Latn_419 (not the best idea, + # but Android has been shipping with it for quite a + # while. Fortunately, MX < US, so if both exist, MX + # would be chosen.) + } + for line in input_file: + line = unicode(line, 'UTF-8').strip(u' \n\uFEFF').encode('UTF-8') + if line.startswith('//'): + continue + if '{' in line and '}' in line: + from_locale = line[:line.index('{')] + to_locale = line[line.index('"')+1:line.rindex('"')] + from_lang, from_scr, from_region = get_locale_parts(from_locale) + _, to_scr, to_region = get_locale_parts(to_locale) + if from_lang == 'und': + continue # not very useful for our purposes + if from_region is None and to_region != '001': + representative_locales.add(to_locale) + if from_scr is None: + likely_script_dict[from_locale] = to_scr + return likely_script_dict, frozenset(representative_locales) + + +# From packLanguageOrRegion() in ResourceTypes.cpp +def pack_language_or_region(inp, base): + """Pack langauge or region in a two-byte tuple.""" + if inp is None: + return (0, 0) + elif len(inp) == 2: + return ord(inp[0]), ord(inp[1]) + else: + assert len(inp) == 3 + base = ord(base) + first = ord(inp[0]) - base + second = ord(inp[1]) - base + third = ord(inp[2]) - base + + return (0x80 | (third << 2) | (second >>3), + ((second << 5) | first) & 0xFF) + + +# From packLanguage() in ResourceTypes.cpp +def pack_language(language): + """Pack language in a two-byte tuple.""" + return pack_language_or_region(language, 'a') + + +# From packRegion() in ResourceTypes.cpp +def pack_region(region): + """Pack region in a two-byte tuple.""" + return pack_language_or_region(region, '0') + + +def pack_to_uint32(locale): + """Pack language+region of locale into a 32-bit unsigned integer.""" + lang, _, region = get_locale_parts(locale) + plang = pack_language(lang) + pregion = pack_region(region) + return (plang[0] << 24) | (plang[1] << 16) | (pregion[0] << 8) | pregion[1] + + +def dump_script_codes(all_scripts): + """Dump the SCRIPT_CODES table.""" + print 'const char SCRIPT_CODES[][4] = {' + for index, script in enumerate(all_scripts): + print " /* %-2d */ {'%c', '%c', '%c', '%c'}," % ( + index, script[0], script[1], script[2], script[3]) + print '};' + print + + +def dump_script_data(likely_script_dict, all_scripts): + """Dump the script data.""" + print + print 'const std::unordered_map<uint32_t, uint8_t> LIKELY_SCRIPTS({' + for locale in sorted(likely_script_dict.keys()): + script = likely_script_dict[locale] + print ' {0x%08Xu, %2du}, // %s -> %s' % ( + pack_to_uint32(locale), + all_scripts.index(script), + locale.replace('_', '-'), + script) + print '});' + + +def pack_to_uint64(locale): + """Pack a full locale into a 64-bit unsigned integer.""" + _, script, _ = get_locale_parts(locale) + return ((pack_to_uint32(locale) << 32) | + (ord(script[0]) << 24) | + (ord(script[1]) << 16) | + (ord(script[2]) << 8) | + ord(script[3])) + + +def dump_representative_locales(representative_locales): + """Dump the set of representative locales.""" + print + print 'std::unordered_set<uint64_t> REPRESENTATIVE_LOCALES({' + for locale in sorted(representative_locales): + print ' 0x%08Xllu, // %s' % ( + pack_to_uint64(locale), + locale) + print '});' + + +def read_and_dump_likely_data(icu_data_dir): + """Read and dump the likely-script data.""" + likely_subtags_txt = os.path.join(icu_data_dir, 'misc', 'likelySubtags.txt') + likely_script_dict, representative_locales = read_likely_subtags( + likely_subtags_txt) + + all_scripts = list(set(likely_script_dict.values())) + assert len(all_scripts) <= 256 + all_scripts.sort() + + dump_script_codes(all_scripts) + dump_script_data(likely_script_dict, all_scripts) + dump_representative_locales(representative_locales) + return likely_script_dict + + +def read_parent_data(icu_data_dir): + """Read locale parent data from ICU data files.""" + all_icu_data_files = glob.glob(os.path.join(icu_data_dir, '*', '*.txt')) + parent_dict = {} + for data_file in all_icu_data_files: + locale = os.path.splitext(os.path.basename(data_file))[0] + with open(data_file) as input_file: + for line in input_file: + if '%%Parent' in line: + parent = line[line.index('"')+1:line.rindex('"')] + if locale in parent_dict: + # Different files shouldn't have different parent info + assert parent_dict[locale] == parent + else: + parent_dict[locale] = parent + elif locale.startswith('ar_') and 'default{"latn"}' in line: + # Arabic parent overrides for ASCII digits. Since + # Unicode extensions are not supported in ResourceTypes, + # we will use ar-015 (Arabic, Northern Africa) instead + # of the more correct ar-u-nu-latn. + parent_dict[locale] = 'ar_015' + return parent_dict + + +def get_likely_script(locale, likely_script_dict): + """Find the likely script for a locale, given the likely-script dictionary. + """ + if locale.count('_') == 2: + # it already has a script + return locale.split('_')[1] + elif locale in likely_script_dict: + return likely_script_dict[locale] + else: + language = locale.split('_')[0] + return likely_script_dict[language] + + +def dump_parent_data(script_organized_dict): + """Dump information for parents of locales.""" + sorted_scripts = sorted(script_organized_dict.keys()) + print + for script in sorted_scripts: + parent_dict = script_organized_dict[script] + print ('const std::unordered_map<uint32_t, uint32_t> %s_PARENTS({' + % script.upper()) + for locale in sorted(parent_dict.keys()): + parent = parent_dict[locale] + print ' {0x%08Xu, 0x%08Xu}, // %s -> %s' % ( + pack_to_uint32(locale), + pack_to_uint32(parent), + locale.replace('_', '-'), + parent.replace('_', '-')) + print '});' + print + + print 'const struct {' + print ' const char script[4];' + print ' const std::unordered_map<uint32_t, uint32_t>* map;' + print '} SCRIPT_PARENTS[] = {' + for script in sorted_scripts: + print " {{'%c', '%c', '%c', '%c'}, &%s_PARENTS}," % ( + script[0], script[1], script[2], script[3], + script.upper()) + print '};' + + +def dump_parent_tree_depth(parent_dict): + """Find and dump the depth of the parent tree.""" + max_depth = 1 + for locale, _ in parent_dict.items(): + depth = 1 + while locale in parent_dict: + locale = parent_dict[locale] + depth += 1 + max_depth = max(max_depth, depth) + assert max_depth < 5 # Our algorithms assume small max_depth + print + print 'const size_t MAX_PARENT_DEPTH = %d;' % max_depth + + +def read_and_dump_parent_data(icu_data_dir, likely_script_dict): + """Read parent data from ICU and dump it.""" + parent_dict = read_parent_data(icu_data_dir) + script_organized_dict = collections.defaultdict(dict) + for locale in parent_dict: + parent = parent_dict[locale] + if parent == 'root': + continue + script = get_likely_script(locale, likely_script_dict) + script_organized_dict[script][locale] = parent_dict[locale] + dump_parent_data(script_organized_dict) + dump_parent_tree_depth(parent_dict) + + +def main(): + """Read the data files from ICU and dump the output to a C++ file.""" + source_root = sys.argv[1] + icu_data_dir = os.path.join( + source_root, + 'external', 'icu', 'icu4c', 'source', 'data') + + print '// Auto-generated by %s' % sys.argv[0] + print + likely_script_dict = read_and_dump_likely_data(icu_data_dir) + read_and_dump_parent_data(icu_data_dir, likely_script_dict) + + +if __name__ == '__main__': + main() |