mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
fix(js): decode FormCalc Unicode string escapes and preserve chained unary not/! (#6122)
This commit is contained in:
1 parent
9b2c8d6026
commit
19619ad2cc
2 files changed
+152
-48
No files matched your search
+123
-44
@@ -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}"))
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user