fix(js): decode FormCalc Unicode string escapes and preserve chained unary not/! (#6122)

This commit is contained in:
jonathanwxh-cell authored and GitHub committed 2026-11-09 08:43:09 +08:00
1 parent 9b2c8d6026
commit 19619ad2cc
2 files changed
+152 -48

No files matched your search

+123 -44
View File
@@ -49,52 +49,12 @@ pub(super) fn lex(src: &str) -> Result<Vec<Token>, String> {
}
}
'"' => {
let mut s = String::new();
i += 1;
loop {
match chars.get(i) {
None => return Err(format!("line {line}: unterminated string")),
Some('"') if chars.get(i + 1) == Some(&'"') => {
s.push('"');
i += 2;
}
Some('"') => {
i += 1;
break;
}
Some(&ch) => {
if ch == '\n' {
line += 1;
}
s.push(ch);
i += 1;
}
}
if s.len() > MAX_STRING {
return Err(format!("line {line}: a string is too long"));
}
}
out.push(Token { tok: Tok::Str(s), line });
let tok_line = line;
let s = lex_string(&chars, &mut i, &mut line)?;
out.push(Token { tok: Tok::Str(s), line: tok_line });
}
c if c.is_ascii_digit() || (c == '.' && chars.get(i + 1).is_some_and(char::is_ascii_digit)) => {
let start = i;
while i < chars.len() && (chars[i].is_ascii_digit() || (chars[i] == '.' && chars.get(i + 1) != Some(&'.'))) {
i += 1;
}
if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') {
let mut j = i + 1;
if j < chars.len() && (chars[j] == '+' || chars[j] == '-') {
j += 1;
}
if j < chars.len() && chars[j].is_ascii_digit() {
i = j;
while i < chars.len() && chars[i].is_ascii_digit() {
i += 1;
}
}
}
let text: String = chars[start..i].iter().collect();
let n: f64 = text.parse().map_err(|_| format!("line {line}: bad number {text}"))?;
let n = lex_number(&chars, &mut i, line)?;
out.push(Token { tok: Tok::Num(n), line });
}
'!' => {
@@ -127,3 +87,122 @@ pub(super) fn lex(src: &str) -> Result<Vec<Token>, String> {
out.push(Token { tok: Tok::Eof, line });
Ok(out)
}
fn hex_u32(digits: &[char]) -> Option<u32> {
let mut v = 0u32;
for &c in digits {
v = (v << 4) | c.to_digit(16)?;
}
Some(v)
}
/// FormCalc string escape (`\uXXXX` or `\UXXXXXXXX`, XFA 3.3 part 8 p. 1054); any other `\` stays literal.
fn unicode_escape(chars: &[char], at: usize) -> Option<(char, usize)> {
if chars.get(at) != Some(&'\\') {
return None;
}
match chars.get(at + 1)? {
'u' => {
let hi = hex_u32(chars.get(at + 2..at + 6)?)?;
if (0xD800..=0xDBFF).contains(&hi)
&& chars.get(at + 6..at + 8) == Some(&['\\', 'u'])
&& let Some(lo) = chars.get(at + 8..at + 12).and_then(hex_u32).filter(|lo| (0xDC00..=0xDFFF).contains(lo))
{
let scalar = 0x1_0000 + ((hi - 0xD800) << 10) + (lo - 0xDC00);
return Some((char::from_u32(scalar)?, 12));
}
Some((char::from_u32(hi)?, 6))
}
'U' => {
let code = hex_u32(chars.get(at + 2..at + 10)?)?;
Some((char::from_u32(code)?, 10))
}
_ => None,
}
}
fn lex_string(chars: &[char], i: &mut usize, line: &mut usize) -> Result<String, String> {
let mut s = String::new();
*i += 1;
loop {
match chars.get(*i) {
None => return Err(format!("line {line}: unterminated string")),
Some('"') if chars.get(*i + 1) == Some(&'"') => {
s.push('"');
*i += 2;
}
Some('"') => {
*i += 1;
return Ok(s);
}
Some(&ch) => {
if let Some((decoded, step)) = unicode_escape(chars, *i) {
s.push(decoded);
*i += step;
} else {
if ch == '\n' {
*line += 1;
}
s.push(ch);
*i += 1;
}
}
}
if s.len() > MAX_STRING {
return Err(format!("line {line}: a string is too long"));
}
}
}
/// Whether `c` may start a FormCalc identifier.
fn is_ident_start(c: char) -> bool {
c.is_alphabetic() || c == '_' || c == '$'
}
/// End index of a numeric exponent starting at `at` (`e`/`E`, optional sign, digits), or `None`
/// if no valid exponent starts there or if identifier characters immediately follow the digits.
fn exponent_end(chars: &[char], at: usize) -> Option<usize> {
if !matches!(chars.get(at), Some('e' | 'E')) {
return None;
}
let mut j = at + 1;
if matches!(chars.get(j), Some('+' | '-')) {
j += 1;
}
if !chars.get(j).is_some_and(char::is_ascii_digit) {
return None;
}
while chars.get(j).is_some_and(char::is_ascii_digit) {
j += 1;
}
if chars.get(j).is_some_and(|&c| c.is_alphanumeric() || c == '_' || c == '$') {
return None;
}
Some(j)
}
/// Scan a FormCalc number literal starting at `*i`, stopping before `.member` or `..` while
/// rejecting malformed multi-dot literals such as `1.2.3`.
fn lex_number(chars: &[char], i: &mut usize, line: usize) -> Result<f64, String> {
let start = *i;
let mut seen_dot = false;
while *i < chars.len() {
let next = chars.get(*i + 1).copied();
let next_is_member = next.is_some_and(is_ident_start) && exponent_end(chars, *i + 1).is_none();
if chars[*i].is_ascii_digit() {
*i += 1;
} else if chars[*i] == '.' && !seen_dot && next != Some('.') && !next_is_member {
seen_dot = true;
*i += 1;
} else if chars[*i] == '.' && seen_dot && next.is_some_and(|c| c.is_ascii_digit()) {
return Err(format!("line {line}: bad number"));
} else {
break;
}
}
if let Some(end) = exponent_end(chars, *i) {
*i = end;
}
let text: String = chars[start..*i].iter().collect();
text.parse().map_err(|_| format!("line {line}: bad number {text}"))
}
+29 -4
View File
@@ -341,10 +341,7 @@ impl Parser {
Ok(Expr::Neg(Box::new(self.unary()?)))
} else if self.eat_op("+") {
self.unary()
} else if self.eat_kw("not") || matches!(self.peek(), Tok::Ident(w) if w == "!") {
if matches!(self.peek(), Tok::Ident(w) if w == "!") {
self.at += 1;
}
} else if self.eat_kw("not") || self.eat_kw("!") {
Ok(Expr::Not(Box::new(self.unary()?)))
} else {
self.postfix()
@@ -449,3 +446,31 @@ pub(super) fn parse(src: &str) -> Result<Vec<Stmt>, String> {
t => Err(format!("line {}: unexpected {}", p.line(), describe(t))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unary_not_does_not_swallow_following_bang() {
assert_eq!(parse("not !1"), Ok(vec![Stmt::Expr(Expr::Not(Box::new(Expr::Not(Box::new(Expr::Num(1.0))))))]));
assert_eq!(parse("not !flag"), Ok(vec![Stmt::Expr(Expr::Not(Box::new(Expr::Not(Box::new(Expr::Ident("flag".into()))))))]));
assert_eq!(parse("!!1"), Ok(vec![Stmt::Expr(Expr::Not(Box::new(Expr::Not(Box::new(Expr::Num(1.0))))))]));
}
#[test]
fn string_literals_decode_xfa_unicode_escapes_and_keep_other_backslashes() {
assert_eq!(parse(r#""\u0041\u20ac\U00000042\U0001F600""#), Ok(vec![Stmt::Expr(Expr::Str("A\u{20ac}B\u{1F600}".into()))]));
assert_eq!(parse(r#""\uD83D\uDE00""#), Ok(vec![Stmt::Expr(Expr::Str("\u{1F600}".into()))]));
assert_eq!(parse(r#""C:\temp\new\u12""#), Ok(vec![Stmt::Expr(Expr::Str(r"C:\temp\new\u12".into()))]));
}
#[test]
fn number_literals_stop_before_second_dot_for_member_access() {
assert_eq!(parse("1.5.rawValue"), Ok(vec![Stmt::Expr(Expr::Member(Box::new(Expr::Num(1.5)), "rawValue".into()))]));
assert_eq!(parse("1.rawValue"), Ok(vec![Stmt::Expr(Expr::Member(Box::new(Expr::Num(1.0)), "rawValue".into()))]));
assert_eq!(parse("1.e2foo"), Ok(vec![Stmt::Expr(Expr::Member(Box::new(Expr::Num(1.0)), "e2foo".into()))]));
assert_eq!(parse("1.e2"), Ok(vec![Stmt::Expr(Expr::Num(100.0))]));
assert!(parse("1.2.3").is_err(), "a second decimal point followed by digits is a bad number");
}
}