Skip to main content

serpent_serializer/
load.rs

1//! The deserializer.
2//!
3//! Parses the Lua subset that the serializer emits and rebuilds a [`Value`].
4//! Handles both forms: a bare expression from `line` and `block`, and the
5//! `do local _ = ...; ... return _ end` block from `dump`, including the
6//! self-reference section that wires shared and cyclic references.
7//!
8//! Safety follows serpent's sandbox. In safe mode any function call in the
9//! input is rejected with "cannot call functions", and any global assignment or
10//! read resolves against a sandbox, so it cannot touch real globals. Unsafe mode
11//! is not modeled because this loader has no real global environment to expose.
12
13use crate::value::{Global, Key, Table, Value};
14use std::collections::HashMap;
15
16/// Options for [`load`].
17#[derive(Default)]
18pub struct LoadOptions {
19    /// When `false`, allow function calls in the input. Defaults to safe (true).
20    /// The exact serpent gate is `safe == false`, so only an explicit `false`
21    /// disables safety.
22    pub safe: Option<bool>,
23}
24
25/// An error from [`load`].
26///
27/// Each variant names a distinct failure the parser can hit, so callers can
28/// match on the mode instead of parsing a message string.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum LoadError {
31    /// A function call reached in safe mode. The sandbox rejects calls.
32    UnsafeCall,
33    /// An unexpected byte at a source position. `byte` is `None` at end of input.
34    UnexpectedByte {
35        /// The offending byte, or `None` at end of input.
36        byte: Option<u8>,
37        /// The byte offset in the input.
38        pos: usize,
39    },
40    /// A string or table that never closed. `what` names which one.
41    Unterminated {
42        /// Either `"string"` or `"table"`.
43        what: &'static str,
44    },
45    /// A `return NAME` in a `do` block that names an undeclared local.
46    UnknownVariable(String),
47    /// A numeric literal that did not parse.
48    BadNumber(String),
49    /// Any other syntax error, with a message.
50    Syntax(String),
51}
52
53impl std::fmt::Display for LoadError {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        match self {
56            LoadError::UnsafeCall => write!(f, "cannot call functions"),
57            LoadError::UnexpectedByte { byte, pos } => {
58                write!(f, "unexpected byte {byte:?} at {pos}")
59            }
60            LoadError::Unterminated { what } => write!(f, "unterminated {what}"),
61            LoadError::UnknownVariable(name) => write!(f, "unknown variable {name}"),
62            LoadError::BadNumber(text) => write!(f, "bad number {text}"),
63            LoadError::Syntax(msg) => write!(f, "{msg}"),
64        }
65    }
66}
67
68impl std::error::Error for LoadError {}
69
70/// Deserialize a serpent fragment.
71///
72/// Accepts either a bare expression or a `do ... end` block. Returns `Ok(value)`
73/// on success. Returns a [`LoadError`] on a parse error, or in safe mode when
74/// the input tries to call a function.
75///
76/// # Errors
77///
78/// Returns [`LoadError`] when parsing fails or when a call is rejected in safe
79/// mode.
80pub fn load(data: &str, opts: &LoadOptions) -> Result<Value, LoadError> {
81    let safe = opts.safe != Some(false);
82    let mut p = Parser::new(data, safe);
83    p.parse_chunk()
84}
85
86struct Parser<'a> {
87    src: &'a [u8],
88    pos: usize,
89    safe: bool,
90    /// Local variables declared in a `do ... end` block, by name.
91    locals: HashMap<String, Value>,
92    /// Stable ids for global names, so a global used twice stays one identity.
93    global_ids: HashMap<String, usize>,
94    next_global: usize,
95}
96
97impl<'a> Parser<'a> {
98    fn new(src: &'a str, safe: bool) -> Self {
99        Parser {
100            src: src.as_bytes(),
101            pos: 0,
102            safe,
103            locals: HashMap::new(),
104            global_ids: HashMap::new(),
105            next_global: 1,
106        }
107    }
108
109    /// A `Global` value standing in for a name looked up in the sandbox. Two
110    /// lookups of the same name share one identity, matching serpent's sandbox
111    /// where every global read returns the same table.
112    fn global_value(&mut self, name: String) -> Value {
113        let id = *self.global_ids.entry(name.clone()).or_insert_with(|| {
114            let id = self.next_global;
115            self.next_global += 1;
116            id
117        });
118        Value::Global(Global { name, id })
119    }
120
121    fn parse_chunk(&mut self) -> Result<Value, LoadError> {
122        self.skip_ws();
123        let v = if self.looking_at("do") {
124            self.parse_do_block()?
125        } else {
126            self.parse_expr()?
127        };
128        self.skip_comment();
129        self.skip_ws();
130        if let Some(byte) = self.peek() {
131            return Err(LoadError::UnexpectedByte {
132                byte: Some(byte),
133                pos: self.pos,
134            });
135        }
136        Ok(v)
137    }
138
139    /// Parse `do local NAME = EXPR; ...statements...; [return NAME;] end`.
140    ///
141    /// serpent's `dump` always ends with `return NAME`, but the sandbox test
142    /// inputs omit it, so a block without a `return` yields nil.
143    fn parse_do_block(&mut self) -> Result<Value, LoadError> {
144        self.expect_word("do")?;
145        self.skip_ws();
146        loop {
147            self.skip_ws();
148            if self.looking_at("return") || self.at_end_keyword() {
149                break;
150            }
151            if self.looking_at("local") {
152                self.parse_local()?;
153            } else {
154                self.parse_statement()?;
155            }
156            self.skip_sep();
157        }
158        let mut val = Value::Nil;
159        if self.looking_at("return") {
160            self.expect_word("return")?;
161            self.skip_ws();
162            let name = self.parse_name()?;
163            val = self
164                .locals
165                .get(&name)
166                .cloned()
167                .ok_or(LoadError::UnknownVariable(name))?;
168            self.skip_sep();
169        }
170        self.skip_ws();
171        self.expect_word("end")?;
172        Ok(val)
173    }
174
175    /// Whether the cursor is at the `end` keyword, not just any word starting
176    /// with those letters.
177    fn at_end_keyword(&self) -> bool {
178        if !self.looking_at("end") {
179            return false;
180        }
181        match self.src.get(self.pos + 3) {
182            None => true,
183            Some(b) => !(b.is_ascii_alphanumeric() || *b == b'_'),
184        }
185    }
186
187    /// Parse `local NAME = EXPR` or `local NAME = {}`.
188    fn parse_local(&mut self) -> Result<(), LoadError> {
189        self.expect_word("local")?;
190        self.skip_ws();
191        let name = self.parse_name()?;
192        self.skip_ws();
193        if self.peek() == Some(b'=') {
194            self.pos += 1;
195            let v = self.parse_expr()?;
196            self.locals.insert(name, v);
197        } else {
198            self.locals.insert(name, Value::Nil);
199        }
200        Ok(())
201    }
202
203    /// Parse a statement inside a `do ... end` block. It is either an assignment
204    /// into a self-reference path, a call statement, or an assignment to a global
205    /// that the sandbox discards.
206    fn parse_statement(&mut self) -> Result<(), LoadError> {
207        // Left side: NAME then a chain of [expr] or .name.
208        let base = self.parse_name()?;
209        let known_local = matches!(self.locals.get(&base), Some(Value::Table(_)));
210        if !known_local {
211            return self.consume_sandbox_statement();
212        }
213        let mut table = match self.locals.get(&base) {
214            Some(Value::Table(t)) => t.clone(),
215            _ => unreachable!("checked known_local"),
216        };
217        let mut pending_key: Option<Key> = None;
218        loop {
219            self.skip_ws();
220            match self.peek() {
221                Some(b'[') => {
222                    self.pos += 1;
223                    let k = self.parse_expr()?;
224                    self.skip_ws();
225                    self.expect_byte(b']')?;
226                    if let Some(prev) = pending_key.take() {
227                        table = self.descend(&table, prev)?;
228                    }
229                    pending_key = Some(value_to_key(k)?);
230                }
231                Some(b'.') => {
232                    self.pos += 1;
233                    let field = self.parse_name()?;
234                    if let Some(prev) = pending_key.take() {
235                        table = self.descend(&table, prev)?;
236                    }
237                    pending_key = Some(Key::Str(field.into_bytes()));
238                }
239                _ => break,
240            }
241        }
242        self.skip_ws();
243        self.expect_byte(b'=')?;
244        let rhs = self.parse_expr()?;
245        let key = pending_key.ok_or(LoadError::Syntax("assignment with no key".to_string()))?;
246        table.set(key, rhs);
247        Ok(())
248    }
249
250    /// Consume a statement whose target is a global or `_G` chain, not a known
251    /// local. A call is rejected in safe mode. An assignment resolves against the
252    /// sandbox and is discarded, so it never touches real globals.
253    fn consume_sandbox_statement(&mut self) -> Result<(), LoadError> {
254        // Walk the field/index chain following the base name.
255        loop {
256            self.skip_ws();
257            match self.peek() {
258                Some(b'.') => {
259                    self.pos += 1;
260                    let _ = self.parse_name();
261                }
262                Some(b'[') => {
263                    self.pos += 1;
264                    let _ = self.parse_expr()?;
265                    self.skip_ws();
266                    self.expect_byte(b']')?;
267                }
268                _ => break,
269            }
270        }
271        self.skip_ws();
272        match self.peek() {
273            Some(b'(') => {
274                if self.safe {
275                    return Err(LoadError::UnsafeCall);
276                }
277                self.skip_balanced_parens()?;
278                Ok(())
279            }
280            Some(b'=') if self.peek_at(1) != Some(b'=') => {
281                self.pos += 1;
282                // The right side runs in the sandbox and its result is dropped.
283                let _ = self.parse_expr()?;
284                Ok(())
285            }
286            _ => Ok(()),
287        }
288    }
289
290    /// Follow a key into a nested table, for multi-step assignment paths.
291    fn descend(&self, table: &Table, key: Key) -> Result<Table, LoadError> {
292        match table.get(&key) {
293            Some(Value::Table(t)) => Ok(t),
294            _ => Err(LoadError::Syntax("path does not reach a table".to_string())),
295        }
296    }
297
298    fn parse_expr(&mut self) -> Result<Value, LoadError> {
299        self.skip_ws();
300        match self.peek() {
301            Some(b'{') => self.parse_table(),
302            Some(b'"') | Some(b'\'') => self.parse_string(),
303            Some(b'-') | Some(b'0'..=b'9') => self.parse_number_or_special(),
304            Some(c) if c.is_ascii_alphabetic() || c == b'_' => self.parse_word_expr(),
305            other => Err(LoadError::UnexpectedByte {
306                byte: other,
307                pos: self.pos,
308            }),
309        }
310    }
311
312    /// A word-starting expression: a keyword literal, a local variable with an
313    /// access chain, a `function() ... end` stub, or a global lookup or call.
314    fn parse_word_expr(&mut self) -> Result<Value, LoadError> {
315        let word = self.parse_name()?;
316        match word.as_str() {
317            "nil" => return Ok(Value::Nil),
318            "true" => return Ok(Value::Bool(true)),
319            "false" => return Ok(Value::Bool(false)),
320            "function" => return self.parse_function_stub(),
321            _ => {}
322        }
323
324        if let Some(base) = self.locals.get(&word).cloned() {
325            // Follow field and index access into the local value. serpent uses
326            // the helper table this way, for example __._table1 or _[key].
327            return self.follow_access(base);
328        }
329
330        // A global chain such as math.random, io.stdin, or _G.print, optionally
331        // called. Capture the dotted name for a stable sandbox identity.
332        let mut name = word;
333        self.skip_ws();
334        while matches!(self.peek(), Some(b'.') | Some(b'[')) {
335            if self.peek() == Some(b'.') {
336                self.pos += 1;
337                let field = self.parse_name()?;
338                name.push('.');
339                name.push_str(&field);
340            } else {
341                self.pos += 1;
342                let _ = self.parse_expr();
343                self.skip_ws();
344                let _ = self.expect_byte(b']');
345                name.push_str("[]");
346            }
347            self.skip_ws();
348        }
349        if self.peek() == Some(b'(') {
350            if self.safe {
351                return Err(LoadError::UnsafeCall);
352            }
353            self.skip_balanced_parens()?;
354            return Ok(Value::Nil);
355        }
356        // A global name resolves to a sandbox stand-in with a stable identity.
357        Ok(self.global_value(name))
358    }
359
360    /// Follow `.field` and `[index]` access starting from `base`.
361    fn follow_access(&mut self, mut cur: Value) -> Result<Value, LoadError> {
362        loop {
363            self.skip_ws();
364            let key = match self.peek() {
365                Some(b'.') => {
366                    self.pos += 1;
367                    Key::Str(self.parse_name()?.into_bytes())
368                }
369                Some(b'[') => {
370                    self.pos += 1;
371                    let k = self.parse_expr()?;
372                    self.skip_ws();
373                    self.expect_byte(b']')?;
374                    value_to_key(k)?
375                }
376                _ => return Ok(cur),
377            };
378            let next = match &cur {
379                Value::Table(t) => t.get(&key).unwrap_or(Value::Nil),
380                _ => Value::Nil,
381            };
382            cur = next;
383        }
384    }
385
386    /// Parse a `function() ... end` stub. The serializer emits either a bytecode
387    /// reload wrapped in a call, or under nocode an empty body. This loader
388    /// cannot run functions, so it yields nil after consuming the body.
389    fn parse_function_stub(&mut self) -> Result<Value, LoadError> {
390        // Skip the parameter list.
391        self.skip_ws();
392        if self.peek() == Some(b'(') {
393            self.skip_balanced_parens()?;
394        }
395        // Skip to the matching `end`, accounting for nested blocks.
396        let mut depth = 1;
397        while depth > 0 {
398            self.skip_ws();
399            if self.at_end_keyword() {
400                self.pos += 3;
401                depth -= 1;
402            } else if self.looking_at("function") || self.looking_at("do") || self.looking_at("if")
403            {
404                // Nested block openers add depth. Advance past the word.
405                let _ = self.parse_name();
406            } else if self.pos < self.src.len() {
407                self.pos += 1;
408            } else {
409                break;
410            }
411        }
412        Ok(Value::Nil)
413    }
414
415    fn skip_balanced_parens(&mut self) -> Result<(), LoadError> {
416        self.expect_byte(b'(')?;
417        let mut depth = 1;
418        while depth > 0 {
419            match self.next_byte() {
420                Some(b'(') => depth += 1,
421                Some(b')') => depth -= 1,
422                Some(_) => {}
423                None => return Err(LoadError::Syntax("unbalanced parentheses".to_string())),
424            }
425        }
426        Ok(())
427    }
428
429    fn parse_table(&mut self) -> Result<Value, LoadError> {
430        self.expect_byte(b'{')?;
431        let table = Table::new();
432        let mut array_idx = 1u64;
433        loop {
434            // Skip a comment before the element, so pretty output that leads
435            // with `--[[...]]` inside the braces parses.
436            self.skip_comment();
437            self.skip_ws();
438            match self.peek() {
439                Some(b'}') => {
440                    self.pos += 1;
441                    break;
442                }
443                None => return Err(LoadError::Unterminated { what: "table" }),
444                _ => {}
445            }
446            // [key] = value, name = value, or a positional value.
447            if self.peek() == Some(b'[') {
448                self.pos += 1;
449                let key = self.parse_expr()?;
450                self.skip_ws();
451                self.expect_byte(b']')?;
452                self.skip_ws();
453                self.expect_byte(b'=')?;
454                let val = self.parse_expr()?;
455                table.set(value_to_key(key)?, val);
456            } else if let Some(name) = self.try_named_field()? {
457                let val = self.parse_expr()?;
458                table.set(Key::Str(name.into_bytes()), val);
459            } else {
460                let val = self.parse_expr()?;
461                table.set(Key::Number(array_idx as f64), val);
462                array_idx += 1;
463            }
464            // A value may carry a trailing --[[...]] comment before the
465            // separator, for example a nested table in pretty output.
466            self.skip_comment();
467            self.skip_ws();
468            match self.peek() {
469                Some(b',') | Some(b';') => {
470                    self.pos += 1;
471                }
472                Some(b'}') => {}
473                other => {
474                    return Err(LoadError::Syntax(format!(
475                        "expected , or }} in table, got {other:?}"
476                    )))
477                }
478            }
479        }
480        Ok(Value::Table(table))
481    }
482
483    /// Try to parse a `name =` field prefix. Returns the name on success and
484    /// leaves the cursor after `=`. Restores position when the lookahead is not
485    /// a named field.
486    fn try_named_field(&mut self) -> Result<Option<String>, LoadError> {
487        let save = self.pos;
488        match self.peek() {
489            Some(c) if c.is_ascii_alphabetic() || c == b'_' => {}
490            _ => return Ok(None),
491        }
492        let name = self.parse_name()?;
493        self.skip_ws();
494        if self.peek() == Some(b'=') && self.peek_at(1) != Some(b'=') {
495            self.pos += 1;
496            Ok(Some(name))
497        } else {
498            self.pos = save;
499            Ok(None)
500        }
501    }
502
503    fn parse_string(&mut self) -> Result<Value, LoadError> {
504        let quote = self
505            .next_byte()
506            .ok_or(LoadError::Unterminated { what: "string" })?;
507        let mut bytes = Vec::new();
508        loop {
509            match self.next_byte() {
510                None => return Err(LoadError::Unterminated { what: "string" }),
511                Some(b) if b == quote => break,
512                Some(b'\\') => {
513                    let e = self
514                        .next_byte()
515                        .ok_or(LoadError::Unterminated { what: "string" })?;
516                    match e {
517                        b'n' => bytes.push(b'\n'),
518                        b'r' => bytes.push(b'\r'),
519                        b't' => bytes.push(b'\t'),
520                        b'a' => bytes.push(7),
521                        b'b' => bytes.push(8),
522                        b'f' => bytes.push(12),
523                        b'v' => bytes.push(11),
524                        b'\\' => bytes.push(b'\\'),
525                        b'"' => bytes.push(b'"'),
526                        b'\'' => bytes.push(b'\''),
527                        b'\n' => bytes.push(b'\n'),
528                        b'0'..=b'9' => {
529                            // Up to three decimal digits.
530                            let mut n = (e - b'0') as u32;
531                            for _ in 0..2 {
532                                if let Some(d @ b'0'..=b'9') = self.peek() {
533                                    n = n * 10 + (d - b'0') as u32;
534                                    self.pos += 1;
535                                } else {
536                                    break;
537                                }
538                            }
539                            bytes.push(n as u8);
540                        }
541                        other => bytes.push(other),
542                    }
543                }
544                Some(b) => bytes.push(b),
545            }
546        }
547        Ok(Value::Str(bytes))
548    }
549
550    /// Parse a number, or a special form: `1/0`, `-1/0`, `0/0`.
551    fn parse_number_or_special(&mut self) -> Result<Value, LoadError> {
552        let start = self.pos;
553        if self.peek() == Some(b'-') {
554            self.pos += 1;
555        }
556        while matches!(
557            self.peek(),
558            Some(b'0'..=b'9')
559                | Some(b'.')
560                | Some(b'e')
561                | Some(b'E')
562                | Some(b'+')
563                | Some(b'-')
564                | Some(b'x')
565                | Some(b'X')
566                | Some(b'a'..=b'f')
567                | Some(b'A'..=b'F')
568                | Some(b'p')
569                | Some(b'P')
570        ) {
571            self.pos += 1;
572        }
573        let text = std::str::from_utf8(&self.src[start..self.pos])
574            .map_err(|_| LoadError::BadNumber("bad bytes".to_string()))?;
575        // Handle the division forms serpent emits for special numbers.
576        self.skip_ws();
577        if self.peek() == Some(b'/') {
578            self.pos += 1;
579            self.skip_ws();
580            let dstart = self.pos;
581            if self.peek() == Some(b'-') {
582                self.pos += 1;
583            }
584            while matches!(self.peek(), Some(b'0'..=b'9') | Some(b'.')) {
585                self.pos += 1;
586            }
587            let denom = std::str::from_utf8(&self.src[dstart..self.pos]).unwrap_or("");
588            let num: f64 = text
589                .trim()
590                .parse()
591                .map_err(|_| LoadError::BadNumber(text.trim().to_string()))?;
592            let den: f64 = denom
593                .trim()
594                .parse()
595                .map_err(|_| LoadError::BadNumber(denom.trim().to_string()))?;
596            self.skip_comment();
597            return Ok(Value::Number(num / den));
598        }
599        let n: f64 = if let Some(hex) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X"))
600        {
601            i64::from_str_radix(hex, 16)
602                .map(|v| v as f64)
603                .map_err(|_| LoadError::BadNumber(text.to_string()))?
604        } else {
605            text.parse()
606                .map_err(|_| LoadError::BadNumber(text.to_string()))?
607        };
608        self.skip_comment();
609        Ok(Value::Number(n))
610    }
611
612    fn parse_name(&mut self) -> Result<String, LoadError> {
613        self.skip_ws();
614        let start = self.pos;
615        match self.peek() {
616            Some(c) if c.is_ascii_alphabetic() || c == b'_' => {}
617            _ => return Err(LoadError::Syntax(format!("expected name at {}", self.pos))),
618        }
619        while matches!(self.peek(), Some(c) if c.is_ascii_alphanumeric() || c == b'_') {
620            self.pos += 1;
621        }
622        Ok(String::from_utf8_lossy(&self.src[start..self.pos]).into_owned())
623    }
624
625    fn skip_comment(&mut self) {
626        self.skip_ws();
627        if self.looking_at("--[[") {
628            self.pos += 4;
629            while self.pos < self.src.len() && !self.looking_at("]]") {
630                self.pos += 1;
631            }
632            if self.looking_at("]]") {
633                self.pos += 2;
634            }
635        }
636    }
637
638    fn skip_sep(&mut self) {
639        self.skip_ws();
640        while matches!(self.peek(), Some(b';') | Some(b'\n')) {
641            self.pos += 1;
642            self.skip_ws();
643        }
644    }
645
646    fn skip_ws(&mut self) {
647        loop {
648            match self.peek() {
649                Some(b) if b == b' ' || b == b'\t' || b == b'\r' || b == b'\n' => {
650                    self.pos += 1;
651                }
652                _ => break,
653            }
654        }
655    }
656
657    fn looking_at(&self, s: &str) -> bool {
658        self.src[self.pos..].starts_with(s.as_bytes())
659    }
660
661    fn expect_word(&mut self, w: &str) -> Result<(), LoadError> {
662        self.skip_ws();
663        if self.looking_at(w) {
664            self.pos += w.len();
665            Ok(())
666        } else {
667            Err(LoadError::Syntax(format!("expected {w}")))
668        }
669    }
670
671    fn expect_byte(&mut self, b: u8) -> Result<(), LoadError> {
672        self.skip_ws();
673        if self.peek() == Some(b) {
674            self.pos += 1;
675            Ok(())
676        } else {
677            Err(LoadError::Syntax(format!("expected {}", b as char)))
678        }
679    }
680
681    fn peek(&self) -> Option<u8> {
682        self.src.get(self.pos).copied()
683    }
684
685    fn peek_at(&self, off: usize) -> Option<u8> {
686        self.src.get(self.pos + off).copied()
687    }
688
689    fn next_byte(&mut self) -> Option<u8> {
690        let b = self.src.get(self.pos).copied();
691        if b.is_some() {
692            self.pos += 1;
693        }
694        b
695    }
696}
697
698fn value_to_key(v: Value) -> Result<Key, LoadError> {
699    match v {
700        Value::Bool(b) => Ok(Key::Bool(b)),
701        Value::Number(n) => Ok(Key::Number(n)),
702        Value::Str(s) => Ok(Key::Str(s)),
703        Value::Table(t) => Ok(Key::Table(t)),
704        Value::Function(f) => Ok(Key::Function(f)),
705        Value::Global(g) => Ok(Key::Global(g)),
706        Value::Nil => Err(LoadError::Syntax("nil is not a valid key".to_string())),
707    }
708}