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_keyword("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_keyword("return") || self.at_end_keyword() {
149                break;
150            }
151            if self.looking_at_keyword("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_keyword("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        self.looking_at_keyword("end")
179    }
180
181    /// Parse `local NAME = EXPR` or `local NAME = {}`.
182    fn parse_local(&mut self) -> Result<(), LoadError> {
183        self.expect_word("local")?;
184        self.skip_ws();
185        let name = self.parse_name()?;
186        self.skip_ws();
187        if self.peek() == Some(b'=') {
188            self.pos += 1;
189            let v = self.parse_expr()?;
190            self.locals.insert(name, v);
191        } else {
192            self.locals.insert(name, Value::Nil);
193        }
194        Ok(())
195    }
196
197    /// Parse a statement inside a `do ... end` block. It is either an assignment
198    /// into a self-reference path, a call statement, or an assignment to a global
199    /// that the sandbox discards.
200    fn parse_statement(&mut self) -> Result<(), LoadError> {
201        // Left side: NAME then a chain of [expr] or .name.
202        let base = self.parse_name()?;
203        let known_local = matches!(self.locals.get(&base), Some(Value::Table(_)));
204        if !known_local {
205            return self.consume_sandbox_statement();
206        }
207        let mut table = match self.locals.get(&base) {
208            Some(Value::Table(t)) => t.clone(),
209            _ => unreachable!("checked known_local"),
210        };
211        let mut pending_key: Option<Key> = None;
212        loop {
213            self.skip_ws();
214            match self.peek() {
215                Some(b'[') => {
216                    self.pos += 1;
217                    let k = self.parse_expr()?;
218                    self.skip_ws();
219                    self.expect_byte(b']')?;
220                    if let Some(prev) = pending_key.take() {
221                        table = self.descend(&table, prev)?;
222                    }
223                    pending_key = Some(value_to_key(k)?);
224                }
225                Some(b'.') => {
226                    self.pos += 1;
227                    let field = self.parse_name()?;
228                    if let Some(prev) = pending_key.take() {
229                        table = self.descend(&table, prev)?;
230                    }
231                    pending_key = Some(Key::Str(field.into_bytes()));
232                }
233                _ => break,
234            }
235        }
236        self.skip_ws();
237        self.expect_byte(b'=')?;
238        let rhs = self.parse_expr()?;
239        let key = pending_key.ok_or(LoadError::Syntax("assignment with no key".to_string()))?;
240        table.set(key, rhs);
241        Ok(())
242    }
243
244    /// Consume a statement whose target is a global or `_G` chain, not a known
245    /// local. A call is rejected in safe mode. An assignment resolves against the
246    /// sandbox and is discarded, so it never touches real globals.
247    fn consume_sandbox_statement(&mut self) -> Result<(), LoadError> {
248        // Walk the field/index chain following the base name.
249        loop {
250            self.skip_ws();
251            match self.peek() {
252                Some(b'.') => {
253                    self.pos += 1;
254                    let _ = self.parse_name();
255                }
256                Some(b'[') => {
257                    self.pos += 1;
258                    let _ = self.parse_expr()?;
259                    self.skip_ws();
260                    self.expect_byte(b']')?;
261                }
262                _ => break,
263            }
264        }
265        self.skip_ws();
266        match self.peek() {
267            Some(b'(') => {
268                if self.safe {
269                    return Err(LoadError::UnsafeCall);
270                }
271                self.skip_balanced_parens()?;
272                Ok(())
273            }
274            Some(b'=') if self.peek_at(1) != Some(b'=') => {
275                self.pos += 1;
276                // The right side runs in the sandbox and its result is dropped.
277                let _ = self.parse_expr()?;
278                Ok(())
279            }
280            _ => Ok(()),
281        }
282    }
283
284    /// Follow a key into a nested table, for multi-step assignment paths.
285    fn descend(&self, table: &Table, key: Key) -> Result<Table, LoadError> {
286        match table.get(&key) {
287            Some(Value::Table(t)) => Ok(t),
288            _ => Err(LoadError::Syntax("path does not reach a table".to_string())),
289        }
290    }
291
292    fn parse_expr(&mut self) -> Result<Value, LoadError> {
293        self.skip_ws();
294        match self.peek() {
295            Some(b'{') => self.parse_table(),
296            Some(b'"') | Some(b'\'') => self.parse_string(),
297            Some(b'-') | Some(b'0'..=b'9') => self.parse_number_or_special(),
298            Some(c) if c.is_ascii_alphabetic() || c == b'_' => self.parse_word_expr(),
299            other => Err(LoadError::UnexpectedByte {
300                byte: other,
301                pos: self.pos,
302            }),
303        }
304    }
305
306    /// A word-starting expression: a keyword literal, a local variable with an
307    /// access chain, a `function() ... end` stub, or a global lookup or call.
308    fn parse_word_expr(&mut self) -> Result<Value, LoadError> {
309        let word = self.parse_name()?;
310        match word.as_str() {
311            "nil" => return Ok(Value::Nil),
312            "true" => return Ok(Value::Bool(true)),
313            "false" => return Ok(Value::Bool(false)),
314            "function" => return self.parse_function_stub(),
315            _ => {}
316        }
317
318        if let Some(base) = self.locals.get(&word).cloned() {
319            // Follow field and index access into the local value. serpent uses
320            // the helper table this way, for example __._table1 or _[key].
321            return self.follow_access(base);
322        }
323
324        // A global chain such as math.random, io.stdin, or _G.print, optionally
325        // called. Capture the dotted name for a stable sandbox identity.
326        let mut name = word;
327        self.skip_ws();
328        while matches!(self.peek(), Some(b'.') | Some(b'[')) {
329            if self.peek() == Some(b'.') {
330                self.pos += 1;
331                let field = self.parse_name()?;
332                name.push('.');
333                name.push_str(&field);
334            } else {
335                self.pos += 1;
336                self.parse_expr()?;
337                self.skip_ws();
338                self.expect_byte(b']')?;
339                name.push_str("[]");
340            }
341            self.skip_ws();
342        }
343        if self.peek() == Some(b'(') {
344            if self.safe {
345                return Err(LoadError::UnsafeCall);
346            }
347            self.skip_balanced_parens()?;
348            return Ok(Value::Nil);
349        }
350        // A global name resolves to a sandbox stand-in with a stable identity.
351        Ok(self.global_value(name))
352    }
353
354    /// Follow `.field` and `[index]` access starting from `base`.
355    fn follow_access(&mut self, mut cur: Value) -> Result<Value, LoadError> {
356        loop {
357            self.skip_ws();
358            let key = match self.peek() {
359                Some(b'.') => {
360                    self.pos += 1;
361                    Key::Str(self.parse_name()?.into_bytes())
362                }
363                Some(b'[') => {
364                    self.pos += 1;
365                    let k = self.parse_expr()?;
366                    self.skip_ws();
367                    self.expect_byte(b']')?;
368                    value_to_key(k)?
369                }
370                _ => return Ok(cur),
371            };
372            let next = match &cur {
373                Value::Table(t) => t.get(&key).unwrap_or(Value::Nil),
374                _ => Value::Nil,
375            };
376            cur = next;
377        }
378    }
379
380    /// Parse a `function() ... end` stub. The serializer emits either a bytecode
381    /// reload wrapped in a call, or under nocode an empty body. This loader
382    /// cannot run functions, so it yields nil after consuming the body.
383    fn parse_function_stub(&mut self) -> Result<Value, LoadError> {
384        // Skip the parameter list.
385        self.skip_ws();
386        if self.peek() == Some(b'(') {
387            self.skip_balanced_parens()?;
388        }
389        // Skip to the matching `end`, accounting for nested blocks.
390        let mut depth = 1;
391        while depth > 0 {
392            self.skip_ws();
393            match self.peek() {
394                None => return Err(LoadError::Syntax("expected end".to_string())),
395                Some(b'-') if self.peek_at(1) == Some(b'-') => {
396                    self.skip_function_body_comment();
397                }
398                Some(b'[') => {
399                    if !self.skip_long_bracket() {
400                        self.pos += 1;
401                    }
402                }
403                Some(quote @ (b'"' | b'\'')) => {
404                    self.pos += 1;
405                    self.skip_quoted_bytes(quote)?;
406                }
407                Some(c) if is_ident_start(c) => {
408                    let word = self.parse_name()?;
409                    match word.as_str() {
410                        "end" => depth -= 1,
411                        "function" | "do" | "if" => depth += 1,
412                        _ => {}
413                    }
414                }
415                Some(_) => {
416                    self.pos += 1;
417                }
418            }
419        }
420        Ok(Value::Nil)
421    }
422
423    fn skip_balanced_parens(&mut self) -> Result<(), LoadError> {
424        self.expect_byte(b'(')?;
425        let mut depth = 1;
426        while depth > 0 {
427            match self.next_byte() {
428                Some(b'"') => self.skip_quoted_bytes(b'"')?,
429                Some(b'\'') => self.skip_quoted_bytes(b'\'')?,
430                Some(b'(') => depth += 1,
431                Some(b')') => depth -= 1,
432                Some(_) => {}
433                None => return Err(LoadError::Syntax("unbalanced parentheses".to_string())),
434            }
435        }
436        Ok(())
437    }
438
439    fn parse_table(&mut self) -> Result<Value, LoadError> {
440        self.expect_byte(b'{')?;
441        let table = Table::new();
442        let mut array_idx = 1u64;
443        loop {
444            // Skip a comment before the element, so pretty output that leads
445            // with `--[[...]]` inside the braces parses.
446            self.skip_comment();
447            self.skip_ws();
448            match self.peek() {
449                Some(b'}') => {
450                    self.pos += 1;
451                    break;
452                }
453                None => return Err(LoadError::Unterminated { what: "table" }),
454                _ => {}
455            }
456            // [key] = value, name = value, or a positional value.
457            if self.peek() == Some(b'[') {
458                self.pos += 1;
459                let key = self.parse_expr()?;
460                self.skip_ws();
461                self.expect_byte(b']')?;
462                self.skip_ws();
463                self.expect_byte(b'=')?;
464                let val = self.parse_expr()?;
465                table.set(value_to_key(key)?, val);
466            } else if let Some(name) = self.try_named_field()? {
467                let val = self.parse_expr()?;
468                table.set(Key::Str(name.into_bytes()), val);
469            } else {
470                let val = self.parse_expr()?;
471                table.set(Key::Number(array_idx as f64), val);
472                array_idx += 1;
473            }
474            // A value may carry a trailing --[[...]] comment before the
475            // separator, for example a nested table in pretty output.
476            self.skip_comment();
477            self.skip_ws();
478            match self.peek() {
479                Some(b',') | Some(b';') => {
480                    self.pos += 1;
481                }
482                Some(b'}') => {}
483                other => {
484                    return Err(LoadError::Syntax(format!(
485                        "expected , or }} in table, got {other:?}"
486                    )))
487                }
488            }
489        }
490        Ok(Value::Table(table))
491    }
492
493    /// Try to parse a `name =` field prefix. Returns the name on success and
494    /// leaves the cursor after `=`. Restores position when the lookahead is not
495    /// a named field.
496    fn try_named_field(&mut self) -> Result<Option<String>, LoadError> {
497        let save = self.pos;
498        match self.peek() {
499            Some(c) if c.is_ascii_alphabetic() || c == b'_' => {}
500            _ => return Ok(None),
501        }
502        let name = self.parse_name()?;
503        self.skip_ws();
504        if self.peek() == Some(b'=') && self.peek_at(1) != Some(b'=') {
505            self.pos += 1;
506            Ok(Some(name))
507        } else {
508            self.pos = save;
509            Ok(None)
510        }
511    }
512
513    fn parse_string(&mut self) -> Result<Value, LoadError> {
514        let quote = self
515            .next_byte()
516            .ok_or(LoadError::Unterminated { what: "string" })?;
517        let mut bytes = Vec::new();
518        loop {
519            match self.next_byte() {
520                None => return Err(LoadError::Unterminated { what: "string" }),
521                Some(b) if b == quote => break,
522                Some(b'\\') => {
523                    let e = self
524                        .next_byte()
525                        .ok_or(LoadError::Unterminated { what: "string" })?;
526                    match e {
527                        b'n' => bytes.push(b'\n'),
528                        b'r' => bytes.push(b'\r'),
529                        b't' => bytes.push(b'\t'),
530                        b'a' => bytes.push(7),
531                        b'b' => bytes.push(8),
532                        b'f' => bytes.push(12),
533                        b'v' => bytes.push(11),
534                        b'\\' => bytes.push(b'\\'),
535                        b'"' => bytes.push(b'"'),
536                        b'\'' => bytes.push(b'\''),
537                        b'\n' => bytes.push(b'\n'),
538                        b'0'..=b'9' => {
539                            // Up to three decimal digits.
540                            let mut n = (e - b'0') as u32;
541                            for _ in 0..2 {
542                                if let Some(d @ b'0'..=b'9') = self.peek() {
543                                    n = n * 10 + (d - b'0') as u32;
544                                    self.pos += 1;
545                                } else {
546                                    break;
547                                }
548                            }
549                            bytes.push(n as u8);
550                        }
551                        other => bytes.push(other),
552                    }
553                }
554                Some(b) => bytes.push(b),
555            }
556        }
557        Ok(Value::Str(bytes))
558    }
559
560    /// Parse a number, or a special form: `1/0`, `-1/0`, `0/0`.
561    fn parse_number_or_special(&mut self) -> Result<Value, LoadError> {
562        let start = self.pos;
563        if self.peek() == Some(b'-') {
564            self.pos += 1;
565        }
566        while matches!(
567            self.peek(),
568            Some(b'0'..=b'9')
569                | Some(b'.')
570                | Some(b'e')
571                | Some(b'E')
572                | Some(b'+')
573                | Some(b'-')
574                | Some(b'x')
575                | Some(b'X')
576                | Some(b'a'..=b'f')
577                | Some(b'A'..=b'F')
578                | Some(b'p')
579                | Some(b'P')
580        ) {
581            self.pos += 1;
582        }
583        let text = std::str::from_utf8(&self.src[start..self.pos])
584            .map_err(|_| LoadError::BadNumber("bad bytes".to_string()))?;
585        // Handle the division forms serpent emits for special numbers.
586        self.skip_ws();
587        if self.peek() == Some(b'/') {
588            self.pos += 1;
589            self.skip_ws();
590            let dstart = self.pos;
591            if self.peek() == Some(b'-') {
592                self.pos += 1;
593            }
594            while matches!(self.peek(), Some(b'0'..=b'9') | Some(b'.')) {
595                self.pos += 1;
596            }
597            let denom = std::str::from_utf8(&self.src[dstart..self.pos]).unwrap_or("");
598            let num: f64 = text
599                .trim()
600                .parse()
601                .map_err(|_| LoadError::BadNumber(text.trim().to_string()))?;
602            let den: f64 = denom
603                .trim()
604                .parse()
605                .map_err(|_| LoadError::BadNumber(denom.trim().to_string()))?;
606            self.skip_comment();
607            return Ok(Value::Number(num / den));
608        }
609        let n: f64 = if let Some(hex) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X"))
610        {
611            i64::from_str_radix(hex, 16)
612                .map(|v| v as f64)
613                .map_err(|_| LoadError::BadNumber(text.to_string()))?
614        } else {
615            text.parse()
616                .map_err(|_| LoadError::BadNumber(text.to_string()))?
617        };
618        self.skip_comment();
619        Ok(Value::Number(n))
620    }
621
622    fn parse_name(&mut self) -> Result<String, LoadError> {
623        self.skip_ws();
624        let start = self.pos;
625        match self.peek() {
626            Some(c) if c.is_ascii_alphabetic() || c == b'_' => {}
627            _ => return Err(LoadError::Syntax(format!("expected name at {}", self.pos))),
628        }
629        while matches!(self.peek(), Some(c) if c.is_ascii_alphanumeric() || c == b'_') {
630            self.pos += 1;
631        }
632        Ok(String::from_utf8_lossy(&self.src[start..self.pos]).into_owned())
633    }
634
635    fn skip_comment(&mut self) {
636        self.skip_ws();
637        if self.looking_at("--[[") {
638            self.pos += 4;
639            while self.pos < self.src.len() && !self.looking_at("]]") {
640                self.pos += 1;
641            }
642            if self.looking_at("]]") {
643                self.pos += 2;
644            }
645        }
646    }
647
648    fn skip_function_body_comment(&mut self) {
649        self.pos += 2;
650        let is_long_comment = self.peek() == Some(b'[') && self.skip_long_bracket();
651        if !is_long_comment {
652            while !matches!(self.peek(), None | Some(b'\n') | Some(b'\r')) {
653                self.pos += 1;
654            }
655        }
656    }
657
658    fn skip_long_bracket(&mut self) -> bool {
659        if self.peek() != Some(b'[') {
660            return false;
661        }
662        let mut close_len = 1;
663        while self.peek_at(close_len) == Some(b'=') {
664            close_len += 1;
665        }
666        if self.peek_at(close_len) != Some(b'[') {
667            return false;
668        }
669        self.pos += close_len + 1;
670        while self.pos < self.src.len() && !self.looking_at_long_bracket_close(close_len) {
671            self.pos += 1;
672        }
673        if self.looking_at_long_bracket_close(close_len) {
674            self.pos += close_len + 1;
675        }
676        true
677    }
678
679    fn looking_at_long_bracket_close(&self, len: usize) -> bool {
680        self.peek() == Some(b']')
681            && (1..len).all(|i| self.peek_at(i) == Some(b'='))
682            && self.peek_at(len) == Some(b']')
683    }
684
685    fn skip_sep(&mut self) {
686        self.skip_ws();
687        while matches!(self.peek(), Some(b';') | Some(b'\n')) {
688            self.pos += 1;
689            self.skip_ws();
690        }
691    }
692
693    fn skip_ws(&mut self) {
694        loop {
695            match self.peek() {
696                Some(b) if b == b' ' || b == b'\t' || b == b'\r' || b == b'\n' => {
697                    self.pos += 1;
698                }
699                _ => break,
700            }
701        }
702    }
703
704    fn looking_at(&self, s: &str) -> bool {
705        self.src[self.pos..].starts_with(s.as_bytes())
706    }
707
708    fn looking_at_keyword(&self, s: &str) -> bool {
709        self.looking_at(s) && !matches!(self.peek_at(s.len()), Some(b) if is_ident_byte(b))
710    }
711
712    fn expect_word(&mut self, w: &str) -> Result<(), LoadError> {
713        self.skip_ws();
714        if self.looking_at_keyword(w) {
715            self.pos += w.len();
716            Ok(())
717        } else {
718            Err(LoadError::Syntax(format!("expected {w}")))
719        }
720    }
721
722    fn expect_byte(&mut self, b: u8) -> Result<(), LoadError> {
723        self.skip_ws();
724        if self.peek() == Some(b) {
725            self.pos += 1;
726            Ok(())
727        } else {
728            Err(LoadError::Syntax(format!("expected {}", b as char)))
729        }
730    }
731
732    fn peek(&self) -> Option<u8> {
733        self.src.get(self.pos).copied()
734    }
735
736    fn peek_at(&self, off: usize) -> Option<u8> {
737        self.src.get(self.pos + off).copied()
738    }
739
740    fn next_byte(&mut self) -> Option<u8> {
741        let b = self.src.get(self.pos).copied();
742        if b.is_some() {
743            self.pos += 1;
744        }
745        b
746    }
747
748    fn skip_quoted_bytes(&mut self, quote: u8) -> Result<(), LoadError> {
749        loop {
750            match self.next_byte() {
751                Some(b) if b == quote => return Ok(()),
752                Some(b'\\') => {
753                    if self.next_byte().is_none() {
754                        return Err(LoadError::Unterminated { what: "string" });
755                    }
756                }
757                Some(_) => {}
758                None => return Err(LoadError::Unterminated { what: "string" }),
759            }
760        }
761    }
762}
763
764fn is_ident_byte(b: u8) -> bool {
765    b.is_ascii_alphanumeric() || b == b'_'
766}
767
768fn is_ident_start(b: u8) -> bool {
769    b.is_ascii_alphabetic() || b == b'_'
770}
771
772fn value_to_key(v: Value) -> Result<Key, LoadError> {
773    match v {
774        Value::Bool(b) => Ok(Key::Bool(b)),
775        Value::Number(n) => Ok(Key::Number(n)),
776        Value::Str(s) => Ok(Key::Str(s)),
777        Value::Table(t) => Ok(Key::Table(t)),
778        Value::Function(f) => Ok(Key::Function(f)),
779        Value::Global(g) => Ok(Key::Global(g)),
780        Value::Nil => Err(LoadError::Syntax("nil is not a valid key".to_string())),
781    }
782}