Repository navigation
Conversation
StringSubstitutor.substitute() decided whether a variable prefix was
escaped by comparing the prefix position with the offset of the range
being substituted rather than with the start of the builder:
if (pos > offset && builder.charAt(pos - 1) == escapeCh) {
offset is the start of the processed range, not the start of the builder.
When a caller substitutes only part of a builder and the escape character
is the character directly before that range, pos equals offset for the
prefix, the test fails, and the escape is not recognised:
new StringSubstitutor(map, "${", "}", '~')
replaceIn(new TextStringBuilder("~${user}"), 0, 8) -> "${user}"
replaceIn(new TextStringBuilder("~${user}"), 1, 7) -> "~fred" (wrong)
The whole-string overloads pass offset 0, which is why they are not
affected.
An escape character is now honoured when it lies in the range being
substituted or immediately before it, since the character directly before
the range is the one that prefixes the range's first position. Behaviour
for offset 0 is unchanged, an escape character that is not the variable's
own escape is still left untouched, and an escape character inside the
range behaves as before.
Add StringSubstitutorTest.testSubstituteEscapeAtStartOfRange. The new
test fails on master with "expected: <${user}> but was: <~fred>" and
passes with this change.
Assisted-By: DeepSeek Harness (AI agent), reviewed by the submitter
Signed-off-by: simpleqt <2015900818@qq.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
StringSubstitutorignores the escape character when the variable's escape character is the first character of the range passed to the offset/length overloads, so an escaped variable is substituted anyway.Root cause
StringSubstitutor.substitute(TextStringBuilder, int offset, int length, List)decides whether a variable prefix is escaped with:offsetis the start of the range being processed, not the start of the builder. When a caller substitutes only part of a builder and the escape character is immediately before that range,pos == offsetfor the prefix, so the test fails and the escape is not recognised:replaceIn(builder, 0, length)on~${user}→${user}(correct)replaceIn(builder, 1, length - 1)on the same text →~fred(escape ignored, variable substituted)The whole-string overloads hide the bug because they pass
offset = 0.Reproduction
With
new StringSubstitutor(map, "${", "}", '~')andmap.put("user", "fred"):~here is the escape character; the caller's intent is that${user}is left literal, exactly as it is when the whole builder is processed.Fix
The escape character is now honoured when it lies in the range being substituted or immediately before it, because the character directly before the range is the one that prefixes the range's first position:
This keeps the existing behaviour for
offset == 0(and for ranges shorter than one character), and makes substitution of a sub-range agree with substitution of the whole builder. An escape character that is not the variable's own escape is still left untouched, and an escape character inside the range behaves as before.Tests
Added
StringSubstitutorTest.testSubstituteEscapeAtStartOfRange, which covers: the whole builder, the escape character immediately before the substituted range, the same followed by an unescaped variable, an escape character inside the range, and an unrelated escape character before the range that must be left alone.Evidence on the new test:
expected: <${user}> but was: <~fred>— 80 tests run, 1 failure.Full verification with the default
mvngoal: 2045 tests, 0 failures; checkstyle, PMD and SpotBugs all pass.Note: 6 tests in
PathFenceResolverTestandFileStringLookupTesterror on this Windows machine withIllegalArgumentException: 'other' has different root, because theirwrite(Path, String)helper doesCURRENT_PATH.toAbsolutePath().relativize(path.toAbsolutePath())and the@TempDiris onC:\while the working directory is onG:\. Those 6 errors are present on unmodifiedmasterand are unrelated to this change.Jira
CONTRIBUTING.mdasks for a Jira ticket, andRELEASE-NOTES.txtis generated from Jira. I do not have an Apache Jira account, so there is no ticket key to reference here; the commit message has no Jira key for that reason. Happy to update the commit message and add aRELEASE-NOTES.txtentry if a ticket is created and a key is provided.AI disclosure
An AI agent assisted with investigating, implementing and testing this change, and with drafting this description. The change was reviewed and verified by the submitter.