/*! * Copyright (c) Squirrel Chat et al., All rights reserved. * SPDX-License-Identifier: BSD-3-Clause * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * 3. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import { TomlDate } from './date.js' import { TomlError } from './error.js' // let CTRL_REGEX = /[\x00-\x08\x0f-\x1f\x7f]/ let INT_REGEX = /^((0x[0-9a-fA-F](_?[0-9a-fA-F])*)|(([+-]|0[ob])?\d(_?\d)*))$/ let FLOAT_REGEX = /^[+-]?\d(_?\d)*(\.\d(_?\d)*)?([eE][+-]?\d(_?\d)*)?$/ let LEADING_ZERO = /^[+-]?0[0-9_]/ export function parseString(str: string, ptr: number): [string, number] { let c = str[ptr++]! let first = c let isLiteral = c === "'" let isMultiline = c === str[ptr] && c === str[ptr + 1] if (isMultiline) { // Trim initial newline if (str[ptr += 2] === '\n') ptr++ else if (str[ptr] === '\r' && str[ptr + 1] === '\n') ptr += 2 } /* The fast path does not seem to bring significant performance gains, so it's commented out. Kept for reference and/or future fafoing. Without: spec 5.08 µs/iter 3.88 ipc (99.44% cache) 23.90 branch misses 28.61k cycles 111.01k instructions 5MB 115.73 ms/iter 2.51 ipc (98.36% cache) 3.12M branch misses 619.30M cycles 1.56G instructions With: spec 5.09 µs/iter 3.90 ipc (99.46% cache) 24.42 branch misses 28.57k cycles 111.49k instructions 5MB 113.89 ms/iter 2.47 ipc (98.38% cache) 3.12M branch misses 611.94M cycles 1.51G instructions if (c === "'") { // Literal strings fast path - no transform needs to occur; just grab the str and that's it let endPtr = str.indexOf(isMultiline ? "'''" : "'", ptr) if (endPtr < 0) { throw new TomlError("unfinished string literal", { toml: str, ptr }) } if (isMultiline) { // If the string ends with 4-5 quotes, then the first 1-2 are part of the string if (str[endPtr + 3] === "'") endPtr++ if (str[endPtr + 3] === "'") endPtr++ } let string = str.slice(ptr, endPtr) if (CTRL_REGEX.test(string)) { let match = string.match(CTRL_REGEX)! throw new TomlError('control characters are not allowed in strings', { toml: str, ptr: ptr + (match.index ?? 0) }) } return [string, endPtr + (isMultiline ? 3 : 1)] } */ let parsed = '' let sliceStart = ptr // states: // 0 - decoding // 1 - decoding escape // 2 - whitespace escape (no newline encountered yet, must fail on non-whitespace) // 3 - whitespace escape (newline encountered, allowed to transition back to normal decode) let state = 0 for (let i = ptr; i < str.length; i++) { c = str[i]! // Deal with newlines first, since that simplifies control character checking and handling across all states if (isMultiline && (c === '\n' || (c === '\r' && str[i + 1] === '\n'))) { state = state && 3 } // Control characters are banned in TOML, so we throw an error if we encounter them else if ((c < '\x20' && c !== '\t') || c === '\x7f') { throw new TomlError('control characters are not allowed in strings', { toml: str, ptr: i, }) } // The string might terminate while we're parsing through a newline escape. // It must have encountered a newline; otherwise, it'll simply fail in another branch. else if ((!state || state === 3) && c === first && (!isMultiline || (str[i + 1] === first && str[i + 2] === first))) { if (isMultiline) { // If the string ends with 4-5 quotes, then the first 1-2 are part of the string if (str[i + 3] === first) i++ if (str[i + 3] === first) i++ } return [ // If we're in a newline escape still, then there's nothing to add. // Also try to avoid concat if there's nothing to add to parsed, or nothing has been added to parsed. state ? parsed : parsed + str.slice(sliceStart, i), i + (isMultiline ? 3 : 1), ] } else if (!state) { if (!isLiteral && c === '\\') { parsed += str.slice(sliceStart, (sliceStart = i)) state = 1 } } else if (state === 1) { if (c === 'x' || c === 'u' || c === 'U') { // Unicode escape let value = 0 let len = c === 'x' ? 2 : c === 'u' ? 4 : 8 for (let j = 0; j < len; j++, i++) { let hex = str.charCodeAt(i + 1) let digit = /* 0-9 */ hex >= 0x30 && hex <= 0x39 ? hex - 0x30 : /* A-F */ hex >= 0x41 && hex <= 0x46 ? hex - 0x41 + 10 : /* a-f */ hex >= 0x61 && hex <= 0x66 ? hex - 0x61 + 10 : -1 if (digit < 0) throw new TomlError('invalid non-hex character in unicode escape', { toml: str, ptr: i + 1 }) value = (value << 4) | digit } // Because JS does bitwise on signed 32bit integers, all 0xfzzzzzzz values are actually seen as negative if (value < 0 || value > 0x10ffff || (value >= 0xd800 && value <= 0xdfff)) { throw new TomlError('invalid unicode escape', { toml: str, ptr: i }) } parsed += String.fromCodePoint(value) sliceStart = i + 1 state = 0 } else if (c === ' ' || c === '\t') { // If it was a newline, it'd have been handled earlier state = 2 } else { if (c === 'b') parsed += '\b' else if (c === 't') parsed += '\t' else if (c === 'n') parsed += '\n' else if (c === 'f') parsed += '\f' else if (c === 'r') parsed += '\r' else if (c === 'e') parsed += '\x1b' else if (c === '"') parsed += '"' else if (c === '\\') parsed += '\\' else throw new TomlError('unrecognized escape sequence', { toml: str, ptr: i }) sliceStart = i + 1 state = 0 } } else if (c !== ' ' && c !== '\t') { if (state === 2) { throw new TomlError('invalid escape: only line-ending whitespace may be escaped', { toml: str, ptr: sliceStart, }) } // State cannot be zero, or we'd have branched earlier already. // If it's a backslash, immediately transition to the escape state so it can be processed. state = !isLiteral && c === '\\' ? 1 : 0 sliceStart = i } } throw new TomlError('unfinished string', { toml: str, ptr }) } export type IntegersAsBigInt = undefined | boolean | 'asNeeded' export function parseValue(value: string, toml: string, ptr: number, integersAsBigInt: IntegersAsBigInt): boolean | number | bigint | TomlDate { // Constant values if (value === 'true') return true if (value === 'false') return false if (value === '-inf') return -Infinity if (value === 'inf' || value === '+inf') return Infinity if (value === 'nan' || value === '+nan' || value === '-nan') return NaN // Avoid FP representation of -0 if (value === '-0') return integersAsBigInt ? 0n : 0 // Numbers let isInt = INT_REGEX.test(value) if (isInt || FLOAT_REGEX.test(value)) { if (LEADING_ZERO.test(value)) { throw new TomlError('leading zeroes are not allowed', { toml: toml, ptr: ptr, }) } value = value.replace(/_/g, '') let numeric: number | bigint = +value if (isNaN(numeric)) { throw new TomlError('invalid number', { toml: toml, ptr: ptr, }) } if (isInt) { if ((isInt = !Number.isSafeInteger(numeric)) && !integersAsBigInt) { throw new TomlError('integer value cannot be represented losslessly', { toml: toml, ptr: ptr, }) } if (isInt || integersAsBigInt === true) numeric = BigInt(value) } return numeric } const date = new TomlDate(value) if (!date.isValid()) { throw new TomlError('invalid value', { toml: toml, ptr: ptr, }) } return date }