pub type GroupName = Box<[u16]>;
const GROUP_NAME_BUFFER_SIZE: usize = 128;
const UTF8_CHAR_LEN_MAX: usize = 6;
const CAPTURE_COUNT_MAX: u32 = u8::MAX as u32;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct CaptureSummary {
pub(super) capture_count: u32,
pub(super) has_named_captures: bool,
}
pub(super) fn parse(units: &[u16], start: usize) -> Option<(GroupName, usize)> {
let mut position = start;
let mut normalized = Vec::new();
let mut normalized_utf8_len = 0_usize;
loop {
let unit = *units.get(position)?;
if unit == u16::from(b'>') {
if normalized.is_empty() {
return None;
}
return Some((normalized.into_boxed_slice(), position + 1));
}
let code_point = if unit == u16::from(b'\\') {
if units.get(position + 1) != Some(&u16::from(b'u')) {
return None;
}
position += 2;
parse_unicode_escape(units, &mut position)?
} else {
position += 1;
if is_high_surrogate(unit) {
match units.get(position).copied() {
Some(low) if is_low_surrogate(low) => {
position += 1;
combine_surrogates(unit, low)
}
_ => u32::from(unit),
}
} else {
u32::from(unit)
}
};
let first = normalized.is_empty();
if !(if first {
is_identifier_start(code_point)
} else {
is_identifier_continue(code_point)
}) {
return None;
}
if normalized_utf8_len + UTF8_CHAR_LEN_MAX + 1 > GROUP_NAME_BUFFER_SIZE {
return None;
}
normalized_utf8_len += utf8_len(code_point);
push_code_point(&mut normalized, code_point);
}
}
pub(super) fn capture_summary(units: &[u16]) -> CaptureSummary {
let mut capture_index = 1_u32;
let mut has_named_captures = false;
let mut position = 0_usize;
while position < units.len() {
match units[position] {
unit if unit == u16::from(b'(') => {
let plain = units.get(position + 1) != Some(&u16::from(b'?'));
let named = units.get(position + 1) == Some(&u16::from(b'?'))
&& units.get(position + 2) == Some(&u16::from(b'<'))
&& !matches!(units.get(position + 3).copied(), Some(0x3d | 0x21));
if named {
has_named_captures = true;
}
if plain || named {
capture_index += 1;
if capture_index >= CAPTURE_COUNT_MAX {
break;
}
}
}
unit if unit == u16::from(b'\\') => {
position = position.saturating_add(1);
}
unit if unit == u16::from(b'[') => {
position += 1;
while position < units.len() && units[position] != u16::from(b']') {
if units[position] == u16::from(b'\\') {
position = position.saturating_add(1);
}
position = position.saturating_add(1);
}
}
_ => {}
}
position = position.saturating_add(1);
}
CaptureSummary {
capture_count: capture_index,
has_named_captures,
}
}
pub(super) fn matching_capture_indices(units: &[u16], name: &[u16]) -> Vec<u8> {
let mut captures = Vec::new();
let mut capture_index = 1_u32;
let mut position = 0_usize;
while position < units.len() {
match units[position] {
unit if unit == u16::from(b'(') => {
let plain = units.get(position + 1) != Some(&u16::from(b'?'));
let named = units.get(position + 1) == Some(&u16::from(b'?'))
&& units.get(position + 2) == Some(&u16::from(b'<'))
&& !matches!(units.get(position + 3).copied(), Some(0x3d | 0x21));
if named {
if let Some((candidate, after_name)) = parse(units, position + 3) {
if *candidate == *name {
captures.push(
u8::try_from(capture_index)
.expect("capture prepass exceeded QuickJS's u8 limit"),
);
}
capture_index += 1;
if capture_index >= CAPTURE_COUNT_MAX {
break;
}
position = after_name;
continue;
} else {
position += 3;
}
}
if plain || named {
capture_index += 1;
if capture_index >= CAPTURE_COUNT_MAX {
break;
}
}
}
unit if unit == u16::from(b'\\') => {
position = position.saturating_add(1);
}
unit if unit == u16::from(b'[') => {
position += 1;
while position < units.len() && units[position] != u16::from(b']') {
if units[position] == u16::from(b'\\') {
position = position.saturating_add(1);
}
position = position.saturating_add(1);
}
}
_ => {}
}
position = position.saturating_add(1);
}
captures
}
fn parse_unicode_escape(units: &[u16], position: &mut usize) -> Option<u32> {
let mut code_point;
if units.get(*position) == Some(&u16::from(b'{')) {
*position += 1;
code_point = 0_u32;
let mut digits = 0_usize;
loop {
let digit = hex_value(*units.get(*position)?)?;
digits += 1;
code_point = code_point.checked_mul(16)?.checked_add(digit)?;
if code_point > 0x10_ffff {
return None;
}
*position += 1;
if units.get(*position) == Some(&u16::from(b'}')) {
*position += 1;
break;
}
}
debug_assert!(digits > 0);
} else {
code_point = parse_four_hex(units, position)?;
if is_high_surrogate_u32(code_point)
&& units.get(*position) == Some(&u16::from(b'\\'))
&& units.get(*position + 1) == Some(&u16::from(b'u'))
{
let mut low_position = *position + 2;
if let Some(low) = parse_four_hex(units, &mut low_position)
&& is_low_surrogate_u32(low)
{
*position = low_position;
code_point = combine_surrogates_u32(code_point, low);
}
}
}
Some(code_point)
}
fn parse_four_hex(units: &[u16], position: &mut usize) -> Option<u32> {
let mut value = 0_u32;
for _ in 0..4 {
value = value * 16 + hex_value(*units.get(*position)?)?;
*position += 1;
}
Some(value)
}
const fn hex_value(unit: u16) -> Option<u32> {
match unit {
0x30..=0x39 => Some((unit - 0x30) as u32),
0x41..=0x46 => Some((unit - 0x41 + 10) as u32),
0x61..=0x66 => Some((unit - 0x61 + 10) as u32),
_ => None,
}
}
fn is_identifier_start(code_point: u32) -> bool {
if code_point < 0x80 {
matches!(code_point, 0x24 | 0x41..=0x5a | 0x5f | 0x61..=0x7a)
} else {
crate::unicode::is_id_start(code_point)
}
}
fn is_identifier_continue(code_point: u32) -> bool {
if code_point < 0x80 {
is_identifier_start(code_point) || (0x30..=0x39).contains(&code_point)
} else {
matches!(code_point, 0x200c | 0x200d) || crate::unicode::is_id_continue(code_point)
}
}
const fn utf8_len(code_point: u32) -> usize {
match code_point {
0..=0x7f => 1,
0x80..=0x7ff => 2,
0x800..=0xffff => 3,
_ => 4,
}
}
fn push_code_point(output: &mut Vec<u16>, code_point: u32) {
if code_point <= 0xffff {
output.push(code_point as u16);
} else {
let scalar = code_point - 0x1_0000;
output.push(0xd800 | (scalar >> 10) as u16);
output.push(0xdc00 | (scalar & 0x3ff) as u16);
}
}
const fn is_high_surrogate(unit: u16) -> bool {
unit >= 0xd800 && unit <= 0xdbff
}
const fn is_low_surrogate(unit: u16) -> bool {
unit >= 0xdc00 && unit <= 0xdfff
}
const fn is_high_surrogate_u32(code_point: u32) -> bool {
code_point >= 0xd800 && code_point <= 0xdbff
}
const fn is_low_surrogate_u32(code_point: u32) -> bool {
code_point >= 0xdc00 && code_point <= 0xdfff
}
const fn combine_surrogates(high: u16, low: u16) -> u32 {
combine_surrogates_u32(high as u32, low as u32)
}
const fn combine_surrogates_u32(high: u32, low: u32) -> u32 {
0x1_0000 + ((high - 0xd800) << 10) + (low - 0xdc00)
}
#[cfg(test)]
mod tests {
use super::*;
fn parsed(source: &str) -> Option<String> {
let units = source.encode_utf16().collect::<Vec<_>>();
parse(&units, 0).map(|(name, after)| {
assert_eq!(after, units.len());
String::from_utf16_lossy(&name)
})
}
#[test]
fn group_names_normalize_raw_and_escaped_unicode() {
for source in ["a$𐒤_\u{200C}>", r"\u0061$\uD801\uDCA4_\u200C>"] {
assert_eq!(parsed(source).unwrap(), "a$𐒤_\u{200c}", "{source}",);
}
assert_eq!(parsed(r"\u{41}>").unwrap(), "A");
}
#[test]
fn group_names_reject_invalid_identifier_forms_and_escapes() {
for source in [
">",
"0a>",
"🦊>",
r"\x61>",
r"\u{}>",
r"\u{110000}>",
r"\uD800>",
"a->",
"unterminated",
] {
assert!(parsed(source).is_none(), "{source}");
}
}
#[test]
fn group_name_uses_quickjs_historical_128_byte_guard() {
assert!(parsed(&format!("{}>", "a".repeat(122))).is_some());
assert!(parsed(&format!("{}>", "a".repeat(123))).is_none());
assert!(parsed(&format!("{}𐒤>", "a".repeat(121))).is_some());
}
#[test]
fn capture_scans_skip_escapes_and_classes_and_normalize_names() {
let units = r"\((?<a>x)[(](b)(?<\u0061>y)"
.encode_utf16()
.collect::<Vec<_>>();
assert_eq!(
capture_summary(&units),
CaptureSummary {
capture_count: 4,
has_named_captures: true,
},
);
let name = "a".encode_utf16().collect::<Vec<_>>();
assert_eq!(matching_capture_indices(&units, &name), vec![1, 3]);
}
}