Ruby 4.1.0dev (2026-10-28 revision 130c9ec44bd35c7319cdc99c301419188510e3a9)
regexp.c
1#include "prism/internal/regexp.h"
2
5#include "prism/internal/buffer.h"
6#include "prism/internal/char.h"
7#include "prism/internal/diagnostic.h"
8#include "prism/internal/encoding.h"
9#include "prism/internal/memchr.h"
10#include "prism/internal/parser.h"
11#include "prism/internal/stringy.h"
12#include "prism/internal/strncasecmp.h"
13
14#include <assert.h>
15#include <string.h>
16
18#define PM_REGEXP_PARSE_DEPTH_MAX 4096
19
23typedef struct {
26
28 const uint8_t *start;
29
31 const uint8_t *cursor;
32
34 const uint8_t *end;
35
38
40 pm_regexp_name_callback_t name_callback;
41
43 void *name_data;
44
46 const uint8_t *node_start;
47
49 const uint8_t *node_end;
50
57
64 const uint8_t *property_name;
65
68
73 const uint8_t *unicode_property_name;
74
77
80
83
89
92
100
103
106
112
115
118
121
124
128
134static PRISM_INLINE void
135pm_regexp_parse_error(pm_regexp_parser_t *regexp_parser, const uint8_t *start, const uint8_t *end, const char *message) {
136 if (regexp_parser->report_errors) {
137 pm_parser_t *parser = regexp_parser->parser;
138 pm_diagnostic_list_append_format(&parser->metadata_arena, &parser->error_list, (uint32_t) (start - parser->start), (uint32_t) (end - start), PM_ERR_REGEXP_PARSE_ERROR, message);
139 }
140}
141
147#define pm_regexp_parse_error_format(regexp_parser_, err_start_, err_end_, diag_id, ...) \
148 do { \
149 if ((regexp_parser_)->report_errors) { \
150 pm_parser_t *parser_ = (regexp_parser_)->parser; \
151 pm_diagnostic_list_append_format(&parser_->metadata_arena, &parser_->error_list, (uint32_t) ((err_start_) - parser_->start), (uint32_t) ((err_end_) - (err_start_)), diag_id, __VA_ARGS__); \
152 } \
153 } while (0)
154
159static void
160pm_regexp_parser_named_capture(pm_regexp_parser_t *parser, const uint8_t *start, const uint8_t *end) {
161 pm_string_t string;
162 pm_string_shared_init(&string, start, end);
163 parser->name_callback(parser->parser, &string, parser->name_data);
164}
165
169static PRISM_INLINE bool
170pm_regexp_char_is_eof(pm_regexp_parser_t *parser) {
171 return parser->cursor >= parser->end;
172}
173
177static PRISM_INLINE bool
178pm_regexp_char_accept(pm_regexp_parser_t *parser, uint8_t value) {
179 if (!pm_regexp_char_is_eof(parser) && *parser->cursor == value) {
180 parser->cursor++;
181 return true;
182 }
183 return false;
184}
185
189static PRISM_INLINE bool
190pm_regexp_char_expect(pm_regexp_parser_t *parser, uint8_t value) {
191 if (!pm_regexp_char_is_eof(parser) && *parser->cursor == value) {
192 parser->cursor++;
193 return true;
194 }
195 return false;
196}
197
201static bool
202pm_regexp_char_find(pm_regexp_parser_t *parser, uint8_t value) {
203 if (pm_regexp_char_is_eof(parser)) {
204 return false;
205 }
206
207 const uint8_t *end = (const uint8_t *) pm_memchr(parser->cursor, value, (size_t) (parser->end - parser->cursor), parser->encoding_changed, parser->encoding);
208 if (end == NULL) {
209 return false;
210 }
211
212 parser->cursor = end + 1;
213 return true;
214}
215
221static PRISM_INLINE void
222pm_regexp_hex_group_boundary(pm_regexp_parser_t *parser) {
223 if (parser->hex_group_active) {
224 pm_buffer_append_byte(&parser->hex_escape_buffer, 0x00);
225 parser->hex_group_active = false;
226 }
227}
228
232static PRISM_INLINE void
233pm_regexp_track_hex_escape(pm_regexp_parser_t *parser, uint8_t byte) {
234 if (byte >= 0x80) {
235 pm_buffer_append_byte(&parser->hex_escape_buffer, byte);
236 parser->hex_group_active = true;
237 parser->has_hex_escape = true;
238
239 parser->explicit_encoding = parser->encoding;
240 parser->last_escape_was_unicode = false;
241 } else {
242 pm_regexp_hex_group_boundary(parser);
243 }
244}
245
249static PRISM_INLINE int
250pm_regexp_hex_digit_value(uint8_t byte) {
251 if (byte >= '0' && byte <= '9') return byte - '0';
252 if (byte >= 'a' && byte <= 'f') return byte - 'a' + 10;
253 if (byte >= 'A' && byte <= 'F') return byte - 'A' + 10;
254 return -1;
255}
256
289static bool
290pm_regexp_parse_range_quantifier(pm_regexp_parser_t *parser) {
291 const uint8_t *savepoint = parser->cursor;
292
293 enum {
294 PM_REGEXP_RANGE_QUANTIFIER_STATE_START,
295 PM_REGEXP_RANGE_QUANTIFIER_STATE_MINIMUM,
296 PM_REGEXP_RANGE_QUANTIFIER_STATE_MAXIMUM,
297 PM_REGEXP_RANGE_QUANTIFIER_STATE_COMMA
298 } state = PM_REGEXP_RANGE_QUANTIFIER_STATE_START;
299
300 while (1) {
301 if (parser->cursor >= parser->end) {
302 parser->cursor = savepoint;
303 return true;
304 }
305
306 switch (state) {
307 case PM_REGEXP_RANGE_QUANTIFIER_STATE_START:
308 switch (*parser->cursor) {
309 case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
310 parser->cursor++;
311 state = PM_REGEXP_RANGE_QUANTIFIER_STATE_MINIMUM;
312 break;
313 case ',':
314 parser->cursor++;
315 state = PM_REGEXP_RANGE_QUANTIFIER_STATE_COMMA;
316 break;
317 default:
318 parser->cursor = savepoint;
319 return true;
320 }
321 break;
322 case PM_REGEXP_RANGE_QUANTIFIER_STATE_MINIMUM:
323 switch (*parser->cursor) {
324 case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
325 parser->cursor++;
326 break;
327 case ',':
328 parser->cursor++;
329 state = PM_REGEXP_RANGE_QUANTIFIER_STATE_MAXIMUM;
330 break;
331 case '}':
332 parser->cursor++;
333 return true;
334 default:
335 parser->cursor = savepoint;
336 return true;
337 }
338 break;
339 case PM_REGEXP_RANGE_QUANTIFIER_STATE_COMMA:
340 switch (*parser->cursor) {
341 case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
342 parser->cursor++;
343 state = PM_REGEXP_RANGE_QUANTIFIER_STATE_MAXIMUM;
344 break;
345 default:
346 parser->cursor = savepoint;
347 return true;
348 }
349 break;
350 case PM_REGEXP_RANGE_QUANTIFIER_STATE_MAXIMUM:
351 switch (*parser->cursor) {
352 case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
353 parser->cursor++;
354 break;
355 case '}':
356 parser->cursor++;
357 return true;
358 default:
359 parser->cursor = savepoint;
360 return true;
361 }
362 break;
363 }
364 }
365
366 return true;
367}
368
377static bool
378pm_regexp_parse_quantifier(pm_regexp_parser_t *parser) {
379 while (!pm_regexp_char_is_eof(parser)) {
380 switch (*parser->cursor) {
381 case '*':
382 case '+':
383 case '?':
384 parser->cursor++;
385 break;
386 case '{':
387 parser->cursor++;
388 if (!pm_regexp_parse_range_quantifier(parser)) return false;
389 break;
390 default:
391 // In this case there is no quantifier.
392 return true;
393 }
394 }
395
396 return true;
397}
398
403static bool
404pm_regexp_parse_posix_class(pm_regexp_parser_t *parser) {
405 if (!pm_regexp_char_expect(parser, ':')) {
406 return false;
407 }
408
409 pm_regexp_char_accept(parser, '^');
410
411 return (
412 pm_regexp_char_find(parser, ':') &&
413 pm_regexp_char_expect(parser, ']') &&
414 pm_regexp_char_expect(parser, ']')
415 );
416}
417
429typedef enum {
430 PM_REGEXP_PROPERTY_POSIX,
431 PM_REGEXP_PROPERTY_SCRIPT,
432 PM_REGEXP_PROPERTY_UNICODE
433} pm_regexp_property_type_t;
434
439static pm_regexp_property_type_t
440pm_regexp_classify_property(const uint8_t *name, size_t length) {
441 // Skip leading '^' for negated properties like \p{^Hiragana}.
442 if (length > 0 && name[0] == '^') {
443 name++;
444 length--;
445 }
446
447#define PM_REGEXP_CASECMP(str_) (pm_strncasecmp(name, (const uint8_t *) (str_), length) == 0)
448
449 switch (length) {
450 case 3:
451 if (PM_REGEXP_CASECMP("Han")) return PM_REGEXP_PROPERTY_SCRIPT;
452 break;
453 case 4:
454 if (PM_REGEXP_CASECMP("Word")) return PM_REGEXP_PROPERTY_POSIX;
455 break;
456 case 5:
457 /* Most properties are length 5, so dispatch on first character. */
458 switch (name[0] | 0x20) {
459 case 'a':
460 if (PM_REGEXP_CASECMP("Alnum")) return PM_REGEXP_PROPERTY_POSIX;
461 if (PM_REGEXP_CASECMP("Alpha")) return PM_REGEXP_PROPERTY_POSIX;
462 if (PM_REGEXP_CASECMP("ASCII")) return PM_REGEXP_PROPERTY_POSIX;
463 break;
464 case 'b':
465 if (PM_REGEXP_CASECMP("Blank")) return PM_REGEXP_PROPERTY_POSIX;
466 break;
467 case 'c':
468 if (PM_REGEXP_CASECMP("Cntrl")) return PM_REGEXP_PROPERTY_POSIX;
469 break;
470 case 'd':
471 if (PM_REGEXP_CASECMP("Digit")) return PM_REGEXP_PROPERTY_POSIX;
472 break;
473 case 'g':
474 if (PM_REGEXP_CASECMP("Graph")) return PM_REGEXP_PROPERTY_POSIX;
475 if (PM_REGEXP_CASECMP("Greek")) return PM_REGEXP_PROPERTY_SCRIPT;
476 break;
477 case 'l':
478 if (PM_REGEXP_CASECMP("Lower")) return PM_REGEXP_PROPERTY_POSIX;
479 if (PM_REGEXP_CASECMP("Latin")) return PM_REGEXP_PROPERTY_SCRIPT;
480 break;
481 case 'p':
482 if (PM_REGEXP_CASECMP("Print")) return PM_REGEXP_PROPERTY_POSIX;
483 if (PM_REGEXP_CASECMP("Punct")) return PM_REGEXP_PROPERTY_POSIX;
484 break;
485 case 's':
486 if (PM_REGEXP_CASECMP("Space")) return PM_REGEXP_PROPERTY_POSIX;
487 break;
488 case 'u':
489 if (PM_REGEXP_CASECMP("Upper")) return PM_REGEXP_PROPERTY_POSIX;
490 break;
491 }
492 break;
493 case 6:
494 if (PM_REGEXP_CASECMP("XDigit")) return PM_REGEXP_PROPERTY_POSIX;
495 break;
496 case 8:
497 if (PM_REGEXP_CASECMP("Hiragana")) return PM_REGEXP_PROPERTY_SCRIPT;
498 if (PM_REGEXP_CASECMP("Katakana")) return PM_REGEXP_PROPERTY_SCRIPT;
499 if (PM_REGEXP_CASECMP("Cyrillic")) return PM_REGEXP_PROPERTY_SCRIPT;
500 break;
501 }
502
503#undef PM_REGEXP_CASECMP
504
505 // Everything else is Unicode-only (general categories, other scripts, etc.).
506 return PM_REGEXP_PROPERTY_UNICODE;
507}
508
518static bool
519pm_regexp_parse_property_escape(pm_regexp_parser_t *parser) {
520 assert(*parser->cursor == 'p' || *parser->cursor == 'P');
521
522 if (parser->cursor + 1 < parser->end && parser->cursor[1] == '{') {
523 const uint8_t *name_start = parser->cursor + 2;
524 const uint8_t *search = name_start;
525
526 while (search < parser->end && *search != '}') search++;
527
528 if (search < parser->end) {
529 size_t name_length = (size_t) (search - name_start);
530 parser->has_property_escape = true;
531
532 pm_regexp_property_type_t type = pm_regexp_classify_property(name_start, name_length);
533
534 // Track the first non-POSIX property name (for /n error messages).
535 if (type >= PM_REGEXP_PROPERTY_SCRIPT && parser->property_name == NULL) {
536 parser->property_name = name_start;
537 parser->property_name_length = name_length;
538 }
539
540 // Track the first Unicode-only property name (for /e, /s error messages).
541 if (type == PM_REGEXP_PROPERTY_UNICODE) {
542 parser->has_unicode_property_escape = true;
543 if (parser->unicode_property_name == NULL) {
544 parser->unicode_property_name = name_start;
545 parser->unicode_property_name_length = name_length;
546 }
547 }
548
549 parser->cursor = search + 1; // skip past '}'
550 return true;
551 }
552 }
553
554 // Not a property escape, just skip the single character after '\'.
555 parser->cursor++;
556 return false;
557}
558
565static void
566pm_regexp_parse_unicode_escape(pm_regexp_parser_t *parser) {
567 const uint8_t *escape_start = parser->cursor - 2; // points to '\'
568
569 if (pm_regexp_char_is_eof(parser)) {
570 pm_regexp_parse_error(parser, escape_start, parser->cursor, "invalid Unicode escape");
571 return;
572 }
573
574 if (*parser->cursor == '{') {
575 parser->cursor++; // skip '{'
576
577 // Skip leading whitespace.
578 while (!pm_regexp_char_is_eof(parser) && pm_char_is_whitespace(*parser->cursor)) {
579 parser->cursor++;
580 }
581
582 bool has_codepoint = false;
583
584 while (!pm_regexp_char_is_eof(parser) && *parser->cursor != '}') {
585 // Parse the hex digits to compute the codepoint value.
586 uint32_t value = 0;
587 size_t hex_count = 0;
588
589 int digit;
590 while (!pm_regexp_char_is_eof(parser) && (digit = pm_regexp_hex_digit_value(*parser->cursor)) >= 0) {
591 value = (value << 4) | (uint32_t) digit;
592 hex_count++;
593 parser->cursor++;
594 }
595
596 if (hex_count == 0) {
597 // Skip to '}' or end of regexp to find the full extent.
598 while (!pm_regexp_char_is_eof(parser) && *parser->cursor != '}') {
599 parser->cursor++;
600 }
601
602 const uint8_t *escape_end = parser->cursor;
603 if (!pm_regexp_char_is_eof(parser)) {
604 escape_end++;
605 parser->cursor++; // skip '}'
606 }
607
608 pm_regexp_parse_error_format(parser, escape_start, escape_end, PM_ERR_ESCAPE_INVALID_UNICODE_LIST, (int) (escape_end - escape_start), (const char *) escape_start);
609 return;
610 }
611
612 if (hex_count > 6) {
613 pm_regexp_parse_error(parser, escape_start, parser->cursor, "invalid Unicode range");
614 }
615
616 // Track encoding state for this codepoint.
617 if (value >= 0x80) {
618 parser->has_unicode_escape = true;
619 parser->explicit_encoding = PM_ENCODING_UTF_8_ENTRY;
620 parser->last_escape_was_unicode = true;
621 pm_regexp_hex_group_boundary(parser);
622 }
623
624 // Check for invalid Unicode range (surrogates or > 0x10FFFF).
625 if (value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF)) {
626 parser->invalid_unicode_range = true;
627 }
628
629 has_codepoint = true;
630
631 // Skip whitespace between codepoints.
632 while (!pm_regexp_char_is_eof(parser) && pm_char_is_whitespace(*parser->cursor)) {
633 parser->cursor++;
634 }
635 }
636
637 if (pm_regexp_char_is_eof(parser)) {
638 pm_regexp_parse_error(parser, escape_start, parser->cursor, "unterminated Unicode escape");
639 } else {
640 if (!has_codepoint) {
641 pm_regexp_parse_error_format(parser, escape_start, parser->cursor + 1, PM_ERR_ESCAPE_INVALID_UNICODE_LIST, (int) (parser->cursor + 1 - escape_start), (const char *) escape_start);
642 }
643 parser->cursor++; // skip '}'
644 }
645 } else {
646 // \uNNNN form — need exactly 4 hex digits.
647 uint32_t value = 0;
648 size_t hex_count = 0;
649
650 int digit;
651 while (hex_count < 4 && !pm_regexp_char_is_eof(parser) && (digit = pm_regexp_hex_digit_value(*parser->cursor)) >= 0) {
652 value = (value << 4) | (uint32_t) digit;
653 hex_count++;
654 parser->cursor++;
655 }
656
657 if (hex_count < 4) {
658 pm_regexp_parse_error(parser, escape_start, parser->cursor, "invalid Unicode escape");
659 } else if (value >= 0x80) {
660 parser->has_unicode_escape = true;
661 parser->explicit_encoding = PM_ENCODING_UTF_8_ENTRY;
662 parser->last_escape_was_unicode = true;
663 pm_regexp_hex_group_boundary(parser);
664 }
665
666 // Check for invalid Unicode range.
667 if (hex_count == 4 && (value > 0x10FFFF || (value >= 0xD800 && value <= 0xDFFF))) {
668 parser->invalid_unicode_range = true;
669 }
670 }
671}
672
673// Forward declaration because character sets can be nested.
674static bool
675pm_regexp_parse_lbracket(pm_regexp_parser_t *parser, uint16_t depth);
676
681static int
682pm_regexp_parse_hex_escape(pm_regexp_parser_t *parser) {
683 int value = -1;
684
685 if (!pm_regexp_char_is_eof(parser)) {
686 int digit = pm_regexp_hex_digit_value(*parser->cursor);
687 if (digit >= 0) {
688 value = digit;
689 parser->cursor++;
690
691 if (!pm_regexp_char_is_eof(parser)) {
692 digit = pm_regexp_hex_digit_value(*parser->cursor);
693 if (digit >= 0) {
694 value = (value << 4) | digit;
695 parser->cursor++;
696 }
697 }
698 }
699 }
700
701 if (value >= 0) {
702 pm_regexp_track_hex_escape(parser, (uint8_t) value);
703 }
704
705 return value;
706}
707
713static void
714pm_regexp_parse_backslash_escape(pm_regexp_parser_t *parser) {
715 if (pm_regexp_char_is_eof(parser)) return;
716
717 switch (*parser->cursor) {
718 case 'u':
719 parser->cursor++; // skip 'u'
720 pm_regexp_parse_unicode_escape(parser);
721 break;
722 case 'p':
723 case 'P':
724 pm_regexp_parse_property_escape(parser);
725 break;
726 case 'x':
727 parser->cursor++; // skip 'x'
728 pm_regexp_parse_hex_escape(parser);
729 break;
730 case 'M':
731 // \M-x produces (x | 0x80), always >= 0x80
732 if (parser->cursor + 2 < parser->end && parser->cursor[1] == '-') {
733 parser->cursor += 2; // skip 'M-'
734 if (!pm_regexp_char_is_eof(parser)) {
735 if (*parser->cursor == '\\') {
736 parser->cursor++;
737 // \M-\C-x or \M-\cx — the resulting byte is always >= 0x80
738 // We just need to track it as a hex escape >= 0x80.
739 pm_regexp_parse_backslash_escape(parser);
740 } else {
741 parser->cursor++;
742 }
743 // \M-x always produces a byte >= 0x80
744 pm_regexp_track_hex_escape(parser, 0x80);
745 }
746 } else {
747 parser->cursor++;
748 }
749 break;
750 case 'C':
751 // \C-x produces (x & 0x1F)
752 if (parser->cursor + 2 < parser->end && parser->cursor[1] == '-') {
753 parser->cursor += 2; // skip 'C-'
754 if (!pm_regexp_char_is_eof(parser)) {
755 if (*parser->cursor == '\\') {
756 parser->cursor++;
757 pm_regexp_parse_backslash_escape(parser);
758 } else {
759 parser->cursor++;
760 }
761 }
762 } else {
763 parser->cursor++;
764 }
765 break;
766 case 'c':
767 // \cx produces (x & 0x1F)
768 parser->cursor++; // skip 'c'
769 if (!pm_regexp_char_is_eof(parser)) {
770 if (*parser->cursor == '\\') {
771 parser->cursor++;
772 pm_regexp_parse_backslash_escape(parser);
773 } else {
774 parser->cursor++;
775 }
776 }
777 break;
778 default:
779 pm_regexp_hex_group_boundary(parser);
780 parser->cursor++;
781 break;
782 }
783}
784
790static void
791pm_regexp_parse_invalid_multibyte(pm_regexp_parser_t *parser, const uint8_t *cursor) {
792 uint8_t byte = *cursor;
793 if (byte >= 0x80 && parser->encoding_changed && parser->encoding->multibyte) {
794 size_t width = parser->encoding->char_width(cursor, (ptrdiff_t) (parser->end - cursor));
795 if (width > 1) {
796 parser->cursor += width - 1;
797 } else if (width == 0) {
798 parser->has_invalid_multibyte = true;
799 pm_regexp_parse_error_format(parser, cursor, cursor + 1, PM_ERR_INVALID_MULTIBYTE_CHAR, parser->encoding->name);
800 }
801 }
802}
803
808static bool
809pm_regexp_parse_character_set(pm_regexp_parser_t *parser, uint16_t depth) {
810 pm_regexp_char_accept(parser, '^');
811
812 while (!pm_regexp_char_is_eof(parser) && *parser->cursor != ']') {
813 switch (*parser->cursor++) {
814 case '[':
815 pm_regexp_parse_lbracket(parser, (uint16_t) (depth + 1));
816 break;
817 case '\\':
818 pm_regexp_parse_backslash_escape(parser);
819 break;
820 default:
821 // We've already advanced the cursor by one byte. If the byte
822 // was >= 0x80 in a multibyte encoding, we may need to consume
823 // additional continuation bytes and validate the character.
824 if (*(parser->cursor - 1) >= 0x80) {
825 parser->non_ascii_literal_count++;
826 }
827 pm_regexp_parse_invalid_multibyte(parser, parser->cursor - 1);
828 break;
829 }
830 }
831
832 return pm_regexp_char_expect(parser, ']');
833}
834
838static bool
839pm_regexp_parse_lbracket(pm_regexp_parser_t *parser, uint16_t depth) {
840 if (depth >= PM_REGEXP_PARSE_DEPTH_MAX) {
841 pm_regexp_parse_error(parser, parser->start, parser->end, "parse depth limit over");
842 return false;
843 }
844
845 if ((parser->cursor < parser->end) && parser->cursor[0] == ']') {
846 parser->cursor++;
847 pm_regexp_parse_error(parser, parser->cursor - 1, parser->cursor, "empty char-class");
848 return true;
849 }
850
851 const uint8_t *reset = parser->cursor;
852
853 if ((parser->cursor + 2 < parser->end) && parser->cursor[0] == '[' && parser->cursor[1] == ':') {
854 parser->cursor++;
855 if (pm_regexp_parse_posix_class(parser)) return true;
856
857 parser->cursor = reset;
858 }
859
860 return pm_regexp_parse_character_set(parser, depth);
861}
862
863// Forward declaration here since parsing groups needs to go back up the grammar
864// to parse expressions within them.
865static bool
866pm_regexp_parse_expression(pm_regexp_parser_t *parser, uint16_t depth);
867
872typedef enum {
873 PM_REGEXP_OPTION_STATE_INVALID,
874 PM_REGEXP_OPTION_STATE_TOGGLEABLE,
875 PM_REGEXP_OPTION_STATE_ADDABLE,
876 PM_REGEXP_OPTION_STATE_ADDED,
877 PM_REGEXP_OPTION_STATE_REMOVED
878} pm_regexp_option_state_t;
879
880// These are the options that are configurable on the regular expression (or
881// from within a group).
882
884#define PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM 'a'
885
887#define PRISM_REGEXP_OPTION_STATE_SLOT_MAXIMUM 'x'
888
890#define PRISM_REGEXP_OPTION_STATE_SLOTS (PRISM_REGEXP_OPTION_STATE_SLOT_MAXIMUM - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM + 1)
891
895typedef struct {
897 uint8_t values[PRISM_REGEXP_OPTION_STATE_SLOTS];
899
903static void
904pm_regexp_options_init(pm_regexp_options_t *options) {
905 memset(options, PM_REGEXP_OPTION_STATE_INVALID, sizeof(uint8_t) * PRISM_REGEXP_OPTION_STATE_SLOTS);
906 options->values['i' - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM] = PM_REGEXP_OPTION_STATE_TOGGLEABLE;
907 options->values['m' - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM] = PM_REGEXP_OPTION_STATE_TOGGLEABLE;
908 options->values['x' - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM] = PM_REGEXP_OPTION_STATE_TOGGLEABLE;
909 options->values['d' - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM] = PM_REGEXP_OPTION_STATE_ADDABLE;
910 options->values['a' - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM] = PM_REGEXP_OPTION_STATE_ADDABLE;
911 options->values['u' - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM] = PM_REGEXP_OPTION_STATE_ADDABLE;
912}
913
918static bool
919pm_regexp_options_add(pm_regexp_options_t *options, uint8_t key) {
920 if (key >= PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM && key <= PRISM_REGEXP_OPTION_STATE_SLOT_MAXIMUM) {
921 key = (uint8_t) (key - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM);
922
923 switch (options->values[key]) {
924 case PM_REGEXP_OPTION_STATE_INVALID:
925 case PM_REGEXP_OPTION_STATE_REMOVED:
926 return false;
927 case PM_REGEXP_OPTION_STATE_TOGGLEABLE:
928 case PM_REGEXP_OPTION_STATE_ADDABLE:
929 options->values[key] = PM_REGEXP_OPTION_STATE_ADDED;
930 return true;
931 case PM_REGEXP_OPTION_STATE_ADDED:
932 return true;
933 }
934 }
935
936 return false;
937}
938
943static bool
944pm_regexp_options_remove(pm_regexp_options_t *options, uint8_t key) {
945 if (key >= PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM && key <= PRISM_REGEXP_OPTION_STATE_SLOT_MAXIMUM) {
946 key = (uint8_t) (key - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM);
947
948 switch (options->values[key]) {
949 case PM_REGEXP_OPTION_STATE_INVALID:
950 case PM_REGEXP_OPTION_STATE_ADDABLE:
951 return false;
952 case PM_REGEXP_OPTION_STATE_TOGGLEABLE:
953 case PM_REGEXP_OPTION_STATE_ADDED:
954 case PM_REGEXP_OPTION_STATE_REMOVED:
955 options->values[key] = PM_REGEXP_OPTION_STATE_REMOVED;
956 return true;
957 }
958 }
959
960 return false;
961}
962
966static uint8_t
967pm_regexp_options_state(pm_regexp_options_t *options, uint8_t key) {
968 if (key >= PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM && key <= PRISM_REGEXP_OPTION_STATE_SLOT_MAXIMUM) {
969 key = (uint8_t) (key - PRISM_REGEXP_OPTION_STATE_SLOT_MINIMUM);
970 return options->values[key];
971 }
972
973 return false;
974}
975
997static bool
998pm_regexp_parse_group(pm_regexp_parser_t *parser, uint16_t depth) {
999 const uint8_t *group_start = parser->cursor;
1000
1001 pm_regexp_options_t options;
1002 pm_regexp_options_init(&options);
1003
1004 // First, parse any options for the group.
1005 if (pm_regexp_char_accept(parser, '?')) {
1006 if (pm_regexp_char_is_eof(parser)) {
1007 pm_regexp_parse_error(parser, group_start, parser->cursor, "end pattern in group");
1008 return false;
1009 }
1010
1011 switch (*parser->cursor) {
1012 case '#': { // inline comments
1013 parser->cursor++;
1014 if (pm_regexp_char_is_eof(parser)) {
1015 pm_regexp_parse_error(parser, group_start, parser->cursor, "end pattern in group");
1016 return false;
1017 }
1018
1019 if (parser->encoding_changed && parser->encoding->multibyte) {
1020 bool escaped = false;
1021
1022 // Here we're going to take a slow path and iterate through
1023 // each multibyte character to find the close paren. We do
1024 // this because \ can be a trailing byte in some encodings.
1025 while (parser->cursor < parser->end) {
1026 if (!escaped && *parser->cursor == ')') {
1027 parser->cursor++;
1028 return true;
1029 }
1030
1031 size_t width = parser->encoding->char_width(parser->cursor, (ptrdiff_t) (parser->end - parser->cursor));
1032 if (width == 0) {
1033 if (*parser->cursor >= 0x80) {
1034 parser->has_invalid_multibyte = true;
1035 pm_regexp_parse_error_format(parser, parser->cursor, parser->cursor + 1, PM_ERR_INVALID_MULTIBYTE_CHAR, parser->encoding->name);
1036 parser->cursor++;
1037 continue;
1038 }
1039 return false;
1040 }
1041
1042 escaped = (width == 1) && (*parser->cursor == '\\');
1043 parser->cursor += width;
1044 }
1045
1046 return false;
1047 } else {
1048 // Here we can take the fast path and use memchr to find the
1049 // next ) because we are safe checking backward for \ since
1050 // it cannot be a trailing character.
1051 bool found = pm_regexp_char_find(parser, ')');
1052
1053 while (found && (parser->start <= parser->cursor - 2) && (*(parser->cursor - 2) == '\\')) {
1054 found = pm_regexp_char_find(parser, ')');
1055 }
1056
1057 return found;
1058 }
1059 }
1060 case ':': // non-capturing group
1061 case '=': // positive lookahead
1062 case '!': // negative lookahead
1063 case '>': // atomic group
1064 case '~': // absence operator
1065 parser->cursor++;
1066 break;
1067 case '<':
1068 parser->cursor++;
1069 if (pm_regexp_char_is_eof(parser)) {
1070 pm_regexp_parse_error(parser, group_start, parser->cursor, "end pattern with unmatched parenthesis");
1071 return false;
1072 }
1073
1074 switch (*parser->cursor) {
1075 case '=': // positive lookbehind
1076 case '!': // negative lookbehind
1077 parser->cursor++;
1078 break;
1079 default: { // named capture group
1080 const uint8_t *start = parser->cursor;
1081 if (!pm_regexp_char_find(parser, '>')) {
1082 return false;
1083 }
1084
1085 if (parser->cursor - start == 1) {
1086 pm_regexp_parse_error(parser, start, parser->cursor, "group name is empty");
1087 }
1088
1089 if (parser->name_callback != NULL) {
1090 pm_regexp_parser_named_capture(parser, start, parser->cursor - 1);
1091 }
1092
1093 break;
1094 }
1095 }
1096 break;
1097 case '\'': { // named capture group
1098 const uint8_t *start = ++parser->cursor;
1099 if (!pm_regexp_char_find(parser, '\'')) {
1100 return false;
1101 }
1102
1103 if (parser->name_callback != NULL) {
1104 pm_regexp_parser_named_capture(parser, start, parser->cursor - 1);
1105 }
1106
1107 break;
1108 }
1109 case '(': // conditional expression
1110 if (!pm_regexp_char_find(parser, ')')) {
1111 return false;
1112 }
1113 break;
1114 case 'i': case 'm': case 'x': case 'd': case 'a': case 'u': // options
1115 while (!pm_regexp_char_is_eof(parser) && *parser->cursor != '-' && *parser->cursor != ':' && *parser->cursor != ')') {
1116 if (!pm_regexp_options_add(&options, *parser->cursor)) {
1117 return false;
1118 }
1119 parser->cursor++;
1120 }
1121
1122 if (pm_regexp_char_is_eof(parser)) {
1123 return false;
1124 }
1125
1126 // If we are at the end of the group of options and there is no
1127 // subexpression, then we are going to be setting the options
1128 // for the parent group. In this case we are safe to return now.
1129 if (*parser->cursor == ')') {
1130 if (pm_regexp_options_state(&options, 'x') == PM_REGEXP_OPTION_STATE_ADDED) {
1131 parser->extended_mode = true;
1132 }
1133
1134 parser->cursor++;
1135 return true;
1136 }
1137
1138 // If we hit a -, then we're done parsing options.
1139 if (*parser->cursor != '-') break;
1140
1142 case '-':
1143 parser->cursor++;
1144 while (!pm_regexp_char_is_eof(parser) && *parser->cursor != ':' && *parser->cursor != ')') {
1145 if (!pm_regexp_options_remove(&options, *parser->cursor)) {
1146 return false;
1147 }
1148 parser->cursor++;
1149 }
1150
1151 if (pm_regexp_char_is_eof(parser)) {
1152 return false;
1153 }
1154
1155 // If we are at the end of the group of options and there is no
1156 // subexpression, then we are going to be setting the options
1157 // for the parent group. In this case we are safe to return now.
1158 if (*parser->cursor == ')') {
1159 switch (pm_regexp_options_state(&options, 'x')) {
1160 case PM_REGEXP_OPTION_STATE_ADDED:
1161 parser->extended_mode = true;
1162 break;
1163 case PM_REGEXP_OPTION_STATE_REMOVED:
1164 parser->extended_mode = false;
1165 break;
1166 }
1167
1168 parser->cursor++;
1169 return true;
1170 }
1171
1172 break;
1173 default:
1174 parser->cursor++;
1175 pm_regexp_parse_error(parser, parser->cursor - 1, parser->cursor, "undefined group option");
1176 break;
1177 }
1178 }
1179
1180 bool extended_mode = parser->extended_mode;
1181 switch (pm_regexp_options_state(&options, 'x')) {
1182 case PM_REGEXP_OPTION_STATE_ADDED:
1183 parser->extended_mode = true;
1184 break;
1185 case PM_REGEXP_OPTION_STATE_REMOVED:
1186 parser->extended_mode = false;
1187 break;
1188 }
1189
1190 // Now, parse the expressions within this group.
1191 while (!pm_regexp_char_is_eof(parser) && *parser->cursor != ')') {
1192 if (!pm_regexp_parse_expression(parser, (uint16_t) (depth + 1))) {
1193 parser->extended_mode = extended_mode;
1194 return false;
1195 }
1196 pm_regexp_char_accept(parser, '|');
1197 }
1198
1199 // Finally, make sure we have a closing parenthesis.
1200 parser->extended_mode = extended_mode;
1201 if (pm_regexp_char_expect(parser, ')')) return true;
1202
1203 pm_regexp_parse_error(parser, group_start, parser->cursor, "end pattern with unmatched parenthesis");
1204 return false;
1205}
1206
1219static bool
1220pm_regexp_parse_item(pm_regexp_parser_t *parser, uint16_t depth) {
1221 switch (*parser->cursor) {
1222 case '^':
1223 case '$':
1224 parser->cursor++;
1225 return pm_regexp_parse_quantifier(parser);
1226 case '\\':
1227 parser->cursor++;
1228 pm_regexp_parse_backslash_escape(parser);
1229 return pm_regexp_parse_quantifier(parser);
1230 case '(':
1231 parser->cursor++;
1232 return pm_regexp_parse_group(parser, depth) && pm_regexp_parse_quantifier(parser);
1233 case '[':
1234 parser->cursor++;
1235 return pm_regexp_parse_lbracket(parser, depth) && pm_regexp_parse_quantifier(parser);
1236 case '*':
1237 case '?':
1238 case '+':
1239 parser->cursor++;
1240 pm_regexp_parse_error(parser, parser->cursor - 1, parser->cursor, "target of repeat operator is not specified");
1241 return true;
1242 case ')':
1243 parser->cursor++;
1244 pm_regexp_parse_error(parser, parser->cursor - 1, parser->cursor, "unmatched close parenthesis");
1245 return true;
1246 case '#':
1247 if (parser->extended_mode) {
1248 if (!pm_regexp_char_find(parser, '\n')) parser->cursor = parser->end;
1249 return true;
1250 }
1252 default: {
1253 size_t width;
1254 if (!parser->encoding_changed) {
1255 width = pm_encoding_utf_8_char_width(parser->cursor, (ptrdiff_t) (parser->end - parser->cursor));
1256 } else {
1257 width = parser->encoding->char_width(parser->cursor, (ptrdiff_t) (parser->end - parser->cursor));
1258 }
1259
1260 if (width == 0) {
1261 if (*parser->cursor >= 0x80 && parser->encoding_changed) {
1262 if (parser->encoding->multibyte) {
1263 // Invalid multibyte character in a multibyte encoding.
1264 // Emit the error at the byte location immediately.
1265 parser->has_invalid_multibyte = true;
1266 pm_regexp_parse_error_format(parser, parser->cursor, parser->cursor + 1, PM_ERR_INVALID_MULTIBYTE_CHAR, parser->encoding->name);
1267 } else {
1268 // Non-ASCII byte in a single-byte encoding (e.g.,
1269 // US-ASCII). Count it for later error reporting.
1270 parser->non_ascii_literal_count++;
1271 }
1272 parser->cursor++;
1273 return pm_regexp_parse_quantifier(parser);
1274 }
1275 return false;
1276 }
1277
1278 // Count non-ASCII literal bytes.
1279 for (size_t i = 0; i < width; i++) {
1280 if (parser->cursor[i] >= 0x80) parser->non_ascii_literal_count++;
1281 }
1282
1283 parser->cursor += width;
1284 return pm_regexp_parse_quantifier(parser);
1285 }
1286 }
1287}
1288
1293static bool
1294pm_regexp_parse_expression(pm_regexp_parser_t *parser, uint16_t depth) {
1295 if (depth >= PM_REGEXP_PARSE_DEPTH_MAX) {
1296 pm_regexp_parse_error(parser, parser->start, parser->end, "parse depth limit over");
1297 return false;
1298 }
1299
1300 if (!pm_regexp_parse_item(parser, depth)) {
1301 return false;
1302 }
1303
1304 while (!pm_regexp_char_is_eof(parser) && *parser->cursor != ')' && *parser->cursor != '|') {
1305 if (!pm_regexp_parse_item(parser, depth)) {
1306 return false;
1307 }
1308 }
1309
1310 return true;
1311}
1312
1319static bool
1320pm_regexp_parse_pattern(pm_regexp_parser_t *parser) {
1321 do {
1322 if (pm_regexp_char_is_eof(parser)) return true;
1323 if (!pm_regexp_parse_expression(parser, 0)) return false;
1324 } while (pm_regexp_char_accept(parser, '|'));
1325
1326 return pm_regexp_char_is_eof(parser);
1327}
1328
1329// ---------------------------------------------------------------------------
1330// Encoding validation
1331// ---------------------------------------------------------------------------
1332
1337static bool
1338pm_regexp_validate_hex_escapes(const pm_encoding_t *encoding, const pm_buffer_t *buffer) {
1339 const uint8_t *data = (const uint8_t *) pm_buffer_value(buffer);
1340 size_t len = pm_buffer_length(buffer);
1341 size_t i = 0;
1342
1343 while (i < len) {
1344 size_t group_start = i;
1345 while (i < len && data[i] != 0x00) i++;
1346
1347 for (size_t j = group_start; j < i; ) {
1348 size_t width = encoding->char_width(data + j, (ptrdiff_t) (i - j));
1349 if (width == 0) return false;
1350 j += width;
1351 }
1352
1353 if (i < len) i++; // skip sentinel
1354 }
1355
1356 return true;
1357}
1358
1363static void
1364pm_regexp_format_for_error(pm_buffer_t *buffer, const pm_encoding_t *encoding, const uint8_t *source, size_t length) {
1365 size_t index = 0;
1366
1367 if (encoding == PM_ENCODING_UTF_8_ENTRY) {
1368 pm_buffer_append_string(buffer, (const char *) source, length);
1369 return;
1370 }
1371
1372 while (index < length) {
1373 if (source[index] < 0x80) {
1374 pm_buffer_append_byte(buffer, source[index]);
1375 index++;
1376 } else if (encoding->multibyte) {
1377 size_t width = encoding->char_width(source + index, (ptrdiff_t) (length - index));
1378
1379 if (width > 1) {
1380 pm_buffer_append_string(buffer, "\\x{", 3);
1381 for (size_t i = 0; i < width; i++) {
1382 pm_buffer_append_format(buffer, "%02X", source[index + i]);
1383 }
1384 pm_buffer_append_byte(buffer, '}');
1385 index += width;
1386 } else {
1387 pm_buffer_append_format(buffer, "\\x%02X", source[index]);
1388 index++;
1389 }
1390 } else {
1391 pm_buffer_append_format(buffer, "\\x%02X", source[index]);
1392 index++;
1393 }
1394 }
1395}
1396
1400#define PM_REGEXP_ENCODING_ERROR(parser, diag_id, ...) \
1401 pm_diagnostic_list_append_format( \
1402 &(parser)->parser->metadata_arena, \
1403 &(parser)->parser->error_list, \
1404 (uint32_t) ((parser)->node_start - (parser)->parser->start), \
1405 (uint32_t) ((parser)->node_end - (parser)->node_start), \
1406 diag_id, __VA_ARGS__)
1407
1427static pm_node_flags_t
1428pm_regexp_validate_encoding_modifier(pm_regexp_parser_t *parser, bool ascii_only, pm_node_flags_t flags, char modifier, const pm_encoding_t *modifier_encoding, const char *source_start, int source_length) {
1429
1430 if (parser->explicit_encoding == NULL) {
1431 if (ascii_only) {
1432 // Check property escapes against the modifier's encoding tier.
1433 // /n (ASCII-8BIT): only POSIX properties are valid.
1434 // /e, /s: POSIX and script properties are valid.
1435 // /u: all properties are valid.
1436 if (modifier == 'n' && parser->property_name != NULL) {
1437 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_INVALID_CHAR_PROPERTY,
1438 (int) parser->property_name_length, (const char *) parser->property_name,
1439 source_length, source_start);
1440 } else if (modifier != 'u' && parser->has_unicode_property_escape) {
1441 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_INVALID_CHAR_PROPERTY,
1442 (int) parser->unicode_property_name_length, (const char *) parser->unicode_property_name,
1443 source_length, source_start);
1444 }
1445 return modifier == 'n' ? PM_REGULAR_EXPRESSION_FLAGS_FORCED_US_ASCII_ENCODING : flags;
1446 }
1447
1448 if (parser->encoding == PM_ENCODING_US_ASCII_ENTRY) {
1449 for (uint32_t i = 0; i < parser->non_ascii_literal_count; i++) {
1450 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_CHAR, parser->encoding->name);
1451 }
1452 } else if (parser->encoding != modifier_encoding) {
1453 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_ENCODING_OPTION_MISMATCH, modifier, parser->encoding->name);
1454
1455 if (modifier == 'n' && !ascii_only) {
1456 pm_buffer_t formatted = { 0 };
1457 pm_regexp_format_for_error(&formatted, parser->encoding, (const uint8_t *) source_start, (size_t) source_length);
1458 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_NON_ESCAPED_MBC, (int) formatted.length, (const char *) formatted.value);
1459 pm_buffer_cleanup(&formatted);
1460 }
1461 }
1462
1463 return flags;
1464 }
1465
1466 // Mixed unicode + hex escapes.
1467 if (parser->has_unicode_escape && parser->has_hex_escape) {
1468 if (modifier == 'n') {
1469 if (parser->last_escape_was_unicode) {
1470 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_UTF8_CHAR_NON_UTF8_REGEXP, source_length, source_start);
1471 } else {
1472 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_ESCAPED_NON_ASCII_IN_UTF8, source_length, source_start);
1473 }
1474 } else {
1475 if (!pm_regexp_validate_hex_escapes(modifier_encoding, &parser->hex_escape_buffer)) {
1476 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_ESCAPE, source_length, source_start);
1477 }
1478 }
1479
1480 return flags;
1481 }
1482
1483 if (modifier != 'u' && parser->explicit_encoding == PM_ENCODING_UTF_8_ENTRY) {
1484 if (parser->last_escape_was_unicode) {
1485 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_INCOMPAT_CHAR_ENCODING, source_length, source_start);
1486 } else if (parser->encoding != PM_ENCODING_UTF_8_ENTRY) {
1487 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_INCOMPAT_CHAR_ENCODING, source_length, source_start);
1488 }
1489 }
1490
1491 if (modifier != 'n' && !pm_regexp_validate_hex_escapes(modifier_encoding, &parser->hex_escape_buffer)) {
1492 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_ESCAPE, source_length, source_start);
1493 }
1494
1495 return flags;
1496}
1497
1514static pm_node_flags_t
1515pm_regexp_validate_encoding(pm_regexp_parser_t *parser, bool ascii_only, pm_node_flags_t flags, const char *source_start, int source_length) {
1516
1517 // Invalid multibyte characters suppress further validation.
1518 // Errors were already emitted at the byte locations during parsing.
1519 if (parser->has_invalid_multibyte) {
1520 return flags;
1521 }
1522
1523 if (parser->invalid_unicode_range) {
1524 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_INVALID_UNICODE_RANGE, source_length, source_start);
1525 return flags;
1526 }
1527
1528 // Check modifier flags first.
1529 if (flags & PM_REGULAR_EXPRESSION_FLAGS_ASCII_8BIT) {
1530 return pm_regexp_validate_encoding_modifier(parser, ascii_only, flags, 'n', PM_ENCODING_ASCII_8BIT_ENTRY, source_start, source_length);
1531 }
1532 if (flags & PM_REGULAR_EXPRESSION_FLAGS_UTF_8) {
1533 return pm_regexp_validate_encoding_modifier(parser, ascii_only, flags, 'u', PM_ENCODING_UTF_8_ENTRY, source_start, source_length);
1534 }
1535 if (flags & PM_REGULAR_EXPRESSION_FLAGS_EUC_JP) {
1536 return pm_regexp_validate_encoding_modifier(parser, ascii_only, flags, 'e', PM_ENCODING_EUC_JP_ENTRY, source_start, source_length);
1537 }
1538 if (flags & PM_REGULAR_EXPRESSION_FLAGS_WINDOWS_31J) {
1539 return pm_regexp_validate_encoding_modifier(parser, ascii_only, flags, 's', PM_ENCODING_WINDOWS_31J_ENTRY, source_start, source_length);
1540 }
1541
1542 // No modifier — check for non-ASCII literals in US-ASCII encoding.
1543 if (parser->encoding == PM_ENCODING_US_ASCII_ENTRY && parser->explicit_encoding == NULL && !ascii_only) {
1544 for (uint32_t i = 0; i < parser->non_ascii_literal_count; i++) {
1545 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_CHAR, parser->encoding->name);
1546 }
1547 }
1548
1549 // ASCII-only regexps get downgraded to US-ASCII, unless property escapes
1550 // force UTF-8.
1551 if (ascii_only) {
1552 if (parser->has_property_escape) {
1553 return PM_REGULAR_EXPRESSION_FLAGS_FORCED_UTF8_ENCODING;
1554 }
1555 return PM_REGULAR_EXPRESSION_FLAGS_FORCED_US_ASCII_ENCODING;
1556 }
1557
1558 // Check explicit encoding from escape sequences.
1559 if (parser->explicit_encoding != NULL) {
1560 // Mixed unicode + hex escapes without modifier.
1561 if (parser->has_unicode_escape && parser->has_hex_escape && parser->encoding != PM_ENCODING_UTF_8_ENTRY) {
1562 if (parser->encoding != PM_ENCODING_US_ASCII_ENTRY &&
1563 parser->encoding != PM_ENCODING_ASCII_8BIT_ENTRY &&
1564 !pm_regexp_validate_hex_escapes(parser->encoding, &parser->hex_escape_buffer)) {
1565 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_ESCAPE, source_length, source_start);
1566 } else if (parser->last_escape_was_unicode) {
1567 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_UTF8_CHAR_NON_UTF8_REGEXP, source_length, source_start);
1568 } else {
1569 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_REGEXP_ESCAPED_NON_ASCII_IN_UTF8, source_length, source_start);
1570 }
1571
1572 return 0;
1573 }
1574
1575 if (parser->explicit_encoding == PM_ENCODING_UTF_8_ENTRY) {
1576 if (!pm_regexp_validate_hex_escapes(parser->explicit_encoding, &parser->hex_escape_buffer)) {
1577 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_ESCAPE, source_length, source_start);
1578 }
1579
1580 return PM_REGULAR_EXPRESSION_FLAGS_FORCED_UTF8_ENCODING;
1581 } else if (parser->encoding == PM_ENCODING_US_ASCII_ENTRY) {
1582 return PM_REGULAR_EXPRESSION_FLAGS_FORCED_BINARY_ENCODING;
1583 } else {
1584 if (!pm_regexp_validate_hex_escapes(parser->explicit_encoding, &parser->hex_escape_buffer)) {
1585 PM_REGEXP_ENCODING_ERROR(parser, PM_ERR_INVALID_MULTIBYTE_ESCAPE, source_length, source_start);
1586 }
1587 }
1588 }
1589
1590 return 0;
1591}
1592
1600pm_node_flags_t
1601pm_regexp_parse(pm_parser_t *parser, pm_regular_expression_node_t *node, pm_regexp_name_callback_t name_callback, void *name_data) {
1602 const uint8_t *source = parser->start + node->content_loc.start;
1603 size_t size = node->content_loc.length;
1604 bool extended_mode = PM_NODE_FLAG_P(node, PM_REGULAR_EXPRESSION_FLAGS_EXTENDED);
1605 pm_node_flags_t flags = PM_NODE_FLAGS(node);
1606
1607 const uint8_t *node_start = parser->start + node->base.location.start;
1608 const uint8_t *node_end = parser->start + node->base.location.start + node->base.location.length;
1609
1610 // First pass: walk raw source for encoding validation (no name extraction).
1611 pm_regexp_parser_t regexp_parser = {
1612 .parser = parser,
1613 .start = source,
1614 .cursor = source,
1615 .end = source + size,
1616 .extended_mode = extended_mode,
1617 .encoding_changed = parser->encoding_changed,
1618 .encoding = parser->encoding,
1619 .name_callback = NULL,
1620 .name_data = NULL,
1621 .report_errors = true,
1622 .node_start = node_start,
1623 .node_end = node_end,
1624 .has_unicode_escape = false,
1625 .has_hex_escape = false,
1626 .last_escape_was_unicode = false,
1627 .explicit_encoding = NULL,
1628 .has_property_escape = false,
1629 .has_unicode_property_escape = false,
1630 .property_name = NULL,
1631 .property_name_length = 0,
1632 .unicode_property_name = NULL,
1633 .unicode_property_name_length = 0,
1634 .non_ascii_literal_count = 0,
1635 .invalid_unicode_range = false,
1636 .hex_escape_buffer = { 0 },
1637 .hex_group_active = false,
1638 .has_invalid_multibyte = false,
1639 };
1640
1641 pm_regexp_parse_pattern(&regexp_parser);
1642
1643 // Compute ascii_only from the regexp parser's tracked state. We cannot
1644 // use node->unescaped for this because regexp unescaped content preserves
1645 // escape text (e.g., \x80 is 4 ASCII chars), not the binary values.
1646 bool ascii_only = !regexp_parser.has_hex_escape && !regexp_parser.has_unicode_escape && regexp_parser.non_ascii_literal_count == 0;
1647 // Use the unescaped content for error messages to match CRuby's format,
1648 // where Ruby escapes like \M-\C-? are resolved to bytes but regexp escapes
1649 // like \u{80} are preserved as text.
1650 const char *error_source = (const char *) pm_string_source(&node->unescaped);
1651 int error_source_length = (int) pm_string_length(&node->unescaped);
1652 pm_node_flags_t encoding_flags = pm_regexp_validate_encoding(&regexp_parser, ascii_only, flags, error_source, error_source_length);
1653 pm_buffer_cleanup(&regexp_parser.hex_escape_buffer);
1654
1655 // Second pass: walk unescaped content for named capture extraction.
1656 if (name_callback != NULL) {
1657 pm_regexp_parse_named_captures(parser, pm_string_source(&node->unescaped), pm_string_length(&node->unescaped), extended_mode, name_callback, name_data);
1658 }
1659
1660 return encoding_flags;
1661}
1662
1673void
1674pm_regexp_parse_named_captures(pm_parser_t *parser, const uint8_t *source, size_t size, bool extended_mode, pm_regexp_name_callback_t name_callback, void *name_data) {
1675 pm_regexp_parser_t regexp_parser = {
1676 .parser = parser,
1677 .start = source,
1678 .cursor = source,
1679 .end = source + size,
1680 .extended_mode = extended_mode,
1681 .encoding_changed = parser->encoding_changed,
1682 .encoding = parser->encoding,
1683 .name_callback = name_callback,
1684 .name_data = name_data,
1685 .report_errors = false,
1686 .has_unicode_escape = false,
1687 .has_hex_escape = false,
1688 .last_escape_was_unicode = false,
1689 .explicit_encoding = NULL,
1690 .has_property_escape = false,
1691 .has_unicode_property_escape = false,
1692 .property_name = NULL,
1693 .property_name_length = 0,
1694 .unicode_property_name = NULL,
1695 .unicode_property_name_length = 0,
1696 .non_ascii_literal_count = 0,
1697 .invalid_unicode_range = false,
1698 .hex_escape_buffer = { 0 },
1699 .hex_group_active = false,
1700 .has_invalid_multibyte = false,
1701 };
1702
1703 pm_regexp_parse_pattern(&regexp_parser);
1704 pm_buffer_cleanup(&regexp_parser.hex_escape_buffer);
1705}
#define PRISM_FALLTHROUGH
We use -Wimplicit-fallthrough to guard potentially unintended fall-through between cases of a switch.
Definition fallthrough.h:15
int len
Length of the buffer.
Definition io.h:8
#define PRISM_INLINE
Old Visual Studio versions do not support the inline keyword, so we need to define it to be __inline.
Definition inline.h:12
VALUE type(ANYARGS)
ANYARGS-ed function type.
uint32_t start
The offset of the location from the start of the source.
Definition ast.h:581
uint32_t length
The length of the location.
Definition ast.h:584
pm_location_t location
This is the location of the node in the source.
Definition ast.h:1115
This is the set of options that are configurable on the regular expression.
Definition regexp.c:895
uint8_t values[PRISM_REGEXP_OPTION_STATE_SLOTS]
The current state of each option.
Definition regexp.c:897
This is the parser that is going to handle parsing regular expressions.
Definition regexp.c:23
const uint8_t * cursor
A pointer to the current position in the source.
Definition regexp.c:31
bool has_unicode_escape
Whether a \u{...} escape with value >= 0x80 was seen.
Definition regexp.c:102
size_t unicode_property_name_length
Length of the first Unicode-only property name found.
Definition regexp.c:76
bool has_hex_escape
Whether a \xNN escape (or \M-x, etc.) with value >= 0x80 was seen.
Definition regexp.c:105
const uint8_t * start
A pointer to the start of the source that we are parsing.
Definition regexp.c:28
const uint8_t * end
A pointer to the end of the source that we are parsing.
Definition regexp.c:34
void * name_data
The data to pass to the name callback.
Definition regexp.c:43
bool hex_group_active
Whether we are accumulating consecutive hex escape bytes.
Definition regexp.c:123
pm_buffer_t hex_escape_buffer
Buffer of hex escape byte values >= 0x80, separated by 0x00 sentinels.
Definition regexp.c:79
bool extended_mode
Whether or not the regular expression currently being parsed is in extended mode, wherein whitespace ...
Definition regexp.c:88
pm_parser_t * parser
The parser that is currently being used.
Definition regexp.c:25
size_t property_name_length
Length of the first non-POSIX property name found.
Definition regexp.c:67
bool invalid_unicode_range
Whether a \u escape with invalid range (surrogate or > 0x10FFFF) was seen.
Definition regexp.c:120
const uint8_t * property_name
Pointer to the first non-POSIX property name (for /n error messages).
Definition regexp.c:64
const pm_encoding_t * encoding
The encoding of the source.
Definition regexp.c:37
const pm_encoding_t * explicit_encoding
The explicit encoding determined by escape sequences.
Definition regexp.c:56
const uint8_t * node_end
The end of the regexp node (for error locations).
Definition regexp.c:49
uint32_t non_ascii_literal_count
Count of non-ASCII literal bytes (not from escapes).
Definition regexp.c:82
const uint8_t * node_start
The start of the regexp node (for error locations).
Definition regexp.c:46
bool report_errors
Whether syntax errors are reported.
Definition regexp.c:99
const uint8_t * unicode_property_name
Pointer to the first Unicode-only property name (for /e, /s error messages).
Definition regexp.c:73
pm_regexp_name_callback_t name_callback
The callback to call when a named capture group is found.
Definition regexp.c:40
bool encoding_changed
Whether the encoding has changed from the default.
Definition regexp.c:91
bool has_unicode_property_escape
Whether a Unicode-only property escape was found (not POSIX or script).
Definition regexp.c:117
bool has_invalid_multibyte
Whether an invalid multibyte character was found during parsing.
Definition regexp.c:126
bool last_escape_was_unicode
Tracks whether the last encoding-setting escape was \u (true) or \x (false).
Definition regexp.c:111
bool has_property_escape
Whether any \p{...} or \P{...} property escape was found.
Definition regexp.c:114
RegularExpressionNode.
Definition ast.h:7003
pm_node_t base
The embedded base node.
Definition ast.h:7005
pm_string_t unescaped
RegularExpressionNode::unescaped.
Definition ast.h:7025
pm_location_t content_loc
RegularExpressionNode::content_loc.
Definition ast.h:7015
A view of a sequence of bytes.
Definition stringy.h:23