Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b23f1e07 authored by Adam Lesinski's avatar Adam Lesinski
Browse files

AAPT2: Verify positional Java String format arguments in strings

Change-Id: Id415969035a0d5712857c0e11e140155566a960c
parent 2ae4a877
Loading
Loading
Loading
Loading
+32 −5
Original line number Original line Diff line number Diff line
@@ -62,6 +62,7 @@ bool ResourceParser::flattenXmlSubtree(XmlPullParser* parser, std::u16string* ou
                                       StyleString* outStyleString) {
                                       StyleString* outStyleString) {
    std::vector<Span> spanStack;
    std::vector<Span> spanStack;


    bool error = false;
    outRawString->clear();
    outRawString->clear();
    outStyleString->spans.clear();
    outStyleString->spans.clear();
    util::StringBuilder builder;
    util::StringBuilder builder;
@@ -84,7 +85,6 @@ bool ResourceParser::flattenXmlSubtree(XmlPullParser* parser, std::u16string* ou
            spanStack.pop_back();
            spanStack.pop_back();


        } else if (event == XmlPullParser::Event::kText) {
        } else if (event == XmlPullParser::Event::kText) {
            // TODO(adamlesinski): Verify format strings.
            outRawString->append(parser->getText());
            outRawString->append(parser->getText());
            builder.append(parser->getText());
            builder.append(parser->getText());


@@ -116,9 +116,10 @@ bool ResourceParser::flattenXmlSubtree(XmlPullParser* parser, std::u16string* ou
            if (builder.str().size() > std::numeric_limits<uint32_t>::max()) {
            if (builder.str().size() > std::numeric_limits<uint32_t>::max()) {
                mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber()))
                mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber()))
                             << "style string '" << builder.str() << "' is too long");
                             << "style string '" << builder.str() << "' is too long");
                return false;
                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 == XmlPullParser::Event::kComment) {
            // Skip
            // Skip
@@ -129,7 +130,7 @@ bool ResourceParser::flattenXmlSubtree(XmlPullParser* parser, std::u16string* ou
    assert(spanStack.empty() && "spans haven't been fully processed");
    assert(spanStack.empty() && "spans haven't been fully processed");


    outStyleString->str = builder.str();
    outStyleString->str = builder.str();
    return true;
    return !error;
}
}


bool ResourceParser::parse(XmlPullParser* parser) {
bool ResourceParser::parse(XmlPullParser* parser) {
@@ -437,13 +438,39 @@ std::unique_ptr<Item> ResourceParser::parseXml(XmlPullParser* parser, const uint
bool ResourceParser::parseString(XmlPullParser* parser, ParsedResource* outResource) {
bool ResourceParser::parseString(XmlPullParser* parser, ParsedResource* outResource) {
    const Source source = mSource.withLine(parser->getLineNumber());
    const Source source = mSource.withLine(parser->getLineNumber());


    // TODO(adamlesinski): Read "untranslateable" attribute.
    bool formatted = true;
    if (Maybe<StringPiece16> formattedAttr = findAttribute(parser, u"formatted")) {
        if (!ResourceUtils::tryParseBool(formattedAttr.value(), &formatted)) {
            mDiag->error(DiagMessage(source) << "invalid value for 'formatted'. Must be a boolean");
            return false;
        }
    }

    bool untranslateable = false;
    if (Maybe<StringPiece16> untranslateableAttr = findAttribute(parser, u"untranslateable")) {
        if (!ResourceUtils::tryParseBool(untranslateableAttr.value(), &untranslateable)) {
            mDiag->error(DiagMessage(source)
                         << "invalid value for 'untranslateable'. Must be a boolean");
            return false;
        }
    }


    outResource->value = parseXml(parser, android::ResTable_map::TYPE_STRING, kNoRawString);
    outResource->value = parseXml(parser, android::ResTable_map::TYPE_STRING, kNoRawString);
    if (!outResource->value) {
    if (!outResource->value) {
        mDiag->error(DiagMessage(source) << "not a valid string");
        mDiag->error(DiagMessage(source) << "not a valid string");
        return false;
        return false;
    }
    }

    if (formatted || untranslateable) {
        if (String* stringValue = valueCast<String>(outResource->value.get())) {
            if (!util::verifyJavaStringFormat(*stringValue->value)) {
                mDiag->error(DiagMessage(mSource.withLine(parser->getLineNumber()))
                             << "multiple substitutions specified in non-positional format; "
                                "did you mean to add the formatted=\"false\" attribute?");
                return false;
            }
        }
    }
    return true;
    return true;
}
}


+27 −9
Original line number Original line Diff line number Diff line
@@ -328,19 +328,37 @@ std::unique_ptr<BinaryPrimitive> tryParseColor(const StringPiece16& str) {
    return error ? std::unique_ptr<BinaryPrimitive>() : util::make_unique<BinaryPrimitive>(value);
    return error ? std::unique_ptr<BinaryPrimitive>() : util::make_unique<BinaryPrimitive>(value);
}
}


std::unique_ptr<BinaryPrimitive> tryParseBool(const StringPiece16& str) {
bool tryParseBool(const StringPiece16& str, bool* outValue) {
    StringPiece16 trimmedStr(util::trimWhitespace(str));
    StringPiece16 trimmedStr(util::trimWhitespace(str));
    uint32_t data = 0;
    if (trimmedStr == u"true" || trimmedStr == u"TRUE") {
    if (trimmedStr == u"true" || trimmedStr == u"TRUE") {
        data = 0xffffffffu;
        if (outValue) {
    } else if (trimmedStr != u"false" && trimmedStr != u"FALSE") {
            *outValue = true;
        return {};
        }
        return true;
    } else if (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 = {};
        android::Res_value value = {};
        value.dataType = android::Res_value::TYPE_INT_BOOLEAN;
        value.dataType = android::Res_value::TYPE_INT_BOOLEAN;
    value.data = data;

        if (result) {
            value.data = 0xffffffffu;
        } else {
            value.data = 0;
        }
        return util::make_unique<BinaryPrimitive>(value);
        return util::make_unique<BinaryPrimitive>(value);
    }
    }
    return {};
}


std::unique_ptr<BinaryPrimitive> tryParseInt(const StringPiece16& str) {
std::unique_ptr<BinaryPrimitive> tryParseInt(const StringPiece16& str) {
    android::Res_value value;
    android::Res_value value;
+5 −0
Original line number Original line Diff line number Diff line
@@ -59,6 +59,11 @@ bool isReference(const StringPiece16& str);
 */
 */
bool tryParseAttributeReference(const StringPiece16& str, ResourceNameRef* outReference);
bool tryParseAttributeReference(const StringPiece16& str, ResourceNameRef* outReference);


/**
 * 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
 * Returns a Reference, or None Maybe instance if the string `str` was parsed as a
 * valid reference to a style.
 * valid reference to a style.
+99 −0
Original line number Original line Diff line number Diff line
@@ -189,6 +189,105 @@ Maybe<std::u16string> getFullyQualifiedClassName(const StringPiece16& package,
    return result;
    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) {
static Maybe<char16_t> parseUnicodeCodepoint(const char16_t** start, const char16_t* end) {
    char16_t code = 0;
    char16_t code = 0;
    for (size_t i = 0; i < 4 && *start != end; i++, (*start)++) {
    for (size_t i = 0; i < 4 && *start != end; i++, (*start)++) {
+8 −0
Original line number Original line Diff line number Diff line
@@ -158,6 +158,14 @@ inline StringPiece16 getString(const android::ResStringPool& pool, size_t idx) {
    return StringPiece16();
    return StringPiece16();
}
}


/**
 * 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 {
class StringBuilder {
public:
public:
    StringBuilder& append(const StringPiece16& str);
    StringBuilder& append(const StringPiece16& str);
Loading