| // Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| package com.android.tools.r8.shaking; |
| |
| import com.android.tools.r8.dex.Constants; |
| import com.android.tools.r8.graph.DexAccessFlags; |
| import com.android.tools.r8.graph.DexField; |
| import com.android.tools.r8.graph.DexItemFactory; |
| import com.android.tools.r8.graph.DexString; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.logging.Log; |
| import com.android.tools.r8.shaking.ProguardConfiguration.Builder; |
| import com.android.tools.r8.shaking.ProguardTypeMatcher.ClassOrType; |
| import com.android.tools.r8.shaking.ProguardTypeMatcher.MatchSpecificType; |
| import com.android.tools.r8.utils.DescriptorUtils; |
| import com.android.tools.r8.utils.LongInterval; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.Iterables; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.nio.CharBuffer; |
| import java.nio.charset.StandardCharsets; |
| import java.nio.file.Files; |
| import java.nio.file.NoSuchFileException; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| |
| public class ProguardConfigurationParser { |
| |
| private final Builder configurationBuilder; |
| |
| private final DexItemFactory dexItemFactory; |
| |
| private static final List<String> ignoredSingleArgOptions = ImmutableList |
| .of("protomapping", |
| "optimizationpasses", |
| "target"); |
| private static final List<String> ignoredOptionalSingleArgOptions = ImmutableList |
| .of("keepdirectories"); |
| private static final List<String> ignoredFlagOptions = ImmutableList |
| .of("forceprocessing", "dontusemixedcaseclassnames", |
| "dontpreverify", "experimentalshrinkunusedprotofields", |
| "filterlibraryjarswithorginalprogramjars", |
| "dontskipnonpubliclibraryclasses", |
| "dontskipnonpubliclibraryclassmembers", |
| "invokebasemethod"); |
| private static final List<String> ignoredClassDescriptorOptions = ImmutableList |
| .of("isclassnamestring", |
| "alwaysinline", "identifiernamestring", "whyarenotsimple"); |
| |
| private static final List<String> warnedSingleArgOptions = ImmutableList |
| .of("printusage", |
| "renamesourcefileattribute", |
| "dontnote", |
| "printconfiguration", |
| // TODO -outjars (http://b/37137994) and -adaptresourcefilecontents (http://b/37139570) |
| // should be reported as errors, not just as warnings! |
| "outjars", |
| "adaptresourcefilecontents"); |
| private static final List<String> warnedFlagOptions = ImmutableList |
| .of("dontoptimize"); |
| |
| // Those options are unsupported and are treated as compilation errors. |
| // Just ignoring them would produce outputs incompatible with user expectations. |
| private static final List<String> unsupportedFlagOptions = ImmutableList |
| .of("skipnonpubliclibraryclasses"); |
| |
| public ProguardConfigurationParser(DexItemFactory dexItemFactory) { |
| this.dexItemFactory = dexItemFactory; |
| configurationBuilder = ProguardConfiguration.builder(dexItemFactory); |
| } |
| |
| public ProguardConfiguration getConfig() { |
| return configurationBuilder.build(); |
| } |
| |
| public void parse(Path path) throws ProguardRuleParserException, IOException { |
| parse(Collections.singletonList(path)); |
| } |
| |
| public void parse(List<Path> pathList) throws ProguardRuleParserException, IOException { |
| for (Path path : pathList) { |
| new ProguardFileParser(path).parse(); |
| } |
| } |
| |
| private class ProguardFileParser { |
| |
| private final Path path; |
| private final String contents; |
| private int position = 0; |
| private Path baseDirectory; |
| |
| public ProguardFileParser(Path path) throws IOException { |
| this.path = path; |
| contents = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); |
| baseDirectory = path.getParent(); |
| if (baseDirectory == null) { |
| // path parent can be null only if it's root dir or if its a one element path relative to |
| // current directory. |
| baseDirectory = Paths.get("."); |
| } |
| } |
| |
| public void parse() throws ProguardRuleParserException { |
| do { |
| skipWhitespace(); |
| } while (parseOption()); |
| } |
| |
| private boolean parseOption() throws ProguardRuleParserException { |
| if (eof()) { |
| return false; |
| } |
| if (acceptArobaseInclude()) { |
| return true; |
| } |
| expectChar('-'); |
| String option; |
| if (Iterables.any(ignoredSingleArgOptions, this::skipOptionWithSingleArg) |
| || Iterables.any(ignoredOptionalSingleArgOptions, this::skipOptionWithOptionalSingleArg) |
| || Iterables.any(ignoredFlagOptions, this::skipFlag) |
| || Iterables.any(ignoredClassDescriptorOptions, this::skipOptionWithClassSpec) |
| || parseOptimizationOption()) { |
| // Intentionally left empty. |
| } else if ( |
| (option = Iterables.find(warnedSingleArgOptions, |
| this::skipOptionWithSingleArg, null)) != null |
| || (option = Iterables.find(warnedFlagOptions, this::skipFlag, null)) != null) { |
| System.out.println("WARNING: Ignoring option: -" + option); |
| } else if ((option = Iterables.find(unsupportedFlagOptions, this::skipFlag, null)) != null) { |
| throw parseError("Unsupported option: -" + option); |
| } else if (acceptString("keepattributes")) { |
| parseKeepAttributes(); |
| } else if (acceptString("keeppackagenames")) { |
| ProguardKeepRule rule = parseKeepPackageNamesRule(); |
| configurationBuilder.addRule(rule); |
| } else if (acceptString("checkdiscard")) { |
| ProguardKeepRule rule = parseCheckDiscardRule(); |
| configurationBuilder.addRule(rule); |
| } else if (acceptString("keep")) { |
| ProguardKeepRule rule = parseKeepRule(); |
| configurationBuilder.addRule(rule); |
| } else if (acceptString("whyareyoukeeping")) { |
| ProguardKeepRule rule = parseWhyAreYouKeepingRule(); |
| configurationBuilder.addRule(rule); |
| } else if (acceptString("dontobfuscate")) { |
| configurationBuilder.setObfuscating(false); |
| } else if (acceptString("dontshrink")) { |
| configurationBuilder.setShrinking(false); |
| } else if (acceptString("verbose")) { |
| configurationBuilder.setVerbose(true); |
| } else if (acceptString("ignorewarnings")) { |
| configurationBuilder.setIgnoreWarnings(true); |
| } else if (acceptString("dontwarn")) { |
| do { |
| ProguardTypeMatcher pattern = ProguardTypeMatcher.create(parseClassName(), |
| ClassOrType.CLASS, dexItemFactory); |
| configurationBuilder.addDontWarnPattern(pattern); |
| } while (acceptChar(',')); |
| } else if (acceptString("repackageclasses")) { |
| skipWhitespace(); |
| if (acceptChar('\'')) { |
| configurationBuilder.setPackagePrefix(parsePackageNameOrEmptyString()); |
| expectChar('\''); |
| } else { |
| configurationBuilder.setPackagePrefix(""); |
| } |
| } else if (acceptString("allowaccessmodification")) { |
| configurationBuilder.setAllowAccessModification(true); |
| } else if (acceptString("printmapping")) { |
| configurationBuilder.setPrintMapping(true); |
| skipWhitespace(); |
| if (!eof() && peekChar() != '-') { |
| configurationBuilder.setPrintMappingOutput(parseFileName()); |
| } |
| } else if (acceptString("assumenosideeffects")) { |
| ProguardAssumeNoSideEffectRule rule = parseAssumeNoSideEffectsRule(); |
| configurationBuilder.addRule(rule); |
| } else if (acceptString("assumevalues")) { |
| ProguardAssumeValuesRule rule = parseAssumeValuesRule(); |
| configurationBuilder.addRule(rule); |
| } else if (acceptString("include")) { |
| skipWhitespace(); |
| parseInclude(); |
| } else if (acceptString("basedirectory")) { |
| skipWhitespace(); |
| baseDirectory = parseFileName(); |
| } else if (acceptString("injars")) { |
| configurationBuilder.addInjars(parseClassPath()); |
| } else if (acceptString("libraryjars")) { |
| configurationBuilder.addLibraryJars(parseClassPath()); |
| } else if (acceptString("printseeds")) { |
| configurationBuilder.setPrintSeed(true); |
| skipWhitespace(); |
| if (!eof() && peekChar() != '-') { |
| configurationBuilder.setSeedFile(parseFileName()); |
| } |
| } else if (acceptString("obfuscationdictionary")) { |
| configurationBuilder.setObfuscationDictionary(parseFileName()); |
| } else if (acceptString("classobfuscationdictionary")) { |
| configurationBuilder.setClassObfuscationDictionary(parseFileName()); |
| } else if (acceptString("packageobfuscationdictionary")) { |
| configurationBuilder.setPackageObfuscationDictionary(parseFileName()); |
| } else { |
| throw parseError("Unknown option"); |
| } |
| return true; |
| } |
| |
| private void parseInclude() throws ProguardRuleParserException { |
| Path included = parseFileName(); |
| try { |
| new ProguardFileParser(included).parse(); |
| } catch (FileNotFoundException | NoSuchFileException e) { |
| throw parseError("Included file '" + included.toString() + "' not found", e); |
| } catch (IOException e) { |
| throw parseError("Failed to read included file '" + included.toString() + "'", e); |
| } |
| } |
| |
| private boolean acceptArobaseInclude() throws ProguardRuleParserException { |
| if (remainingChars() < 2) { |
| return false; |
| } |
| if (!acceptChar('@')) { |
| return false; |
| } |
| parseInclude(); |
| return true; |
| } |
| |
| private void parseKeepAttributes() throws ProguardRuleParserException { |
| String attributesPattern = acceptPatternList(); |
| if (attributesPattern == null) { |
| throw parseError("Expected attribute pattern list"); |
| } |
| configurationBuilder.addAttributeRemovalPattern(attributesPattern); |
| } |
| |
| private boolean skipFlag(String name) { |
| if (acceptString(name)) { |
| if (Log.ENABLED) { |
| Log.debug(ProguardConfigurationParser.class, "Skipping '-%s` flag", name); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean skipOptionWithSingleArg(String name) { |
| if (acceptString(name)) { |
| if (Log.ENABLED) { |
| Log.debug(ProguardConfigurationParser.class, "Skipping '-%s` option", name); |
| } |
| skipSingleArgument(); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean skipOptionWithOptionalSingleArg(String name) { |
| if (acceptString(name)) { |
| if (Log.ENABLED) { |
| Log.debug(ProguardConfigurationParser.class, "Skipping '-%s` option", name); |
| } |
| skipWhitespace(); |
| if (!eof() && peekChar() != '-') { |
| skipSingleArgument(); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean skipOptionWithClassSpec(String name) { |
| if (acceptString(name)) { |
| if (Log.ENABLED) { |
| Log.debug(ProguardConfigurationParser.class, "Skipping '-%s` option", name); |
| } |
| try { |
| ProguardKeepRule.Builder keepRuleBuilder = ProguardKeepRule.builder(); |
| parseClassFlagsAndAnnotations(keepRuleBuilder); |
| keepRuleBuilder.setClassType(parseClassType()); |
| keepRuleBuilder.setClassNames(parseClassNames()); |
| parseInheritance(keepRuleBuilder); |
| parseMemberRules(keepRuleBuilder, true); |
| return true; |
| } catch (ProguardRuleParserException e) { |
| System.out.println(e); |
| return false; |
| } |
| } |
| return false; |
| |
| } |
| |
| private boolean parseOptimizationOption() { |
| if (!acceptString("optimizations")) { |
| return false; |
| } |
| skipWhitespace(); |
| do { |
| skipOptimizationName(); |
| skipWhitespace(); |
| } while (acceptChar(',')); |
| return true; |
| } |
| |
| private void skipOptimizationName() { |
| if (acceptChar('!')) { |
| skipWhitespace(); |
| } |
| for (char next = peekChar(); |
| Character.isAlphabetic(next) || next == '/' || next == '*'; |
| next = peekChar()) { |
| readChar(); |
| } |
| } |
| |
| private void skipSingleArgument() { |
| skipWhitespace(); |
| while (!eof() && !Character.isWhitespace(peekChar())) { |
| readChar(); |
| } |
| } |
| |
| private ProguardKeepRule parseKeepRule() |
| throws ProguardRuleParserException { |
| ProguardKeepRule.Builder keepRuleBuilder = ProguardKeepRule.builder(); |
| parseRuleTypeAndModifiers(keepRuleBuilder); |
| parseClassSpec(keepRuleBuilder, false); |
| return keepRuleBuilder.build(); |
| } |
| |
| private ProguardKeepRule parseWhyAreYouKeepingRule() |
| throws ProguardRuleParserException { |
| ProguardKeepRule.Builder keepRuleBuilder = ProguardKeepRule.builder(); |
| keepRuleBuilder.getModifiersBuilder().setFlagsToHaveNoEffect(); |
| keepRuleBuilder.getModifiersBuilder().whyAreYouKeeping = true; |
| keepRuleBuilder.setType(ProguardKeepRuleType.KEEP); |
| parseClassSpec(keepRuleBuilder, false); |
| return keepRuleBuilder.build(); |
| } |
| |
| private ProguardKeepRule parseKeepPackageNamesRule() |
| throws ProguardRuleParserException { |
| ProguardKeepRule.Builder keepRuleBuilder = ProguardKeepRule.builder(); |
| keepRuleBuilder.getModifiersBuilder().setFlagsToHaveNoEffect(); |
| keepRuleBuilder.getModifiersBuilder().keepPackageNames = true; |
| keepRuleBuilder.setType(ProguardKeepRuleType.KEEP); |
| keepRuleBuilder.setClassNames(parseClassNames()); |
| return keepRuleBuilder.build(); |
| } |
| |
| private ProguardKeepRule parseCheckDiscardRule() |
| throws ProguardRuleParserException { |
| ProguardKeepRule.Builder keepRuleBuilder = ProguardKeepRule.builder(); |
| keepRuleBuilder.getModifiersBuilder().setFlagsToHaveNoEffect(); |
| keepRuleBuilder.getModifiersBuilder().checkDiscarded = true; |
| parseClassSpec(keepRuleBuilder, false); |
| keepRuleBuilder.setType(keepRuleBuilder.getMemberRules().isEmpty() ? ProguardKeepRuleType.KEEP |
| : ProguardKeepRuleType.KEEP_CLASS_MEMBERS); |
| return keepRuleBuilder.build(); |
| } |
| |
| private void parseClassSpec( |
| ProguardConfigurationRule.Builder builder, boolean allowValueSpecification) |
| throws ProguardRuleParserException { |
| parseClassFlagsAndAnnotations(builder); |
| builder.setClassType(parseClassType()); |
| builder.setClassNames(parseClassNames()); |
| parseInheritance(builder); |
| parseMemberRules(builder, allowValueSpecification); |
| } |
| |
| private void parseRuleTypeAndModifiers(ProguardKeepRule.Builder builder) |
| throws ProguardRuleParserException { |
| if (acceptString("names")) { |
| builder.setType(ProguardKeepRuleType.KEEP); |
| builder.getModifiersBuilder().allowsShrinking = true; |
| } else if (acceptString("class")) { |
| if (acceptString("members")) { |
| builder.setType(ProguardKeepRuleType.KEEP_CLASS_MEMBERS); |
| } else if (acceptString("eswithmembers")) { |
| builder.setType(ProguardKeepRuleType.KEEP_CLASSES_WITH_MEMBERS); |
| } else if (acceptString("membernames")) { |
| builder.setType(ProguardKeepRuleType.KEEP_CLASS_MEMBERS); |
| builder.getModifiersBuilder().allowsShrinking = true; |
| } else if (acceptString("eswithmembernames")) { |
| builder.setType(ProguardKeepRuleType.KEEP_CLASSES_WITH_MEMBERS); |
| builder.getModifiersBuilder().allowsShrinking = true; |
| } else { |
| // The only path to here is through "-keep" followed by "class". |
| unacceptString("-keepclass"); |
| throw parseError("Unknown option"); |
| } |
| } else { |
| builder.setType(ProguardKeepRuleType.KEEP); |
| } |
| parseRuleModifiers(builder); |
| } |
| |
| private void parseRuleModifiers(ProguardKeepRule.Builder builder) { |
| while (acceptChar(',')) { |
| if (acceptString("allow")) { |
| if (acceptString("shrinking")) { |
| builder.getModifiersBuilder().allowsShrinking = true; |
| } else if (acceptString("optimization")) { |
| builder.getModifiersBuilder().allowsOptimization = true; |
| } else if (acceptString("obfuscation")) { |
| builder.getModifiersBuilder().allowsObfuscation = true; |
| } |
| } else if (acceptString("includedescriptorclasses")) { |
| builder.getModifiersBuilder().includeDescriptorClasses = true; |
| } |
| } |
| } |
| |
| private ProguardTypeMatcher parseAnnotation() throws ProguardRuleParserException { |
| skipWhitespace(); |
| int startPosition = position; |
| if (acceptChar('@')) { |
| String className = parseClassName(); |
| if (className.equals("interface")) { |
| // Not an annotation after all but a class type. Move position back to start |
| // so this can be dealt with as a class type instead. |
| position = startPosition; |
| return null; |
| } |
| return ProguardTypeMatcher.create(className, ClassOrType.CLASS, dexItemFactory); |
| } |
| return null; |
| } |
| |
| private boolean parseNegation() { |
| skipWhitespace(); |
| return acceptChar('!'); |
| } |
| |
| private void parseClassFlagsAndAnnotations(ProguardClassSpecification.Builder builder) |
| throws ProguardRuleParserException { |
| while (true) { |
| skipWhitespace(); |
| ProguardTypeMatcher annotation = parseAnnotation(); |
| if (annotation != null) { |
| // TODO(ager): Should we only allow one annotation? It looks that way from the |
| // proguard keep rule description, but that seems like a strange restriction? |
| assert builder.getClassAnnotation() == null; |
| builder.setClassAnnotation(annotation); |
| } else { |
| DexAccessFlags flags = |
| parseNegation() ? builder.getNegatedClassAccessFlags() : |
| builder.getClassAccessFlags(); |
| skipWhitespace(); |
| if (acceptString("public")) { |
| flags.setPublic(); |
| } else if (acceptString("final")) { |
| flags.setFinal(); |
| } else if (acceptString("abstract")) { |
| flags.setAbstract(); |
| } else { |
| break; |
| } |
| } |
| } |
| } |
| |
| private ProguardClassType parseClassType() throws ProguardRuleParserException { |
| skipWhitespace(); |
| if (acceptString("interface")) { |
| return ProguardClassType.INTERFACE; |
| } else if (acceptString("@interface")) { |
| return ProguardClassType.ANNOTATION_INTERFACE; |
| } else if (acceptString("class")) { |
| return ProguardClassType.CLASS; |
| } else if (acceptString("enum")) { |
| return ProguardClassType.ENUM; |
| } else { |
| throw parseError("Expected interface|class|enum"); |
| } |
| } |
| |
| private void parseInheritance(ProguardClassSpecification.Builder classSpecificationBuilder) |
| throws ProguardRuleParserException { |
| skipWhitespace(); |
| if (acceptString("implements")) { |
| classSpecificationBuilder.setInheritanceIsExtends(false); |
| } else if (acceptString("extends")) { |
| classSpecificationBuilder.setInheritanceIsExtends(true); |
| } else { |
| return; |
| } |
| classSpecificationBuilder.setInheritanceAnnotation(parseAnnotation()); |
| classSpecificationBuilder.setInheritanceClassName(ProguardTypeMatcher.create(parseClassName(), |
| ClassOrType.CLASS, dexItemFactory)); |
| } |
| |
| private void parseMemberRules(ProguardClassSpecification.Builder classSpecificationBuilder, |
| boolean allowValueSpecification) |
| throws ProguardRuleParserException { |
| skipWhitespace(); |
| if (!eof() && acceptChar('{')) { |
| ProguardMemberRule rule = null; |
| while ((rule = parseMemberRule(allowValueSpecification)) != null) { |
| classSpecificationBuilder.getMemberRules().add(rule); |
| } |
| skipWhitespace(); |
| expectChar('}'); |
| } else { |
| // If there are no member rules, a default rule for the parameterless constructor |
| // applies. So we add that here. |
| ProguardMemberRule.Builder defaultRuleBuilder = ProguardMemberRule.builder(); |
| defaultRuleBuilder.setName(Constants.INSTANCE_INITIALIZER_NAME); |
| defaultRuleBuilder.setRuleType(ProguardMemberType.INIT); |
| defaultRuleBuilder.setArguments(Collections.emptyList()); |
| classSpecificationBuilder.getMemberRules().add(defaultRuleBuilder.build()); |
| } |
| } |
| |
| private ProguardMemberRule parseMemberRule(boolean allowValueSpecification) |
| throws ProguardRuleParserException { |
| ProguardMemberRule.Builder ruleBuilder = ProguardMemberRule.builder(); |
| skipWhitespace(); |
| ruleBuilder.setAnnotation(parseAnnotation()); |
| parseMemberAccessFlags(ruleBuilder); |
| parseMemberPattern(ruleBuilder, allowValueSpecification); |
| return ruleBuilder.isValid() ? ruleBuilder.build() : null; |
| } |
| |
| private void parseMemberAccessFlags(ProguardMemberRule.Builder ruleBuilder) { |
| boolean found = true; |
| while (found && !eof()) { |
| found = false; |
| DexAccessFlags flags = |
| parseNegation() ? ruleBuilder.getNegatedAccessFlags() : ruleBuilder.getAccessFlags(); |
| skipWhitespace(); |
| switch (peekChar()) { |
| case 'a': |
| if (found = acceptString("abstract")) { |
| flags.setAbstract(); |
| } |
| break; |
| case 'f': |
| if (found = acceptString("final")) { |
| flags.setFinal(); |
| } |
| break; |
| case 'n': |
| if (found = acceptString("native")) { |
| flags.setNative(); |
| } |
| break; |
| case 'p': |
| if (found = acceptString("public")) { |
| flags.setPublic(); |
| } else if (found = acceptString("private")) { |
| flags.setPrivate(); |
| } else if (found = acceptString("protected")) { |
| flags.setProtected(); |
| } |
| break; |
| case 's': |
| if (found = acceptString("synchronized")) { |
| flags.setSynchronized(); |
| } else if (found = acceptString("static")) { |
| flags.setStatic(); |
| } else if (found = acceptString("strictfp")) { |
| flags.setStrict(); |
| } |
| break; |
| case 't': |
| if (found = acceptString("transient")) { |
| flags.setTransient(); |
| } |
| break; |
| case 'v': |
| if (found = acceptString("volatile")) { |
| flags.setVolatile(); |
| } |
| break; |
| } |
| } |
| } |
| |
| private void parseMemberPattern( |
| ProguardMemberRule.Builder ruleBuilder, boolean allowValueSpecification) |
| throws ProguardRuleParserException { |
| skipWhitespace(); |
| if (acceptString("<methods>")) { |
| ruleBuilder.setRuleType(ProguardMemberType.ALL_METHODS); |
| } else if (acceptString("<fields>")) { |
| ruleBuilder.setRuleType(ProguardMemberType.ALL_FIELDS); |
| } else if (acceptString("<init>")) { |
| ruleBuilder.setRuleType(ProguardMemberType.INIT); |
| ruleBuilder.setName("<init>"); |
| ruleBuilder.setArguments(parseArgumentList()); |
| } else { |
| String first = acceptClassName(); |
| if (first != null) { |
| skipWhitespace(); |
| if (first.equals("*") && hasNextChar(';')) { |
| ruleBuilder.setRuleType(ProguardMemberType.ALL); |
| } else { |
| if (hasNextChar('(')) { |
| ruleBuilder.setRuleType(ProguardMemberType.CONSTRUCTOR); |
| ruleBuilder.setName(first); |
| ruleBuilder.setArguments(parseArgumentList()); |
| } else { |
| String second = acceptClassName(); |
| if (second != null) { |
| skipWhitespace(); |
| if (hasNextChar('(')) { |
| ruleBuilder.setRuleType(ProguardMemberType.METHOD); |
| ruleBuilder.setName(second); |
| ruleBuilder |
| .setTypeMatcher( |
| ProguardTypeMatcher.create(first, ClassOrType.TYPE, dexItemFactory)); |
| ruleBuilder.setArguments(parseArgumentList()); |
| } else { |
| ruleBuilder.setRuleType(ProguardMemberType.FIELD); |
| ruleBuilder.setName(second); |
| ruleBuilder |
| .setTypeMatcher( |
| ProguardTypeMatcher.create(first, ClassOrType.TYPE, dexItemFactory)); |
| } |
| skipWhitespace(); |
| // Parse "return ..." if present. |
| if (acceptString("return")) { |
| skipWhitespace(); |
| if (acceptString("true")) { |
| ruleBuilder.setReturnValue(new ProguardMemberRuleReturnValue(true)); |
| } else if (acceptString("false")) { |
| ruleBuilder.setReturnValue(new ProguardMemberRuleReturnValue(false)); |
| } else { |
| String qualifiedFieldName = acceptFieldName(); |
| if (qualifiedFieldName != null) { |
| if (ruleBuilder.getTypeMatcher() instanceof MatchSpecificType) { |
| int lastDotIndex = qualifiedFieldName.lastIndexOf("."); |
| DexType fieldType = ((MatchSpecificType) ruleBuilder.getTypeMatcher()).type; |
| DexType fieldClass = |
| dexItemFactory.createType( |
| DescriptorUtils.javaTypeToDescriptor( |
| qualifiedFieldName.substring(0, lastDotIndex))); |
| DexString fieldName = |
| dexItemFactory.createString( |
| qualifiedFieldName.substring(lastDotIndex + 1)); |
| DexField field = dexItemFactory |
| .createField(fieldClass, fieldType, fieldName); |
| ruleBuilder.setReturnValue(new ProguardMemberRuleReturnValue(field)); |
| } else { |
| throw parseError("Expected specific type"); |
| } |
| } else { |
| Integer min = acceptInteger(); |
| Integer max = min; |
| if (min == null) { |
| throw parseError("Expected integer value"); |
| } |
| skipWhitespace(); |
| if (acceptString("..")) { |
| max = acceptInteger(); |
| if (max == null) { |
| throw parseError("Expected integer value"); |
| } |
| } |
| if (!allowValueSpecification) { |
| throw parseError("Unexpected value specification"); |
| } |
| ruleBuilder.setReturnValue( |
| new ProguardMemberRuleReturnValue(new LongInterval(min, max))); |
| } |
| } |
| } |
| } else { |
| throw parseError("Expected field or method name"); |
| } |
| } |
| } |
| } |
| } |
| // If we found a member pattern eat the terminating ';'. |
| if (ruleBuilder.isValid()) { |
| skipWhitespace(); |
| expectChar(';'); |
| } |
| } |
| |
| private List<ProguardTypeMatcher> parseArgumentList() throws ProguardRuleParserException { |
| List<ProguardTypeMatcher> arguments = new ArrayList<>(); |
| skipWhitespace(); |
| expectChar('('); |
| skipWhitespace(); |
| if (acceptChar(')')) { |
| return arguments; |
| } |
| if (acceptString("...")) { |
| arguments |
| .add(ProguardTypeMatcher.create("...", ClassOrType.TYPE, dexItemFactory)); |
| } else { |
| for (String name = parseClassName(); name != null; name = |
| acceptChar(',') ? parseClassName() : null) { |
| arguments |
| .add(ProguardTypeMatcher.create(name, ClassOrType.TYPE, dexItemFactory)); |
| skipWhitespace(); |
| } |
| } |
| skipWhitespace(); |
| expectChar(')'); |
| return arguments; |
| } |
| |
| private Path parseFileName() throws ProguardRuleParserException { |
| skipWhitespace(); |
| int start = position; |
| int end = position; |
| while (!eof(end)) { |
| char current = contents.charAt(end); |
| if (current != File.pathSeparatorChar && !Character.isWhitespace(current)) { |
| end++; |
| } else { |
| break; |
| } |
| } |
| if (start == end) { |
| throw parseError("File name expected"); |
| } |
| position = end; |
| return baseDirectory.resolve(contents.substring(start, end)); |
| } |
| |
| private List<Path> parseClassPath() throws ProguardRuleParserException { |
| List<Path> classPath = new ArrayList<>(); |
| skipWhitespace(); |
| Path file = parseFileName(); |
| classPath.add(file); |
| while (acceptChar(File.pathSeparatorChar)) { |
| file = parseFileName(); |
| classPath.add(file); |
| } |
| return classPath; |
| } |
| |
| private ProguardAssumeNoSideEffectRule parseAssumeNoSideEffectsRule() |
| throws ProguardRuleParserException { |
| ProguardAssumeNoSideEffectRule.Builder builder = ProguardAssumeNoSideEffectRule.builder(); |
| parseClassSpec(builder, true); |
| return builder.build(); |
| } |
| |
| private ProguardAssumeValuesRule parseAssumeValuesRule() throws ProguardRuleParserException { |
| ProguardAssumeValuesRule.Builder builder = ProguardAssumeValuesRule.builder(); |
| parseClassSpec(builder, true); |
| return builder.build(); |
| } |
| |
| private void skipWhitespace() { |
| while (!eof() && Character.isWhitespace(contents.charAt(position))) { |
| position++; |
| } |
| skipComment(); |
| } |
| |
| private void skipComment() { |
| if (eof()) { |
| return; |
| } |
| if (peekChar() == '#') { |
| while (!eof() && readChar() != '\n') { |
| ; |
| } |
| skipWhitespace(); |
| } |
| } |
| |
| private boolean eof() { |
| return position == contents.length(); |
| } |
| |
| private boolean eof(int position) { |
| return position == contents.length(); |
| } |
| |
| private boolean hasNextChar(char c) { |
| if (eof()) { |
| return false; |
| } |
| return peekChar() == c; |
| } |
| |
| private boolean acceptChar(char c) { |
| if (hasNextChar(c)) { |
| position++; |
| return true; |
| } |
| return false; |
| } |
| |
| private char peekChar() { |
| return contents.charAt(position); |
| } |
| |
| private char readChar() { |
| return contents.charAt(position++); |
| } |
| |
| private int remainingChars() { |
| return contents.length() - position; |
| } |
| |
| private void expectChar(char c) throws ProguardRuleParserException { |
| if (eof() || readChar() != c) { |
| throw parseError("Expected char '" + c + "'"); |
| } |
| } |
| |
| private void expectString(String expected) throws ProguardRuleParserException { |
| if (remainingChars() < expected.length()) { |
| throw parseError("Expected string '" + expected + "'"); |
| } |
| for (int i = 0; i < expected.length(); i++) { |
| if (expected.charAt(i) != readChar()) { |
| throw parseError("Expected string '" + expected + "'"); |
| } |
| } |
| } |
| |
| private boolean acceptString(String expected) { |
| if (remainingChars() < expected.length()) { |
| return false; |
| } |
| for (int i = 0; i < expected.length(); i++) { |
| if (expected.charAt(i) != contents.charAt(position + i)) { |
| return false; |
| } |
| } |
| position += expected.length(); |
| return true; |
| } |
| |
| private Integer acceptInteger() { |
| skipWhitespace(); |
| int start = position; |
| int end = position; |
| while (!eof(end)) { |
| char current = contents.charAt(end); |
| if (Character.isDigit(current)) { |
| end++; |
| } else { |
| break; |
| } |
| } |
| if (start == end) { |
| return null; |
| } |
| position = end; |
| return Integer.parseInt(contents.substring(start, end)); |
| } |
| |
| private String acceptClassName() { |
| skipWhitespace(); |
| int start = position; |
| int end = position; |
| while (!eof(end)) { |
| char current = contents.charAt(end); |
| if (Character.isJavaIdentifierPart(current) || |
| current == '.' || |
| current == '*' || |
| current == '?' || |
| current == '%' || |
| current == '[' || |
| current == ']') { |
| end++; |
| } else { |
| break; |
| } |
| } |
| if (start == end) { |
| return null; |
| } |
| position = end; |
| return contents.substring(start, end); |
| } |
| |
| private String acceptFieldName() { |
| skipWhitespace(); |
| int start = position; |
| int end = position; |
| while (!eof(end)) { |
| char current = contents.charAt(end); |
| if ((start == end && Character.isJavaIdentifierStart(current)) || |
| (start < end) && (Character.isJavaIdentifierPart(current) || current == '.')) { |
| end++; |
| } else { |
| break; |
| } |
| } |
| if (start == end) { |
| return null; |
| } |
| position = end; |
| return contents.substring(start, end); |
| } |
| |
| private String acceptPatternList() { |
| skipWhitespace(); |
| int start = position; |
| int end = position; |
| while (!eof(end)) { |
| char current = contents.charAt(end); |
| if (Character.isJavaIdentifierPart(current) || |
| current == '!' || |
| current == '*' || |
| current == ',') { |
| end++; |
| } else { |
| break; |
| } |
| } |
| if (start == end) { |
| return null; |
| } |
| position = end; |
| return contents.substring(start, end); |
| } |
| |
| private void unacceptString(String expected) { |
| assert position >= expected.length(); |
| position -= expected.length(); |
| for (int i = 0; i < expected.length(); i++) { |
| assert expected.charAt(i) == contents.charAt(position + i); |
| } |
| } |
| |
| |
| private void checkNotNegatedPattern() throws ProguardRuleParserException { |
| skipWhitespace(); |
| if (acceptChar('!')) { |
| throw parseError("Negated filters are not supported"); |
| } |
| } |
| |
| private List<ProguardTypeMatcher> parseClassNames() throws ProguardRuleParserException { |
| List<ProguardTypeMatcher> classNames = new ArrayList<>(); |
| checkNotNegatedPattern(); |
| classNames |
| .add(ProguardTypeMatcher.create(parseClassName(), ClassOrType.CLASS, dexItemFactory)); |
| skipWhitespace(); |
| while (acceptChar(',')) { |
| checkNotNegatedPattern(); |
| classNames |
| .add(ProguardTypeMatcher.create(parseClassName(), ClassOrType.CLASS, dexItemFactory)); |
| skipWhitespace(); |
| } |
| return classNames; |
| } |
| |
| private String parsePackageNameOrEmptyString() { |
| String name = acceptClassName(); |
| return name == null ? "" : name; |
| } |
| |
| private String parseClassName() throws ProguardRuleParserException { |
| String name = acceptClassName(); |
| if (name == null) { |
| throw parseError("Class name expected"); |
| } |
| return name; |
| } |
| |
| private String snippetForPosition() { |
| // TODO(ager): really should deal with \r as well to get column right. |
| String[] lines = contents.split("\n", -1); // -1 to get trailing empty lines represented. |
| int remaining = position; |
| for (int lineNumber = 0; lineNumber < lines.length; lineNumber++) { |
| String line = lines[lineNumber]; |
| if (remaining <= line.length() || lineNumber == lines.length - 1) { |
| String arrow = CharBuffer.allocate(remaining).toString().replace( '\0', ' ' ) + '^'; |
| return path.toString() + ":" + (lineNumber + 1) + ":" + remaining + "\n" + line |
| + '\n' + arrow; |
| } |
| remaining -= (line.length() + 1); // Include newline. |
| } |
| return path.toString(); |
| } |
| |
| private ProguardRuleParserException parseError(String message) { |
| return new ProguardRuleParserException(message, snippetForPosition()); |
| } |
| |
| private ProguardRuleParserException parseError(String message, Throwable cause) { |
| return new ProguardRuleParserException(message, snippetForPosition(), cause); |
| } |
| } |
| } |