diff --git a/src/test/resources/unit/regex/branch_reset_call_native_parity.t b/src/test/resources/unit/regex/branch_reset_call_native_parity.t new file mode 100644 index 000000000..6fefb32fb --- /dev/null +++ b/src/test/resources/unit/regex/branch_reset_call_native_parity.t @@ -0,0 +1,24 @@ +use strict; +use warnings; +use Test::More; + +sub check { + my ($pattern, $yes1, $yes2, $no1, $no2, $label) = @_; + my $regex = eval { qr/$pattern/ }; + ok(defined($regex), "$label compiles") or diag($@); + ok($yes1 =~ $regex, "$label first branch matches"); + is($1, substr($yes1, 0, 1), "$label first branch capture"); + ok($yes2 =~ $regex, "$label second branch matches"); + is($1, substr($yes2, 0, 1), "$label second branch capture"); + ok($no1 !~ $regex, "$label rejects first wrong call target"); + ok($no2 !~ $regex, "$label rejects second wrong call target"); +} + +check('(?|(?1)|(?2))(?&d)', '11', '21', '12', '22', + 'named ampersand'); +check('(?|(?1)|(?2))(?P>d)', '11', '21', '12', '22', + 'named Python'); +check('(?|(1)|(2))(?1)', '11', '21', '12', '22', + 'absolute numeric'); + +done_testing; diff --git a/src/test/resources/unit/regex/branch_reset_relative_call_native_parity.t b/src/test/resources/unit/regex/branch_reset_relative_call_native_parity.t new file mode 100644 index 000000000..47a90d49d --- /dev/null +++ b/src/test/resources/unit/regex/branch_reset_relative_call_native_parity.t @@ -0,0 +1,22 @@ +use strict; +use warnings; +use Test::More; + +my $regex = eval { qr/(*MARK:joni)((?|(?a)(?-1)|(?b)(?-1)|(?c)(?-1)))/ }; +ok(defined($regex), 'branch-reset relative-call pattern compiles') or diag($@); + +for my $case ( + ['aa', 'aa'], + ['bb', 'bb'], + ['cc', 'cc'], +) { + my ($input, $capture) = @$case; + ok($input =~ $regex, "$input uses its lexical branch capture"); + is($1, $capture, "$input preserves the outer capture"); +} + +for my $input (qw(ab bc ca)) { + ok($input !~ $regex, "$input rejects a different call target"); +} + +done_testing; diff --git a/third_party/joni/src/org/joni/Analyser.java b/third_party/joni/src/org/joni/Analyser.java index 2ed5b9bd5..3e0703738 100644 --- a/third_party/joni/src/org/joni/Analyser.java +++ b/third_party/joni/src/org/joni/Analyser.java @@ -1312,11 +1312,15 @@ protected final int subexpRecursiveCheckTrav(Node node) { } private void setCallAttr(CallNode cn) { - EncloseNode en = env.memNodes[cn.groupNum]; + EncloseNode en = cn.lexicalTarget != null + ? cn.lexicalTarget + : env.memNodes[cn.groupNum]; if (en == null) newValueException(UNDEFINED_NAME_REFERENCE, cn.nameP, cn.nameEnd); // Perl subroutine calls do not replace captures already visible in the // caller. Reused branch-reset numbers need the existing snapshot path. - if (env.isMultiplexMemNode(cn.groupNum)) cn.setRecursion(); + if (cn.lexicalTarget == null && env.isMultiplexMemNode(cn.groupNum)) { + cn.setRecursion(); + } en.setCalled(); cn.setTarget(en); env.btMemStart = BitStatus.bsOnAt(env.btMemStart, cn.groupNum); diff --git a/third_party/joni/src/org/joni/Parser.java b/third_party/joni/src/org/joni/Parser.java index 0c2c79b1f..cfcee0f48 100644 --- a/third_party/joni/src/org/joni/Parser.java +++ b/third_party/joni/src/org/joni/Parser.java @@ -61,11 +61,23 @@ class Parser extends Lexer { protected int returnCode; // return code used by parser methods (they itself return parsed nodes) // this approach will not affect recursive calls + private EncloseNode[] lexicalMemNodes; protected Parser(Regex regex, Syntax syntax, byte[]bytes, int p, int end, WarnCallback warnings) { super(regex, syntax, bytes, p, end, warnings); } + private void setLexicalMemNode(EncloseNode node) { + if (lexicalMemNodes == null) { + lexicalMemNodes = new EncloseNode[Config.SCANENV_MEMNODES_SIZE]; + } else if (node.regNum >= lexicalMemNodes.length) { + EncloseNode[] expanded = new EncloseNode[lexicalMemNodes.length << 1]; + System.arraycopy(lexicalMemNodes, 0, expanded, 0, lexicalMemNodes.length); + lexicalMemNodes = expanded; + } + lexicalMemNodes[node.regNum] = node; + } + private static final int POSIX_BRACKET_NAME_MIN_LEN = 4; private static final int POSIX_BRACKET_CHECK_LIMIT_LENGTH = 20; private static final byte[] BRACKET_END = ":]".getBytes(); @@ -888,6 +900,10 @@ private Node parseEnclose(TokenType term) { node = en; } + if (node instanceof EncloseNode en && en.type == EncloseType.MEMORY) { + setLexicalMemNode(en); + } + fetchToken(); Node target = parseSubExp(term); @@ -1750,12 +1766,16 @@ private BackRefNode newBackRef(int[]backRefs) { private Node parseCall() { int gNum = token.getCallGNum(); + boolean backwardRelative = gNum < 0; if (gNum < 0 || token.getCallRel()) { if (gNum > 0) gNum--; gNum = backrefRelToAbs(gNum); if (gNum <= 0) newValueException(INVALID_BACKREF); } - Node node = new CallNode(bytes, token.getCallNameP(), token.getCallNameEnd(), gNum); + CallNode node = new CallNode(bytes, token.getCallNameP(), token.getCallNameEnd(), gNum); + if (backwardRelative && lexicalMemNodes != null && gNum < lexicalMemNodes.length) { + node.lexicalTarget = lexicalMemNodes[gNum]; + } env.numCall++; return node; } diff --git a/third_party/joni/src/org/joni/ast/CallNode.java b/third_party/joni/src/org/joni/ast/CallNode.java index 1bc7f9e92..1035bc16d 100644 --- a/third_party/joni/src/org/joni/ast/CallNode.java +++ b/third_party/joni/src/org/joni/ast/CallNode.java @@ -27,6 +27,7 @@ public final class CallNode extends StateNode { public final int nameEnd; public int groupNum; + public EncloseNode lexicalTarget; public EncloseNode target; public UnsetAddrList unsetAddrList; diff --git a/third_party/joni/test/org/joni/test/TestPerlBranchResetRelativeCall.java b/third_party/joni/test/org/joni/test/TestPerlBranchResetRelativeCall.java new file mode 100644 index 000000000..5c7f86b41 --- /dev/null +++ b/third_party/joni/test/org/joni/test/TestPerlBranchResetRelativeCall.java @@ -0,0 +1,54 @@ +/* + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.joni.test; + +import static org.joni.constants.SyntaxProperties.ALLOW_MULTIPLEX_DEFINITION_NAME_CALL; +import static org.junit.Assert.assertEquals; + +import java.nio.charset.StandardCharsets; + +import org.jcodings.specific.UTF8Encoding; +import org.joni.Option; +import org.joni.Regex; +import org.joni.Syntax; +import org.junit.Test; + +public class TestPerlBranchResetRelativeCall { + private static final Syntax PERL_SYNTAX = new Syntax( + "PERL_TEST", Syntax.RUBY.op, Syntax.RUBY.op2, Syntax.RUBY.op3, + Syntax.RUBY.behavior | ALLOW_MULTIPLEX_DEFINITION_NAME_CALL, + Syntax.RUBY.options, Syntax.RUBY.metaCharTable); + + private static void assertMatches(String input) { + String pattern = "((?|(?a)\\g<-1>|(?b)\\g<-1>|(?c)\\g<-1>))"; + byte[] patternBytes = pattern.getBytes(StandardCharsets.UTF_8); + byte[] inputBytes = input.getBytes(StandardCharsets.UTF_8); + Regex regex = new Regex(patternBytes, 0, patternBytes.length, + Option.CAPTURE_GROUP, UTF8Encoding.INSTANCE, PERL_SYNTAX); + assertEquals(0, regex.matcher(inputBytes).search(0, inputBytes.length, Option.NONE)); + } + + @Test + public void relativeCallsUseTheirLexicalBranchResetCapture() { + assertMatches("aa"); + assertMatches("bb"); + assertMatches("cc"); + } +}