Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import org.joni.Syntax;

import static org.joni.constants.SyntaxProperties.ALLOW_MULTIPLEX_DEFINITION_NAME_CALL;
import static org.joni.constants.SyntaxProperties.OP2_ESC_H_HORIZONTAL_WHITESPACE;
import static org.joni.constants.SyntaxProperties.OP2_OPTION_PERL;
import static org.joni.constants.SyntaxProperties.OP2_OPTION_RUBY;
import static org.joni.constants.SyntaxProperties.OP2_PLUS_POSSESSIVE_INTERVAL;
Expand Down Expand Up @@ -45,7 +46,8 @@ final class JoniRegexPattern {
// by callouts and control verbs while changing only that default policy.
private static final Syntax PERLONJAVA_SYNTAX = new Syntax(
"PERLONJAVA", Syntax.RUBY.op,
(Syntax.RUBY.op2 & ~OP2_OPTION_RUBY) | OP2_OPTION_PERL | OP2_PLUS_POSSESSIVE_INTERVAL,
(Syntax.RUBY.op2 & ~OP2_OPTION_RUBY) | OP2_OPTION_PERL
| OP2_PLUS_POSSESSIVE_INTERVAL | OP2_ESC_H_HORIZONTAL_WHITESPACE,
Syntax.RUBY.op3,
Syntax.RUBY.behavior | ALLOW_MULTIPLEX_DEFINITION_NAME_CALL,
Syntax.RUBY.options & ~(Option.ASCII_RANGE
Expand Down
83 changes: 83 additions & 0 deletions src/test/resources/unit/regex/horizontal_whitespace_escape.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use strict;
use warnings;
use feature 'unicode_strings';
use Test::More;

my @horizontal = (0x09, 0x20, 0xA0, 0x1680, 0x2000, 0x200A,
0x202F, 0x205F, 0x3000);
my @other = (0x0A, 0x0B, 0x41, 0x85, 0x180E, 0x2028, 0x2029);

sub check_code_point {
my ($code, $expected, $upgraded) = @_;
my $character = chr($code);
if ($upgraded) {
utf8::upgrade($character);
}
else {
utf8::downgrade($character, 1)
or die sprintf "U+%04X cannot be represented as bytes", $code;
}
my $mode = $upgraded ? 'Unicode' : 'byte';
my $label = sprintf 'U+%04X %s', $code, $mode;

is($character =~ /\A\h\z/ ? 1 : 0, $expected,
"direct h $label");
is($character =~ /\A\H\z/ ? 1 : 0, 1 - $expected,
"direct H $label");
is($character =~ /\A[\h]\z/ ? 1 : 0, $expected,
"class h $label");
is($character =~ /\A[\H]\z/ ? 1 : 0, 1 - $expected,
"class H $label");
}

for my $code (@horizontal) {
check_code_point($code, 1, 1);
check_code_point($code, 1, 0) if $code <= 0xFF;
}
for my $code (@other) {
check_code_point($code, 0, 1);
check_code_point($code, 0, 0) if $code <= 0xFF;
}

my $horizontal_run = join '', map chr, @horizontal;
my $other_run = join '', map chr, @other;
ok($horizontal_run =~ /\A\h+\z/,
'direct h matches a run of horizontal whitespace');
ok($horizontal_run =~ /\A[\h]+\z/,
'class h matches a run of horizontal whitespace');
ok($other_run =~ /\A\H+\z/,
'direct H matches a run without horizontal whitespace');
ok($other_run =~ /\A[\H]+\z/,
'class H matches a run without horizontal whitespace');

my $ideographic_space = chr 0x3000;
my $line_feed = "\n";
ok($ideographic_space =~ /\A(?a:\h)\z/,
'scoped a keeps direct h Unicode-aware');
ok($ideographic_space =~ /\A(?aa:\h)\z/,
'scoped aa keeps direct h Unicode-aware');
ok($ideographic_space =~ /\A(?a:[\h])\z/,
'scoped a keeps class h Unicode-aware');
ok($ideographic_space =~ /\A(?aa:[\h])\z/,
'scoped aa keeps class h Unicode-aware');
ok($line_feed =~ /\A(?a:\H)\z/,
'scoped a keeps direct H complement semantics');
ok($line_feed =~ /\A(?aa:\H)\z/,
'scoped aa keeps direct H complement semantics');
ok($line_feed =~ /\A(?a:[\H])\z/,
'scoped a keeps class H complement semantics');
ok($line_feed =~ /\A(?aa:[\H])\z/,
'scoped aa keeps class H complement semantics');

my $byte_nbsp = chr 0xA0;
utf8::downgrade($byte_nbsp, 1);
ok($byte_nbsp =~ /\A(?a:\h)\z/,
'scoped a keeps direct h byte semantics');
ok($byte_nbsp =~ /\A(?aa:\h)\z/,
'scoped aa keeps direct h byte semantics');
ok($byte_nbsp =~ /\A(?a:[\h])\z/,
'scoped a keeps class h byte semantics');
ok($byte_nbsp =~ /\A(?aa:[\h])\z/,
'scoped aa keeps class h byte semantics');

done_testing();
124 changes: 120 additions & 4 deletions third_party/joni/src/org/joni/Lexer.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class Lexer extends ScannerSupport {
protected final ScanEnvironment env;
protected final Syntax syntax; // fast access to syntax
protected final Token token = new Token(); // current token
private int perlHorizontalWhitespaceTokenIndex = -1;
private boolean perlHorizontalWhitespaceNegated;
private boolean perlHorizontalWhitespaceSingleByte;
private int perlVerticalWhitespaceTokenIndex = -1;
private boolean perlVerticalWhitespaceNegated;

Expand Down Expand Up @@ -552,6 +555,84 @@ private void fetchTokenInCCFor_charType(boolean flag, int type) {
token.setPropNot(flag);
}

private void startPerlHorizontalWhitespace(boolean negated, TokenType openType) {
perlHorizontalWhitespaceTokenIndex = 0;
perlHorizontalWhitespaceNegated = negated;
perlHorizontalWhitespaceSingleByte = enc.isSingleByte();
token.type = openType;
}

private TokenType fetchPerlHorizontalWhitespaceToken() {
token.base = 0;
token.escaped = false;

int index = perlHorizontalWhitespaceTokenIndex++;
if (perlHorizontalWhitespaceNegated) {
if (index == 0) {
token.type = TokenType.CHAR;
token.setC('^');
return token.type;
}
index--;
}

if (perlHorizontalWhitespaceSingleByte && index >= 3) {
token.type = TokenType.CC_CLOSE;
token.setC(']');
perlHorizontalWhitespaceTokenIndex = -1;
return token.type;
}

switch (index) {
case 0:
token.type = TokenType.CODE_POINT;
token.setCode(0x09);
break;
case 1:
token.type = TokenType.CODE_POINT;
token.setCode(0x20);
break;
case 2:
token.type = TokenType.CODE_POINT;
token.setCode(0xa0);
break;
case 3:
token.type = TokenType.CODE_POINT;
token.setCode(0x1680);
break;
case 4:
token.type = TokenType.CODE_POINT;
token.setCode(0x2000);
break;
case 5:
token.type = TokenType.CC_RANGE;
token.setC('-');
break;
case 6:
token.type = TokenType.CODE_POINT;
token.setCode(0x200a);
break;
case 7:
token.type = TokenType.CODE_POINT;
token.setCode(0x202f);
break;
case 8:
token.type = TokenType.CODE_POINT;
token.setCode(0x205f);
break;
case 9:
token.type = TokenType.CODE_POINT;
token.setCode(0x3000);
break;
default:
token.type = TokenType.CC_CLOSE;
token.setC(']');
perlHorizontalWhitespaceTokenIndex = -1;
break;
}
return token.type;
}

private boolean usesPerlVerticalWhitespaceEscape() {
return syntax.op2EscVVerticalWhiteSpace()
|| syntax.op2OptionPerl() && (syntax == Syntax.Perl
Expand Down Expand Up @@ -914,6 +995,9 @@ private void fetchTokenInCCFor_and() {
}

protected final TokenType fetchTokenInCC() {
if (perlHorizontalWhitespaceTokenIndex >= 0) {
return fetchPerlHorizontalWhitespaceToken();
}
if (perlVerticalWhitespaceTokenIndex >= 0) {
return fetchPerlVerticalWhitespaceToken();
}
Expand Down Expand Up @@ -959,10 +1043,26 @@ protected final TokenType fetchTokenInCC() {
fetchTokenInCCFor_charType(true, CharacterType.SPACE);
break;
case 'h':
if (syntax.op2EscHXDigit()) fetchTokenInCCFor_charType(false, CharacterType.XDIGIT);
if (syntax.op2EscHHorizontalWhiteSpace()) {
if (enc.isSingleByte() || isAsciiRange(env.option)) {
startPerlHorizontalWhitespace(false, TokenType.CC_CC_OPEN);
} else {
fetchTokenInCCFor_charType(false, CharacterType.BLANK);
}
} else if (syntax.op2EscHXDigit()) {
fetchTokenInCCFor_charType(false, CharacterType.XDIGIT);
}
break;
case 'H':
if (syntax.op2EscHXDigit()) fetchTokenInCCFor_charType(true, CharacterType.XDIGIT);
if (syntax.op2EscHHorizontalWhiteSpace()) {
if (enc.isSingleByte() || isAsciiRange(env.option)) {
startPerlHorizontalWhitespace(true, TokenType.CC_CC_OPEN);
} else {
fetchTokenInCCFor_charType(true, CharacterType.BLANK);
}
} else if (syntax.op2EscHXDigit()) {
fetchTokenInCCFor_charType(true, CharacterType.XDIGIT);
}
break;
case 'p':
case 'P':
Expand Down Expand Up @@ -1418,10 +1518,26 @@ protected final void fetchToken() {
if (syntax.opEscDDigit()) fetchTokenInCCFor_charType(true, CharacterType.DIGIT);
break;
case 'h':
if (syntax.op2EscHXDigit()) fetchTokenInCCFor_charType(false, CharacterType.XDIGIT);
if (syntax.op2EscHHorizontalWhiteSpace()) {
if (enc.isSingleByte()) {
startPerlHorizontalWhitespace(false, TokenType.CC_OPEN);
} else {
fetchTokenInCCFor_charType(false, CharacterType.BLANK);
}
} else if (syntax.op2EscHXDigit()) {
fetchTokenInCCFor_charType(false, CharacterType.XDIGIT);
}
break;
case 'H':
if (syntax.op2EscHXDigit()) fetchTokenInCCFor_charType(true, CharacterType.XDIGIT);
if (syntax.op2EscHHorizontalWhiteSpace()) {
if (enc.isSingleByte()) {
startPerlHorizontalWhitespace(true, TokenType.CC_OPEN);
} else {
fetchTokenInCCFor_charType(true, CharacterType.BLANK);
}
} else if (syntax.op2EscHXDigit()) {
fetchTokenInCCFor_charType(true, CharacterType.XDIGIT);
}
break;
case 'A':
if (syntax.opEscAZBufAnchor()) fetchTokenFor_anchor(AnchorType.BEGIN_BUF);
Expand Down
Loading