diff --git a/build_config/host-debug.rb b/build_config/host-debug.rb index 600a4d78f..5f1e10c6e 100644 --- a/build_config/host-debug.rb +++ b/build_config/host-debug.rb @@ -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 diff --git a/mrbgems/mruby-regexp/include/re_internal.h b/mrbgems/mruby-regexp/include/re_internal.h new file mode 100644 index 000000000..82ace6461 --- /dev/null +++ b/mrbgems/mruby-regexp/include/re_internal.h @@ -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 +#include + +/* 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 */ diff --git a/mrbgems/mruby-regexp/mrbgem.rake b/mrbgems/mruby-regexp/mrbgem.rake new file mode 100644 index 000000000..c5879908d --- /dev/null +++ b/mrbgems/mruby-regexp/mrbgem.rake @@ -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 diff --git a/mrbgems/mruby-regexp/mrblib/string_regexp.rb b/mrbgems/mruby-regexp/mrblib/string_regexp.rb new file mode 100644 index 000000000..28b225ec8 --- /dev/null +++ b/mrbgems/mruby-regexp/mrblib/string_regexp.rb @@ -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 diff --git a/mrbgems/mruby-regexp/src/re_compile.c b/mrbgems/mruby-regexp/src/re_compile.c new file mode 100644 index 000000000..0cf9e3928 --- /dev/null +++ b/mrbgems/mruby-regexp/src/re_compile.c @@ -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 +#include + +/* 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); + } +} diff --git a/mrbgems/mruby-regexp/src/re_exec.c b/mrbgems/mruby-regexp/src/re_exec.c new file mode 100644 index 000000000..aa9deaed0 --- /dev/null +++ b/mrbgems/mruby-regexp/src/re_exec.c @@ -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 + +/* 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; +} diff --git a/mrbgems/mruby-regexp/src/re_utf8.c b/mrbgems/mruby-regexp/src/re_utf8.c new file mode 100644 index 000000000..82f22b1bb --- /dev/null +++ b/mrbgems/mruby-regexp/src/re_utf8.c @@ -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; +} diff --git a/mrbgems/mruby-regexp/src/regexp.c b/mrbgems/mruby-regexp/src/regexp.c new file mode 100644 index 000000000..1c6ec180c --- /dev/null +++ b/mrbgems/mruby-regexp/src/regexp.c @@ -0,0 +1,427 @@ +/* +** regexp.c - Regexp class and MatchData class +** +** See Copyright Notice in mruby.h +*/ + +#include +#include +#include +#include +#include +#include +#include +#include "re_internal.h" + +#include + +/* 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) +{ +} diff --git a/mrbgems/mruby-regexp/test/regexp.rb b/mrbgems/mruby-regexp/test/regexp.rb new file mode 100644 index 000000000..45ee42051 --- /dev/null +++ b/mrbgems/mruby-regexp/test/regexp.rb @@ -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