diff --git a/java/src/org/openqa/selenium/By.java b/java/src/org/openqa/selenium/By.java index ef28311f10157..24c890308e797 100644 --- a/java/src/org/openqa/selenium/By.java +++ b/java/src/org/openqa/selenium/By.java @@ -448,7 +448,11 @@ public final Parameters getRemoteParameters() { private String cssEscape(String using) { using = CSS_ESCAPE.matcher(using).replaceAll("\\\\$1"); - if (!using.isEmpty() && Character.isDigit(using.charAt(0))) { + // CSS only requires the leading-digit escape for ASCII 0-9; non-ASCII Unicode digits + // (e.g. Arabic-Indic, fullwidth) are already valid identifier-start code points and must + // be left untouched, or they collide with the escape for a different ASCII digit. + char first = using.isEmpty() ? '\0' : using.charAt(0); + if (first >= '0' && first <= '9') { using = "\\" + (30 + Integer.parseInt(using.substring(0, 1))) + " " + using.substring(1); } return using; diff --git a/java/src/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodec.java b/java/src/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodec.java index b7ad8623db030..eb04e54be920c 100644 --- a/java/src/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodec.java +++ b/java/src/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodec.java @@ -389,7 +389,11 @@ private Map asElement(Object id) { private String cssEscape(String using) { using = CSS_ESCAPE.matcher(using).replaceAll("\\\\$1"); - if (!using.isEmpty() && Character.isDigit(using.charAt(0))) { + // CSS only requires the leading-digit escape for ASCII 0-9; non-ASCII Unicode digits + // (e.g. Arabic-Indic, fullwidth) are already valid identifier-start code points and must + // be left untouched, or they collide with the escape for a different ASCII digit. + char first = using.isEmpty() ? '\0' : using.charAt(0); + if (first >= '0' && first <= '9') { using = "\\" + (30 + Integer.parseInt(using.substring(0, 1))) + " " + using.substring(1); } return using; diff --git a/java/test/org/openqa/selenium/ByTest.java b/java/test/org/openqa/selenium/ByTest.java index 46df1f203b7e3..19ce041c0bffd 100644 --- a/java/test/org/openqa/selenium/ByTest.java +++ b/java/test/org/openqa/selenium/ByTest.java @@ -122,4 +122,30 @@ void ensureIdIsSerializedProperly() { .containsEntry("using", "css selector") .containsEntry("value", "#one\\ two"); } + + @Test + void ensureLeadingAsciiDigitIsEscapedAsCodePoint() { + By by = By.className("5foo"); + + Json json = new Json(); + Map blob = json.toType(json.toJson(by), MAP_TYPE); + + assertThat(blob).containsEntry("using", "css selector").containsEntry("value", ".\\35 foo"); + } + + @Test + void ensureLeadingNonAsciiDigitIsNotMisescapedAsADifferentAsciiDigit() { + // U+0665 (Arabic-Indic digit five) has numeric value 5, but is not an ASCII digit. + // It is already a valid CSS identifier-start code point and must be passed through as-is, + // rather than being (mis)escaped to the same selector as an ASCII '5'. + By arabicIndicFive = By.className("٥foo"); + By asciiFive = By.className("5foo"); + + Json json = new Json(); + Map arabicBlob = json.toType(json.toJson(arabicIndicFive), MAP_TYPE); + Map asciiBlob = json.toType(json.toJson(asciiFive), MAP_TYPE); + + assertThat(arabicBlob).containsEntry("using", "css selector").containsEntry("value", ".٥foo"); + assertThat(arabicBlob.get("value")).isNotEqualTo(asciiBlob.get("value")); + } } diff --git a/java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java b/java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java new file mode 100644 index 0000000000000..22ff7e4e0809d --- /dev/null +++ b/java/test/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodecTest.java @@ -0,0 +1,73 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.remote.codec.w3c; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openqa.selenium.json.Json.MAP_TYPE; +import static org.openqa.selenium.remote.http.Contents.string; + +import java.util.Map; +import java.util.UUID; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.remote.Command; +import org.openqa.selenium.remote.DriverCommand; +import org.openqa.selenium.remote.SessionId; +import org.openqa.selenium.remote.http.HttpRequest; + +@Tag("UnitTests") +class W3CHttpCommandCodecTest { + + private final W3CHttpCommandCodec codec = new W3CHttpCommandCodec(); + private final SessionId sessionId = new SessionId(UUID.randomUUID()); + private final Json json = new Json(); + + @Test + void ensureLeadingAsciiDigitClassNameIsEscapedAsCodePoint() { + Map params = encodeFindElement("class name", "5foo"); + + assertThat(params).containsEntry("using", "css selector").containsEntry("value", ".\\35 foo"); + } + + @Test + void ensureLeadingAsciiDigitIdIsEscapedAsCodePoint() { + Map params = encodeFindElement("id", "5foo"); + + assertThat(params).containsEntry("using", "css selector").containsEntry("value", "#\\35 foo"); + } + + @Test + 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 arabicIndicFive = encodeFindElement("class name", "٥foo"); + Map asciiFive = encodeFindElement("class name", "5foo"); + + assertThat(arabicIndicFive) + .containsEntry("using", "css selector") + .containsEntry("value", ".٥foo"); + assertThat(arabicIndicFive.get("value")).isNotEqualTo(asciiFive.get("value")); + } + + private Map encodeFindElement(String strategy, Object value) { + HttpRequest request = + codec.encode(new Command(sessionId, DriverCommand.FIND_ELEMENT(strategy, value))); + return json.toType(string(request), MAP_TYPE); + } +}