@sveltejs/kit
Version:
SvelteKit is the fastest way to build Svelte apps
934 lines (801 loc) • 26.6 kB
JavaScript
/** @import { BinaryFormMeta, InternalRemoteFormIssue } from 'types' */
/** @import { StandardSchemaV1 } from '@standard-schema/spec' */
import { DEV } from 'esm-env';
import * as devalue from 'devalue';
import { stream_from_iterable, text_decoder, text_encoder } from './utils.js';
import { noop } from '../utils/functions.js';
import { SvelteKitError } from '@sveltejs/kit/internal';
import * as e from '../messages/shared-errors.js';
import * as w from '../messages/shared-warnings.js';
/**
* Sets a parsed form field value in a nested object, mutating the original object.
* @param {Record<string, any>} object
* @param {{ name: string; type: 'number' | 'boolean' | null }} field
* @param {any} value
*/
export function set_nested_value(object, field, value) {
deep_set(object, split_path(field.name), value);
}
/**
* Separates a form field's path from the metadata encoded in its name.
* @param {string} form_id
* @param {string} key
* @returns {{ name: string; type: 'number' | 'boolean' | null; is_array: boolean }}
*/
export function parse_form_key(form_id, key) {
const suffix = '/' + form_id;
let name = key;
let image_coordinate = '';
if (name.startsWith('i:') && (name.endsWith(suffix + '.x') || name.endsWith(suffix + '.y'))) {
image_coordinate = name[name.length - 1];
name = name.slice(0, -2);
}
if (!name.endsWith(suffix)) {
e.form_field_unbound({ name });
}
name = name.slice(0, -suffix.length);
/** @type {'number' | 'boolean' | null} */
let type = null;
if (name.startsWith('n:')) {
name = name.slice(2);
type = 'number';
} else if (name.startsWith('b:')) {
name = name.slice(2);
type = 'boolean';
} else if (name.startsWith('i:')) {
name = name.slice(2);
type = 'number';
}
const is_array = name.endsWith('[]');
if (is_array) name = name.slice(0, -2);
if (image_coordinate) name += '.' + image_coordinate;
return { name, type, is_array };
}
/**
* @param {'number' | 'boolean' | null} type
* @param {any} value
* @returns {any}
*/
export function coerce_form_value(type, value) {
if (Array.isArray(value)) return value.map((value) => coerce_form_value(type, value));
if (type === 'number') return value === '' ? undefined : parseFloat(value);
if (type === 'boolean') return value === 'on';
return value;
}
/** Pass this to set_nested_value to delete the last part of the given path */
export const DELETE_KEY = {};
/**
* Convert `FormData` into a POJO
* @param {string} form_id
* @param {FormData} data
*/
export function convert_formdata(form_id, data) {
/** @type {Record<string, any>} */
const result = {};
for (const field_name of data.keys()) {
/** @type {any[]} */
const values = data.getAll(field_name);
const field = parse_form_key(form_id, field_name);
// an empty `<input type="file">` will submit a non-existent file, bizarrely
const entries = values.filter(
(entry) => typeof entry === 'string' || entry.name !== '' || entry.size > 0
);
if (entries.length === 0 && !field.is_array) continue;
if (entries.length > 1 && !field.is_array) {
e.form_field_duplicate({ name: field.name, count: String(entries.length) });
}
set_nested_value(
result,
field,
coerce_form_value(field.type, field.is_array ? entries : entries[0])
);
}
return result;
}
export const BINARY_FORM_CONTENT_TYPE = 'application/x-sveltekit-formdata';
const BINARY_FORM_VERSION = 0;
const HEADER_BYTES = 1 + 4 + 2;
/**
* The binary format is as follows:
* - 1 byte: Format version
* - 4 bytes: Length of the header (u32)
* - 2 bytes: Length of the file offset table (u16)
* - header: devalue.stringify([data, meta])
* - file offset table: JSON.stringify([offset1, offset2, ...]) (empty if no files) (offsets start from the end of the table)
* - file1, file2, ...
* @param {Record<string, any>} data
* @param {BinaryFormMeta} meta
*/
export function serialize_binary_form(data, meta) {
/** @type {Array<BlobPart>} */
const blob_parts = [new Uint8Array([BINARY_FORM_VERSION])];
/** @type {Array<[file: File, index: number]>} */
const files = [];
const encoded_header = devalue.stringify([data, meta], {
File: (file) => {
if (!(file instanceof File)) return;
files.push([file, files.length]);
return [file.name, file.type, file.size, file.lastModified, files.length - 1];
}
});
const encoded_header_buffer = text_encoder.encode(encoded_header);
let encoded_file_offsets = '';
if (files.length) {
// Sort small files to the front
files.sort(([a], [b]) => a.size - b.size);
/** @type {Array<number>} */
const file_offsets = new Array(files.length);
let start = 0;
for (const [file, index] of files) {
file_offsets[index] = start;
start += file.size;
}
encoded_file_offsets = JSON.stringify(file_offsets);
}
const length_buffer = new Uint8Array(4);
const length_view = new DataView(length_buffer.buffer);
length_view.setUint32(0, encoded_header_buffer.byteLength, true);
blob_parts.push(length_buffer.slice());
length_view.setUint16(0, encoded_file_offsets.length, true);
blob_parts.push(length_buffer.slice(0, 2));
blob_parts.push(encoded_header_buffer);
blob_parts.push(encoded_file_offsets);
for (const [file] of files) {
blob_parts.push(file);
}
return {
blob: new Blob(blob_parts)
};
}
/**
* @param {Request} request
* @param {string} form_id
* @returns {Promise<{ data: Record<string, any>; meta: BinaryFormMeta; form_data: FormData | null }>}
*/
export async function deserialize_binary_form(request, form_id) {
if (request.headers.get('content-type') !== BINARY_FORM_CONTENT_TYPE) {
const form_data = await request.formData();
return { data: convert_formdata(form_id, form_data), meta: {}, form_data };
}
if (!request.body) {
throw deserialize_error('no body');
}
const reader = request.body.getReader();
/** @type {Array<Promise<Uint8Array<ArrayBuffer> | undefined>>} */
const chunks = [];
/**
* @param {number} index
* @returns {Promise<Uint8Array<ArrayBuffer> | undefined>}
*/
function get_chunk(index) {
if (index in chunks) return chunks[index];
let i = chunks.length;
while (i <= index) {
// chain reads so only one is ever pending — workerd forbids concurrent reads
const previous = chunks[i - 1] ?? Promise.resolve(undefined);
chunks[i] = previous.then(() => reader.read()).then((chunk) => chunk.value);
i++;
}
return chunks[index];
}
/**
* @param {number} offset
* @param {number} length
* @returns {Promise<Uint8Array | null>}
*/
async function get_buffer(offset, length) {
/** @type {Uint8Array<ArrayBuffer>[]} */
const parts = [];
let total = 0;
for await (const part of read_range(get_chunk, offset, length)) {
parts.push(part);
total += part.byteLength;
}
if (total < length || parts.length === 0) return null;
// If the buffer is completely contained in one chunk, return the subarray as-is
if (parts.length === 1) return parts[0];
const buffer = new Uint8Array(length);
let cursor = 0;
for (const part of parts) {
buffer.set(part, cursor);
cursor += part.byteLength;
}
return buffer;
}
const header = await get_buffer(0, HEADER_BYTES);
if (!header) throw deserialize_error('too short');
if (header[0] !== BINARY_FORM_VERSION) {
throw deserialize_error(`got version ${header[0]}, expected version ${BINARY_FORM_VERSION}`);
}
const header_view = new DataView(header.buffer, header.byteOffset, header.byteLength);
const data_length = header_view.getUint32(1, true);
const file_offsets_length = header_view.getUint16(5, true);
// Validation uses embedded binary header fields (data_length, file_offsets_length)
// rather than Content-Length, which proxies/middleboxes may strip or corrupt.
// See: https://github.com/sveltejs/kit/issues/15299
// Read the form data
const data_buffer = await get_buffer(HEADER_BYTES, data_length);
if (!data_buffer) throw deserialize_error('data too short');
/** @type {Array<number | undefined>} */
let file_offsets;
/** @type {number} */
let files_start_offset;
if (file_offsets_length > 0) {
// Read the file offset table
const file_offsets_buffer = await get_buffer(HEADER_BYTES + data_length, file_offsets_length);
if (!file_offsets_buffer) throw deserialize_error('file offset table too short');
const parsed_offsets = JSON.parse(text_decoder.decode(file_offsets_buffer));
if (
!Array.isArray(parsed_offsets) ||
parsed_offsets.some((n) => typeof n !== 'number' || !Number.isInteger(n) || n < 0)
) {
throw deserialize_error('invalid file offset table');
}
file_offsets = /** @type {Array<number>} */ (parsed_offsets);
files_start_offset = HEADER_BYTES + data_length + file_offsets_length;
}
/** @type {Array<{ offset: number, size: number }>} */
const file_spans = [];
const [data, meta] = devalue.parse(text_decoder.decode(data_buffer), {
File: ([name, type, size, last_modified, index]) => {
if (
typeof name !== 'string' ||
typeof type !== 'string' ||
!Number.isSafeInteger(size) ||
size < 0 ||
!Number.isSafeInteger(last_modified) ||
!Number.isSafeInteger(index) ||
index < 0
) {
throw deserialize_error('invalid file metadata');
}
let offset = file_offsets[index];
// Check that the file offset table entry has not been already
// used. If not, immediately mark it as used.
if (offset === undefined) {
throw deserialize_error('duplicate file offset table index');
}
file_offsets[index] = undefined;
offset += files_start_offset;
file_spans.push({ offset, size });
return new Proxy(new LazyFile(name, type, size, last_modified, get_chunk, offset), {
getPrototypeOf() {
// Trick validators into thinking this is a normal File
return File.prototype;
}
});
}
});
// Sort file spans in increasing order primarily by offset
// and secondarily by size (to allow 0-length files).
file_spans.sort((a, b) => a.offset - b.offset || a.size - b.size);
// Check that file spans do not overlap and there are no gaps between them.
for (let i = 1; i < file_spans.length; i++) {
const previous = file_spans[i - 1];
const current = file_spans[i];
const previous_end = previous.offset + previous.size;
if (previous_end < current.offset) {
throw deserialize_error('gaps in file data');
}
if (previous_end > current.offset) {
throw deserialize_error('overlapping file data');
}
}
// Read the request body asynchronously so it doesn't stall
void (async () => {
let has_more = true;
while (has_more) {
const chunk = await get_chunk(chunks.length);
has_more = !!chunk;
}
})().catch(noop); // prevent unhandled rejection potentially crashing the process
return { data, meta, form_data: null };
}
/**
* @param {string} message
*/
function deserialize_error(message) {
return new SvelteKitError(400, 'Bad Request', `Could not deserialize binary form: ${message}`);
}
/**
* Yields the chunks that make up the byte range `[offset, offset + length)`,
* trimmed to its boundaries. Ends early if the underlying data runs out.
* @param {(index: number) => Promise<Uint8Array<ArrayBuffer> | undefined>} get_chunk
* @param {number} offset
* @param {number} length
* @returns {AsyncGenerator<Uint8Array<ArrayBuffer>, void, void>}
*/
async function* read_range(get_chunk, offset, length) {
let chunk_start = 0;
for (let index = 0; ; index++) {
const chunk = await get_chunk(index);
if (!chunk) return;
const chunk_end = chunk_start + chunk.byteLength;
if (chunk_end > offset) {
yield chunk.subarray(
Math.max(0, offset - chunk_start),
Math.min(chunk.byteLength, offset + length - chunk_start)
);
if (offset + length <= chunk_end) return;
}
chunk_start = chunk_end;
}
}
/** @implements {File} */
class LazyFile {
/** @type {(index: number) => Promise<Uint8Array<ArrayBuffer> | undefined>} */
#get_chunk;
/** @type {number} */
#offset;
/**
* @param {string} name
* @param {string} type
* @param {number} size
* @param {number} last_modified
* @param {(index: number) => Promise<Uint8Array<ArrayBuffer> | undefined>} get_chunk
* @param {number} offset
*/
constructor(name, type, size, last_modified, get_chunk, offset) {
this.name = name;
this.type = type;
this.size = size;
this.lastModified = last_modified;
this.webkitRelativePath = '';
this.#get_chunk = get_chunk;
this.#offset = offset;
// TODO - hacky, required for private members to be accessed on proxy
this.arrayBuffer = this.arrayBuffer.bind(this);
this.bytes = this.bytes.bind(this);
this.slice = this.slice.bind(this);
this.stream = this.stream.bind(this);
this.text = this.text.bind(this);
}
/** @type {ArrayBuffer | undefined} */
#buffer;
async arrayBuffer() {
this.#buffer ??= await new Response(this.stream()).arrayBuffer();
return this.#buffer;
}
async bytes() {
return new Uint8Array(await this.arrayBuffer());
}
/**
* @param {number=} start
* @param {number=} end
* @param {string=} contentType
*/
slice(start = 0, end = this.size, contentType = this.type) {
// https://github.com/nodejs/node/blob/a5f3cd8cb5ba9e7911d93c5fd3ebc6d781220dd8/lib/internal/blob.js#L240
if (start < 0) {
start = Math.max(this.size + start, 0);
} else {
start = Math.min(start, this.size);
}
if (end < 0) {
end = Math.max(this.size + end, 0);
} else {
end = Math.min(end, this.size);
}
const size = Math.max(end - start, 0);
const file = new LazyFile(
this.name,
contentType,
size,
this.lastModified,
this.#get_chunk,
this.#offset + start
);
return file;
}
stream() {
const range = read_range(this.#get_chunk, this.#offset, this.size);
const size = this.size;
return stream_from_iterable(
(async function* () {
let cursor = 0;
for await (const chunk of range) {
cursor += chunk.byteLength;
yield chunk;
}
if (cursor < size) throw new Error('incomplete file data');
})()
);
}
async text() {
return text_decoder.decode(await this.arrayBuffer());
}
}
const path_regex = /^[a-zA-Z_$]\w*(\.[a-zA-Z_$]\w*|\[\d+\])*$/;
/**
* @param {string} path
*/
export function split_path(path) {
if (!path_regex.test(path)) {
e.form_field_invalid_name({ name: path });
}
return path.split(/\.|\[|\]/).filter(Boolean);
}
/**
* Check if a property key is dangerous and could lead to prototype pollution
* @param {string} key
*/
function check_prototype_pollution(key) {
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
e.form_field_forbidden_key({ key });
}
}
/**
* Sets a value in a nested object using an array of keys, mutating the original object.
* @param {Record<string, any>} object
* @param {string[]} keys
* @param {any} value
*/
export function deep_set(object, keys, value) {
let current = object;
for (let i = 0; i < keys.length - 1; i += 1) {
const key = keys[i];
check_prototype_pollution(key);
const is_array = /^\d+$/.test(keys[i + 1]);
const inner = Object.hasOwn(current, key) ? current[key] : undefined;
const exists = inner != null;
if (exists && is_array !== Array.isArray(inner)) {
e.form_field_array_conflict({ key: keys[i + 1] });
}
if (!exists) {
if (value === DELETE_KEY) {
// don't create the nested structure if we want to delete the key anyway
return;
}
current[key] = is_array ? [] : {};
}
current = current[key];
}
const final_key = keys[keys.length - 1];
check_prototype_pollution(final_key);
if (value === DELETE_KEY) {
delete current[final_key];
} else {
current[final_key] = value;
}
}
/**
* @param {StandardSchemaV1.Issue} issue
* @param {boolean} server Whether this issue came from server validation
*/
export function normalize_issue(issue, server = false) {
/** @type {InternalRemoteFormIssue} */
const normalized = { name: '', path: [], message: issue.message, server };
if (issue.path !== undefined) {
let name = '';
for (const segment of issue.path) {
const key = /** @type {string | number} */ (
typeof segment === 'object' ? segment.key : segment
);
normalized.path.push(key);
if (typeof key === 'number') {
name += `[${key}]`;
} else if (typeof key === 'string') {
name += name === '' ? key : '.' + key;
}
}
normalized.name = name;
}
return normalized;
}
/**
* @param {InternalRemoteFormIssue[]} issues
*/
export function flatten_issues(issues) {
/** @type {Record<string, InternalRemoteFormIssue[]>} */
const result = Object.create(null);
for (const issue of issues) {
(result.$ ??= []).push(issue);
let name = '';
if (issue.path !== undefined) {
for (const key of issue.path) {
if (typeof key === 'number') {
name += `[${key}]`;
} else if (typeof key === 'string') {
name += name === '' ? key : '.' + key;
}
(result[name] ??= []).push(issue);
}
}
}
return result;
}
/**
* Gets a nested value from an object using a path array
* @param {Record<string, any>} object
* @param {(string | number)[]} path
* @returns {any}
*/
export function deep_get(object, path) {
let current = object;
for (const key of path) {
if (current === null || typeof current !== 'object' || !Object.hasOwn(current, key)) {
return undefined;
}
current = current[key];
}
return current;
}
/** name prefixes that tell the server which type to coerce a submitted string to */
const type_prefixes = /** @type {Record<string, string | undefined>} */ ({
number: 'n:',
boolean: 'b:'
});
/**
* adds props; a function becomes a getter that is computed each time it is read
* @param {Record<string, any>} base_props
* @param {Record<string, unknown>} props
*/
function add_props(base_props, props) {
for (const prop in props) {
const value = props[prop];
if (typeof value === 'function') {
Object.defineProperty(base_props, prop, {
enumerable: true,
get: /** @type {() => unknown} */ (value)
});
} else {
base_props[prop] = value;
}
}
return base_props;
}
/**
* @param {string} type
* @param {boolean} is_array
* @param {unknown} input_value
*/
function get_type_prefix(type, is_array, input_value) {
if (type === 'number' || type === 'range') return 'n:';
if (type === 'image') return 'i:';
if (type === 'checkbox' && !is_array) return 'b:';
if (type === 'hidden' || type === 'submit') return type_prefixes[typeof input_value] ?? '';
return '';
}
/**
* A deep-clone implementation specifically for form data, where
* we don't need to worry about cycles and whatnot
* @param {any} value
* @returns {any}
*/
function deep_clone(value) {
if (value !== null && typeof value === 'object') {
if (value instanceof Date) {
return new Date(value.getTime());
}
if (value instanceof File) {
return value;
}
if (Array.isArray(value)) {
return value.map(deep_clone);
}
/** @type {Record<string, any>} */
const clone = {};
for (const key of Object.keys(value)) {
clone[key] = deep_clone(value[key]);
}
return clone;
}
return value;
}
const warned_sites = new Set();
// keyed by the stack, so every place that enumerates warns once with its call site
const warn_no_keys = () => {
// Capture the original call site, not a generated diagnostic or factory's frames.
const error = new Error();
if (warned_sites.has(error.stack)) return;
warned_sites.add(error.stack);
w.form_fields_enumerated();
};
// fields are created as they are accessed, so there is nothing to enumerate
/** @type {ProxyHandler<object> | null} */
const dev_traps = DEV
? {
has(target, prop) {
if (typeof prop !== 'symbol') warn_no_keys();
return prop in target;
},
ownKeys(target) {
warn_no_keys();
return Reflect.ownKeys(target);
}
}
: null;
/** @param {InternalRemoteFormIssue} issue */
const public_issue = (issue) => ({ path: issue.path, message: issue.message });
/** @typedef {{
* form_id: string,
* get: () => Record<string, any>,
* set: (path: (string | number)[], value: any) => void,
* get_issues: (path?: (string | number)[], all?: boolean) => Record<string, InternalRemoteFormIssue[]>,
* get_touched: () => Record<string, boolean>,
* get_dirty: () => Record<string, boolean>
* }} FieldContext */
/**
* @param {FieldContext} context
* @param {(string | number)[]} path
* @param {string} prop
* @returns {any} a method of the field at `path`, or undefined for a nested field
*/
function create_field_method(context, path, prop) {
switch (prop) {
case 'set':
return (/** @type {any} */ value) => {
context.set(path, value);
return value;
};
case 'value':
return () => deep_clone(deep_get(context.get(), path));
case 'issues':
case 'allIssues': {
const key = build_path_string(path);
const all = prop === 'allIssues';
return () => {
const issues = context.get_issues(path, all)[key === '' ? '$' : key];
if (all) return issues?.map(public_issue);
const own = issues?.filter((issue) => issue.name === key).map(public_issue);
return own?.length ? own : undefined;
};
}
case 'touched':
case 'dirty': {
const key = build_path_string(path);
return () => {
const object = prop === 'dirty' ? context.get_dirty() : context.get_touched();
if (Object.hasOwn(object, key)) return true;
for (const candidate in object) {
if (!Object.hasOwn(object, candidate)) continue;
if (key === '') return true;
if (!candidate.startsWith(key)) continue;
const next = candidate[key.length];
if (next === '.' || next === '[') return true;
}
return false;
};
}
case 'as': {
const key = build_path_string(path);
/**
* the field's value, or `fallback` until the field has been edited
* (without a fallback there is nothing to suppress, so `dirty` is not read)
* @param {unknown} [fallback]
*/
const read = (fallback) =>
deep_get(context.get(), path) ??
(fallback !== undefined && Object.hasOwn(context.get_dirty(), key) ? undefined : fallback);
/**
* @param {string} type
* @param {unknown} [input_value]
* @param {boolean} [checked]
*/
return (type, input_value, checked) => {
const is_array =
type === 'file multiple' ||
type === 'select multiple' ||
(type === 'checkbox' && typeof input_value === 'string');
const type_prefix = get_type_prefix(type, is_array, input_value);
// Base properties for all input types
/** @type {Record<string, any>} */
const base_props = {
name: type_prefix + key + (is_array ? '[]' : '') + '/' + context.form_id,
get 'aria-invalid'() {
const issues = context.get_issues();
return key in issues ? 'true' : undefined;
}
};
// Add type attribute only for non-text inputs and non-select elements
if (type !== 'text' && type !== 'select' && type !== 'select multiple') {
base_props.type = type === 'file multiple' ? 'file' : type;
}
// Handle submit and hidden inputs
if (type === 'submit' || type === 'hidden') {
if (DEV) {
if (input_value === null || input_value === undefined) {
e.form_input_missing_value({ type: `\`${type}\`` });
}
}
return add_props(base_props, {
value: typeof input_value === 'boolean' ? (input_value ? 'on' : 'off') : input_value
});
}
// Handle select inputs
if (type === 'select' || type === 'select multiple') {
return add_props(base_props, {
multiple: is_array,
value: () => {
const value = read(input_value);
// copied, so the state array can't be edited through the props
return Array.isArray(value) ? [...value] : value;
}
});
}
// Handle checkbox inputs
if (type === 'checkbox' || type === 'radio') {
// radio and checkbox array inputs take their option as the second argument
// and whether it is checked as the third, a single checkbox only the latter
const has_option = type === 'radio' || is_array;
if (DEV && has_option && !input_value) {
e.form_input_missing_value({ type: type === 'radio' ? 'Radio' : 'Checkbox array' });
}
if (has_option) {
base_props.value = input_value ?? 'on';
} else {
checked = /** @type {boolean | undefined} */ (input_value);
}
return add_props(base_props, {
defaultChecked: checked,
checked: () => {
const value = read();
if (value == null) return read(checked);
if (type === 'radio') return value === input_value;
if (is_array) return /** @type {unknown[]} */ (value).includes(input_value);
return value;
}
});
}
// Handle file inputs
if (type === 'file' || type === 'file multiple') {
return add_props(base_props, {
multiple: is_array,
files: () => {
const value = read();
const files = value instanceof File ? [value] : value;
if (!Array.isArray(files) || !files.every((f) => f instanceof File)) return null;
// a FileList-like object where DataTransfer does not exist
if (typeof DataTransfer === 'undefined') {
return Object.assign({ length: files.length }, files);
}
const transfer = new DataTransfer();
for (const file of files) transfer.items.add(file);
return transfer.files;
}
});
}
if (type === 'image') return base_props;
// Handle all other input types (text, number, etc.)
return add_props(base_props, {
defaultValue: input_value,
value: () => String(read(input_value) ?? '')
});
};
}
}
}
/**
* Creates a proxy-based field accessor for form data
* @param {FieldContext} context
* @param {any} target - Function or empty POJO
* @param {(string | number)[]} path - Current access path
* @returns {any} Proxy object with name(), value(), and issues() methods
*/
export function create_field_proxy(context, target = {}, path = []) {
return new Proxy(target, {
...dev_traps,
get(target, prop) {
if (typeof prop === 'symbol') return target[prop];
// array access like jobs[0]
const next = [...path, /^\d+$/.test(prop) ? parseInt(prop, 10) : prop];
return create_field_proxy(context, create_field_method(context, path, prop), next);
}
});
}
/**
* Builds a path string from an array of path segments
* @param {(string | number)[]} path
* @returns {string}
*/
export function build_path_string(path) {
let result = '';
for (const segment of path) {
if (typeof segment === 'number') {
result += `[${segment}]`;
} else {
result += result === '' ? segment : '.' + segment;
}
}
return result;
}