diff --git a/Cargo.lock b/Cargo.lock index 7b8ae79d6080..e9e9083c85b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1382,6 +1382,7 @@ dependencies = [ "bun_collections", "bun_core", "bun_highway", + "bun_simdutf_sys", "bun_wyhash", "bytemuck", "const_format", diff --git a/docs/runtime/toml.mdx b/docs/runtime/toml.mdx index fb8940b3de75..8414d9ae1774 100644 --- a/docs/runtime/toml.mdx +++ b/docs/runtime/toml.mdx @@ -45,14 +45,15 @@ console.log(data); #### Supported TOML Features -Bun's TOML parser supports the [TOML v1.0 specification](https://toml.io/en/v1.0.0), including: +Bun's TOML parser implements the full [TOML v1.1.0 specification](https://github.com/toml-lang/toml/releases/tag/1.1.0) and passes the complete official [toml-test](https://github.com/toml-lang/toml-test) conformance suite. -- **Strings**: basic (`"..."`) and literal (`'...'`), including multi-line -- **Integers**: decimal, hex (`0x`), octal (`0o`), and binary (`0b`) -- **Floats** +- **Strings**: basic (`"..."`) and literal (`'...'`), including multi-line, with all escapes (`\uHHHH`, `\UHHHHHHHH`, and TOML 1.1's `\xHH` and `\e`) +- **Integers**: decimal, hex (`0x`), octal (`0o`), and binary (`0b`). Integers that cannot be represented losslessly as a JavaScript number — outside ±(2^53 - 1) — throw +- **Floats**: including `inf` and `nan` - **Booleans**: `true` and `false` +- **Date/times**: offset date-time, local date-time, local date, and local time, returned as strings of their source text - **Arrays**: including mixed types and nested arrays -- **Tables**: standard (`[table]`) and inline (`{ key = "value" }`) +- **Tables**: standard (`[table]`) and inline (`{ key = "value" }`), including TOML 1.1 multi-line inline tables - **Array of tables**: `[[array]]` - **Dotted keys**: `a.b.c = "value"` - **Comments**: using `#` @@ -82,16 +83,47 @@ role = "backend" #### Error Handling -`Bun.TOML.parse()` throws if the TOML is invalid: +`Bun.TOML.parse()` throws a `SyntaxError` if the TOML is invalid: ```ts try { Bun.TOML.parse("invalid = = ="); } catch (error) { console.error("Failed to parse TOML:", error.message); + // Failed to parse TOML: TOML Parse error: Expected a value but found '=' } ``` +### `Bun.TOML.stringify()` + +Serialize a JavaScript object to a TOML document. Scalar keys come first, +followed by `[table]` and `[[array-of-tables]]` sections: + +```ts +Bun.TOML.stringify({ + name: "app", + server: { host: "localhost", port: 8080 }, + points: [{ x: 1 }, { x: 2 }], +}); +// name = "app" +// +// [server] +// host = "localhost" +// port = 8080 +// +// [[points]] +// x = 1 +// +// [[points]] +// x = 2 +``` + +The top-level value must be an object — a TOML document is a table. `Date` +values become TOML offset date-times. Because TOML cannot represent them, +`null` values, `BigInt`, and circular structures throw; `undefined`, +function, and symbol properties are skipped (inside arrays they throw, +since TOML arrays cannot have holes). + --- ## Module Import diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 38fe81334a06..f4c810924ab6 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -777,14 +777,47 @@ declare module "bun" { */ namespace TOML { /** - * Parse a TOML string into a JavaScript object. + * Parse a TOML (v1.1.0) document into a JavaScript object. + * + * Date/time values parse as strings of their source text. Integers + * outside `Number.MAX_SAFE_INTEGER` throw, since they cannot be + * represented losslessly as JavaScript numbers. * * @category Utilities * - * @param input The TOML string to parse + * @param input The TOML document to parse, as a string or UTF-8 bytes * @returns A JavaScript object + * @throws {SyntaxError} If the input is not valid TOML */ - export function parse(input: string): object; + export function parse( + input: string | NodeJS.TypedArray | DataView | ArrayBufferLike | Blob, + ): object; + + /** + * Serialize a JavaScript object to a TOML document. + * + * The top-level value must be an object (a TOML document is a table). + * `Date` values become TOML offset date-times. `null`, `BigInt`, and + * circular structures throw, since TOML cannot represent them; + * `undefined`, function, and symbol properties are skipped (inside + * arrays they throw, since TOML arrays cannot have holes). + * + * @category Utilities + * + * @param input The JavaScript object to serialize. + * @param replacer Not supported; pass `undefined` or `null`. + * @param space Accepted for signature parity with `YAML.stringify` and + * `JSON5.stringify`, but ignored: TOML output is line-oriented. + * @returns A TOML document string, or `undefined` if the input is `undefined`, a function, or a symbol. + * + * @example + * ```js + * import { TOML } from "bun"; + * TOML.stringify({ name: "app", server: { port: 8080 } }); + * // 'name = "app"\n\n[server]\nport = 8080\n' + * ``` + */ + export function stringify(input: unknown, replacer?: undefined | null, space?: string | number): string | undefined; } /** @@ -869,7 +902,7 @@ declare module "bun" { * Bun.JSONL.parse('{bad}\n'); // throws SyntaxError * ``` */ - export function parse(input: string | NodeJS.TypedArray | DataView | ArrayBufferLike): unknown[]; + export function parse(input: string | NodeJS.TypedArray | DataView | ArrayBufferLike): unknown[]; /** * Parse a JSONL chunk, designed for streaming use. @@ -904,7 +937,7 @@ declare module "bun" { * ``` */ export function parseChunk( - input: string | NodeJS.TypedArray | DataView | ArrayBufferLike, + input: string | NodeJS.TypedArray | DataView | ArrayBufferLike, start?: number, end?: number, ): ParseChunkResult; @@ -1303,7 +1336,7 @@ declare module "bun" { * ``` */ export function html( - input: string | NodeJS.TypedArray | DataView | ArrayBufferLike, + input: string | NodeJS.TypedArray | DataView | ArrayBufferLike, options?: Options, ): string; @@ -1379,7 +1412,7 @@ declare module "bun" { * ``` */ export function ansi( - input: string | NodeJS.TypedArray | DataView | ArrayBufferLike, + input: string | NodeJS.TypedArray | DataView | ArrayBufferLike, theme?: AnsiTheme, ): string; @@ -1421,7 +1454,7 @@ declare module "bun" { * ``` */ export function render( - input: string | NodeJS.TypedArray | DataView | ArrayBufferLike, + input: string | NodeJS.TypedArray | DataView | ArrayBufferLike, callbacks?: RenderCallbacks, options?: Options, ): string; @@ -1470,7 +1503,7 @@ declare module "bun" { * ``` */ export function react( - input: string | NodeJS.TypedArray | DataView | ArrayBufferLike, + input: string | NodeJS.TypedArray | DataView | ArrayBufferLike, components?: ComponentOverrides, options?: ReactOptions, ): import("./jsx.d.ts").JSX.Element; diff --git a/src/ast/e.rs b/src/ast/e.rs index 0ebe4fa08eeb..ced3ced5e3ab 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -1249,7 +1249,7 @@ impl Default for Object { } } -/// used in TOML parser to merge properties. +/// Dotted-key path used by the INI parser (`get_or_put_object`). /// /// Node types are lifetime-free, so `next` is a raw `*mut Rope` /// into the bump arena. Segments are bulk-freed at arena reset. @@ -1280,7 +1280,7 @@ impl Rope { /// Re-borrow `next` as `Option<&Rope>`. Same `StoreRef` arena contract: /// the pointee is a bump allocation valid until arena reset. Centralises - /// the one `unsafe` so the `set_rope`/`get_or_put_*`/`get_rope` walkers + /// the one `unsafe` so the `get_or_put_object`/`get_rope` walkers /// don't repeat `if !next.is_null() { unsafe { &*next } }` at every hop. #[inline] pub fn next_ref<'a>(&self) -> Option<&'a Rope> { @@ -1315,7 +1315,7 @@ pub struct RopeQuery<'a> { // ── live Object accessor surface ─────────────────────────────────────────── // Adapted to the current `Vec` API (`append(v)`, `slice()`, `slice_mut()`). -// `set_rope`/`get_or_put_array`/sort helpers stay in the gated impl below. +// Sort helpers stay in the gated impl below. impl Object { pub const EMPTY: Object = Object { properties: bun_alloc::AstAlloc::vec(), @@ -1464,15 +1464,10 @@ pub fn own_key_property_flags(key: &Expr) -> crate::flags::PropertySet { // `toJS` alias deleted — lives in `js_parser_jsc` extension trait. impl Object { - pub fn set(&mut self, key: Expr, _bump: &Bump, value: Expr) -> Result<(), SetError> { - let head_key = match key.data.e_string() { - Some(s) => s.data, - None => return Err(SetError::Clobber), - }; - if self.has_property(&head_key) { - return Err(SetError::Clobber); - } - // `&mut self` so the borrow checker tracks the write. + /// Appends a property without checking for an existing key. Callers that + /// need duplicate detection must check `as_property` with UTF-8 bytes + /// first — a UTF-16 EString key's raw `data` view is not byte-comparable. + pub fn append_property(&mut self, key: Expr, value: Expr) { VecExt::append( &mut self.properties, G::Property { @@ -1482,140 +1477,6 @@ impl Object { ..G::Property::default() }, ); - Ok(()) - } - - // this is terribly, shamefully slow - pub fn set_rope(&mut self, rope: &Rope, bump: &Bump, value: Expr) -> Result<(), SetError> { - let head_key = match rope.head.data.e_string() { - Some(s) => s.data, - None => return Err(SetError::Clobber), - }; - if let Some(existing) = self.get(&head_key) { - match existing.data { - crate::expr::Data::EArray(mut array) => { - let Some(next) = rope.next_ref() else { - array.push(bump, value)?; - return Ok(()); - }; - - if let Some(last) = array.items.last_mut() { - if !matches!(last.data, crate::expr::Data::EObject(_)) { - return Err(SetError::Clobber); - } - last.data - .e_object_mut() - .unwrap() - .set_rope(next, bump, value)?; - return Ok(()); - } - - array.push(bump, value)?; - return Ok(()); - } - crate::expr::Data::EObject(mut object) => { - if let Some(next) = rope.next_ref() { - object.set_rope(next, bump, value)?; - return Ok(()); - } - - return Err(SetError::Clobber); - } - _ => { - return Err(SetError::Clobber); - } - } - } - - let mut value_ = value; - if let Some(next) = rope.next_ref() { - let mut obj = Expr::init(Object::default(), rope.head.loc); - obj.data - .e_object_mut() - .unwrap() - .set_rope(next, bump, value)?; - value_ = obj; - } - - VecExt::append( - &mut self.properties, - G::Property { - key: Some(rope.head), - value: Some(value_), - flags: own_key_property_flags(&rope.head), - ..G::Property::default() - }, - ); - Ok(()) - } - - pub fn get_or_put_array(&mut self, rope: &Rope, bump: &Bump) -> Result { - let head_key = match rope.head.data.e_string() { - Some(s) => s.data, - None => return Err(SetError::Clobber), - }; - if let Some(existing) = self.get(&head_key) { - match existing.data { - crate::expr::Data::EArray(mut array) => { - let Some(next) = rope.next_ref() else { - return Ok(existing); - }; - - if let Some(last) = array.items.last_mut() { - if !matches!(last.data, crate::expr::Data::EObject(_)) { - return Err(SetError::Clobber); - } - return last - .data - .e_object_mut() - .unwrap() - .get_or_put_array(next, bump); - } - - return Err(SetError::Clobber); - } - crate::expr::Data::EObject(mut object) => { - let Some(next) = rope.next_ref() else { - return Err(SetError::Clobber); - }; - return object.get_or_put_array(next, bump); - } - _ => { - return Err(SetError::Clobber); - } - } - } - - if let Some(next) = rope.next_ref() { - let mut obj = Expr::init(Object::default(), rope.head.loc); - let out = obj - .data - .e_object_mut() - .unwrap() - .get_or_put_array(next, bump)?; - VecExt::append( - &mut self.properties, - G::Property { - key: Some(rope.head), - value: Some(obj), - flags: own_key_property_flags(&rope.head), - ..G::Property::default() - }, - ); - return Ok(out); - } - - let out = Expr::init(Array::default(), rope.head.loc); - VecExt::append( - &mut self.properties, - G::Property { - key: Some(rope.head), - value: Some(out), - flags: own_key_property_flags(&rope.head), - ..G::Property::default() - }, - ); - Ok(out) } /// Assumes each key in the property is a string diff --git a/src/ast/lexer_log.rs b/src/ast/lexer_log.rs index ad0c7b359db0..2db8ba683fef 100644 --- a/src/ast/lexer_log.rs +++ b/src/ast/lexer_log.rs @@ -1,10 +1,10 @@ //! Shared lexer→Log error-reporting cluster. //! -//! js_parser, json, and toml lexers each carried a near-identical 50-line block +//! The js_parser and json lexers each carried a near-identical 50-line block //! of `{syntax_error, add_error, add_range_error, add_default_error, //! add_syntax_error}` that gate on `is_log_disabled`, dedup against //! `prev_error_loc`, push into `Log`, then record the loc. This trait -//! collapses all three. +//! collapses both. //! //! The trait carries a `'s` lifetime so `source()` can hand back the lexer's //! stored `&'s Source` *without* borrowing `self` — that is what lets the @@ -17,7 +17,7 @@ use crate::{AddErrorOptions, Loc, Log, Range, Source, usize2loc}; pub trait LexerLog<'s> { /// Per-lexer error variant returned from the `*_error` family - /// (`Error::SyntaxError` for js/toml, `crate::Error::SyntaxError` for + /// (`Error::SyntaxError` for js, `crate::Error::SyntaxError` for /// the JSON-subset lexer). type Err; @@ -29,16 +29,11 @@ pub trait LexerLog<'s> { fn start(&self) -> usize; fn syntax_err() -> Self::Err; - /// js/json gate every push on this; toml has no flag (default `false`). + /// js/json gate every push on this. #[inline] fn is_log_disabled(&self) -> bool { false } - /// toml threads `should_redact_logs` into every message; js/json don't. - #[inline] - fn should_redact(&self) -> bool { - false - } // ── provided cluster ──────────────────────────────────────────────── @@ -52,13 +47,11 @@ pub trait LexerLog<'s> { return; } let source = self.source(); - let redact = self.should_redact(); self.log_mut().add_error_fmt_opts( args, AddErrorOptions { source: Some(source), loc: l, - redact_sensitive_information: redact, ..Default::default() }, ); @@ -74,14 +67,12 @@ pub trait LexerLog<'s> { return Ok(()); } let source = self.source(); - let redact = self.should_redact(); self.log_mut().add_error_fmt_opts( args, AddErrorOptions { source: Some(source), loc: r.loc, len: r.len, - redact_sensitive_information: redact, ..Default::default() }, ); diff --git a/src/bun_core/string/immutable.rs b/src/bun_core/string/immutable.rs index d224cd8a525d..86ab2415c60a 100644 --- a/src/bun_core/string/immutable.rs +++ b/src/bun_core/string/immutable.rs @@ -233,7 +233,7 @@ pub mod unicode { /// (invalid lead byte → 1). Stops early at EOF or a truncated trailing sequence, /// returning the slice up to the last complete codepoint boundary. /// -/// Shared body of `js_parser::Lexer::peek` / `toml::Lexer::peek`. +/// Shared body of `js_parser::Lexer::peek`. #[inline] pub fn peek_n_codepoints_wtf8(bytes: &[u8], at: usize, n: usize) -> &[u8] { let mut end = at; @@ -250,9 +250,9 @@ pub fn peek_n_codepoints_wtf8(bytes: &[u8], at: usize, n: usize) -> &[u8] { &bytes[at..end] } -/// WTF-8 codepoint stepper shared by the JS / JSON / TOML lexers. +/// WTF-8 codepoint stepper shared by the JS and JSON lexers. /// -/// The JS, JSON, and TOML lexers all call the same +/// The JS and JSON lexers call the same /// `wtf8_byte_sequence_length_with_invalid` / `decode_wtf8_rune_t_multibyte` /// pair defined alongside this module, so the stepper belongs here. /// diff --git a/src/ini/lib.rs b/src/ini/lib.rs index 194aedb197ec..b091a627e160 100644 --- a/src/ini/lib.rs +++ b/src/ini/lib.rs @@ -246,7 +246,7 @@ mod draft { /// consumed by `E::Object::get_or_put_object`, which recurses once per /// `rope.next` link, so an unbounded header overflows the stack. Past the /// cap the remainder of the header (dots included) becomes the final - /// segment. Mirrors `MAX_DOTTED_KEY_SEGMENTS` in the TOML parser. + /// segment. const MAX_SECTION_ROPE_SEGMENTS: usize = 512; // ────────────────────────────────────────────────────────────────────────── diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index c3561c766780..93aa3628dab9 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -1059,6 +1059,20 @@ impl JSValue { pub fn get_unix_timestamp(self) -> f64 { JSC__JSValue__getUnixTimestamp(self) } + /// `Date.prototype.toISOString` output via JSC's date cache: `None` when + /// `self` is not a `Date` or its time value is `NaN`; years outside + /// 0000-9999 use ECMAScript's expanded `+YYYYYY`/`-YYYYYY` form. + pub fn to_iso_string<'a>( + self, + global: &JSGlobalObject, + buf: &'a mut [u8; 64], + ) -> Option<&'a [u8]> { + let len = JSC__JSValue__toISOString(self, global, buf); + if len <= 0 { + return None; + } + Some(&buf[..len as usize]) + } /// Returns `(ptr, len)` of the cell's `ClassInfo` name (static C string). pub fn get_class_info_name(self) -> Option<&'static [u8]> { if !self.is_cell() { @@ -2007,6 +2021,13 @@ unsafe extern "C" { exception: &mut ZigException, ); safe fn JSC__JSValue__getUnixTimestamp(this: JSValue) -> f64; + // safe: `&mut [u8; 64]` is ABI-identical to the non-null 64-byte out-buffer + // the C++ side requires (`Bun::toISOString` writes at most 28 bytes). + safe fn JSC__JSValue__toISOString( + this: JSValue, + global: &JSGlobalObject, + buf: &mut [u8; 64], + ) -> i32; safe fn JSC__JSValue__isPrimitive(this: JSValue) -> bool; safe fn JSC__JSValue__getOwnByValue( this: JSValue, diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index e694c218f38b..3a30e116154e 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -5746,10 +5746,11 @@ extern "C" EncodedJSValue JSC__JSValue__dateInstanceFromNullTerminatedString(JSC return JSValue::encode(date); } -// this is largely copied from dateProtoFuncToISOString -extern "C" int JSC__JSValue__toISOString(JSC::JSGlobalObject* globalObject, EncodedJSValue dateValue, char* buf) +// Formats a Date's internal time value with JSC's date cache, as +// `Date.prototype.toISOString` does (`Bun::toISOString` is copied from it). +// Returns -1 when `dateValue` is not a Date or its time value is NaN. +extern "C" int JSC__JSValue__toISOString(EncodedJSValue dateValue, JSC::JSGlobalObject* globalObject, char buf[64]) { - char buffer[64]; JSC::DateInstance* thisDateObj = dynamicDowncast(JSC::JSValue::decode(dateValue)); if (!thisDateObj) return -1; @@ -5759,7 +5760,7 @@ extern "C" int JSC__JSValue__toISOString(JSC::JSGlobalObject* globalObject, Enco auto& vm = JSC::getVM(globalObject); - return static_cast(Bun::toISOString(vm, thisDateObj->internalNumber(), buffer)); + return static_cast(Bun::toISOString(vm, thisDateObj->internalNumber(), buf)); } extern "C" int JSC__JSValue__DateNowISOString(JSC::JSGlobalObject* globalObject, char* buf) diff --git a/src/parsers/Cargo.toml b/src/parsers/Cargo.toml index 3d44175867eb..bc135f3c7166 100644 --- a/src/parsers/Cargo.toml +++ b/src/parsers/Cargo.toml @@ -29,6 +29,7 @@ bun_collections.workspace = true bun_highway.workspace = true bun_ast.workspace = true bun_wyhash.workspace = true +bun_simdutf_sys.workspace = true [dev-dependencies] criterion = "0.5" diff --git a/src/parsers/error.rs b/src/parsers/error.rs index 7ad71cf079b0..67d8d8144e99 100644 --- a/src/parsers/error.rs +++ b/src/parsers/error.rs @@ -37,18 +37,4 @@ impl bun_core::output::ErrName for Error { } } -impl From for Error { - fn from(e: crate::toml::lexer::Error) -> Self { - use crate::toml::lexer::Error as LexErr; - match e { - LexErr::UTF8Fail => Error::UTF8Fail, - LexErr::OutOfMemory => Error::Alloc(bun_alloc::AllocError), - LexErr::SyntaxError => Error::SyntaxError, - LexErr::UnexpectedSyntax => Error::UnexpectedSyntax, - LexErr::JSONStringsMustUseDoubleQuotes => Error::JSONStringsMustUseDoubleQuotes, - LexErr::ParserError => Error::ParserError, - } - } -} - pub type Result = core::result::Result; diff --git a/src/parsers/toml.rs b/src/parsers/toml.rs index 94872ac93e43..aff96cfad4fb 100644 --- a/src/parsers/toml.rs +++ b/src/parsers/toml.rs @@ -1,466 +1,1864 @@ -use bun_alloc::Arena as Bump; -use bun_collections::VecExt as _; +//! TOML v1.1.0 token-based scanner/parser. +//! +//! Architecture (mirrors `json5.rs`): a scanner reads source bytes and +//! produces typed tokens; the parser only consumes tokens and never touches +//! source bytes — `Parser` has no access to the byte cursor, so the boundary +//! is enforced by the compiler, not convention. +//! +//! TOML's lexical grammar is positional (`3.14` is a float in value position +//! but two key segments in key position; `1979-05-27` is a date or a bare +//! key), so the parser selects a scan mode per grammar production, and each +//! mode returns a narrow token type that can only represent what is legal at +//! that position. Trivia is positional too (`ws` vs `ws-comment-newline` in +//! the ABNF), so each scan mode skips exactly the trivia its position allows. +//! +//! JS value mapping: +//! - integers parse as `f64` but are validated as 64-bit integers first; +//! values outside `Number.MAX_SAFE_INTEGER` are errors (TOML requires +//! lossless handling or an error) +//! - date/time values (all four kinds) become strings of their source text +//! - strings are UTF-8; non-ASCII content is re-encoded to UTF-16 EStrings +//! so both the JS conversion and the printer paths agree -use bun_ast::{self, self as js_ast, E, Expr, LexerLog as _}; +use bun_alloc::Arena as Bump; +use bun_alloc::ArenaVec; +use bun_alloc::ArenaVecExt as _; +use bun_ast::{self, E, Expr, Loc, Log, Source}; +use bun_collections::HashMap; use bun_core::{self, StackCheck}; -#[path = "toml/lexer.rs"] -pub mod lexer; -pub use self::lexer::Lexer; -use self::lexer::T; +/// Tracks how a table or array came to exist, which decides whether later +/// syntax may extend it. See "Table" and "Array of Tables" in the spec. +#[derive(Copy, Clone, PartialEq, Eq)] +enum Kind { + /// `[a]` — explicitly defined by a table header. + Header, + /// Created on the way to a deeper header (`[a.b]` creates `a`). + HeaderImplicit, + /// Created by a dotted key (`a.b = 1` creates `a`); records the block so + /// only dotted keys from the same block may extend it. + Dotted, + /// An element of an array of tables. + ArrayElem, + /// `{ ... }` — closed to all later extension. + Inline, + /// `[[a]]` — appendable only by another `[[a]]`. + AotArray, + /// `a = [ ... ]` — a value; never extendable. + StaticArray, +} + +#[derive(Copy, Clone)] +struct Meta { + kind: Kind, + block: u32, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +enum PErr { + /// Already logged. + Syntax, + Oom, + StackOverflow, +} + +impl From for PErr { + fn from(_: bun_alloc::AllocError) -> Self { + PErr::Oom + } +} + +type PResult = Result; + +/// A decoded key segment: the key text (borrowed from the source or built in +/// the bump arena when escapes were involved) plus its source position. +#[derive(Copy, Clone)] +struct KeySeg<'a> { + text: &'a [u8], + pos: usize, +} + +// ── tokens ────────────────────────────────────────────────────────────────── +// +// Each scan mode returns its own narrow token type: a token that is illegal +// at a grammar position cannot be produced there. + +/// What begins a top-level expression. +enum LineStart<'a> { + Eof, + /// `[` (`aot` for `[[`) at `pos`. + TableOpen { + aot: bool, + pos: usize, + }, + Key(KeySeg<'a>), +} -type Rope = js_ast::e::Rope; -use js_ast::e::SetError; +/// What follows a key segment in a `key = value` path. +enum KeyvalSep { + Dot, + Equals, +} -// ────────────────────────────────────────────────────────────────────────── -// TOML parser -// ────────────────────────────────────────────────────────────────────────── +/// What follows a key segment inside a `[...]` / `[[...]]` header. +enum HeaderSep { + Dot, + Close, +} + +/// What begins an entry inside an inline table. +enum InlineKey<'a> { + Key(KeySeg<'a>), + Close, + Eof { pos: usize }, +} + +/// What separates or ends list elements (arrays and inline tables). +enum ListSep { + Comma, + Close, +} + +/// A value token: the payload is fully decoded by the scanner. +#[derive(Copy, Clone)] +struct ValueToken<'a> { + pos: usize, + data: ValueData<'a>, +} + +#[derive(Copy, Clone)] +enum ValueData<'a> { + String { + text: &'a [u8], + is_ascii: bool, + }, + Number(f64), + /// All four TOML date/time kinds, as their source text (always ASCII). + DateTime(&'a [u8]), + Boolean(bool), + ArrayOpen, + InlineOpen, +} -pub struct TOML<'a> { - pub lexer: Lexer<'a>, - // No separate `log` field — all logging goes through `lexer.log`, avoiding - // a second `&mut Log` borrow overlapping `lexer.log`. - pub bump: &'a Bump, - pub stack_check: StackCheck, +/// What occupies an array element position. +enum ArrayItem<'a> { + Value(ValueToken<'a>), + Close, + Eof { pos: usize }, } -impl<'a> TOML<'a> { - pub fn init( +pub struct TOML; + +impl TOML { + pub fn parse<'a>( + source: &'a Source, + log: &mut Log, bump: &'a Bump, - source_: &'a bun_ast::Source, - log: &'a mut bun_ast::Log, redact_logs: bool, - ) -> crate::Result> { - Ok(TOML { - lexer: Lexer::init(log, source_, bump, redact_logs)?, + ) -> crate::Result { + let mut parser = Parser { + scanner: Scanner { + src: source.contents.as_ref(), + pos: 0, + bump, + source, + log, + redact: redact_logs, + }, bump, stack_check: StackCheck::init(), - }) + meta: HashMap::default(), + block: 0, + }; + match parser.parse_root() { + Ok(root) => Ok(root), + Err(PErr::Syntax) => Err(crate::Error::SyntaxError), + Err(PErr::Oom) => Err(crate::Error::Alloc(bun_alloc::AllocError)), + Err(PErr::StackOverflow) => Err(crate::Error::StackOverflow), + } } +} + +const MAX_SAFE_INTEGER: i64 = (1 << 53) - 1; + +const BARE_CR: &[u8] = b"Bare carriage return is not allowed; use \\r\\n or \\n"; +const UNDERSCORE_IN_NUMBER: &[u8] = b"Underscores in numbers must be surrounded by digits"; + +fn is_bare_key_char(c: u8) -> bool { + c.is_ascii_alphanumeric() || c == b'-' || c == b'_' +} + +fn loc_of(pos: usize) -> Loc { + Loc { + start: i32::try_from(pos).expect("source length is bounded by i32::MAX"), + } +} + +// ── scanner ───────────────────────────────────────────────────────────────── + +/// Owns the byte cursor. The only component that reads source bytes; every +/// public method scans one token (or one fixed construct) for one grammar +/// position and skips exactly the leading trivia that position allows. +struct Scanner<'a, 'log> { + src: &'a [u8], + pos: usize, + bump: &'a Bump, + source: &'a Source, + log: &'log mut Log, + redact: bool, +} + +impl<'a, 'log> Scanner<'a, 'log> { + // ── error helpers ────────────────────────────────────────────────────── + + fn err(&mut self, pos: usize, msg: &'static [u8]) -> PErr { + self.err_fmt(pos, format_args!("{}", bstr::BStr::new(msg))) + } + + fn err_fmt(&mut self, pos: usize, args: core::fmt::Arguments<'_>) -> PErr { + self.log.add_error_fmt_opts( + args, + bun_ast::AddErrorOptions { + source: Some(self.source), + loc: loc_of(pos), + len: 0, + redact_sensitive_information: self.redact, + }, + ); + PErr::Syntax + } + + /// `{before} '{key}'{after}`; the key text is omitted when redacting. + fn err_keyed( + &mut self, + pos: usize, + before: &'static str, + key: &[u8], + after: &'static str, + ) -> PErr { + if self.redact { + self.err_fmt(pos, format_args!("{}{}", before, after)) + } else { + self.err_fmt( + pos, + format_args!("{} '{}'{}", before, bstr::BStr::new(key), after), + ) + } + } + + fn err_char(&mut self, pos: usize, what: &'static str) -> PErr { + match self.src.get(pos).copied() { + None => self.err_fmt(pos, format_args!("{} end of file", what)), + Some(_) if self.redact => self.err_fmt(pos, format_args!("{} (redacted)", what)), + Some(c) if c.is_ascii_graphic() => { + self.err_fmt(pos, format_args!("{} '{}'", what, c as char)) + } + Some(c) => self.err_fmt(pos, format_args!("{} (0x{:02X})", what, c)), + } + } + + /// A bare word in value position is almost always an unquoted string, + /// which the old parser silently accepted; name the fix directly. + fn err_unquoted_string(&mut self, pos: usize) -> PErr { + let mut end = pos; + while end < self.src.len() && is_bare_key_char(self.peek_at(end)) && end - pos < 64 { + end += 1; + } + if self.redact || end == pos { + return self.err(pos, b"Strings must be quoted"); + } + self.err_fmt( + pos, + format_args!( + "Strings must be quoted: \"{}\"", + bstr::BStr::new(&self.src[pos..end]) + ), + ) + } + + // ── byte cursor ──────────────────────────────────────────────────────── #[inline] - pub fn source(&self) -> &'a bun_ast::Source { - self.lexer.source + fn peek(&self) -> u8 { + self.peek_at(self.pos) } - // Single generic forwarding to Expr::init. - pub fn e(&self, t: D, loc: bun_ast::Loc) -> Expr - where - D: js_ast::ExprInit, - { - Expr::init(t, loc) + #[inline] + fn peek_at(&self, pos: usize) -> u8 { + if pos < self.src.len() { + self.src[pos] + } else { + 0 + } } - pub fn parse( - source_: &'a bun_ast::Source, - log: &'a mut bun_ast::Log, - bump: &'a Bump, - redact_logs: bool, - ) -> crate::Result { - match source_.contents.len() { - // This is to be consisntent with how disabled JS files are handled - 0 => { - return Ok(Expr { - loc: bun_ast::Loc { start: 0 }, - data: Expr::init(E::Object::default(), bun_ast::Loc::EMPTY).data, - }); + #[inline] + fn at_eof(&self) -> bool { + self.pos >= self.src.len() + } + + /// Skips spaces and tabs (`ws` in the ABNF). + fn skip_ws(&mut self) { + while matches!(self.peek(), b' ' | b'\t') { + self.pos += 1; + } + } + + /// Consumes a newline (LF or CRLF). Returns an error for a bare CR. + fn expect_newline(&mut self) -> PResult<()> { + match self.peek() { + b'\n' => { + self.pos += 1; + Ok(()) } - _ => {} + b'\r' => { + if self.peek_at(self.pos + 1) == b'\n' { + self.pos += 2; + Ok(()) + } else { + Err(self.err(self.pos, BARE_CR)) + } + } + _ => Err(self.err_char(self.pos, "Expected a newline but found")), } + } - // The `Lexer` borrows the `Source` (`&'a Source`) so - // `identifier`/`string_literal_slice` can point into `source.contents` - // for `'a` without a self-referential struct — no copy needed. - let mut parser = TOML::init(bump, source_, log, redact_logs)?; + /// Scans a `# comment` up to (not including) the line terminator, + /// rejecting control characters. + fn skip_comment(&mut self) -> PResult<()> { + debug_assert_eq!(self.peek(), b'#'); + self.pos += 1; + loop { + match self.peek() { + 0 if self.at_eof() => return Ok(()), + b'\n' => return Ok(()), + b'\r' => { + if self.peek_at(self.pos + 1) == b'\n' { + return Ok(()); + } + return Err(self.err(self.pos, BARE_CR)); + } + b'\t' => self.pos += 1, + c if c < 0x20 || c == 0x7F => { + return Err( + self.err_char(self.pos, "Control character is not allowed in a comment:") + ); + } + _ => self.pos += 1, + } + } + } - parser.run_parser() + /// Skips whitespace, comments, and newlines (`ws-comment-newline`). + fn skip_ws_comment_newline(&mut self) -> PResult<()> { + loop { + match self.peek() { + b' ' | b'\t' => self.pos += 1, + b'\n' | b'\r' => self.expect_newline()?, + b'#' => self.skip_comment()?, + _ => return Ok(()), + } + } } - pub fn parse_maybe_trailing_comma(&mut self, closer: T) -> crate::Result { - self.lexer.expect(T::t_comma)?; + // ── document setup ───────────────────────────────────────────────────── - if self.lexer.token == closer { - return Ok(false); + /// Whole-document validation and BOM handling, before any scanning. + /// Returns the position of the first content byte. + fn init_document(&mut self) -> PResult { + // A TOML document must be valid UTF-8 as a whole. + let validation = bun_simdutf_sys::simdutf::validate::with_errors::utf8(self.src); + if !validation.is_successful() { + return Err(self.err(validation.count, b"Invalid UTF-8 byte sequence")); + } + // Skip a leading byte-order mark. + if self.src.starts_with(b"\xEF\xBB\xBF") { + self.pos = 3; } + Ok(self.pos) + } + + // ── scan modes ───────────────────────────────────────────────────────── + // + // expression = ws-comment-newline ( keyval / table ) — what may begin a + // top-level expression. - Ok(true) + fn scan_line_start(&mut self) -> PResult> { + self.skip_ws_comment_newline()?; + if self.at_eof() { + return Ok(LineStart::Eof); + } + if self.peek() == b'[' { + let pos = self.pos; + self.pos += 1; + // `array-table-open = %x5B.5B`: the second bracket is adjacent. + let aot = self.peek() == b'['; + if aot { + self.pos += 1; + } + return Ok(LineStart::TableOpen { aot, pos }); + } + Ok(LineStart::Key(self.scan_key_segment()?)) } - // ── AST-producing methods ────────────────────────────────────────────── + /// One key segment (bare, basic-quoted, or literal-quoted). + fn scan_key_segment(&mut self) -> PResult> { + let pos = self.pos; + match self.peek() { + b'"' => { + let (text, _) = self.scan_basic_string(false)?; + Ok(KeySeg { text, pos }) + } + b'\'' => { + let (text, _) = self.scan_literal_string(false)?; + Ok(KeySeg { text, pos }) + } + c if is_bare_key_char(c) => { + let start = self.pos; + while is_bare_key_char(self.peek()) { + self.pos += 1; + } + Ok(KeySeg { + text: &self.src[start..self.pos], + pos, + }) + } + _ => Err(self.err_char(pos, "Expected a key but found")), + } + } - pub fn parse_key_segment(&mut self) -> crate::Result> { - let loc = self.lexer.loc(); + /// The segment after a dot or a table-open bracket (`dot-sep = ws "." ws` + /// and `std-table-open = "[" ws`: spaces/tabs only, then a key). + fn scan_key_after_sep(&mut self) -> PResult> { + self.skip_ws(); + self.scan_key_segment() + } - match self.lexer.token { - T::t_string_literal => { - let str = self.lexer.to_string(loc); - self.lexer.next()?; - Ok(Some(str)) + /// After a key segment in a `key = value` path: `ws` then `.` or `=`. + fn scan_keyval_sep(&mut self) -> PResult { + self.skip_ws(); + match self.peek() { + b'.' => { + self.pos += 1; + Ok(KeyvalSep::Dot) } - T::t_identifier => { - let str = E::String::init(self.lexer.identifier); - self.lexer.next()?; - Ok(Some(self.e(str, loc))) + b'=' => { + self.pos += 1; + Ok(KeyvalSep::Equals) } - T::t_false => { - self.lexer.next()?; - Ok(Some(self.e(E::String::init(b"false"), loc))) + _ => Err(self.err_char(self.pos, "Expected '=' after a key but found")), + } + } + + /// After a key segment in a header: `ws` then `.` or the closing + /// bracket(s). `]]` must be adjacent (`array-table-close = %x5D.5D`). + fn scan_header_sep(&mut self, aot: bool) -> PResult { + self.skip_ws(); + match self.peek() { + b'.' => { + self.pos += 1; + Ok(HeaderSep::Dot) } - T::t_true => { - self.lexer.next()?; - Ok(Some(self.e(E::String::init(b"true"), loc))) + b']' => { + self.pos += 1; + if aot { + if self.peek() != b']' { + return Err(self.err_char( + self.pos, + "Expected ']]' to close an array-of-tables header but found", + )); + } + self.pos += 1; + } + Ok(HeaderSep::Close) } - // what we see as a number here could actually be a string - T::t_numeric_literal => { - let literal = self.lexer.raw(); - self.lexer.next()?; - Ok(Some(self.e(E::String::init(literal), loc))) + _ => Err(if aot { + self.err_char( + self.pos, + "Expected ']]' to close an array-of-tables header but found", + ) + } else { + self.err_char(self.pos, "Expected ']' to close a table header but found") + }), + } + } + + /// What begins an inline-table entry: `ws-comment-newline` then a key or + /// the closing brace (TOML 1.1 allows multi-line inline tables). + fn scan_inline_key(&mut self) -> PResult> { + self.skip_ws_comment_newline()?; + if self.peek() == b'}' { + self.pos += 1; + return Ok(InlineKey::Close); + } + if self.at_eof() { + return Ok(InlineKey::Eof { pos: self.pos }); + } + Ok(InlineKey::Key(self.scan_key_segment()?)) + } + + /// The value after `=`: `keyval-sep = ws %x3D ws` — spaces/tabs only, + /// never a newline or comment, then exactly one value. + fn scan_value_required(&mut self) -> PResult> { + self.skip_ws(); + match self.peek() { + b'\n' | b'\r' => { + return Err(self.err( + self.pos, + b"Missing value after '='; values must be on the same line", + )); + } + 0 if self.at_eof() => { + return Err(self.err(self.pos, b"Missing value after '='")); } + _ => {} + } + self.scan_value_token() + } - _ => Ok(None), + /// An array element position: `ws-comment-newline` then a value, the + /// closing bracket (empty array or trailing comma), or EOF. + fn scan_array_item(&mut self) -> PResult> { + self.skip_ws_comment_newline()?; + if self.peek() == b']' { + self.pos += 1; + return Ok(ArrayItem::Close); } + if self.at_eof() { + return Ok(ArrayItem::Eof { pos: self.pos }); + } + Ok(ArrayItem::Value(self.scan_value_token()?)) } - #[allow(clippy::mut_from_ref)] - pub fn parse_key(&mut self, bump: &'a Bump) -> crate::Result<&'a mut Rope> { - // Allocate from the caller-provided bump and return `&mut Rope` - // borrowed from it. - let rope: &mut Rope = bump.alloc(Rope { - head: match self.parse_key_segment()? { - Some(seg) => seg, - None => { - self.lexer.expected_string(b"key")?; - return Err(crate::Error::SyntaxError); + /// After a list element: `ws-comment-newline` then `,` or the closer. + fn scan_list_sep(&mut self, close: u8, what: &'static str) -> PResult { + self.skip_ws_comment_newline()?; + let c = self.peek(); + if c == b',' { + self.pos += 1; + return Ok(ListSep::Comma); + } + if c == close { + self.pos += 1; + return Ok(ListSep::Close); + } + Err(self.err_char(self.pos, what)) + } + + /// After an expression: optional whitespace, optional comment, then a + /// newline or EOF. + fn scan_line_end(&mut self, after: &'static [u8]) -> PResult<()> { + self.skip_ws(); + if self.peek() == b'#' { + self.skip_comment()?; + } + if self.at_eof() { + return Ok(()); + } + match self.peek() { + b'\n' | b'\r' => self.expect_newline(), + _ => Err(self.err_fmt( + self.pos, + format_args!( + "Expected a newline or end of file after {}", + bstr::BStr::new(after) + ), + )), + } + } + + // ── value scanning ───────────────────────────────────────────────────── + + /// One value token at the cursor (leading trivia already handled by the + /// mode wrappers). Scalars are fully decoded here. + fn scan_value_token(&mut self) -> PResult> { + let pos = self.pos; + let data = match self.peek() { + b'"' => { + let (text, is_ascii) = if self.src[self.pos..].starts_with(b"\"\"\"") { + self.scan_basic_string(true)? + } else { + self.scan_basic_string(false)? + }; + ValueData::String { text, is_ascii } + } + b'\'' => { + let (text, is_ascii) = if self.src[self.pos..].starts_with(b"'''") { + self.scan_literal_string(true)? + } else { + self.scan_literal_string(false)? + }; + ValueData::String { text, is_ascii } + } + b't' => { + self.expect_keyword(b"true")?; + ValueData::Boolean(true) + } + b'f' => { + self.expect_keyword(b"false")?; + ValueData::Boolean(false) + } + b'[' => { + self.pos += 1; + ValueData::ArrayOpen + } + b'{' => { + self.pos += 1; + ValueData::InlineOpen + } + b'i' | b'n' | b'+' | b'-' | b'0'..=b'9' => self.scan_number_or_datetime()?, + c if c.is_ascii_alphabetic() => return Err(self.err_unquoted_string(pos)), + _ => return Err(self.err_char(pos, "Expected a value but found")), + }; + Ok(ValueToken { pos, data }) + } + + fn expect_keyword(&mut self, word: &'static [u8]) -> PResult<()> { + let pos = self.pos; + if self.src[self.pos..].starts_with(word) { + let after = self.peek_at(self.pos + word.len()); + // A keyword must be followed by a value terminator, not more + // bare characters: `truex` and `tru` are both errors. + if !is_bare_key_char(after) { + self.pos += word.len(); + return Ok(()); + } + } + Err(self.err_unquoted_string(pos)) + } + + // ── numbers and date/times ───────────────────────────────────────────── + + fn scan_number_or_datetime(&mut self) -> PResult> { + // Date/times start with an unsigned digit run: `DDDD-` or `DD:`. + if self.peek().is_ascii_digit() { + let d1 = self.digit_run_len(self.pos); + if d1 == 4 && self.peek_at(self.pos + 4) == b'-' { + let text = self.scan_datetime_from_date()?; + self.expect_value_terminator()?; + return Ok(ValueData::DateTime(text)); + } + if d1 == 2 && self.peek_at(self.pos + 2) == b':' { + let start = self.pos; + self.scan_time_digits()?; + self.expect_value_terminator()?; + return Ok(ValueData::DateTime(&self.src[start..self.pos])); + } + } + + self.scan_number() + } + + fn digit_run_len(&self, start: usize) -> usize { + let mut i = start; + while self.peek_at(i).is_ascii_digit() { + i += 1; + } + i - start + } + + /// Exactly `n` ASCII digits starting at `pos`; returns their value. + fn read_digits(&mut self, n: usize, what: &'static [u8]) -> PResult { + let mut value: u32 = 0; + for _ in 0..n { + let c = self.peek(); + if !c.is_ascii_digit() { + return Err(self.err(self.pos, what)); + } + value = value * 10 + u32::from(c - b'0'); + self.pos += 1; + } + Ok(value) + } + + /// `YYYY-MM-DD` and everything that may follow it (time, offset). + /// Returns the full source text of the literal. + fn scan_datetime_from_date(&mut self) -> PResult<&'a [u8]> { + let start = self.pos; + + let year = self.read_digits(4, b"Invalid date: expected a 4-digit year")?; + if self.peek() != b'-' { + return Err(self.err(self.pos, b"Invalid date: expected '-' after the year")); + } + self.pos += 1; + let month = self.read_digits(2, b"Invalid date: expected a 2-digit month")?; + if self.peek() != b'-' { + return Err(self.err(self.pos, b"Invalid date: expected '-' after the month")); + } + self.pos += 1; + let day_pos = self.pos; + let day = self.read_digits(2, b"Invalid date: expected a 2-digit day")?; + + if month < 1 || month > 12 { + return Err(self.err(start, b"Invalid date: month must be between 01 and 12")); + } + let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + let max_day: u32 = match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + _ => { + if leap { + 29 + } else { + 28 } - }, - next: core::ptr::null_mut(), - }); - let head: *mut Rope = rope; - let mut rope: *mut Rope = rope; + } + }; + if day < 1 || day > max_day { + return Err(self.err(day_pos, b"Invalid date: day is out of range for the month")); + } + + // Optional time part: 'T'/'t', or a space when a time clearly follows. + let has_time = match self.peek() { + b'T' | b't' => { + self.pos += 1; + true + } + b' ' if self.peek_at(self.pos + 1).is_ascii_digit() + && self.peek_at(self.pos + 2).is_ascii_digit() + && self.peek_at(self.pos + 3) == b':' => + { + self.pos += 1; + true + } + _ => false, + }; + + if has_time { + self.scan_time_digits()?; + // Optional offset. + match self.peek() { + b'Z' | b'z' => { + self.pos += 1; + } + b'+' | b'-' => { + self.pos += 1; + let hour = + self.read_digits(2, b"Invalid date-time offset: expected 2-digit hours")?; + if self.peek() != b':' { + return Err(self.err( + self.pos, + b"Invalid date-time offset: expected ':' between hours and minutes", + )); + } + self.pos += 1; + let minute = + self.read_digits(2, b"Invalid date-time offset: expected 2-digit minutes")?; + if hour > 23 { + return Err(self.err( + start, + b"Invalid date-time offset: hours must be between 00 and 23", + )); + } + if minute > 59 { + return Err(self.err( + start, + b"Invalid date-time offset: minutes must be between 00 and 59", + )); + } + } + _ => {} + } + } + + Ok(&self.src[start..self.pos]) + } + + /// `HH:MM[:SS[.frac]]` — seconds are optional in TOML 1.1. + fn scan_time_digits(&mut self) -> PResult<()> { + let start = self.pos; + let hour = self.read_digits(2, b"Invalid time: expected 2-digit hours")?; + if self.peek() != b':' { + return Err(self.err(self.pos, b"Invalid time: expected ':' after hours")); + } + self.pos += 1; + let minute = self.read_digits(2, b"Invalid time: expected 2-digit minutes")?; + if hour > 23 { + return Err(self.err(start, b"Invalid time: hours must be between 00 and 23")); + } + if minute > 59 { + return Err(self.err(start, b"Invalid time: minutes must be between 00 and 59")); + } + // Seconds are optional in TOML 1.1. + if self.peek() == b':' { + self.pos += 1; + let sec_pos = self.pos; + let second = self.read_digits(2, b"Invalid time: expected 2-digit seconds")?; + // 60 covers leap seconds, per RFC 3339. + if second > 60 { + return Err(self.err(sec_pos, b"Invalid time: seconds must be between 00 and 60")); + } + if self.peek() == b'.' { + self.pos += 1; + if !self.peek().is_ascii_digit() { + return Err(self.err( + self.pos, + b"Invalid time: expected at least one digit of fractional seconds", + )); + } + while self.peek().is_ascii_digit() { + self.pos += 1; + } + } + } + Ok(()) + } + + /// Validates an `_` between digits (per `is_digit`) and consumes it. + fn check_underscore(&mut self, is_digit: impl Fn(u8) -> bool) -> PResult<()> { + if !is_digit(self.peek_at(self.pos.wrapping_sub(1))) + || !is_digit(self.peek_at(self.pos + 1)) + { + return Err(self.err(self.pos, UNDERSCORE_IN_NUMBER)); + } + self.pos += 1; + Ok(()) + } + + /// Scans `digit (digit | _)*` with underscore placement validation. + fn scan_decimal_digits(&mut self) -> PResult<()> { + loop { + let c = self.peek(); + if c.is_ascii_digit() { + self.pos += 1; + } else if c == b'_' { + self.check_underscore(|c| c.is_ascii_digit())?; + } else { + return Ok(()); + } + } + } + + fn scan_number(&mut self) -> PResult> { + let start = self.pos; + + let negative = match self.peek() { + b'-' => { + self.pos += 1; + true + } + b'+' => { + self.pos += 1; + false + } + _ => false, + }; + + // inf / nan, optionally signed. A longer bare word that merely starts + // with them (`infinity`, `nanoseconds`) is an unquoted string. + if self.src[self.pos..].starts_with(b"inf") && !is_bare_key_char(self.peek_at(self.pos + 3)) + { + self.pos += 3; + self.expect_value_terminator()?; + let value = if negative { + f64::NEG_INFINITY + } else { + f64::INFINITY + }; + return Ok(ValueData::Number(value)); + } + if self.src[self.pos..].starts_with(b"nan") && !is_bare_key_char(self.peek_at(self.pos + 3)) + { + self.pos += 3; + self.expect_value_terminator()?; + // The sign of NaN is not observable in TOML. + return Ok(ValueData::Number(f64::NAN)); + } - // Hard cap on dotted-key segments. The rope is consumed by `set_rope`, - // `get_or_put_array`, and `get_or_put_object`, each of which recurses - // once per `rope.next` link with no stack guard of their own. - const MAX_DOTTED_KEY_SEGMENTS: usize = 512; - let mut segments: usize = 1; + // Radix-prefixed integers (unsigned only). + if self.peek() == b'0' && matches!(self.peek_at(self.pos + 1), b'x' | b'o' | b'b') { + if negative || self.src[start] == b'+' { + return Err(self.err( + start, + b"A sign is not allowed on hexadecimal, octal, or binary integers", + )); + } + return self.scan_radix_integer(); + } - while self.lexer.token == T::t_dot { - self.lexer.next()?; + if !self.peek().is_ascii_digit() { + // An unsigned bare word (`linker = isolated`) is an unquoted + // string; anything after a sign is a malformed number. + if start == self.pos && self.peek().is_ascii_alphabetic() { + return Err(self.err_unquoted_string(start)); + } + return Err(self.err_char(self.pos, "Expected a number but found")); + } - let Some(seg) = self.parse_key_segment()? else { + // Integer part, accumulated as an unsigned magnitude so i64::MIN + // (magnitude 2^63) is still distinguishable from a 64-bit overflow. + let int_start = self.pos; + let mut magnitude: u64 = 0; + let mut int_overflow = false; + let mut digits = 0usize; + loop { + let c = self.peek(); + if c.is_ascii_digit() { + digits += 1; + magnitude = match magnitude + .checked_mul(10) + .and_then(|v| v.checked_add(u64::from(c - b'0'))) + { + Some(v) => v, + None => { + int_overflow = true; + 0 + } + }; + self.pos += 1; + } else if c == b'_' { + self.check_underscore(|c| c.is_ascii_digit())?; + } else { break; + } + } + if digits > 1 && self.src[int_start] == b'0' { + return Err(self.err(int_start, b"Leading zeros are not allowed in numbers")); + } + + let mut is_float = false; + + // Fractional part. + if self.peek() == b'.' { + is_float = true; + self.pos += 1; + if !self.peek().is_ascii_digit() { + return Err(self.err( + self.pos, + b"A decimal point must be followed by at least one digit", + )); + } + self.scan_decimal_digits()?; + } + + // Exponent part. + if matches!(self.peek(), b'e' | b'E') { + is_float = true; + self.pos += 1; + if matches!(self.peek(), b'+' | b'-') { + self.pos += 1; + } + if !self.peek().is_ascii_digit() { + return Err(self.err(self.pos, b"An exponent must contain at least one digit")); + } + self.scan_decimal_digits()?; + } + + self.expect_value_terminator()?; + + if is_float { + // Strip underscores and parse the whole literal as f64. + let raw = &self.src[start..self.pos]; + let value = if bun_core::strings::contains(raw, b"_") { + let mut cleaned: ArenaVec<'a, u8> = + ArenaVec::with_capacity_in(raw.len(), self.bump); + for &c in raw { + if c != b'_' { + cleaned.push(c); + } + } + bun_core::fmt::parse_double(cleaned.as_slice()) + } else { + bun_core::fmt::parse_double(raw) + }; + let value = match value { + Ok(v) => v, + Err(_) => return Err(self.err(start, b"Invalid number")), }; - segments += 1; - if segments > MAX_DOTTED_KEY_SEGMENTS { - self.lexer - .add_default_error(b"Dotted key has too many segments")?; - return Err(crate::Error::SyntaxError); + return Ok(ValueData::Number(value)); + } + + let signed_limit = if negative { + 1u64 << 63 // |i64::MIN| + } else { + i64::MAX as u64 + }; + if int_overflow || magnitude > signed_limit { + return Err(self.err(start, b"Integer is outside the 64-bit signed range")); + } + if magnitude > MAX_SAFE_INTEGER as u64 { + return Err(self.err( + start, + b"Integer cannot be losslessly represented as a JavaScript number; it must be within +/-(2^53 - 1)", + )); + } + // magnitude <= 2^53 - 1, so the casts are exact. + let signed = if negative { + -(magnitude as i64) + } else { + magnitude as i64 + }; + Ok(ValueData::Number(signed as f64)) + } + + fn scan_radix_integer(&mut self) -> PResult> { + let start = self.pos; + debug_assert_eq!(self.peek(), b'0'); + let radix_char = self.peek_at(self.pos + 1); + let radix: u64 = match radix_char { + b'x' => 16, + b'o' => 8, + _ => 2, + }; + self.pos += 2; + + let is_digit = |c: u8| -> bool { + match radix { + 16 => c.is_ascii_hexdigit(), + 8 => (b'0'..=b'7').contains(&c), + _ => c == b'0' || c == b'1', } - // SAFETY: `rope` points into `bump` and is live for this call; we are - // the sole mutator. Raw pointers used to avoid stacked &mut reborrows. - unsafe { - rope = (*rope).append(seg, bump)?; + }; + + if !is_digit(self.peek()) { + return Err(self.err( + self.pos, + b"Expected at least one digit after the radix prefix", + )); + } + + let mut value: u64 = 0; + let mut overflow = false; + loop { + let c = self.peek(); + if is_digit(c) { + let digit = u64::from( + bun_core::fmt::hex_digit_value_u32(u32::from(c)).expect("checked by is_digit"), + ); + value = match value.checked_mul(radix).and_then(|v| v.checked_add(digit)) { + Some(v) => v, + None => { + overflow = true; + 0 + } + }; + self.pos += 1; + } else if c == b'_' { + self.check_underscore(is_digit)?; + } else if c.is_ascii_alphanumeric() { + return Err(self.err_char(self.pos, "Invalid digit in number:")); + } else { + break; } } - // SAFETY: `head` was just allocated from `bump` above and is non-null. - Ok(unsafe { &mut *head }) + self.expect_value_terminator()?; + + if overflow || value > i64::MAX as u64 { + return Err(self.err(start, b"Integer is outside the 64-bit signed range")); + } + if value as i64 > MAX_SAFE_INTEGER { + return Err(self.err( + start, + b"Integer cannot be losslessly represented as a JavaScript number; it must be within +/-(2^53 - 1)", + )); + } + Ok(ValueData::Number(value as f64)) } - fn run_parser(&mut self) -> crate::Result { - let root = self.e(E::Object::default(), self.lexer.loc()); - let mut head: *mut E::Object = root - .data - .e_object() - .expect("infallible: variant checked") - .as_ptr(); - // SAFETY: `head` aliases into `root.data`; the raw pointer sidesteps - // overlapping &mut on `root`. + /// A number or keyword value must be followed by something that can + /// legitimately come after a value. + fn expect_value_terminator(&mut self) -> PResult<()> { + match self.peek() { + 0 if self.at_eof() => Ok(()), + b' ' | b'\t' | b'\n' | b'\r' | b',' | b']' | b'}' | b'#' => Ok(()), + _ => Err(self.err_char(self.pos, "Unexpected character after a value:")), + } + } + + // ── strings ──────────────────────────────────────────────────────────── - // Uses the parser's bump directly. - let key_allocator = self.bump; + /// Counts the quote run at the cursor. Runs of 3-5 close a multi-line + /// string (the final 3 are the delimiter, up to 2 belong to the content); + /// longer runs are an error. + fn quote_run_close(&mut self, quote: u8) -> PResult<(usize, bool)> { + let mut run = 0usize; + while self.peek_at(self.pos + run) == quote { + run += 1; + } + if run > 5 { + return Err(self.err( + self.pos, + b"Too many quotes at the end of a multi-line string", + )); + } + Ok((run, run >= 3)) + } + /// Copies the borrowed prefix `src[start..end]` into a buffer the first + /// time decoding has to diverge from the source bytes. + fn materialize<'b>( + bump: &'a Bump, + src: &'a [u8], + start: usize, + end: usize, + buf: &'b mut Option>, + ) -> &'b mut ArenaVec<'a, u8> { + if buf.is_none() { + let mut b: ArenaVec<'a, u8> = ArenaVec::with_capacity_in(end - start + 16, bump); + b.extend_from_slice(&src[start..end]); + *buf = Some(b); + } + buf.as_mut().expect("just set") + } + + /// Returns (decoded bytes, is_ascii). The content borrows the source + /// until an escape, CRLF normalization, or quote-run handling forces a + /// copy — most strings have neither. + fn scan_basic_string(&mut self, multiline: bool) -> PResult<(&'a [u8], bool)> { + let open_pos = self.pos; + self.pos += if multiline { 3 } else { 1 }; + + if multiline { + // A newline immediately after the opening delimiter is trimmed. + match self.peek() { + b'\n' => self.pos += 1, + b'\r' if self.peek_at(self.pos + 1) == b'\n' => self.pos += 2, + _ => {} + } + } + + let start = self.pos; + let mut buf: Option> = None; + let mut is_ascii = true; loop { - let loc = self.lexer.loc(); - match self.lexer.token { - T::t_end_of_file => { - return Ok(root); - } - // child table - T::t_open_bracket => { - self.lexer.next()?; - let key = self.parse_key(key_allocator)?; - - self.lexer.expect(T::t_close_bracket)?; - if !self.lexer.has_newline_before { - self.lexer.expected_string(b"line break")?; - } - - let parent_object = match root - .data - .e_object() - .unwrap() - .get_or_put_object(key, self.bump) - { - Ok(v) => v, - Err(SetError::Clobber) => { - self.lexer.add_default_error(b"Table already defined")?; - return Err(crate::Error::SyntaxError); - } - Err(SetError::OutOfMemory) => { - return Err(crate::Error::Alloc(bun_alloc::AllocError)); + if self.at_eof() { + return Err(self.err(open_pos, b"Unterminated string")); + } + let c = self.peek(); + match c { + b'"' => { + if !multiline { + let text = match buf { + Some(b) => b.into_bump_slice(), + None => &self.src[start..self.pos], + }; + self.pos += 1; + return Ok((text, is_ascii)); + } + let (run, closes) = self.quote_run_close(b'"')?; + if closes { + let extra = run - 3; + let text = match buf.take() { + Some(mut b) => { + for _ in 0..extra { + b.push(b'"'); + } + b.into_bump_slice() + } + None => &self.src[start..self.pos + extra], + }; + self.pos += run; + return Ok((text, is_ascii)); + } + if let Some(b) = &mut buf { + for _ in 0..run { + b.push(b'"'); } - }; - head = parent_object - .data - .e_object() - .expect("infallible: variant checked") - .as_ptr(); - } - // child table array - T::t_open_bracket_double => { - self.lexer.next()?; - - let key = self.parse_key(key_allocator)?; - - self.lexer.expect(T::t_close_bracket_double)?; - if !self.lexer.has_newline_before { - self.lexer.expected_string(b"line break")?; - } - - let array = match root - .data - .e_object() - .unwrap() - .get_or_put_array(key, self.bump) - { - Ok(v) => v, - Err(SetError::Clobber) => { - self.lexer - .add_default_error(b"Cannot overwrite table array")?; - return Err(crate::Error::SyntaxError); + } + self.pos += run; + } + b'\\' => { + // Line-ending backslash (multi-line only): trim all + // whitespace up to the next non-whitespace character. + if multiline { + let mut i = self.pos + 1; + while matches!(self.peek_at(i), b' ' | b'\t') { + i += 1; } - Err(SetError::OutOfMemory) => { - return Err(crate::Error::Alloc(bun_alloc::AllocError)); + let at_line_end = match self.peek_at(i) { + b'\n' => true, + b'\r' if self.peek_at(i + 1) == b'\n' => true, + _ => false, + }; + if at_line_end { + Self::materialize(self.bump, self.src, start, self.pos, &mut buf); + self.pos = i; + loop { + match self.peek() { + b' ' | b'\t' | b'\n' => self.pos += 1, + b'\r' if self.peek_at(self.pos + 1) == b'\n' => self.pos += 2, + _ => break, + } + } + continue; } - }; - let new_head = self.e(E::Object::default(), loc); - array - .data - .e_array() - .expect("infallible: variant checked") - .push(self.bump, new_head)?; - head = new_head - .data - .e_object() - .expect("infallible: variant checked") - .as_ptr(); - } - _ => { - // SAFETY: `head` points to an E.Object inside `root` (or a - // descendant) allocated from the AST store; valid for this call. - unsafe { - self.parse_assignment(&mut *head, key_allocator)?; - } - } - } - } - } - - pub fn parse_assignment(&mut self, obj: &mut E::Object, bump: &'a Bump) -> crate::Result<()> { - self.lexer.allow_double_bracket = false; - let rope = self.parse_key(bump)?; - let rope_end = self.lexer.start; - - let is_array = self.lexer.token == T::t_empty_array; - if is_array { - self.lexer.next()?; - } - - self.lexer.expect_assignment()?; - if !is_array { - let value = self.parse_value()?; - match obj.set_rope(rope, self.bump, value) { - Ok(()) => {} - Err(SetError::Clobber) => { - let loc = rope.head.loc; - debug_assert!(loc.start > 0); - let start: u32 = u32::try_from(loc.start).expect("int cast"); - // ASCII whitespace: ' ', '\t', '\n', '\r', 0x0B, 0x0C. - // Reshaped for borrowck — `self.source()` returns - // `&'a Source` (independent of `&self`), so bind it before - // the `&mut self.lexer` borrow below. - let src: &'a bun_ast::Source = self.source(); - let key_name = bun_core::strings::trim_right( - &src.contents[start as usize..rope_end], - b" \t\n\r\x0B\x0C", - ); - self.lexer.add_error( - start as usize, - format_args!("Cannot redefine key '{}'", bstr::BStr::new(key_name)), + } + let b = Self::materialize(self.bump, self.src, start, self.pos, &mut buf); + self.scan_escape(b, &mut is_ascii)?; + } + b'\n' => { + if !multiline { + return Err(self.err( + open_pos, + b"Unterminated string; newlines must be escaped in basic strings", + )); + } + if let Some(b) = &mut buf { + b.push(b'\n'); + } + self.pos += 1; + } + b'\r' => { + if self.peek_at(self.pos + 1) != b'\n' { + return Err(self.err(self.pos, BARE_CR)); + } + if !multiline { + // A CRLF in a single-line string is the same mistake + // as a bare LF, so it gets the same diagnostic. + return Err(self.err( + open_pos, + b"Unterminated string; newlines must be escaped in basic strings", + )); + } + // CRLF normalizes to LF in multi-line strings. + Self::materialize(self.bump, self.src, start, self.pos, &mut buf).push(b'\n'); + self.pos += 2; + } + b'\t' => { + if let Some(b) = &mut buf { + b.push(b'\t'); + } + self.pos += 1; + } + c if c < 0x20 || c == 0x7F => { + return Err( + self.err_char(self.pos, "Control character must be escaped in a string:") ); - return Err(crate::Error::SyntaxError); } - Err(SetError::OutOfMemory) => { - return Err(crate::Error::Alloc(bun_alloc::AllocError)); + c => { + if c >= 0x80 { + is_ascii = false; + } + if let Some(b) = &mut buf { + b.push(c); + } + self.pos += 1; } } } - self.lexer.allow_double_bracket = true; - Ok(()) } - pub fn parse_value(&mut self) -> crate::Result { - // Recursion depth is guarded only by `StackCheck`. A previous hard - // depth cap was an artificial limit on a feature; the test's recursion - // depth is set to a value that exhausts the 18 MB stack regardless of - // frame size. - if !self.stack_check.is_safe_to_recurse() { - return Err(crate::Error::StackOverflow); + fn scan_escape(&mut self, buf: &mut ArenaVec<'a, u8>, is_ascii: &mut bool) -> PResult<()> { + debug_assert_eq!(self.peek(), b'\\'); + let escape_pos = self.pos; + self.pos += 1; + let c = self.peek(); + self.pos += 1; + match c { + b'b' => buf.push(0x08), + b't' => buf.push(b'\t'), + b'n' => buf.push(b'\n'), + b'f' => buf.push(0x0C), + b'r' => buf.push(b'\r'), + b'"' => buf.push(b'"'), + b'\\' => buf.push(b'\\'), + // TOML 1.1 + b'e' => buf.push(0x1B), + b'x' => { + let cp = self.read_hex_codepoint("hex escape", 2, escape_pos)?; + self.append_scalar(buf, cp, escape_pos, is_ascii)?; + } + b'u' => { + let cp = self.read_hex_codepoint("Unicode escape", 4, escape_pos)?; + self.append_scalar(buf, cp, escape_pos, is_ascii)?; + } + b'U' => { + let cp = self.read_hex_codepoint("Unicode escape", 8, escape_pos)?; + self.append_scalar(buf, cp, escape_pos, is_ascii)?; + } + 0 if self.at_eof() => { + return Err(self.err(escape_pos, b"Unterminated escape sequence")); + } + _ => { + self.pos -= 1; + return Err(self.err_char(self.pos, "Invalid escape sequence:")); + } } - self.parse_value_inner() + Ok(()) } - fn parse_value_inner(&mut self) -> crate::Result { - let loc = self.lexer.loc(); + fn read_hex_codepoint( + &mut self, + what: &'static str, + digits: usize, + escape_pos: usize, + ) -> PResult { + let mut value: u32 = 0; + for _ in 0..digits { + let Some(d) = bun_core::fmt::hex_digit_value_u32(u32::from(self.peek())) else { + return Err(self.err_fmt( + escape_pos, + format_args!( + "A {} must be followed by exactly {} hex digits", + what, digits + ), + )); + }; + value = value * 16 + u32::from(d); + self.pos += 1; + } + Ok(value) + } - self.lexer.allow_double_bracket = true; + fn append_scalar( + &mut self, + buf: &mut ArenaVec<'a, u8>, + cp: u32, + escape_pos: usize, + is_ascii: &mut bool, + ) -> PResult<()> { + let Some(ch) = char::from_u32(cp) else { + return Err(self.err( + escape_pos, + b"Escaped code point must be a Unicode scalar value", + )); + }; + if cp >= 0x80 { + *is_ascii = false; + } + let mut utf8 = [0u8; 4]; + for &b in ch.encode_utf8(&mut utf8).as_bytes() { + buf.push(b); + } + Ok(()) + } - match self.lexer.token { - T::t_false => { - self.lexer.next()?; + /// Returns (decoded bytes, is_ascii). Literal strings have no escapes, so + /// the content borrows the source unless CRLF normalization forces a copy. + fn scan_literal_string(&mut self, multiline: bool) -> PResult<(&'a [u8], bool)> { + let open_pos = self.pos; + self.pos += if multiline { 3 } else { 1 }; - Ok(self.e(E::Boolean { value: false }, loc)) - } - T::t_true => { - self.lexer.next()?; - Ok(self.e(E::Boolean { value: true }, loc)) - } - T::t_string_literal => { - let result = self.lexer.to_string(loc); - self.lexer.next()?; - Ok(result) + if multiline { + // A newline immediately after the opening delimiter is trimmed. + match self.peek() { + b'\n' => self.pos += 1, + b'\r' if self.peek_at(self.pos + 1) == b'\n' => self.pos += 2, + _ => {} } - T::t_identifier => { - let str = E::String::init(self.lexer.identifier); + } - self.lexer.next()?; - Ok(self.e(str, loc)) + let start = self.pos; + let mut buf: Option> = None; + let mut is_ascii = true; + loop { + if self.at_eof() { + return Err(self.err(open_pos, b"Unterminated string")); } - T::t_numeric_literal => { - let value = self.lexer.number; - self.lexer.next()?; - Ok(self.e(E::Number::new(value), loc)) + let c = self.peek(); + match c { + b'\'' => { + if !multiline { + let text = match buf { + Some(b) => b.into_bump_slice(), + None => &self.src[start..self.pos], + }; + self.pos += 1; + return Ok((text, is_ascii)); + } + let (run, closes) = self.quote_run_close(b'\'')?; + if closes { + let extra = run - 3; + let text = match buf.take() { + Some(mut b) => { + for _ in 0..extra { + b.push(b'\''); + } + b.into_bump_slice() + } + None => &self.src[start..self.pos + extra], + }; + self.pos += run; + return Ok((text, is_ascii)); + } + if let Some(b) = &mut buf { + for _ in 0..run { + b.push(b'\''); + } + } + self.pos += run; + } + b'\n' => { + if !multiline { + return Err(self.err( + open_pos, + b"Unterminated string; literal strings cannot contain newlines", + )); + } + if let Some(b) = &mut buf { + b.push(b'\n'); + } + self.pos += 1; + } + b'\r' => { + if self.peek_at(self.pos + 1) != b'\n' { + return Err(self.err(self.pos, BARE_CR)); + } + if !multiline { + // A CRLF in a single-line string is the same mistake + // as a bare LF, so it gets the same diagnostic. + return Err(self.err( + open_pos, + b"Unterminated string; literal strings cannot contain newlines", + )); + } + // CRLF normalizes to LF: switch to a copy if borrowing. + Self::materialize(self.bump, self.src, start, self.pos, &mut buf).push(b'\n'); + self.pos += 2; + } + b'\t' => { + if let Some(b) = &mut buf { + b.push(b'\t'); + } + self.pos += 1; + } + c if c < 0x20 || c == 0x7F => { + return Err(self.err_char( + self.pos, + "Control character is not allowed in a literal string:", + )); + } + c => { + if c >= 0x80 { + is_ascii = false; + } + if let Some(b) = &mut buf { + b.push(c); + } + self.pos += 1; + } } - T::t_minus => { - self.lexer.next()?; - let value = self.lexer.number; + } + } +} + +// ── parser ────────────────────────────────────────────────────────────────── + +/// Consumes tokens from the scanner and builds the `Expr` tree. Has no +/// access to source bytes; every decision is made on a typed token. +struct Parser<'a, 'log> { + scanner: Scanner<'a, 'log>, + bump: &'a Bump, + stack_check: StackCheck, + /// Keyed by `E::Object::as_ptr()` / `E::Array::as_ptr()` addresses. + meta: HashMap, + /// Current definition block: bumped per table header and per inline table. + block: u32, +} + +impl<'a, 'log> Parser<'a, 'log> { + /// Every table/array reachable during parsing was created by this parser + /// and registered in `meta` at construction. + fn meta_of(&self, ptr: usize) -> Meta { + *self + .meta + .get(&ptr) + .expect("table/array was registered at creation") + } + + // ── document structure ───────────────────────────────────────────────── + + fn parse_root(&mut self) -> PResult { + let start = self.scanner.init_document()?; + + let root = Expr::init(E::Object::default(), loc_of(start)); + let root_ptr = root + .data + .e_object() + .expect("infallible: just constructed") + .as_ptr(); - self.lexer.expect(T::t_numeric_literal)?; - Ok(self.e(E::Number::new(-value), loc)) + let mut current: *mut E::Object = root_ptr; + loop { + match self.scanner.scan_line_start()? { + LineStart::Eof => return Ok(root), + LineStart::TableOpen { aot, pos } => { + current = self.parse_table_header(root_ptr, aot, pos)?; + self.scanner.scan_line_end(b"a table header")?; + } + LineStart::Key(first) => { + self.parse_keyval(current, first)?; + self.scanner.scan_line_end(b"a key/value pair")?; + } } - T::t_plus => { - self.lexer.next()?; - let value = self.lexer.number; + } + } - self.lexer.expect(T::t_numeric_literal)?; - Ok(self.e(E::Number::new(value), loc)) + /// The rest of `[a.b]` / `[[a.b]]` after the opening bracket(s). + /// Returns the table that becomes current. + fn parse_table_header( + &mut self, + root: *mut E::Object, + aot: bool, + header_pos: usize, + ) -> PResult<*mut E::Object> { + let mut path: ArenaVec<'a, KeySeg<'a>> = ArenaVec::with_capacity_in(0, self.bump); + path.push(self.scanner.scan_key_after_sep()?); + loop { + match self.scanner.scan_header_sep(aot)? { + HeaderSep::Dot => path.push(self.scanner.scan_key_after_sep()?), + HeaderSep::Close => break, } - T::t_open_brace => { - self.lexer.next()?; - let mut is_single_line = !self.lexer.has_newline_before; - let key_allocator = self.bump; - let expr = self.e(E::Object::default(), loc); - let obj: *mut E::Object = expr - .data - .e_object() - .expect("infallible: variant checked") - .as_ptr(); - // SAFETY: `obj` aliases into `expr.data`; the raw pointer - // sidesteps overlapping &mut on `expr`. + } + + self.block += 1; + self.navigate_header(root, &path, aot, header_pos) + } - while self.lexer.token != T::t_close_brace { - // SAFETY: `obj` points into the AST store and is live here. - if unsafe { (*obj).properties.slice().len() } > 0 { - if self.lexer.has_newline_before { - is_single_line = false; + fn navigate_header( + &mut self, + root: *mut E::Object, + path: &[KeySeg<'a>], + is_aot: bool, + header_pos: usize, + ) -> PResult<*mut E::Object> { + let mut cur: *mut E::Object = root; + for (i, seg) in path.iter().enumerate() { + let last = i + 1 == path.len(); + // SAFETY: `cur` always points at an E::Object inside the AST store, + // created earlier in this parse; the store lives in `self.bump`. + let cur_obj: &mut E::Object = unsafe { &mut *cur }; + let existing = cur_obj.as_property(seg.text).map(|q| q.expr); + match existing { + None => { + if last && is_aot { + let array = self.new_array(seg.pos, Kind::AotArray); + let elem = self.append_aot_elem(array.1, seg.pos)?; + self.insert_key(cur, *seg, array.0)?; + cur = elem; + } else { + let kind = if last { + Kind::Header + } else { + Kind::HeaderImplicit + }; + let (expr, ptr) = self.new_table(seg.pos, kind); + self.insert_key(cur, *seg, expr)?; + cur = ptr; + } + } + Some(found) => { + if let Some(obj) = found.data.e_object() { + let ptr = obj.as_ptr(); + let meta = self.meta_of(ptr as usize); + if last { + if is_aot { + return Err(self.scanner.err_keyed( + header_pos, + "Cannot redefine table", + seg.text, + " as an array of tables", + )); + } + match meta.kind { + Kind::HeaderImplicit => { + self.meta.insert( + ptr as usize, + Meta { + kind: Kind::Header, + block: self.block, + }, + ); + cur = ptr; + } + Kind::Inline => { + return Err(self.scanner.err_keyed( + header_pos, + "Cannot redefine inline table", + seg.text, + "", + )); + } + _ => { + return Err(self.scanner.err_keyed( + header_pos, + "Cannot redefine table", + seg.text, + "", + )); + } + } + } else { + if meta.kind == Kind::Inline { + return Err(self.scanner.err_keyed( + header_pos, + "Cannot extend inline table", + seg.text, + "", + )); + } + cur = ptr; } - if !self.parse_maybe_trailing_comma(T::t_close_brace)? { - break; + } else if let Some(arr) = found.data.e_array() { + let ptr = arr.as_ptr(); + let meta = self.meta_of(ptr as usize); + if meta.kind != Kind::AotArray { + return Err(self.scanner.err_keyed( + header_pos, + "Cannot extend array", + seg.text, + "", + )); } - if self.lexer.has_newline_before { - is_single_line = false; + if last { + if !is_aot { + return Err(self.scanner.err_keyed( + header_pos, + "Cannot redefine array of tables", + seg.text, + " as a table", + )); + } + cur = self.append_aot_elem(ptr, seg.pos)?; + } else { + // Descend into the most recent element. + // SAFETY: AoT arrays only ever contain E::Object + // elements appended by `append_aot_elem`. + let items = unsafe { (*ptr).items.as_slice() }; + let last_elem = items.last().expect("AoT arrays are never empty"); + cur = last_elem + .data + .e_object() + .expect("AoT elements are tables") + .as_ptr(); } + } else { + return Err(self.scanner.err_keyed( + header_pos, + "Cannot redefine key", + seg.text, + if last && is_aot { + " as an array of tables" + } else { + " as a table" + }, + )); } - // SAFETY: see above. - unsafe { - self.parse_assignment(&mut *obj, key_allocator)?; - } - self.lexer.allow_double_bracket = false; - } - - if self.lexer.has_newline_before { - is_single_line = false; - } - let _ = is_single_line; - self.lexer.allow_double_bracket = true; - self.lexer.expect(T::t_close_brace)?; - Ok(expr) - } - T::t_empty_array => { - self.lexer.next()?; - self.lexer.allow_double_bracket = true; - Ok(self.e(E::Array::default(), loc)) - } - T::t_open_bracket => { - self.lexer.next()?; - let mut is_single_line = !self.lexer.has_newline_before; - let array_ = self.e(E::Array::default(), loc); - let array: *mut E::Array = array_ - .data - .e_array() - .expect("infallible: variant checked") - .as_ptr(); - // SAFETY: `array` aliases into `array_.data`; the raw pointer - // sidesteps overlapping &mut on `array_`. - let bump = self.bump; - self.lexer.allow_double_bracket = false; - - while self.lexer.token != T::t_close_bracket { - // SAFETY: `array` points into the AST store and is live here. - if unsafe { (*array).items.slice().len() } > 0 { - if self.lexer.has_newline_before { - is_single_line = false; - } + } + } + } + Ok(cur) + } - if !self.parse_maybe_trailing_comma(T::t_close_bracket)? { - break; - } + /// The rest of `key = value` (including dotted keys) after the first key + /// segment, inserted into `table`. + fn parse_keyval(&mut self, table: *mut E::Object, first: KeySeg<'a>) -> PResult<()> { + let mut path: ArenaVec<'a, KeySeg<'a>> = ArenaVec::with_capacity_in(0, self.bump); + path.push(first); + loop { + match self.scanner.scan_keyval_sep()? { + KeyvalSep::Dot => path.push(self.scanner.scan_key_after_sep()?), + KeyvalSep::Equals => break, + } + } + let token = self.scanner.scan_value_required()?; + let value = self.parse_value(token)?; + self.assign_path(table, &path, value) + } - if self.lexer.has_newline_before { - is_single_line = false; - } + /// Walks the dotted path from `table`, creating dotted tables as needed, + /// and inserts `value` at the final segment. + fn assign_path( + &mut self, + table: *mut E::Object, + path: &[KeySeg<'a>], + value: Expr, + ) -> PResult<()> { + let mut cur = table; + for seg in &path[..path.len() - 1] { + // SAFETY: `cur` points at a live E::Object in the AST store. + let cur_obj: &mut E::Object = unsafe { &mut *cur }; + match cur_obj.as_property(seg.text).map(|q| q.expr) { + None => { + let (expr, ptr) = self.new_table(seg.pos, Kind::Dotted); + self.insert_key(cur, *seg, expr)?; + cur = ptr; + } + Some(found) => { + let Some(obj) = found.data.e_object() else { + return Err(self.scanner.err_keyed( + seg.pos, + "Cannot redefine key", + seg.text, + "", + )); + }; + let ptr = obj.as_ptr(); + let meta = self.meta_of(ptr as usize); + let extendable = meta.kind == Kind::Dotted && meta.block == self.block; + if !extendable { + return Err(self.scanner.err_keyed( + seg.pos, + "Cannot extend table", + seg.text, + " with a dotted key", + )); } + cur = ptr; + } + } + } + let last = path[path.len() - 1]; + self.insert_key(cur, last, value) + } - let value = self.parse_value()?; - // SAFETY: see above. - unsafe { - (*array).push(bump, value).expect("unreachable"); - } + // ── values ───────────────────────────────────────────────────────────── + + fn parse_value(&mut self, token: ValueToken<'a>) -> PResult { + if !self.stack_check.is_safe_to_recurse() { + return Err(PErr::StackOverflow); + } + let loc = loc_of(token.pos); + match token.data { + ValueData::String { text, is_ascii } => Ok(self.string_expr(text, is_ascii, loc)), + ValueData::Number(n) => Ok(Expr::init(E::Number::new(n), loc)), + ValueData::DateTime(text) => Ok(Expr::init(E::String::init(text), loc)), + ValueData::Boolean(b) => Ok(Expr::init(E::Boolean { value: b }, loc)), + ValueData::ArrayOpen => self.parse_array(token.pos), + ValueData::InlineOpen => self.parse_inline_table(token.pos), + } + } + + fn string_expr(&self, text: &'a [u8], is_ascii: bool, loc: Loc) -> Expr { + if is_ascii { + Expr::init(E::String::init(text), loc) + } else { + Expr::init(E::String::init_re_encode_utf8(text, self.bump), loc) + } + } + + /// The rest of `[ ... ]` after the opening bracket. + fn parse_array(&mut self, pos: usize) -> PResult { + let (array, ptr) = self.new_array(pos, Kind::StaticArray); + + loop { + match self.scanner.scan_array_item()? { + ArrayItem::Close => return Ok(array), + ArrayItem::Eof { pos } => { + return Err(self.scanner.err(pos, b"Unterminated array; expected ']'")); } + ArrayItem::Value(token) => { + let value = self.parse_value(token)?; + // SAFETY: `ptr` points at the E::Array constructed above. + unsafe { (*ptr).push(self.bump, value)? }; + } + } + match self + .scanner + .scan_list_sep(b']', "Expected ',' or ']' in an array but found")? + { + ListSep::Comma => {} + ListSep::Close => return Ok(array), + } + } + } + + /// The rest of `{ ... }` after the opening brace. + fn parse_inline_table(&mut self, pos: usize) -> PResult { + // An inline table is its own definition block so dotted keys inside it + // cannot extend outer tables and vice versa. + let outer_block = self.block; + self.block += 1; - if self.lexer.has_newline_before { - is_single_line = false; + let (table, ptr) = self.new_table(pos, Kind::Dotted); + + loop { + match self.scanner.scan_inline_key()? { + InlineKey::Close => break, + InlineKey::Eof { pos } => { + return Err(self + .scanner + .err(pos, b"Unterminated inline table; expected '}'")); + } + InlineKey::Key(first) => { + self.parse_keyval(ptr, first)?; } - let _ = is_single_line; - self.lexer.allow_double_bracket = true; - self.lexer.expect(T::t_close_bracket)?; - Ok(array_) } - _ => { - self.lexer.unexpected()?; - Err(crate::Error::SyntaxError) + match self + .scanner + .scan_list_sep(b'}', "Expected ',' or '}' in an inline table but found")? + { + ListSep::Comma => { + // A trailing comma before '}' is allowed; a second comma + // is not, which `scan_inline_key` will reject. + } + ListSep::Close => break, } } + + // Inline tables are closed: nothing may extend them later. + self.meta.insert( + ptr as usize, + Meta { + kind: Kind::Inline, + block: self.block, + }, + ); + self.block = outer_block; + Ok(table) + } + + // ── table bookkeeping ────────────────────────────────────────────────── + + fn new_table(&mut self, pos: usize, kind: Kind) -> (Expr, *mut E::Object) { + let expr = Expr::init(E::Object::default(), loc_of(pos)); + let ptr = expr + .data + .e_object() + .expect("infallible: just constructed") + .as_ptr(); + self.meta.insert( + ptr as usize, + Meta { + kind, + block: self.block, + }, + ); + (expr, ptr) + } + + fn new_array(&mut self, pos: usize, kind: Kind) -> (Expr, *mut E::Array) { + let expr = Expr::init(E::Array::default(), loc_of(pos)); + let ptr = expr + .data + .e_array() + .expect("infallible: just constructed") + .as_ptr(); + self.meta.insert( + ptr as usize, + Meta { + kind, + block: self.block, + }, + ); + (expr, ptr) + } + + fn append_aot_elem(&mut self, array: *mut E::Array, pos: usize) -> PResult<*mut E::Object> { + let (elem, ptr) = self.new_table(pos, Kind::ArrayElem); + // SAFETY: `array` points at a live E::Array in the AST store. + unsafe { (*array).push(self.bump, elem)? }; + Ok(ptr) + } + + fn insert_key(&mut self, obj: *mut E::Object, seg: KeySeg<'a>, value: Expr) -> PResult<()> { + // SAFETY: `obj` points at a live E::Object in the AST store. + let obj: &mut E::Object = unsafe { &mut *obj }; + // The duplicate check must use the UTF-8 key bytes: `as_property` + // compares correctly against both 8-bit and UTF-16 stored keys. + if obj.as_property(seg.text).is_some() { + return Err(self + .scanner + .err_keyed(seg.pos, "Cannot redefine key", seg.text, "")); + } + let key_loc = loc_of(seg.pos); + let key_expr = if seg.text.is_ascii() { + Expr::init(E::String::init(seg.text), key_loc) + } else { + Expr::init(E::String::init_re_encode_utf8(seg.text, self.bump), key_loc) + }; + obj.append_property(key_expr, value); + Ok(()) } } diff --git a/src/parsers/toml/lexer.rs b/src/parsers/toml/lexer.rs deleted file mode 100644 index 2eef5efa25bb..000000000000 --- a/src/parsers/toml/lexer.rs +++ /dev/null @@ -1,1289 +0,0 @@ -use bun_alloc::Arena; // bumpalo::Bump re-export -use bun_alloc::ArenaVecExt as _; -use bun_ast as js_ast; -use bun_ast::LexerLog; -use bun_core::fmt::hex_digit_value_u32; -use bun_core::strings; -use bun_core::strings::CodePoint; - -#[repr(u8)] -#[derive(Copy, Clone, PartialEq, Eq, Debug, strum::IntoStaticStr)] -#[allow(non_camel_case_types)] -pub enum T { - t_end_of_file, - - t_open_paren, - t_close_paren, - t_open_bracket, - t_open_bracket_double, - - t_close_bracket, - t_close_bracket_double, - - t_open_brace, - t_close_brace, - - t_numeric_literal, - - t_comma, - - t_string_literal, - t_dot, - - t_equal, - - t_true, - t_false, - - t_colon, - - t_identifier, - - t_plus, - t_minus, - - t_empty_array, -} - -bun_core::comptime_string_map! { - static KEYWORDS: T = { - b"true" => T::t_true, - b"false" => T::t_false, - }; -} - -pub struct Lexer<'a> { - // Borrowed (`&'a Source`) rather than owned so - // `identifier`/`string_literal_slice` can borrow `&'a [u8]` from - // `source.contents` without a self-referential struct. - // `bun_ast::Source.contents` is `Cow<'static,[u8]>` so an owned copy - // would tie those slices to `&self` instead of `'a`. - pub source: &'a bun_ast::Source, - pub log: &'a mut bun_ast::Log, - pub start: usize, - pub end: usize, - pub current: usize, - - pub bump: &'a Arena, - - pub code_point: CodePoint, - pub identifier: &'a [u8], - pub number: f64, - pub prev_error_loc: bun_ast::Loc, - pub string_literal_slice: &'a [u8], - pub string_literal_is_ascii: bool, - pub line_number: u32, - pub token: T, - pub allow_double_bracket: bool, - - pub has_newline_before: bool, - - pub should_redact_logs: bool, -} - -#[derive(thiserror::Error, Debug, Copy, Clone, PartialEq, Eq, strum::IntoStaticStr)] -pub enum Error { - #[error("UTF8Fail")] - UTF8Fail, - #[error("OutOfMemory")] - OutOfMemory, - #[error("SyntaxError")] - SyntaxError, - #[error("UnexpectedSyntax")] - UnexpectedSyntax, - #[error("JSONStringsMustUseDoubleQuotes")] - JSONStringsMustUseDoubleQuotes, - #[error("ParserError")] - ParserError, -} - -bun_core::oom_from_alloc!(Error); - -impl<'a> LexerLog<'a> for Lexer<'a> { - type Err = Error; - #[inline] - fn log_mut(&mut self) -> &mut bun_ast::Log { - &mut *self.log - } - #[inline] - fn source(&self) -> &'a bun_ast::Source { - self.source - } - #[inline] - fn prev_error_loc_mut(&mut self) -> &mut bun_ast::Loc { - &mut self.prev_error_loc - } - #[inline] - fn start(&self) -> usize { - self.start - } - #[inline] - fn should_redact(&self) -> bool { - self.should_redact_logs - } - #[inline] - fn syntax_err() -> Error { - Error::SyntaxError - } -} - -impl<'a> Lexer<'a> { - #[inline] - pub fn loc(&self) -> bun_ast::Loc { - bun_ast::usize2loc(self.start) - } - - #[inline(always)] - fn next_codepoint(&mut self) -> CodePoint { - strings::lexer_step::next_codepoint(&self.source.contents, &mut self.current, &mut self.end) - } - - #[inline] - fn step(&mut self) { - self.code_point = self.next_codepoint(); - - self.line_number += (self.code_point == '\n' as CodePoint) as u32; - } - - fn parse_numeric_literal_or_dot(&mut self) -> Result<(), Error> { - // Number or dot; - let first = self.code_point; - self.step(); - - // Dot without a digit after it; - if first == '.' as CodePoint - && (self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint) - { - // "." - self.token = T::t_dot; - return Ok(()); - } - - let mut underscore_count: usize = 0; - let mut last_underscore_end: usize = 0; - let mut has_dot_or_exponent = first == '.' as CodePoint; - let mut base: f32 = 0.0; - - let mut is_legacy_octal_literal = false; - - // Assume this is a number, but potentially change to a date/time later; - self.token = T::t_numeric_literal; - - // Check for binary, octal, or hexadecimal literal; - if first == '0' as CodePoint { - match self.code_point { - c if c == 'b' as CodePoint || c == 'B' as CodePoint => { - base = 2.0; - } - - c if c == 'o' as CodePoint || c == 'O' as CodePoint => { - base = 8.0; - } - - c if c == 'x' as CodePoint || c == 'X' as CodePoint => { - base = 16.0; - } - - c if (('0' as CodePoint..='7' as CodePoint).contains(&c)) - || c == '_' as CodePoint => - { - base = 8.0; - is_legacy_octal_literal = true; - } - _ => {} - } - } - - if base != 0.0 { - // Integer literal; - let mut is_first = true; - let mut is_invalid_legacy_octal_literal = false; - self.number = 0.0; - if !is_legacy_octal_literal { - self.step(); - } - - 'integer_literal: loop { - match self.code_point { - c if c == '_' as CodePoint => { - // Cannot have multiple underscores in a row; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.syntax_error()?; - } - - // The first digit must exist; - if is_first || is_legacy_octal_literal { - self.syntax_error()?; - } - - last_underscore_end = self.end; - underscore_count += 1; - } - - c if c == '0' as CodePoint || c == '1' as CodePoint => { - self.number = - self.number * base as f64 + float64(self.code_point - '0' as CodePoint); - } - - c if ('2' as CodePoint..='7' as CodePoint).contains(&c) => { - if base == 2.0 { - self.syntax_error()?; - } - self.number = - self.number * base as f64 + float64(self.code_point - '0' as CodePoint); - } - c if c == '8' as CodePoint || c == '9' as CodePoint => { - if is_legacy_octal_literal { - is_invalid_legacy_octal_literal = true; - } else if base < 10.0 { - self.syntax_error()?; - } - self.number = - self.number * base as f64 + float64(self.code_point - '0' as CodePoint); - } - c if ('A' as CodePoint..='F' as CodePoint).contains(&c) => { - if base != 16.0 { - self.syntax_error()?; - } - self.number = self.number * base as f64 - + float64(self.code_point + 10 - 'A' as CodePoint); - } - - c if ('a' as CodePoint..='f' as CodePoint).contains(&c) => { - if base != 16.0 { - self.syntax_error()?; - } - self.number = self.number * base as f64 - + float64(self.code_point + 10 - 'a' as CodePoint); - } - _ => { - // The first digit must exist; - if is_first { - self.syntax_error()?; - } - - break 'integer_literal; - } - } - - self.step(); - is_first = false; - } - - let is_big_integer_literal = - self.code_point == 'n' as CodePoint && !has_dot_or_exponent; - - // Slow path: do we need to re-scan the input as text? - if is_big_integer_literal || is_invalid_legacy_octal_literal { - let text = self.raw(); - - // Can't use a leading zero for bigint literals; - if is_big_integer_literal && is_legacy_octal_literal { - self.syntax_error()?; - } - - // Filter out underscores; - if underscore_count > 0 { - let bytes = self - .bump - .alloc_slice_fill_default::(text.len() - underscore_count); - let mut i: usize = 0; - for &char_ in text { - if char_ != b'_' { - bytes[i] = char_; - i += 1; - } - } - // `bytes` is intentionally discarded here. - } - - // Store bigints as text to avoid precision loss; - if is_big_integer_literal { - self.identifier = text; - } else if is_invalid_legacy_octal_literal { - match bun_core::wtf::parse_double(text) { - Ok(num) => { - self.number = num; - } - Err(_) => { - self.add_syntax_error( - self.start, - format_args!("Invalid number {}", bstr::BStr::new(text)), - )?; - } - } - } - } - } else { - // Floating-point literal; - let is_invalid_legacy_octal_literal = first == '0' as CodePoint - && (self.code_point == '8' as CodePoint || self.code_point == '9' as CodePoint); - - // Initial digits; - loop { - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - match self.code_point { - // '-' => { - // if (lexer.raw().len == 5) { - // // Is this possibly a datetime literal that begins with a 4 digit year? - // lexer.step(); - // while (!lexer.has_newline_before) { - // switch (lexer.code_point) { - // ',' => { - // lexer.string_literal_slice = lexer.raw(); - // lexer.token = T.t_string_literal; - // break; - // }, - // } - // } - // } - // }, - c if c == '_' as CodePoint => {} - _ => break, - } - if self.code_point != '_' as CodePoint { - break; - } - - // Cannot have multiple underscores in a row; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.syntax_error()?; - } - - // The specification forbids underscores in this case; - if is_invalid_legacy_octal_literal { - self.syntax_error()?; - } - - last_underscore_end = self.end; - underscore_count += 1; - } - self.step(); - } - - // Fractional digits; - if first != '.' as CodePoint && self.code_point == '.' as CodePoint { - // An underscore must not come last; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.end -= 1; - self.syntax_error()?; - } - - has_dot_or_exponent = true; - self.step(); - if self.code_point == '_' as CodePoint { - self.syntax_error()?; - } - loop { - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - if self.code_point != '_' as CodePoint { - break; - } - - // Cannot have multiple underscores in a row; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.syntax_error()?; - } - - last_underscore_end = self.end; - underscore_count += 1; - } - self.step(); - } - } - - // Exponent; - if self.code_point == 'e' as CodePoint || self.code_point == 'E' as CodePoint { - // An underscore must not come last; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.end -= 1; - self.syntax_error()?; - } - - has_dot_or_exponent = true; - self.step(); - if self.code_point == '+' as CodePoint || self.code_point == '-' as CodePoint { - self.step(); - } - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - self.syntax_error()?; - } - loop { - if self.code_point < '0' as CodePoint || self.code_point > '9' as CodePoint { - if self.code_point != '_' as CodePoint { - break; - } - - // Cannot have multiple underscores in a row; - if last_underscore_end > 0 && self.end == last_underscore_end + 1 { - self.syntax_error()?; - } - - last_underscore_end = self.end; - underscore_count += 1; - } - self.step(); - } - } - - // Take a slice of the text to parse; - let mut text: &[u8] = self.raw(); - - // Filter out underscores; - if underscore_count > 0 { - let mut i: usize = 0; - let bytes = self - .bump - .alloc_slice_fill_default::(text.len() - underscore_count); - for &char_ in text { - if char_ != b'_' { - bytes[i] = char_; - i += 1; - } - } - text = bytes; - } - - if !has_dot_or_exponent && self.end - self.start < 10 { - // Parse a 32-bit integer (very fast path); - let mut number: u32 = 0; - for &c in text { - number = number * 10 + u32::from(c - b'0'); - } - self.number = number as f64; - } else { - // Parse a double-precision floating-point number; - match bun_core::wtf::parse_double(text) { - Ok(num) => { - self.number = num; - } - Err(_) => { - self.add_syntax_error(self.start, format_args!("Invalid number"))?; - } - } - } - } - - Ok(()) - } - - #[inline] - pub fn expect(&mut self, token: T) -> Result<(), Error> { - if self.token != token { - self.expected(token)?; - } - - self.next() - } - - #[inline] - pub fn expect_assignment(&mut self) -> Result<(), Error> { - match self.token { - T::t_equal | T::t_colon => {} - _ => { - self.expected(T::t_equal)?; - } - } - - self.next() - } - - pub fn next(&mut self) -> Result<(), Error> { - self.has_newline_before = self.end == 0; - - loop { - self.start = self.end; - self.token = T::t_end_of_file; - - match self.code_point { - -1 => { - self.token = T::t_end_of_file; - } - - c if c == '\r' as CodePoint - || c == '\n' as CodePoint - || c == 0x2028 - || c == 0x2029 => - { - self.step(); - self.has_newline_before = true; - continue; - } - - c if c == '\t' as CodePoint || c == ' ' as CodePoint => { - self.step(); - continue; - } - - c if c == '[' as CodePoint => { - self.step(); - self.token = T::t_open_bracket; - if self.code_point == '[' as CodePoint && self.allow_double_bracket { - self.step(); - self.token = T::t_open_bracket_double; - return Ok(()); - } - - if self.code_point == ']' as CodePoint { - self.step(); - self.token = T::t_empty_array; - } - } - c if c == ']' as CodePoint => { - self.step(); - self.token = T::t_close_bracket; - - if self.code_point == ']' as CodePoint && self.allow_double_bracket { - self.step(); - self.token = T::t_close_bracket_double; - } - } - c if c == '+' as CodePoint => { - self.step(); - self.token = T::t_plus; - } - c if c == '-' as CodePoint => { - self.step(); - self.token = T::t_minus; - } - - c if c == '{' as CodePoint => { - self.step(); - self.token = T::t_open_brace; - } - c if c == '}' as CodePoint => { - self.step(); - self.token = T::t_close_brace; - } - - c if c == '=' as CodePoint => { - self.step(); - self.token = T::t_equal; - } - c if c == ':' as CodePoint => { - self.step(); - self.token = T::t_colon; - } - c if c == ',' as CodePoint => { - self.step(); - self.token = T::t_comma; - } - c if c == ';' as CodePoint => { - if self.has_newline_before { - self.step(); - - 'single_line_comment: loop { - self.step(); - match self.code_point { - c if c == '\r' as CodePoint - || c == '\n' as CodePoint - || c == 0x2028 - || c == 0x2029 => - { - break 'single_line_comment; - } - -1 => { - break 'single_line_comment; - } - _ => {} - } - } - continue; - } - - self.add_default_error(b"Unexpected semicolon")?; - } - c if c == '#' as CodePoint => { - self.step(); - - 'single_line_comment: loop { - self.step(); - match self.code_point { - c if c == '\r' as CodePoint - || c == '\n' as CodePoint - || c == 0x2028 - || c == 0x2029 => - { - break 'single_line_comment; - } - -1 => { - break 'single_line_comment; - } - _ => {} - } - } - continue; - } - - // unescaped string - c if c == '\'' as CodePoint => { - self.step(); - self.string_literal_is_ascii = true; - let start = self.end; - let mut is_multiline_string_literal = false; - - if self.code_point == '\'' as CodePoint { - self.step(); - // it's a multiline string literal - if self.code_point == '\'' as CodePoint { - self.step(); - is_multiline_string_literal = true; - } else { - // it's an empty string - self.token = T::t_string_literal; - self.string_literal_slice = &self.source.contents[start..start]; - return Ok(()); - } - } - - if is_multiline_string_literal { - loop { - match self.code_point { - -1 => { - self.add_default_error(b"Unterminated string literal")?; - } - c if c == '\'' as CodePoint => { - let end = self.end; - self.step(); - if self.code_point != '\'' as CodePoint { - continue; - } - self.step(); - if self.code_point != '\'' as CodePoint { - continue; - } - self.step(); - self.token = T::t_string_literal; - self.string_literal_slice = - &self.source.contents[start + 2..end]; - return Ok(()); - } - _ => {} - } - self.step(); - } - } else { - loop { - match self.code_point { - c if c == '\r' as CodePoint - || c == '\n' as CodePoint - || c == 0x2028 - || c == 0x2029 => - { - self.add_default_error( - b"Unterminated string literal (single-line)", - )?; - } - -1 => { - self.add_default_error(b"Unterminated string literal")?; - } - c if c == '\'' as CodePoint => { - self.step(); - self.token = T::t_string_literal; - self.string_literal_slice = - &self.source.contents[start..self.end - 1]; - return Ok(()); - } - _ => {} - } - self.step(); - } - } - } - c if c == '"' as CodePoint => { - self.step(); - let mut needs_slow_pass = false; - let start = self.end; - let mut is_multiline_string_literal = false; - self.string_literal_is_ascii = true; - - if self.code_point == '"' as CodePoint { - self.step(); - // it's a multiline basic string - if self.code_point == '"' as CodePoint { - self.step(); - is_multiline_string_literal = true; - } else { - // it's an empty string - self.token = T::t_string_literal; - self.string_literal_slice = &self.source.contents[start..start]; - return Ok(()); - } - } - - // Capture the slice bounds as indices instead of laundering - // a `&'a [u8]` through a raw pointer. On the fast - // path we reslice immediately before `return`; on the slow path we - // reslice after the loop and hand it straight to - // `decode_escape_sequences` without stashing in `self` first. - let slice_lo: usize; - let slice_hi: usize; - if is_multiline_string_literal { - loop { - match self.code_point { - -1 => { - self.add_default_error(b"Unterminated basic string")?; - } - c if c == '\\' as CodePoint => { - self.step(); - needs_slow_pass = true; - if self.code_point == '"' as CodePoint { - self.step(); - continue; - } - } - c if c == '"' as CodePoint => { - let end = self.end; - self.step(); - if self.code_point != '"' as CodePoint { - continue; - } - self.step(); - if self.code_point != '"' as CodePoint { - continue; - } - self.step(); - - self.token = T::t_string_literal; - if needs_slow_pass { - slice_lo = start + 2; - slice_hi = end; - break; - } - self.string_literal_slice = - &self.source.contents[start + 2..end]; - return Ok(()); - } - _ => {} - } - self.step(); - } - } else { - loop { - match self.code_point { - c if c == '\r' as CodePoint - || c == '\n' as CodePoint - || c == 0x2028 - || c == 0x2029 => - { - self.add_default_error( - b"Unterminated basic string (single-line)", - )?; - } - -1 => { - self.add_default_error(b"Unterminated basic string")?; - } - c if c == '\\' as CodePoint => { - self.step(); - needs_slow_pass = true; - if self.code_point == '"' as CodePoint { - self.step(); - continue; - } - } - c if c == '"' as CodePoint => { - self.step(); - - self.token = T::t_string_literal; - if needs_slow_pass { - slice_lo = start; - slice_hi = self.end - 1; - break; - } - self.string_literal_slice = - &self.source.contents[start..self.end - 1]; - return Ok(()); - } - _ => {} - } - self.step(); - } - } - - self.start = start; - if needs_slow_pass { - let text = &self.source.contents[slice_lo..slice_hi]; - let mut array_list = - bun_alloc::ArenaVec::with_capacity_in(text.len(), self.bump); - if is_multiline_string_literal { - self.decode_escape_sequences::(start, text, &mut array_list)?; - } else { - self.decode_escape_sequences::(start, text, &mut array_list)?; - } - self.string_literal_slice = array_list.into_bump_slice(); - self.string_literal_is_ascii = false; - } - - self.token = T::t_string_literal; - } - - c if c == '.' as CodePoint - || ('0' as CodePoint..='9' as CodePoint).contains(&c) => - { - self.parse_numeric_literal_or_dot()?; - } - - c if c == '@' as CodePoint - || ('a' as CodePoint..='z' as CodePoint).contains(&c) - || ('A' as CodePoint..='Z' as CodePoint).contains(&c) - || c == '$' as CodePoint - || c == '_' as CodePoint => - { - self.step(); - while is_identifier_part(self.code_point) { - self.step(); - } - self.identifier = self.raw(); - self.token = KEYWORDS - .get(self.identifier) - .copied() - .unwrap_or(T::t_identifier); - } - - _ => self.unexpected()?, - } - return Ok(()); - } - } - - pub fn decode_escape_sequences( - &mut self, - start: usize, - text: &[u8], - buf: &mut bun_alloc::ArenaVec<'a, u8>, - ) -> Result<(), Error> { - let iterator = strings::CodepointIterator::init(text); - let mut iter = strings::Cursor::default(); - while iterator.next(&mut iter) { - let width = iter.width; - match iter.c { - c if c == '\r' as CodePoint => { - // Convert '\r\n' into '\n'. After `next()` returns for `\r`, - // `iter.i` is the start byte of the `\r` itself — the `\n` - // we're looking for is at `iter.i + 1`. Reading `text[iter.i]` - // would always be `\r`, so the check never fired and a literal - // CRLF in a slow-path multiline basic string decoded to two LFs. - // Match the JS lexer (js_parser/lexer.rs:660-661). - let next_i: usize = iter.i as usize + 1; - if next_i < text.len() && text[next_i] == b'\n' { - iter.i += 1; - } - - // Convert '\r' into '\n' - buf.push(b'\n'); - continue; - } - - c if c == '\\' as CodePoint => { - if !iterator.next(&mut iter) { - return Ok(()); - } - - let c2 = iter.c; - - let width2 = iter.width; - match c2 { - // https://mathiasbynens.be/notes/javascript-escapes#single - c if c == 'b' as CodePoint => { - buf.push(8); - continue; - } - c if c == 'f' as CodePoint => { - // Form feed: U+000C - buf.push(12); - continue; - } - c if c == 'n' as CodePoint => { - buf.push(10); - continue; - } - c if c == 'v' as CodePoint => { - // Vertical tab is invalid JSON - // We're going to allow it. - buf.push(11); - continue; - } - c if c == 't' as CodePoint => { - // Horizontal tab: U+0009 - buf.push(9); - continue; - } - c if c == 'r' as CodePoint => { - buf.push(13); - continue; - } - - // legacy octal literals - c if ('0' as CodePoint..='7' as CodePoint).contains(&c) => { - let octal_start = (iter.i as usize + width2 as usize).saturating_sub(2); - - // 1-3 digit octal - let mut is_bad = false; - let mut value: i64 = (c2 - '0' as CodePoint) as i64; - let mut restore = iter; - - if !iterator.next(&mut iter) { - if value == 0 { - buf.push(0); - return Ok(()); - } - - self.syntax_error()?; - return Ok(()); - } - - let c3: CodePoint = iter.c; - - match c3 { - c if ('0' as CodePoint..='7' as CodePoint).contains(&c) => { - value = value * 8 + (c3 - '0' as CodePoint) as i64; - restore = iter; - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - - let c4 = iter.c; - match c4 { - c if ('0' as CodePoint..='7' as CodePoint).contains(&c) => { - let temp = value * 8 + (c4 - '0' as CodePoint) as i64; - if temp < 256 { - value = temp; - } else { - iter = restore; - } - } - c if c == '8' as CodePoint || c == '9' as CodePoint => { - is_bad = true; - } - _ => { - iter = restore; - } - } - } - c if c == '8' as CodePoint || c == '9' as CodePoint => { - is_bad = true; - } - _ => { - iter = restore; - } - } - - iter.c = i32::try_from(value).expect("int cast"); - if is_bad { - self.add_range_error( - bun_ast::Range { - loc: bun_ast::Loc { - start: i32::try_from(octal_start).expect("int cast"), - }, - len: i32::try_from(iter.i as usize - octal_start) - .expect("int cast"), - }, - format_args!("Invalid legacy octal literal"), - ) - .expect("unreachable"); - } - } - c if c == '8' as CodePoint || c == '9' as CodePoint => { - iter.c = c2; - } - // 2-digit hexadecimal - c if c == 'x' as CodePoint => { - if ALLOW_MULTILINE { - self.end = - (start + iter.i as usize).saturating_sub(width2 as usize); - self.syntax_error()?; - } - - let mut value: CodePoint = 0; - let mut c3: CodePoint; - let mut width3: u8; - - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - c3 = iter.c; - width3 = iter.width; - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as CodePoint, - None => { - self.end = - (start + iter.i as usize).saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - c3 = iter.c; - width3 = iter.width; - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as CodePoint, - None => { - self.end = - (start + iter.i as usize).saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - iter.c = value; - } - c if c == 'u' as CodePoint => { - // We're going to make this an i64 so we don't risk integer overflows - // when people do weird things - let mut value: i64 = 0; - - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - let mut c3 = iter.c; - let mut width3 = iter.width; - - // variable-length - if c3 == '{' as CodePoint { - let hex_start = (iter.i as usize) - .saturating_sub(width as usize) - .saturating_sub(width2 as usize) - .saturating_sub(width3 as usize); - let mut is_first = true; - let mut is_out_of_range = false; - 'variable_length: loop { - if !iterator.next(&mut iter) { - // Ran out of literal before the closing `}`. - return self.syntax_error(); - } - c3 = iter.c; - - if c3 == '}' as CodePoint { - if is_first { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - break 'variable_length; - } - match hex_digit_value_u32(c3 as u32) { - // Saturate: `is_out_of_range` is sticky, so any - // digit count still reports the range error. - Some(d) => value = value.saturating_mul(16) | d as i64, - None => { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - // '\U0010FFFF - // copied from golang utf8.MaxRune - if value > 1114111 { - is_out_of_range = true; - } - is_first = false; - } - - if is_out_of_range { - self.add_range_error( - bun_ast::Range { - loc: bun_ast::Loc { - start: i32::try_from(start + hex_start) - .expect("int cast"), - }, - len: i32::try_from( - (iter.i as usize).saturating_sub(hex_start), - ) - .unwrap(), - }, - format_args!("Unicode escape sequence is out of range"), - )?; - return Ok(()); - } - - // fixed-length - } else { - // Fixed-length - let mut j: usize = 0; - while j < 4 { - match hex_digit_value_u32(c3 as u32) { - Some(d) => value = (value * 16) | d as i64, - None => { - self.end = (start + iter.i as usize) - .saturating_sub(width3 as usize); - return self.syntax_error(); - } - } - - if j < 3 { - if !iterator.next(&mut iter) { - return self.syntax_error(); - } - c3 = iter.c; - - width3 = iter.width; - } - j += 1; - } - } - - iter.c = value as CodePoint; // @truncate - } - c if c == '\r' as CodePoint => { - if !ALLOW_MULTILINE { - self.end = - (start + iter.i as usize).saturating_sub(width2 as usize); - self.add_default_error(b"Unexpected end of line")?; - } - - // Ignore line continuations. A line continuation is not an escaped newline. - // Match the JS lexer (js_parser/lexer.rs:660-661, 937-939): guard on - // the index we actually read (`iter.i + 1`), not `iter.i`. Without - // this, a multiline basic string ending in `\` right before `"""` - // reads `text[len]` and panics even in release (slice bounds checks - // always run). - let next_i: usize = iter.i as usize + 1; - if next_i < text.len() && text[next_i] == b'\n' { - // Make sure Windows CRLF counts as a single newline - iter.i += 1; - } - continue; - } - c if c == '\n' as CodePoint || c == 0x2028 || c == 0x2029 => { - // Ignore line continuations. A line continuation is not an escaped newline. - if !ALLOW_MULTILINE { - self.end = - (start + iter.i as usize).saturating_sub(width2 as usize); - self.add_default_error(b"Unexpected end of line")?; - } - continue; - } - _ => { - iter.c = c2; - } - } - } - _ => {} - } - - match iter.c { - -1 => return self.add_default_error(b"Unexpected end of file"), - 0..=127 => { - buf.push(u8::try_from(iter.c).expect("int cast")); - } - _ => { - let mut part: [u8; 4] = [0; 4]; - let len = strings::encode_wtf8_rune(&mut part, iter.c as u32); - buf.extend_from_slice(&part[0..len]); - } - } - } - Ok(()) - } - - pub fn expected(&mut self, token: T) -> Result<(), Error> { - self.expected_string(<&'static str>::from(token).as_bytes()) - } - - pub fn unexpected(&mut self) -> Result<(), Error> { - let found: &[u8] = 'finder: { - self.start = self.start.min(self.end); - - if self.start == self.source.contents.len() { - break 'finder b"end of file"; - } else { - break 'finder self.raw(); - } - }; - - // Compute the range before borrowing `found` from source. - let range = self.range(); - self.add_range_error(range, format_args!("Unexpected {}", bstr::BStr::new(found))) - } - - pub fn expected_string(&mut self, text: &[u8]) -> Result<(), Error> { - let found: &[u8] = 'finder: { - if self.source.contents.len() != self.start { - break 'finder self.raw(); - } else { - break 'finder b"end of file"; - } - }; - - let range = self.range(); - self.add_range_error( - range, - format_args!( - "Expected {} but found {}", - bstr::BStr::new(text), - bstr::BStr::new(found) - ), - ) - } - - pub fn range(&self) -> bun_ast::Range { - bun_ast::Range { - loc: bun_ast::usize2loc(self.start), - len: (self.end - self.start) as i32, - } - } - - pub fn init( - log: &'a mut bun_ast::Log, - source: &'a bun_ast::Source, - bump: &'a Arena, - redact_logs: bool, - ) -> Result, Error> { - let mut lex = Lexer { - source, - log, - start: 0, - end: 0, - current: 0, - bump, - code_point: -1, - identifier: b"", - number: 0.0, - prev_error_loc: bun_ast::Loc::EMPTY, - string_literal_slice: b"", - string_literal_is_ascii: true, - line_number: 0, - token: T::t_end_of_file, - allow_double_bracket: true, - has_newline_before: false, - should_redact_logs: redact_logs, - }; - lex.step(); - lex.next()?; - - Ok(lex) - } - - #[inline] - pub fn to_string(&self, loc_: bun_ast::Loc) -> js_ast::Expr { - if self.string_literal_is_ascii { - return js_ast::Expr::init(js_ast::E::String::init(self.string_literal_slice), loc_); - } - - js_ast::Expr::init(js_ast::E::String::init(self.string_literal_slice), loc_) - } - - pub fn raw(&self) -> &'a [u8] { - &self.source.contents[self.start..self.end] - } -} - -pub(crate) fn is_identifier_part(code_point: CodePoint) -> bool { - matches!(code_point as u32 as u8 as char, - '0'..='9' - | 'a'..='z' - | 'A'..='Z' - | '$' - | '_' - | '-' - | ':' - ) && (0..=127).contains(&code_point) - // The `(0..=127)` bound is required for the byte cast above to be sound. -} - -#[inline] -fn float64(num: CodePoint) -> f64 { - num as f64 -} diff --git a/src/runtime/api.rs b/src/runtime/api.rs index d4d63fcadb6d..dc54f1fc85dd 100644 --- a/src/runtime/api.rs +++ b/src/runtime/api.rs @@ -277,3 +277,69 @@ pub(crate) fn with_text_format_source( f(&arena, &mut log, &source) } + +// ─── shared Expr → JS conversion for the text-format parsers ───────────────── + +fn estring_to_js( + str: &bun_ast::E::EString, + global: &bun_jsc::JSGlobalObject, +) -> bun_jsc::JsResult { + use bun_jsc::StringJsc as _; + // NOTE: the text-format parsers never build ropes, so the simple + // slice → JS path is sufficient. + if str.is_utf16 { + let zig = bun_core::ZigString::init_utf16(str.slice16()); + let bun_s = bun_core::String::init(zig); + bun_s.to_js(global) + } else { + bun_jsc::bun_string_jsc::create_utf8_for_js(global, str.slice8()) + } +} + +pub(crate) fn expr_to_js( + expr: bun_ast::Expr, + global: &bun_jsc::JSGlobalObject, +) -> bun_jsc::JsResult { + expr_to_js_with_check(expr, global, bun_core::StackCheck::init()) +} + +fn expr_to_js_with_check( + expr: bun_ast::Expr, + global: &bun_jsc::JSGlobalObject, + stack_check: bun_core::StackCheck, +) -> bun_jsc::JsResult { + use bun_ast::expr::Data as ExprData; + use bun_collections::VecExt as _; + use bun_jsc::JSValue; + + if !stack_check.is_safe_to_recurse() { + return Err(global.throw_stack_overflow()); + } + match expr.data { + ExprData::ENull(_) => Ok(JSValue::NULL), + ExprData::EBoolean(boolean) => Ok(JSValue::from(boolean.value)), + ExprData::ENumber(number) => Ok(JSValue::js_number(number.value())), + ExprData::EString(str) => estring_to_js(str.get(), global), + ExprData::EArray(arr) => { + JSValue::create_array_from_iter(global, arr.slice().iter(), |item| { + expr_to_js_with_check(*item, global, stack_check) + }) + } + ExprData::EObject(obj) => { + let js_obj = JSValue::create_empty_object(global, obj.properties.len_u32() as usize); + for prop in obj.properties.slice() { + let key_expr = prop.key.expect("infallible: prop has key"); + let value = expr_to_js_with_check( + prop.value.expect("infallible: prop has value"), + global, + stack_check, + )?; + let key_js = expr_to_js_with_check(key_expr, global, stack_check)?; + let key_str = bun_core::OwnedString::new(key_js.to_bun_string(global)?); + js_obj.put_may_be_index(global, &key_str, value)?; + } + Ok(js_obj) + } + _ => Ok(JSValue::UNDEFINED), + } +} diff --git a/src/runtime/api/JSON5Object.rs b/src/runtime/api/JSON5Object.rs index 1b5da97def4c..332930bfc582 100644 --- a/src/runtime/api/JSON5Object.rs +++ b/src/runtime/api/JSON5Object.rs @@ -1,10 +1,8 @@ -use bun_ast::{E, Expr, expr::Data as ExprData}; use bun_collections::HashMap; -use bun_collections::VecExt; use bun_core::StackCheck; -use bun_core::{OwnedString, String as BunString, ZigString}; +use bun_core::{OwnedString, String as BunString}; use bun_js_parser::lexer; -use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, StringJsc, wtf}; +use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, wtf}; use bun_parsers::json5; pub(crate) fn create(global: &JSGlobalObject) -> JSValue { @@ -76,7 +74,7 @@ pub fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { } }; - expr_to_js(root, global) + super::expr_to_js(root, global) }, ) } @@ -426,56 +424,3 @@ impl Stringifier { } } } - -fn estring_to_js(str: &E::EString, global: &JSGlobalObject) -> JsResult { - // NOTE: the JSON5 parser never builds ropes, so the simple slice → JS - // path is sufficient. - if str.is_utf16 { - let zig = ZigString::init_utf16(str.slice16()); - let bun_s = BunString::init(zig); - bun_s.to_js(global) - } else { - jsc::bun_string_jsc::create_utf8_for_js(global, str.slice8()) - } -} - -fn expr_to_js(expr: Expr, global: &JSGlobalObject) -> JsResult { - expr_to_js_with_check(expr, global, StackCheck::init()) -} - -fn expr_to_js_with_check( - expr: Expr, - global: &JSGlobalObject, - stack_check: StackCheck, -) -> JsResult { - if !stack_check.is_safe_to_recurse() { - return Err(global.throw_stack_overflow()); - } - match expr.data { - ExprData::ENull(_) => Ok(JSValue::NULL), - ExprData::EBoolean(boolean) => Ok(JSValue::from(boolean.value)), - ExprData::ENumber(number) => Ok(JSValue::js_number(number.value())), - ExprData::EString(str) => estring_to_js(str.get(), global), - ExprData::EArray(arr) => { - JSValue::create_array_from_iter(global, arr.slice().iter(), |item| { - expr_to_js_with_check(*item, global, stack_check) - }) - } - ExprData::EObject(obj) => { - let js_obj = JSValue::create_empty_object(global, obj.properties.len_u32() as usize); - for prop in obj.properties.slice() { - let key_expr = prop.key.expect("infallible: prop has key"); - let value = expr_to_js_with_check( - prop.value.expect("infallible: prop has value"), - global, - stack_check, - )?; - let key_js = expr_to_js_with_check(key_expr, global, stack_check)?; - let key_str = OwnedString::new(key_js.to_bun_string(global)?); - js_obj.put_may_be_index(global, &key_str, value)?; - } - Ok(js_obj) - } - _ => Ok(JSValue::UNDEFINED), - } -} diff --git a/src/runtime/api/TOMLObject.rs b/src/runtime/api/TOMLObject.rs index c63980b1b435..a73ba535166e 100644 --- a/src/runtime/api/TOMLObject.rs +++ b/src/runtime/api/TOMLObject.rs @@ -1,10 +1,17 @@ -use bun_core::String as BunString; -use bun_js_printer as js_printer; -use bun_jsc::{CallFrame, JSGlobalObject, JSValue, JsResult, LogJsc, StringJsc}; +use bun_collections::HashMap; +use bun_core::StackCheck; +use bun_core::{OwnedString, String as BunString}; +use bun_jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsError, JsResult, wtf}; use bun_parsers::toml::TOML; pub(crate) fn create(global: &JSGlobalObject) -> JSValue { - bun_jsc::create_host_function_object(global, &[("parse", __jsc_host_parse, 1)]) + bun_jsc::create_host_function_object( + global, + &[ + ("parse", __jsc_host_parse, 1), + ("stringify", __jsc_host_stringify, 3), + ], + ) } #[bun_jsc::host_fn] @@ -13,45 +20,533 @@ pub fn parse(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { global, frame, b"input.toml", - false, + true, true, |arena, log, source| { - let parse_result = match TOML::parse(source, log, arena, false) { + let root = match TOML::parse(source, log, arena, false) { Ok(v) => v, Err(bun_parsers::Error::StackOverflow) => { return Err(global.throw_stack_overflow()); } + Err(bun_parsers::Error::Alloc(_)) => { + return Err(JsError::OutOfMemory); + } Err(_) => { - return Err(global.throw_value(log.to_js(global, "Failed to parse toml")?)); + if let Some(first_msg) = log.msgs.first() { + return Err(global.throw_value(global.create_syntax_error_instance( + format_args!( + "TOML Parse error: {}", + bstr::BStr::new(&first_msg.data.text), + ), + ))); + } + return Err(global.throw_value(global.create_syntax_error_instance( + format_args!("TOML Parse error: Unable to parse TOML"), + ))); } }; - if log.has_errors() { - return Err(global.throw_value(log.to_js(global, "Failed to parse toml")?)); + super::expr_to_js(root, global) + }, + ) +} + +#[bun_jsc::host_fn] +pub(crate) fn stringify(global: &JSGlobalObject, frame: &CallFrame) -> JsResult { + // `space` is accepted for signature parity with YAML/JSON5 but ignored: + // TOML output is line-oriented and has no nesting indentation. + let [value, replacer, _space] = frame.arguments_as_array::<3>(); + + value.ensure_still_alive(); + + if value.is_undefined() || value.is_symbol() || value.is_function() { + return Ok(JSValue::UNDEFINED); + } + + if !replacer.is_undefined_or_null() { + return Err(global.throw(format_args!( + "TOML.stringify does not support the replacer argument" + ))); + } + + let unwrapped = value.unwrap_boxed_primitive(global)?; + if !unwrapped.is_object() || unwrapped.is_array() || unwrapped.is_date() { + return Err(global.throw(format_args!( + "TOML.stringify expects an object at the top level (a TOML document is a table)" + ))); + } + + let mut stringifier = Stringifier { + stack_check: StackCheck::init(), + builder: wtf::StringBuilder::init(), + visiting: HashMap::default(), + path: Vec::new(), + wrote: false, + }; + + if let Err(err) = stringifier.stringify_root(global, unwrapped) { + return match err { + StringifyError::Js(js_err) => Err(js_err), + StringifyError::StackOverflow => Err(global.throw_stack_overflow()), + }; + } + + stringifier.builder.to_string(global) +} + +#[derive(Debug)] +enum StringifyError { + Js(JsError), + StackOverflow, +} + +impl From for StringifyError { + fn from(e: JsError) -> Self { + StringifyError::Js(e) + } +} + +type StringifyResult = Result; + +/// Largest integer a JS number represents exactly; larger integral values +/// must be emitted as TOML floats so they round-trip through any reader. +const MAX_SAFE_INTEGER_F: f64 = 9007199254740991.0; + +/// How a property value is laid out in the document. +enum Layout { + /// `key = value` on the current table's line block. + Keyval, + /// `[path.key]` section. + Table, + /// `[[path.key]]` section per element. + ArrayOfTables, + Skip, +} + +struct Stringifier { + stack_check: StackCheck, + builder: wtf::StringBuilder, + // NOTE: `JSValue` keys live on the heap here, but every entry is also + // live on the native stack via the `stringify` recursion chain, so the + // conservative GC scan keeps them alive. + visiting: HashMap, + /// Header path of the table currently being emitted. Entries are + /// borrowed, not ref-counted: each is pushed and popped within the one + /// `JSPropertyIterator` loop body whose iterator keeps the name alive + /// (the iterator's strings carry no extra reference). + path: Vec, + /// Whether any line has been written (controls blank lines before headers). + wrote: bool, +} + +impl Stringifier { + fn stringify_root(&mut self, global: &JSGlobalObject, root: JSValue) -> StringifyResult<()> { + self.mark_visiting(global, root)?; + self.stringify_table_body(global, root)?; + self.visiting.remove(&root); + Ok(()) + } + + fn mark_visiting(&mut self, global: &JSGlobalObject, value: JSValue) -> StringifyResult<()> { + let was_present = self + .visiting + .get_or_put(value) + .map_err(|_| StringifyError::Js(JsError::OutOfMemory))? + .found_existing; + if was_present { + return Err(global + .throw(format_args!("Converting circular structure to TOML")) + .into()); + } + Ok(()) + } + + /// Decides the layout of one (already unboxed) property value. Reads + /// array elements when classifying arrays. + fn layout_of(&mut self, global: &JSGlobalObject, value: JSValue) -> StringifyResult { + if value.is_undefined() || value.is_symbol() || value.is_function() { + return Ok(Layout::Skip); + } + if value.is_array() { + // An array becomes [[key]] sections when it is non-empty and + // every element is a plain object; otherwise it is inline. + let mut iter = value.array_iterator(global)?; + if iter.len == 0 { + return Ok(Layout::Keyval); + } + while let Some(item) = iter.next()? { + let item = item.unwrap_boxed_primitive(global)?; + if !item.is_object() || item.is_array() || item.is_date() || item.is_function() { + return Ok(Layout::Keyval); + } + } + return Ok(Layout::ArrayOfTables); + } + if value.is_object() && !value.is_date() { + return Ok(Layout::Table); + } + Ok(Layout::Keyval) + } + + /// Emits the body of one table: `key = value` lines first, then + /// `[sub.table]` and `[[array.of.tables]]` sections (a keyval after a + /// header would belong to that header, so the order is forced). + fn stringify_table_body( + &mut self, + global: &JSGlobalObject, + table: JSValue, + ) -> StringifyResult<()> { + if !self.stack_check.is_safe_to_recurse() { + return Err(StringifyError::StackOverflow); + } + + let iter_options = jsc::JSPropertyIteratorOptions { + skip_empty_name: false, + include_value: true, + ..Default::default() + }; + + // Pass 1: keyvals. + let mut iter = + jsc::JSPropertyIterator::init(global, table.to_object(global)?, iter_options)?; + while let Some(prop_name) = iter.next()? { + let value = iter.value.unwrap_boxed_primitive(global)?; + if value.is_null() { + return Err(self.err_null_value(global, &prop_name)); } + if let Layout::Keyval = self.layout_of(global, value)? { + self.append_key_segment(&prop_name); + self.builder.append_latin1(b" = "); + self.stringify_inline_value(global, value)?; + self.builder.append_lchar(b'\n'); + self.wrote = true; + } + } - // for now... - let buffer_writer = js_printer::BufferWriter::init(); - let mut writer = js_printer::BufferPrinter::init(buffer_writer); - if js_printer::print_json( - &mut writer, - parse_result, - source, - js_printer::PrintJsonOptions { - indent: Default::default(), - mangled_props: None, - ..Default::default() - }, - ) - .is_err() - { - return Err(global.throw_value(log.to_js(global, "Failed to print toml")?)); + // Pass 2: sections. Values are re-read; an array-of-tables element + // that is no longer a plain object during emission gets an error. + let mut iter = + jsc::JSPropertyIterator::init(global, table.to_object(global)?, iter_options)?; + while let Some(prop_name) = iter.next()? { + let value = iter.value.unwrap_boxed_primitive(global)?; + match self.layout_of(global, value)? { + Layout::Keyval | Layout::Skip => {} + Layout::Table => { + self.mark_visiting(global, value)?; + self.path.push(prop_name); + self.append_header(false); + self.stringify_table_body(global, value)?; + self.path.pop(); + self.visiting.remove(&value); + } + Layout::ArrayOfTables => { + self.mark_visiting(global, value)?; + self.path.push(prop_name); + let mut items = value.array_iterator(global)?; + while let Some(item) = items.next()? { + let item = item.unwrap_boxed_primitive(global)?; + if !item.is_object() + || item.is_array() + || item.is_date() + || item.is_function() + { + self.path.pop(); + return Err(self.err_changed(global)); + } + self.mark_visiting(global, item)?; + self.append_header(true); + self.stringify_table_body(global, item)?; + self.visiting.remove(&item); + } + self.path.pop(); + self.visiting.remove(&value); + } } + } - let slice = writer.ctx.buffer.slice(); - let mut out = BunString::borrow_utf8(slice); + Ok(()) + } - out.to_js_by_parse_json(global) - }, - ) + /// One value on the right-hand side of `=` (or inside an inline + /// array/table). `value` is already unboxed. + fn stringify_inline_value( + &mut self, + global: &JSGlobalObject, + value: JSValue, + ) -> StringifyResult<()> { + if !self.stack_check.is_safe_to_recurse() { + return Err(StringifyError::StackOverflow); + } + + if value.is_boolean() { + self.builder.append_latin1(if value.as_boolean() { + b"true" + } else { + b"false" + }); + return Ok(()); + } + + if value.is_number() { + self.append_number(value); + return Ok(()); + } + + if value.is_big_int() { + return Err(global + .throw(format_args!("TOML.stringify cannot serialize BigInt")) + .into()); + } + + if value.is_string() { + let str = OwnedString::new(value.to_bun_string(global)?); + self.append_basic_quoted(&str); + return Ok(()); + } + + if value.is_date() { + return self.append_datetime(global, value); + } + + if value.is_array() { + self.mark_visiting(global, value)?; + self.builder.append_lchar(b'['); + let mut iter = value.array_iterator(global)?; + let mut first = true; + while let Some(item) = iter.next()? { + if !first { + self.builder.append_latin1(b", "); + } + first = false; + let item = item.unwrap_boxed_primitive(global)?; + if item.is_null() || item.is_undefined() || item.is_symbol() || item.is_function() { + return Err(self.err_in_array(global, item)); + } + self.stringify_inline_value(global, item)?; + } + self.builder.append_lchar(b']'); + self.visiting.remove(&value); + return Ok(()); + } + + // A plain object inside an inline context becomes an inline table. + self.mark_visiting(global, value)?; + let mut iter = jsc::JSPropertyIterator::init( + global, + value.to_object(global)?, + jsc::JSPropertyIteratorOptions { + skip_empty_name: false, + include_value: true, + ..Default::default() + }, + )?; + let mut first = true; + while let Some(prop_name) = iter.next()? { + let prop_value = iter.value.unwrap_boxed_primitive(global)?; + if prop_value.is_undefined() || prop_value.is_symbol() || prop_value.is_function() { + continue; + } + if prop_value.is_null() { + return Err(self.err_null_value(global, &prop_name)); + } + self.builder + .append_latin1(if first { b"{ " } else { b", " }); + first = false; + self.append_key_segment(&prop_name); + self.builder.append_latin1(b" = "); + self.stringify_inline_value(global, prop_value)?; + } + self.builder + .append_latin1(if first { b"{}" } else { b" }" }); + self.visiting.remove(&value); + Ok(()) + } + + // ── output pieces ────────────────────────────────────────────────────── + + /// `[a.b.c]` or `[[a.b.c]]` from `self.path`, preceded by a blank line + /// when the document already has content. + fn append_header(&mut self, array_of_tables: bool) { + if self.wrote { + self.builder.append_lchar(b'\n'); + } + self.builder + .append_latin1(if array_of_tables { b"[[" } else { b"[" }); + for (i, seg) in self.path.iter().enumerate() { + if i > 0 { + self.builder.append_lchar(b'.'); + } + // Inlined `append_key_segment` to avoid borrowing `self.path` + // across a `&mut self` call. + if is_bare_key(seg) { + self.builder.append_string(*seg); + } else { + append_basic_quoted_to(&mut self.builder, seg); + } + } + self.builder + .append_latin1(if array_of_tables { b"]]\n" } else { b"]\n" }); + self.wrote = true; + } + + fn append_key_segment(&mut self, name: &BunString) { + if is_bare_key(name) { + self.builder.append_string(*name); + } else { + append_basic_quoted_to(&mut self.builder, name); + } + } + + fn append_basic_quoted(&mut self, str: &BunString) { + append_basic_quoted_to(&mut self.builder, str); + } + + fn append_number(&mut self, value: JSValue) { + if value.is_int32() { + self.builder.append_int(value.as_int32()); + return; + } + let num = value.as_number(); + if num.is_nan() { + self.builder.append_latin1(b"nan"); + return; + } + if num.is_infinite() { + self.builder + .append_latin1(if num < 0.0 { b"-inf" } else { b"inf" }); + return; + } + if num == 0.0 { + // A double-encoded zero (is_int32 is an encoding check, not a + // value check); only the negative sign needs float form. + self.builder.append_latin1(if num.is_sign_negative() { + b"-0.0" + } else { + b"0" + }); + return; + } + self.builder.append_double(num); + // Integral doubles beyond the safe range print as bare digits, which + // a TOML reader would treat as an (out-of-range) integer; mark them + // as floats. At 1e21 and above the repr already has an exponent. + if num.fract() == 0.0 && num.abs() > MAX_SAFE_INTEGER_F && num.abs() < 1e21 { + self.builder.append_latin1(b".0"); + } + } + + /// A JS Date as a TOML offset date-time (`1979-05-27T07:32:00.999Z`). + /// A TOML offset date-time is RFC 3339, which the 24-byte + /// `YYYY-MM-DDTHH:mm:ss.sssZ` form of `Date.prototype.toISOString` is. + fn append_datetime(&mut self, global: &JSGlobalObject, value: JSValue) -> StringifyResult<()> { + let mut buf = [0u8; 64]; + let Some(iso) = value.to_iso_string(global, &mut buf) else { + return Err(global + .throw(format_args!( + "TOML.stringify cannot serialize an invalid Date" + )) + .into()); + }; + // The expanded-year form (leading `+`/`-`) has a 6-digit year, which + // TOML's 4-digit `date-fullyear` cannot carry. + if !iso[0].is_ascii_digit() { + return Err(global + .throw(format_args!( + "TOML.stringify cannot serialize a Date outside years 0000-9999" + )) + .into()); + } + self.builder.append_latin1(iso); + Ok(()) + } + + // ── errors ───────────────────────────────────────────────────────────── + + fn err_null_value(&mut self, global: &JSGlobalObject, key: &BunString) -> StringifyError { + let key_utf8 = key.to_utf8_bytes(); + global + .throw(format_args!( + "TOML cannot represent null (key '{}'); remove the key or use a sentinel value", + bstr::BStr::new(&key_utf8) + )) + .into() + } + + fn err_in_array(&mut self, global: &JSGlobalObject, value: JSValue) -> StringifyError { + let what: &str = if value.is_null() { + "null" + } else if value.is_undefined() { + "undefined" + } else if value.is_symbol() { + "a symbol" + } else { + "a function" + }; + global + .throw(format_args!("TOML cannot represent {} in an array", what)) + .into() + } + + fn err_changed(&mut self, global: &JSGlobalObject) -> StringifyError { + global + .throw(format_args!( + "TOML.stringify cannot serialize a value that changed during serialization" + )) + .into() + } +} + +fn is_bare_key(name: &BunString) -> bool { + if name.length() == 0 { + return false; + } + for i in 0..name.length() { + let c = name.char_at(i); + let ok = c < 0x80 && { + let b = c as u8; + b.is_ascii_alphanumeric() || b == b'-' || b == b'_' + }; + if !ok { + return false; + } + } + true +} + +/// TOML basic string with escapes. Unpaired surrogates become U+FFFD, the +/// same USVString conversion `TOML.parse` applies to its string input. +fn append_basic_quoted_to(builder: &mut wtf::StringBuilder, str: &BunString) { + builder.append_lchar(b'"'); + let len = str.length(); + let mut i = 0; + while i < len { + let c = str.char_at(i); + match c { + 0x08 => builder.append_latin1(b"\\b"), + 0x09 => builder.append_latin1(b"\\t"), + 0x0a => builder.append_latin1(b"\\n"), + 0x0c => builder.append_latin1(b"\\f"), + 0x0d => builder.append_latin1(b"\\r"), + 0x22 => builder.append_latin1(b"\\\""), + 0x5c => builder.append_latin1(b"\\\\"), + 0x00..=0x1f | 0x7f => { + builder.append_latin1(b"\\u00"); + builder.append_lchar(bun_core::fmt::hex_char_lower((c >> 4) as u8)); + builder.append_lchar(bun_core::fmt::hex_char_lower(c as u8)); + } + 0xD800..=0xDBFF => { + if i + 1 < len && (0xDC00..=0xDFFF).contains(&str.char_at(i + 1)) { + builder.append_uchar(c); + builder.append_uchar(str.char_at(i + 1)); + i += 1; + } else { + builder.append_uchar(0xFFFD); + } + } + 0xDC00..=0xDFFF => builder.append_uchar(0xFFFD), + _ => builder.append_uchar(c), + } + i += 1; + } + builder.append_lchar(b'"'); } diff --git a/src/runtime/bake/dev_server/error_report_request.rs b/src/runtime/bake/dev_server/error_report_request.rs index 43aaed631675..d3fcbf69a25f 100644 --- a/src/runtime/bake/dev_server/error_report_request.rs +++ b/src/runtime/bake/dev_server/error_report_request.rs @@ -18,7 +18,6 @@ use bun_alloc::ArenaVecExt as _; use bun_alloc::Arena; // bumpalo::Bump re-export -use bun_ast::Log; use bun_collections::ArrayHashMap; use bun_core::{Ordinal, Output}; use bun_core::{String as BunString, strings}; @@ -459,40 +458,6 @@ fn extract_json_encoded_source_code<'a, const N: usize>( let mut rest = &contents[index_of_first_line..]; - // For decoding JSON escapes, the JS Lexer decoding function has - // `decodeEscapeSequences`, which only supports decoding to UTF-16. - // Alternatively, it appears the TOML lexer has copied this exact - // function but for UTF-8. So the decoder can just use that. - // - // This function expects but does not assume the escape sequences - // given are valid, and does not bubble errors up. - // - // Note: `Lexer<'a>` borrows `&'a mut Log` and `&'a Source`; allocate - // both from the caller's arena so their lifetime matches the decoded - // `ArenaVec<'a, u8>` slices we hand back in `result`. - let log: &'a mut Log = arena.alloc(Log::init()); - let source: &'a bun_ast::Source = arena.alloc(bun_ast::Source::init_empty_file(b"")); - let mut l = bun_parsers::toml::Lexer { - log, - source, - start: 0, - end: 0, - current: 0, - bump: arena, - code_point: -1, - identifier: b"", - number: 0.0, - prev_error_loc: bun_ast::Loc::EMPTY, - string_literal_slice: b"", - string_literal_is_ascii: true, - line_number: 0, - token: bun_parsers::toml::lexer::T::t_end_of_file, - allow_double_bracket: true, - has_newline_before: false, - should_redact_logs: false, - }; - // log dropped at scope exit - let mut result: [&'a [u8]; N] = [b""; N]; for decoded_line in result.iter_mut() { let mut has_extra_escapes = false; @@ -515,11 +480,11 @@ fn extract_json_encoded_source_code<'a, const N: usize>( }; let encoded_line = &rest[..end_of_line]; - // Decode it + // Decode JSON escapes straight to UTF-8. if has_extra_escapes { let mut bytes: bun_alloc::ArenaVec<'a, u8> = bun_alloc::ArenaVec::with_capacity_in(encoded_line.len(), arena); - l.decode_escape_sequences::(0, encoded_line, &mut bytes)?; + super::js_escape::decode_js_escape_sequences(encoded_line, &mut bytes)?; *decoded_line = bytes.into_bump_slice(); } else { *decoded_line = encoded_line; diff --git a/src/runtime/bake/dev_server/js_escape.rs b/src/runtime/bake/dev_server/js_escape.rs new file mode 100644 index 000000000000..d5a9854968a3 --- /dev/null +++ b/src/runtime/bake/dev_server/js_escape.rs @@ -0,0 +1,219 @@ +//! Decodes JavaScript string escape sequences from UTF-8 text into UTF-8 +//! bytes for the error-report endpoint. Ported from the old TOML lexer's +//! `decode_escape_sequences` (single-line mode), preserving its lenient +//! semantics exactly. + +use bun_alloc::ArenaVec; +use bun_core::fmt::hex_digit_value_u32; +use bun_core::strings::{self, CodePoint}; + +pub(crate) fn decode_js_escape_sequences<'a>( + text: &[u8], + buf: &mut ArenaVec<'a, u8>, +) -> Result<(), crate::Error> { + let syntax_error = || crate::Error::SyntaxError; + let iterator = strings::CodepointIterator::init(text); + let mut iter = strings::Cursor::default(); + while iterator.next(&mut iter) { + match iter.c { + c if c == '\r' as CodePoint => { + // Convert CRLF and CR into LF. + let next_i: usize = iter.i as usize + 1; + if next_i < text.len() && text[next_i] == b'\n' { + iter.i += 1; + } + buf.push(b'\n'); + continue; + } + + c if c == '\\' as CodePoint => { + if !iterator.next(&mut iter) { + return Ok(()); + } + + let c2 = iter.c; + match c2 { + c if c == 'b' as CodePoint => { + buf.push(8); + continue; + } + c if c == 'f' as CodePoint => { + buf.push(12); + continue; + } + c if c == 'n' as CodePoint => { + buf.push(10); + continue; + } + c if c == 'v' as CodePoint => { + buf.push(11); + continue; + } + c if c == 't' as CodePoint => { + buf.push(9); + continue; + } + c if c == 'r' as CodePoint => { + buf.push(13); + continue; + } + + // Legacy octal literals. + c if ('0' as CodePoint..='7' as CodePoint).contains(&c) => { + let mut value: i64 = (c2 - '0' as CodePoint) as i64; + let mut restore = iter; + + if !iterator.next(&mut iter) { + if value == 0 { + buf.push(0); + return Ok(()); + } + return Err(syntax_error()); + } + + let c3: CodePoint = iter.c; + match c3 { + c if ('0' as CodePoint..='7' as CodePoint).contains(&c) => { + value = value * 8 + (c3 - '0' as CodePoint) as i64; + restore = iter; + if !iterator.next(&mut iter) { + return Err(syntax_error()); + } + + let c4 = iter.c; + match c4 { + c if ('0' as CodePoint..='7' as CodePoint).contains(&c) => { + let temp = value * 8 + (c4 - '0' as CodePoint) as i64; + if temp < 256 { + value = temp; + } else { + iter = restore; + } + } + // An 8 or 9 after octal digits is consumed + // without contributing (original behavior). + c if c == '8' as CodePoint || c == '9' as CodePoint => {} + _ => { + iter = restore; + } + } + } + c if c == '8' as CodePoint || c == '9' as CodePoint => {} + _ => { + iter = restore; + } + } + + iter.c = i32::try_from(value).expect("octal value is at most 255"); + } + c if c == '8' as CodePoint || c == '9' as CodePoint => { + iter.c = c2; + } + // 2-digit hexadecimal. + c if c == 'x' as CodePoint => { + let mut value: CodePoint = 0; + for _ in 0..2 { + if !iterator.next(&mut iter) { + return Err(syntax_error()); + } + match hex_digit_value_u32(iter.c as u32) { + Some(d) => value = (value * 16) | d as CodePoint, + None => return Err(syntax_error()), + } + } + iter.c = value; + } + c if c == 'u' as CodePoint => { + let mut value: i64 = 0; + + if !iterator.next(&mut iter) { + return Err(syntax_error()); + } + let mut c3 = iter.c; + + if c3 == '{' as CodePoint { + // Variable-length `\u{...}`: validate every digit + // up to '}' even when out of range (original + // behavior); the clamp prevents i64 overflow. + let mut is_first = true; + let mut out_of_range = false; + loop { + if !iterator.next(&mut iter) { + // Ran out of input before the closing `}`. + return Err(syntax_error()); + } + c3 = iter.c; + if c3 == '}' as CodePoint { + if is_first { + return Err(syntax_error()); + } + break; + } + match hex_digit_value_u32(c3 as u32) { + Some(d) => { + if value <= 0x10FFFF { + value = (value * 16) | d as i64; + } + if value > 0x10FFFF { + out_of_range = true; + } + } + None => return Err(syntax_error()), + } + is_first = false; + } + if out_of_range { + // Out of range: stop decoding, keeping what was + // decoded so far (original behavior). + return Ok(()); + } + } else { + // Fixed-length `\uHHHH`. + let mut j: usize = 0; + while j < 4 { + match hex_digit_value_u32(c3 as u32) { + Some(d) => value = (value * 16) | d as i64, + None => return Err(syntax_error()), + } + if j < 3 { + if !iterator.next(&mut iter) { + return Err(syntax_error()); + } + c3 = iter.c; + } + j += 1; + } + } + + iter.c = value as CodePoint; + } + // Line continuations are not valid in this single-line mode. + c if c == '\r' as CodePoint + || c == '\n' as CodePoint + || c == 0x2028 + || c == 0x2029 => + { + return Err(syntax_error()); + } + _ => { + iter.c = c2; + } + } + } + _ => {} + } + + match iter.c { + -1 => return Err(syntax_error()), + 0..=127 => { + buf.push(u8::try_from(iter.c).expect("checked range")); + } + _ => { + let mut part: [u8; 4] = [0; 4]; + let len = strings::encode_wtf8_rune(&mut part, iter.c as u32); + buf.extend_from_slice(&part[0..len]); + } + } + } + Ok(()) +} diff --git a/src/runtime/bake/dev_server/mod.rs b/src/runtime/bake/dev_server/mod.rs index 2add0fdb68a4..8ea5a1ab2d82 100644 --- a/src/runtime/bake/dev_server/mod.rs +++ b/src/runtime/bake/dev_server/mod.rs @@ -23,6 +23,7 @@ use super::{Graph, Side}; // ─── submodules ────────────────────────────────────────────────────────────── pub(crate) mod error_report_request; pub(crate) mod hmr_socket; +pub(crate) mod js_escape; pub(crate) mod memory_cost; // NOTE: the `DevServer` scoped-log static (`ScopedLogger`) is declared in diff --git a/src/runtime/error.rs b/src/runtime/error.rs index 84720687203d..19466ef76a57 100644 --- a/src/runtime/error.rs +++ b/src/runtime/error.rs @@ -536,13 +536,6 @@ impl From for Error { } } -impl From for Error { - #[inline] - fn from(e: bun_parsers::toml::lexer::Error) -> Self { - Self::Parsers(e.into()) - } -} - impl From for Error { #[inline] fn from(_: bun_jsc::JsTerminated) -> Self { diff --git a/test/cli/install/bun-install-registry.test.ts b/test/cli/install/bun-install-registry.test.ts index 5bf0cca96d9e..efa137d7deb9 100644 --- a/test/cli/install/bun-install-registry.test.ts +++ b/test/cli/install/bun-install-registry.test.ts @@ -1141,7 +1141,7 @@ describe("bundledDependencies", () => { join(packageDir, "bunfig.toml"), ` [install] -cache = "${join(packageDir, ".bun-cache")}" +cache = "${join(packageDir, ".bun-cache").replaceAll("\\", "\\\\")}" `, ), ]); @@ -1298,7 +1298,7 @@ describe("optionalDependencies", () => { join(packageDir, "bunfig.toml"), ` [install] - cache = "${join(packageDir, ".bun-cache")}" + cache = "${join(packageDir, ".bun-cache").replaceAll("\\", "\\\\")}" optional = false registry = "http://localhost:${port}/" `, @@ -1784,7 +1784,7 @@ test("manifest cache will invalidate when registry changes", async () => { join(packageDir, "bunfig.toml"), ` [install] -cache = "${cacheDir}" +cache = "${cacheDir.replaceAll("\\", "\\\\")}" registry = "http://localhost:${port}" saveTextLockfile = false `, @@ -1819,7 +1819,7 @@ saveTextLockfile = false join(packageDir, "bunfig.toml"), ` [install] -cache = "${cacheDir}" +cache = "${cacheDir.replaceAll("\\", "\\\\")}" `, ), ]); @@ -3118,7 +3118,7 @@ test("--config cli flag works", async () => { join(packageDir, "bunfig2.toml"), ` [install] -cache = "${join(packageDir, ".bun-cache")}" +cache = "${join(packageDir, ".bun-cache").replaceAll("\\", "\\\\")}" registry = "http://localhost:${port}/" dev = false `, diff --git a/test/integration/bun-types/fixture/toml.ts b/test/integration/bun-types/fixture/toml.ts index 1b14d3132559..7fc1f69ec59c 100644 --- a/test/integration/bun-types/fixture/toml.ts +++ b/test/integration/bun-types/fixture/toml.ts @@ -5,3 +5,6 @@ import { expectType } from "./utilities"; expectType(data); expectType(Bun.TOML.parse(data)).is(); expectType(TOML.parse(data)).is(); +// `undefined` when the input is `undefined`, a function, or a symbol. +expectType(Bun.TOML.stringify({ abc: "def" })).is(); +expectType(TOML.stringify({ abc: "def" })).is(); diff --git a/test/js/bun/resolve/toml/toml-parse.test.ts b/test/js/bun/resolve/toml/toml-parse.test.ts index 8ebf533e488b..879e59581e6e 100644 --- a/test/js/bun/resolve/toml/toml-parse.test.ts +++ b/test/js/bun/resolve/toml/toml-parse.test.ts @@ -7,104 +7,89 @@ test("Bun.TOML.parse with non-string input throws", () => { }); // https://github.com/oven-sh/bun/issues/30893 -// TOML copy of decode_escape_sequences had the same unprotected subtraction as the JS -// lexer: `start + iter.i - widthN` underflows whenever an escape lands near byte 0 of -// the source. The string body must open at the start of the file — a quoted KEY at file -// start (`"\x…" = 1`) gives `start = 1`, so `1 + 2 - 4` underflows. A bare-key assignment -// like `key = "…"` puts `start` at 7, which is big enough that the subtraction stays -// positive on unpatched builds and the test wouldn't catch a regression. -// `\u{…}` is a separate case: `hex_start = iter.i - width - width2 - width3` doesn't -// involve `start` at all, so it underflows for *valid* input like `"\u{41}"` regardless -// of where the string sits. -test("Bun.TOML.parse accepts \\u{XX} at start of a basic string (#30893)", () => { - expect(Bun.TOML.parse(`key = "\\u{41}"`)).toEqual({ key: "A" }); +// https://github.com/oven-sh/bun/issues/32025 +// https://github.com/oven-sh/bun/issues/30825 +// `\u{…}` is a JavaScript escape, not TOML. +test("Bun.TOML.parse rejects JS-style \\u{XX} escapes (#30893, #32025, #30825)", () => { + let err: unknown; + try { + Bun.TOML.parse(`key = "\\u{41}"`); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 4 hex digits", + ); + // Arbitrarily long hex-digit runs, including ones that overflowed the old + // parser's i64 accumulator, are rejected at the opening brace. + expect(() => Bun.TOML.parse(`a = "\\u{${Buffer.alloc(64, "f").toString()}}"`)).toThrow(SyntaxError); + expect(() => Bun.TOML.parse('a = "\\u{41"')).toThrow(SyntaxError); }); +// https://github.com/oven-sh/bun/issues/30893: a `\x`/`\u` escape followed by +// a multi-byte codepoint in a quoted key at offset 0 crashed the old parser. test("Bun.TOML.parse rejects \\x escape in quoted key at file start without panicking (#30893)", () => { - // Quoted key at offset 0 puts `start = 1`; `\x` + 4-byte codepoint underflows at L1033. - // Bytes: `"\x" = 1` const input = '"\\x' + String.fromCodePoint(0x3945c) + '" = 1'; expect(() => Bun.TOML.parse(input)).toThrow(); }); test("Bun.TOML.parse rejects \\u escape in quoted key at file start without panicking (#30893)", () => { - // Quoted key at offset 0; `\u` + 4-byte codepoint underflows at L1125 (fixed-length \u branch). const input = '"\\u' + String.fromCodePoint(0x3945c) + '" = 1'; expect(() => Bun.TOML.parse(input)).toThrow(); }); -// https://github.com/oven-sh/bun/issues/30893 -// Off-by-one in the CRLF look-ahead of the `\r` line-continuation branch: the guard -// checked `iter.i < text.len()` but indexed `text[iter.i + 1]`. A multiline basic -// string ending in `\` immediately before `"""` triggers `text[len]` — and slice -// bounds checks fire in release too, so this was a hard crash everywhere (not just -// debug). The JS lexer already reads the index it guards on; this brings the TOML -// copy in line. -test("Bun.TOML.parse handles trailing backslash-CR in multiline basic string (#30893)", () => { - // Bytes: `key = """\"""` — a backslash line-continuation where the newline - // is a bare CR and the string ends immediately after it. - const input = 'key = """\\\r"""'; - expect(Bun.TOML.parse(input)).toEqual({ key: "" }); +// https://github.com/oven-sh/bun/issues/30893: `key = """\"""` crashed the +// old parser (out-of-bounds read in the line-continuation look-ahead). A bare +// CR is not a TOML newline, so a backslash before it is an invalid escape — +// the input must produce a clean SyntaxError, never a crash. +test("Bun.TOML.parse rejects trailing backslash-CR in multiline basic string (#30893)", () => { + let err: unknown; + try { + Bun.TOML.parse('key = """\\\r"""'); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: (0x0D)"); }); -// Pre-existing bug inherited from toml/lexer.zig: the `\t` / `\f` single-char escape -// arms had their output codepoints swapped (`\t` produced 0x0C form feed instead of -// 0x09 tab; `\f` produced 0x09 instead of 0x0C). The TOML spec (and ASCII) define -// `\t` = U+0009 and `\f` = U+000C, and the JS lexer already gets this right. +// The old parser swapped the `\t` / `\f` escape output codepoints; the spec +// defines `\t` = U+0009 and `\f` = U+000C. test("Bun.TOML.parse produces correct codepoints for \\t and \\f escapes", () => { expect(Bun.TOML.parse('k = "a\\tb"').k).toBe("a\u0009b"); expect(Bun.TOML.parse('k = "a\\fb"').k).toBe("a\u000cb"); }); -// The outer `\r` arm in decode_escape_sequences had the same iter.i-semantics bug -// as the `\r` escape arm below it: it indexed `text[iter.i]` for the CRLF lookahead, -// but after `next()` returns for `\r`, `iter.i` IS the `\r` byte, so the check never -// fired. Every literal CRLF in a slow-path multiline TOML basic string (any `"""..."""` -// containing CRLF plus at least one backslash escape to force the slow path) decoded -// to two LFs instead of one. +// The old parser decoded a literal CRLF to two LFs when the multiline string +// also contained a backslash escape; the spec normalizes CRLF to one LF. test("Bun.TOML.parse normalizes literal CRLF to LF in multiline basic strings", () => { - // `"""ab\tc"""` — the `\t` escape forces the slow decode path. const input = 'k = """a\r\nb\\tc"""'; expect(Bun.TOML.parse(input).k).toBe("a\nb\tc"); }); -const overflowingDigits = Buffer.alloc(64, "f").toString(); - -// https://github.com/oven-sh/bun/issues/30825 -// The TOML lexer carries a copy of the JS lexer's variable-length `\u{...}` loop and -// inherited both of its bugs: `value * 16` trapped in debug builds once the escape had -// enough hex digits to overflow `i64`, and falling off the end of the literal before the -// closing `}` accepted the half-parsed value (`"\u{41"` decoded to `"A"`). -test("Bun.TOML.parse rejects out-of-range \\u{...} escapes without overflowing (#30825)", () => { - expect(() => Bun.TOML.parse('a = "\\u{3333333316aaaaaaa}"')).toThrow("Unicode escape sequence is out of range"); - expect(() => Bun.TOML.parse(`a = "\\u{${overflowingDigits}}"`)).toThrow("Unicode escape sequence is out of range"); - expect(() => Bun.TOML.parse(`a = "\\u{0000${overflowingDigits}}"`)).toThrow( - "Unicode escape sequence is out of range", - ); - expect(() => Bun.TOML.parse('a = "\\u{110000}"')).toThrow("Unicode escape sequence is out of range"); -}); - -test("Bun.TOML.parse rejects \\u{...} escapes with no closing brace (#30825)", () => { - expect(() => Bun.TOML.parse('a = "\\u{41"')).toThrow("Syntax Error"); - expect(() => Bun.TOML.parse('a = "\\u{"')).toThrow("Syntax Error"); - expect(() => Bun.TOML.parse('a = "\\u{110000"')).toThrow("Syntax Error"); - expect(() => Bun.TOML.parse(`a = "\\u{${overflowingDigits}"`)).toThrow("Syntax Error"); +// Duplicate detection for non-ASCII keys: keys are stored as UTF-16 EStrings +// internally, and a byte-view comparison of those garbles the check — missing +// real duplicates and falsely rejecting distinct keys. +test("Bun.TOML.parse rejects duplicate non-ASCII keys", () => { + let err: unknown; + try { + Bun.TOML.parse('"é" = 1\n"é" = 2'); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'é'"); }); -test("Bun.TOML.parse still accepts in-range \\u{...} escapes (#30825)", () => { - expect(Bun.TOML.parse('a = "\\u{41}"')).toEqual({ a: "A" }); - // Long enough to overflow if the leading zeros were counted as significant digits. - expect(Bun.TOML.parse(`a = "\\u{${Buffer.alloc(64, "0").toString()}41}"`)).toEqual({ a: "A" }); - expect(Bun.TOML.parse('a = "\\u{10FFFF}"')).toEqual({ a: "\u{10FFFF}" }); +test("Bun.TOML.parse does not conflate distinct keys whose UTF-16 prefixes collide", () => { + // U+0100 stored as UTF-16 has the byte prefix [0x00], which must not match + // a previously-stored U+0000 key. + expect(Bun.TOML.parse('"\\u0000" = 1\n"\\u0100" = 2')).toEqual({ "\u0000": 1, "Ā": 2 }); }); -// https://github.com/oven-sh/bun/issues/31252 -// `Lexer::expect` in the TOML lexer logs a mismatch via `add_range_error` and -// then falls through to `next()` for error recovery, so the parser returned -// `Ok` with a partial AST for inputs like `[1 2]` and `[1 2 3]`. The JS entry -// point only inspected the `Result`, so the logged diagnostic was discarded -// and bogus values like `{"a":[1]}` / `{"a":[1,3]}` leaked out. The entry -// point now also checks `log.has_errors()` on the Ok path. +// https://github.com/oven-sh/bun/issues/31252: the old parser returned a +// partial AST for arrays missing comma separators instead of an error. test("Bun.TOML.parse rejects array values without comma separators (#31252)", () => { expect(() => Bun.TOML.parse("a = [1 2]")).toThrow(); expect(() => Bun.TOML.parse("a = [1 2 3]")).toThrow(); diff --git a/test/js/bun/toml/generate_toml_test_suite.ts b/test/js/bun/toml/generate_toml_test_suite.ts new file mode 100644 index 000000000000..e0ba0b62c358 --- /dev/null +++ b/test/js/bun/toml/generate_toml_test_suite.ts @@ -0,0 +1,416 @@ +#!/usr/bin/env bun +/** + * Generates toml-test-suite.test.ts from the official toml-lang/toml-test repository. + * + * Usage: + * bun bd test/js/bun/toml/generate_toml_test_suite.ts [path-to-toml-test] [--check] + * + * Must run under the debug build (`bun bd`): rejection-test error messages are + * captured from the in-tree TOML.parse at generation time. + * + * If no path is given, clones toml-lang/toml-test into a temp directory. + * Only tests in the TOML v1.1.0 manifest (tests/files-toml-1.1.0) are used. + * --check regenerates to a temp file and exits 1 if it differs from the + * committed suite. + * + * Expected values come from the suite's own tagged-JSON files (no reference + * implementation needed). Encoding of TOML types in JS: + * - integers -> number; values outside Number.MAX_SAFE_INTEGER throw + * (TOML requires lossless handling or an error; mixed number/BigInt + * output is not acceptable API) + * - datetime/datetime-local/date-local/time-local -> string (source text), + * compared via separator/fraction normalization (see generated helper) + * - invalid documents -> SyntaxError, with the exact full message asserted + * when the in-tree parser produced a SyntaxError at generation time + */ + +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +// --------------------------------------------------------------------------- +// 1. Locate toml-test +// --------------------------------------------------------------------------- +// The upstream commit the suite is generated from; bump deliberately. With no +// local path, the clone is checked out at this commit so regeneration and +// --check are stable as upstream advances. +const PINNED_COMMIT = "4d77658d0f903a13454ece4dbfeafeb7c7f31c9f"; + +const checkMode = process.argv.includes("--check"); +let suiteDir = process.argv.slice(2).find(a => a !== "--check"); +if (!suiteDir) { + const tmp = mkdtempSync(join(tmpdir(), "toml-test-")); + console.log(`Cloning toml-lang/toml-test into ${tmp} ...`); + execFileSync("git", ["clone", "https://github.com/toml-lang/toml-test.git", tmp], { stdio: "inherit" }); + execFileSync("git", ["-c", "advice.detachedHead=false", "checkout", PINNED_COMMIT], { + cwd: tmp, + stdio: "inherit", + }); + suiteDir = tmp; +} +const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: suiteDir }).toString().trim(); +const testsDir = join(suiteDir, "tests"); + +// --------------------------------------------------------------------------- +// 2. Read the TOML v1.1.0 manifest +// --------------------------------------------------------------------------- +const manifest = readFileSync(join(testsDir, "files-toml-1.1.0"), "utf8") + .split("\n") + .filter(line => line.endsWith(".toml")) + .sort(); + +// ignoreBOM keeps a leading U+FEFF in the decoded string — the BOM-acceptance +// tests (valid/utf8-bom-*) are vacuous without it. +const utf8Strict = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); + +interface ValidCase { + name: string; + input: string; + expected: unknown; +} +interface RejectionCase { + name: string; + input: string; + message: string | undefined; +} +interface ByteRejectionCase { + name: string; + base64: string; + message: string | undefined; +} +const validCases: ValidCase[] = []; +const invalidCases: RejectionCase[] = []; +const outOfRangeCases: RejectionCase[] = []; +const invalidEncodingCases: ByteRejectionCase[] = []; + +// --------------------------------------------------------------------------- +// 3. Decode toml-test tagged JSON into JS values +// --------------------------------------------------------------------------- +const DATETIME_KINDS = ["datetime", "datetime-local", "date-local", "time-local"] as const; +class TaggedDateTime { + constructor( + public kind: string, + public value: string, + ) {} +} + +function isTagged(v: unknown): v is { type: string; value: string } { + return ( + v !== null && + typeof v === "object" && + !Array.isArray(v) && + typeof (v as any).type === "string" && + typeof (v as any).value === "string" && + Object.keys(v).length === 2 + ); +} + +function decodeTagged(v: unknown): unknown { + if (isTagged(v)) { + const { type, value } = v; + switch (type) { + case "string": + return value; + case "bool": + return value === "true"; + case "integer": { + const big = BigInt(value); + if (big >= BigInt(Number.MIN_SAFE_INTEGER) && big <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(big); + return big; + } + case "float": { + if (/^[-+]?nan$/.test(value)) return NaN; + if (/^[-+]?inf$/.test(value)) return value.startsWith("-") ? -Infinity : Infinity; + return Number(value); + } + default: + if ((DATETIME_KINDS as readonly string[]).includes(type)) return new TaggedDateTime(type, value); + throw new Error(`Unknown tagged type: ${type}`); + } + } + if (Array.isArray(v)) return v.map(decodeTagged); + if (v !== null && typeof v === "object") { + // Null prototype so a "__proto__" key is stored as an own property. + const out: Record = Object.create(null); + for (const [k, val] of Object.entries(v)) out[k] = decodeTagged(val); + return out; + } + throw new Error(`Unexpected raw value in tagged JSON: ${JSON.stringify(v)}`); +} + +function containsBigInt(v: unknown): boolean { + if (typeof v === "bigint") return true; + if (Array.isArray(v)) return v.some(containsBigInt); + if (v instanceof TaggedDateTime) return false; + if (v !== null && typeof v === "object") return Object.values(v).some(containsBigInt); + return false; +} + +// Exact message asserted by rejection tests; captured from the in-tree parser. +function captureSyntaxErrorMessage(input: string | Uint8Array): string | undefined { + try { + (Bun as { TOML: { parse: (s: string | Uint8Array) => unknown } }).TOML.parse(input); + } catch (e) { + if (e instanceof SyntaxError) return e.message; + } + return undefined; +} + +// --------------------------------------------------------------------------- +// 4. Collect cases +// --------------------------------------------------------------------------- +for (const rel of manifest) { + const tomlPath = join(testsDir, rel); + const bytes = readFileSync(tomlPath); + const name = rel.replace(/\.toml$/, ""); + let input: string; + try { + input = utf8Strict.decode(bytes); + } catch { + // Not representable as a JS string: test it as raw bytes instead. + invalidEncodingCases.push({ + name, + base64: bytes.toString("base64"), + message: captureSyntaxErrorMessage(bytes), + }); + continue; + } + if (rel.startsWith("valid/")) { + const expected = decodeTagged(JSON.parse(readFileSync(tomlPath.replace(/\.toml$/, ".json"), "utf8"))); + if (containsBigInt(expected)) { + outOfRangeCases.push({ name, input, message: captureSyntaxErrorMessage(input) }); + } else { + validCases.push({ name, input, expected }); + } + } else { + invalidCases.push({ name, input, message: captureSyntaxErrorMessage(input) }); + } +} + +// --------------------------------------------------------------------------- +// 5. Code generation helpers +// --------------------------------------------------------------------------- + +// JSON.stringify covers C0 controls, quotes, and backslashes; additionally +// escape DEL/C1 controls, U+2028/U+2029, and U+FEFF so the generated source +// stays visibly ASCII-clean where it matters. +function jsString(s: string): string { + return JSON.stringify(s).replace( + /[\u007f-\u009f\u2028\u2029\ufeff]/g, + c => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`, + ); +} + +function valueToJS(val: unknown, indent: number = 0): string { + if (typeof val === "boolean") return String(val); + if (typeof val === "number") { + if (Number.isNaN(val)) return "NaN"; + if (val === Infinity) return "Infinity"; + if (val === -Infinity) return "-Infinity"; + if (Object.is(val, -0)) return "-0"; + return String(val).replace("e+", "e"); + } + if (typeof val === "string") return jsString(val); + if (val instanceof TaggedDateTime) return `dt(${jsString(val.kind)}, ${jsString(val.value)})`; + if (Array.isArray(val)) { + if (val.length === 0) return "[]"; + const items = val.map(v => valueToJS(v, indent + 1)); + const oneLine = `[${items.join(", ")}]`; + if (oneLine.length < 80 && !oneLine.includes("\n")) return oneLine; + const pad = " ".repeat(indent + 1); + const endPad = " ".repeat(indent); + return `[\n${items.map(i => `${pad}${i},`).join("\n")}\n${endPad}]`; + } + if (val !== null && typeof val === "object") { + const entries = Object.entries(val as Record); + if (entries.length === 0) return "{}"; + const parts = entries.map(([k, v]) => { + // A literal "__proto__" key would set the prototype; the computed form + // creates an own property. + const key = k === "__proto__" ? '["__proto__"]' : /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : jsString(k); + return `${key}: ${valueToJS(v, indent + 1)}`; + }); + const oneLine = `{ ${parts.join(", ")} }`; + if (oneLine.length < 80 && !oneLine.includes("\n")) return oneLine; + const pad = " ".repeat(indent + 1); + const endPad = " ".repeat(indent); + return `{\n${parts.map(p => `${pad}${p},`).join("\n")}\n${endPad}}`; + } + throw new Error(`Cannot serialize ${String(val)}`); +} + +// --------------------------------------------------------------------------- +// 6. Generate the test file +// --------------------------------------------------------------------------- +const kindUnion = DATETIME_KINDS.map(k => JSON.stringify(k)).join(" | "); + +let output = `// Tests generated from the official toml-lang/toml-test conformance suite +// Generated from toml-test commit: ${commit} +// Scope: TOML v1.1.0 manifest (tests/files-toml-1.1.0): ${validCases.length} valid + ${outOfRangeCases.length} out-of-range-integer + ${invalidCases.length} invalid + ${invalidEncodingCases.length} invalid-encoding cases +// Regenerate with: bun bd test/js/bun/toml/generate_toml_test_suite.ts [path-to-toml-test] +// +// TOML type encoding asserted by these tests: +// - integer: number; values outside Number.MAX_SAFE_INTEGER throw (TOML +// requires lossless handling or an error — see the out-of-range block) +// - datetime, datetime-local, date-local, time-local: string (source text); +// compared after normalizing the date/time separator to "T", uppercasing +// "Z", padding omitted seconds to ":00", and trimming trailing zeros from +// fractional seconds +// - invalid documents throw SyntaxError; the exact full message is asserted +// where the in-tree parser produced a SyntaxError at generation time +// +// Inputs that are not valid UTF-8 cannot be JS strings; they are passed to +// TOML.parse as raw bytes (base64-decoded) in the invalid-encoding block. +import { TOML } from "bun"; +import { describe, expect, test } from "bun:test"; + +class TomlDateTime { + constructor( + public kind: ${kindUnion}, + public value: string, + ) {} +} +function dt(kind: TomlDateTime["kind"], value: string): TomlDateTime { + return new TomlDateTime(kind, value); +} + +function normalizeDateTime(s: string): string { + return s + .replace(/^(\\d{4}-\\d{2}-\\d{2})[ tT]/, "$1T") + .replace(/[zZ]$/, "Z") + .replace(/(^|T)(\\d{2}:\\d{2})(?=[Z+-]|$)/, "$1$2:00") + .replace(/\\.(\\d+)/, (_, frac: string) => { + const trimmed = frac.replace(/0+$/, ""); + return trimmed === "" ? "" : "." + trimmed; + }); +} + +// Datetime markers become normalized strings; everything else is unchanged. +function normalizeExpected(expected: unknown): unknown { + if (expected instanceof TomlDateTime) return normalizeDateTime(expected.value); + if (Array.isArray(expected)) return expected.map(normalizeExpected); + if (expected !== null && typeof expected === "object") { + const out: Record = Object.create(null); + for (const [k, v] of Object.entries(expected)) out[k] = normalizeExpected(v); + return out; + } + return expected; +} + +// Normalize the positions of \`actual\` that \`expected\` marks as datetimes, in +// lockstep, so a single toEqual compares everything else exactly. +function normalizeActual(actual: unknown, expected: unknown): unknown { + if (expected instanceof TomlDateTime) { + return typeof actual === "string" ? normalizeDateTime(actual) : actual; + } + if (Array.isArray(expected) && Array.isArray(actual)) { + return actual.map((a, i) => normalizeActual(a, expected[i])); + } + if ( + expected !== null && + typeof expected === "object" && + actual !== null && + typeof actual === "object" && + !Array.isArray(actual) + ) { + const out: Record = Object.create(null); + for (const [k, v] of Object.entries(actual)) out[k] = normalizeActual(v, (expected as any)[k]); + return out; + } + return actual; +} + +function expectTomlEqual(parsed: unknown, expected: unknown): void { + expect(normalizeActual(parsed, expected)).toEqual(normalizeExpected(expected) as any); +} +`; + +output += `\n// Each case also asserts that parse(stringify(parse(input))) produces the same +// value: stringify must never emit a document its own parse rejects or reads +// back differently. The TOML text may change (date/times come back as quoted +// strings), but the JS value is a fixed point after one lap. +describe("toml-test/valid", () => {\n`; +for (const tc of validCases) { + output += ` test(${jsString(tc.name)}, () => {\n`; + output += ` const input: string = ${jsString(tc.input)};\n`; + output += ` const expected: any = ${valueToJS(tc.expected, 2)};\n`; + output += ` expectTomlEqual(TOML.parse(input), expected);\n`; + output += ` expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected);\n`; + output += ` });\n\n`; +} +output += `});\n`; + +function emitRejectionTest(name: string, inputDecl: string, message: string | undefined): string { + let body = ` test(${jsString(name)}, () => {\n`; + body += ` ${inputDecl}\n`; + body += ` let err: unknown;\n`; + body += ` try {\n`; + body += ` TOML.parse(input);\n`; + body += ` } catch (e) {\n`; + body += ` err = e;\n`; + body += ` }\n`; + body += ` expect(err).toBeInstanceOf(SyntaxError);\n`; + if (message !== undefined) { + body += ` expect((err as SyntaxError).message).toBe(${jsString(message)});\n`; + } + body += ` });\n\n`; + return body; +} + +output += ` +// Upstream marks these valid, asserting exact 64-bit integers, which JS +// numbers cannot represent. Bun rejects integers outside Number.MAX_SAFE_INTEGER +// instead of returning corrupted values or mixed number/BigInt types; the +// 64-bit range is a "should" in the spec (toml-lang/toml-test#154). +describe("toml-test/valid-out-of-range-integer", () => { +`; +for (const tc of outOfRangeCases) { + output += emitRejectionTest(tc.name, `const input: string = ${jsString(tc.input)};`, tc.message); +} +output += `});\n`; + +output += `\ndescribe("toml-test/invalid", () => {\n`; +for (const tc of invalidCases) { + output += emitRejectionTest(tc.name, `const input: string = ${jsString(tc.input)};`, tc.message); +} +output += `});\n`; + +output += ` +// These inputs are not valid UTF-8, so they are passed as raw bytes; a TOML +// document must be valid UTF-8 as a whole. +describe("toml-test/invalid-encoding", () => { +`; +for (const tc of invalidEncodingCases) { + output += emitRejectionTest(tc.name, `const input = Buffer.from(${jsString(tc.base64)}, "base64");`, tc.message); +} +output += `});\n`; + +const committedPath = join(import.meta.dir, "toml-test-suite.test.ts"); +// The --check comparand must sit beside the committed file: prettier 3 also +// honors .gitignore, whose bare `tmp` rule matches os.tmpdir() on Linux and +// makes prettier silently skip the file there instead of formatting it. +const outPath = checkMode ? join(import.meta.dir, "toml-test-suite.check.ts") : committedPath; +writeFileSync(outPath, output); +// Same prettier invocation as the repo's `bun run prettier` script, pinned to +// the repo config so output is byte-stable wherever it is written. +const repoRoot = join(import.meta.dir, "../../../.."); +execFileSync( + join(repoRoot, "node_modules/.bin/prettier"), + ["--plugin=prettier-plugin-organize-imports", "--config", join(repoRoot, ".prettierrc"), "--write", outPath], + { stdio: "inherit", cwd: repoRoot }, +); +if (checkMode) { + const fresh = readFileSync(outPath, "utf8"); + rmSync(outPath); + const committed = readFileSync(committedPath, "utf8"); + if (fresh !== committed) { + console.error(`MISMATCH: ${committedPath} is stale; regenerate it.`); + process.exit(1); + } + console.log(`OK: ${committedPath} is up to date.`); +} else { + console.log( + `Wrote ${outPath}: ${validCases.length} valid + ${outOfRangeCases.length} out-of-range + ${invalidCases.length} invalid + ${invalidEncodingCases.length} invalid-encoding tests`, + ); +} diff --git a/test/js/bun/toml/toml-test-suite.test.ts b/test/js/bun/toml/toml-test-suite.test.ts new file mode 100644 index 000000000000..30d9ae025da6 --- /dev/null +++ b/test/js/bun/toml/toml-test-suite.test.ts @@ -0,0 +1,8560 @@ +// Tests generated from the official toml-lang/toml-test conformance suite +// Generated from toml-test commit: 4d77658d0f903a13454ece4dbfeafeb7c7f31c9f +// Scope: TOML v1.1.0 manifest (tests/files-toml-1.1.0): 217 valid + 1 out-of-range-integer + 481 invalid + 9 invalid-encoding cases +// Regenerate with: bun bd test/js/bun/toml/generate_toml_test_suite.ts [path-to-toml-test] +// +// TOML type encoding asserted by these tests: +// - integer: number; values outside Number.MAX_SAFE_INTEGER throw (TOML +// requires lossless handling or an error — see the out-of-range block) +// - datetime, datetime-local, date-local, time-local: string (source text); +// compared after normalizing the date/time separator to "T", uppercasing +// "Z", padding omitted seconds to ":00", and trimming trailing zeros from +// fractional seconds +// - invalid documents throw SyntaxError; the exact full message is asserted +// where the in-tree parser produced a SyntaxError at generation time +// +// Inputs that are not valid UTF-8 cannot be JS strings; they are passed to +// TOML.parse as raw bytes (base64-decoded) in the invalid-encoding block. +import { TOML } from "bun"; +import { describe, expect, test } from "bun:test"; + +class TomlDateTime { + constructor( + public kind: "datetime" | "datetime-local" | "date-local" | "time-local", + public value: string, + ) {} +} +function dt(kind: TomlDateTime["kind"], value: string): TomlDateTime { + return new TomlDateTime(kind, value); +} + +function normalizeDateTime(s: string): string { + return s + .replace(/^(\d{4}-\d{2}-\d{2})[ tT]/, "$1T") + .replace(/[zZ]$/, "Z") + .replace(/(^|T)(\d{2}:\d{2})(?=[Z+-]|$)/, "$1$2:00") + .replace(/\.(\d+)/, (_, frac: string) => { + const trimmed = frac.replace(/0+$/, ""); + return trimmed === "" ? "" : "." + trimmed; + }); +} + +// Datetime markers become normalized strings; everything else is unchanged. +function normalizeExpected(expected: unknown): unknown { + if (expected instanceof TomlDateTime) return normalizeDateTime(expected.value); + if (Array.isArray(expected)) return expected.map(normalizeExpected); + if (expected !== null && typeof expected === "object") { + const out: Record = Object.create(null); + for (const [k, v] of Object.entries(expected)) out[k] = normalizeExpected(v); + return out; + } + return expected; +} + +// Normalize the positions of `actual` that `expected` marks as datetimes, in +// lockstep, so a single toEqual compares everything else exactly. +function normalizeActual(actual: unknown, expected: unknown): unknown { + if (expected instanceof TomlDateTime) { + return typeof actual === "string" ? normalizeDateTime(actual) : actual; + } + if (Array.isArray(expected) && Array.isArray(actual)) { + return actual.map((a, i) => normalizeActual(a, expected[i])); + } + if ( + expected !== null && + typeof expected === "object" && + actual !== null && + typeof actual === "object" && + !Array.isArray(actual) + ) { + const out: Record = Object.create(null); + for (const [k, v] of Object.entries(actual)) out[k] = normalizeActual(v, (expected as any)[k]); + return out; + } + return actual; +} + +function expectTomlEqual(parsed: unknown, expected: unknown): void { + expect(normalizeActual(parsed, expected)).toEqual(normalizeExpected(expected) as any); +} + +// Each case also asserts that parse(stringify(parse(input))) produces the same +// value: stringify must never emit a document its own parse rejects or reads +// back differently. The TOML text may change (date/times come back as quoted +// strings), but the JS value is a fixed point after one lap. +describe("toml-test/valid", () => { + test("valid/array/array-subtables", () => { + const input: string = "[[arr]]\n[arr.subtab]\nval=1\n\n[[arr]]\n[arr.subtab]\nval=2\n"; + const expected: any = { arr: [{ subtab: { val: 1 } }, { subtab: { val: 2 } }] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/array", () => { + const input: string = + 'ints = [1, 2, 3, ]\nfloats = [1.1, 2.1, 3.1]\nstrings = ["a", "b", "c"]\ndates = [\n\t1987-07-05T17:45:00Z,\n\t1979-05-27T07:32:00,\n\t2006-06-01,\n\t11:00:00,\n]\ncomments = [\n 1,\n 2, #this is ok\n]\n'; + const expected: any = { + comments: [1, 2], + dates: [ + dt("datetime", "1987-07-05T17:45:00Z"), + dt("datetime-local", "1979-05-27T07:32:00"), + dt("date-local", "2006-06-01"), + dt("time-local", "11:00:00"), + ], + floats: [1.1, 2.1, 3.1], + ints: [1, 2, 3], + strings: ["a", "b", "c"], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/bool", () => { + const input: string = "a = [true, false]\n"; + const expected: any = { a: [true, false] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/empty", () => { + const input: string = "thevoid = [[[[[]]]]]\n"; + const expected: any = { thevoid: [[[[[]]]]] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/hetergeneous", () => { + const input: string = 'mixed = [[1, 2], ["a", "b"], [1.1, 2.1]]\n'; + const expected: any = { + mixed: [ + [1, 2], + ["a", "b"], + [1.1, 2.1], + ], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/mixed-int-array", () => { + const input: string = 'arrays-and-ints = [1, ["Arrays are not integers."]]\n'; + const expected: any = { "arrays-and-ints": [1, ["Arrays are not integers."]] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/mixed-int-float", () => { + const input: string = "ints-and-floats = [1, 1.1]\n"; + const expected: any = { "ints-and-floats": [1, 1.1] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/mixed-int-string", () => { + const input: string = 'strings-and-ints = ["hi", 42]\n'; + const expected: any = { "strings-and-ints": ["hi", 42] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/mixed-string-table", () => { + const input: string = + 'contributors = [\n "Foo Bar ",\n { name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" }\n]\n\n# Start with a table as the first element. This tests a case that some libraries\n# might have where they will check if the first entry is a table/map/hash/assoc\n# array and then encode it as a table array. This was a reasonable thing to do\n# before TOML 1.0 since arrays could only contain one type, but now it\'s no\n# longer.\nmixed = [{k="a"}, "b", 1]\n'; + const expected: any = { + contributors: [ + "Foo Bar ", + { + email: "bazqux@example.com", + name: "Baz Qux", + url: "https://example.com/bazqux", + }, + ], + mixed: [{ k: "a" }, "b", 1], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/nested-double", () => { + const input: string = 'nest = [\n\t[\n\t\t["a"],\n\t\t[1, 2, [3]]\n\t]\n]\n'; + const expected: any = { nest: [[["a"], [1, 2, [3]]]] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/nested-inline-table", () => { + const input: string = "a = [ { b = {} } ]\n"; + const expected: any = { a: [{ b: {} }] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/nested", () => { + const input: string = 'nest = [["a"], ["b"]]\n'; + const expected: any = { nest: [["a"], ["b"]] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/nospaces", () => { + const input: string = "ints = [1,2,3]\n"; + const expected: any = { ints: [1, 2, 3] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/open-parent-table", () => { + const input: string = "[[parent-table.arr]]\n[[parent-table.arr]]\n[parent-table]\nnot-arr = 1\n"; + const expected: any = { "parent-table": { "not-arr": 1, arr: [{}, {}] } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/string-quote-comma-01", () => { + const input: string = 'title = [\n"Client: \\"XXXX\\", Job: XXXX",\n"Code: XXXX"\n]\n'; + const expected: any = { title: ['Client: "XXXX", Job: XXXX', "Code: XXXX"] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/string-quote-comma-02", () => { + const input: string = 'title = [ " \\", ",]\n'; + const expected: any = { title: [' ", '] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/string-with-comma-01", () => { + const input: string = 'title = [\n"Client: XXXX, Job: XXXX",\n"Code: XXXX"\n]\n'; + const expected: any = { title: ["Client: XXXX, Job: XXXX", "Code: XXXX"] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/string-with-comma-02", () => { + const input: string = 'title = [\n"""Client: XXXX,\nJob: XXXX""",\n"Code: XXXX"\n]\n'; + const expected: any = { title: ["Client: XXXX,\nJob: XXXX", "Code: XXXX"] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/strings", () => { + const input: string = "string_array = [ \"all\", 'strings', \"\"\"are the same\"\"\", '''type''']\n"; + const expected: any = { string_array: ["all", "strings", "are the same", "type"] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/table-array-string-backslash", () => { + const input: string = 'foo = [ { bar="\\"{{baz}}\\""} ]\n'; + const expected: any = { foo: [{ bar: '"{{baz}}"' }] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/array/trailing-comma", () => { + const input: string = "arr-1 = [1,]\n\narr-2 = [2,3,]\n\narr-3 = [4,\n]\n\narr-4 = [\n\t5,\n\t6,\n]\n"; + const expected: any = { "arr-1": [1], "arr-3": [4], "arr-2": [2, 3], "arr-4": [5, 6] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/bool/bool", () => { + const input: string = "t = true\nf = false\n"; + const expected: any = { f: false, t: true }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/comment/after-literal-no-ws", () => { + const input: string = "inf=inf#infinity\nnan=nan#not a number\ntrue=true#true\nfalse=false#false\n"; + const expected: any = { false: false, inf: Infinity, nan: NaN, true: true }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/comment/at-eof", () => { + const input: string = '# This is a full-line comment\nkey = "value" # This is a comment at the end of a line\n'; + const expected: any = { key: "value" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/comment/at-eof2", () => { + const input: string = '# This is a full-line comment\nkey = "value" # This is a comment at the end of a line\n'; + const expected: any = { key: "value" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/comment/everywhere", () => { + const input: string = + '# Top comment.\n # Top comment.\n# Top comment.\n\n# [no-extraneous-groups-please]\n\n[group] # Comment\nanswer = 42 # Comment\n# no-extraneous-keys-please = 999\n# Inbetween comment.\nmore = [ # Comment\n # What about multiple # comments?\n # Can you handle it?\n #\n # Evil.\n# Evil.\n 42, 42, # Comments within arrays are fun.\n # What about multiple # comments?\n # Can you handle it?\n #\n # Evil.\n# Evil.\n# ] Did I fool you?\n] # Hopefully not.\n\n# Make sure the space between the datetime and "#" isn\'t lexed.\ndt = 1979-05-27T07:32:12-07:00 # c\nd = 1979-05-27 # Comment\n\n[[aot]] # Comment\nk = 98 # Comment\n[[aot]]# Comment\nk = 99# Comment\n'; + const expected: any = { + aot: [{ k: 98 }, { k: 99 }], + group: { + answer: 42, + d: dt("date-local", "1979-05-27"), + dt: dt("datetime", "1979-05-27T07:32:12-07:00"), + more: [42, 42], + }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/comment/noeol", () => { + const input: string = "# single comment without any eol characters"; + const expected: any = {}; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/comment/nonascii", () => { + const input: string = "# ~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿\n"; + const expected: any = {}; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/comment/tricky", () => { + const input: string = + '[section]#attached comment\n#[notsection]\none = "11"#cmt\ntwo = "22#"\nthree = \'#\'\n\nfour = """# no comment\n# nor this\n#also not comment"""#is_comment\n\nfive = 5.5#66\nsix = 6#7\n8 = "eight"\n#nine = 99\nten = 10e2#1\neleven = 1.11e1#23\n\n["hash#tag"]\n"#!" = "hash bang"\narr3 = [ "#", \'#\', """###""" ]\narr4 = [ 1,# 9, 9,\n2#,9\n,#9\n3#]\n,4]\narr5 = [[[[#["#"],\n["#"]]]]#]\n]\ntbl1 = { "#" = \'}#\'}#}}\n\n\n'; + const expected: any = { + "hash#tag": { + "#!": "hash bang", + arr5: [[[[["#"]]]]], + arr3: ["#", "#", "###"], + arr4: [1, 2, 3, 4], + tbl1: { "#": "}#" }, + }, + section: { + "8": "eight", + eleven: 11.1, + five: 5.5, + four: "# no comment\n# nor this\n#also not comment", + one: "11", + six: 6, + ten: 1000, + three: "#", + two: "22#", + }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/datetime/datetime", () => { + const input: string = + 'space = 1987-07-05 17:45:00Z\n\n# ABNF is case-insensitive, both "Z" and "z" must be supported.\nlower = 1987-07-05t17:45:00z\n'; + const expected: any = { + lower: dt("datetime", "1987-07-05T17:45:00Z"), + space: dt("datetime", "1987-07-05T17:45:00Z"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/datetime/edge", () => { + const input: string = + "first-offset = 0001-01-01 00:00:00Z\nfirst-local = 0001-01-01 00:00:00\nfirst-date = 0001-01-01\n\nlast-offset = 9999-12-31 23:59:59Z\nlast-local = 9999-12-31 23:59:59\nlast-date = 9999-12-31\n"; + const expected: any = { + "first-date": dt("date-local", "0001-01-01"), + "first-local": dt("datetime-local", "0001-01-01T00:00:00"), + "first-offset": dt("datetime", "0001-01-01T00:00:00Z"), + "last-date": dt("date-local", "9999-12-31"), + "last-local": dt("datetime-local", "9999-12-31T23:59:59"), + "last-offset": dt("datetime", "9999-12-31T23:59:59Z"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/datetime/invalid-date-in-string", () => { + const input: string = "s = '2020-01-01x'\n"; + const expected: any = { s: "2020-01-01x" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/datetime/leap-year", () => { + const input: string = + "2000-datetime = 2000-02-29 15:15:15Z\n2000-datetime-local = 2000-02-29 15:15:15\n2000-date = 2000-02-29\n\n2024-datetime = 2024-02-29 15:15:15Z\n2024-datetime-local = 2024-02-29 15:15:15\n2024-date = 2024-02-29\n"; + const expected: any = { + "2000-date": dt("date-local", "2000-02-29"), + "2000-datetime": dt("datetime", "2000-02-29T15:15:15Z"), + "2000-datetime-local": dt("datetime-local", "2000-02-29T15:15:15"), + "2024-date": dt("date-local", "2024-02-29"), + "2024-datetime": dt("datetime", "2024-02-29T15:15:15Z"), + "2024-datetime-local": dt("datetime-local", "2024-02-29T15:15:15"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/datetime/local-date", () => { + const input: string = "bestdayever = 1987-07-05\n"; + const expected: any = { bestdayever: dt("date-local", "1987-07-05") }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/datetime/local-time", () => { + const input: string = "besttimeever = 17:45:00\nmilliseconds = 10:32:00.555\n"; + const expected: any = { + besttimeever: dt("time-local", "17:45:00"), + milliseconds: dt("time-local", "10:32:00.555"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/datetime/local", () => { + const input: string = "local = 1987-07-05T17:45:00\nmilli = 1977-12-21T10:32:00.555\nspace = 1987-07-05 17:45:00\n"; + const expected: any = { + local: dt("datetime-local", "1987-07-05T17:45:00"), + milli: dt("datetime-local", "1977-12-21T10:32:00.555"), + space: dt("datetime-local", "1987-07-05T17:45:00"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/datetime/milliseconds", () => { + const input: string = + "utc1 = 1987-07-05T17:45:56.123Z\nutc2 = 1987-07-05T17:45:56.6Z\nwita1 = 1987-07-05T17:45:56.123+08:00\nwita2 = 1987-07-05T17:45:56.6+08:00\n"; + const expected: any = { + utc1: dt("datetime", "1987-07-05T17:45:56.123Z"), + utc2: dt("datetime", "1987-07-05T17:45:56.600Z"), + wita1: dt("datetime", "1987-07-05T17:45:56.123+08:00"), + wita2: dt("datetime", "1987-07-05T17:45:56.600+08:00"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/datetime/no-seconds", () => { + const input: string = + "# Seconds are optional in date-time and time.\nwithout-seconds-1 = 13:37\nwithout-seconds-2 = 1979-05-27 07:32Z\nwithout-seconds-3 = 1979-05-27 07:32-07:00\nwithout-seconds-4 = 1979-05-27T07:32\n"; + const expected: any = { + "without-seconds-1": dt("time-local", "13:37:00"), + "without-seconds-2": dt("datetime", "1979-05-27T07:32:00Z"), + "without-seconds-3": dt("datetime", "1979-05-27T07:32:00-07:00"), + "without-seconds-4": dt("datetime-local", "1979-05-27T07:32:00"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/datetime/timezone", () => { + const input: string = + "utc = 1987-07-05T17:45:56Z\npdt = 1987-07-05T17:45:56-05:00\nnzst = 1987-07-05T17:45:56+12:00\nnzdt = 1987-07-05T17:45:56+13:00 # DST\n"; + const expected: any = { + nzdt: dt("datetime", "1987-07-05T17:45:56+13:00"), + nzst: dt("datetime", "1987-07-05T17:45:56+12:00"), + pdt: dt("datetime", "1987-07-05T17:45:56-05:00"), + utc: dt("datetime", "1987-07-05T17:45:56Z"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/empty-crlf", () => { + const input: string = "\r\n"; + const expected: any = {}; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/empty-lf", () => { + const input: string = "\n"; + const expected: any = {}; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/empty-nothing", () => { + const input: string = ""; + const expected: any = {}; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/empty-space", () => { + const input: string = " "; + const expected: any = {}; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/empty-tab", () => { + const input: string = "\t"; + const expected: any = {}; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/example", () => { + const input: string = + "best-day-ever = 1987-07-05T17:45:00Z\n\n[numtheory]\nboring = false\nperfection = [6, 28, 496]\n"; + const expected: any = { + "best-day-ever": dt("datetime", "1987-07-05T17:45:00Z"), + numtheory: { boring: false, perfection: [6, 28, 496] }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/float/exponent-upper", () => { + const input: string = + '# Both upper- and lower-case "e" is valid, so repeat the exponent.toml test with\n# upper-case.\nexp = 3E2\npos-exp = 3E+2\nneg-exp = 3E-2\nzero-exp = 3E0\nfrac = 3.1E2\nneg = -1E-1\nzero = 0E2\nzero-plus = +0E2\n'; + const expected: any = { + exp: 300, + frac: 310, + neg: -0.1, + "neg-exp": 0.03, + "pos-exp": 300, + zero: 0, + "zero-exp": 3, + "zero-plus": 0, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/float/exponent", () => { + const input: string = + "# Please keep exponent-upper.toml in sync with this.\n\nexp = 3e2\npos-exp = 3e+2\nneg-exp = 3e-2\nzero-exp = 3e0\nfrac = 3.1e2\nneg = -1e-1\nzero = 0e2\nzero-plus = +0e2\n"; + const expected: any = { + exp: 300, + frac: 310, + neg: -0.1, + "neg-exp": 0.03, + "pos-exp": 300, + zero: 0, + "zero-exp": 3, + "zero-plus": 0, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/float/float", () => { + const input: string = + "pi = 3.14\npospi = +3.14\nnegpi = -3.14\nzero-intpart = 0.123\nleading-zero-fractional = 0.0123\n"; + const expected: any = { + negpi: -3.14, + pi: 3.14, + pospi: 3.14, + "zero-intpart": 0.123, + "leading-zero-fractional": 0.0123, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/float/inf-and-nan", () => { + const input: string = + "# We don't encode +nan and -nan back with the signs; many languages don't\n# support a sign on NaN (it doesn't really make much sense).\nnan = nan\nnan_neg = -nan\nnan_plus = +nan\ninfinity = inf\ninfinity_neg = -inf\ninfinity_plus = +inf\n"; + const expected: any = { + infinity: Infinity, + infinity_neg: -Infinity, + infinity_plus: Infinity, + nan: NaN, + nan_neg: NaN, + nan_plus: NaN, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/float/long", () => { + const input: string = "longpi = 3.141592653589793\nneglongpi = -3.141592653589793\n"; + const expected: any = { longpi: 3.141592653589793, neglongpi: -3.141592653589793 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/float/max-int", () => { + const input: string = + "# Maximum and minimum safe natural numbers.\nmax_float = 9_007_199_254_740_991.0\nmin_float = -9_007_199_254_740_991.0\n"; + const expected: any = { max_float: 9007199254740991, min_float: -9007199254740991 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/float/underscore", () => { + const input: string = "before = 3_141.5927\nafter = 3141.592_7\nexponent = 3e1_4\n"; + const expected: any = { after: 3141.5927, before: 3141.5927, exponent: 300000000000000 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/float/zero", () => { + const input: string = + "zero = 0.0\nsigned-pos = +0.0\nsigned-neg = -0.0\nexponent = 0e0\nexponent-two-0 = 0e00\nexponent-signed-pos = +0e0\nexponent-signed-neg = -0e0\n"; + const expected: any = { + exponent: 0, + "exponent-signed-neg": -0, + "exponent-signed-pos": 0, + "exponent-two-0": 0, + "signed-neg": -0, + "signed-pos": 0, + zero: 0, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/implicit-and-explicit-after", () => { + const input: string = "[a.b.c]\nanswer = 42\n\n[a]\nbetter = 43\n"; + const expected: any = { a: { better: 43, b: { c: { answer: 42 } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/implicit-and-explicit-before", () => { + const input: string = "[a]\nbetter = 43\n\n[a.b.c]\nanswer = 42\n"; + const expected: any = { a: { better: 43, b: { c: { answer: 42 } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/implicit-groups", () => { + const input: string = "[a.b.c]\nanswer = 42\n"; + const expected: any = { a: { b: { c: { answer: 42 } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/array-01", () => { + const input: string = + 'arr = [ {\'a\'= 1}, {\'a\'= 2} ]\n\npeople = [{first_name = "Bruce", last_name = "Springsteen"},\n {first_name = "Eric", last_name = "Clapton"},\n {first_name = "Bob", last_name = "Seger"}]\n'; + const expected: any = { + arr: [{ a: 1 }, { a: 2 }], + people: [ + { first_name: "Bruce", last_name: "Springsteen" }, + { first_name: "Eric", last_name: "Clapton" }, + { first_name: "Bob", last_name: "Seger" }, + ], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/array-02", () => { + const input: string = + '# "No newlines are allowed between the curly braces unless they are valid within\n# a value"\n\na = { a = [\n]}\n'; + const expected: any = { a: { a: [] } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/array-03", () => { + const input: string = "b = { a = [\n\t\t1,\n\t\t2,\n\t], b = [\n\t\t3,\n\t\t4,\n\t]}\n"; + const expected: any = { b: { a: [1, 2], b: [3, 4] } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/bool", () => { + const input: string = "a = {a = true, b = false}\n"; + const expected: any = { a: { a: true, b: false } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/empty", () => { + const input: string = + 'empty1 = {}\nempty2 = { }\nempty_in_array = [ { not_empty = 1 }, {} ]\nempty_in_array2 = [{},{not_empty=1}]\nmany_empty = [{},{},{}]\nnested_empty = {"empty"={}}\nwith_cmt ={ }#nothing here\n'; + const expected: any = { + empty1: {}, + empty2: {}, + with_cmt: {}, + empty_in_array: [{ not_empty: 1 }, {}], + empty_in_array2: [{}, { not_empty: 1 }], + many_empty: [{}, {}, {}], + nested_empty: { empty: {} }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/end-in-bool", () => { + const input: string = 'black = { python=">3.6", version=">=18.9b0", allow_prereleases=true }\n'; + const expected: any = { black: { allow_prereleases: true, python: ">3.6", version: ">=18.9b0" } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/inline-table", () => { + const input: string = + 'name = { first = "Tom", last = "Preston-Werner" }\npoint = { x = 1, y = 2 }\nsimple = { a = 1 }\nstr-key = { "a" = 1 }\ntable-array = [{ "a" = 1 }, { "b" = 2 }]\n'; + const expected: any = { + name: { first: "Tom", last: "Preston-Werner" }, + point: { x: 1, y: 2 }, + simple: { a: 1 }, + "str-key": { a: 1 }, + "table-array": [{ a: 1 }, { b: 2 }], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/key-dotted-01", () => { + const input: string = + 'a = { a.b = 1 }\nb = { "a"."b" = 1 }\nc = { a . b = 1 }\nd = { \'a\' . "b" = 1 }\ne = {a.b=1}\n'; + const expected: any = { + a: { a: { b: 1 } }, + b: { a: { b: 1 } }, + c: { a: { b: 1 } }, + d: { a: { b: 1 } }, + e: { a: { b: 1 } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/key-dotted-02", () => { + const input: string = "many.dots.here.dot.dot.dot = {a.b.c = 1, a.b.d = 2}\n"; + const expected: any = { + many: { dots: { here: { dot: { dot: { dot: { a: { b: { c: 1, d: 2 } } } } } } } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/key-dotted-03", () => { + const input: string = "[tbl]\na.b.c = {d.e=1}\n\n[tbl.x]\na.b.c = {d.e=1}\n"; + const expected: any = { + tbl: { a: { b: { c: { d: { e: 1 } } } }, x: { a: { b: { c: { d: { e: 1 } } } } } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/key-dotted-04", () => { + const input: string = "[[arr]]\nt = {a.b=1}\nT = {a.b=1}\n\n[[arr]]\nt = {a.b=2}\nT = {a.b=2}\n"; + const expected: any = { + arr: [ + { T: { a: { b: 1 } }, t: { a: { b: 1 } } }, + { T: { a: { b: 2 } }, t: { a: { b: 2 } } }, + ], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/key-dotted-05", () => { + const input: string = + 'arr-1 = [{a.b = 1}]\narr-2 = ["str", {a.b = 1}]\n\narr-3 = [{a.b = 1}, {a.b = 2}]\narr-4 = ["str", {a.b = 1}, {a.b = 2}]\n'; + const expected: any = { + "arr-1": [{ a: { b: 1 } }], + "arr-2": ["str", { a: { b: 1 } }], + "arr-3": [{ a: { b: 1 } }, { a: { b: 2 } }], + "arr-4": ["str", { a: { b: 1 } }, { a: { b: 2 } }], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/key-dotted-06", () => { + const input: string = "top.dot.dot = [\n\t{dot.dot.dot = 1},\n\t{dot.dot.dot = 2},\n]\n"; + const expected: any = { + top: { dot: { dot: [{ dot: { dot: { dot: 1 } } }, { dot: { dot: { dot: 2 } } }] } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/key-dotted-07", () => { + const input: string = "arr = [\n\t{a.b = [{c.d = 1}]}\n]\n"; + const expected: any = { arr: [{ a: { b: [{ c: { d: 1 } }] } }] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/multiline", () => { + const input: string = + 'tbl_multiline = { a = 1, b = """\nmultiline\n""", c = """and yet\nanother line""", d = 4 }\n'; + const expected: any = { tbl_multiline: { a: 1, b: "multiline\n", c: "and yet\nanother line", d: 4 } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/nest", () => { + const input: string = + "tbl_tbl_empty = { tbl_0 = {} }\ntbl_tbl_val = { tbl_1 = { one = 1 } }\ntbl_arr_tbl = { arr_tbl = [ { one = 1 } ] }\narr_tbl_tbl = [ { tbl = { one = 1 } } ]\n\n# Array-of-array-of-table is interesting because it can only\n# be represented in inline form.\narr_arr_tbl_empty = [ [ {} ] ]\narr_arr_tbl_val = [ [ { one = 1 } ] ]\narr_arr_tbls = [ [ { one = 1 }, { two = 2 } ] ]\n"; + const expected: any = { + arr_arr_tbl_empty: [[{}]], + arr_arr_tbl_val: [[{ one: 1 }]], + arr_arr_tbls: [[{ one: 1 }, { two: 2 }]], + arr_tbl_tbl: [{ tbl: { one: 1 } }], + tbl_arr_tbl: { arr_tbl: [{ one: 1 }] }, + tbl_tbl_empty: { tbl_0: {} }, + tbl_tbl_val: { tbl_1: { one: 1 } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/newline-comment", () => { + const input: string = + '# Identical to newline.toml, but with comments that shouldn\'t affect the\n# results.\n\ntrailing-comma-1 = {#comment\n\t# comment\n\tc = 1,#comment\n\t#comment\n}#comment\ntrailing-comma-2 = { c = 1, }#comment\n\ntbl-1 = {#comment\n\thello = "world",#comment\n\t1 = 2,#comment\n\tarr = [1,#comment\n\t 2,#comment\n\t 3,#comment\n\t ],#comment\n\ttbl = {#comment\n\t\t k = 1,#comment\n\t}#comment\n}#comment\n\ntbl-2 = {#comment\n\tk = """\n\tHello\n\t"""#comment\n}#comment\n'; + const expected: any = { + "tbl-1": { "1": 2, hello: "world", arr: [1, 2, 3], tbl: { k: 1 } }, + "tbl-2": { k: "\tHello\n\t" }, + "trailing-comma-1": { c: 1 }, + "trailing-comma-2": { c: 1 }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/newline", () => { + const input: string = + '# TOML 1.1 supports newlines in inline tables and trailing commas.\n\ntrailing-comma-1 = {\n\tc = 1,\n}\ntrailing-comma-2 = { c = 1, }\n\ntbl-1 = {\n\thello = "world",\n\t1 = 2,\n\tarr = [1,\n\t 2,\n\t 3,\n\t ],\n\ttbl = {\n\t\t k = 1,\n\t}\n}\n\ntbl-2 = {\n\tk = """\n\tHello\n\t"""\n}\n\nno-newline-before-brace = {\na = 1,\nb = 2}\n\nno-newline-before-brace-with-comma = {\na = 1,\nb = 2,}\n'; + const expected: any = { + "no-newline-before-brace": { a: 1, b: 2 }, + "no-newline-before-brace-with-comma": { a: 1, b: 2 }, + "tbl-1": { "1": 2, hello: "world", arr: [1, 2, 3], tbl: { k: 1 } }, + "tbl-2": { k: "\tHello\n\t" }, + "trailing-comma-1": { c: 1 }, + "trailing-comma-2": { c: 1 }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/inline-table/spaces", () => { + const input: string = + '# https://github.com/toml-lang/toml-test/issues/146\nclap-1 = { version = "4" , features = ["derive", "cargo"] }\n\n# Contains some literal tabs!\nclap-2 = { version = "4"\t \t,\t \tfeatures = [ "derive" \t , \t "cargo" ] , nest = { \t "a" = \'x\' , \t \'b\' = [ 1.5 , 9.0 ] } }\n'; + const expected: any = { + "clap-1": { version: "4", features: ["derive", "cargo"] }, + "clap-2": { version: "4", features: ["derive", "cargo"], nest: { a: "x", b: [1.5, 9] } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/integer/float64-max", () => { + const input: string = + "# Maximum and minimum safe float64 natural numbers. Mainly here for\n# -int-as-float.\nmax_int = 9_007_199_254_740_991\nmin_int = -9_007_199_254_740_991\n"; + const expected: any = { max_int: 9007199254740991, min_int: -9007199254740991 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/integer/integer", () => { + const input: string = "answer = 42\nposanswer = +42\nneganswer = -42\nzero = 0\n"; + const expected: any = { answer: 42, neganswer: -42, posanswer: 42, zero: 0 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/integer/literals", () => { + const input: string = + "bin1 = 0b11010110\nbin2 = 0b1_0_1\n\noct1 = 0o01234567\noct2 = 0o755\noct3 = 0o7_6_5\n\nhex1 = 0xDEADBEEF\nhex2 = 0xdeadbeef\nhex3 = 0xdead_beef\nhex4 = 0x00987\n"; + const expected: any = { + bin1: 214, + bin2: 5, + hex1: 3735928559, + hex2: 3735928559, + hex3: 3735928559, + hex4: 2439, + oct1: 342391, + oct2: 493, + oct3: 501, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/integer/underscore", () => { + const input: string = "kilo = 1_000\nx = 1_1_1_1\n"; + const expected: any = { kilo: 1000, x: 1111 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/integer/zero", () => { + const input: string = + "d1 = 0\nd2 = +0\nd3 = -0\n\nh1 = 0x0\nh2 = 0x00\nh3 = 0x00000\n\no1 = 0o0\na2 = 0o00\na3 = 0o00000\n\nb1 = 0b0\nb2 = 0b00\nb3 = 0b00000\n"; + const expected: any = { + a2: 0, + a3: 0, + b1: 0, + b2: 0, + b3: 0, + d1: 0, + d2: 0, + d3: 0, + h1: 0, + h2: 0, + h3: 0, + o1: 0, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/alphanum", () => { + const input: string = + 'alpha = "a"\n123 = "num"\n000111 = "leading"\n10e3 = "false float"\none1two2 = "mixed"\nwith-dash = "dashed"\nunder_score = "___"\n34-11 = 23\n\n[2018_10]\n001 = 1\n\n[a-a-a]\n_ = false\n'; + const expected: any = { + "123": "num", + "000111": "leading", + "10e3": "false float", + "34-11": 23, + alpha: "a", + one1two2: "mixed", + under_score: "___", + "with-dash": "dashed", + "2018_10": { "001": 1 }, + "a-a-a": { _: false }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/case-sensitive", () => { + const input: string = + 'sectioN = "NN"\n\n[section]\nname = "lower"\nNAME = "upper"\nName = "capitalized"\n\n[Section]\nname = "different section!!"\n"μ" = "greek small letter mu"\n"Μ" = "greek capital letter MU"\nM = "latin letter M"\n\n'; + const expected: any = { + sectioN: "NN", + Section: { + M: "latin letter M", + name: "different section!!", + "Μ": "greek capital letter MU", + "μ": "greek small letter mu", + }, + section: { NAME: "upper", Name: "capitalized", name: "lower" }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/dotted-01", () => { + const input: string = 'name.first = "Arthur"\n"name".\'last\' = "Dent"\n\nmany.dots.dot.dot.dot = 42\n'; + const expected: any = { + many: { dots: { dot: { dot: { dot: 42 } } } }, + name: { first: "Arthur", last: "Dent" }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/dotted-02", () => { + const input: string = + "# Note: this file contains literal tab characters.\n\n# Space are ignored, and key parts can be quoted.\ncount.a = 1\ncount . b = 2\n\"count\".\"c\" = 3\n\"count\" . \"d\" = 4\n'count'.'e' = 5\n'count' . 'f' = 6\n\"count\".'g' = 7\n\"count\" . 'h' = 8\ncount.'i' = 9\ncount \t.\t 'j'\t = 10\n\"count\".k = 11\n\"count\" . l = 12\n"; + const expected: any = { + count: { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11, l: 12 }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/dotted-03", () => { + const input: string = + 'top.key = 1\n\n[tbl]\na.b.c = 42.666\n\n[a.few.dots]\npolka.dot = "again?"\npolka.dance-with = "Dot"\n\n'; + const expected: any = { + a: { few: { dots: { polka: { "dance-with": "Dot", dot: "again?" } } } }, + tbl: { a: { b: { c: 42.666 } } }, + top: { key: 1 }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/dotted-04", () => { + const input: string = "top.key = 1\n\n[[arr]]\na.b.c=1\na.b.d=2\n\n[[arr]]\na.b.c=3\na.b.d=4\n\n"; + const expected: any = { + arr: [{ a: { b: { c: 1, d: 2 } } }, { a: { b: { c: 3, d: 4 } } }], + top: { key: 1 }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/dotted-empty", () => { + const input: string = '\'\'.x = "empty.x"\nx."" = "x.empty"\n[a]\n"".\'\' = "empty.empty"\n'; + const expected: any = { + "": { x: "empty.x" }, + a: { "": { "": "empty.empty" } }, + x: { "": "x.empty" }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/empty-01", () => { + const input: string = '"" = "blank"\n'; + const expected: any = { "": "blank" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/empty-02", () => { + const input: string = "'' = \"blank\"\n"; + const expected: any = { "": "blank" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/empty-03", () => { + const input: string = "''=0\n"; + const expected: any = { "": 0 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/equals-nospace", () => { + const input: string = "answer=42\n"; + const expected: any = { answer: 42 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/escapes", () => { + const input: string = + '"\\n" = "newline"\n"\\b" = "bell"\n"\\u00c0" = "latin capital letter A with grave"\n"\\"" = "just a quote"\n\n["backsp\\b\\b"]\n\n["\\"quoted\\""]\nquote = true\n\n["a.b"."\\u00c0"]\n'; + const expected: any = { + "\b": "bell", + "\n": "newline", + '"': "just a quote", + "backsp\b\b": {}, + "À": "latin capital letter A with grave", + '"quoted"': { quote: true }, + "a.b": { "À": {} }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/like-date", () => { + const input: string = + '# \'-\' is a valid character in keys: make a key that looks like a date.\n2001-02-03 = 1\n"2001-02-04" = 2\n\'2001-02-05\' = 3\n\n# Also include datetime and time for good measure; these need to be quoted as\n# \':\' isn\'t a valid bare key.\n"2001-02-06T15:16:17+01:00" = 4\n"2001-02-07T15:16:17" = 5\n"15:16:17" = 6\n\n# Dotted keys\na.2001-02-08 = 7\na.2001-02-09.2001-02-10 = 8\n2001-02-11.a.2001-02-12 = 9\n\n# Table names\n[2002-01-02]\nk = 10\n\n[2002-01-02.2024-01-03]\nk = 11\n\n[[2002-01-04]]\nk = 12\n'; + const expected: any = { + "15:16:17": 6, + "2001-02-03": 1, + "2001-02-04": 2, + "2001-02-05": 3, + "2001-02-06T15:16:17+01:00": 4, + "2001-02-07T15:16:17": 5, + "2002-01-04": [{ k: 12 }], + "2001-02-11": { a: { "2001-02-12": 9 } }, + "2002-01-02": { k: 10, "2024-01-03": { k: 11 } }, + a: { "2001-02-08": 7, "2001-02-09": { "2001-02-10": 8 } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/numeric-01", () => { + const input: string = "1 = true\n"; + const expected: any = { "1": true }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/numeric-02", () => { + const input: string = "1.2 = true\n"; + const expected: any = { "1": { "2": true } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/numeric-03", () => { + const input: string = "0123 = true\n"; + const expected: any = { "0123": true }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/numeric-04", () => { + const input: string = "01.23 = true\n"; + const expected: any = { "01": { "23": true } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/numeric-05", () => { + const input: string = "23.01 = true\n"; + const expected: any = { "23": { "01": true } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/numeric-06", () => { + const input: string = "-1 = true\n"; + const expected: any = { "-1": true }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/numeric-07", () => { + const input: string = "-01 = true\n"; + const expected: any = { "-01": true }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/numeric-08", () => { + const input: string = "1 = 'one'\n01 = 'zero one'\n"; + const expected: any = { "1": "one", "01": "zero one" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/quoted-dots", () => { + const input: string = + 'plain = 1\n"with.dot" = 2\n\n[plain_table]\nplain = 3\n"with.dot" = 4\n\n[table.withdot]\nplain = 5\n"key.with.dots" = 6\n"escaped\\u002edot" = 7\n'; + const expected: any = { + plain: 1, + "with.dot": 2, + plain_table: { plain: 3, "with.dot": 4 }, + table: { withdot: { "escaped.dot": 7, "key.with.dots": 6, plain: 5 } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/quoted-unicode", () => { + const input: string = + '\n"\\u0000" = "null"\n\'\\u0000\' = "different key"\n"\\u0008 \\u000c \\U00000041 \\u007f \\u0080 \\u00ff \\ud7ff \\ue000 \\uffff \\U00010000 \\U0010ffff" = "escaped key"\n\n"~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿" = "basic key"\n\'l ~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿\' = "literal key"\n'; + const expected: any = { + "\u0000": "null", + "\b \f A \u007f \u0080 ÿ ퟿  ￿ 𐀀 􏿿": "escaped key", + "\\u0000": "different key", + "l ~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿": "literal key", + "~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿": "basic key", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/space", () => { + const input: string = + '# Keep whitespace inside quotes keys at all positions.\n"a b" = 1\n" c d " = 2\n" much \t\t whitespace \t\\n \\r\\n " = 3\n\n[ " tbl " ]\n"\\ttab\\ttab\\t" = "tab"\n'; + const expected: any = { + " much \t\t whitespace \t\n \r\n ": 3, + " c d ": 2, + "a b": 1, + " tbl ": { "\ttab\ttab\t": "tab" }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/special-chars", () => { + const input: string = '"=~!@$^&*()_+-`1234567890[]|/?><.,;:\'=" = 1\n'; + const expected: any = { "=~!@$^&*()_+-`1234567890[]|/?><.,;:'=": 1 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/special-word", () => { + const input: string = 'false = false\ntrue = 1\ninf = 100000000\nnan = "ceci n\'est pas un nombre"\n\n'; + const expected: any = { false: false, inf: 100000000, nan: "ceci n'est pas un nombre", true: 1 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/start", () => { + const input: string = + "# Table and keys can start with any character; there is no requirement for it to\n# start with a letter.\n\n[-key]\n-key = 1\n\n[_key]\n_key = 2\n\n[1key]\n1key = 3\n\n[-]\n- = 4\n\n[_]\n_ = 5\n\n[1] \n1 = 6\n\n[---] \n--- = 7\n\n[___]\n___ = 8\n\n[111]\n111 = 9\n\n[inline]\n--- = {--- = 10, ___ = 11, 111 = 12}\n"; + const expected: any = { + "1": { "1": 6 }, + "111": { "111": 9 }, + "-": { "-": 4 }, + "---": { "---": 7 }, + "-key": { "-key": 1 }, + "1key": { "1key": 3 }, + _: { _: 5 }, + ___: { ___: 8 }, + _key: { _key: 2 }, + inline: { "---": { "111": 12, "---": 10, ___: 11 } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/key/zero", () => { + const input: string = "0=0\n"; + const expected: any = { "0": 0 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/multibyte", () => { + const input: string = + '# Test multibyte throughout\n\n# Tèƨƭ ƒïℓè ƒôř TÓM£\n# Óñℓ¥ ƭλïƨ ôñè ƭřïèƨ ƭô è₥úℓáƭè á TÓM£ ƒïℓè ωřïƭƭèñ β¥ á úƨèř ôƒ ƭλè ƙïñδ ôƒ ƥářƨèř ωřïƭèřƨ ƥřôβáβℓ¥ λáƭè\n\n[\'𝐭𝐛𝐥\']\nstring = "𝓼𝓽𝓻𝓲𝓷𝓰 - #" # " 𝓼𝓽𝓻𝓲𝓷𝓰\n\t[\'𝐭𝐛𝐥\'.sub]\n\t\'𝕒𝕣𝕣𝕒𝕪\' = [ "] ", " # "] # ] 𝓪𝓻𝓻𝓪𝔂\n\t\'𝕒𝕣𝕣𝕒𝕪𝟚\' = [ "Tèƨƭ #11 ]ƥřôƲèδ ƭλáƭ", "Éжƥèřï₥èñƭ #9 ωáƨ á ƨúççèƨƨ" ]\n\t# Ýôú δïδñ\'ƭ ƭλïñƙ ïƭ\'δ áƨ èáƨ¥ áƨ çλúçƙïñϱ ôúƭ ƭλè ℓáƨƭ #, δïδ ¥ôú?\n\tanother_test_string = "§á₥è ƭλïñϱ, βúƭ ωïƭλ á ƨƭřïñϱ #"\n\tescapes = " Âñδ ωλèñ \\"\'ƨ ářè ïñ ƭλè ƨƭřïñϱ, áℓôñϱ ωïƭλ # \\"" # "áñδ çô₥₥èñƭƨ ářè ƭλèřè ƭôô"\n\t# Tλïñϱƨ ωïℓℓ ϱèƭ λářδèř\n\t\t[\'𝐭𝐛𝐥\'.sub."βïƭ#"]\n\t\t"ωλáƭ?" = "Ýôú δôñ\'ƭ ƭλïñƙ ƨô₥è úƨèř ωôñ\'ƭ δô ƭλáƭ?"\n\t\tmulti_line_array = [\n\t\t\t"]",\n\t\t\t# ] Óλ ¥èƨ Ì δïδ\n\t\t\t]\n'; + const expected: any = { + "𝐭𝐛𝐥": { + string: "𝓼𝓽𝓻𝓲𝓷𝓰 - #", + sub: { + another_test_string: "§á₥è ƭλïñϱ, βúƭ ωïƭλ á ƨƭřïñϱ #", + escapes: ' Âñδ ωλèñ "\'ƨ ářè ïñ ƭλè ƨƭřïñϱ, áℓôñϱ ωïƭλ # "', + "βïƭ#": { + multi_line_array: ["]"], + "ωλáƭ?": "Ýôú δôñ'ƭ ƭλïñƙ ƨô₥è úƨèř ωôñ'ƭ δô ƭλáƭ?", + }, + "𝕒𝕣𝕣𝕒𝕪": ["] ", " # "], + "𝕒𝕣𝕣𝕒𝕪𝟚": ["Tèƨƭ #11 ]ƥřôƲèδ ƭλáƭ", "Éжƥèřï₥èñƭ #9 ωáƨ á ƨúççèƨƨ"], + }, + }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/newline-crlf", () => { + const input: string = 'os = "DOS"\r\nnewline = "crlf"\r\n'; + const expected: any = { newline: "crlf", os: "DOS" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/newline-lf", () => { + const input: string = 'os = "unix"\nnewline = "lf"\n'; + const expected: any = { newline: "lf", os: "unix" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-0", () => { + const input: string = + '# This is a full-line comment\nkey = "value" # This is a comment at the end of a line\nanother = "# This is not a comment"\n'; + const expected: any = { another: "# This is not a comment", key: "value" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-1", () => { + const input: string = 'key = "value"\n'; + const expected: any = { key: "value" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-10", () => { + const input: string = + '# RECOMMENDED\n\napple.type = "fruit"\napple.skin = "thin"\napple.color = "red"\n\norange.type = "fruit"\norange.skin = "thick"\norange.color = "orange"\n'; + const expected: any = { + apple: { color: "red", skin: "thin", type: "fruit" }, + orange: { color: "orange", skin: "thick", type: "fruit" }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-11", () => { + const input: string = '3.14159 = "pi"\n'; + const expected: any = { "3": { "14159": "pi" } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-12", () => { + const input: string = 'str = "I\'m a string. \\"You can quote me\\". Name\\tJos\\xE9\\nLocation\\tSF."\n'; + const expected: any = { str: 'I\'m a string. "You can quote me". Name\tJosé\nLocation\tSF.' }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-13", () => { + const input: string = 'str1 = """\nRoses are red\nViolets are blue"""\n'; + const expected: any = { str1: "Roses are red\nViolets are blue" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-14", () => { + const input: string = + '# On a Unix system, the above multi-line string will most likely be the same as:\nstr2 = "Roses are red\\nViolets are blue"\n\n# On a Windows system, it will most likely be equivalent to:\nstr3 = "Roses are red\\r\\nViolets are blue"\n'; + const expected: any = { + str2: "Roses are red\nViolets are blue", + str3: "Roses are red\r\nViolets are blue", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-15", () => { + const input: string = + '# The following strings are byte-for-byte equivalent:\nstr1 = "The quick brown fox jumps over the lazy dog."\n\nstr2 = """\nThe quick brown \\\n\n\n fox jumps over \\\n the lazy dog."""\n\nstr3 = """\\\n The quick brown \\\n fox jumps over \\\n the lazy dog.\\\n """\n'; + const expected: any = { + str1: "The quick brown fox jumps over the lazy dog.", + str2: "The quick brown fox jumps over the lazy dog.", + str3: "The quick brown fox jumps over the lazy dog.", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-16", () => { + const input: string = + 'str4 = """Here are two quotation marks: "". Simple enough."""\n# str5 = """Here are three quotation marks: """.""" # INVALID\nstr5 = """Here are three quotation marks: ""\\"."""\nstr6 = """Here are fifteen quotation marks: ""\\"""\\"""\\"""\\"""\\"."""\n\n# "This," she said, "is just a pointless statement."\nstr7 = """"This," she said, "is just a pointless statement.""""\n'; + const expected: any = { + str4: 'Here are two quotation marks: "". Simple enough.', + str5: 'Here are three quotation marks: """.', + str6: 'Here are fifteen quotation marks: """"""""""""""".', + str7: '"This," she said, "is just a pointless statement."', + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-17", () => { + const input: string = + "# What you see is what you get.\nwinpath = 'C:\\Users\\nodejs\\templates'\nwinpath2 = '\\\\ServerX\\admin$\\system32\\'\nquoted = 'Tom \"Dubs\" Preston-Werner'\nregex = '<\\i\\c*\\s*>'\n"; + const expected: any = { + quoted: 'Tom "Dubs" Preston-Werner', + regex: "<\\i\\c*\\s*>", + winpath: "C:\\Users\\nodejs\\templates", + winpath2: "\\\\ServerX\\admin$\\system32\\", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-18", () => { + const input: string = + "regex2 = '''I [dw]on't need \\d{2} apples'''\nlines = '''\nThe first newline is\ntrimmed in literal strings.\n All other whitespace\n is preserved.\n'''\n"; + const expected: any = { + lines: "The first newline is\ntrimmed in literal strings.\n All other whitespace\n is preserved.\n", + regex2: "I [dw]on't need \\d{2} apples", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-19", () => { + const input: string = + "quot15 = '''Here are fifteen quotation marks: \"\"\"\"\"\"\"\"\"\"\"\"\"\"\"'''\n\n# apos15 = '''Here are fifteen apostrophes: '''''''''''''''''' # INVALID\napos15 = \"Here are fifteen apostrophes: '''''''''''''''\"\n\n# 'That,' she said, 'is still pointless.'\nstr = ''''That,' she said, 'is still pointless.''''\n"; + const expected: any = { + apos15: "Here are fifteen apostrophes: '''''''''''''''", + quot15: 'Here are fifteen quotation marks: """""""""""""""', + str: "'That,' she said, 'is still pointless.'", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-20", () => { + const input: string = "int1 = +99\nint2 = 42\nint3 = 0\nint4 = -17\n"; + const expected: any = { int1: 99, int2: 42, int3: 0, int4: -17 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-21", () => { + const input: string = + "int5 = 1_000\nint6 = 5_349_221\nint7 = 53_49_221 # Indian number system grouping\nint8 = 1_2_3_4_5 # VALID but discouraged\n"; + const expected: any = { int5: 1000, int6: 5349221, int7: 5349221, int8: 12345 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-22", () => { + const input: string = + "# hexadecimal with prefix `0x`\nhex1 = 0xDEADBEEF\nhex2 = 0xdeadbeef\nhex3 = 0xdead_beef\n\n# octal with prefix `0o`\noct1 = 0o01234567\noct2 = 0o755 # useful for Unix file permissions\n\n# binary with prefix `0b`\nbin1 = 0b11010110\n"; + const expected: any = { + bin1: 214, + hex1: 3735928559, + hex2: 3735928559, + hex3: 3735928559, + oct1: 342391, + oct2: 493, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-23", () => { + const input: string = + "# fractional\nflt1 = +1.0\nflt2 = 3.1415\nflt3 = -0.01\n\n# exponent\nflt4 = 5e+22\nflt5 = 1e06\nflt6 = -2E-2\n\n# both\nflt7 = 6.626e-34\n"; + const expected: any = { + flt1: 1, + flt2: 3.1415, + flt3: -0.01, + flt4: 5e22, + flt5: 1000000, + flt6: -0.02, + flt7: 6.626e-34, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-24", () => { + const input: string = "flt8 = 224_617.445_991_228\n"; + const expected: any = { flt8: 224617.445991228 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-25", () => { + const input: string = + "# infinity\nsf1 = inf # positive infinity\nsf2 = +inf # positive infinity\nsf3 = -inf # negative infinity\n\n# not a number\nsf4 = nan # actual sNaN/qNaN encoding is implementation-specific\nsf5 = +nan # same as `nan`\nsf6 = -nan # valid, actual encoding is implementation-specific\n"; + const expected: any = { sf1: Infinity, sf2: Infinity, sf3: -Infinity, sf4: NaN, sf5: NaN, sf6: NaN }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-26", () => { + const input: string = "bool1 = true\nbool2 = false\n"; + const expected: any = { bool1: true, bool2: false }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-27", () => { + const input: string = + "odt1 = 1979-05-27T07:32:00Z\nodt2 = 1979-05-27T00:32:00-07:00\nodt3 = 1979-05-27T00:32:00.5-07:00\nodt4 = 1979-05-27T00:32:00.999-07:00\n"; + const expected: any = { + odt1: dt("datetime", "1979-05-27T07:32:00Z"), + odt2: dt("datetime", "1979-05-27T00:32:00-07:00"), + odt3: dt("datetime", "1979-05-27T00:32:00.5-07:00"), + odt4: dt("datetime", "1979-05-27T00:32:00.999-07:00"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-28", () => { + const input: string = "odt4 = 1979-05-27 07:32:00Z\n"; + const expected: any = { odt4: dt("datetime", "1979-05-27T07:32:00Z") }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-29", () => { + const input: string = "odt5 = 1979-05-27 07:32Z\nodt6 = 1979-05-27 07:32-07:00\n"; + const expected: any = { + odt5: dt("datetime", "1979-05-27T07:32:00Z"), + odt6: dt("datetime", "1979-05-27T07:32:00-07:00"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-3", () => { + const input: string = 'key = "value"\nbare_key = "value"\nbare-key = "value"\n1234 = "value"\n'; + const expected: any = { "1234": "value", "bare-key": "value", bare_key: "value", key: "value" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-30", () => { + const input: string = "ldt1 = 1979-05-27T07:32:00\nldt2 = 1979-05-27T07:32:00.5\nldt3 = 1979-05-27T00:32:00.999\n"; + const expected: any = { + ldt1: dt("datetime-local", "1979-05-27T07:32:00"), + ldt2: dt("datetime-local", "1979-05-27T07:32:00.5"), + ldt3: dt("datetime-local", "1979-05-27T00:32:00.999"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-31", () => { + const input: string = "ldt3 = 1979-05-27T07:32\n"; + const expected: any = { ldt3: dt("datetime-local", "1979-05-27T07:32:00") }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-32", () => { + const input: string = "ld1 = 1979-05-27\n"; + const expected: any = { ld1: dt("date-local", "1979-05-27") }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-33", () => { + const input: string = "lt1 = 07:32:00\nlt2 = 00:32:00.5\nlt3 = 00:32:00.999\n"; + const expected: any = { + lt1: dt("time-local", "07:32:00"), + lt2: dt("time-local", "00:32:00.5"), + lt3: dt("time-local", "00:32:00.999"), + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-34", () => { + const input: string = "lt3 = 07:32\n"; + const expected: any = { lt3: dt("time-local", "07:32:00") }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-35", () => { + const input: string = + 'integers = [ 1, 2, 3 ]\ncolors = [ "red", "yellow", "green" ]\nnested_arrays_of_ints = [ [ 1, 2 ], [3, 4, 5] ]\nnested_mixed_array = [ [ 1, 2 ], ["a", "b", "c"] ]\nstring_array = [ "all", \'strings\', """are the same""", \'\'\'type\'\'\' ]\n\n# Mixed-type arrays are allowed\nnumbers = [ 0.1, 0.2, 0.5, 1, 2, 5 ]\ncontributors = [\n "Foo Bar ",\n { name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" }\n]\n'; + const expected: any = { + colors: ["red", "yellow", "green"], + contributors: [ + "Foo Bar ", + { + email: "bazqux@example.com", + name: "Baz Qux", + url: "https://example.com/bazqux", + }, + ], + integers: [1, 2, 3], + nested_arrays_of_ints: [ + [1, 2], + [3, 4, 5], + ], + nested_mixed_array: [ + [1, 2], + ["a", "b", "c"], + ], + numbers: [0.1, 0.2, 0.5, 1, 2, 5], + string_array: ["all", "strings", "are the same", "type"], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-36", () => { + const input: string = "integers2 = [\n 1, 2, 3\n]\n\nintegers3 = [\n 1,\n 2, # this is ok\n]\n"; + const expected: any = { integers2: [1, 2, 3], integers3: [1, 2] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-37", () => { + const input: string = "[table]\n"; + const expected: any = { table: {} }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-38", () => { + const input: string = + '[table-1]\nkey1 = "some string"\nkey2 = 123\n\n[table-2]\nkey1 = "another string"\nkey2 = 456\n'; + const expected: any = { + "table-1": { key1: "some string", key2: 123 }, + "table-2": { key1: "another string", key2: 456 }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-39", () => { + const input: string = '[dog."tater.man"]\ntype.name = "pug"\n'; + const expected: any = { dog: { "tater.man": { type: { name: "pug" } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-4", () => { + const input: string = + '"127.0.0.1" = "value"\n"character encoding" = "value"\n"ʎǝʞ" = "value"\n\'key2\' = "value"\n\'quoted "value"\' = "value"\n'; + const expected: any = { + "127.0.0.1": "value", + "character encoding": "value", + key2: "value", + 'quoted "value"': "value", + "ʎǝʞ": "value", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-40", () => { + const input: string = + "[a.b.c] # this is best practice\n[ d.e.f ] # same as [d.e.f]\n[ g . h . i ] # same as [g.h.i]\n[ j . \"ʞ\" . 'l' ] # same as [j.\"ʞ\".'l']\n"; + const expected: any = { + a: { b: { c: {} } }, + d: { e: { f: {} } }, + g: { h: { i: {} } }, + j: { "ʞ": { l: {} } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-41", () => { + const input: string = + "# [x] you\n# [x.y] don't\n# [x.y.z] need these\n[x.y.z.w] # for this to work\n\n[x] # defining a super-table afterward is ok\n"; + const expected: any = { x: { y: { z: { w: {} } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-42", () => { + const input: string = "# VALID BUT DISCOURAGED\n[fruit.apple]\n[animal]\n[fruit.orange]\n"; + const expected: any = { animal: {}, fruit: { apple: {}, orange: {} } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-43", () => { + const input: string = "# RECOMMENDED\n[fruit.apple]\n[fruit.orange]\n[animal]\n"; + const expected: any = { animal: {}, fruit: { apple: {}, orange: {} } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-44", () => { + const input: string = + '# Top-level table begins.\nname = "Fido"\nbreed = "pug"\n\n# Top-level table ends.\n[owner]\nname = "Regina Dogman"\nmember_since = 1999-08-04\n'; + const expected: any = { + breed: "pug", + name: "Fido", + owner: { member_since: dt("date-local", "1999-08-04"), name: "Regina Dogman" }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-45", () => { + const input: string = + 'fruit.apple.color = "red"\n# Defines a table named fruit\n# Defines a table named fruit.apple\n\nfruit.apple.taste.sweet = true\n# Defines a table named fruit.apple.taste\n# fruit and fruit.apple were already created\n'; + const expected: any = { fruit: { apple: { color: "red", taste: { sweet: true } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-46", () => { + const input: string = + '[fruit]\napple.color = "red"\napple.taste.sweet = true\n\n# [fruit.apple] # INVALID\n# [fruit.apple.taste] # INVALID\n\n[fruit.apple.texture] # you can add sub-tables\nsmooth = true\n'; + const expected: any = { + fruit: { apple: { color: "red", taste: { sweet: true }, texture: { smooth: true } } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-47", () => { + const input: string = + 'name = { first = "Tom", last = "Preston-Werner" }\npoint = {x=1, y=2}\nanimal = { type.name = "pug" }\ncontact = {\n personal = {\n name = "Donald Duck",\n email = "donald@duckburg.com",\n },\n work = {\n name = "Coin cleaner",\n email = "donald@ScroogeCorp.com",\n },\n}\n'; + const expected: any = { + animal: { type: { name: "pug" } }, + contact: { + personal: { email: "donald@duckburg.com", name: "Donald Duck" }, + work: { email: "donald@ScroogeCorp.com", name: "Coin cleaner" }, + }, + name: { first: "Tom", last: "Preston-Werner" }, + point: { x: 1, y: 2 }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-48", () => { + const input: string = + '[name]\nfirst = "Tom"\nlast = "Preston-Werner"\n\n[point]\nx = 1\ny = 2\n\n[animal]\ntype.name = "pug"\n\n[contact.personal]\nname = "Donald Duck"\nemail = "donald@duckburg.com"\n\n[contact.work]\nname = "Coin cleaner"\nemail = "donald@ScroogeCorp.com"\n'; + const expected: any = { + animal: { type: { name: "pug" } }, + contact: { + personal: { email: "donald@duckburg.com", name: "Donald Duck" }, + work: { email: "donald@ScroogeCorp.com", name: "Coin cleaner" }, + }, + name: { first: "Tom", last: "Preston-Werner" }, + point: { x: 1, y: 2 }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-49", () => { + const input: string = '[product]\ntype = { name = "Nail" }\n# type.edible = false # INVALID\n'; + const expected: any = { product: { type: { name: "Nail" } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-50", () => { + const input: string = '[product]\ntype.name = "Nail"\n# type = { edible = false } # INVALID\n'; + const expected: any = { product: { type: { name: "Nail" } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-51", () => { + const input: string = + '[[product]]\nname = "Hammer"\nsku = 738594937\n\n[[product]] # empty table within the array\n\n[[product]]\nname = "Nail"\nsku = 284758393\n\ncolor = "gray"\n'; + const expected: any = { + product: [{ name: "Hammer", sku: 738594937 }, {}, { color: "gray", name: "Nail", sku: 284758393 }], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-52", () => { + const input: string = + '[[fruits]]\nname = "apple"\n\n[fruits.physical] # subtable\ncolor = "red"\nshape = "round"\n\n[[fruits.varieties]] # nested array of tables\nname = "red delicious"\n\n[[fruits.varieties]]\nname = "granny smith"\n\n\n[[fruits]]\nname = "banana"\n\n[[fruits.varieties]]\nname = "plantain"\n'; + const expected: any = { + fruits: [ + { + name: "apple", + physical: { color: "red", shape: "round" }, + varieties: [{ name: "red delicious" }, { name: "granny smith" }], + }, + { name: "banana", varieties: [{ name: "plantain" }] }, + ], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-53", () => { + const input: string = + "points = [ { x = 1, y = 2, z = 3 },\n { x = 7, y = 8, z = 9 },\n { x = 2, y = 4, z = 8 } ]\n"; + const expected: any = { + points: [ + { x: 1, y: 2, z: 3 }, + { x: 7, y: 8, z: 9 }, + { x: 2, y: 4, z: 8 }, + ], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-6", () => { + const input: string = + 'name = "Orange"\nphysical.color = "orange"\nphysical.shape = "round"\nsite."google.com" = true\n'; + const expected: any = { + name: "Orange", + physical: { color: "orange", shape: "round" }, + site: { "google.com": true }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-7", () => { + const input: string = + 'fruit.name = "banana" # this is best practice\nfruit. color = "yellow" # same as fruit.color\nfruit . flavor = "banana" # same as fruit.flavor\n'; + const expected: any = { fruit: { color: "yellow", flavor: "banana", name: "banana" } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-8", () => { + const input: string = + '# This makes the key "fruit" into a table.\nfruit.apple.smooth = true\n\n# So then you can add to the table "fruit" like so:\nfruit.orange = 2\n'; + const expected: any = { fruit: { orange: 2, apple: { smooth: true } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-1.1.0/common-9", () => { + const input: string = + '# VALID BUT DISCOURAGED\n\napple.type = "fruit"\norange.type = "fruit"\n\napple.skin = "thin"\norange.skin = "thick"\n\napple.color = "red"\norange.color = "orange"\n'; + const expected: any = { + apple: { color: "red", skin: "thin", type: "fruit" }, + orange: { color: "orange", skin: "thick", type: "fruit" }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-example-1-compact", () => { + const input: string = + '#Useless spaces eliminated.\ntitle="TOML Example"\n[owner]\nname="Lance Uppercut"\ndob=1979-05-27T07:32:00-08:00#First class dates\n[database]\nserver="192.168.1.1"\nports=[8001,8001,8002]\nconnection_max=5000\nenabled=true\n[servers]\n[servers.alpha]\nip="10.0.0.1"\ndc="eqdc10"\n[servers.beta]\nip="10.0.0.2"\ndc="eqdc10"\n[clients]\ndata=[["gamma","delta"],[1,2]]\nhosts=[\n"alpha",\n"omega"\n]\n'; + const expected: any = { + title: "TOML Example", + clients: { + data: [ + ["gamma", "delta"], + [1, 2], + ], + hosts: ["alpha", "omega"], + }, + database: { + connection_max: 5000, + enabled: true, + server: "192.168.1.1", + ports: [8001, 8001, 8002], + }, + owner: { dob: dt("datetime", "1979-05-27T07:32:00-08:00"), name: "Lance Uppercut" }, + servers: { + alpha: { dc: "eqdc10", ip: "10.0.0.1" }, + beta: { dc: "eqdc10", ip: "10.0.0.2" }, + }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/spec-example-1", () => { + const input: string = + '# This is a TOML document. Boom.\n\ntitle = "TOML Example"\n\n[owner]\nname = "Lance Uppercut"\ndob = 1979-05-27T07:32:00-08:00 # First class dates? Why not?\n\n[database]\nserver = "192.168.1.1"\nports = [ 8001, 8001, 8002 ]\nconnection_max = 5000\nenabled = true\n\n[servers]\n\n # You can indent as you please. Tabs or spaces. TOML don\'t care.\n [servers.alpha]\n ip = "10.0.0.1"\n dc = "eqdc10"\n\n [servers.beta]\n ip = "10.0.0.2"\n dc = "eqdc10"\n\n[clients]\ndata = [ ["gamma", "delta"], [1, 2] ]\n\n# Line breaks are OK when inside arrays\nhosts = [\n "alpha",\n "omega"\n]\n'; + const expected: any = { + title: "TOML Example", + clients: { + data: [ + ["gamma", "delta"], + [1, 2], + ], + hosts: ["alpha", "omega"], + }, + database: { + connection_max: 5000, + enabled: true, + server: "192.168.1.1", + ports: [8001, 8001, 8002], + }, + owner: { dob: dt("datetime", "1979-05-27T07:32:00-08:00"), name: "Lance Uppercut" }, + servers: { + alpha: { dc: "eqdc10", ip: "10.0.0.1" }, + beta: { dc: "eqdc10", ip: "10.0.0.2" }, + }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/basic-escape-01", () => { + const input: string = '# Escape "\ntest = "\\"one\\""\n'; + const expected: any = { test: '"one"' }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/basic-escape-02", () => { + const input: string = '# Escape \\ and then "\ntest = "\\\\\\"one"\n'; + const expected: any = { test: '\\"one' }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/basic-escape-03", () => { + const input: string = '# Escape \\ four times and then "\ntest = "\\\\\\\\\\\\\\\\\\"one"\n'; + const expected: any = { test: '\\\\\\\\"one' }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/empty", () => { + const input: string = 'answer = ""\n'; + const expected: any = { answer: "" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/ends-in-whitespace-escape", () => { + const input: string = 'beee = """\nheeee\ngeeee\\ \n\n\n """\n'; + const expected: any = { beee: "heeee\ngeeee" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/escape-esc", () => { + const input: string = 'esc = "\\e There is no escape! \\e"\n'; + const expected: any = { esc: "\u001b There is no escape! \u001b" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/escape-tricky", () => { + const input: string = + 'end_esc = "String does not end here\\" but ends here\\\\"\nlit_end_esc = \'String ends here\\\'\n\nmultiline_unicode = """\n\\u00a0"""\n\nmultiline_not_unicode = """\n\\\\u0041"""\n\nmultiline_end_esc = """When will it end? \\"""...""\\" should be here\\""""\n\nlit_multiline_not_unicode = \'\'\'\n\\u007f\'\'\'\n\nlit_multiline_end = \'\'\'There is no escape\\\'\'\'\n'; + const expected: any = { + end_esc: 'String does not end here" but ends here\\', + lit_end_esc: "String ends here\\", + lit_multiline_end: "There is no escape\\", + lit_multiline_not_unicode: "\\u007f", + multiline_end_esc: 'When will it end? """...""" should be here"', + multiline_not_unicode: "\\u0041", + multiline_unicode: " ", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/escaped-escape", () => { + const input: string = 'answer = "\\\\x64"\n'; + const expected: any = { answer: "\\x64" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/escapes", () => { + const input: string = + 'backspace = "|\\b."\ntab = "|\\t."\nnewline = "|\\n."\nformfeed = "|\\f."\ncarriage = "|\\r."\nquote = "|\\"."\nbackslash = "|\\\\."\ndelete = "|\\u007F."\nunitseparator = "|\\u001F."\n\n# \\u is escaped, so should NOT be interperted as a \\u escape.\nnotunicode1 = "|\\\\u."\nnotunicode2 = "|\\u005Cu."\nnotunicode3 = "|\\\\u0075."\nnotunicode4 = "|\\\\\\u0075."\n'; + const expected: any = { + backslash: "|\\.", + backspace: "|\b.", + carriage: "|\r.", + delete: "|\u007f.", + formfeed: "|\f.", + newline: "|\n.", + notunicode1: "|\\u.", + notunicode2: "|\\u.", + notunicode3: "|\\u0075.", + notunicode4: "|\\u.", + quote: '|".', + tab: "|\t.", + unitseparator: "|\u001f.", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/hex-escape", () => { + const input: string = + '# \\x for the first 255 codepoints\n\nwhitespace = "\\x20 \\x09 \\x1b \\x0d\\x0a"\nbs = "\\x7f"\nnul = "\\x00"\nhello = "\\x68\\x65\\x6c\\x6c\\x6f\\x0a"\nhigher-than-127 = "S\\xf8rmirb\\xe6ren"\n\nmultiline = """\n\\x20 \\x09 \\x1b \\x0d\\x0a\n\\x7f\n\\x00\n\\x68\\x65\\x6c\\x6c\\x6f\\x0a\n\\x53\\xF8\\x72\\x6D\\x69\\x72\\x62\\xE6\\x72\\x65\\x6E\n"""\n\n# Not inside literals.\nliteral = \'\\x20 \\x09 \\x0d\\x0a\'\nmultiline-literal = \'\'\'\n\\x20 \\x09 \\x0d\\x0a\n\'\'\'\n'; + const expected: any = { + bs: "\u007f", + hello: "hello\n", + "higher-than-127": "Sørmirbæren", + literal: "\\x20 \\x09 \\x0d\\x0a", + multiline: " \t \u001b \r\n\n\u007f\n\u0000\nhello\n\nSørmirbæren\n", + "multiline-literal": "\\x20 \\x09 \\x0d\\x0a\n", + nul: "\u0000", + whitespace: " \t \u001b \r\n", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/multibyte-escape", () => { + const input: string = + '# Test each multibyte length: 2, 3, and 4 bytes:\n# ɑ € 𐫱\n\nbasic-1 = "\\u0251 \\u20ac \\U00010AF1 \\u0251\\u20ac\\U00010AF1"\nml-basic-1 = """\\u0251 \\u20ac \\U00010AF1 \\u0251\\u20ac\\U00010AF1"""\n\n# Again, but only using \\U\nbasic-2 = "\\U00000251 \\U000020ac \\U00010AF1 \\U00000251\\U000020ac\\U00010AF1"\nml-basic-2 = """\\U00000251 \\U000020ac \\U00010AF1 \\U00000251\\U000020ac\\U00010AF1"""\n'; + const expected: any = { + "basic-1": "ɑ € 𐫱 ɑ€𐫱", + "ml-basic-1": "ɑ € 𐫱 ɑ€𐫱", + "basic-2": "ɑ € 𐫱 ɑ€𐫱", + "ml-basic-2": "ɑ € 𐫱 ɑ€𐫱", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/multibyte", () => { + const input: string = + "# Test each multibyte length: 2, 3, and 4 bytes:\n# ɑ € 𐫱\n\nbasic = \"ɑ € 𐫱 ɑ€𐫱\"\nraw = 'ɑ € 𐫱 ɑ€𐫱'\nml-basic = \"\"\"ɑ € 𐫱 ɑ€𐫱\"\"\"\nml-raw = '''ɑ € 𐫱 ɑ€𐫱'''\n"; + const expected: any = { + basic: "ɑ € 𐫱 ɑ€𐫱", + "ml-basic": "ɑ € 𐫱 ɑ€𐫱", + "ml-raw": "ɑ € 𐫱 ɑ€𐫱", + raw: "ɑ € 𐫱 ɑ€𐫱", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/multiline-empty", () => { + const input: string = + 'empty-1 = """"""\n\n# A newline immediately following the opening delimiter will be trimmed.\nempty-2 = """\n"""\n\n# \\ at the end of line trims newlines as well; note that last \\ is followed by\n# two spaces, which are ignored.\nempty-3 = """\\\n """\nempty-4 = """\\\n \\\n \\ \n """\n\n'; + const expected: any = { "empty-1": "", "empty-2": "", "empty-3": "", "empty-4": "" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/multiline-escaped-crlf", () => { + const input: string = + '# The following line should be an unescaped backslash followed by a Windows\r\n# newline sequence ("\\r\\n")\r\n0="""\\\r\n"""\r\n'; + const expected: any = { "0": "" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/multiline-quotes", () => { + const input: string = + '# Make sure that quotes inside multiline strings are allowed, including right\n# after the opening \'\'\'/""" and before the closing \'\'\'/"""\n\nlit_one = \'\'\'\'one quote\'\'\'\'\nlit_two = \'\'\'\'\'two quotes\'\'\'\'\'\nlit_one_space = \'\'\' \'one quote\' \'\'\'\nlit_two_space = \'\'\' \'\'two quotes\'\' \'\'\'\n\none = """"one quote""""\ntwo = """""two quotes"""""\none_space = """ "one quote" """\ntwo_space = """ ""two quotes"" """\n\nmismatch1 = """aaa\'\'\'bbb"""\nmismatch2 = \'\'\'aaa"""bbb\'\'\'\n\n# Three opening """, then one escaped ", then two "" (allowed), and then three\n# closing """\nescaped = """lol\\""""""\n\nfive-quotes = """\nClosing with five quotes\n"""""\nfour-quotes = """\nClosing with four quotes\n""""\n'; + const expected: any = { + escaped: 'lol"""', + "five-quotes": 'Closing with five quotes\n""', + "four-quotes": 'Closing with four quotes\n"', + lit_one: "'one quote'", + lit_one_space: " 'one quote' ", + lit_two: "''two quotes''", + lit_two_space: " ''two quotes'' ", + mismatch1: "aaa'''bbb", + mismatch2: 'aaa"""bbb', + one: '"one quote"', + one_space: ' "one quote" ', + two: '""two quotes""', + two_space: ' ""two quotes"" ', + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/multiline", () => { + const input: string = + '# NOTE: this file includes some literal tab characters.\n\nequivalent_one = "The quick brown fox jumps over the lazy dog."\nequivalent_two = """\nThe quick brown \\\n\n\n fox jumps over \\\n the lazy dog."""\n\nequivalent_three = """\\\n The quick brown \\\n fox jumps over \\\n the lazy dog.\\\n """\n\nwhitespace-after-bs = """\\\n The quick brown \\\n fox jumps over \\ \n the lazy dog.\\\t\n """\n\nno-space = """a\\\n b"""\n\n# Has tab character.\nkeep-ws-before = """a \t\\\n b"""\n\nescape-bs-1 = """a \\\\\nb"""\n\nescape-bs-2 = """a \\\\\\\nb"""\n\nescape-bs-3 = """a \\\\\\\\\n b"""\n'; + const expected: any = { + equivalent_one: "The quick brown fox jumps over the lazy dog.", + equivalent_three: "The quick brown fox jumps over the lazy dog.", + equivalent_two: "The quick brown fox jumps over the lazy dog.", + "escape-bs-1": "a \\\nb", + "escape-bs-2": "a \\b", + "escape-bs-3": "a \\\\\n b", + "keep-ws-before": "a \tb", + "no-space": "ab", + "whitespace-after-bs": "The quick brown fox jumps over the lazy dog.", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/nl", () => { + const input: string = + "nl_mid = \"val\\nue\"\nnl_end = \"\"\"value\\n\"\"\"\n\nlit_nl_end = '''value\\n'''\nlit_nl_mid = 'val\\nue'\nlit_nl_uni = 'val\\ue'\n"; + const expected: any = { + lit_nl_end: "value\\n", + lit_nl_mid: "val\\nue", + lit_nl_uni: "val\\ue", + nl_end: "value\n", + nl_mid: "val\nue", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/quoted-unicode", () => { + const input: string = + "\nescaped_string = \"\\u0000 \\u0008 \\u000c \\U00000041 \\u007f \\u0080 \\u00ff \\ud7ff \\ue000 \\uffff \\U00010000 \\U0010ffff\"\nnot_escaped_string = '\\u0000 \\u0008 \\u000c \\U00000041 \\u007f \\u0080 \\u00ff \\ud7ff \\ue000 \\uffff \\U00010000 \\U0010ffff'\n\nbasic_string = \"~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿\"\nliteral_string = '~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿'\n"; + const expected: any = { + basic_string: "~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿", + escaped_string: "\u0000 \b \f A \u007f \u0080 ÿ ퟿  ￿ 𐀀 􏿿", + literal_string: "~ \u0080 ÿ ퟿  ￿ 𐀀 􏿿", + not_escaped_string: + "\\u0000 \\u0008 \\u000c \\U00000041 \\u007f \\u0080 \\u00ff \\ud7ff \\ue000 \\uffff \\U00010000 \\U0010ffff", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/raw-empty", () => { + const input: string = "empty = ''\n"; + const expected: any = { empty: "" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/raw-multiline", () => { + const input: string = + "# Single ' should be allowed.\noneline = '''This string has a ' quote character.'''\n\n# A newline immediately following the opening delimiter will be trimmed.\nfirstnl = '''\nThis string has a ' quote character.'''\n\n# All other whitespace and newline characters remain intact.\nmultiline = '''\nThis string\nhas ' a quote character\nand more than\none newline\nin it.'''\n\n# Tab character in literal string does not need to be escaped\nmultiline_with_tab = '''First line\n\t Followed by a tab'''\n\nthis-str-has-apostrophes='''' there's one already\n'' two more\n'''''\n"; + const expected: any = { + firstnl: "This string has a ' quote character.", + multiline: "This string\nhas ' a quote character\nand more than\none newline\nin it.", + multiline_with_tab: "First line\n\t Followed by a tab", + oneline: "This string has a ' quote character.", + "this-str-has-apostrophes": "' there's one already\n'' two more\n''", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/raw", () => { + const input: string = + "backspace = 'This string has a \\b backspace character.'\ntab = 'This string has a \\t tab character.'\nunescaped_tab = 'This string has an \t unescaped tab character.'\nnewline = 'This string has a \\n new line character.'\nformfeed = 'This string has a \\f form feed character.'\ncarriage = 'This string has a \\r carriage return character.'\nslash = 'This string has a \\/ slash character.'\nbackslash = 'This string has a \\\\ backslash character.'\n"; + const expected: any = { + backslash: "This string has a \\\\ backslash character.", + backspace: "This string has a \\b backspace character.", + carriage: "This string has a \\r carriage return character.", + formfeed: "This string has a \\f form feed character.", + newline: "This string has a \\n new line character.", + slash: "This string has a \\/ slash character.", + tab: "This string has a \\t tab character.", + unescaped_tab: "This string has an \t unescaped tab character.", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/simple", () => { + const input: string = 'answer = "You are not drinking enough whisky."\n'; + const expected: any = { answer: "You are not drinking enough whisky." }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/start-mb", () => { + const input: string = + '# Start first line with a multibyte character.\n#\n# https://github.com/marzer/tomlplusplus/issues/190\ns1 = "§"\ns2 = \'§\'\ns3 = """\\\n§"""\ns4 = """\n§"""\ns5 = """§"""\ns6 = \'\'\'\n§\'\'\'\ns7 = \'\'\'§\'\'\'\n'; + const expected: any = { s1: "§", s2: "§", s3: "§", s4: "§", s5: "§", s6: "§", s7: "§" }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/unicode-escape", () => { + const input: string = + 'delta-1 = "\\u03B4"\ndelta-2 = "\\U000003B4"\na = "\\u0061"\nb = "\\u0062"\nc = "\\U00000063"\nnull-1 = "\\u0000"\nnull-2 = "\\U00000000"\n\nml-delta-1 = """\\u03B4"""\nml-delta-2 = """\\U000003B4"""\nml-a = """\\u0061"""\nml-b = """\\u0062"""\nml-c = """\\U00000063"""\nml-null-1 = """\\u0000"""\nml-null-2 = """\\U00000000"""\n'; + const expected: any = { + a: "a", + b: "b", + c: "c", + "delta-1": "δ", + "delta-2": "δ", + "ml-a": "a", + "ml-b": "b", + "ml-c": "c", + "ml-delta-1": "δ", + "ml-delta-2": "δ", + "ml-null-1": "\u0000", + "ml-null-2": "\u0000", + "null-1": "\u0000", + "null-2": "\u0000", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/string/with-pound", () => { + const input: string = + 'pound = "We see no # comments here."\npoundcomment = "But there are # some comments here." # Did I # mess you up?\n'; + const expected: any = { + pound: "We see no # comments here.", + poundcomment: "But there are # some comments here.", + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/array-empty-name", () => { + const input: string = "# Silly thing to do, but valid.\n\n[['']]\na = 1\n[['']]\na = 2\n"; + const expected: any = { "": [{ a: 1 }, { a: 2 }] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/array-empty", () => { + const input: string = "[[a]]\n"; + const expected: any = { a: [{}] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/array-implicit-and-explicit-after", () => { + const input: string = "[[a.b]]\nx = 1\n\n[a]\ny = 2\n"; + const expected: any = { a: { b: [{ x: 1 }], y: 2 } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/array-implicit", () => { + const input: string = '[[albums.songs]]\nname = "Glory Days"\n'; + const expected: any = { albums: { songs: [{ name: "Glory Days" }] } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/array-many", () => { + const input: string = + '[[people]]\nfirst_name = "Bruce"\nlast_name = "Springsteen"\n\n[[people]]\nfirst_name = "Eric"\nlast_name = "Clapton"\n\n[[people]]\nfirst_name = "Bob"\nlast_name = "Seger"\n'; + const expected: any = { + people: [ + { first_name: "Bruce", last_name: "Springsteen" }, + { first_name: "Eric", last_name: "Clapton" }, + { first_name: "Bob", last_name: "Seger" }, + ], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/array-nest", () => { + const input: string = + '[[albums]]\nname = "Born to Run"\n\n [[albums.songs]]\n name = "Jungleland"\n\n [[albums.songs]]\n name = "Meeting Across the River"\n\n[[albums]]\nname = "Born in the USA"\n \n [[albums.songs]]\n name = "Glory Days"\n\n [[albums.songs]]\n name = "Dancing in the Dark"\n'; + const expected: any = { + albums: [ + { + name: "Born to Run", + songs: [{ name: "Jungleland" }, { name: "Meeting Across the River" }], + }, + { + name: "Born in the USA", + songs: [{ name: "Glory Days" }, { name: "Dancing in the Dark" }], + }, + ], + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/array-one", () => { + const input: string = '[[people]]\nfirst_name = "Bruce"\nlast_name = "Springsteen"\n'; + const expected: any = { people: [{ first_name: "Bruce", last_name: "Springsteen" }] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/array-table-array", () => { + const input: string = + '[[a]]\n [[a.b]]\n [a.b.c]\n d = "val0"\n [[a.b]]\n [a.b.c]\n d = "val1"\n'; + const expected: any = { a: [{ b: [{ c: { d: "val0" } }, { c: { d: "val1" } }] }] }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/array-within-dotted", () => { + const input: string = '[fruit]\napple.color = "red"\n\n[[fruit.apple.seeds]]\nsize = 2\n'; + const expected: any = { fruit: { apple: { color: "red", seeds: [{ size: 2 }] } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/empty-name", () => { + const input: string = "['']\nx = 1\n\n[\"\".a]\nx = 2\n\n[a.'']\nx = 3\n"; + const expected: any = { "": { x: 1, a: { x: 2 } }, a: { "": { x: 3 } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/empty", () => { + const input: string = "[a]\n"; + const expected: any = { a: {} }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/keyword-with-values", () => { + const input: string = "[true]\nk = 1\n\n[false]\nk = 2\n\n[inf]\nk = 3\n\n[nan]\nk = 4\n"; + const expected: any = { false: { k: 2 }, inf: { k: 3 }, nan: { k: 4 }, true: { k: 1 } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/keyword", () => { + const input: string = "[true]\n\n[false]\n\n[inf]\n\n[nan]\n\n\n"; + const expected: any = { false: {}, inf: {}, nan: {}, true: {} }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/names-with-values", () => { + const input: string = + "[a.b.c]\nkey = 1\n\n[a.\"b.c\"]\nkey = 2\n\n[a.'d.e']\nkey = 3\n\n[a.' x ']\nkey = 4\n\n[ d.e.f ]\nkey = 5\n\n[ g . h . i ]\nkey = 6\n\n[ j . \"ʞ\" . 'l' ]\nkey = 7\n\n[x.1.2]\nkey = 8\n"; + const expected: any = { + a: { + " x ": { key: 4 }, + b: { c: { key: 1 } }, + "b.c": { key: 2 }, + "d.e": { key: 3 }, + }, + d: { e: { f: { key: 5 } } }, + g: { h: { i: { key: 6 } } }, + j: { "ʞ": { l: { key: 7 } } }, + x: { "1": { "2": { key: 8 } } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/names", () => { + const input: string = + "[a.b.c]\n[a.\"b.c\"]\n[a.'d.e']\n[a.' x ']\n[ d.e.f ]\n[ g . h . i ]\n[ j . \"ʞ\" . 'l' ]\n\n[x.1.2]\n"; + const expected: any = { + a: { " x ": {}, "b.c": {}, "d.e": {}, b: { c: {} } }, + d: { e: { f: {} } }, + g: { h: { i: {} } }, + j: { "ʞ": { l: {} } }, + x: { "1": { "2": {} } }, + }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/no-eol-01", () => { + const input: string = "# No newline at end of file.\n[table]"; + const expected: any = { table: {} }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/no-eol-02", () => { + const input: string = "# No newline at end of file.\n[table]\na=1"; + const expected: any = { table: { a: 1 } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/sub-empty", () => { + const input: string = "[a]\n[a.b]\n"; + const expected: any = { a: { b: {} } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/sub", () => { + const input: string = + '[a]\nkey = 1\n\n# a.extend is a key inside the "a" table.\n[a.extend]\nkey = 2\n\n[a.extend.more]\nkey = 3\n'; + const expected: any = { a: { key: 1, extend: { key: 2, more: { key: 3 } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/whitespace", () => { + const input: string = '["valid key"]\n'; + const expected: any = { "valid key": {} }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/with-literal-string", () => { + const input: string = "['a']\n[a.'\"b\"']\n[a.'\"b\"'.c]\nanswer = 42 \n"; + const expected: any = { a: { '"b"': { c: { answer: 42 } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/with-pound", () => { + const input: string = '["key#group"]\nanswer = 42\n'; + const expected: any = { "key#group": { answer: 42 } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/with-single-quotes", () => { + const input: string = "['a']\n[a.'b']\n[a.'b'.c]\nanswer = 42 \n"; + const expected: any = { a: { b: { c: { answer: 42 } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/without-super-with-values", () => { + const input: string = + "# [x] you\n# [x.y] don't\n# [x.y.z] need these\n[x.y.z.w] # for this to work\na = 1\nb = 2\n[x] # defining a super-table afterwards is ok\nc = 3\nd = 4\n"; + const expected: any = { x: { c: 3, d: 4, y: { z: { w: { a: 1, b: 2 } } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/table/without-super", () => { + const input: string = + "# [x] you\n# [x.y] don't\n# [x.y.z] need these\n[x.y.z.w] # for this to work\n[x] # defining a super-table afterwards is ok\n"; + const expected: any = { x: { y: { z: { w: {} } } } }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/utf8-bom-01", () => { + const input: string = + "\ufeff# This file starts with an UTF-8 BOM (\\xEF\\xBB\\xBF), which isn't recommended to use but valid.\na=1\n"; + const expected: any = { a: 1 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); + + test("valid/utf8-bom-02", () => { + const input: string = + "\ufeffa=1# This file starts with an UTF-8 BOM (\\xEF\\xBB\\xBF), which isn't recommended to use but valid.\n"; + const expected: any = { a: 1 }; + expectTomlEqual(TOML.parse(input), expected); + expectTomlEqual(TOML.parse(TOML.stringify(TOML.parse(input))), expected); + }); +}); + +// Upstream marks these valid, asserting exact 64-bit integers, which JS +// numbers cannot represent. Bun rejects integers outside Number.MAX_SAFE_INTEGER +// instead of returning corrupted values or mixed number/BigInt types; the +// 64-bit range is a "should" in the spec (toml-lang/toml-test#154). +describe("toml-test/valid-out-of-range-integer", () => { + test("valid/integer/long", () => { + const input: string = + '# int64 "should" be supported, but is not mandatory. It\'s fine to skip this\n# test.\nint64-max = 9223372036854775807\nint64-max-neg = -9223372036854775808\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Integer cannot be losslessly represented as a JavaScript number; it must be within +/-(2^53 - 1)", + ); + }); +}); + +describe("toml-test/invalid", () => { + test("invalid/array/double-comma-01", () => { + const input: string = "double-comma-01 = [1,,2]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found ','"); + }); + + test("invalid/array/double-comma-02", () => { + const input: string = "double-comma-02 = [1,2,,]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found ','"); + }); + + test("invalid/array/extend-defined-aot", () => { + const input: string = "[[tab.arr]]\n[tab]\narr.val1=1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'arr'"); + }); + + test("invalid/array/extending-table", () => { + const input: string = + "a = [{ b = 1 }]\n\n# Cannot extend tables within static arrays\n# https://github.com/toml-lang/toml/issues/908\n[a.c]\nfoo = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend array 'a'"); + }); + + test("invalid/array/missing-separator-01", () => { + const input: string = "arrr = [true false]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ',' or ']' in an array but found 'f'"); + }); + + test("invalid/array/missing-separator-02", () => { + const input: string = "wrong = [ 1 2 3 ]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ',' or ']' in an array but found '2'"); + }); + + test("invalid/array/no-close-01", () => { + const input: string = "no-close-01 = [ 1, 2, 3\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ',' or ']' in an array but found end of file", + ); + }); + + test("invalid/array/no-close-02", () => { + const input: string = "no-close-02 = [1,\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated array; expected ']'"); + }); + + test("invalid/array/no-close-03", () => { + const input: string = "no-close-03 = [42 #]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ',' or ']' in an array but found end of file", + ); + }); + + test("invalid/array/no-close-04", () => { + const input: string = "no-close-04 = [{ key = 42\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ',' or '}' in an inline table but found end of file", + ); + }); + + test("invalid/array/no-close-05", () => { + const input: string = "no-close-05 = [{ key = 42}\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ',' or ']' in an array but found end of file", + ); + }); + + test("invalid/array/no-close-06", () => { + const input: string = "no-close-06 = [{ key = 42 #}]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ',' or '}' in an inline table but found end of file", + ); + }); + + test("invalid/array/no-close-07", () => { + const input: string = "no-close-07 = [{ key = 42} #]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ',' or ']' in an array but found end of file", + ); + }); + + test("invalid/array/no-close-08", () => { + const input: string = "no-close-08 = [\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated array; expected ']'"); + }); + + test("invalid/array/no-close-table-01", () => { + const input: string = "no-close-table-01 = [{ key = 42\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ',' or '}' in an inline table but found end of file", + ); + }); + + test("invalid/array/no-close-table-02", () => { + const input: string = "no-close-table-02 = [{ key = 42 #\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ',' or '}' in an inline table but found end of file", + ); + }); + + test("invalid/array/no-close-table-03", () => { + const input: string = "no-close-table-03 = [1,{a=1]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ',' or '}' in an inline table but found ']'"); + }); + + test("invalid/array/no-close-table-04", () => { + const input: string = "no-close-table-04 = [1,{2]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found ']'"); + }); + + test("invalid/array/no-comma-01", () => { + const input: string = "no-comma-01 = [true false]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ',' or ']' in an array but found 'f'"); + }); + + test("invalid/array/no-comma-02", () => { + const input: string = "no-comma-02 = [ 1 2 3 ]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ',' or ']' in an array but found '2'"); + }); + + test("invalid/array/no-comma-03", () => { + const input: string = "no-comma-03 = [ 1 #,]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ',' or ']' in an array but found end of file", + ); + }); + + test("invalid/array/only-comma-01", () => { + const input: string = "only-comma-01 = [,]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found ','"); + }); + + test("invalid/array/only-comma-02", () => { + const input: string = "only-comma-02 = [,,]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found ','"); + }); + + test("invalid/array/tables-01", () => { + const input: string = "# INVALID TOML DOC\nfruit = []\n\n[[fruit]] # Not allowed\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend array 'fruit'"); + }); + + test("invalid/array/tables-02", () => { + const input: string = + '# INVALID TOML DOC\n[[fruit]]\n name = "apple"\n\n [[fruit.variety]]\n name = "red delicious"\n\n # This table conflicts with the previous table\n [fruit.variety]\n name = "granny smith"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine array of tables 'variety' as a table"); + }); + + test("invalid/array/text-after-array-entries", () => { + const input: string = 'array = [\n "Is there life after an array separator?", No\n "Entry"\n]\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "No"'); + }); + + test("invalid/array/text-before-array-separator", () => { + const input: string = 'array = [\n "Is there life before an array separator?" No,\n "Entry"\n]\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ',' or ']' in an array but found 'N'"); + }); + + test("invalid/array/text-in-array", () => { + const input: string = 'array = [\n "Entry 1",\n I don\'t belong,\n "Entry 2",\n]\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "I"'); + }); + + test("invalid/bool/almost-false-with-extra", () => { + const input: string = "almost-false-with-extra = falsify\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "falsify"'); + }); + + test("invalid/bool/almost-false", () => { + const input: string = "almost-false = fals\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "fals"'); + }); + + test("invalid/bool/almost-true-with-extra", () => { + const input: string = "almost-true-with-extra = truthy\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "truthy"'); + }); + + test("invalid/bool/almost-true", () => { + const input: string = "almost-true = tru\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "tru"'); + }); + + test("invalid/bool/capitalized-false", () => { + const input: string = "capitalized-false = False\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "False"'); + }); + + test("invalid/bool/capitalized-true", () => { + const input: string = "capitalized-true = True\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "True"'); + }); + + test("invalid/bool/just-f", () => { + const input: string = "just-f = f\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "f"'); + }); + + test("invalid/bool/just-t", () => { + const input: string = "just-t = t\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "t"'); + }); + + test("invalid/bool/mixed-case-false", () => { + const input: string = "mixed-case-false = falsE\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "falsE"'); + }); + + test("invalid/bool/mixed-case-true", () => { + const input: string = "mixed-case-true = trUe\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "trUe"'); + }); + + test("invalid/bool/mixed-case", () => { + const input: string = "mixed-case = valid = False\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "valid"'); + }); + + test("invalid/bool/starting-same-false", () => { + const input: string = "starting-same-false = falsey\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "falsey"'); + }); + + test("invalid/bool/starting-same-true", () => { + const input: string = "starting-same-true = truer\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "truer"'); + }); + + test("invalid/bool/wrong-case-false", () => { + const input: string = "wrong-case-false = FALSE\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "FALSE"'); + }); + + test("invalid/bool/wrong-case-true", () => { + const input: string = "wrong-case-true = TRUE\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "TRUE"'); + }); + + test("invalid/control/bare-cr", () => { + const input: string = "# The following line contains a single carriage return control character\r\n\r"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Bare carriage return is not allowed; use \\r\\n or \\n", + ); + }); + + test("invalid/control/bare-formfeed", () => { + const input: string = "bare-formfeed = \f\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found (0x0C)"); + }); + + test("invalid/control/bare-null", () => { + const input: string = 'bare-null = "some value" \u0000\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/control/bare-vertical-tab", () => { + const input: string = "bare-vertical-tab = \u000b\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found (0x0B)"); + }); + + test("invalid/control/comment-cr", () => { + const input: string = 'comment-cr = "Carriage return in comment" # \ra=1\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Bare carriage return is not allowed; use \\r\\n or \\n", + ); + }); + + test("invalid/control/comment-del", () => { + const input: string = 'comment-del = "0x7f" # \u007f\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a comment: (0x7F)", + ); + }); + + test("invalid/control/comment-ff", () => { + const input: string = 'comment-ff = "0x7f" # \f\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a comment: (0x0C)", + ); + }); + + test("invalid/control/comment-lf", () => { + const input: string = 'comment-lf = "ctrl-P" # \u0010\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a comment: (0x10)", + ); + }); + + test("invalid/control/comment-null", () => { + const input: string = 'comment-null = "null" # \u0000\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a comment: (0x00)", + ); + }); + + test("invalid/control/comment-us", () => { + const input: string = 'comment-us = "ctrl-_" # \u001f\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a comment: (0x1F)", + ); + }); + + test("invalid/control/linetab-number-01", () => { + const input: string = "linetab-number-01 = 1\u000b\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: (0x0B)"); + }); + + test("invalid/control/linetab-number-02", () => { + const input: string = "linetab-number-02 = 1.5\u000b\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: (0x0B)"); + }); + + test("invalid/control/linetab-number-03", () => { + const input: string = "linetab-number-03 = 0xff\u000b\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: (0x0B)"); + }); + + test("invalid/control/linetab-number-04", () => { + const input: string = "linetab-number-04 = +inf\u000b\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: (0x0B)"); + }); + + test("invalid/control/multi-cr", () => { + const input: string = 'multi-cr = """null\r"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Bare carriage return is not allowed; use \\r\\n or \\n", + ); + }); + + test("invalid/control/multi-del", () => { + const input: string = 'multi-del = """null\u007f"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character must be escaped in a string: (0x7F)", + ); + }); + + test("invalid/control/multi-lf", () => { + const input: string = 'multi-lf = """null\u0010"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character must be escaped in a string: (0x10)", + ); + }); + + test("invalid/control/multi-null", () => { + const input: string = 'multi-null = """null\u0000"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character must be escaped in a string: (0x00)", + ); + }); + + test("invalid/control/multi-us", () => { + const input: string = 'multi-us = """null\u001f"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character must be escaped in a string: (0x1F)", + ); + }); + + test("invalid/control/only-ff", () => { + const input: string = "\f"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0x0C)"); + }); + + test("invalid/control/only-null", () => { + const input: string = "\u0000"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0x00)"); + }); + + test("invalid/control/only-vt", () => { + const input: string = "\u000b"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0x0B)"); + }); + + test("invalid/control/rawmulti-cr", () => { + const input: string = "rawmulti-cr = '''null\r'''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Bare carriage return is not allowed; use \\r\\n or \\n", + ); + }); + + test("invalid/control/rawmulti-del", () => { + const input: string = "rawmulti-del = '''null\u007f'''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a literal string: (0x7F)", + ); + }); + + test("invalid/control/rawmulti-lf", () => { + const input: string = "rawmulti-lf = '''null\u0010'''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a literal string: (0x10)", + ); + }); + + test("invalid/control/rawmulti-null", () => { + const input: string = "rawmulti-null = '''null\u0000'''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a literal string: (0x00)", + ); + }); + + test("invalid/control/rawmulti-us", () => { + const input: string = "rawmulti-us = '''null\u001f'''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a literal string: (0x1F)", + ); + }); + + test("invalid/control/rawstring-cr", () => { + const input: string = "rawstring-cr = 'null\r'\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Bare carriage return is not allowed; use \\r\\n or \\n", + ); + }); + + test("invalid/control/rawstring-del", () => { + const input: string = "rawstring-del = 'null\u007f'\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a literal string: (0x7F)", + ); + }); + + test("invalid/control/rawstring-lf", () => { + const input: string = "rawstring-lf = 'null\u0010'\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a literal string: (0x10)", + ); + }); + + test("invalid/control/rawstring-null", () => { + const input: string = "rawstring-null = 'null\u0000'\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a literal string: (0x00)", + ); + }); + + test("invalid/control/rawstring-us", () => { + const input: string = "rawstring-us = 'null\u001f'\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character is not allowed in a literal string: (0x1F)", + ); + }); + + test("invalid/control/string-bs", () => { + const input: string = 'string-bs = "backspace\b"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character must be escaped in a string: (0x08)", + ); + }); + + test("invalid/control/string-cr", () => { + const input: string = 'string-cr = "null\r"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Bare carriage return is not allowed; use \\r\\n or \\n", + ); + }); + + test("invalid/control/string-del", () => { + const input: string = 'string-del = "null\u007f"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character must be escaped in a string: (0x7F)", + ); + }); + + test("invalid/control/string-lf", () => { + const input: string = 'string-lf = "null\u0010"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character must be escaped in a string: (0x10)", + ); + }); + + test("invalid/control/string-null", () => { + const input: string = 'string-null = "null\u0000"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character must be escaped in a string: (0x00)", + ); + }); + + test("invalid/control/string-us", () => { + const input: string = 'string-us = "null\u001f"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Control character must be escaped in a string: (0x1F)", + ); + }); + + test("invalid/datetime/day-zero", () => { + const input: string = "foo = 1997-09-00T09:09:09.09Z\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/datetime/feb-29", () => { + const input: string = '"not a leap year" = 2100-02-29T15:15:15Z\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/datetime/feb-30", () => { + const input: string = '"only 28 or 29 days in february" = 1988-02-30T15:15:15Z\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/datetime/hour-over", () => { + const input: string = "# time-hour = 2DIGIT ; 00-23\nd = 2006-01-01T24:00:00-00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: hours must be between 00 and 23"); + }); + + test("invalid/datetime/leading-zero-date", () => { + const input: string = "# No leading zero on year allowed.\nd = 02026-05-07\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/datetime/leading-zero-datetime", () => { + const input: string = "# No leading zero on year allowed.\nd = 02026-05-07T14:15:16Z\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/datetime/mday-over", () => { + const input: string = + "# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on\n# ; month/year\nd = 2006-01-32T00:00:00-00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/datetime/mday-under", () => { + const input: string = + "# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on\n# ; month/year\nd = 2006-01-00T00:00:00-00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/datetime/minute-over", () => { + const input: string = "# time-minute = 2DIGIT ; 00-59\nd = 2006-01-01T00:60:00-00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: minutes must be between 00 and 59"); + }); + + test("invalid/datetime/month-over", () => { + const input: string = "# date-month = 2DIGIT ; 01-12\nd = 2006-13-01T00:00:00-00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: month must be between 01 and 12"); + }); + + test("invalid/datetime/month-under", () => { + const input: string = "# date-month = 2DIGIT ; 01-12\nd = 2007-00-01T00:00:00-00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: month must be between 01 and 12"); + }); + + test("invalid/datetime/no-date-time-sep", () => { + const input: string = "foo = 1997-09-0909:09:09\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '0'"); + }); + + test("invalid/datetime/no-leads-month", () => { + const input: string = + '# Month "7" instead of "07"; the leading zero is required.\nno-leads = 1987-7-05T17:45:00Z\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: expected a 2-digit month"); + }); + + test("invalid/datetime/no-leads-with-milli", () => { + const input: string = + '# Day "5" instead of "05"; the leading zero is required.\nwith-milli = 1987-07-5T17:45:00.12Z\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: expected a 2-digit day"); + }); + + test("invalid/datetime/no-leads", () => { + const input: string = + '# Month "7" instead of "07"; the leading zero is required.\nno-leads = 1987-7-05T17:45:00Z\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: expected a 2-digit month"); + }); + + test("invalid/datetime/no-t", () => { + const input: string = '# No "t" or "T" between the date and time.\nno-t = 1987-07-0517:45:00Z\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '1'"); + }); + + test("invalid/datetime/no-year-month-sep", () => { + const input: string = "foo = 199709-09\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '-'"); + }); + + test("invalid/datetime/offset-minus-minute-1digit", () => { + const input: string = "foo = 1997-09-09T09:09:09.09+09:9\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date-time offset: expected 2-digit minutes"); + }); + + test("invalid/datetime/offset-minus-no-hour-minute-sep", () => { + const input: string = "foo = 1997-09-09T09:09:09.09+0909\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Invalid date-time offset: expected ':' between hours and minutes", + ); + }); + + test("invalid/datetime/offset-minus-no-hour-minute", () => { + const input: string = "foo = 1997-09-09T09:09:09.09+\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date-time offset: expected 2-digit hours"); + }); + + test("invalid/datetime/offset-minus-no-minute", () => { + const input: string = "foo = 1997-09-09T09:09:09.09+09\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Invalid date-time offset: expected ':' between hours and minutes", + ); + }); + + test("invalid/datetime/offset-overflow-hour", () => { + const input: string = "# Hour must be 00-24\nd = 1985-06-18 17:04:07+25:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Invalid date-time offset: hours must be between 00 and 23", + ); + }); + + test("invalid/datetime/offset-overflow-minute", () => { + const input: string = "d = 1985-06-18 17:04:07+12:60\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Invalid date-time offset: minutes must be between 00 and 59", + ); + }); + + test("invalid/datetime/offset-plus-minute-1digit", () => { + const input: string = "foo = 1997-09-09T09:09:09.09+09:9\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date-time offset: expected 2-digit minutes"); + }); + + test("invalid/datetime/offset-plus-no-hour-minute-sep", () => { + const input: string = "foo = 1997-09-09T09:09:09.09+0909\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Invalid date-time offset: expected ':' between hours and minutes", + ); + }); + + test("invalid/datetime/offset-plus-no-hour-minute", () => { + const input: string = "foo = 1997-09-09T09:09:09.09+\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date-time offset: expected 2-digit hours"); + }); + + test("invalid/datetime/offset-plus-no-minute", () => { + const input: string = "foo = 1997-09-09T09:09:09.09+09\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Invalid date-time offset: expected ':' between hours and minutes", + ); + }); + + test("invalid/datetime/only-T", () => { + const input: string = "foo = T\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "T"'); + }); + + test("invalid/datetime/only-TZ", () => { + const input: string = "foo = TZ\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "TZ"'); + }); + + test("invalid/datetime/only-Tdot", () => { + const input: string = "foo = T.\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "T"'); + }); + + test("invalid/datetime/second-over", () => { + const input: string = + "# time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second\n# ; rules\nd = 2006-01-01T00:00:61-00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: seconds must be between 00 and 60"); + }); + + test("invalid/datetime/second-trailing-dot", () => { + const input: string = "foo = 1997-09-09T09:09:09.\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Invalid time: expected at least one digit of fractional seconds", + ); + }); + + test("invalid/datetime/second-trailing-dotz", () => { + const input: string = "foo = 2016-09-09T09:09:09.Z\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Invalid time: expected at least one digit of fractional seconds", + ); + }); + + test("invalid/datetime/time-no-leads", () => { + const input: string = "# Leading 0 is always required.\nd = 2023-10-01T1:32:00Z\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: expected 2-digit hours"); + }); + + test("invalid/datetime/trailing-x", () => { + const input: string = "sign=2020-01-01x\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: 'x'"); + }); + + test("invalid/datetime/y10k-date", () => { + const input: string = "# Maximum RFC3399 year is 9999.\nd = 10000-01-01\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '-'"); + }); + + test("invalid/datetime/y10k-datetime", () => { + const input: string = "# Maximum RFC3399 year is 9999.\nd = 10000-01-01 00:00:00z\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '-'"); + }); + + test("invalid/encoding/bom-not-at-start-01", () => { + const input: string = "# Contains UTF-8 BOM between = and 1\na=\ufeff1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found (0xEF)"); + }); + + test("invalid/encoding/bom-not-at-start-02", () => { + const input: string = "\ufeff\ufeff# Contains two UTF-8 BOMS at the start\na=1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0xEF)"); + }); + + test("invalid/encoding/bom-not-at-start-03", () => { + const input: string = "\ufeff\ufeffa=1\n# Contains two UTF-8 BOMS at the start\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0xEF)"); + }); + + test("invalid/encoding/ideographic-space", () => { + const input: string = '# First on next line is U+3000 IDEOGRAPHIC SPACE\n foo = "bar"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0xE3)"); + }); + + test("invalid/encoding/utf16-comment", () => { + const input: string = + "\u0000#\u0000 \u0000U\u0000T\u0000F\u0000-\u00001\u00006\u0000 \u0000w\u0000i\u0000t\u0000h\u0000o\u0000u\u0000t\u0000 \u0000B\u0000O\u0000M\u0000\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0x00)"); + }); + + test("invalid/encoding/utf16-key", () => { + const input: string = '\u0000k\u0000 \u0000=\u0000 \u0000"\u0000v\u0000"\u0000\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0x00)"); + }); + + test("invalid/float/arabic-zero-01", () => { + const input: string = "arabic-zero-01 = 1.٠\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/arabic-zero-02", () => { + const input: string = "arabic-zero-02 = ٠\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found (0xD9)"); + }); + + test("invalid/float/arabic-zero-03", () => { + const input: string = "arabic-zero-03 = 1e٠\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: An exponent must contain at least one digit"); + }); + + test("invalid/float/arabic-zero-04", () => { + const input: string = "arabic-zero-04 = +٠\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a number but found (0xD9)"); + }); + + test("invalid/float/double-dot-01", () => { + const input: string = "double-dot-01 = 0..1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/double-dot-02", () => { + const input: string = "double-dot-02 = 0.1.2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '.'"); + }); + + test("invalid/float/exp-dot-01", () => { + const input: string = "exp-dot-01 = 1e2.3\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '.'"); + }); + + test("invalid/float/exp-dot-02", () => { + const input: string = "exp-dot-02 = 1.e2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/exp-dot-03", () => { + const input: string = "exp-dot-03 = 3.e+20\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/exp-double-e-01", () => { + const input: string = "exp-double-e-01 = 1ee2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: An exponent must contain at least one digit"); + }); + + test("invalid/float/exp-double-e-02", () => { + const input: string = "exp-double-e-02 = 1e2e3\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: 'e'"); + }); + + test("invalid/float/exp-double-us", () => { + const input: string = "exp-double-us = 1e__23\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: An exponent must contain at least one digit"); + }); + + test("invalid/float/exp-leading-us", () => { + const input: string = "exp-leading-us = 1e_23\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: An exponent must contain at least one digit"); + }); + + test("invalid/float/exp-trailing-us-01", () => { + const input: string = "exp-trailing-us-01 = 1_e2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/float/exp-trailing-us-02", () => { + const input: string = "exp-trailing-us-02 = 1.2_e2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/float/exp-trailing-us", () => { + const input: string = "exp-trailing-us = 1e23_\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/float/inf-capital", () => { + const input: string = "v = Inf\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "Inf"'); + }); + + test("invalid/float/inf-incomplete-01", () => { + const input: string = "inf-incomplete-01 = in\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "in"'); + }); + + test("invalid/float/inf-incomplete-02", () => { + const input: string = "inf-incomplete-02 = +in\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a number but found 'i'"); + }); + + test("invalid/float/inf-incomplete-03", () => { + const input: string = "inf-incomplete-03 = -in\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a number but found 'i'"); + }); + + test("invalid/float/inf_underscore", () => { + const input: string = "inf_underscore = in_f\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "in_f"'); + }); + + test("invalid/float/leading-dot-neg", () => { + const input: string = "leading-dot-neg = -.12345\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a number but found '.'"); + }); + + test("invalid/float/leading-dot-plus", () => { + const input: string = "leading-dot-plus = +.12345\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a number but found '.'"); + }); + + test("invalid/float/leading-dot", () => { + const input: string = "leading-dot = .12345\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found '.'"); + }); + + test("invalid/float/leading-us", () => { + const input: string = "leading-us = _1.2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found '_'"); + }); + + test("invalid/float/leading-zero-neg", () => { + const input: string = "leading-zero-neg = -03.14\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/float/leading-zero-plus", () => { + const input: string = "leading-zero-plus = +03.14\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/float/leading-zero", () => { + const input: string = "leading-zero = 03.14\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/float/nan-capital", () => { + const input: string = "v = NaN\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "NaN"'); + }); + + test("invalid/float/nan-incomplete-01", () => { + const input: string = "nan-incomplete-01 = na\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "na"'); + }); + + test("invalid/float/nan-incomplete-02", () => { + const input: string = "nan-incomplete-02 = +na\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a number but found 'n'"); + }); + + test("invalid/float/nan-incomplete-03", () => { + const input: string = "nan-incomplete-03 = -na\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a number but found 'n'"); + }); + + test("invalid/float/nan_underscore", () => { + const input: string = "nan_underscore = na_n\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "na_n"'); + }); + + test("invalid/float/trailing-dot-01", () => { + const input: string = "trailing-point = 1.\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/trailing-dot-02", () => { + const input: string = "a = 1.\nb = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/trailing-dot-min", () => { + const input: string = "trailing-dot-min = -1.\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/trailing-dot-plus", () => { + const input: string = "trailing-dot-plus = +1.\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/trailing-dot", () => { + const input: string = "trailing-dot = 1.\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/trailing-exp-dot", () => { + const input: string = "trailing-exp-dot = 0.e\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/trailing-exp-minus", () => { + const input: string = "trailing-exp-minus = 0.0e-\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: An exponent must contain at least one digit"); + }); + + test("invalid/float/trailing-exp-plus", () => { + const input: string = "trailing-exp-plus = 0.0e+\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: An exponent must contain at least one digit"); + }); + + test("invalid/float/trailing-exp", () => { + const input: string = "trailing-exp = 0.0E\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: An exponent must contain at least one digit"); + }); + + test("invalid/float/trailing-us-exp-01", () => { + const input: string = "trailing-us-exp-1 = 1_e2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/float/trailing-us-exp-02", () => { + const input: string = "trailing-us-exp-2 = 1.2_e2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/float/trailing-us", () => { + const input: string = "trailing-us = 1.2_\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/float/us-after-dot", () => { + const input: string = "us-after-dot = 1._2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A decimal point must be followed by at least one digit", + ); + }); + + test("invalid/float/us-before-dot", () => { + const input: string = "us-before-dot = 1_.2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/inline-table/bad-key-syntax", () => { + const input: string = "tbl = { a = 1, [b] }\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '['"); + }); + + test("invalid/inline-table/double-comma", () => { + const input: string = "t = {x=3,,y=4}\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found ','"); + }); + + test("invalid/inline-table/duplicate-key-01", () => { + const input: string = "# Duplicate keys within an inline table are invalid\na={b=1, b=2}\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'b'"); + }); + + test("invalid/inline-table/duplicate-key-02", () => { + const input: string = "table1 = { table2.dupe = 1, table2.dupe = 2 }\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'dupe'"); + }); + + test("invalid/inline-table/duplicate-key-03", () => { + const input: string = 'tbl = { fruit = { apple.color = "red" }, fruit.apple.texture = { smooth = true } }\n\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend table 'fruit' with a dotted key"); + }); + + test("invalid/inline-table/duplicate-key-04", () => { + const input: string = 'tbl = { a.b = "a_b", a.b.c = "a_b_c" }\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'b'"); + }); + + test("invalid/inline-table/empty-01", () => { + const input: string = "t = {,}\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found ','"); + }); + + test("invalid/inline-table/empty-02", () => { + const input: string = "t = {,\n}\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found ','"); + }); + + test("invalid/inline-table/empty-03", () => { + const input: string = "t = {\n,\n}\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found ','"); + }); + + test("invalid/inline-table/no-close-01", () => { + const input: string = "a={\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated inline table; expected '}'"); + }); + + test("invalid/inline-table/no-close-02", () => { + const input: string = "a={b=1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ',' or '}' in an inline table but found end of file", + ); + }); + + test("invalid/inline-table/no-comma-01", () => { + const input: string = "t = {x = 3 y = 4}\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ',' or '}' in an inline table but found 'y'"); + }); + + test("invalid/inline-table/no-comma-02", () => { + const input: string = "arrr = { comma-missing = true valid-toml = false }\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ',' or '}' in an inline table but found 'v'"); + }); + + test("invalid/inline-table/overwrite-01", () => { + const input: string = + 'a.b=0\n# Since table "a" is already defined, it can\'t be replaced by an inline table.\na={}\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'a'"); + }); + + test("invalid/inline-table/overwrite-02", () => { + const input: string = "a={}\n# Inline tables are immutable and can't be extended\n[a.b]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend inline table 'a'"); + }); + + test("invalid/inline-table/overwrite-03", () => { + const input: string = "a = { b = 1 }\na.b = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend table 'a' with a dotted key"); + }); + + test("invalid/inline-table/overwrite-04", () => { + const input: string = "inline-t = { nest = {} }\n\n[[inline-t.nest]]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend inline table 'inline-t'"); + }); + + test("invalid/inline-table/overwrite-05", () => { + const input: string = "inline-t = { nest = {} }\n\n[inline-t.nest]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend inline table 'inline-t'"); + }); + + test("invalid/inline-table/overwrite-06", () => { + const input: string = "a = { b = 1, b.c = 2 }\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'b'"); + }); + + test("invalid/inline-table/overwrite-07", () => { + const input: string = 'tab = { inner.table = [{}], inner.table.val = "bad" }'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'table'"); + }); + + test("invalid/inline-table/overwrite-08", () => { + const input: string = 'tab = { inner = { dog = "best" }, inner.cat = "worst" }'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend table 'inner' with a dotted key"); + }); + + test("invalid/inline-table/overwrite-09", () => { + const input: string = "[tab.nested]\ninline-t = { nest = {} }\n\n[tab]\nnested.inline-t.nest = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend table 'nested' with a dotted key"); + }); + + test("invalid/inline-table/overwrite-10", () => { + const input: string = + '# Set implicit "b", overwrite "b" (illegal!) and then set another implicit.\n#\n# Caused panic: https://github.com/BurntSushi/toml/issues/403\na = {b.a = 1, b = 2, b.c = 3}\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'b'"); + }); + + test("invalid/integer/arabic-zero-01", () => { + const input: string = "arabic-zero-01 = 1٠\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: (0xD9)"); + }); + + test("invalid/integer/arabic-zero-02", () => { + const input: string = "arabic-zero-02 = 1_0٠\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: (0xD9)"); + }); + + test("invalid/integer/arabic-zero-03", () => { + const input: string = "arabic-zero-03 = ٠.1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found (0xD9)"); + }); + + test("invalid/integer/arabic-zero-04", () => { + const input: string = "arabic-zero-04 = ٠e0\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found (0xD9)"); + }); + + test("invalid/integer/capital-bin", () => { + const input: string = "capital-bin = 0B0\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: 'B'"); + }); + + test("invalid/integer/capital-hex", () => { + const input: string = "capital-hex = 0X1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: 'X'"); + }); + + test("invalid/integer/capital-oct", () => { + const input: string = "capital-oct = 0O0\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: 'O'"); + }); + + test("invalid/integer/double-sign-nex", () => { + const input: string = "double-sign-nex = --99\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a number but found '-'"); + }); + + test("invalid/integer/double-sign-plus", () => { + const input: string = "double-sign-plus = ++99\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a number but found '+'"); + }); + + test("invalid/integer/double-us", () => { + const input: string = "double-us = 1__23\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/integer/incomplete-bin", () => { + const input: string = "incomplete-bin = 0b\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected at least one digit after the radix prefix"); + }); + + test("invalid/integer/incomplete-hex", () => { + const input: string = "incomplete-hex = 0x\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected at least one digit after the radix prefix"); + }); + + test("invalid/integer/incomplete-oct", () => { + const input: string = "incomplete-oct = 0o\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected at least one digit after the radix prefix"); + }); + + test("invalid/integer/invalid-bin", () => { + const input: string = "invalid-bin = 0b0012\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid digit in number: '2'"); + }); + + test("invalid/integer/invalid-hex-01", () => { + const input: string = "invalid-hex-01 = 0xaafz\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid digit in number: 'z'"); + }); + + test("invalid/integer/invalid-hex-02", () => { + const input: string = "invalid-hex-02 = 0xgabba00f1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected at least one digit after the radix prefix"); + }); + + test("invalid/integer/invalid-hex-03", () => { + const input: string = "a = 0x-1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected at least one digit after the radix prefix"); + }); + + test("invalid/integer/invalid-oct", () => { + const input: string = "invalid-oct = 0o778\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid digit in number: '8'"); + }); + + test("invalid/integer/leading-us-bin", () => { + const input: string = "leading-us-bin = _0b1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found '_'"); + }); + + test("invalid/integer/leading-us-hex", () => { + const input: string = "leading-us-hex = _0x1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found '_'"); + }); + + test("invalid/integer/leading-us-oct", () => { + const input: string = "leading-us-oct = _0o1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found '_'"); + }); + + test("invalid/integer/leading-us", () => { + const input: string = "leading-us = _123\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found '_'"); + }); + + test("invalid/integer/leading-zero-01", () => { + const input: string = "leading-zero-01 = 01\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/integer/leading-zero-02", () => { + const input: string = "leading-zero-02 = 00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/integer/leading-zero-03", () => { + const input: string = "leading-zero-03 = 0_0\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/integer/leading-zero-sign-01", () => { + const input: string = "leading-zero-sign-01 = -01\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/integer/leading-zero-sign-02", () => { + const input: string = "leading-zero-sign-02 = +01\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/integer/leading-zero-sign-03", () => { + const input: string = "leading-zero-sign-03 = +0_1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + }); + + test("invalid/integer/negative-bin", () => { + const input: string = "negative-bin = -0b11010110\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A sign is not allowed on hexadecimal, octal, or binary integers", + ); + }); + + test("invalid/integer/negative-hex", () => { + const input: string = "negative-hex = -0xff\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A sign is not allowed on hexadecimal, octal, or binary integers", + ); + }); + + test("invalid/integer/negative-oct", () => { + const input: string = "negative-oct = -0o755\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A sign is not allowed on hexadecimal, octal, or binary integers", + ); + }); + + test("invalid/integer/positive-bin", () => { + const input: string = "positive-bin = +0b11010110\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A sign is not allowed on hexadecimal, octal, or binary integers", + ); + }); + + test("invalid/integer/positive-hex", () => { + const input: string = "positive-hex = +0xff\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A sign is not allowed on hexadecimal, octal, or binary integers", + ); + }); + + test("invalid/integer/positive-oct", () => { + const input: string = "positive-oct = +0o755\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A sign is not allowed on hexadecimal, octal, or binary integers", + ); + }); + + test("invalid/integer/text-after-integer", () => { + const input: string = "answer = 42 the ultimate answer?\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/integer/trailing-us-bin", () => { + const input: string = "trailing-us-bin = 0b1_\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/integer/trailing-us-hex", () => { + const input: string = "trailing-us-hex = 0x1_\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/integer/trailing-us-oct", () => { + const input: string = "trailing-us-oct = 0o1_\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/integer/trailing-us", () => { + const input: string = "trailing-us = 123_\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + }); + + test("invalid/integer/us-after-bin", () => { + const input: string = "us-after-bin = 0b_1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected at least one digit after the radix prefix"); + }); + + test("invalid/integer/us-after-hex", () => { + const input: string = "us-after-hex = 0x_1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected at least one digit after the radix prefix"); + }); + + test("invalid/integer/us-after-oct", () => { + const input: string = "us-after-oct = 0o_1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected at least one digit after the radix prefix"); + }); + + test("invalid/key/after-array", () => { + const input: string = '[[agencies]] owner = "S Cjelli"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a table header", + ); + }); + + test("invalid/key/after-table", () => { + const input: string = '[error] this = "should not be here"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a table header", + ); + }); + + test("invalid/key/after-value", () => { + const input: string = 'first = "Tom" last = "Preston-Werner" # INVALID\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/key/bare-invalid-character-01", () => { + const input: string = "! = 123\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '!'"); + }); + + test("invalid/key/bare-invalid-character-02", () => { + const input: string = "bare!key = 123\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '!'"); + }); + + test("invalid/key/dot", () => { + const input: string = ". = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '.'"); + }); + + test("invalid/key/dotdot", () => { + const input: string = ".. = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '.'"); + }); + + test("invalid/key/dotted-redefine-table-01", () => { + const input: string = "a = false\na.b = true\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'a'"); + }); + + test("invalid/key/dotted-redefine-table-02", () => { + const input: string = "# Defined a.b as int\na.b = 1\n# Tries to access it as table: error\na.b.c = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'b'"); + }); + + test("invalid/key/duplicate-keys-01", () => { + const input: string = 'name = "Tom"\nname = "Pradyun"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'name'"); + }); + + test("invalid/key/duplicate-keys-02", () => { + const input: string = "dupe = false\ndupe = true\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'dupe'"); + }); + + test("invalid/key/duplicate-keys-03", () => { + const input: string = 'spelling = "favorite"\n"spelling" = "favourite"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'spelling'"); + }); + + test("invalid/key/duplicate-keys-04", () => { + const input: string = 'spelling = "favorite"\n\'spelling\' = "favourite"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'spelling'"); + }); + + test("invalid/key/duplicate-keys-05", () => { + const input: string = 'a = 1\n"\\u0061" = 1\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'a'"); + }); + + test("invalid/key/duplicate-keys-06", () => { + const input: string = '"a\'b" = 1\n"a\\u0027b" = 2\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'a'b'"); + }); + + test("invalid/key/duplicate-keys-07", () => { + const input: string = '"" = 1\n"" = 2\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key ''"); + }); + + test("invalid/key/duplicate-keys-08", () => { + const input: string = "arr = [1]\narr = [2]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'arr'"); + }); + + test("invalid/key/duplicate-keys-09", () => { + const input: string = "tbl = {k=1}\ntbl = {kk=2}\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'tbl'"); + }); + + test("invalid/key/empty", () => { + const input: string = " = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '='"); + }); + + test("invalid/key/end-in-escape", () => { + const input: string = '"backslash is the last char\\\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: (0x0A)"); + }); + + test("invalid/key/escape", () => { + const input: string = '\\u00c0 = "latin capital letter A with grave"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '\\'"); + }); + + test("invalid/key/hash", () => { + const input: string = "a# = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '#'"); + }); + + test("invalid/key/multiline-key-01", () => { + const input: string = '"""key""" = 1\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '\"'"); + }); + + test("invalid/key/multiline-key-02", () => { + const input: string = "'''key''' = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '''"); + }); + + test("invalid/key/multiline-key-03", () => { + const input: string = '"""key""" = """v"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '\"'"); + }); + + test("invalid/key/multiline-key-04", () => { + const input: string = "'''key''' = '''v'''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '''"); + }); + + test("invalid/key/newline-01", () => { + const input: string = "barekey\n = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found (0x0A)"); + }); + + test("invalid/key/newline-02", () => { + const input: string = '"quoted\nkey" = 1\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/key/newline-03", () => { + const input: string = "'quoted\nkey' = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; literal strings cannot contain newlines", + ); + }); + + test("invalid/key/newline-04", () => { + const input: string = '"""long\nkey""" = 1\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '\"'"); + }); + + test("invalid/key/newline-05", () => { + const input: string = "'''long\nkey''' = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '''"); + }); + + test("invalid/key/newline-06", () => { + const input: string = "key =\n1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Missing value after '='; values must be on the same line", + ); + }); + + test("invalid/key/no-eol-01", () => { + const input: string = "a = 1 b = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/key/no-eol-02", () => { + const input: string = "0=0r=false\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: 'r'"); + }); + + test("invalid/key/no-eol-03", () => { + const input: string = '0=""o=""m=""r=""00="0"q="""0"""e="""0"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/key/no-eol-04", () => { + const input: string = '[[0000l0]]\n0="0"[[0000l0]]\n0="0"[[0000l0]]\n0="0"l="0"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/key/no-eol-05", () => { + const input: string = '0=[0]00=[0,0,0]t=["0","0","0"]s=[1000-00-00T00:00:00Z,2000-00-00T00:00:00Z]\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/key/no-eol-06", () => { + const input: string = "0=0r0=0r=false\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: 'r'"); + }); + + test("invalid/key/no-eol-07", () => { + const input: string = "0=0r0=0r=falsefal=false\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: 'r'"); + }); + + test("invalid/key/only-float", () => { + const input: string = "1.1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found (0x0A)"); + }); + + test("invalid/key/only-int", () => { + const input: string = "1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found (0x0A)"); + }); + + test("invalid/key/only-str", () => { + const input: string = '""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found (0x0A)"); + }); + + test("invalid/key/open-bracket", () => { + const input: string = "[abc = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found '='"); + }); + + test("invalid/key/partial-quoted", () => { + const input: string = 'partial"quoted" = 5\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '\"'"); + }); + + test("invalid/key/quoted-unclosed-01", () => { + const input: string = '"key = x\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/key/quoted-unclosed-02", () => { + const input: string = '"key\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/key/single-open-bracket", () => { + const input: string = "[\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0x0A)"); + }); + + test("invalid/key/space-quoted", () => { + const input: string = '# Tab literal between a and b below.\n"a" "b" = 1\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '\"'"); + }); + + test("invalid/key/space", () => { + const input: string = "a b = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found 'b'"); + }); + + test("invalid/key/special-character", () => { + const input: string = 'μ = "greek small letter mu"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0xCE)"); + }); + + test("invalid/key/start-bracket", () => { + const input: string = "[a]\n[xyz = 5\n[b]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found '='"); + }); + + test("invalid/key/start-dot", () => { + const input: string = ".key = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '.'"); + }); + + test("invalid/key/tab-quoted", () => { + const input: string = '# Tab literal between a and b below.\n"a"\t"b" = 1\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found '\"'"); + }); + + test("invalid/key/tab", () => { + const input: string = "# Tab literal between a and b below.\na\tb = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found 'b'"); + }); + + test("invalid/key/two-equals-01", () => { + const input: string = "key= = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found '='"); + }); + + test("invalid/key/two-equals-02", () => { + const input: string = "a==1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found '='"); + }); + + test("invalid/key/two-equals-03", () => { + const input: string = "a=b=1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "b"'); + }); + + test("invalid/key/without-value-01", () => { + const input: string = "key\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found (0x0A)"); + }); + + test("invalid/key/without-value-02", () => { + const input: string = "key = \n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Missing value after '='; values must be on the same line", + ); + }); + + test("invalid/key/without-value-03", () => { + const input: string = '"key"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found (0x0A)"); + }); + + test("invalid/key/without-value-04", () => { + const input: string = '"key" = \n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Missing value after '='; values must be on the same line", + ); + }); + + test("invalid/key/without-value-05", () => { + const input: string = "fs.fw\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected '=' after a key but found (0x0A)"); + }); + + test("invalid/key/without-value-06", () => { + const input: string = "fs.fw =\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Missing value after '='; values must be on the same line", + ); + }); + + test("invalid/key/without-value-07", () => { + const input: string = "fs.\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0x0A)"); + }); + + test("invalid/local-date/day-1digit", () => { + const input: string = "foo = 1997-09-9\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: expected a 2-digit day"); + }); + + test("invalid/local-date/feb-29", () => { + const input: string = '"not a leap year" = 2100-02-29\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/local-date/feb-30", () => { + const input: string = '"only 28 or 29 days in february" = 1988-02-30\n\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/local-date/mday-over", () => { + const input: string = + "# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on\n# ; month/year\nd = 2006-01-32\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/local-date/mday-under", () => { + const input: string = + "# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on\n# ; month/year\nd = 2006-01-00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/local-date/month-over", () => { + const input: string = "# date-month = 2DIGIT ; 01-12\nd = 2006-13-01\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: month must be between 01 and 12"); + }); + + test("invalid/local-date/month-under", () => { + const input: string = "# date-month = 2DIGIT ; 01-12\nd = 2007-00-01\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: month must be between 01 and 12"); + }); + + test("invalid/local-date/no-leads-with-milli", () => { + const input: string = '# Day "5" instead of "05"; the leading zero is required.\nwith-milli = 1987-07-5\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: expected a 2-digit day"); + }); + + test("invalid/local-date/no-leads", () => { + const input: string = '# Month "7" instead of "07"; the leading zero is required.\nno-leads = 1987-7-05\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: expected a 2-digit month"); + }); + + test("invalid/local-date/trailing-t", () => { + const input: string = "# Date cannot end with trailing T\nd = 2006-01-30T\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: expected 2-digit hours"); + }); + + test("invalid/local-date/y10k", () => { + const input: string = "# Maximum RFC3399 year is 9999.\nd = 10000-01-01\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '-'"); + }); + + test("invalid/local-date/year-3digits", () => { + const input: string = "foo = 199-09-09\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '-'"); + }); + + test("invalid/local-datetime/feb-29", () => { + const input: string = '"not a leap year" = 2100-02-29T15:15:15\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/local-datetime/feb-30", () => { + const input: string = '"only 28 or 29 days in february" = 1988-02-30T15:15:15\n\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/local-datetime/hour-over", () => { + const input: string = "# time-hour = 2DIGIT ; 00-23\nd = 2006-01-01T24:00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: hours must be between 00 and 23"); + }); + + test("invalid/local-datetime/mday-over", () => { + const input: string = + "# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on\n# ; month/year\nd = 2006-01-32T00:00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/local-datetime/mday-under", () => { + const input: string = + "# date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on\n# ; month/year\nd = 2006-01-00T00:00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: day is out of range for the month"); + }); + + test("invalid/local-datetime/minute-over", () => { + const input: string = "# time-minute = 2DIGIT ; 00-59\nd = 2006-01-01T00:60:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: minutes must be between 00 and 59"); + }); + + test("invalid/local-datetime/month-over", () => { + const input: string = "# date-month = 2DIGIT ; 01-12\nd = 2006-13-01T00:00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: month must be between 01 and 12"); + }); + + test("invalid/local-datetime/month-under", () => { + const input: string = "# date-month = 2DIGIT ; 01-12\nd = 2007-00-01T00:00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: month must be between 01 and 12"); + }); + + test("invalid/local-datetime/no-leads-with-milli", () => { + const input: string = + '# Day "5" instead of "05"; the leading zero is required.\nwith-milli = 1987-07-5T17:45:00.12\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: expected a 2-digit day"); + }); + + test("invalid/local-datetime/no-leads", () => { + const input: string = '# Month "7" instead of "07"; the leading zero is required.\nno-leads = 1987-7-05T17:45:00\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid date: expected a 2-digit month"); + }); + + test("invalid/local-datetime/no-t", () => { + const input: string = '# No "t" or "T" between the date and time.\nno-t = 1987-07-0517:45:00\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '1'"); + }); + + test("invalid/local-datetime/second-over", () => { + const input: string = + "# time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second\n# ; rules\nd = 2006-01-01T00:00:61\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: seconds must be between 00 and 60"); + }); + + test("invalid/local-datetime/time-no-leads", () => { + const input: string = "# Leading 0 is always required.\nd = 2023-10-01T1:32:00Z\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: expected 2-digit hours"); + }); + + test("invalid/local-datetime/y10k", () => { + const input: string = "# Maximum RFC3399 year is 9999.\nd = 10000-01-01 00:00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: '-'"); + }); + + test("invalid/local-time/hour-over", () => { + const input: string = "# time-hour = 2DIGIT ; 00-23\nd = 24:00:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: hours must be between 00 and 23"); + }); + + test("invalid/local-time/minute-over", () => { + const input: string = "# time-minute = 2DIGIT ; 00-59\nd = 00:60:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: minutes must be between 00 and 59"); + }); + + test("invalid/local-time/second-over", () => { + const input: string = + "# time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second\n# ; rules\nd = 00:00:61\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: seconds must be between 00 and 60"); + }); + + test("invalid/local-time/time-no-leads-01", () => { + const input: string = "# Leading 0 is always required.\nd = 1:32:00\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unexpected character after a value: ':'"); + }); + + test("invalid/local-time/time-no-leads-02", () => { + const input: string = "# Leading 0 is always required.\nd = 01:32:0\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid time: expected 2-digit seconds"); + }); + + test("invalid/local-time/trailing-dot", () => { + const input: string = "t = 12:13:14.\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Invalid time: expected at least one digit of fractional seconds", + ); + }); + + test("invalid/local-time/trailing-dotdot", () => { + const input: string = "t = 12:13:14..\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Invalid time: expected at least one digit of fractional seconds", + ); + }); + + test("invalid/spec-1.1.0/common-16-0", () => { + const input: string = + 'str4 = """Here are two quotation marks: "". Simple enough."""\nstr5 = """Here are three quotation marks: """.""" # INVALID\nstr5 = """Here are three quotation marks: ""\\"."""\nstr6 = """Here are fifteen quotation marks: ""\\"""\\"""\\"""\\"""\\"."""\n\n# "This," she said, "is just a pointless statement."\nstr7 = """"This," she said, "is just a pointless statement.""""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/spec-1.1.0/common-19-0", () => { + const input: string = + "quot15 = '''Here are fifteen quotation marks: \"\"\"\"\"\"\"\"\"\"\"\"\"\"\"'''\n\napos15 = '''Here are fifteen apostrophes: '''''''''''''''''' # INVALID\napos15 = \"Here are fifteen apostrophes: '''''''''''''''\"\n\n# 'That,' she said, 'is still pointless.'\nstr = ''''That,' she said, 'is still pointless.''''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Too many quotes at the end of a multi-line string"); + }); + + test("invalid/spec-1.1.0/common-2", () => { + const input: string = "key = # INVALID\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a value but found '#'"); + }); + + test("invalid/spec-1.1.0/common-46-0", () => { + const input: string = + '[fruit]\napple.color = "red"\napple.taste.sweet = true\n\n[fruit.apple] # INVALID\n# [fruit.apple.taste] # INVALID\n\n[fruit.apple.texture] # you can add sub-tables\nsmooth = true\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'apple'"); + }); + + test("invalid/spec-1.1.0/common-46-1", () => { + const input: string = + '[fruit]\napple.color = "red"\napple.taste.sweet = true\n\n# [fruit.apple] # INVALID\n[fruit.apple.taste] # INVALID\n\n[fruit.apple.texture] # you can add sub-tables\nsmooth = true\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'taste'"); + }); + + test("invalid/spec-1.1.0/common-49-0", () => { + const input: string = '[product]\ntype = { name = "Nail" }\ntype.edible = false # INVALID\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend table 'type' with a dotted key"); + }); + + test("invalid/spec-1.1.0/common-5", () => { + const input: string = + '= "no key name" # INVALID\n"""key""" = "not allowed" # INVALID\n"" = "blank" # VALID but discouraged\n\'\' = \'blank\' # VALID but discouraged\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '='"); + }); + + test("invalid/spec-1.1.0/common-50-0", () => { + const input: string = '[product]\ntype.name = "Nail"\ntype = { edible = false } # INVALID\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'type'"); + }); + + test("invalid/string/bad-byte-escape", () => { + const input: string = 'naughty = "\\xAg"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A hex escape must be followed by exactly 2 hex digits", + ); + }); + + test("invalid/string/bad-concat", () => { + const input: string = 'no_concat = "first" "second"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/string/bad-escape-01", () => { + const input: string = 'invalid-escape = "This string has a bad \\a escape character."\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: 'a'"); + }); + + test("invalid/string/bad-escape-02", () => { + const input: string = 'invalid-escape = "This string has a bad \\ escape character."\n\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: (0x20)"); + }); + + test("invalid/string/bad-escape-03", () => { + const input: string = 'backslash = "\\"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/string/bad-escape-04", () => { + const input: string = 'a = "a \\\\\\ b"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: (0x20)"); + }); + + test("invalid/string/bad-escape-05", () => { + const input: string = 'a = "a \\\\\\\\\\ b"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: (0x20)"); + }); + + test("invalid/string/bad-hex-esc-01", () => { + const input: string = 'bad-hex-esc-01 = "\\x0g"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A hex escape must be followed by exactly 2 hex digits", + ); + }); + + test("invalid/string/bad-hex-esc-02", () => { + const input: string = 'bad-hex-esc-02 = "\\xG0"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A hex escape must be followed by exactly 2 hex digits", + ); + }); + + test("invalid/string/bad-hex-esc-03", () => { + const input: string = 'bad-hex-esc-03 = "\\x"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A hex escape must be followed by exactly 2 hex digits", + ); + }); + + test("invalid/string/bad-hex-esc-04", () => { + const input: string = 'bad-hex-esc-04 = "\\x 50"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A hex escape must be followed by exactly 2 hex digits", + ); + }); + + test("invalid/string/bad-hex-esc-05", () => { + const input: string = 'bad-hex-esc-5 = "\\x 50"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A hex escape must be followed by exactly 2 hex digits", + ); + }); + + test("invalid/string/bad-multiline", () => { + const input: string = 'multi = "first line\nsecond line"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/string/bad-slash-escape", () => { + const input: string = 'invalid-escape = "This string has a bad \\/ escape character."\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: '/'"); + }); + + test("invalid/string/bad-uni-esc-01", () => { + const input: string = 'bad-uni-esc-01 = "val\\ue"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 4 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-02", () => { + const input: string = 'bad-uni-esc-02 = "val\\Ux"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 8 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-03", () => { + const input: string = 'bad-uni-esc-03 = "val\\U0000000"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 8 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-04", () => { + const input: string = 'bad-uni-esc-04 = "val\\U0000"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 8 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-05", () => { + const input: string = 'bad-uni-esc-05 = "val\\Ugggggggg"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 8 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-06", () => { + const input: string = 'bad-uni-esc-06 = "This string contains a non scalar unicode codepoint \\uD801"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Escaped code point must be a Unicode scalar value"); + }); + + test("invalid/string/bad-uni-esc-07", () => { + const input: string = 'bad-uni-esc-07 = "\\uabag"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 4 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-ml-01", () => { + const input: string = 'bad-uni-esc-ml-01 = """val\\ue"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 4 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-ml-02", () => { + const input: string = 'bad-uni-esc-ml-02 = """val\\Ux"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 8 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-ml-03", () => { + const input: string = 'bad-uni-esc-ml-03 = """val\\U0000000"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 8 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-ml-04", () => { + const input: string = 'bad-uni-esc-ml-04 = """val\\U0000"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 8 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-ml-05", () => { + const input: string = 'bad-uni-esc-ml-05 = """val\\Ugggggggg"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 8 hex digits", + ); + }); + + test("invalid/string/bad-uni-esc-ml-06", () => { + const input: string = 'bad-uni-esc-ml-06 = """This string contains a non scalar unicode codepoint \\uD801"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Escaped code point must be a Unicode scalar value"); + }); + + test("invalid/string/bad-uni-esc-ml-07", () => { + const input: string = 'bad-uni-esc-ml-07 = """\\uabag"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: A Unicode escape must be followed by exactly 4 hex digits", + ); + }); + + test("invalid/string/basic-multiline-out-of-range-unicode-escape-01", () => { + const input: string = 'a = """\\UFFFFFFFF"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Escaped code point must be a Unicode scalar value"); + }); + + test("invalid/string/basic-multiline-out-of-range-unicode-escape-02", () => { + const input: string = 'a = """\\U00D80000"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Escaped code point must be a Unicode scalar value"); + }); + + test("invalid/string/basic-multiline-quotes", () => { + const input: string = 'str5 = """Here are three quotation marks: """."""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/string/basic-multiline-unknown-escape", () => { + const input: string = 'a = """\\@"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: '@'"); + }); + + test("invalid/string/basic-out-of-range-unicode-escape-01", () => { + const input: string = 'a = "\\UFFFFFFFF"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Escaped code point must be a Unicode scalar value"); + }); + + test("invalid/string/basic-out-of-range-unicode-escape-02", () => { + const input: string = 'a = "\\U00D80000"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Escaped code point must be a Unicode scalar value"); + }); + + test("invalid/string/basic-unknown-escape", () => { + const input: string = 'a = "\\@"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: '@'"); + }); + + test("invalid/string/literal-multiline-quotes-01", () => { + const input: string = "a = '''6 apostrophes: ''''''\n\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Too many quotes at the end of a multi-line string"); + }); + + test("invalid/string/literal-multiline-quotes-02", () => { + const input: string = "a = '''15 apostrophes: ''''''''''''''''''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Too many quotes at the end of a multi-line string"); + }); + + test("invalid/string/missing-quotes-array", () => { + const input: string = "name = [value]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "value"'); + }); + + test("invalid/string/missing-quotes-inline-table", () => { + const input: string = "name = { key = value }\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "value"'); + }); + + test("invalid/string/missing-quotes", () => { + const input: string = "name = value\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "value"'); + }); + + test("invalid/string/multiline-bad-escape-01", () => { + const input: string = 'k = """t\\a"""\n\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: 'a'"); + }); + + test("invalid/string/multiline-bad-escape-02", () => { + const input: string = '# \\ is not a valid escape.\nk = """t\\ t"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: (0x20)"); + }); + + test("invalid/string/multiline-bad-escape-03", () => { + const input: string = '# \\ is not a valid escape.\nk = """t\\ """\n\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: (0x20)"); + }); + + test("invalid/string/multiline-bad-escape-04", () => { + const input: string = 'backslash = """\\"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/multiline-escape-space-01", () => { + const input: string = 'a = """\n foo \\ \\n\n bar"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: (0x20)"); + }); + + test("invalid/string/multiline-escape-space-02", () => { + const input: string = 'bee = """\nhee \\\n\ngee \\ """\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid escape sequence: (0x20)"); + }); + + test("invalid/string/multiline-lit-no-close-01", () => { + const input: string = "invalid = '''\n this will fail\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/multiline-lit-no-close-02", () => { + const input: string = "x='''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/multiline-lit-no-close-03", () => { + const input: string = "not-closed= '''\ndiibaa\nblibae ete\neteta\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/multiline-lit-no-close-04", () => { + const input: string = "bee = '''\nhee\ngee ''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/multiline-no-close-01", () => { + const input: string = 'invalid = """\n this will fail\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/multiline-no-close-02", () => { + const input: string = 'x="""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/multiline-no-close-03", () => { + const input: string = 'not-closed= """\ndiibaa\nblibae ete\neteta\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/multiline-no-close-04", () => { + const input: string = 'bee = """\nhee\ngee ""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/multiline-no-close-05", () => { + const input: string = 'bee = """\nhee\ngee\\\t \n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/multiline-quotes-01", () => { + const input: string = 'a = """6 quotes: """"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Too many quotes at the end of a multi-line string"); + }); + + test("invalid/string/no-close-01", () => { + const input: string = 'no-ending-quote = "One time, at band camp\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/string/no-close-02", () => { + const input: string = '"a-string".must-be = "closed\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/string/no-close-03", () => { + const input: string = "no-ending-quote = 'One time, at band camp\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; literal strings cannot contain newlines", + ); + }); + + test("invalid/string/no-close-04", () => { + const input: string = "'a-string'.must-be = 'closed\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; literal strings cannot contain newlines", + ); + }); + + test("invalid/string/no-close-05", () => { + const input: string = '# No newline at end\nno-ending-quote = "One time, at band camp'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/no-close-06", () => { + const input: string = '# No newline at end\n"a-string".must-be = "closed'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/no-close-07", () => { + const input: string = "# No newline at end\nno-ending-quote = 'One time, at band camp"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/no-close-08", () => { + const input: string = "# No newline at end\n'a-string'.must-be = 'closed"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Unterminated string"); + }); + + test("invalid/string/no-close-09", () => { + const input: string = '# Newlines are not allowed in "-strings.\na = "\n"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/string/no-close-10", () => { + const input: string = "# Newlines are not allowed in '-strings.\na = '\n'\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; literal strings cannot contain newlines", + ); + }); + + test("invalid/string/no-open-01", () => { + const input: string = 's = a"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "a"'); + }); + + test("invalid/string/no-open-02", () => { + const input: string = 'a = [a"]\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "a"'); + }); + + test("invalid/string/no-open-03", () => { + const input: string = "s = a'\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "a"'); + }); + + test("invalid/string/no-open-04", () => { + const input: string = "a = [a']\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "a"'); + }); + + test("invalid/string/no-open-05", () => { + const input: string = 'a = a"""\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "a"'); + }); + + test("invalid/string/no-open-06", () => { + const input: string = 'a = [a"""]\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "a"'); + }); + + test("invalid/string/no-open-07", () => { + const input: string = "a = a'''\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "a"'); + }); + + test("invalid/string/no-open-08", () => { + const input: string = "a = [a''']\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe('TOML Parse error: Strings must be quoted: "a"'); + }); + + test("invalid/string/text-after-string", () => { + const input: string = 'string = "Is there life after strings?" No.\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); + + test("invalid/string/wrong-close", () => { + const input: string = "bad-ending-quote = \"double and single'\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/table/append-with-dotted-keys-01", () => { + const input: string = + '# First a.b.c defines a table: a.b.c = {z=9}\n#\n# Then we define a.b.c.t = "str" to add a str to the above table, making it:\n#\n# a.b.c = {z=9, t="..."}\n#\n# While this makes sense, logically, it was decided this is not valid TOML as\n# it\'s too confusing/convoluted.\n# \n# See: https://github.com/toml-lang/toml/issues/846\n# https://github.com/toml-lang/toml/pull/859\n\n[a.b.c]\n z = 9\n\n[a]\n b.c.t = "Using dotted keys to add to [a.b.c] after explicitly defining it above is not allowed"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend table 'b' with a dotted key"); + }); + + test("invalid/table/append-with-dotted-keys-02", () => { + const input: string = + '# This is the same issue as in injection-1.toml, except that nests one level\n# deeper. See that file for a more complete description.\n\n[a.b.c.d]\n z = 9\n\n[a]\n b.c.d.k.t = "Using dotted keys to add to [a.b.c.d] after explicitly defining it above is not allowed"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend table 'b' with a dotted key"); + }); + + test("invalid/table/append-with-dotted-keys-03", () => { + const input: string = "[[a.b]]\n\n[a]\nb.y = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'b'"); + }); + + test("invalid/table/append-with-dotted-keys-04", () => { + const input: string = + '[dependencies.foo]\nversion = "0.16"\n\n[dependencies]\nlibc = "0.2"\n\n[dependencies]\nrand = "0.3.14"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'dependencies'"); + }); + + test("invalid/table/append-with-dotted-keys-05", () => { + const input: string = "a.b.c = 1\na.b = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'b'"); + }); + + test("invalid/table/append-with-dotted-keys-06", () => { + const input: string = "a = 1\na.b = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'a'"); + }); + + test("invalid/table/append-with-dotted-keys-07", () => { + const input: string = 'a = {k1 = 1, k1.name = "joe"}\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'k1'"); + }); + + test("invalid/table/append-with-dotted-keys-08", () => { + const input: string = + '[a.b.c]\nz = 9\n\n[[totally_unrelated]]\nx = 123\n\n[a]\nb.c.t = "this should NOT be allowed"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend table 'b' with a dotted key"); + }); + + test("invalid/table/array-empty", () => { + const input: string = '[[]]\nname = "Born to Run"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found ']'"); + }); + + test("invalid/table/array-implicit", () => { + const input: string = + '# This test is a bit tricky. It should fail because the first use of\n# `[[albums.songs]]` without first declaring `albums` implies that `albums`\n# must be a table. The alternative would be quite weird. Namely, it wouldn\'t\n# comply with the TOML spec: "Each double-bracketed sub-table will belong to \n# the most *recently* defined table element *above* it."\n#\n# This is in contrast to the *valid* test, table-array-implicit where\n# `[[albums.songs]]` works by itself, so long as `[[albums]]` isn\'t declared\n# later. (Although, `[albums]` could be.)\n[[albums.songs]]\nname = "Glory Days"\n\n[[albums]]\nname = "Born in the USA"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'albums' as an array of tables"); + }); + + test("invalid/table/array-no-close-01", () => { + const input: string = '[[albums]\nname = "Born to Run"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']]' to close an array-of-tables header but found (0x0A)", + ); + }); + + test("invalid/table/array-no-close-02", () => { + const input: string = "[[closing-bracket.missing]\nblaa=2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']]' to close an array-of-tables header but found (0x0A)", + ); + }); + + test("invalid/table/array-no-close-03", () => { + const input: string = "[[a\n[[b]]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']]' to close an array-of-tables header but found (0x0A)", + ); + }); + + test("invalid/table/array-no-close-04", () => { + const input: string = "[[a\nb = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']]' to close an array-of-tables header but found (0x0A)", + ); + }); + + test("invalid/table/bare-invalid-character-01", () => { + const input: string = "[!]\nk = 123\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '!'"); + }); + + test("invalid/table/bare-invalid-character-02", () => { + const input: string = "[bare!key]\nk = 123\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found '!'"); + }); + + test("invalid/table/dot", () => { + const input: string = "[.]\nk = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '.'"); + }); + + test("invalid/table/dotdot", () => { + const input: string = "[..]\nk = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '.'"); + }); + + test("invalid/table/duplicate-key-01", () => { + const input: string = "[a]\nb = 1\n\n[a]\nc = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'a'"); + }); + + test("invalid/table/duplicate-key-02", () => { + const input: string = '[fruit]\ntype = "apple"\n\n[fruit.type]\napple = "yes"\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'type' as a table"); + }); + + test("invalid/table/duplicate-key-03", () => { + const input: string = '[fruit]\napple.color = "red"\n\n[[fruit.apple]]\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'apple' as an array of tables"); + }); + + test("invalid/table/duplicate-key-04", () => { + const input: string = '[fruit]\napple.color = "red"\n\n[fruit.apple] # INVALID\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'apple'"); + }); + + test("invalid/table/duplicate-key-05", () => { + const input: string = "[fruit]\napple.taste.sweet = true\n\n[fruit.apple.taste] # INVALID\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'taste'"); + }); + + test("invalid/table/duplicate-key-06", () => { + const input: string = "[tbl]\n[[tbl]]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'tbl' as an array of tables"); + }); + + test("invalid/table/duplicate-key-07", () => { + const input: string = "[[tbl]]\n[tbl]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine array of tables 'tbl' as a table"); + }); + + test("invalid/table/duplicate-key-08", () => { + const input: string = "[a]\nb = { c = 2, d = {} }\n[a.b]\nc = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine inline table 'b'"); + }); + + test("invalid/table/duplicate-key-09", () => { + const input: string = '[a]\nfoo="bar"\n[a.b]\nfoo="bar"\n[a]\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'a'"); + }); + + test("invalid/table/duplicate-key-10", () => { + const input: string = "a = []\n[[a.b]]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot extend array 'a'"); + }); + + test("invalid/table/duplicate-key-11", () => { + const input: string = "[a]\n[a.b]\n[a.b]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'b'"); + }); + + test("invalid/table/duplicate-key-12", () => { + const input: string = "[a]\n[a.b]\nc = 1\n[a.b]\nc = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'b'"); + }); + + test("invalid/table/empty-implicit-table", () => { + const input: string = "[naughty..naughty]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '.'"); + }); + + test("invalid/table/empty", () => { + const input: string = "[]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found ']'"); + }); + + test("invalid/table/equals-sign", () => { + const input: string = "[name=bad]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found '='"); + }); + + test("invalid/table/llbrace", () => { + const input: string = "[ [table]]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found '['"); + }); + + test("invalid/table/multiline-key-01", () => { + const input: string = '["""tbl"""]\nk = 1\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found '\"'"); + }); + + test("invalid/table/multiline-key-02", () => { + const input: string = "['''tbl''']\nk = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found '''"); + }); + + test("invalid/table/nested-brackets-close", () => { + const input: string = "[a]b]\nzyx = 42\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a table header", + ); + }); + + test("invalid/table/nested-brackets-open", () => { + const input: string = "[a[b]\nzyx = 42\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found '['"); + }); + + test("invalid/table/newline-01", () => { + const input: string = "[tbl\n]\nk = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']' to close a table header but found (0x0A)", + ); + }); + + test("invalid/table/newline-02", () => { + const input: string = '["tbl\n"]\nk = 1\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/table/newline-03", () => { + const input: string = '["tbl"\n]\nk = 1\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']' to close a table header but found (0x0A)", + ); + }); + + test("invalid/table/newline-04", () => { + const input: string = "[tbl.\n]\nk = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0x0A)"); + }); + + test("invalid/table/newline-05", () => { + const input: string = "[tbl\n.sub]\nk = 1\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']' to close a table header but found (0x0A)", + ); + }); + + test("invalid/table/no-close-01", () => { + const input: string = "[where will it end\nname = value\n\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found 'w'"); + }); + + test("invalid/table/no-close-02", () => { + const input: string = "[closing-bracket.missingö\nblaa=2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']' to close a table header but found (0xC3)", + ); + }); + + test("invalid/table/no-close-03", () => { + const input: string = '["where will it end]\nname = value\n\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + }); + + test("invalid/table/no-close-04", () => { + const input: string = "[\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found (0x0A)"); + }); + + test("invalid/table/no-close-05", () => { + const input: string = "[fwfw.wafw\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']' to close a table header but found (0x0A)", + ); + }); + + test("invalid/table/no-close-06", () => { + const input: string = "[a\n[b]\n[c\n[d]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']' to close a table header but found (0x0A)", + ); + }); + + test("invalid/table/no-close-07", () => { + const input: string = "[']\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Unterminated string; literal strings cannot contain newlines", + ); + }); + + test("invalid/table/no-close-08", () => { + const input: string = "[''']\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found '''"); + }); + + test("invalid/table/no-close-09", () => { + const input: string = '["where will it end""]\nname = value\n'; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found '\"'"); + }); + + test("invalid/table/overwrite-array-in-parent", () => { + const input: string = "[[parent-table.arr]]\n[parent-table]\nnot-arr = 1\narr = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'arr'"); + }); + + test("invalid/table/overwrite-bool-with-array", () => { + const input: string = "a=true\n[[a]]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'a' as an array of tables"); + }); + + test("invalid/table/overwrite-with-deep-table", () => { + const input: string = "a=1\n[a.b.c.d]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'a' as a table"); + }); + + test("invalid/table/redefine-01", () => { + const input: string = "# Define b as int, and try to use it as a table: error\n[a]\nb = 1\n\n[a.b]\nc = 2\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine key 'b' as a table"); + }); + + test("invalid/table/redefine-02", () => { + const input: string = + "# Define t2 as a table via dotted key in [t1] block, and then redefine [t1.t2]\n[t1]\nt2.t3.v = 0\n[t1.t2]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 't2'"); + }); + + test("invalid/table/redefine-03", () => { + const input: string = + "# Define t2.t3 as a table via dotted key in [t1] block, and then redefine [t1.t2.t3]\n[t1]\nt2.t3.v = 0\n[t1.t2.t3]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 't3'"); + }); + + test("invalid/table/rrbrace", () => { + const input: string = "[[table] ]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected ']]' to close an array-of-tables header but found (0x20)", + ); + }); + + test("invalid/table/super-twice", () => { + const input: string = "[a.b]\n[a]\n[a]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Cannot redefine table 'a'"); + }); + + test("invalid/table/text-after-table", () => { + const input: string = "[error] this shouldn't be here\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe( + "TOML Parse error: Expected a newline or end of file after a table header", + ); + }); + + test("invalid/table/trailing-dot", () => { + const input: string = "[a.]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected a key but found ']'"); + }); + + test("invalid/table/whitespace", () => { + const input: string = "[invalid key]\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found 'k'"); + }); + + test("invalid/table/with-pound", () => { + const input: string = "[key#group]\nanswer = 42\n"; + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Expected ']' to close a table header but found '#'"); + }); +}); + +// These inputs are not valid UTF-8, so they are passed as raw bytes; a TOML +// document must be valid UTF-8 as a whole. +describe("toml-test/invalid-encoding", () => { + test("invalid/encoding/bad-codepoint", () => { + const input = Buffer.from("IyBJbnZhbGlkIGNvZGVwb2ludCBVK0Q4MDAgOiDtoIAK", "base64"); + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid UTF-8 byte sequence"); + }); + + test("invalid/encoding/bad-utf8-at-end", () => { + const input = Buffer.from( + "IyBUaGVyZSBpcyBhIDB4ZGEgYXQgYWZ0ZXIgdGhlIHF1b3RlcywgYW5kIG5vIEVPTCBhdCB0aGUgZW5kIG9mIHRoZSBmaWxlLgojCiMgVGhpcyBpcyBhIGJpdCBvZiBhbiBlZGdlIGNhc2U6IFRoaXMgaW5kaWNhdGVzIHRoZXJlIHNob3VsZCBiZSB0d28gYnl0ZXMKIyAoMGIxMTAxXzEwMTApIGJ1dCB0aGVyZSBpcyBubyBieXRlIHRvIGZvbGxvdyBiZWNhdXNlIGl0J3MgdGhlIGVuZCBvZiB0aGUgZmlsZS4KeCA9ICIiIiIiIto=", + "base64", + ); + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid UTF-8 byte sequence"); + }); + + test("invalid/encoding/bad-utf8-in-array", () => { + const input = Buffer.from( + "IyBodHRwczovL2dpdGh1Yi5jb20vbWFyemVyL3RvbWxwbHVzcGx1cy9pc3N1ZXMvMTAwCmZsID1bIFtbW1tbW1tbW1tbW1tbWzaAhgAAAC02wp8gAA==", + "base64", + ); + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid UTF-8 byte sequence"); + }); + + test("invalid/encoding/bad-utf8-in-comment", () => { + const input = Buffer.from("IyDDCg==", "base64"); + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid UTF-8 byte sequence"); + }); + + test("invalid/encoding/bad-utf8-in-multiline-literal", () => { + const input = Buffer.from( + "IyBUaGUgZm9sbG93aW5nIGxpbmUgY29udGFpbnMgYW4gaW52YWxpZCBVVEYtOCBzZXF1ZW5jZS4KYmFkID0gJycnwycnJwo=", + "base64", + ); + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid UTF-8 byte sequence"); + }); + + test("invalid/encoding/bad-utf8-in-multiline", () => { + const input = Buffer.from( + "IyBUaGUgZm9sbG93aW5nIGxpbmUgY29udGFpbnMgYW4gaW52YWxpZCBVVEYtOCBzZXF1ZW5jZS4KYmFkID0gIiIiwyIiIgo=", + "base64", + ); + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid UTF-8 byte sequence"); + }); + + test("invalid/encoding/bad-utf8-in-string-literal", () => { + const input = Buffer.from( + "IyBUaGUgZm9sbG93aW5nIGxpbmUgY29udGFpbnMgYW4gaW52YWxpZCBVVEYtOCBzZXF1ZW5jZS4KYmFkID0gJ8MnCg==", + "base64", + ); + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid UTF-8 byte sequence"); + }); + + test("invalid/encoding/bad-utf8-in-string", () => { + const input = Buffer.from( + "IyBUaGUgZm9sbG93aW5nIGxpbmUgY29udGFpbnMgYW4gaW52YWxpZCBVVEYtOCBzZXF1ZW5jZS4KYmFkID0gIsMiCg==", + "base64", + ); + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid UTF-8 byte sequence"); + }); + + test("invalid/encoding/utf16-bom", () => { + const input = Buffer.from("/v8AIwAgAFUAVABGAC0AMQA2ACAAdwBpAHQAaAAgAEIATwBNAAo=", "base64"); + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + expect((err as SyntaxError).message).toBe("TOML Parse error: Invalid UTF-8 byte sequence"); + }); +}); diff --git a/test/js/bun/toml/toml.test.ts b/test/js/bun/toml/toml.test.ts new file mode 100644 index 000000000000..43f9b20b21c1 --- /dev/null +++ b/test/js/bun/toml/toml.test.ts @@ -0,0 +1,785 @@ +import { TOML } from "bun"; +import { describe, expect, test } from "bun:test"; + +// Hand-written coverage beyond the official conformance suite +// (toml-test-suite.test.ts): the JS-facing API surface, JS value mapping, +// Bun-specific input types, and robustness on adversarial inputs. + +function syntaxError(input: string | Uint8Array): SyntaxError { + let err: unknown; + try { + TOML.parse(input); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(SyntaxError); + return err as SyntaxError; +} + +describe("input types", () => { + const doc = 'a = 1\n[t]\nb = "x"\n'; + const expected = { a: 1, t: { b: "x" } }; + + test("string", () => { + expect(TOML.parse(doc)).toEqual(expected); + }); + + test("Buffer", () => { + expect(TOML.parse(Buffer.from(doc))).toEqual(expected); + }); + + test("Uint8Array subarray respects byteOffset and length", () => { + const padded = Buffer.from("<<<" + doc + ">>>"); + expect(TOML.parse(padded.subarray(3, 3 + doc.length))).toEqual(expected); + }); + + test("DataView", () => { + const bytes = new TextEncoder().encode(doc); + expect(TOML.parse(new DataView(bytes.buffer))).toEqual(expected); + }); + + test("ArrayBuffer", () => { + expect(TOML.parse(new TextEncoder().encode(doc).buffer)).toEqual(expected); + }); + + test("SharedArrayBuffer", () => { + const bytes = new TextEncoder().encode(doc); + const sab = new SharedArrayBuffer(bytes.length); + new Uint8Array(sab).set(bytes); + expect(TOML.parse(sab)).toEqual(expected); + }); + + test("Blob parses synchronously", () => { + expect(TOML.parse(new Blob([doc]))).toEqual(expected); + }); + + test("DataView over a SharedArrayBuffer", () => { + const bytes = new TextEncoder().encode(doc); + const sab = new SharedArrayBuffer(bytes.length); + new Uint8Array(sab).set(bytes); + expect(TOML.parse(new DataView(sab))).toEqual(expected); + }); + + test("non-string values are coerced via toString", () => { + expect(TOML.parse({ toString: () => "a = 1" } as any)).toEqual({ a: 1 }); + // A number coerces to a string that is not valid TOML. + expect(() => TOML.parse(123 as any)).toThrow(SyntaxError); + }); + + test("null and undefined throw", () => { + expect(() => TOML.parse(null as any)).toThrow(); + expect(() => TOML.parse(undefined as any)).toThrow(); + expect(() => (TOML.parse as any)()).toThrow(); + }); + + test("invalid UTF-8 bytes throw SyntaxError", () => { + expect(syntaxError(new Uint8Array([0x61, 0x20, 0x3d, 0x20, 0xff])).message).toBe( + "TOML Parse error: Invalid UTF-8 byte sequence", + ); + }); + + test("lone surrogates: replaced in string input (USVString), rejected in byte input", () => { + // A JS string is converted to UTF-8 before parsing, so unpaired + // surrogates become U+FFFD (the same semantics as TextEncoder and the + // YAML/JSON5 siblings). The same content as bytes is ill-formed UTF-8 + // and must be rejected. + expect(TOML.parse('a = "\uD800"')).toEqual({ a: "�" }); + const encodedSurrogate = new Uint8Array([0x61, 0x20, 0x3d, 0x20, 0x22, 0xed, 0xa0, 0x80, 0x22]); + expect(syntaxError(encodedSurrogate).message).toBe("TOML Parse error: Invalid UTF-8 byte sequence"); + }); + + test("UTF-8 BOM is skipped in both string and byte input", () => { + expect(TOML.parse("\uFEFFa = 1")).toEqual({ a: 1 }); + expect(TOML.parse(new Uint8Array([0xef, 0xbb, 0xbf, 0x61, 0x20, 0x3d, 0x20, 0x31]))).toEqual({ a: 1 }); + }); +}); + +describe("JS value mapping", () => { + test("returns a plain object with Object.prototype", () => { + const o = TOML.parse("a = 1"); + expect(Object.getPrototypeOf(o)).toBe(Object.prototype); + }); + + test("__proto__ key becomes an own property, not the prototype", () => { + const o = TOML.parse('"__proto__" = 1') as any; + expect(Object.getOwnPropertyNames(o)).toEqual(["__proto__"]); + expect(Object.getOwnPropertyDescriptor(o, "__proto__")!.value).toBe(1); + expect(Object.getPrototypeOf(o)).toBe(Object.prototype); + // A table under "__proto__" must not pollute Object.prototype. + const p = TOML.parse('"__proto__" = { polluted = true }') as any; + expect(Object.getOwnPropertyDescriptor(p, "__proto__")!.value).toEqual({ polluted: true }); + expect(({} as any).polluted).toBeUndefined(); + }); + + test("__proto__ table becomes an own property", () => { + const o = TOML.parse('["__proto__"]\nx = 1') as any; + expect(Object.getOwnPropertyDescriptor(o, "__proto__")!.value).toEqual({ x: 1 }); + expect(Object.getPrototypeOf(o)).toBe(Object.prototype); + }); + + test("constructor and prototype keys are plain data properties", () => { + const o = TOML.parse("constructor = 1\nprototype = 2") as any; + expect(o.constructor).toBe(1); + expect(o.prototype).toBe(2); + }); + + test("digit-only bare keys are strings regardless of magnitude", () => { + // Keys are always strings; the integer range rules never apply to them. + // Tables keyed by snowflake IDs are the realistic shape of this. + expect(TOML.parse("9007199254740993 = 1")).toEqual({ "9007199254740993": 1 }); + expect(TOML.parse("[175928847299117063]\nk = 1")).toEqual({ "175928847299117063": { k: 1 } }); + expect(TOML.parse("t = { 99999999999999999999 = 1 }")).toEqual({ t: { "99999999999999999999": 1 } }); + }); + + test("property order: array-index keys first (JS semantics), then insertion order", () => { + const o = TOML.parse('b = 1\na = 2\n"2" = 3\n"1" = 4') as any; + expect(Object.keys(o)).toEqual(["1", "2", "b", "a"]); + }); + + test("unicode keys are preserved without normalization", () => { + // NFC "é" (U+00E9) and NFD "é" (U+0065 U+0301) are distinct keys. + const composed = "é"; + const decomposed = "é"; + const o = TOML.parse(`"${composed}" = 1\n"${decomposed}" = 2`) as any; + expect(o[composed]).toBe(1); + expect(o[decomposed]).toBe(2); + expect(Object.keys(o)).toHaveLength(2); + }); + + test("non-ASCII and astral-plane content round-trips", () => { + const o = TOML.parse('emoji = "🐰🐶"\n"日本語" = "テスト"\nmixed = "aé中🦊"') as any; + expect(o.emoji).toBe("🐰🐶"); + expect(o["日本語"]).toBe("テスト"); + expect(o.mixed).toBe("aé中🦊"); + }); +}); + +describe("numbers", () => { + test("safe integer boundaries", () => { + expect(TOML.parse(`max = 9007199254740991\nmin = -9007199254740991`)).toEqual({ + max: Number.MAX_SAFE_INTEGER, + min: Number.MIN_SAFE_INTEGER, + }); + expect(syntaxError("a = 9007199254740992").message).toBe( + "TOML Parse error: Integer cannot be losslessly represented as a JavaScript number; it must be within +/-(2^53 - 1)", + ); + expect(() => TOML.parse("a = -9007199254740992")).toThrow(SyntaxError); + // Out of even the 64-bit range. + expect(syntaxError("a = 99999999999999999999").message).toBe( + "TOML Parse error: Integer is outside the 64-bit signed range", + ); + }); + + test("the 64-bit boundary picks the right diagnostic", () => { + // i64::MIN is inside the 64-bit signed range, so it gets the lossless + // message; one further is genuinely outside the 64-bit range. + expect(syntaxError("a = -9223372036854775808").message).toBe( + "TOML Parse error: Integer cannot be losslessly represented as a JavaScript number; it must be within +/-(2^53 - 1)", + ); + expect(syntaxError("a = 9223372036854775807").message).toBe( + "TOML Parse error: Integer cannot be losslessly represented as a JavaScript number; it must be within +/-(2^53 - 1)", + ); + expect(syntaxError("a = -9223372036854775809").message).toBe( + "TOML Parse error: Integer is outside the 64-bit signed range", + ); + expect(syntaxError("a = 9223372036854775808").message).toBe( + "TOML Parse error: Integer is outside the 64-bit signed range", + ); + }); + + test("radix integers at the safe-range boundary", () => { + expect(TOML.parse("a = 0x1FFFFFFFFFFFFF")).toEqual({ a: Number.MAX_SAFE_INTEGER }); + expect(() => TOML.parse("a = 0x20000000000000")).toThrow(SyntaxError); + }); + + test("float -0.0 is negative zero; integer -0 is positive zero", () => { + expect(Object.is((TOML.parse("a = -0.0") as any).a, -0)).toBe(true); + expect(Object.is((TOML.parse("a = -0") as any).a, 0)).toBe(true); + }); + + test("inf and nan", () => { + const o = TOML.parse("a = inf\nb = -inf\nc = +inf\nd = nan\ne = -nan\nf = +nan") as any; + expect(o.a).toBe(Infinity); + expect(o.b).toBe(-Infinity); + expect(o.c).toBe(Infinity); + expect(Number.isNaN(o.d)).toBe(true); + expect(Number.isNaN(o.e)).toBe(true); + expect(Number.isNaN(o.f)).toBe(true); + }); + + test("underscores and exponents", () => { + expect(TOML.parse("a = 1_000_000\nb = 1_2.3_4e1_0\nc = 5e2\nd = 2E-3")).toEqual({ + a: 1000000, + b: 12.34e10, + c: 500, + d: 0.002, + }); + }); + + test("float precision is exact f64", () => { + const o = TOML.parse("a = 0.1\nb = 3.141592653589793\nc = 5e-324\nd = 1.7976931348623157e308") as any; + expect(o.a).toBe(0.1); + expect(o.b).toBe(Math.PI); + expect(o.c).toBe(Number.MIN_VALUE); + expect(o.d).toBe(Number.MAX_VALUE); + }); +}); + +describe("date/times return their source text", () => { + test("all four kinds", () => { + const o = TOML.parse( + ["odt = 1979-05-27T07:32:00Z", "ldt = 1979-05-27T07:32:00", "ld = 1979-05-27", "lt = 07:32:00"].join("\n"), + ) as any; + expect(o).toEqual({ + odt: "1979-05-27T07:32:00Z", + ldt: "1979-05-27T07:32:00", + ld: "1979-05-27", + lt: "07:32:00", + }); + for (const key of ["odt", "ldt", "ld", "lt"]) { + expect(typeof o[key]).toBe("string"); + } + }); + + test("source spelling is preserved verbatim", () => { + const o = TOML.parse( + [ + "lower = 1979-05-27t07:32:00.500z", + "space = 1979-05-27 07:32:00+13:00", + "frac = 07:32:00.999999999", + "noseconds = 07:32", + "datenoseconds = 1979-05-27T07:32Z", + ].join("\n"), + ) as any; + expect(o.lower).toBe("1979-05-27t07:32:00.500z"); + expect(o.space).toBe("1979-05-27 07:32:00+13:00"); + expect(o.frac).toBe("07:32:00.999999999"); + expect(o.noseconds).toBe("07:32"); + expect(o.datenoseconds).toBe("1979-05-27T07:32Z"); + }); + + test("offset date-times feed directly into Date and Temporal-style consumers", () => { + const odt = (TOML.parse("a = 1979-05-27T00:32:00-07:00") as any).a; + expect(new Date(odt).getTime()).toBe(296638320000); + }); +}); + +describe("strings", () => { + test("all escapes including TOML 1.1 \\x and \\e", () => { + expect(TOML.parse('a = "\\b\\t\\n\\f\\r\\"\\\\\\e\\x41\\u00e9\\U0001F600"')).toEqual({ + a: '\b\t\n\f\r"\\\x1b\x41é\u{1F600}', + }); + }); + + test("escaped NUL and control characters decode", () => { + expect((TOML.parse('a = "\\u0000\\u001F"') as any).a).toBe("\u0000\u001F"); + }); + + test("multi-line basic: leading newline trim, CRLF normalization, line-ending backslash", () => { + expect((TOML.parse('a = """\nline1\r\nline2"""') as any).a).toBe("line1\nline2"); + expect((TOML.parse('a = """\\\n trimmed"""') as any).a).toBe("trimmed"); + expect((TOML.parse('a = """one \\\n\n\n two"""') as any).a).toBe("one two"); + }); + + test("multi-line literal: verbatim except CRLF normalization", () => { + expect((TOML.parse("a = '''\nC:\\path\\to\\file'''") as any).a).toBe("C:\\path\\to\\file"); + expect((TOML.parse("a = '''x\r\ny'''") as any).a).toBe("x\ny"); + }); + + test("quotes adjacent to multi-line delimiters", () => { + // Open """, two content quotes, x, two content quotes, an escaped quote + // (consuming one of the trailing five), then a run of four: close + 1. + expect((TOML.parse('a = """""x""\\"""""') as any).a).toBe('""x""""'); + expect((TOML.parse("a = '''''x'''''") as any).a).toBe("''x''"); + }); + + test("single-line literal strings take backslashes verbatim", () => { + expect((TOML.parse("a = 'C:\\Users\\nodejs\\templates'") as any).a).toBe("C:\\Users\\nodejs\\templates"); + }); + + test("lone surrogate escapes are rejected", () => { + expect(syntaxError('a = "\\uD800"').message).toBe( + "TOML Parse error: Escaped code point must be a Unicode scalar value", + ); + expect(() => TOML.parse('a = "\\UFFFFFFFF"')).toThrow(SyntaxError); + }); + + test("CRLF in a single-line string gets the newline diagnostic, not the bare-CR one", () => { + expect(syntaxError('a = "x\r\ny"').message).toBe( + "TOML Parse error: Unterminated string; newlines must be escaped in basic strings", + ); + expect(syntaxError("a = 'x\r\ny'").message).toBe( + "TOML Parse error: Unterminated string; literal strings cannot contain newlines", + ); + // A genuinely bare CR keeps its own message. + expect(syntaxError('a = "x\ry"').message).toBe( + "TOML Parse error: Bare carriage return is not allowed; use \\r\\n or \\n", + ); + }); +}); + +describe("structure", () => { + test("the toml.io front-page example", () => { + const o = TOML.parse(` +title = "TOML Example" + +[owner] +name = "Tom Preston-Werner" + +[database] +enabled = true +ports = [ 8000, 8001, 8002 ] +data = [ ["delta", "phi"], [3.14] ] +temp_targets = { cpu = 79.5, case = 72.0 } + +[servers.alpha] +ip = "10.0.0.1" +role = "frontend" + +[servers.beta] +ip = "10.0.0.2" +role = "backend" +`); + expect(o).toEqual({ + title: "TOML Example", + owner: { name: "Tom Preston-Werner" }, + database: { + enabled: true, + ports: [8000, 8001, 8002], + data: [["delta", "phi"], [3.14]], + temp_targets: { cpu: 79.5, case: 72.0 }, + }, + servers: { + alpha: { ip: "10.0.0.1", role: "frontend" }, + beta: { ip: "10.0.0.2", role: "backend" }, + }, + }); + }); + + test("TOML 1.1 multi-line inline tables with trailing comma", () => { + expect( + TOML.parse(`t = { + a = 1, + # comments are allowed here + b = { c = 2 }, +}`), + ).toEqual({ t: { a: 1, b: { c: 2 } } }); + }); + + test("newlines and comments are not allowed between '=' and the value in inline tables", () => { + // keyval-sep is `ws %x3D ws`: ws-comment-newline is permitted around + // keyvals and commas, but never between '=' and the value. + expect(syntaxError("t = { a =\n1 }").message).toBe( + "TOML Parse error: Missing value after '='; values must be on the same line", + ); + expect(syntaxError("t = { a = # c\n1 }").message).toBe("TOML Parse error: Expected a value but found '#'"); + // A newline between the key and '=' is rejected for the same reason. + expect(() => TOML.parse("t = { a\n= 1 }")).toThrow(SyntaxError); + // The allowed positions (around keyvals and commas) still work. + expect(TOML.parse("t = {\na = 1\n,\nb = 2\n}")).toEqual({ t: { a: 1, b: 2 } }); + }); + + test("array of tables accumulates in order", () => { + const o = TOML.parse(` +[[fruit]] +name = "apple" +[fruit.physical] +color = "red" +[[fruit.variety]] +name = "red delicious" +[[fruit.variety]] +name = "granny smith" +[[fruit]] +name = "banana" +`) as any; + expect(o.fruit).toHaveLength(2); + expect(o.fruit[0].physical.color).toBe("red"); + expect(o.fruit[0].variety.map((v: any) => v.name)).toEqual(["red delicious", "granny smith"]); + expect(o.fruit[1]).toEqual({ name: "banana" }); + }); + + test("empty and comment-only documents parse to an empty table", () => { + expect(TOML.parse("")).toEqual({}); + expect(TOML.parse(" \n# just a comment\n\n")).toEqual({}); + expect(TOML.parse(new Uint8Array(0))).toEqual({}); + }); + + test("whole-document CRLF line endings", () => { + expect(TOML.parse('a = 1\r\n[t]\r\nb = "x"\r\n')).toEqual({ a: 1, t: { b: "x" } }); + }); +}); + +describe("robustness", () => { + // Recursion-overflow depths must hold on every build: release frames are + // much smaller than debug/ASAN frames, so a depth that overflows locally + // can parse successfully on a release build. 2M frames exceeds any stack + // even at tiny frame sizes, while the parses-fine depth of 1000 stays well + // under the limit even with large sanitizer frames. + const OVERFLOW_DEPTH = 2_000_000; + + test("deeply nested arrays throw instead of crashing", () => { + const open = Buffer.alloc(OVERFLOW_DEPTH, "[").toString(); + const close = Buffer.alloc(OVERFLOW_DEPTH, "]").toString(); + expect(() => TOML.parse("a = " + open + close)).toThrow(RangeError); + }); + + test("deeply nested inline tables throw instead of crashing", () => { + const open = Buffer.alloc(OVERFLOW_DEPTH * 6, "{ b = ").toString(); + const close = Buffer.alloc(OVERFLOW_DEPTH * 2, " }").toString(); + expect(() => TOML.parse("a = " + open + "1" + close)).toThrow(RangeError); + }); + + test("deep dotted keys parse beyond the old 512-segment cap", () => { + const depth = 1000; + const o = TOML.parse(Array(depth).fill("a").join(".") + " = 1"); + let cur: any = o; + for (let i = 0; i < depth - 1; i++) cur = cur.a; + expect(cur).toEqual({ a: 1 }); + }); + + test("extremely deep dotted keys and headers throw instead of crashing", () => { + // Parsing these is iterative (every segment is processed before the limit + // can trip), so unlike OVERFLOW_DEPTH this depth is paid in full: it must + // stay small enough to be fast in debug builds while still overflowing + // the JS-conversion recursion at release frame sizes. + const depth = 250_000; + const path = Buffer.alloc(depth * 2 - 1, "a.").toString(); + expect(() => TOML.parse(path + " = 1")).toThrow(RangeError); + expect(() => TOML.parse(`[${path}]`)).toThrow(RangeError); + }); + + test("a very long string value round-trips", () => { + const long = Buffer.alloc(1 << 20, "x").toString(); + expect((TOML.parse(`a = "${long}"`) as any).a).toBe(long); + }); + + test("a table with many keys preserves every entry", () => { + const n = 1000; + let doc = ""; + for (let i = 0; i < n; i++) doc += `key_${i} = ${i}\n`; + const o = TOML.parse(doc) as any; + expect(Object.keys(o)).toHaveLength(n); + expect(o.key_0).toBe(0); + expect(o[`key_${n - 1}`]).toBe(n - 1); + }); + + test("values survive garbage collection", () => { + const doc = 'a = "héllo wörld 🌍"\nb = [1, 2.5, true, "x"]\n[t]\nc = 1979-05-27\n'; + const results: unknown[] = []; + for (let i = 0; i < 100; i++) { + results.push(TOML.parse(doc)); + } + Bun.gc(true); + for (const o of results) { + expect(o).toEqual({ a: "héllo wörld 🌍", b: [1, 2.5, true, "x"], t: { c: "1979-05-27" } }); + } + }); +}); + +describe("error contract", () => { + test("errors are SyntaxError instances with the TOML Parse error prefix", () => { + const err = syntaxError("a = = ="); + expect(err).toBeInstanceOf(Error); + expect(err.name).toBe("SyntaxError"); + expect(err.message).toStartWith("TOML Parse error: "); + }); + + test("end-of-file errors name the end of file", () => { + expect(syntaxError("a").message).toBe("TOML Parse error: Expected '=' after a key but found end of file"); + expect(syntaxError("[a").message).toBe( + "TOML Parse error: Expected ']' to close a table header but found end of file", + ); + }); + + test("array-of-tables errors name the right header kind", () => { + expect(syntaxError("[[a").message).toBe( + "TOML Parse error: Expected ']]' to close an array-of-tables header but found end of file", + ); + expect(syntaxError("a = 1\n[[a]]").message).toBe("TOML Parse error: Cannot redefine key 'a' as an array of tables"); + // For a non-last segment the table wording is the correct one. + expect(syntaxError("a = 1\n[[a.b]]").message).toBe("TOML Parse error: Cannot redefine key 'a' as a table"); + expect(syntaxError("a = 1\n[a]").message).toBe("TOML Parse error: Cannot redefine key 'a' as a table"); + }); + + test("unquoted string values name the fix", () => { + // The old parser silently accepted bare words as strings; this is the + // most common spec violation in real-world bunfig.toml files. + expect(syntaxError("linker = isolated").message).toBe('TOML Parse error: Strings must be quoted: "isolated"'); + expect(syntaxError("a = tru").message).toBe('TOML Parse error: Strings must be quoted: "tru"'); + expect(syntaxError("a = nope").message).toBe('TOML Parse error: Strings must be quoted: "nope"'); + // Bare words that merely start with inf/nan are unquoted strings too. + expect(syntaxError("timeout = infinity").message).toBe('TOML Parse error: Strings must be quoted: "infinity"'); + expect(syntaxError("unit = nanoseconds").message).toBe('TOML Parse error: Strings must be quoted: "nanoseconds"'); + }); + + test("common mistakes produce specific messages", () => { + expect(syntaxError("a = 1\na = 2").message).toBe("TOML Parse error: Cannot redefine key 'a'"); + expect(syntaxError("[a]\n[a]").message).toBe("TOML Parse error: Cannot redefine table 'a'"); + expect(syntaxError("a = 01").message).toBe("TOML Parse error: Leading zeros are not allowed in numbers"); + expect(syntaxError("a = 1_").message).toBe("TOML Parse error: Underscores in numbers must be surrounded by digits"); + expect(syntaxError('a = "x" y = 2').message).toBe( + "TOML Parse error: Expected a newline or end of file after a key/value pair", + ); + }); +}); + +describe("TOML.stringify", () => { + function stringifyError(value: unknown): Error { + let err: unknown; + try { + TOML.stringify(value); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(Error); + return err as Error; + } + + test("layout: keyvals first, then tables, then arrays of tables", () => { + expect( + TOML.stringify({ + server: { host: "localhost", port: 8080 }, + name: "app", + points: [{ x: 1 }, { x: 2 }], + debug: true, + }), + ).toBe( + 'name = "app"\ndebug = true\n\n[server]\nhost = "localhost"\nport = 8080\n\n[[points]]\nx = 1\n\n[[points]]\nx = 2\n', + ); + }); + + test("nested tables emit dotted headers", () => { + expect(TOML.stringify({ a: { b: { c: 1 } } })).toBe("[a]\n\n[a.b]\nc = 1\n"); + }); + + test("round-trips through TOML.parse", () => { + const value = { + title: "example", + count: 42, + pi: 3.14, + on: true, + off: false, + list: [1, "two", [3.5], { four: 4 }], + empty: [], + nested: { deep: { key: "value" } }, + multi: 'line one\nline two\t"quoted"', + }; + expect(TOML.parse(TOML.stringify(value))).toEqual(value); + }); + + test("keys: bare when possible, quoted otherwise", () => { + expect(TOML.stringify({ bare_key: 1, "key with space": 2, ключ: 3, "": 4, "a.b": 5 })).toBe( + 'bare_key = 1\n"key with space" = 2\n"ключ" = 3\n"" = 4\n"a.b" = 5\n', + ); + expect(TOML.stringify({ t: { "dotted.seg": 1 } })).toBe('[t]\n"dotted.seg" = 1\n'); + expect(TOML.stringify({ "dotted.tbl": { a: 1 } })).toBe('["dotted.tbl"]\na = 1\n'); + }); + + test("string escaping is exact and round-trips", () => { + expect(TOML.stringify({ s: 'a"b\\c\nd\te\u0000f' })).toBe('s = "a\\"b\\\\c\\nd\\te\\u0000f"\n'); + const original = { s: '\b\t\n\f\r"\\中🦊' }; + expect(TOML.parse(TOML.stringify(original))).toEqual(original); + }); + + test("lone surrogates are replaced with U+FFFD like the parse boundary", () => { + expect(TOML.stringify({ s: "a\uD800b" })).toBe('s = "a�b"\n'); + // A well-formed pair passes through. + expect(TOML.stringify({ s: "🦊" })).toBe('s = "🦊"\n'); + }); + + test("numbers: integers, floats, special values", () => { + expect(TOML.stringify({ i: 5, f: 0.5, nz: -0.0, n: NaN, p: Infinity, m: -Infinity })).toBe( + "i = 5\nf = 0.5\nnz = -0.0\nn = nan\np = inf\nm = -inf\n", + ); + expect(TOML.stringify({ max: Number.MAX_SAFE_INTEGER })).toBe("max = 9007199254740991\n"); + // A double-encoded +0 (not an int32-tagged value) must not gain a sign. + expect(TOML.stringify({ z: new Float64Array(1)[0] })).toBe("z = 0\n"); + expect(Object.is(TOML.parse(TOML.stringify({ z: new Float64Array(1)[0] })).z, 0)).toBe(true); + }); + + test("integral doubles beyond the safe range are emitted as floats", () => { + // Bare digits would round-trip as an out-of-range TOML integer. + expect(TOML.stringify({ big: 1e20 })).toBe("big = 100000000000000000000.0\n"); + expect(TOML.parse(TOML.stringify({ big: 1e20 }))).toEqual({ big: 1e20 }); + expect(TOML.parse(TOML.stringify({ big: 1e21 }))).toEqual({ big: 1e21 }); + }); + + test("Date becomes a TOML offset date-time", () => { + const d = new Date(Date.UTC(1979, 4, 27, 7, 32, 0, 999)); + expect(TOML.stringify({ d })).toBe("d = 1979-05-27T07:32:00.999Z\n"); + // parse returns datetimes as source-text strings. + expect(TOML.parse(TOML.stringify({ d }))).toEqual({ d: "1979-05-27T07:32:00.999Z" }); + expect(TOML.stringify({ d: new Date(0) })).toBe("d = 1970-01-01T00:00:00.000Z\n"); + // The 4-digit-year bounds (`Date.UTC(0, ...)` remaps year 0 to 1900, so raw ms). + expect(TOML.stringify({ d: new Date(-62167219200000) })).toBe("d = 0000-01-01T00:00:00.000Z\n"); + expect(TOML.stringify({ d: new Date(Date.UTC(9999, 11, 31, 23, 59, 59, 999)) })).toBe( + "d = 9999-12-31T23:59:59.999Z\n", + ); + }); + + test("invalid and unrepresentable Dates throw", () => { + expect(stringifyError({ d: new Date(NaN) }).message).toBe("TOML.stringify cannot serialize an invalid Date"); + // One millisecond before year 0000, and the first instant of year 10000. + expect(stringifyError({ d: new Date(-62167219200001) }).message).toBe( + "TOML.stringify cannot serialize a Date outside years 0000-9999", + ); + expect(stringifyError({ d: new Date(Date.UTC(10000, 0, 1)) }).message).toBe( + "TOML.stringify cannot serialize a Date outside years 0000-9999", + ); + }); + + test("null values throw with the offending key", () => { + expect(stringifyError({ a: { broken: null } }).message).toBe( + "TOML cannot represent null (key 'broken'); remove the key or use a sentinel value", + ); + expect(stringifyError({ list: [1, null] }).message).toBe("TOML cannot represent null in an array"); + expect(stringifyError({ list: [1, undefined] }).message).toBe("TOML cannot represent undefined in an array"); + }); + + test("BigInt throws like the YAML and JSON5 siblings", () => { + expect(stringifyError({ n: 1n }).message).toBe("TOML.stringify cannot serialize BigInt"); + }); + + test("circular structures throw", () => { + const cycle: any = { a: 1 }; + cycle.self = cycle; + expect(stringifyError(cycle).message).toBe("Converting circular structure to TOML"); + const arrCycle: any = { list: [] }; + arrCycle.list.push(arrCycle.list); + expect(stringifyError(arrCycle).message).toBe("Converting circular structure to TOML"); + }); + + test("top level must be a plain object", () => { + const msg = "TOML.stringify expects an object at the top level (a TOML document is a table)"; + expect(stringifyError([1, 2]).message).toBe(msg); + expect(stringifyError(null).message).toBe(msg); + expect(stringifyError("str").message).toBe(msg); + expect(stringifyError(5).message).toBe(msg); + expect(stringifyError(new Date(0)).message).toBe(msg); + expect(TOML.stringify(undefined)).toBeUndefined(); + }); + + test("replacer is rejected; space is accepted and ignored", () => { + expect(() => TOML.stringify({}, (() => 1) as any)).toThrow("TOML.stringify does not support the replacer argument"); + expect(TOML.stringify({ a: { b: 1 } }, null, 2)).toBe(TOML.stringify({ a: { b: 1 } })); + }); + + test("undefined, function, and symbol properties are skipped", () => { + expect(TOML.stringify({ a: 1, u: undefined, f: () => 1, s: Symbol("x") })).toBe("a = 1\n"); + }); + + test("empty shapes", () => { + expect(TOML.stringify({})).toBe(""); + expect(TOML.stringify({ t: {} })).toBe("[t]\n"); + expect(TOML.stringify({ a: [] })).toBe("a = []\n"); + expect(TOML.stringify({ a: [{}] })).toBe("[[a]]\n"); + }); + + test("mixed arrays use inline tables", () => { + expect(TOML.stringify({ a: [1, { b: 2 }, {}] })).toBe("a = [1, { b = 2 }, {}]\n"); + expect(TOML.parse(TOML.stringify({ a: [1, { b: 2 }] }))).toEqual({ a: [1, { b: 2 }] }); + }); + + test("boxed primitives unwrap", () => { + expect(TOML.stringify({ n: new Number(5), s: new String("x"), b: new Boolean(true) })).toBe( + 'n = 5\ns = "x"\nb = true\n', + ); + }); + + test("stringify is GC-safe under stress", () => { + // Unique keys each iteration force fresh WTF strings through the + // header-path bookkeeping; a refcount imbalance there crashes under GC. + for (let i = 0; i < 2000; i++) { + TOML.stringify({ ["table" + i]: { ["inner" + i]: { deep: [{ a: i }, { b: i }] } } }); + if (i % 256 === 0) Bun.gc(true); + } + Bun.gc(true); + expect(TOML.parse(TOML.stringify({ ok: true }))).toEqual({ ok: true }); + }); +}); + +// The TOML.stringify suite above covers parse(stringify(jsValue)). These cover +// the other direction, stringify of a value produced by parse (read, modify, +// write back), where the four date/time types lose their TOML type. +describe("stringify(parse) round-trips", () => { + test("all four date/time types become quoted strings on the way back out", () => { + // parse returns a date/time literal as the string of its source text, so + // stringify sees a plain string and must quote it. The TOML type changes, + // but the JS value is a fixed point after one stringify/parse lap. + const cases: [string, string][] = [ + ["d = 1979-05-27T07:32:00Z", 'd = "1979-05-27T07:32:00Z"\n'], + ["d = 1979-05-27T07:32:00", 'd = "1979-05-27T07:32:00"\n'], + ["d = 1979-05-27", 'd = "1979-05-27"\n'], + ["d = 07:32:00", 'd = "07:32:00"\n'], + ]; + for (const [doc, requoted] of cases) { + const once = TOML.parse(doc); + expect(TOML.stringify(once)).toBe(requoted); + expect(TOML.parse(TOML.stringify(once))).toEqual(once as any); + } + }); + + test("a date literal and a string of the same text are indistinguishable after parse", () => { + // This is why the previous test cannot preserve the TOML type: both + // documents produce the identical JS value, so stringify has nothing to go on. + expect(TOML.parse("a = 1979-05-27")).toEqual({ a: "1979-05-27" }); + expect(TOML.parse('a = "1979-05-27"')).toEqual({ a: "1979-05-27" }); + }); + + test("nan, inf, -inf, and signed zero round-trip as values, not just as text", () => { + // toEqual treats NaN as NaN and -0 as 0, so assert with Object.is. + for (const x of [NaN, Infinity, -Infinity, -0, 0]) { + expect(Object.is(TOML.parse(TOML.stringify({ x })).x, x)).toBe(true); + } + }); + + test("2 ** 53 is the first integral double emitted in float form", () => { + // TOML.parse rejects a bare integer one past Number.MAX_SAFE_INTEGER (the + // "losslessly represented" test above), so stringify's `.0` suffix at this + // boundary is what keeps its own output reparseable. + expect(TOML.stringify({ x: Number.MAX_SAFE_INTEGER })).toBe("x = 9007199254740991\n"); + expect(TOML.stringify({ x: 2 ** 53 })).toBe("x = 9007199254740992.0\n"); + expect(TOML.stringify({ x: -Number.MAX_SAFE_INTEGER })).toBe("x = -9007199254740991\n"); + expect(TOML.stringify({ x: -(2 ** 53) })).toBe("x = -9007199254740992.0\n"); + for (const x of [Number.MAX_SAFE_INTEGER, 2 ** 53, -Number.MAX_SAFE_INTEGER, -(2 ** 53)]) { + expect(TOML.parse(TOML.stringify({ x }))).toEqual({ x }); + } + // Without the suffix, the same digits are not reparseable at all. + expect(() => TOML.parse("x = 9007199254740992")).toThrow(SyntaxError); + }); + + test("float extremes round-trip exactly and the exponent form is valid TOML", () => { + for (const x of [Number.MAX_VALUE, Number.MIN_VALUE, Number.EPSILON, 1e-7, 1e-300, 0.1, 1 / 3]) { + expect(Object.is(TOML.parse(TOML.stringify({ x })).x, x)).toBe(true); + } + // JSC's shortest repr picks exponent form here; TOML allows `int-part exp`. + expect(TOML.stringify({ x: 1e-7 })).toBe("x = 1e-7\n"); + expect(TOML.stringify({ x: Number.MAX_VALUE })).toBe("x = 1.7976931348623157e+308\n"); + expect(TOML.stringify({ x: Number.MIN_VALUE })).toBe("x = 5e-324\n"); + }); + + test("stringify(parse(doc)) is a fixed point on a multi-type document", () => { + const doc = [ + 'title = "ex"', + "n = 5", + "f = 2.5", + "b = true", + "dt = 1979-05-27T07:32:00Z", + "ld = 1979-05-27", + "lt = 07:32:00", + "arr = [1, 2, 3]", + "mixed = [1, 'two', [3]]", + "[tbl]", + "k = 'v'", + "[[aot]]", + "x = 1", + "[[aot]]", + "x = 2", + ].join("\n"); + const once = TOML.parse(doc); + expect(TOML.parse(TOML.stringify(once))).toEqual(once as any); + // The emitted text is stable after one lap: a second stringify/parse + // produces the identical document. + expect(TOML.stringify(TOML.parse(TOML.stringify(once)))).toBe(TOML.stringify(once)); + }); +});