Skip to content

Commit

Permalink
Support a wider char range in class specifications
Browse files Browse the repository at this point in the history
  • Loading branch information
daphnis.chevreton committed Jul 2, 2024
1 parent 6075d17 commit ee3deb6
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 3 deletions.
47 changes: 44 additions & 3 deletions base/src/main/java/proguard/ConfigurationParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,6 @@
import java.util.List;
import java.util.Properties;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;


/**
* This class parses ProGuard configurations. Configurations can be read from an
Expand All @@ -51,6 +49,8 @@
*/
public class ConfigurationParser implements AutoCloseable
{
private final boolean useDalvikVerification = System.getProperty("proguard.use.dalvik.identifier.verification") != null;

private final WordReader reader;
private final Properties properties;

Expand Down Expand Up @@ -1955,7 +1955,7 @@ private void checkNextWordIsJavaIdentifier(String expectedDescription, boolean a
private void checkJavaIdentifier(String expectedDescription, String identifier, boolean allowGenerics)
throws ParseException
{
if (!isJavaIdentifier(identifier))
if (!isValidIdentifier(identifier))
{
throw new ParseException("Expecting " + expectedDescription +
" before " + reader.locationDescription());
Expand All @@ -1968,6 +1968,10 @@ private void checkJavaIdentifier(String expectedDescription, String identifier,
}
}

private boolean isValidIdentifier(String word)
{
return useDalvikVerification ? isDexIdentifier(word) : isJavaIdentifier(word);
}

/**
* Returns whether the given word is a valid Java identifier.
Expand Down Expand Up @@ -2002,6 +2006,43 @@ private boolean isJavaIdentifier(String word)
return true;
}

/**
* Returns whether the given word is a valid DEX identifier. Special wildcard characters for
* ProGuard class specifiction syntaxs are accepted. The list of valid identifier can be
* found at https://source.android.com/docs/core/runtime/dex-format#simplename
*/
private boolean isDexIdentifier(String word) {
if (word.isEmpty()) {
return false;
}

int[] codePoints = word.codePoints().toArray();

for (int index = 0; index < codePoints.length; index++) {
int c = codePoints[index];

boolean isLetterOrNumber = Character.isLetterOrDigit(c);
boolean isValidSymbol = c == '$' || c == '-' || c == '_';
boolean isWithinSupportedUnicodeRanges =
(c >= 0x00a1 && c <= 0x1fff)
|| (c >= 0x2010 && c <= 0x2027)
|| (c >= 0x2030 && c <= 0xd7ff)
|| (c >= 0xe000 && c <= 0xffef)
|| (c >= 0x10000 && c <= 0x10ffff);
boolean isProGuardSymbols =
c == '.' || c == '[' || c == ']' || c == '<' || c == '>' || c == '-' || c == '!'
|| c == '*' || c == '?' || c == '%';

if (!(isLetterOrNumber
|| isValidSymbol
|| isWithinSupportedUnicodeRanges
|| isProGuardSymbols)) {
return false;
}
}

return true;
}

/**
* Returns whether the given word contains angle brackets around
Expand Down
76 changes: 76 additions & 0 deletions base/src/test/kotlin/proguard/ConfigurationParserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ package proguard

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FreeSpec
import io.kotest.extensions.system.withSystemProperty
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.string.shouldContain
import io.mockk.every
import io.mockk.mockkObject
import proguard.classfile.AccessConstants.PUBLIC
import testutils.asConfiguration
import java.io.ByteArrayOutputStream
Expand Down Expand Up @@ -39,6 +43,9 @@ class ConfigurationParserTest : FreeSpec({
return configuration
}

fun parseRulesAsArguments(rules: String) =
rules.split(' ', '\n').toTypedArray()

"Keep rule tests" - {
"Keep rule with <fields> wildcard should be valid" {
parseConfiguration("-keep class * { <fields>; }")
Expand Down Expand Up @@ -428,4 +435,73 @@ class ConfigurationParserTest : FreeSpec({
}
}
}

"Class specification with unicode identifiers" - {
"Given some -keep rules with class specifications containing supported characters for DEX file" - {
val rules = """
-keep class uu.☱ { *; }
-keep class uu.o { ** ☱; }
-keep class uu.o { *** ☱(); }
-keep class uu.o1
-keep class uu.o${"$"}o
-keep class uu.o-o
-keep class uu.o_o
-keep class ‐ { <methods>; }
-keep class ‧ { <fields>; }
-keep class ‰
-keep class * { ** ퟿(...); }
-keep class { *; }
-keep class ￯ { int[] foo; }
-if class **𐀀 { ** 𐀀*(); }
-keep class <1> { ** 𐀀*(); }
-keep class * extends 􏿿
-keep class ** implements ☱
""".trimIndent()

val reader = ArgumentWordReader(parseRulesAsArguments(rules), null)
mockkObject(reader)
every { reader.locationDescription() } returns "dummyOrigin"

"When the given rules are parsed" - {
withSystemProperty("proguard.use.dalvik.identifier.verification", "true") {
val configuration = parseConfiguration(reader)
"Then 'keep' should contain 16 keep class specifications" {
configuration.keep shouldHaveSize 16
}
}
}
}

"Given some -keep rules with class specifications containing unsupported identifier for DEX file" - {
val rules = """
-keep class uu.${String(Character.toChars(0x00a1 - 1))} { *; }
-keep class uu.o { ** ${String(Character.toChars(0x1fff + 1))}; }
-keep class uu.o { *** ${String(Character.toChars(0x2010 - 1))}(); }
-keep class ${String(Character.toChars(0x2027 + 1))} { <methods>; }
-keep class ${String(Character.toChars(0x2030 - 1))} { <fields>; }
-keep class ${String(Character.toChars(0xd7ff + 1))}
-keep class * { ** ${String(Character.toChars(0xe000 - 1))}(...); }
-keep class ${String(Character.toChars(0xffef + 1))} { *; }
-keep class ${String(Character.toChars(0x10000 - 1))} { int[] foo; }
-keep class * extends !
-keep class ** implements @
-keep class #
-keep class %
-keep class ^
-keep class &
-keep class ;
-keep class ,
""".trimIndent()

val reader = ArgumentWordReader(parseRulesAsArguments(rules), null)
mockkObject(reader)
every { reader.locationDescription() } returns "dummyOrigin"

"When the given rule is parsed, then a ParseException should be thrown" - {
withSystemProperty("proguard.use.dalvik.identifier.verification", "true") {
shouldThrow<ParseException> { parseConfiguration(reader) }
}
}
}
}
})

0 comments on commit ee3deb6

Please sign in to comment.