Skip to content

[java] Fix By.className()/By.id() misescaping non-ASCII leading digits#17815

Open
diemol wants to merge 2 commits into
trunkfrom
fix/css-escape-non-ascii-digit
Open

[java] Fix By.className()/By.id() misescaping non-ASCII leading digits#17815
diemol wants to merge 2 commits into
trunkfrom
fix/css-escape-non-ascii-digit

Conversation

@diemol

@diemol diemol commented Jul 23, 2026

Copy link
Copy Markdown
Member

🔗 Related Issues

Fixes #17805

💥 What does this PR do?

PreW3CLocator.cssEscape (used by By.className() / By.id(), and duplicated in W3CHttpCommandCodec) used Character.isDigit(...) to decide whether a leading character needs CSS's leading-digit escape. Character.isDigit is true for any Unicode decimal digit, not just ASCII 0-9, so a leading non-ASCII digit (e.g. Arabic-Indic ٥, U+0665) was rewritten using its numeric value as if it were the ASCII digit of that value. That collides with a completely different class/id: By.className("٥foo") and By.className("5foo") both produced the selector .\35 foo.

The fix narrows the check to the ASCII range ('0'-'9') in both copies of cssEscape, and adds regression tests to ByTest.java covering both the existing ASCII-digit escape and the non-ASCII-digit collision.

🔧 Implementation Notes

Per the CSS Syntax spec, digit is defined as ASCII U+0030-U+0039 only. Non-ASCII code points are already valid identifier-start characters (ident-start code point) and require no escaping at all — they can be emitted as-is, including as the first character of the identifier. So the minimal, spec-correct fix is to stop misclassifying non-ASCII digits as needing the ASCII digit-escape, rather than adding new escaping logic for them.

I considered implementing a full CSS serialize-an-identifier/CSS.escape() port, but that's unnecessary here: the existing CSS_ESCAPE regex already leaves non-ASCII characters untouched, so restricting the digit check to ASCII is sufficient and keeps the diff minimal.

🤖 AI assistance

  • No substantial AI assistance used
  • AI assisted (complete below)
    • Tool(s): Claude Code
    • What was generated: Investigated and confirmed the reported defect against the current trunk code and against the CSS Syntax/CSSOM specs (via jshell and spec review), authored the one-line fix in both By.java and W3CHttpCommandCodec.java, and wrote the regression tests.
    • I reviewed all AI output and can explain the change

💡 Additional Considerations

None — this is a minimal, self-contained fix with no behavior change for existing ASCII-digit or non-digit inputs.

🔄 Types of changes

  • Bug fix (backwards compatible)

PreW3CLocator.cssEscape (used by By.className/By.id, duplicated in
W3CHttpCommandCodec) used Character.isDigit() to detect a leading digit
needing CSS escaping. That accepts any Unicode decimal digit, not just
ASCII 0-9, so a leading non-ASCII digit (e.g. Arabic-Indic U+0665) got
rewritten using its numeric value as if it were the ASCII digit of the
same value, producing the same selector as an unrelated ASCII-digit
class/id (By.className("٥foo") collided with By.className("5foo")).

Per the CSS Syntax spec, "digit" is ASCII 0-9 only; non-ASCII code
points are already valid identifier-start characters and need no
escaping, so the fix narrows the check to the ASCII range instead of
adding new escaping logic.
@selenium-ci selenium-ci added the C-java Java Bindings label Jul 23, 2026
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

Fix CSS escaping for non-ASCII leading digits in By.className()/By.id()

🐞 Bug fix 🧪 Tests 🕐 10-20 Minutes

Grey Divider

AI Description

• Restrict leading-digit CSS escaping to ASCII 0-9 to avoid Unicode digit collisions.
• Apply the same fix in both By and W3C command codec cssEscape implementations.
• Add regression tests for ASCII-leading digit escaping and non-ASCII digit pass-through.
Diagram

graph TD
  A["By.className()/By.id()"] --> B["By.java cssEscape()"] --> D["CSS selector value"]
  E["Remote commands"] --> C["W3CHttpCommandCodec cssEscape()"] --> D
  F["ByTest.java"] --> G["JSON serialization"] --> D
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize cssEscape in a shared utility
  • ➕ Eliminates duplicated escaping logic between By and W3CHttpCommandCodec
  • ➕ Reduces risk of future behavioral divergence and missed fixes
  • ➖ Larger refactor touching more call sites/APIs
  • ➖ May be harder to backport as a minimal bug fix
2. Implement full CSS identifier serializer (CSS.escape/serialize-an-identifier)
  • ➕ More comprehensive coverage of edge cases per spec
  • ➕ Could replace regex + special-casing with a single canonical implementation
  • ➖ Significantly larger change surface for a narrow bug
  • ➖ Higher risk of subtle compatibility regressions in existing escaping behavior

Recommendation: The PR’s minimal spec-correct fix (treat only ASCII '0'–'9' as “digit” for the leading-digit escape) is the best choice for a targeted collision bug and keeps existing behavior stable. Consider a follow-up refactor to deduplicate cssEscape to prevent future inconsistencies, but it’s reasonable to keep this PR narrowly scoped.

Files changed (3) +36 / -2

Bug fix (2) +10 / -2
By.javaFix leading-digit detection to only escape ASCII digits in cssEscape +5/-1

Fix leading-digit detection to only escape ASCII digits in cssEscape

• Replaces Character.isDigit-based detection with an ASCII '0'–'9' range check for the first character. Adds clarifying comments explaining why non-ASCII digits must not be escaped to avoid selector collisions.

java/src/org/openqa/selenium/By.java

W3CHttpCommandCodec.javaMirror ASCII-only leading digit escape logic in W3C codec cssEscape +5/-1

Mirror ASCII-only leading digit escape logic in W3C codec cssEscape

• Applies the same ASCII-only leading-digit check as By.java in the codec’s duplicated cssEscape implementation. Prevents non-ASCII digits from being rewritten into an ASCII-digit escape sequence.

java/src/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodec.java

Tests (1) +26 / -0
ByTest.javaAdd regression tests for ASCII and non-ASCII leading digit className serialization +26/-0

Add regression tests for ASCII and non-ASCII leading digit className serialization

• Adds a test ensuring a leading ASCII digit is escaped as a code point per CSS rules. Adds a test ensuring a leading Arabic-Indic digit is passed through unchanged and does not collide with ASCII '5' serialization.

java/test/org/openqa/selenium/ByTest.java

@qodo-code-review

qodo-code-review Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 18 rules

Grey Divider


Remediation recommended

1. Non-ASCII source literal 🐞 Bug ☼ Reliability
Description
W3CHttpCommandCodecTest embeds the Arabic-Indic digit ٥ directly in Java string literals; if the
compiler reads sources using a non-UTF-8 default encoding, this file may fail to compile or the
literal may be decoded incorrectly. Using \u0665 escapes makes the test encoding-agnostic and
aligns with existing test patterns in the repo.
Code

java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[R56-65]

+  void ensureLeadingNonAsciiDigitClassNameIsNotMisescapedAsADifferentAsciiDigit() {
+    // U+0665 (Arabic-Indic digit five) has numeric value 5, but is not an ASCII digit.
+    // It must be passed through as-is, not (mis)escaped to the same selector as an ASCII '5'.
+    Map<String, Object> arabicIndicFive = encodeFindElement("class name", "٥foo");
+    Map<String, Object> asciiFive = encodeFindElement("class name", "5foo");
+
+    assertThat(arabicIndicFive)
+        .containsEntry("using", "css selector")
+        .containsEntry("value", ".٥foo");
+    assertThat(arabicIndicFive.get("value")).isNotEqualTo(asciiFive.get("value"));
Evidence
The test hard-codes ٥ in both the input and expected selector value. The build’s Bazel
java_library wrapper shows no explicit -encoding being passed to javac, meaning source decoding
may depend on platform defaults. Existing i18n tests use \uXXXX escapes, demonstrating an
established approach to avoid encoding sensitivity.

java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[55-66]
java/private/java_library.bzl[6-50]
java/test/org/openqa/selenium/I18nTest.java[35-49]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`W3CHttpCommandCodecTest` contains raw non-ASCII characters (`٥`) in Java string literals. If source encoding is not consistently UTF-8, this can break compilation or alter the test input.

### Issue Context
The Bazel Java wrapper in this repo does not obviously enforce a `-encoding` flag, and other tests use `\uXXXX` escapes for non-ASCII strings.

### Fix Focus Areas
- java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[56-65]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Deprecated string helper used 🐞 Bug ⚙ Maintainability
Description
The new test calls deprecated Contents.string(HttpMessage) to read the request body, which adds
avoidable deprecation usage and future maintenance risk. HttpRequest already exposes
contentAsString(), so the test can parse the JSON body without the deprecated helper.
Code

java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[R68-72]

+  private Map<String, Object> encodeFindElement(String strategy, Object value) {
+    HttpRequest request =
+        codec.encode(new Command(sessionId, DriverCommand.FIND_ELEMENT(strategy, value)));
+    return json.toType(string(request), MAP_TYPE);
+  }
Evidence
The test imports and uses Contents.string(...). In Contents, the string(HttpMessage<?>)
overload is marked deprecated and recommends HttpMessage#contentAsString(), which is implemented
on HttpMessage and thus available on HttpRequest.

java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[20-23]
java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[68-72]
java/src/org/openqa/selenium/remote/http/Contents.java[121-138]
java/src/org/openqa/selenium/remote/http/HttpMessage.java[221-232]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`W3CHttpCommandCodecTest` uses deprecated `Contents.string(HttpMessage)`.

### Issue Context
`Contents.string(HttpMessage<?>)` is annotated `@Deprecated` and explicitly recommends using `HttpMessage#contentAsString()`.

### Fix Focus Areas
- java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[20-23]
- java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[68-72]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit ba21930 ⚖️ Balanced

Results up to commit 9a65035 ⚖️ Balanced


No changes from previous review

Qodo Logo

W3CHttpCommandCodec carries a byte-identical copy of the leading-digit
CSS-escape logic fixed in By.java, but had no test file exercising it
at all. ByTest/RemotableByTest only cover the By.java copy, since
By.toJson() pre-escapes client-side before anything reaches a command
codec, so a regression in this copy alone (e.g. from a future
de-duplication refactor) would go unnoticed.

Add W3CHttpCommandCodecTest exercising encode() for the "class name"/
"id" locator strategies directly, mirroring the ByTest cases. Verified
these tests fail against the pre-fix implementation and reproduce the
exact collision from the original report.
@diemol

diemol commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

Added a second commit: W3CHttpCommandCodec carries a byte-identical copy of the buggy cssEscape logic, but had no existing test file at all for that class. The original ByTest/RemotableByTest coverage only exercises the By.java copy, since By's locators pre-escape client-side before anything reaches a command codec — so a regression introduced only in W3CHttpCommandCodec (e.g. by a future de-duplication refactor) would have gone completely unnoticed by CI.

Added W3CHttpCommandCodecTest exercising encode() directly for the "class name"/"id" strategies. Verified locally that these new tests fail against the pre-fix implementation and reproduce the exact By.className("٥foo") vs By.className("5foo") collision from the issue, then pass again with the fix restored.

Comment on lines +56 to +65
void ensureLeadingNonAsciiDigitClassNameIsNotMisescapedAsADifferentAsciiDigit() {
// U+0665 (Arabic-Indic digit five) has numeric value 5, but is not an ASCII digit.
// It must be passed through as-is, not (mis)escaped to the same selector as an ASCII '5'.
Map<String, Object> arabicIndicFive = encodeFindElement("class name", "٥foo");
Map<String, Object> asciiFive = encodeFindElement("class name", "5foo");

assertThat(arabicIndicFive)
.containsEntry("using", "css selector")
.containsEntry("value", ".٥foo");
assertThat(arabicIndicFive.get("value")).isNotEqualTo(asciiFive.get("value"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Non-ascii source literal 🐞 Bug ☼ Reliability

W3CHttpCommandCodecTest embeds the Arabic-Indic digit ٥ directly in Java string literals; if the
compiler reads sources using a non-UTF-8 default encoding, this file may fail to compile or the
literal may be decoded incorrectly. Using \u0665 escapes makes the test encoding-agnostic and
aligns with existing test patterns in the repo.
Agent Prompt
### Issue description
`W3CHttpCommandCodecTest` contains raw non-ASCII characters (`٥`) in Java string literals. If source encoding is not consistently UTF-8, this can break compilation or alter the test input.

### Issue Context
The Bazel Java wrapper in this repo does not obviously enforce a `-encoding` flag, and other tests use `\uXXXX` escapes for non-ASCII strings.

### Fix Focus Areas
- java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[56-65]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +68 to +72
private Map<String, Object> encodeFindElement(String strategy, Object value) {
HttpRequest request =
codec.encode(new Command(sessionId, DriverCommand.FIND_ELEMENT(strategy, value)));
return json.toType(string(request), MAP_TYPE);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

2. Deprecated string helper used 🐞 Bug ⚙ Maintainability

The new test calls deprecated Contents.string(HttpMessage) to read the request body, which adds
avoidable deprecation usage and future maintenance risk. HttpRequest already exposes
contentAsString(), so the test can parse the JSON body without the deprecated helper.
Agent Prompt
### Issue description
`W3CHttpCommandCodecTest` uses deprecated `Contents.string(HttpMessage)`.

### Issue Context
`Contents.string(HttpMessage<?>)` is annotated `@Deprecated` and explicitly recommends using `HttpMessage#contentAsString()`.

### Fix Focus Areas
- java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[20-23]
- java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java[68-72]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-code-review

Copy link
Copy Markdown
Contributor

Code review by qodo was updated up to the latest commit ba21930

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

C-java Java Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: By.className()/By.id() escape a leading non-ASCII Unicode digit to the wrong ASCII code point, colliding with a different class name

2 participants