mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
mruby-regexp: add built-in regexp engine with Pike VM
implement a lightweight NFA-based regular expression engine for mruby:
engine (src/re_compile.c, src/re_exec.c, src/re_utf8.c):
- Pike VM (Thompson NFA simulation) with O(n*m) time guarantee
- ReDoS-resistant by design (no backtracking for basic patterns)
- supports: literals, ., *, +, ?, {n,m}, [], [^], |, ()
- character classes: \d, \w, \s and negations
- anchors: ^, $, \A, \z, \Z, \b, \B
- flags: i (ignorecase), m (multiline/dotall)
- captures with MatchData
Ruby API (src/regexp.c, mrblib/string_regexp.rb):
- Regexp.new, #match, #match?, #=~, #===, #source, #inspect
- Regexp.escape, Regexp::IGNORECASE/MULTILINE constants
- MatchData#[], #captures, #to_a, #begin, #end, #pre_match, #post_match
- String#match, #match?, #=~, #sub, #gsub, #scan, #split
~1700 lines of C + ~120 lines of Ruby. no external dependencies.
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,9 @@ MRuby::Build.new('host') do |conf|
|
||||
# Generate mruby debugger command (require mruby-eval)
|
||||
conf.gem :core => "mruby-bin-debugger"
|
||||
|
||||
# Regexp
|
||||
conf.gem :core => "mruby-regexp"
|
||||
|
||||
# test
|
||||
conf.enable_test
|
||||
# bintest
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
** re_internal.h - internal definitions for regexp engine
|
||||
**
|
||||
** See Copyright Notice in mruby.h
|
||||
*/
|
||||
|
||||
#ifndef MRB_RE_INTERNAL_H
|
||||
#define MRB_RE_INTERNAL_H
|
||||
|
||||
#include <mruby.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* Bytecode instructions for the NFA engine */
|
||||
enum re_opcode {
|
||||
RE_CHAR, /* match literal byte: operand = byte value */
|
||||
RE_ANY, /* match any character (. without DOTALL) */
|
||||
RE_ANY_NL, /* match any character including newline (. with DOTALL) */
|
||||
RE_CLASS, /* match character class: operand = class_id */
|
||||
RE_NCLASS, /* match negated character class: operand = class_id */
|
||||
RE_MATCH, /* successful match */
|
||||
RE_JMP, /* unconditional jump: operand = target offset */
|
||||
RE_SPLIT, /* fork: operand = target offset (greedy: try next first) */
|
||||
RE_SPLITNG, /* fork: operand = target offset (non-greedy: try jump first) */
|
||||
RE_SAVE, /* save capture position: operand = slot number */
|
||||
RE_BOL, /* assert beginning of line (^) */
|
||||
RE_EOL, /* assert end of line ($) */
|
||||
RE_BOT, /* assert beginning of text (\A) */
|
||||
RE_EOT, /* assert end of text (\z) */
|
||||
RE_EOTNL, /* assert end of text or before final \n (\Z) */
|
||||
RE_WBOUND, /* assert word boundary (\b) */
|
||||
RE_NWBOUND, /* assert non-word boundary (\B) */
|
||||
RE_BACKREF, /* backreference: operand = group number */
|
||||
};
|
||||
|
||||
/* Bytecode instruction (4 bytes each for alignment) */
|
||||
typedef struct {
|
||||
uint8_t op;
|
||||
uint8_t a; /* small operand or class id */
|
||||
uint16_t offset; /* jump target or extended operand */
|
||||
} re_inst;
|
||||
|
||||
/* Character class bitmap (ASCII range) */
|
||||
#define RE_CLASS_BITMAP_SIZE 16 /* 128 bits = 16 bytes for ASCII */
|
||||
typedef struct {
|
||||
uint8_t bitmap[RE_CLASS_BITMAP_SIZE]; /* bitmap for 0-127 */
|
||||
mrb_bool negated;
|
||||
mrb_bool utf8_any; /* match any non-ASCII byte if true */
|
||||
} re_charclass;
|
||||
|
||||
/* Compiled regexp pattern */
|
||||
typedef struct mrb_regexp_pattern {
|
||||
re_inst *code; /* bytecode array */
|
||||
uint32_t code_len; /* number of instructions */
|
||||
re_charclass *classes; /* character class table */
|
||||
uint16_t num_classes;
|
||||
uint16_t num_captures; /* number of capture groups (including group 0) */
|
||||
uint32_t flags;
|
||||
mrb_bool has_backref; /* true if pattern uses \1-\9 */
|
||||
} mrb_regexp_pattern;
|
||||
|
||||
/* Regexp flags */
|
||||
#define RE_FLAG_IGNORECASE 1
|
||||
#define RE_FLAG_MULTILINE 2 /* ^ and $ match at \n boundaries */
|
||||
#define RE_FLAG_DOTALL 4 /* . matches \n (Ruby's /m for dot behavior) */
|
||||
|
||||
/* Note: Ruby's /m flag means BOTH multiline anchors AND dotall.
|
||||
Ruby's /i flag is ignorecase. */
|
||||
|
||||
/* Step limit for ReDoS protection */
|
||||
#ifndef MRB_REGEXP_STEP_LIMIT
|
||||
#define MRB_REGEXP_STEP_LIMIT 1000000
|
||||
#endif
|
||||
|
||||
/* Maximum captures */
|
||||
#define RE_MAX_CAPTURES 32
|
||||
|
||||
/* Compile a pattern string into bytecode */
|
||||
mrb_regexp_pattern* re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags);
|
||||
|
||||
/* Free a compiled pattern */
|
||||
void re_free(mrb_state *mrb, mrb_regexp_pattern *pat);
|
||||
|
||||
/* Execute a match.
|
||||
Returns number of captures filled (0 = no match).
|
||||
captures[2*n] = start, captures[2*n+1] = end for group n. */
|
||||
int re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
|
||||
const char *str, mrb_int len, mrb_int start,
|
||||
int *captures, int captures_size);
|
||||
|
||||
/* UTF-8 helpers */
|
||||
int re_utf8_charlen(const char *s, const char *end);
|
||||
uint32_t re_utf8_decode(const char *s, int *len);
|
||||
mrb_bool re_is_word_char(uint32_t c);
|
||||
|
||||
#endif /* MRB_RE_INTERNAL_H */
|
||||
@@ -0,0 +1,7 @@
|
||||
MRuby::Gem::Specification.new('mruby-regexp') do |spec|
|
||||
spec.license = 'MIT'
|
||||
spec.authors = 'mruby developers'
|
||||
spec.summary = 'Regexp class (built-in NFA engine)'
|
||||
|
||||
spec.add_dependency 'mruby-string-ext', :core => 'mruby-string-ext'
|
||||
end
|
||||
@@ -0,0 +1,126 @@
|
||||
class String
|
||||
def match(re, pos = 0)
|
||||
re = Regexp.new(re) if re.is_a?(String)
|
||||
re.match(self, pos)
|
||||
end
|
||||
|
||||
def match?(re, pos = 0)
|
||||
re = Regexp.new(re) if re.is_a?(String)
|
||||
re.match?(self, pos)
|
||||
end
|
||||
|
||||
def =~(re)
|
||||
re =~ self
|
||||
end
|
||||
|
||||
def sub(pattern, replacement = nil, &block)
|
||||
pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a?(String)
|
||||
md = pattern.match(self)
|
||||
return self.dup unless md
|
||||
|
||||
pre = md.pre_match
|
||||
post = md.post_match
|
||||
if block
|
||||
rep = block.call(md[0]).to_s
|
||||
else
|
||||
rep = replacement.to_s
|
||||
# handle \0, \1, etc. in replacement string
|
||||
rep = rep.gsub(/\\(\d)/) { md[$1.to_i] || "" } if rep.include?("\\")
|
||||
end
|
||||
pre + rep + post
|
||||
end
|
||||
|
||||
def gsub(pattern, replacement = nil, &block)
|
||||
pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a?(String)
|
||||
result = ""
|
||||
rest = self
|
||||
while rest.length > 0
|
||||
md = pattern.match(rest)
|
||||
break unless md
|
||||
result += md.pre_match
|
||||
if block
|
||||
result += block.call(md[0]).to_s
|
||||
else
|
||||
rep = replacement.to_s
|
||||
rep = rep.gsub(/\\(\d)/) { md[$1.to_i] || "" } if rep.include?("\\")
|
||||
result += rep
|
||||
end
|
||||
matched_len = md[0].length
|
||||
if matched_len == 0
|
||||
# avoid infinite loop on zero-length match
|
||||
result += rest[0] if rest.length > 0
|
||||
rest = rest[1..-1] || ""
|
||||
else
|
||||
rest = md.post_match
|
||||
end
|
||||
end
|
||||
result + rest
|
||||
end
|
||||
|
||||
def scan(pattern)
|
||||
pattern = Regexp.new(Regexp.escape(pattern)) if pattern.is_a?(String)
|
||||
result = []
|
||||
pos = 0
|
||||
while pos <= self.length
|
||||
md = pattern.match(self, pos)
|
||||
break unless md
|
||||
if md.captures.empty?
|
||||
result << md[0]
|
||||
elsif md.captures.length == 1
|
||||
result << md.captures[0]
|
||||
else
|
||||
result << md.captures
|
||||
end
|
||||
if md[0].length == 0
|
||||
pos = md.end(0) + 1
|
||||
else
|
||||
pos = md.end(0)
|
||||
end
|
||||
end
|
||||
if block_given?
|
||||
result.each { |m| yield m }
|
||||
self
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def split(pattern = nil, limit = -1)
|
||||
return super if pattern.nil?
|
||||
if pattern.is_a?(String)
|
||||
return super if pattern.length == 1 || !pattern.include?('\\')
|
||||
pattern = Regexp.new(Regexp.escape(pattern))
|
||||
end
|
||||
result = []
|
||||
rest = self
|
||||
count = 0
|
||||
while rest.length > 0
|
||||
if limit > 0 && count >= limit - 1
|
||||
result << rest
|
||||
return result
|
||||
end
|
||||
md = pattern.match(rest)
|
||||
break unless md
|
||||
result << md.pre_match
|
||||
rest = md.post_match
|
||||
count += 1
|
||||
# skip zero-length match at beginning
|
||||
if md[0].length == 0
|
||||
if rest.length > 0
|
||||
result[-1] = result[-1] + rest[0]
|
||||
rest = rest[1..-1] || ""
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
result << rest
|
||||
# remove trailing empty strings if no limit
|
||||
if limit < 0
|
||||
while result.length > 0 && result[-1] == ""
|
||||
result.pop
|
||||
end
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,611 @@
|
||||
/*
|
||||
** re_compile.c - regexp pattern compiler
|
||||
**
|
||||
** Compiles a regular expression pattern string into bytecode
|
||||
** for the NFA execution engine.
|
||||
**
|
||||
** See Copyright Notice in mruby.h
|
||||
*/
|
||||
|
||||
#include "re_internal.h"
|
||||
#include <mruby/error.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Compiler state */
|
||||
typedef struct {
|
||||
mrb_state *mrb;
|
||||
const char *src; /* pattern source */
|
||||
const char *src_end;
|
||||
const char *p; /* current position */
|
||||
re_inst *code; /* instruction array */
|
||||
uint32_t code_len;
|
||||
uint32_t code_capa;
|
||||
re_charclass *classes;
|
||||
uint16_t num_classes;
|
||||
uint16_t class_capa;
|
||||
uint16_t num_captures;
|
||||
uint32_t flags;
|
||||
mrb_bool has_backref;
|
||||
} re_compiler;
|
||||
|
||||
static void compile_alt(re_compiler *c); /* forward */
|
||||
|
||||
static void
|
||||
compile_error(re_compiler *c, const char *msg)
|
||||
{
|
||||
mrb_raisef(c->mrb, mrb_exc_get_id(c->mrb, MRB_SYM(RegexpError)), "%s: /%s/", msg, c->src);
|
||||
}
|
||||
|
||||
static uint32_t
|
||||
emit(re_compiler *c, uint8_t op, uint8_t a, uint16_t offset)
|
||||
{
|
||||
if (c->code_len >= c->code_capa) {
|
||||
c->code_capa = c->code_capa ? c->code_capa * 2 : 64;
|
||||
c->code = (re_inst*)mrb_realloc(c->mrb, c->code, sizeof(re_inst) * c->code_capa);
|
||||
}
|
||||
uint32_t pos = c->code_len++;
|
||||
c->code[pos].op = op;
|
||||
c->code[pos].a = a;
|
||||
c->code[pos].offset = offset;
|
||||
return pos;
|
||||
}
|
||||
|
||||
static void
|
||||
patch(re_compiler *c, uint32_t pos, uint16_t offset)
|
||||
{
|
||||
c->code[pos].offset = offset;
|
||||
}
|
||||
|
||||
/* Insert an instruction at position `pos` by shifting code.
|
||||
Adjusts all jump offsets >= pos by +1. */
|
||||
static void
|
||||
insert_inst(re_compiler *c, uint32_t pos, uint8_t op, uint8_t a, uint16_t offset)
|
||||
{
|
||||
emit(c, RE_JMP, 0, 0); /* grow array */
|
||||
uint32_t len = c->code_len - 1 - pos;
|
||||
memmove(&c->code[pos + 1], &c->code[pos], sizeof(re_inst) * len);
|
||||
c->code[pos].op = op;
|
||||
c->code[pos].a = a;
|
||||
c->code[pos].offset = offset;
|
||||
|
||||
/* fix all jump targets that point at or past the insertion point */
|
||||
for (uint32_t i = 0; i < c->code_len; i++) {
|
||||
if (i == pos) continue;
|
||||
switch (c->code[i].op) {
|
||||
case RE_JMP: case RE_SPLIT: case RE_SPLITNG:
|
||||
if (c->code[i].offset >= pos && c->code[i].offset < 0xffff) {
|
||||
c->code[i].offset++;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
peek(re_compiler *c)
|
||||
{
|
||||
if (c->p >= c->src_end) return -1;
|
||||
return (uint8_t)*c->p;
|
||||
}
|
||||
|
||||
static int
|
||||
next_char(re_compiler *c)
|
||||
{
|
||||
if (c->p >= c->src_end) return -1;
|
||||
return (uint8_t)*c->p++;
|
||||
}
|
||||
|
||||
static uint16_t
|
||||
add_class(re_compiler *c)
|
||||
{
|
||||
if (c->num_classes >= c->class_capa) {
|
||||
c->class_capa = c->class_capa ? c->class_capa * 2 : 8;
|
||||
c->classes = (re_charclass*)mrb_realloc(c->mrb, c->classes, sizeof(re_charclass) * c->class_capa);
|
||||
}
|
||||
uint16_t id = c->num_classes++;
|
||||
memset(&c->classes[id], 0, sizeof(re_charclass));
|
||||
return id;
|
||||
}
|
||||
|
||||
static void
|
||||
class_set_bit(re_charclass *cc, uint8_t ch)
|
||||
{
|
||||
if (ch < 128) {
|
||||
cc->bitmap[ch >> 3] |= (1 << (ch & 7));
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
class_set_range(re_charclass *cc, uint8_t lo, uint8_t hi)
|
||||
{
|
||||
for (int i = lo; i <= hi; i++) {
|
||||
class_set_bit(cc, (uint8_t)i);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
class_add_shorthand(re_charclass *cc, int ch)
|
||||
{
|
||||
switch (ch) {
|
||||
case 'd':
|
||||
class_set_range(cc, '0', '9');
|
||||
break;
|
||||
case 'D':
|
||||
class_set_range(cc, 0, '0'-1);
|
||||
class_set_range(cc, '9'+1, 127);
|
||||
cc->utf8_any = TRUE;
|
||||
break;
|
||||
case 'w':
|
||||
class_set_range(cc, 'a', 'z');
|
||||
class_set_range(cc, 'A', 'Z');
|
||||
class_set_range(cc, '0', '9');
|
||||
class_set_bit(cc, '_');
|
||||
break;
|
||||
case 'W':
|
||||
for (int i = 0; i < 128; i++) {
|
||||
if (!re_is_word_char(i)) class_set_bit(cc, (uint8_t)i);
|
||||
}
|
||||
cc->utf8_any = TRUE;
|
||||
break;
|
||||
case 's':
|
||||
class_set_bit(cc, ' ');
|
||||
class_set_bit(cc, '\t');
|
||||
class_set_bit(cc, '\n');
|
||||
class_set_bit(cc, '\r');
|
||||
class_set_bit(cc, '\f');
|
||||
class_set_bit(cc, '\v');
|
||||
break;
|
||||
case 'S':
|
||||
for (int i = 0; i < 128; i++) {
|
||||
if (i != ' ' && i != '\t' && i != '\n' && i != '\r' && i != '\f' && i != '\v')
|
||||
class_set_bit(cc, (uint8_t)i);
|
||||
}
|
||||
cc->utf8_any = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
parse_escape(re_compiler *c)
|
||||
{
|
||||
int ch = next_char(c);
|
||||
if (ch < 0) compile_error(c, "trailing backslash");
|
||||
switch (ch) {
|
||||
case 'n': return '\n';
|
||||
case 't': return '\t';
|
||||
case 'r': return '\r';
|
||||
case 'f': return '\f';
|
||||
case 'v': return '\v';
|
||||
case 'a': return '\a';
|
||||
case 'e': return 0x1b;
|
||||
default: return ch; /* literal: \., \\, \/, \(, etc. */
|
||||
}
|
||||
}
|
||||
|
||||
/* Parse [...] character class */
|
||||
static void
|
||||
compile_charclass(re_compiler *c)
|
||||
{
|
||||
uint16_t id = add_class(c);
|
||||
re_charclass *cc = &c->classes[id];
|
||||
mrb_bool negated = FALSE;
|
||||
|
||||
if (peek(c) == '^') {
|
||||
next_char(c);
|
||||
negated = TRUE;
|
||||
}
|
||||
|
||||
mrb_bool first = TRUE;
|
||||
while (peek(c) != ']' || first) {
|
||||
int ch;
|
||||
|
||||
if (peek(c) < 0) compile_error(c, "unterminated character class");
|
||||
first = FALSE;
|
||||
|
||||
if (peek(c) == '\\') {
|
||||
next_char(c);
|
||||
int esc = peek(c);
|
||||
if (esc == 'd' || esc == 'D' || esc == 'w' || esc == 'W' || esc == 's' || esc == 'S') {
|
||||
next_char(c);
|
||||
class_add_shorthand(cc, esc);
|
||||
continue;
|
||||
}
|
||||
ch = parse_escape(c);
|
||||
}
|
||||
else {
|
||||
ch = next_char(c);
|
||||
}
|
||||
|
||||
/* check for range a-z */
|
||||
if (peek(c) == '-' && c->p + 1 < c->src_end && c->p[1] != ']') {
|
||||
next_char(c); /* skip '-' */
|
||||
int hi;
|
||||
if (peek(c) == '\\') {
|
||||
next_char(c);
|
||||
hi = parse_escape(c);
|
||||
}
|
||||
else {
|
||||
hi = next_char(c);
|
||||
}
|
||||
if (ch < 128 && hi < 128) {
|
||||
class_set_range(cc, (uint8_t)ch, (uint8_t)hi);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (ch < 128) class_set_bit(cc, (uint8_t)ch);
|
||||
}
|
||||
}
|
||||
next_char(c); /* skip ']' */
|
||||
|
||||
cc->negated = negated;
|
||||
emit(c, negated ? RE_NCLASS : RE_CLASS, (uint8_t)id, 0);
|
||||
}
|
||||
|
||||
/* Parse {n}, {n,}, {n,m} quantifier. Returns min,max via pointers. */
|
||||
static mrb_bool
|
||||
parse_quantifier(re_compiler *c, int *min_out, int *max_out)
|
||||
{
|
||||
const char *save = c->p;
|
||||
int min = 0, max = -1;
|
||||
|
||||
while (peek(c) >= '0' && peek(c) <= '9') {
|
||||
min = min * 10 + (next_char(c) - '0');
|
||||
}
|
||||
if (peek(c) == ',') {
|
||||
next_char(c);
|
||||
if (peek(c) >= '0' && peek(c) <= '9') {
|
||||
max = 0;
|
||||
while (peek(c) >= '0' && peek(c) <= '9') {
|
||||
max = max * 10 + (next_char(c) - '0');
|
||||
}
|
||||
}
|
||||
/* else max = -1 (unlimited) */
|
||||
}
|
||||
else {
|
||||
max = min; /* {n} means exactly n */
|
||||
}
|
||||
if (peek(c) != '}') {
|
||||
c->p = save; /* not a quantifier, treat { as literal */
|
||||
return FALSE;
|
||||
}
|
||||
next_char(c); /* skip '}' */
|
||||
*min_out = min;
|
||||
*max_out = max;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/* Compile a single atom (character, class, group, etc.) */
|
||||
static void
|
||||
compile_atom(re_compiler *c)
|
||||
{
|
||||
int ch = peek(c);
|
||||
|
||||
switch (ch) {
|
||||
case '(':
|
||||
{
|
||||
next_char(c);
|
||||
mrb_bool capturing = TRUE;
|
||||
|
||||
if (peek(c) == '?' && c->p + 1 < c->src_end && c->p[1] == ':') {
|
||||
next_char(c); next_char(c); /* skip ?: */
|
||||
capturing = FALSE;
|
||||
}
|
||||
|
||||
uint16_t group = 0;
|
||||
if (capturing) {
|
||||
if (c->num_captures >= RE_MAX_CAPTURES) {
|
||||
compile_error(c, "too many capture groups");
|
||||
}
|
||||
group = c->num_captures++;
|
||||
emit(c, RE_SAVE, 0, group * 2);
|
||||
}
|
||||
|
||||
compile_alt(c);
|
||||
|
||||
if (peek(c) != ')') compile_error(c, "unmatched '('");
|
||||
next_char(c);
|
||||
|
||||
if (capturing) {
|
||||
emit(c, RE_SAVE, 0, group * 2 + 1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case '[':
|
||||
next_char(c);
|
||||
compile_charclass(c);
|
||||
break;
|
||||
|
||||
case '.':
|
||||
next_char(c);
|
||||
emit(c, (c->flags & RE_FLAG_DOTALL) ? RE_ANY_NL : RE_ANY, 0, 0);
|
||||
break;
|
||||
|
||||
case '^':
|
||||
next_char(c);
|
||||
emit(c, RE_BOL, 0, 0);
|
||||
break;
|
||||
|
||||
case '$':
|
||||
next_char(c);
|
||||
emit(c, RE_EOL, 0, 0);
|
||||
break;
|
||||
|
||||
case '\\':
|
||||
next_char(c);
|
||||
ch = peek(c);
|
||||
if (ch >= '1' && ch <= '9') {
|
||||
next_char(c);
|
||||
emit(c, RE_BACKREF, (uint8_t)(ch - '0'), 0);
|
||||
c->has_backref = TRUE;
|
||||
}
|
||||
else if (ch == 'd' || ch == 'D' || ch == 'w' || ch == 'W' || ch == 's' || ch == 'S') {
|
||||
next_char(c);
|
||||
uint16_t id = add_class(c);
|
||||
class_add_shorthand(&c->classes[id], ch);
|
||||
emit(c, (ch >= 'A' && ch <= 'Z') ? RE_NCLASS : RE_CLASS, (uint8_t)id, 0);
|
||||
}
|
||||
else if (ch == 'A') {
|
||||
next_char(c);
|
||||
emit(c, RE_BOT, 0, 0);
|
||||
}
|
||||
else if (ch == 'z') {
|
||||
next_char(c);
|
||||
emit(c, RE_EOT, 0, 0);
|
||||
}
|
||||
else if (ch == 'Z') {
|
||||
next_char(c);
|
||||
emit(c, RE_EOTNL, 0, 0);
|
||||
}
|
||||
else if (ch == 'b') {
|
||||
next_char(c);
|
||||
emit(c, RE_WBOUND, 0, 0);
|
||||
}
|
||||
else if (ch == 'B') {
|
||||
next_char(c);
|
||||
emit(c, RE_NWBOUND, 0, 0);
|
||||
}
|
||||
else {
|
||||
ch = parse_escape(c);
|
||||
if (c->flags & RE_FLAG_IGNORECASE) {
|
||||
if (ch >= 'A' && ch <= 'Z') {
|
||||
uint16_t id = add_class(c);
|
||||
class_set_bit(&c->classes[id], (uint8_t)ch);
|
||||
class_set_bit(&c->classes[id], (uint8_t)(ch + 32));
|
||||
emit(c, RE_CLASS, (uint8_t)id, 0);
|
||||
break;
|
||||
}
|
||||
else if (ch >= 'a' && ch <= 'z') {
|
||||
uint16_t id = add_class(c);
|
||||
class_set_bit(&c->classes[id], (uint8_t)ch);
|
||||
class_set_bit(&c->classes[id], (uint8_t)(ch - 32));
|
||||
emit(c, RE_CLASS, (uint8_t)id, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
emit(c, RE_CHAR, (uint8_t)ch, 0);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (ch < 0 || ch == ')' || ch == '|' || ch == '*' || ch == '+' || ch == '?' || ch == '{') {
|
||||
return; /* not an atom */
|
||||
}
|
||||
next_char(c);
|
||||
if ((c->flags & RE_FLAG_IGNORECASE) && ch < 128) {
|
||||
if (ch >= 'A' && ch <= 'Z') {
|
||||
uint16_t id = add_class(c);
|
||||
class_set_bit(&c->classes[id], (uint8_t)ch);
|
||||
class_set_bit(&c->classes[id], (uint8_t)(ch + 32));
|
||||
emit(c, RE_CLASS, (uint8_t)id, 0);
|
||||
break;
|
||||
}
|
||||
else if (ch >= 'a' && ch <= 'z') {
|
||||
uint16_t id = add_class(c);
|
||||
class_set_bit(&c->classes[id], (uint8_t)ch);
|
||||
class_set_bit(&c->classes[id], (uint8_t)(ch - 32));
|
||||
emit(c, RE_CLASS, (uint8_t)id, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
emit(c, RE_CHAR, (uint8_t)ch, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compile atom with quantifiers (*, +, ?, {n,m}) */
|
||||
static void
|
||||
compile_quantified(re_compiler *c)
|
||||
{
|
||||
uint32_t start = c->code_len;
|
||||
compile_atom(c);
|
||||
if (c->code_len == start) return; /* no atom emitted */
|
||||
|
||||
int ch = peek(c);
|
||||
if (ch == '*' || ch == '+' || ch == '?') {
|
||||
next_char(c);
|
||||
mrb_bool nongreedy = (peek(c) == '?');
|
||||
if (nongreedy) next_char(c);
|
||||
|
||||
uint32_t atom_len = c->code_len - start;
|
||||
|
||||
if (ch == '*') {
|
||||
/* e* → L: SPLIT(body, end); body; JMP L; end:
|
||||
SPLIT offset = end (after JMP), patched after JMP is emitted */
|
||||
insert_inst(c, start, nongreedy ? RE_SPLITNG : RE_SPLIT, 0, 0);
|
||||
emit(c, RE_JMP, 0, start);
|
||||
c->code[start].offset = (uint16_t)c->code_len; /* patch: skip to end */
|
||||
}
|
||||
else if (ch == '+') {
|
||||
/* e+ → body; SPLIT/SPLITNG(start)
|
||||
SPLIT: first=pc+1(end), second=offset(start) → non-greedy
|
||||
SPLITNG: first=offset(start), second=pc+1(end) → greedy */
|
||||
emit(c, nongreedy ? RE_SPLIT : RE_SPLITNG, 0, start);
|
||||
}
|
||||
else { /* ? */
|
||||
/* e? → SPLIT(body, end); body; end: */
|
||||
insert_inst(c, start, nongreedy ? RE_SPLITNG : RE_SPLIT, 0, 0);
|
||||
c->code[start].offset = (uint16_t)c->code_len; /* patch: skip to end */
|
||||
}
|
||||
}
|
||||
else if (ch == '{') {
|
||||
const char *save = c->p;
|
||||
next_char(c);
|
||||
int min, max;
|
||||
if (!parse_quantifier(c, &min, &max)) {
|
||||
c->p = save;
|
||||
return; /* not a quantifier */
|
||||
}
|
||||
mrb_bool nongreedy = (peek(c) == '?');
|
||||
if (nongreedy) next_char(c);
|
||||
|
||||
/* For {n,m}: repeat atom min times, then optional (max-min) times */
|
||||
uint32_t atom_end = c->code_len;
|
||||
uint32_t atom_size = atom_end - start;
|
||||
|
||||
/* First, we have one copy already. We need min-1 more mandatory copies. */
|
||||
for (int i = 1; i < min; i++) {
|
||||
for (uint32_t j = 0; j < atom_size; j++) {
|
||||
emit(c, c->code[start + j].op, c->code[start + j].a, c->code[start + j].offset);
|
||||
}
|
||||
}
|
||||
/* Then optional copies */
|
||||
if (max < 0) {
|
||||
/* {n,} = min copies + * */
|
||||
uint32_t loop_start = c->code_len;
|
||||
uint32_t split_pos = emit(c, nongreedy ? RE_SPLITNG : RE_SPLIT, 0, 0);
|
||||
for (uint32_t j = 0; j < atom_size; j++) {
|
||||
emit(c, c->code[start + j].op, c->code[start + j].a, c->code[start + j].offset);
|
||||
}
|
||||
emit(c, RE_JMP, 0, loop_start);
|
||||
patch(c, split_pos, c->code_len);
|
||||
}
|
||||
else {
|
||||
for (int i = min; i < max; i++) {
|
||||
uint32_t split_pos = emit(c, nongreedy ? RE_SPLITNG : RE_SPLIT, 0, 0);
|
||||
for (uint32_t j = 0; j < atom_size; j++) {
|
||||
emit(c, c->code[start + j].op, c->code[start + j].a, c->code[start + j].offset);
|
||||
}
|
||||
patch(c, split_pos, c->code_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Compile a sequence of quantified atoms */
|
||||
static void
|
||||
compile_seq(re_compiler *c)
|
||||
{
|
||||
while (peek(c) >= 0 && peek(c) != ')' && peek(c) != '|') {
|
||||
compile_quantified(c);
|
||||
}
|
||||
}
|
||||
|
||||
/* Compile alternation: seq | seq | ... */
|
||||
static void
|
||||
compile_alt(re_compiler *c)
|
||||
{
|
||||
uint32_t alt_start = c->code_len;
|
||||
compile_seq(c);
|
||||
|
||||
if (peek(c) != '|') return;
|
||||
|
||||
/* a|b → SPLIT L1 L2; L1: a; JMP END; L2: b; END:
|
||||
We need to insert SPLIT before already-emitted code for first alt.
|
||||
Strategy: emit JMP after first alt, then for each subsequent alt,
|
||||
insert a SPLIT before it by shifting code. */
|
||||
|
||||
/* Collect all alternatives, then emit SPLIT chain at the end.
|
||||
This avoids insert_inst offset corruption for multi-way alternation. */
|
||||
uint32_t alt_starts[64]; /* start positions of each alternative */
|
||||
int num_alts = 0;
|
||||
alt_starts[num_alts++] = alt_start;
|
||||
|
||||
while (peek(c) == '|') {
|
||||
next_char(c);
|
||||
emit(c, RE_JMP, 0, 0); /* placeholder: jump to end */
|
||||
alt_starts[num_alts++] = c->code_len;
|
||||
if (num_alts >= 64) compile_error(c, "too many alternatives");
|
||||
compile_seq(c);
|
||||
}
|
||||
|
||||
if (num_alts <= 1) return; /* shouldn't happen, but safety */
|
||||
|
||||
/* Now insert SPLIT chain before the alternatives.
|
||||
For n alternatives: n-1 SPLIT instructions, each pointing to
|
||||
their respective alternative. */
|
||||
uint32_t split_count = (uint32_t)(num_alts - 1);
|
||||
/* Insert split_count instructions at alt_starts[0] */
|
||||
for (uint32_t i = 0; i < split_count; i++) {
|
||||
insert_inst(c, alt_starts[0], RE_JMP, 0, 0); /* placeholder */
|
||||
/* adjust all alt_starts by +1 due to insertion */
|
||||
for (int j = 0; j < num_alts; j++) {
|
||||
alt_starts[j]++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Now set up SPLIT chain: each SPLIT tries next instruction or jumps to alt */
|
||||
for (uint32_t i = 0; i < split_count; i++) {
|
||||
uint32_t pos = alt_starts[0] - split_count + i;
|
||||
c->code[pos].op = RE_SPLIT;
|
||||
c->code[pos].a = 0;
|
||||
c->code[pos].offset = (uint16_t)alt_starts[i + 1];
|
||||
}
|
||||
|
||||
/* Patch JMPs (they are right before each alt_starts[1..n-1]) to point to end */
|
||||
uint32_t end = c->code_len;
|
||||
for (int i = 1; i < num_alts; i++) {
|
||||
uint32_t jmp_pos = alt_starts[i] - 1;
|
||||
c->code[jmp_pos].op = RE_JMP;
|
||||
c->code[jmp_pos].offset = (uint16_t)end;
|
||||
}
|
||||
}
|
||||
|
||||
mrb_regexp_pattern*
|
||||
re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags)
|
||||
{
|
||||
re_compiler c;
|
||||
memset(&c, 0, sizeof(c));
|
||||
c.mrb = mrb;
|
||||
c.src = pattern;
|
||||
c.src_end = pattern + len;
|
||||
c.p = pattern;
|
||||
c.flags = flags;
|
||||
c.num_captures = 1; /* group 0 = whole match */
|
||||
|
||||
/* group 0 start */
|
||||
emit(&c, RE_SAVE, 0, 0);
|
||||
|
||||
compile_alt(&c);
|
||||
|
||||
if (c.p < c.src_end) {
|
||||
compile_error(&c, "unmatched ')'");
|
||||
}
|
||||
|
||||
/* group 0 end */
|
||||
emit(&c, RE_SAVE, 0, 1);
|
||||
emit(&c, RE_MATCH, 0, 0);
|
||||
|
||||
mrb_regexp_pattern *pat = (mrb_regexp_pattern*)mrb_malloc(mrb, sizeof(mrb_regexp_pattern));
|
||||
pat->code = c.code;
|
||||
pat->code_len = c.code_len;
|
||||
pat->classes = c.classes;
|
||||
pat->num_classes = c.num_classes;
|
||||
pat->num_captures = c.num_captures;
|
||||
pat->flags = flags;
|
||||
pat->has_backref = c.has_backref;
|
||||
|
||||
return pat;
|
||||
}
|
||||
|
||||
void
|
||||
re_free(mrb_state *mrb, mrb_regexp_pattern *pat)
|
||||
{
|
||||
if (pat) {
|
||||
mrb_free(mrb, pat->code);
|
||||
mrb_free(mrb, pat->classes);
|
||||
mrb_free(mrb, pat);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
** re_exec.c - NFA execution engine (Pike VM)
|
||||
**
|
||||
** Executes compiled regexp bytecode using Thompson/Pike NFA simulation.
|
||||
** O(pattern * text) time complexity guarantees ReDoS resistance.
|
||||
**
|
||||
** See Copyright Notice in mruby.h
|
||||
*/
|
||||
|
||||
#include "re_internal.h"
|
||||
#include <string.h>
|
||||
|
||||
/* NFA thread: a position in the bytecode + captured positions */
|
||||
typedef struct {
|
||||
uint32_t pc;
|
||||
int captures[RE_MAX_CAPTURES * 2];
|
||||
} re_thread;
|
||||
|
||||
/* Thread list for NFA simulation */
|
||||
typedef struct {
|
||||
re_thread *threads;
|
||||
int count;
|
||||
int capa;
|
||||
} re_threadlist;
|
||||
|
||||
/* Match result: updated when RE_MATCH is reached during epsilon traversal */
|
||||
typedef struct {
|
||||
mrb_bool matched;
|
||||
int captures[RE_MAX_CAPTURES * 2];
|
||||
} re_match_result;
|
||||
|
||||
static void
|
||||
threadlist_init(mrb_state *mrb, re_threadlist *l, int capa)
|
||||
{
|
||||
l->threads = (re_thread*)mrb_malloc(mrb, sizeof(re_thread) * capa);
|
||||
l->count = 0;
|
||||
l->capa = capa;
|
||||
}
|
||||
|
||||
static void
|
||||
threadlist_free(mrb_state *mrb, re_threadlist *l)
|
||||
{
|
||||
mrb_free(mrb, l->threads);
|
||||
}
|
||||
|
||||
/* Add a thread, following epsilon transitions (JMP, SPLIT, SAVE, assertions).
|
||||
visited[] prevents adding duplicate threads at the same pc.
|
||||
When RE_MATCH is reached, records in result and does NOT add to thread list. */
|
||||
static void
|
||||
add_thread(const mrb_regexp_pattern *pat, re_threadlist *list,
|
||||
re_thread t, const char *str, const char *sp, const char *str_end,
|
||||
uint8_t *visited, re_match_result *result)
|
||||
{
|
||||
for (;;) {
|
||||
if (t.pc >= pat->code_len) return;
|
||||
if (visited[t.pc]) return;
|
||||
visited[t.pc] = 1;
|
||||
|
||||
re_inst inst = pat->code[t.pc];
|
||||
switch (inst.op) {
|
||||
case RE_JMP:
|
||||
t.pc = inst.offset;
|
||||
continue;
|
||||
|
||||
case RE_SPLIT:
|
||||
/* greedy: try pc+1 first, then jump target */
|
||||
{
|
||||
re_thread t2 = t;
|
||||
t2.pc = inst.offset;
|
||||
add_thread(pat, list, t2, str, sp, str_end, visited, result);
|
||||
}
|
||||
t.pc++;
|
||||
continue;
|
||||
|
||||
case RE_SPLITNG:
|
||||
/* non-greedy: try jump target first, then pc+1 */
|
||||
{
|
||||
re_thread t2 = t;
|
||||
t2.pc = t.pc + 1;
|
||||
add_thread(pat, list, t2, str, sp, str_end, visited, result);
|
||||
}
|
||||
t.pc = inst.offset;
|
||||
continue;
|
||||
|
||||
case RE_SAVE:
|
||||
t.captures[inst.offset] = (int)(sp - str);
|
||||
t.pc++;
|
||||
continue;
|
||||
|
||||
case RE_BOL:
|
||||
if (sp == str || ((pat->flags & RE_FLAG_MULTILINE) && sp > str && sp[-1] == '\n')) {
|
||||
t.pc++; continue;
|
||||
}
|
||||
return;
|
||||
|
||||
case RE_EOL:
|
||||
if (sp == str_end || ((pat->flags & RE_FLAG_MULTILINE) && *sp == '\n')) {
|
||||
t.pc++; continue;
|
||||
}
|
||||
return;
|
||||
|
||||
case RE_BOT:
|
||||
if (sp == str) { t.pc++; continue; }
|
||||
return;
|
||||
|
||||
case RE_EOT:
|
||||
if (sp == str_end) { t.pc++; continue; }
|
||||
return;
|
||||
|
||||
case RE_EOTNL:
|
||||
if (sp == str_end || (sp + 1 == str_end && *sp == '\n')) { t.pc++; continue; }
|
||||
return;
|
||||
|
||||
case RE_WBOUND:
|
||||
{
|
||||
mrb_bool before = (sp > str) && re_is_word_char((uint8_t)sp[-1]);
|
||||
mrb_bool after = (sp < str_end) && re_is_word_char((uint8_t)*sp);
|
||||
if (before != after) { t.pc++; continue; }
|
||||
}
|
||||
return;
|
||||
|
||||
case RE_NWBOUND:
|
||||
{
|
||||
mrb_bool before = (sp > str) && re_is_word_char((uint8_t)sp[-1]);
|
||||
mrb_bool after = (sp < str_end) && re_is_word_char((uint8_t)*sp);
|
||||
if (before == after) { t.pc++; continue; }
|
||||
}
|
||||
return;
|
||||
|
||||
case RE_MATCH:
|
||||
/* match found during epsilon traversal.
|
||||
always update: greedy quantifiers may find longer matches later
|
||||
at the same starting position. thread priority ensures correctness. */
|
||||
if (result) {
|
||||
result->matched = TRUE;
|
||||
memcpy(result->captures, t.captures, sizeof(t.captures));
|
||||
}
|
||||
return; /* don't add to thread list */
|
||||
|
||||
default:
|
||||
/* consuming instruction: add to thread list */
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
/* add to thread list */
|
||||
if (list->count < list->capa) {
|
||||
list->threads[list->count++] = t;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if character matches a character class */
|
||||
static mrb_bool
|
||||
class_match(const re_charclass *cc, uint8_t ch)
|
||||
{
|
||||
if (ch >= 128) return cc->utf8_any;
|
||||
return (cc->bitmap[ch >> 3] >> (ch & 7)) & 1;
|
||||
}
|
||||
|
||||
/* Pike VM: NFA simulation with submatch tracking */
|
||||
int
|
||||
re_exec(mrb_state *mrb, const mrb_regexp_pattern *pat,
|
||||
const char *str, mrb_int len, mrb_int start,
|
||||
int *captures, int captures_size)
|
||||
{
|
||||
const char *sp = str + start;
|
||||
const char *str_end = str + len;
|
||||
int ncap = pat->num_captures * 2;
|
||||
|
||||
int list_capa = (int)pat->code_len * 2 + 16;
|
||||
re_threadlist curr, next;
|
||||
threadlist_init(mrb, &curr, list_capa);
|
||||
threadlist_init(mrb, &next, list_capa);
|
||||
|
||||
uint8_t *visited = (uint8_t*)mrb_calloc(mrb, 1, pat->code_len + 1);
|
||||
|
||||
re_match_result result;
|
||||
result.matched = FALSE;
|
||||
memset(result.captures, -1, sizeof(result.captures));
|
||||
|
||||
for (; sp <= str_end; sp++) {
|
||||
/* Add a new initial thread at current position (unanchored search) */
|
||||
if (!result.matched) {
|
||||
re_thread t0;
|
||||
memset(t0.captures, -1, sizeof(t0.captures));
|
||||
t0.pc = 0;
|
||||
memset(visited, 0, pat->code_len + 1);
|
||||
add_thread(pat, &curr, t0, str, sp, str_end, visited, &result);
|
||||
/* if match found during epsilon traversal (empty pattern), done */
|
||||
if (result.matched && curr.count == 0) break;
|
||||
}
|
||||
|
||||
if (sp >= str_end) break;
|
||||
|
||||
/* Process all current threads against current character */
|
||||
memset(visited, 0, pat->code_len + 1);
|
||||
next.count = 0;
|
||||
|
||||
int ch = (uint8_t)*sp;
|
||||
int advance = re_utf8_charlen(sp, str_end);
|
||||
|
||||
for (int i = 0; i < curr.count; i++) {
|
||||
re_thread *th = &curr.threads[i];
|
||||
if (th->pc >= pat->code_len) continue;
|
||||
|
||||
re_inst inst = pat->code[th->pc];
|
||||
switch (inst.op) {
|
||||
case RE_CHAR:
|
||||
if (ch == inst.a) {
|
||||
th->pc++;
|
||||
add_thread(pat, &next, *th, str, sp + 1, str_end, visited, &result);
|
||||
}
|
||||
break;
|
||||
|
||||
case RE_ANY:
|
||||
if (ch != '\n') {
|
||||
th->pc++;
|
||||
add_thread(pat, &next, *th, str, sp + advance, str_end, visited, &result);
|
||||
}
|
||||
break;
|
||||
|
||||
case RE_ANY_NL:
|
||||
th->pc++;
|
||||
add_thread(pat, &next, *th, str, sp + advance, str_end, visited, &result);
|
||||
break;
|
||||
|
||||
case RE_CLASS:
|
||||
if (class_match(&pat->classes[inst.a], (uint8_t)ch)) {
|
||||
th->pc++;
|
||||
add_thread(pat, &next, *th, str, sp + advance, str_end, visited, &result);
|
||||
}
|
||||
break;
|
||||
|
||||
case RE_NCLASS:
|
||||
if (!class_match(&pat->classes[inst.a], (uint8_t)ch)) {
|
||||
th->pc++;
|
||||
add_thread(pat, &next, *th, str, sp + advance, str_end, visited, &result);
|
||||
}
|
||||
break;
|
||||
|
||||
case RE_BACKREF:
|
||||
/* TODO: backtracking for backreferences */
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* swap curr and next */
|
||||
{
|
||||
re_threadlist tmp = curr;
|
||||
curr = next;
|
||||
next = tmp;
|
||||
}
|
||||
|
||||
/* if matched and no more threads, we're done */
|
||||
if (result.matched && curr.count == 0) break;
|
||||
}
|
||||
|
||||
threadlist_free(mrb, &curr);
|
||||
threadlist_free(mrb, &next);
|
||||
mrb_free(mrb, visited);
|
||||
|
||||
if (result.matched && captures) {
|
||||
int copy = ncap < captures_size ? ncap : captures_size;
|
||||
memcpy(captures, result.captures, sizeof(int) * copy);
|
||||
}
|
||||
return result.matched ? (ncap > 0 ? ncap : 1) : 0;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
** re_utf8.c - UTF-8 utility functions for regexp engine
|
||||
**
|
||||
** See Copyright Notice in mruby.h
|
||||
*/
|
||||
|
||||
#include "re_internal.h"
|
||||
|
||||
/* Return byte length of UTF-8 character at s.
|
||||
Returns 1 for invalid sequences (treat as single byte). */
|
||||
int
|
||||
re_utf8_charlen(const char *s, const char *end)
|
||||
{
|
||||
uint8_t c = (uint8_t)*s;
|
||||
int len;
|
||||
|
||||
if (c < 0x80) return 1;
|
||||
else if (c < 0xc0) return 1; /* invalid continuation */
|
||||
else if (c < 0xe0) len = 2;
|
||||
else if (c < 0xf0) len = 3;
|
||||
else if (c < 0xf8) len = 4;
|
||||
else return 1; /* invalid */
|
||||
|
||||
if (s + len > end) return 1; /* truncated */
|
||||
return len;
|
||||
}
|
||||
|
||||
/* Decode a UTF-8 character and return its codepoint.
|
||||
*len is set to the byte length consumed. */
|
||||
uint32_t
|
||||
re_utf8_decode(const char *s, int *len)
|
||||
{
|
||||
uint8_t c = (uint8_t)s[0];
|
||||
uint32_t cp;
|
||||
|
||||
if (c < 0x80) {
|
||||
*len = 1;
|
||||
return c;
|
||||
}
|
||||
else if (c < 0xc0) {
|
||||
*len = 1;
|
||||
return c; /* invalid, return as-is */
|
||||
}
|
||||
else if (c < 0xe0) {
|
||||
*len = 2;
|
||||
cp = (c & 0x1f) << 6;
|
||||
cp |= ((uint8_t)s[1] & 0x3f);
|
||||
return cp;
|
||||
}
|
||||
else if (c < 0xf0) {
|
||||
*len = 3;
|
||||
cp = (c & 0x0f) << 12;
|
||||
cp |= ((uint8_t)s[1] & 0x3f) << 6;
|
||||
cp |= ((uint8_t)s[2] & 0x3f);
|
||||
return cp;
|
||||
}
|
||||
else {
|
||||
*len = 4;
|
||||
cp = (c & 0x07) << 18;
|
||||
cp |= ((uint8_t)s[1] & 0x3f) << 12;
|
||||
cp |= ((uint8_t)s[2] & 0x3f) << 6;
|
||||
cp |= ((uint8_t)s[3] & 0x3f);
|
||||
return cp;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if character is a "word" character (\w): [a-zA-Z0-9_] */
|
||||
mrb_bool
|
||||
re_is_word_char(uint32_t c)
|
||||
{
|
||||
if (c >= 'a' && c <= 'z') return TRUE;
|
||||
if (c >= 'A' && c <= 'Z') return TRUE;
|
||||
if (c >= '0' && c <= '9') return TRUE;
|
||||
if (c == '_') return TRUE;
|
||||
return FALSE;
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
/*
|
||||
** regexp.c - Regexp class and MatchData class
|
||||
**
|
||||
** See Copyright Notice in mruby.h
|
||||
*/
|
||||
|
||||
#include <mruby.h>
|
||||
#include <mruby/class.h>
|
||||
#include <mruby/data.h>
|
||||
#include <mruby/string.h>
|
||||
#include <mruby/array.h>
|
||||
#include <mruby/variable.h>
|
||||
#include <mruby/error.h>
|
||||
#include "re_internal.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* Regexp data type */
|
||||
static void regexp_free(mrb_state *mrb, void *ptr) {
|
||||
re_free(mrb, (mrb_regexp_pattern*)ptr);
|
||||
}
|
||||
|
||||
static const struct mrb_data_type regexp_type = { "Regexp", regexp_free };
|
||||
|
||||
/* MatchData */
|
||||
typedef struct {
|
||||
mrb_value source; /* source string */
|
||||
int *captures; /* capture positions [start0,end0,start1,end1,...] */
|
||||
int num_captures; /* number of capture groups (including 0) */
|
||||
} mrb_match_data;
|
||||
|
||||
static void matchdata_free(mrb_state *mrb, void *ptr) {
|
||||
mrb_match_data *md = (mrb_match_data*)ptr;
|
||||
if (md) {
|
||||
mrb_free(mrb, md->captures);
|
||||
mrb_free(mrb, md);
|
||||
}
|
||||
}
|
||||
|
||||
static const struct mrb_data_type matchdata_type = { "MatchData", matchdata_free };
|
||||
|
||||
/* Parse flags from string or integer */
|
||||
static uint32_t
|
||||
parse_flags(mrb_state *mrb, mrb_value flags_val)
|
||||
{
|
||||
uint32_t flags = 0;
|
||||
if (mrb_integer_p(flags_val)) {
|
||||
mrb_int f = mrb_integer(flags_val);
|
||||
if (f & 1) flags |= RE_FLAG_IGNORECASE;
|
||||
if (f & 4) flags |= RE_FLAG_MULTILINE | RE_FLAG_DOTALL;
|
||||
return flags;
|
||||
}
|
||||
if (mrb_string_p(flags_val)) {
|
||||
const char *s = RSTRING_PTR(flags_val);
|
||||
mrb_int len = RSTRING_LEN(flags_val);
|
||||
for (mrb_int i = 0; i < len; i++) {
|
||||
switch (s[i]) {
|
||||
case 'i': flags |= RE_FLAG_IGNORECASE; break;
|
||||
case 'm': flags |= RE_FLAG_MULTILINE | RE_FLAG_DOTALL; break;
|
||||
case 'x': break; /* TODO: extended mode */
|
||||
}
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
if (mrb_test(flags_val)) flags |= RE_FLAG_IGNORECASE;
|
||||
return flags;
|
||||
}
|
||||
|
||||
/*
|
||||
* Regexp.new(pattern, flags=nil)
|
||||
* Regexp.compile(pattern, flags=nil)
|
||||
*/
|
||||
static mrb_value
|
||||
regexp_init(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_value pattern;
|
||||
mrb_value flags_val = mrb_nil_value();
|
||||
mrb_regexp_pattern *pat;
|
||||
|
||||
mrb_get_args(mrb, "S|o", &pattern, &flags_val);
|
||||
|
||||
uint32_t flags = parse_flags(mrb, flags_val);
|
||||
pat = re_compile(mrb, RSTRING_PTR(pattern), RSTRING_LEN(pattern), flags);
|
||||
|
||||
DATA_TYPE(self) = ®exp_type;
|
||||
DATA_PTR(self) = pat;
|
||||
|
||||
/* store source for #source and #inspect */
|
||||
mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@source"), pattern);
|
||||
mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@flags"), mrb_int_value(mrb, (mrb_int)flags));
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/* Create MatchData from captures */
|
||||
static mrb_value
|
||||
create_matchdata(mrb_state *mrb, mrb_value str, int *captures, int ncap)
|
||||
{
|
||||
struct RClass *md_class = mrb_class_get(mrb, "MatchData");
|
||||
mrb_match_data *md = (mrb_match_data*)mrb_malloc(mrb, sizeof(mrb_match_data));
|
||||
md->source = str;
|
||||
md->num_captures = ncap / 2;
|
||||
md->captures = (int*)mrb_malloc(mrb, sizeof(int) * ncap);
|
||||
memcpy(md->captures, captures, sizeof(int) * ncap);
|
||||
|
||||
mrb_value obj = mrb_obj_value(mrb_data_object_alloc(mrb, md_class, md, &matchdata_type));
|
||||
/* store in $~ */
|
||||
return obj;
|
||||
}
|
||||
|
||||
/*
|
||||
* Regexp#match(str, pos=0)
|
||||
*/
|
||||
static mrb_value
|
||||
regexp_match(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_value str;
|
||||
mrb_int pos = 0;
|
||||
mrb_regexp_pattern *pat;
|
||||
|
||||
mrb_get_args(mrb, "S|i", &str, &pos);
|
||||
pat = DATA_GET_PTR(mrb, self, ®exp_type, mrb_regexp_pattern);
|
||||
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
|
||||
|
||||
int captures[RE_MAX_CAPTURES * 2];
|
||||
memset(captures, -1, sizeof(captures));
|
||||
int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), pos,
|
||||
captures, pat->num_captures * 2);
|
||||
|
||||
if (ncap == 0) return mrb_nil_value();
|
||||
|
||||
return create_matchdata(mrb, str, captures, pat->num_captures * 2);
|
||||
}
|
||||
|
||||
/*
|
||||
* Regexp#match?(str, pos=0)
|
||||
*/
|
||||
static mrb_value
|
||||
regexp_match_p(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_value str;
|
||||
mrb_int pos = 0;
|
||||
mrb_regexp_pattern *pat;
|
||||
|
||||
mrb_get_args(mrb, "S|i", &str, &pos);
|
||||
pat = DATA_GET_PTR(mrb, self, ®exp_type, mrb_regexp_pattern);
|
||||
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
|
||||
|
||||
int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), pos, NULL, 0);
|
||||
return mrb_bool_value(ncap > 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Regexp#=~(str)
|
||||
*/
|
||||
static mrb_value
|
||||
regexp_match_op(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_value str;
|
||||
mrb_regexp_pattern *pat;
|
||||
|
||||
mrb_get_args(mrb, "o", &str);
|
||||
if (mrb_nil_p(str)) return mrb_nil_value();
|
||||
mrb_ensure_string_type(mrb, str);
|
||||
|
||||
pat = DATA_GET_PTR(mrb, self, ®exp_type, mrb_regexp_pattern);
|
||||
if (!pat) mrb_raise(mrb, E_ARGUMENT_ERROR, "uninitialized Regexp");
|
||||
|
||||
int captures[RE_MAX_CAPTURES * 2];
|
||||
memset(captures, -1, sizeof(captures));
|
||||
int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), 0,
|
||||
captures, pat->num_captures * 2);
|
||||
|
||||
if (ncap == 0) return mrb_nil_value();
|
||||
return mrb_int_value(mrb, captures[0]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Regexp#===(str)
|
||||
*/
|
||||
static mrb_value
|
||||
regexp_case_match(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_value str;
|
||||
mrb_regexp_pattern *pat;
|
||||
|
||||
mrb_get_args(mrb, "o", &str);
|
||||
if (!mrb_string_p(str)) return mrb_false_value();
|
||||
|
||||
pat = DATA_GET_PTR(mrb, self, ®exp_type, mrb_regexp_pattern);
|
||||
if (!pat) return mrb_false_value();
|
||||
|
||||
int ncap = re_exec(mrb, pat, RSTRING_PTR(str), RSTRING_LEN(str), 0, NULL, 0);
|
||||
return mrb_bool_value(ncap > 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Regexp#source
|
||||
*/
|
||||
static mrb_value
|
||||
regexp_source(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
return mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@source"));
|
||||
}
|
||||
|
||||
/*
|
||||
* Regexp#inspect
|
||||
*/
|
||||
static mrb_value
|
||||
regexp_inspect(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_value src = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@source"));
|
||||
mrb_value flags_val = mrb_iv_get(mrb, self, mrb_intern_lit(mrb, "@flags"));
|
||||
uint32_t flags = mrb_nil_p(flags_val) ? 0 : (uint32_t)mrb_integer(flags_val);
|
||||
|
||||
mrb_value result = mrb_str_new_lit(mrb, "/");
|
||||
mrb_str_cat_str(mrb, result, src);
|
||||
mrb_str_cat_lit(mrb, result, "/");
|
||||
if (flags & RE_FLAG_IGNORECASE) mrb_str_cat_lit(mrb, result, "i");
|
||||
if (flags & RE_FLAG_MULTILINE) mrb_str_cat_lit(mrb, result, "m");
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* Regexp.escape(str)
|
||||
*/
|
||||
static mrb_value
|
||||
regexp_escape(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_value str;
|
||||
mrb_get_args(mrb, "S", &str);
|
||||
|
||||
const char *s = RSTRING_PTR(str);
|
||||
mrb_int len = RSTRING_LEN(str);
|
||||
mrb_value result = mrb_str_new_capa(mrb, len + len / 4);
|
||||
|
||||
for (mrb_int i = 0; i < len; i++) {
|
||||
char c = s[i];
|
||||
switch (c) {
|
||||
case '\\': case '.': case '*': case '+': case '?': case '|':
|
||||
case '(': case ')': case '[': case ']': case '{': case '}':
|
||||
case '^': case '$':
|
||||
mrb_str_cat_lit(mrb, result, "\\");
|
||||
/* fall through */
|
||||
default:
|
||||
mrb_str_cat(mrb, result, &c, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* --- MatchData methods --- */
|
||||
|
||||
/*
|
||||
* MatchData#[](n)
|
||||
*/
|
||||
static mrb_value
|
||||
matchdata_aref(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_int idx;
|
||||
mrb_get_args(mrb, "i", &idx);
|
||||
|
||||
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
|
||||
if (!md) return mrb_nil_value();
|
||||
|
||||
if (idx < 0 || idx >= md->num_captures) return mrb_nil_value();
|
||||
int start = md->captures[idx * 2];
|
||||
int end = md->captures[idx * 2 + 1];
|
||||
if (start < 0) return mrb_nil_value();
|
||||
|
||||
return mrb_str_substr(mrb, md->source, start, end - start);
|
||||
}
|
||||
|
||||
/*
|
||||
* MatchData#captures
|
||||
*/
|
||||
static mrb_value
|
||||
matchdata_captures(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
|
||||
if (!md) return mrb_ary_new(mrb);
|
||||
|
||||
mrb_value ary = mrb_ary_new_capa(mrb, md->num_captures - 1);
|
||||
for (int i = 1; i < md->num_captures; i++) {
|
||||
int start = md->captures[i * 2];
|
||||
int end = md->captures[i * 2 + 1];
|
||||
if (start < 0) {
|
||||
mrb_ary_push(mrb, ary, mrb_nil_value());
|
||||
}
|
||||
else {
|
||||
mrb_ary_push(mrb, ary, mrb_str_substr(mrb, md->source, start, end - start));
|
||||
}
|
||||
}
|
||||
return ary;
|
||||
}
|
||||
|
||||
/*
|
||||
* MatchData#to_a
|
||||
*/
|
||||
static mrb_value
|
||||
matchdata_to_a(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
|
||||
if (!md) return mrb_ary_new(mrb);
|
||||
|
||||
mrb_value ary = mrb_ary_new_capa(mrb, md->num_captures);
|
||||
for (int i = 0; i < md->num_captures; i++) {
|
||||
int start = md->captures[i * 2];
|
||||
int end = md->captures[i * 2 + 1];
|
||||
if (start < 0) {
|
||||
mrb_ary_push(mrb, ary, mrb_nil_value());
|
||||
}
|
||||
else {
|
||||
mrb_ary_push(mrb, ary, mrb_str_substr(mrb, md->source, start, end - start));
|
||||
}
|
||||
}
|
||||
return ary;
|
||||
}
|
||||
|
||||
/*
|
||||
* MatchData#begin(n) / MatchData#end(n)
|
||||
*/
|
||||
static mrb_value
|
||||
matchdata_begin(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_int idx;
|
||||
mrb_get_args(mrb, "i", &idx);
|
||||
|
||||
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
|
||||
if (!md || idx < 0 || idx >= md->num_captures) return mrb_nil_value();
|
||||
int pos = md->captures[idx * 2];
|
||||
if (pos < 0) return mrb_nil_value();
|
||||
return mrb_int_value(mrb, pos);
|
||||
}
|
||||
|
||||
static mrb_value
|
||||
matchdata_end(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_int idx;
|
||||
mrb_get_args(mrb, "i", &idx);
|
||||
|
||||
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
|
||||
if (!md || idx < 0 || idx >= md->num_captures) return mrb_nil_value();
|
||||
int pos = md->captures[idx * 2 + 1];
|
||||
if (pos < 0) return mrb_nil_value();
|
||||
return mrb_int_value(mrb, pos);
|
||||
}
|
||||
|
||||
/*
|
||||
* MatchData#pre_match / #post_match
|
||||
*/
|
||||
static mrb_value
|
||||
matchdata_pre(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
|
||||
if (!md || md->captures[0] < 0) return mrb_nil_value();
|
||||
return mrb_str_substr(mrb, md->source, 0, md->captures[0]);
|
||||
}
|
||||
|
||||
static mrb_value
|
||||
matchdata_post(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
|
||||
if (!md || md->captures[1] < 0) return mrb_nil_value();
|
||||
int pos = md->captures[1];
|
||||
return mrb_str_substr(mrb, md->source, pos, RSTRING_LEN(md->source) - pos);
|
||||
}
|
||||
|
||||
/*
|
||||
* MatchData#length / #size
|
||||
*/
|
||||
static mrb_value
|
||||
matchdata_length(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
|
||||
if (!md) return mrb_fixnum_value(0);
|
||||
return mrb_fixnum_value(md->num_captures);
|
||||
}
|
||||
|
||||
/* --- Gem init --- */
|
||||
|
||||
void
|
||||
mrb_mruby_regexp_gem_init(mrb_state *mrb)
|
||||
{
|
||||
struct RClass *re = mrb_define_class(mrb, "Regexp", mrb->object_class);
|
||||
MRB_SET_INSTANCE_TT(re, MRB_TT_CDATA);
|
||||
|
||||
/* Constants */
|
||||
mrb_define_const(mrb, re, "IGNORECASE", mrb_fixnum_value(1));
|
||||
mrb_define_const(mrb, re, "EXTENDED", mrb_fixnum_value(2));
|
||||
mrb_define_const(mrb, re, "MULTILINE", mrb_fixnum_value(4));
|
||||
|
||||
/* Class methods */
|
||||
mrb_define_method(mrb, re, "initialize", regexp_init, MRB_ARGS_ARG(1, 2));
|
||||
mrb_define_class_method(mrb, re, "compile", regexp_init, MRB_ARGS_ARG(1, 2));
|
||||
mrb_define_class_method(mrb, re, "escape", regexp_escape, MRB_ARGS_REQ(1));
|
||||
mrb_define_class_method(mrb, re, "quote", regexp_escape, MRB_ARGS_REQ(1));
|
||||
|
||||
/* Instance methods */
|
||||
mrb_define_method(mrb, re, "match", regexp_match, MRB_ARGS_ARG(1, 1));
|
||||
mrb_define_method(mrb, re, "match?", regexp_match_p, MRB_ARGS_ARG(1, 1));
|
||||
mrb_define_method(mrb, re, "=~", regexp_match_op, MRB_ARGS_REQ(1));
|
||||
mrb_define_method(mrb, re, "===", regexp_case_match, MRB_ARGS_REQ(1));
|
||||
mrb_define_method(mrb, re, "source", regexp_source, MRB_ARGS_NONE());
|
||||
mrb_define_method(mrb, re, "inspect", regexp_inspect, MRB_ARGS_NONE());
|
||||
mrb_define_method(mrb, re, "to_s", regexp_inspect, MRB_ARGS_NONE());
|
||||
|
||||
/* MatchData class */
|
||||
struct RClass *md = mrb_define_class(mrb, "MatchData", mrb->object_class);
|
||||
MRB_SET_INSTANCE_TT(md, MRB_TT_CDATA);
|
||||
|
||||
mrb_define_method(mrb, md, "[]", matchdata_aref, MRB_ARGS_REQ(1));
|
||||
mrb_define_method(mrb, md, "captures", matchdata_captures, MRB_ARGS_NONE());
|
||||
mrb_define_method(mrb, md, "to_a", matchdata_to_a, MRB_ARGS_NONE());
|
||||
mrb_define_method(mrb, md, "length", matchdata_length, MRB_ARGS_NONE());
|
||||
mrb_define_method(mrb, md, "size", matchdata_length, MRB_ARGS_NONE());
|
||||
mrb_define_method(mrb, md, "begin", matchdata_begin, MRB_ARGS_REQ(1));
|
||||
mrb_define_method(mrb, md, "end", matchdata_end, MRB_ARGS_REQ(1));
|
||||
mrb_define_method(mrb, md, "pre_match", matchdata_pre, MRB_ARGS_NONE());
|
||||
mrb_define_method(mrb, md, "post_match", matchdata_post, MRB_ARGS_NONE());
|
||||
}
|
||||
|
||||
void
|
||||
mrb_mruby_regexp_gem_final(mrb_state *mrb)
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
assert("Regexp.new") do
|
||||
re = Regexp.new("abc")
|
||||
assert_kind_of Regexp, re
|
||||
end
|
||||
|
||||
assert("Regexp#match - simple") do
|
||||
re = Regexp.new("abc")
|
||||
md = re.match("xabcy")
|
||||
assert_kind_of MatchData, md
|
||||
assert_equal "abc", md[0]
|
||||
end
|
||||
|
||||
assert("Regexp#match - no match") do
|
||||
re = Regexp.new("xyz")
|
||||
assert_nil re.match("abc")
|
||||
end
|
||||
|
||||
assert("Regexp#match?") do
|
||||
re = Regexp.new("abc")
|
||||
assert_true re.match?("xabcy")
|
||||
assert_false re.match?("xyz")
|
||||
end
|
||||
|
||||
assert("Regexp#=~") do
|
||||
re = Regexp.new("bc")
|
||||
assert_equal 1, re =~ "abcd"
|
||||
assert_nil re =~ "xyz"
|
||||
end
|
||||
|
||||
assert("Regexp#===") do
|
||||
re = Regexp.new("abc")
|
||||
assert_true re === "abc"
|
||||
assert_false re === "xyz"
|
||||
end
|
||||
|
||||
assert("Regexp - character class") do
|
||||
re = Regexp.new("[a-z]+")
|
||||
md = re.match("123abc456")
|
||||
assert_equal "abc", md[0]
|
||||
end
|
||||
|
||||
assert("Regexp - dot") do
|
||||
re = Regexp.new("a.c")
|
||||
assert_true re.match?("abc")
|
||||
assert_true re.match?("axc")
|
||||
assert_false re.match?("ac")
|
||||
end
|
||||
|
||||
assert("Regexp - alternation") do
|
||||
re = Regexp.new("cat|dog")
|
||||
assert_equal "cat", re.match("I have a cat")[0]
|
||||
assert_equal "dog", re.match("I have a dog")[0]
|
||||
end
|
||||
|
||||
assert("Regexp - quantifiers") do
|
||||
assert_equal "aaa", Regexp.new("a+").match("aaa")[0]
|
||||
assert_equal "", Regexp.new("a*").match("bbb")[0]
|
||||
assert_equal "ab", Regexp.new("ab?").match("ab")[0]
|
||||
assert_equal "a", Regexp.new("ab?").match("ac")[0]
|
||||
end
|
||||
|
||||
assert("Regexp - captures") do
|
||||
re = Regexp.new("(\\w+)@(\\w+)")
|
||||
md = re.match("user@host")
|
||||
assert_equal "user@host", md[0]
|
||||
assert_equal "user", md[1]
|
||||
assert_equal "host", md[2]
|
||||
end
|
||||
|
||||
assert("Regexp - \\d \\w \\s") do
|
||||
assert_true Regexp.new("\\d+").match?("123")
|
||||
assert_true Regexp.new("\\w+").match?("abc_123")
|
||||
assert_true Regexp.new("\\s+").match?(" ")
|
||||
assert_false Regexp.new("\\d+").match?("abc")
|
||||
end
|
||||
|
||||
assert("Regexp - anchors") do
|
||||
assert_true Regexp.new("^abc").match?("abc")
|
||||
assert_false Regexp.new("^abc").match?("xabc")
|
||||
assert_true Regexp.new("abc$").match?("abc")
|
||||
assert_false Regexp.new("abc$").match?("abcx")
|
||||
end
|
||||
|
||||
assert("Regexp - case insensitive") do
|
||||
re = Regexp.new("abc", Regexp::IGNORECASE)
|
||||
assert_true re.match?("ABC")
|
||||
assert_true re.match?("Abc")
|
||||
end
|
||||
|
||||
assert("Regexp - repetition {n,m}") do
|
||||
assert_equal "aaa", Regexp.new("a{3}").match("aaaa")[0]
|
||||
assert_equal "aa", Regexp.new("a{2,3}").match("aa")[0]
|
||||
assert_equal "aaa", Regexp.new("a{2,3}").match("aaaa")[0]
|
||||
end
|
||||
|
||||
assert("MatchData#captures") do
|
||||
re = Regexp.new("(a)(b)(c)")
|
||||
md = re.match("abc")
|
||||
assert_equal ["a", "b", "c"], md.captures
|
||||
end
|
||||
|
||||
assert("MatchData#pre_match / #post_match") do
|
||||
re = Regexp.new("bc")
|
||||
md = re.match("abcde")
|
||||
assert_equal "a", md.pre_match
|
||||
assert_equal "de", md.post_match
|
||||
end
|
||||
|
||||
assert("MatchData#begin / #end") do
|
||||
re = Regexp.new("bc")
|
||||
md = re.match("abcde")
|
||||
assert_equal 1, md.begin(0)
|
||||
assert_equal 3, md.end(0)
|
||||
end
|
||||
|
||||
assert("Regexp.escape") do
|
||||
assert_equal "a\\.b\\*c", Regexp.escape("a.b*c")
|
||||
end
|
||||
|
||||
assert("Regexp#inspect") do
|
||||
re = Regexp.new("abc", Regexp::IGNORECASE)
|
||||
assert_equal "/abc/i", re.inspect
|
||||
end
|
||||
|
||||
assert("String#match") do
|
||||
md = "hello world".match(Regexp.new("(\\w+)\\s(\\w+)"))
|
||||
assert_equal "hello", md[1]
|
||||
assert_equal "world", md[2]
|
||||
end
|
||||
|
||||
assert("String#sub") do
|
||||
assert_equal "hXllo", "hello".sub(Regexp.new("e"), "X")
|
||||
end
|
||||
|
||||
assert("String#gsub") do
|
||||
assert_equal "h-ll-", "hello".gsub(Regexp.new("[eo]"), "-")
|
||||
end
|
||||
|
||||
assert("String#scan") do
|
||||
assert_equal ["1", "2", "3"], "a1b2c3".scan(Regexp.new("\\d"))
|
||||
end
|
||||
Reference in New Issue
Block a user