Support a wider char range in class specifications

This commit is contained in:
daphnis.chevreton
2024-07-02 09:52:46 +02:00
parent 6075d17bee
commit ee3deb69fa
2 changed files with 120 additions and 3 deletions

View File

@@ -39,8 +39,6 @@ import java.util.ArrayList;
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
@@ -51,6 +49,8 @@ import java.util.function.BiFunction;
*/
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;
@@ -1955,7 +1955,7 @@ public class ConfigurationParser implements AutoCloseable
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());
@@ -1968,6 +1968,10 @@ public class ConfigurationParser implements AutoCloseable
}
}
private boolean isValidIdentifier(String word)
{
return useDalvikVerification ? isDexIdentifier(word) : isJavaIdentifier(word);
}
/**
* Returns whether the given word is a valid Java identifier.
@@ -2002,6 +2006,43 @@ public class ConfigurationParser implements AutoCloseable
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

View File

@@ -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
@@ -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>; }")
@@ -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) }
}
}
}
}
})