Skip to content
Merged
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
12 changes: 7 additions & 5 deletions mrbgems/mruby-regexp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ simulation) with backtracking fallback.
- `(?!...)` negative lookahead
- `(?<=...)` positive lookbehind (fixed-length only)
- `(?<!...)` negative lookbehind (fixed-length only)
- `(?>...)` atomic group
- `(?imx-imx)` options for the rest of the enclosing group,
`(?imx-imx:...)` options for the group's own body

Expand Down Expand Up @@ -145,13 +146,14 @@ $~ # last MatchData
The gem uses two execution engines:

**Pike VM (NFA simulation)**: Used for patterns without
backreferences, non-greedy quantifiers, or lookahead. Guarantees
O(pattern x text) time complexity, making it immune to ReDoS
attacks.
backreferences, non-greedy quantifiers, lookaround or atomic groups.
Guarantees O(pattern x text) time complexity, making it immune to
ReDoS attacks.

**Backtracking engine**: Used when patterns contain `\1`-`\9`
backreferences, non-greedy quantifiers (`*?`, `+?`, `??`), or
lookahead assertions (`(?=...)`, `(?!...)`). Protected by a
backreferences, non-greedy quantifiers (`*?`, `+?`, `??`),
lookaround assertions (`(?=...)`, `(?!...)`, `(?<=...)`, `(?<!...)`)
or atomic groups (`(?>...)`). Protected by a
configurable step limit (`MRB_REGEXP_STEP_LIMIT`, default 1M) to
prevent excessive backtracking.

Expand Down
7 changes: 7 additions & 0 deletions mrbgems/mruby-regexp/include/re_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ enum re_opcode {
The executor rewinds by bytes against a binary subject
and by characters otherwise, and the sub-pattern body
starts past this instruction, at pc + 2. */
RE_ATOMIC, /* atomic group (?>...): offset = nesting depth of the
group, 1 for an outermost one. The body follows and
ends at the RE_ATOMIC_END with the same depth; once
the body has matched, a failure after it fails the
whole group rather than backtracking into it. */
RE_ATOMIC_END, /* end of an atomic group's body: offset = the depth of
the RE_ATOMIC it closes */
};

/* Bytecode instruction (4 bytes each for alignment) */
Expand Down
24 changes: 23 additions & 1 deletion mrbgems/mruby-regexp/src/re_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ typedef struct {
mrb_bool has_backref;
mrb_bool needs_backtrack;
mrb_bool dont_capture; /* pattern declares a named group: plain (...) does not capture */
uint32_t atomic_depth; /* how many (?>...) groups enclose the parse point */
uint32_t atom_start; /* where the atom a quantifier binds to begins;
compile_quantified sets it to the position
before the atom, and a `\u{...}` list moves it
Expand Down Expand Up @@ -1272,6 +1273,25 @@ compile_atom(re_compiler *c)
c->flags = saved_flags;
break;
}
else if (c->p[1] == '>') {
/* atomic group (?>...): the body is a non-capturing group whose
first match is its only one. The two instructions bracketing it
carry the group's nesting depth, which is how the executor pairs
the end of a body with the group it closes when a failure after
the body has to fail the group; see bt_match(). The depth counts
instructions the pattern holds, so it fits the field. */
next_char(c); next_char(c); /* skip ?> */
c->atomic_depth++;
emit(c, RE_ATOMIC, 0, (uint16_t)c->atomic_depth);
compile_alt(c);
emit(c, RE_ATOMIC_END, 0, (uint16_t)c->atomic_depth);
c->atomic_depth--;
if (peek(c) != ')') compile_error(c, "unmatched '('");
next_char(c);
c->needs_backtrack = TRUE; /* the Pike VM cannot cut a thread */
c->flags = saved_flags;
break;
}
else if (c->p[1] == '\'' ||
(c->p[1] == '<' && c->p + 2 < c->src_end &&
c->p[2] != '=' && c->p[2] != '!')) {
Expand Down Expand Up @@ -1322,7 +1342,7 @@ compile_atom(re_compiler *c)
}
else {
/* (?X) with an unsupported X: not one of the recognized (?: (?= (?!
(?<= (?<! (?<name> (?'name' (?imx forms. Comment groups (?#...)
(?<= (?<! (?<name> (?'name' (?> (?imx forms. Comment groups (?#...)
never get here either, having been removed by
preprocess_pattern(). The absent operator (?~...) and
conditionals (?(...)) are not implemented. Raise here rather
Expand Down Expand Up @@ -2097,6 +2117,7 @@ first_set_walk(const re_inst *code, uint32_t code_len,
case RE_SAVE:
case RE_BOL: case RE_EOL: case RE_BOT: case RE_EOT: case RE_EOTNL:
case RE_WBOUND: case RE_NWBOUND:
case RE_ATOMIC: case RE_ATOMIC_END:
pc++;
continue; /* zero-width, keep walking */
case RE_JMP:
Expand Down Expand Up @@ -2158,6 +2179,7 @@ epsilon_path(const re_inst *code, uint32_t pc, uint32_t goal,
case RE_SAVE:
case RE_BOL: case RE_EOL: case RE_BOT: case RE_EOT: case RE_EOTNL:
case RE_WBOUND: case RE_NWBOUND:
case RE_ATOMIC: case RE_ATOMIC_END:
pc++;
break;
case RE_JMP:
Expand Down
Loading
Loading