libredex/ProguardParser.cpp (1,121 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include <fstream> #include <iostream> #include <vector> #include "Debug.h" #include "Macros.h" #include "ProguardLexer.h" #include "ProguardMap.h" #include "ProguardParser.h" #include "ProguardRegex.h" #include "ReadMaybeMapped.h" namespace keep_rules { namespace proguard_parser { namespace { struct TokenIndex { const std::vector<Token>& vec; std::vector<Token>::const_iterator it; TokenIndex(const std::vector<Token>& vec, std::vector<Token>::const_iterator it) : vec(vec), it(it) {} void skip_comments() { while (it != vec.end() && it->type == TokenType::comment) { ++it; } } void next() { redex_assert(it != vec.end()); ++it; skip_comments(); } std::string_view data() const { return it->data; } std::string str() const { return std::string{it->data}; } std::string show() const { return it->show(); } size_t line() const { return it->line; } TokenType type() const { return it->type; } std::string show_context(size_t lines) const { redex_assert(it != vec.end()); size_t this_line = line(); auto start_it = it; while (start_it != vec.begin() && start_it->line >= this_line - lines) { --start_it; } if (start_it->line < this_line - lines) { ++start_it; } auto end_it = it; while (end_it != vec.end() && end_it->line <= this_line + lines) { ++end_it; } std::string ret; std::optional<size_t> last_line = std::nullopt; bool new_line = true; for (auto show_it = start_it; show_it != end_it; ++show_it) { if (!last_line || last_line != show_it->line) { if (last_line) { ret.append("\n"); } ret.append(std::to_string(show_it->line)); ret.append(": "); last_line = show_it->line; new_line = true; } if (!new_line) { ret.append(" "); } if (show_it == it) { ret.append("!>"); } ret.append(show_it->show()); if (show_it == it) { ret.append("<!"); } new_line = false; } return ret; } }; bool parse_boolean_command(TokenIndex& idx, TokenType boolean_option, bool* option, bool value) { if (idx.type() != boolean_option) { return false; } idx.next(); *option = value; return true; } void skip_to_next_command(TokenIndex& idx) { while ((idx.type() != TokenType::eof_token) && (!idx.it->is_command())) { idx.next(); } } bool parse_single_filepath_command(TokenIndex& idx, TokenType filepath_command_token, std::string* filepath) { if (idx.type() == filepath_command_token) { unsigned int line_number = idx.line(); idx.next(); // Consume the command token. // Fail without consumption if this is an end of file token. if (idx.type() == TokenType::eof_token) { std::cerr << "Expecting at least one file as an argument but found end of " "file at line " << line_number << std::endl << idx.show_context(2) << std::endl; return true; } // Fail without consumption if this is a command token. if (idx.it->is_command()) { std::cerr << "Expecting a file path argument but got command " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; return true; } // Parse the filename. if (idx.type() != TokenType::filepath) { std::cerr << "Expected a filepath but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; return true; } *filepath = idx.str(); idx.next(); // Consume the filepath token return true; } return false; } template <bool kOptional = false> void parse_filepaths(TokenIndex& idx, std::vector<std::string>* into) { std::vector<std::string> filepaths; if (idx.type() != TokenType::filepath) { if (!kOptional) { std::cerr << "Expected filepath but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; } return; } while (idx.type() == TokenType::filepath) { into->push_back(idx.str()); idx.next(); } } bool parse_filepath_command(TokenIndex& idx, TokenType filepath_command_token, const std::string& basedir, std::vector<std::string>* filepaths) { if (idx.type() == filepath_command_token) { unsigned int line_number = idx.line(); idx.next(); // Consume the command token. // Fail without consumption if this is an end of file token. if (idx.type() == TokenType::eof_token) { std::cerr << "Expecting at least one file as an argument but found end of " "file at line " << line_number << std::endl; return true; } // Fail without consumption if this is a command token. if (idx.it->is_command()) { std::cerr << "Expecting a file path argument but got command " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; return true; } // Parse the filename. if (idx.type() != TokenType::filepath) { std::cerr << "Expected a filepath but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; return true; } parse_filepaths(idx, filepaths); return true; } return false; } bool parse_optional_filepath_command(TokenIndex& idx, TokenType filepath_command_token, std::vector<std::string>* filepaths) { if (idx.type() != filepath_command_token) { return false; } idx.next(); // Consume the command token. // Parse an optional filepath argument. parse_filepaths</*kOptional=*/true>(idx, filepaths); return true; } bool parse_jars(TokenIndex& idx, TokenType jar_token, const std::string& basedir, std::vector<std::string>* jars) { if (idx.type() == jar_token) { unsigned int line_number = idx.line(); idx.next(); // Consume the jar token. // Fail without consumption if this is an end of file token. if (idx.type() == TokenType::eof_token) { std::cerr << "Expecting at least one file as an argument but found end of " "file at line " << line_number << std::endl << idx.show_context(2) << std::endl; return true; } // Parse the list of filenames. parse_filepaths(idx, jars); return true; } return false; } bool parse_dontusemixedcaseclassnames(TokenIndex& idx, bool* dontusemixedcaseclassnames) { if (idx.type() != TokenType::dontusemixedcaseclassnames_token) { return false; } *dontusemixedcaseclassnames = true; idx.next(); return true; } bool parse_dontpreverify(TokenIndex& idx, bool* dontpreverify) { if (idx.type() != TokenType::dontpreverify_token) { return false; } *dontpreverify = true; idx.next(); return true; } bool parse_verbose(TokenIndex& idx, bool* verbose) { if (idx.type() != TokenType::verbose_token) { return false; } *verbose = true; idx.next(); return true; } bool parse_bool_command(TokenIndex& idx, TokenType bool_command_token, bool new_value, bool* bool_value) { if (idx.type() == bool_command_token) { idx.next(); // Consume the boolean command token. *bool_value = new_value; return true; } return false; } bool parse_repackageclasses(TokenIndex& idx) { if (idx.type() != TokenType::repackageclasses) { return false; } // Ignore repackageclasses. idx.next(); if (idx.type() == TokenType::identifier) { std::cerr << "Ignoring -repackageclasses " << idx.data() << std::endl << idx.show_context(2) << std::endl; idx.next(); } return true; } bool parse_target(TokenIndex& idx, std::string* target_version) { if (idx.type() == TokenType::target) { idx.next(); // Consume the target command token. // Check to make sure the next TokenType is a version token. if (idx.type() != TokenType::target_version_token) { std::cerr << "Expected a target version but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; return true; } *target_version = idx.str(); // Consume the target version token. idx.next(); return true; } return false; } bool parse_allowaccessmodification(TokenIndex& idx, bool* allowaccessmodification) { if (idx.type() != TokenType::allowaccessmodification_token) { return false; } idx.next(); *allowaccessmodification = true; return true; } bool parse_filter_list_command(TokenIndex& idx, TokenType filter_command_token, std::vector<std::string>* filters) { if (idx.type() != filter_command_token) { return false; } idx.next(); while (idx.type() == TokenType::filter_pattern) { filters->push_back(idx.str()); idx.next(); } return true; } bool parse_optimizationpasses_command(TokenIndex& idx) { if (idx.type() != TokenType::optimizationpasses) { return false; } idx.next(); // Comsume the next token. idx.next(); return true; } bool is_modifier(TokenType tok) { return tok == TokenType::includedescriptorclasses_token || tok == TokenType::allowshrinking_token || tok == TokenType::allowoptimization_token || tok == TokenType::allowobfuscation_token; } bool parse_modifiers(TokenIndex& idx, KeepSpec* keep) { while (idx.type() == TokenType::comma) { idx.next(); if (!is_modifier(idx.type())) { std::cerr << "Expected keep option modifier but found : " << idx.show() << " at line number " << idx.line() << std::endl << idx.show_context(2) << std::endl; return false; } switch (idx.type()) { case TokenType::includedescriptorclasses_token: keep->includedescriptorclasses = true; break; case TokenType::allowshrinking_token: keep->allowshrinking = true; break; case TokenType::allowoptimization_token: keep->allowoptimization = true; break; case TokenType::allowobfuscation_token: keep->allowobfuscation = true; break; default: break; } idx.next(); } return true; } DexAccessFlags process_access_modifier(TokenType type, bool* is_access_flag) { *is_access_flag = true; switch (type) { case TokenType::publicToken: return ACC_PUBLIC; case TokenType::privateToken: return ACC_PRIVATE; case TokenType::final: return ACC_FINAL; case TokenType::abstract: return ACC_ABSTRACT; case TokenType::synthetic: return ACC_SYNTHETIC; case TokenType::staticToken: return ACC_STATIC; case TokenType::volatileToken: return ACC_VOLATILE; case TokenType::native: return ACC_NATIVE; case TokenType::protectedToken: return ACC_PROTECTED; case TokenType::transient: return ACC_TRANSIENT; default: *is_access_flag = false; return ACC_PUBLIC; } } bool is_negation_or_class_access_modifier(TokenType type) { switch (type) { case TokenType::notToken: case TokenType::publicToken: case TokenType::privateToken: case TokenType::protectedToken: case TokenType::final: case TokenType::abstract: case TokenType::synthetic: case TokenType::native: case TokenType::staticToken: case TokenType::volatileToken: case TokenType::transient: return true; default: return false; } } std::string parse_annotation_type(TokenIndex& idx) { if (idx.type() != TokenType::annotation_application) { return ""; } idx.next(); if (idx.type() != TokenType::identifier) { std::cerr << "Expecting a class identifier after @ but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; return ""; } const auto& typ = idx.data(); idx.next(); return convert_wildcard_type(typ); } bool is_access_flag_set(const DexAccessFlags accessFlags, const DexAccessFlags checkingFlag) { return accessFlags & checkingFlag; } void set_access_flag(DexAccessFlags& accessFlags, const DexAccessFlags settingFlag) { accessFlags = accessFlags | settingFlag; } bool parse_access_flags(TokenIndex& idx, DexAccessFlags& setFlags_, DexAccessFlags& unsetFlags_) { bool negated = false; while (is_negation_or_class_access_modifier(idx.type())) { // Copy the iterator so we can peek and see if the next TokenType is an // access token; we don't want to modify the main iterator otherwise auto access_it = idx.it; if (idx.type() == TokenType::notToken) { negated = true; ++access_it; } bool ok; DexAccessFlags access_flag = process_access_modifier(access_it->type, &ok); if (ok) { idx.it = ++access_it; if (negated) { if (is_access_flag_set(setFlags_, access_flag)) { std::cerr << "Access flag " << idx.show() << " occurs with conflicting settings at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; return false; } set_access_flag(unsetFlags_, access_flag); negated = false; } else { if (is_access_flag_set(unsetFlags_, access_flag)) { std::cerr << "Access flag " << idx.show() << " occurs with conflicting settings at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; return false; } set_access_flag(setFlags_, access_flag); negated = false; } } else { break; } } return true; } /* * Parse [!](class|interface|enum|@interface). */ bool parse_class_token(TokenIndex& idx, DexAccessFlags& setFlags_, DexAccessFlags& unsetFlags_) { bool negated = false; if (idx.type() == TokenType::notToken) { negated = true; idx.next(); } // Make sure the next keyword is interface, class, enum. switch (idx.type()) { case TokenType::interface: set_access_flag(negated ? unsetFlags_ : setFlags_, ACC_INTERFACE); break; case TokenType::enumToken: set_access_flag(negated ? unsetFlags_ : setFlags_, ACC_ENUM); break; case TokenType::annotation: set_access_flag(negated ? unsetFlags_ : setFlags_, ACC_ANNOTATION); break; case TokenType::classToken: break; default: std::cerr << "Expected interface, class or enum but got " << idx.show() << " at line number " << idx.line() << std::endl << idx.show_context(2) << std::endl; return false; } idx.next(); return true; } // Consume an expected token, indicating if that TokenType was found. // If some other TokenType is found, then it is not consumed and false // is returned. bool consume_token(TokenIndex& idx, const TokenType& tok) { if (idx.type() != tok) { std::cerr << "Unexpected TokenType " << idx.show() << std::endl << idx.show_context(2) << std::endl; return false; } idx.next(); return true; } // Consume an expected semicolon, complaining if one was not found. void gobble_semicolon(TokenIndex& idx, bool* ok) { *ok = consume_token(idx, TokenType::semiColon); if (!*ok) { std::cerr << "Expecting a semicolon but found " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; return; } } void skip_to_semicolon(TokenIndex& idx) { while ((idx.type() != TokenType::semiColon) && (idx.type() != TokenType::eof_token)) { idx.next(); } if (idx.type() == TokenType::semiColon) { idx.next(); } } void parse_member_specification(TokenIndex& idx, ClassSpecification* class_spec, bool* ok) { MemberSpecification member_specification; *ok = true; member_specification.annotationType = parse_annotation_type(idx); if (!parse_access_flags(idx, member_specification.requiredSetAccessFlags, member_specification.requiredUnsetAccessFlags)) { // There was a problem parsing the access flags. Return an empty class spec // for now. std::cerr << "Problem parsing access flags for member specification.\n"; *ok = false; skip_to_semicolon(idx); return; } // The next TokenType better be an identifier. if (idx.type() != TokenType::identifier) { std::cerr << "Expecting field or member specification but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; *ok = false; skip_to_semicolon(idx); return; } const auto& ident = idx.data(); // Check for "*". if (ident == "*") { member_specification.name = ""; member_specification.descriptor = ""; idx.next(); gobble_semicolon(idx, ok); class_spec->methodSpecifications.push_back(member_specification); class_spec->fieldSpecifications.push_back(member_specification); return; } // Check for <methods> if (ident == "<methods>") { member_specification.name = ""; member_specification.descriptor = ""; idx.next(); gobble_semicolon(idx, ok); class_spec->methodSpecifications.push_back(member_specification); return; } // Check for <fields> if (ident == "<fields>") { member_specification.name = ""; member_specification.descriptor = ""; idx.next(); gobble_semicolon(idx, ok); class_spec->fieldSpecifications.push_back(member_specification); return; } // Check for <init> if (ident == "<init>") { member_specification.name = "<init>"; member_specification.descriptor = "V"; set_access_flag(member_specification.requiredSetAccessFlags, ACC_CONSTRUCTOR); idx.next(); } else { // This TokenType is the type for the member specification. if (idx.type() != TokenType::identifier) { std::cerr << "Expecting type identifier but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; *ok = false; skip_to_semicolon(idx); return; } const auto& typ = idx.data(); idx.next(); member_specification.descriptor = convert_wildcard_type(typ); if (idx.type() != TokenType::identifier) { std::cerr << "Expecting identifier name for class member but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; *ok = false; skip_to_semicolon(idx); return; } member_specification.name = idx.str(); idx.next(); } // Check to see if this is a method specification. if (idx.type() == TokenType::openBracket) { consume_token(idx, TokenType::openBracket); std::string arg = "("; while (true) { // If there is a ")" next we are done. if (idx.type() == TokenType::closeBracket) { consume_token(idx, TokenType::closeBracket); break; } if (idx.type() != TokenType::identifier) { std::cerr << "Expecting type identifier but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; *ok = false; return; } const auto& typ = idx.data(); consume_token(idx, TokenType::identifier); arg += convert_wildcard_type(typ); // The next TokenType better be a comma or a closing bracket. if (idx.type() != TokenType::comma && idx.type() != TokenType::closeBracket) { std::cerr << "Expecting comma or ) but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; *ok = false; return; } // If the next TokenType is a comma (rather than closing bracket) consume // it and check that it is followed by an identifier. if (idx.type() == TokenType::comma) { consume_token(idx, TokenType::comma); if (idx.type() != TokenType::identifier) { std::cerr << "Expecting type identifier after comma but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; *ok = false; return; } } } arg += ")"; arg += member_specification.descriptor; member_specification.descriptor = arg; } // if with value, look for return if (idx.type() == TokenType::returns) { idx.next(); const auto& rident = idx.data(); if (rident == "true") { member_specification.return_value.value_type = AssumeReturnValue::ValueType::ValueBool; member_specification.return_value.value.v = 1; idx.next(); } if (rident == "false") { member_specification.return_value.value_type = AssumeReturnValue::ValueType::ValueBool; member_specification.return_value.value.v = 0; idx.next(); } } // Make sure member specification ends with a semicolon. gobble_semicolon(idx, ok); if (!ok) { return; } if (member_specification.descriptor[0] == '(') { class_spec->methodSpecifications.push_back(member_specification); } else { class_spec->fieldSpecifications.push_back(member_specification); } } void parse_member_specifications(TokenIndex& idx, ClassSpecification* class_spec, bool* ok) { if (idx.type() == TokenType::openCurlyBracket) { idx.next(); while ((idx.type() != TokenType::closeCurlyBracket) && (idx.type() != TokenType::eof_token)) { parse_member_specification(idx, class_spec, ok); if (!*ok) { // We failed to parse a member specification so skip to the next // semicolon. skip_to_semicolon(idx); } } if (idx.type() == TokenType::closeCurlyBracket) { idx.next(); } } } bool member_comparison(const MemberSpecification& m1, const MemberSpecification& m2) { return m1.name < m2.name; } std::string parse_class_name(TokenIndex& idx, bool* ok) { if (idx.type() != TokenType::identifier) { std::cerr << "Expected class name but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; *ok = false; return ""; } auto name = idx.str(); idx.next(); return name; } void parse_class_names( TokenIndex& idx, bool* ok, std::vector<ClassSpecification::ClassNameSpec>& class_names) { bool negated{false}; auto maybe_negated = [&]() { if (idx.type() == TokenType::notToken) { negated = true; idx.next(); } }; auto push_to = [&](std::string&& s) { class_names.emplace_back(s, negated); negated = false; }; maybe_negated(); push_to(parse_class_name(idx, ok)); if (!*ok) { return; } // Maybe consume comma delimited list while (idx.type() == TokenType::comma) { // Consume comma idx.next(); maybe_negated(); push_to(parse_class_name(idx, ok)); if (!*ok) { return; } } } ClassSpecification parse_class_specification(TokenIndex& idx, bool* ok) { ClassSpecification class_spec; *ok = true; class_spec.annotationType = parse_annotation_type(idx); if (!parse_access_flags( idx, class_spec.setAccessFlags, class_spec.unsetAccessFlags)) { // There was a problem parsing the access flags. Return an empty class spec // for now. std::cerr << "Problem parsing access flags for class specification.\n"; *ok = false; return class_spec; } if (!parse_class_token( idx, class_spec.setAccessFlags, class_spec.unsetAccessFlags)) { *ok = false; return class_spec; } // Parse the class name(s). parse_class_names(idx, ok, class_spec.classNames); if (!*ok) { return class_spec; } // Parse extends/implements if present, treating implements like extends. if ((idx.type() == TokenType::extends) || (idx.type() == TokenType::implements)) { idx.next(); class_spec.extendsAnnotationType = parse_annotation_type(idx); if (idx.type() != TokenType::identifier) { std::cerr << "Expecting a class name after extends/implements but got " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; *ok = false; class_spec.extendsClassName = ""; } else { class_spec.extendsClassName = idx.str(); } idx.next(); } // Parse the member specifications, if there are any parse_member_specifications(idx, &class_spec, ok); std::sort(class_spec.fieldSpecifications.begin(), class_spec.fieldSpecifications.end(), member_comparison); std::sort(class_spec.methodSpecifications.begin(), class_spec.methodSpecifications.end(), member_comparison); return class_spec; } bool parse_keep(TokenIndex& idx, TokenType keep_kind, KeepSpecSet* spec, bool mark_classes, bool mark_conditionally, bool allowshrinking, const std::string& filename, uint32_t line, bool* ok) { if (idx.type() == keep_kind) { idx.next(); // Consume the keep token auto keep = std::make_unique<KeepSpec>(); keep->mark_classes = mark_classes; keep->mark_conditionally = mark_conditionally; keep->allowshrinking = allowshrinking; keep->source_filename = filename; keep->source_line = line; if (!parse_modifiers(idx, &*keep)) { skip_to_next_command(idx); return true; } keep->class_spec = parse_class_specification(idx, ok); spec->emplace(std::move(keep)); return true; } return false; } void parse(const std::vector<Token>& vec, ProguardConfiguration* pg_config, size_t& parse_errors, size_t& unimplemented, const std::string& filename) { bool ok; TokenIndex idx{vec, vec.begin()}; while (idx.it != idx.vec.end()) { // Break out if we are at the end of the TokenType stream. if (idx.type() == TokenType::eof_token) { break; } if (idx.type() == TokenType::comment) { idx.next(); continue; } uint32_t line = idx.line(); if (!idx.it->is_command()) { std::cerr << "Expecting command but found " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; idx.next(); skip_to_next_command(idx); continue; } // Input/Output Options if (parse_filepath_command(idx, TokenType::include, pg_config->basedirectory, &pg_config->includes)) { continue; } if (parse_single_filepath_command( idx, TokenType::basedirectory, &pg_config->basedirectory)) { continue; } if (parse_jars(idx, TokenType::injars, pg_config->basedirectory, &pg_config->injars)) { continue; } if (parse_jars(idx, TokenType::outjars, pg_config->basedirectory, &pg_config->outjars)) { continue; } if (parse_jars(idx, TokenType::libraryjars, pg_config->basedirectory, &pg_config->libraryjars)) { continue; } // -skipnonpubliclibraryclasses not supported if (idx.type() == TokenType::dontskipnonpubliclibraryclasses) { // Silenty ignore the dontskipnonpubliclibraryclasses option. idx.next(); continue; } // -dontskipnonpubliclibraryclassmembers not supported if (parse_filepath_command(idx, TokenType::keepdirectories, pg_config->basedirectory, &pg_config->keepdirectories)) { continue; } if (parse_target(idx, &pg_config->target_version)) { continue; } // -forceprocessing not supported // Keep Options if (parse_keep(idx, TokenType::keep, &pg_config->keep_rules, true, // mark_classes false, // mark_conditionally false, // allowshrinking filename, line, &ok)) { if (!ok) { ++parse_errors; } continue; } if (parse_keep(idx, TokenType::keepclassmembers, &pg_config->keep_rules, false, // mark_classes false, // mark_conditionally false, // allowshrinking filename, line, &ok)) { if (!ok) { ++parse_errors; } continue; } if (parse_keep(idx, TokenType::keepclasseswithmembers, &pg_config->keep_rules, false, // mark_classes true, // mark_conditionally false, // allowshrinking filename, line, &ok)) { if (!ok) { ++parse_errors; } continue; } if (parse_keep(idx, TokenType::keepnames, &pg_config->keep_rules, true, // mark_classes false, // mark_conditionally true, // allowshrinking filename, line, &ok)) { if (!ok) { ++parse_errors; } continue; } if (parse_keep(idx, TokenType::keepclassmembernames, &pg_config->keep_rules, false, // mark_classes false, // mark_conditionally true, // allowshrinking filename, line, &ok)) { if (!ok) { ++parse_errors; } continue; } if (parse_keep(idx, TokenType::keepclasseswithmembernames, &pg_config->keep_rules, false, // mark_classes true, // mark_conditionally true, // allowshrinking filename, line, &ok)) { if (!ok) { ++parse_errors; } continue; } if (parse_optional_filepath_command( idx, TokenType::printseeds, &pg_config->printseeds)) { continue; } // Shrinking Options if (parse_bool_command( idx, TokenType::dontshrink, false, &pg_config->shrink)) { continue; } if (parse_optional_filepath_command( idx, TokenType::printusage, &pg_config->printusage)) { continue; } // Optimization Options if (parse_boolean_command( idx, TokenType::dontoptimize, &pg_config->optimize, false)) { continue; } if (parse_filter_list_command( idx, TokenType::optimizations, &pg_config->optimization_filters)) { continue; } if (parse_optimizationpasses_command(idx)) { continue; } if (parse_keep(idx, TokenType::assumenosideeffects, &pg_config->assumenosideeffects_rules, false, // mark_classes false, // mark_conditionally false, // allowshrinking filename, line, &ok)) { continue; } if (parse_keep(idx, TokenType::whyareyoukeeping, &pg_config->whyareyoukeeping_rules, false, // mark_classes false, // mark_conditionally false, // allowshrinking filename, line, &ok)) { continue; } // Obfuscation Options if (idx.type() == TokenType::dontobfuscate) { pg_config->dontobfuscate = true; idx.next(); continue; } // Redex ignores -dontskipnonpubliclibraryclasses if (idx.type() == TokenType::dontskipnonpubliclibraryclasses) { idx.next(); continue; } if (parse_optional_filepath_command( idx, TokenType::printmapping, &pg_config->printmapping)) { continue; } if (parse_optional_filepath_command(idx, TokenType::printconfiguration, &pg_config->printconfiguration)) { continue; } if (parse_allowaccessmodification(idx, &pg_config->allowaccessmodification)) { continue; } if (parse_dontusemixedcaseclassnames( idx, &pg_config->dontusemixedcaseclassnames)) { continue; } if (parse_filter_list_command( idx, TokenType::keeppackagenames, &pg_config->keeppackagenames)) { continue; } if (parse_dontpreverify(idx, &pg_config->dontpreverify)) { continue; } if (parse_verbose(idx, &pg_config->verbose)) { continue; } if (parse_repackageclasses(idx)) { continue; } if (parse_filter_list_command( idx, TokenType::dontwarn, &pg_config->dontwarn)) { continue; } if (parse_filter_list_command( idx, TokenType::keepattributes, &pg_config->keepattributes)) { continue; } // Skip unknown token. if (idx.it->is_command()) { const auto& name = idx.data(); // It is benign to drop -dontnote if (name != "dontnote") { std::cerr << "Unimplemented command (skipping): " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; ++unimplemented; } } else { std::cerr << "Unexpected TokenType " << idx.show() << " at line " << idx.line() << std::endl << idx.show_context(2) << std::endl; ++parse_errors; } idx.next(); skip_to_next_command(idx); } } Stats parse(const std::string_view& config, ProguardConfiguration* pg_config, const std::string& filename) { Stats ret{}; std::vector<Token> tokens = lex(config); bool ok = true; // Check for bad tokens. for (auto& tok : tokens) { if (tok.type == TokenType::unknownToken) { std::string spelling ATTRIBUTE_UNUSED = std::string(tok.data); ++ret.unknown_tokens; ok = false; } // std::cout << tok.show() << " at line " << tok.line << std::endl; } if (!ok) { std::cerr << "Found " << ret.unknown_tokens << " unkown tokens in " << filename << "\n"; pg_config->ok = false; return ret; } parse(tokens, pg_config, ret.parse_errors, ret.unimplemented, filename); if (ret.parse_errors == 0) { pg_config->ok = ok; } else { pg_config->ok = false; std::cerr << "Found " << ret.parse_errors << " parse errors in " << filename << "\n"; } return ret; } } // namespace Stats parse(std::istream& config, ProguardConfiguration* pg_config, const std::string& filename) { std::stringstream buffer; buffer << config.rdbuf(); return parse(buffer.str(), pg_config, filename); } Stats parse_file(const std::string& filename, ProguardConfiguration* pg_config) { Stats ret{}; redex::read_file_with_contents(filename, [&](const char* data, size_t s) { std::string_view view(data, s); ret += parse(view, pg_config, filename); // Parse the included files. for (const auto& included_filename : pg_config->includes) { if (pg_config->already_included.find(included_filename) != pg_config->already_included.end()) { continue; } pg_config->already_included.emplace(included_filename); ret += parse_file(included_filename, pg_config); } }); return ret; } size_t remove_default_blocklisted_rules(ProguardConfiguration* pg_config) { // TODO: Make the set of excluded rules configurable. auto blocklisted_rules = R"( # The proguard-android-optimize.txt file that is bundled with the Android SDK # has a keep rule to prevent removal of all resource ID fields. This is likely # because ProGuard runs before aapt which can change the values of those # fields. Since this is no longer true in our case, this rule is redundant and # hampers our optimizations. # # I chose to exclude this rule instead of unmarking all resource IDs so that # if a resource ID really needs to be kept, the user can still keep it by # writing a keep rule that does a non-wildcard match. -keepclassmembers class **.R$* { public static <fields>; } # See keepclassnames.pro, or T1890454. -keepnames class * )"; return remove_blocklisted_rules(blocklisted_rules, pg_config); } size_t remove_blocklisted_rules(const std::string& rules, ProguardConfiguration* pg_config) { ProguardConfiguration pg_config_blocklist; parse(rules, &pg_config_blocklist, "<internal blocklist>"); size_t removed{0}; pg_config->keep_rules.erase_if([&](const KeepSpec& ks) { for (const auto& blocklisted_ks : pg_config_blocklist.keep_rules) { if (ks == *blocklisted_ks) { removed++; return true; } } return false; }); return removed; } } // namespace proguard_parser } // namespace keep_rules