"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.__tsfTypeAliases = exports.FormBodyBuilder = exports.defaultFormBodyLimits = void 0;
exports.parseFormUrlEncodedBody = parseFormUrlEncodedBody;
exports.assertTopLevelFormFieldName = assertTopLevelFormFieldName;
const tslib_1 = require("tslib");
const _a = tslib_1.__importStar(require("@zyno-io/ts-reflection/type-metadata-runtime"));
const errors_1 = require("./errors");
const __tsf_metadata_type = _a.createCompactMetadataRegistryV1("[1,[{\"kind\":6},{\"kind\":2},{\"kind\":3},{\"kind\":18,\"typeName\":\"Record\",\"utilityType\":\"Record\",\"typeArguments\":[{\"kind\":6},{\"kind\":2}],\"index\":{\"kind\":2},\"types\":[]},{\"kind\":18,\"typeName\":\"FormBodyLimits\",\"types\":[{\"kind\":20,\"name\":\"maxFormFields\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"maxFormFieldNameLength\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"maxFormDepth\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"maxFormArrayIndex\",\"type\":{\"kind\":7},\"optional\":false}]}]]", []);
exports.defaultFormBodyLimits = {
    maxFormFields: 10000,
    maxFormFieldNameLength: 2048,
    maxFormDepth: 16,
    maxFormArrayIndex: 10000
};
const unsafePropertyNames = new Set(['__proto__', 'prototype', 'constructor']);
const canonicalArrayIndexPattern = /^(?:0|[1-9]\d*)$/;
const numericLikePropertyPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
let _FormBodyBuilderMetadata_1;
class FormBodyBuilder {
    limits;
    root = createObjectNode();
    structureCount = 0;
    constructor(limits) {
        this.limits = limits;
    }
    add(name, value) {
        this.consumeStructure();
        const tokens = parseFormFieldPath(name, this.limits);
        insertValue(this.root, tokens, value, name);
    }
    addTopLevelFile(name, value) {
        assertTopLevelFormFieldName(name, this.limits);
        insertValue(this.root, [{ kind: 'property', name }], value, name);
    }
    mergeObject(value) {
        const source = importObjectNode(value, this.limits, () => this.consumeStructure());
        mergeNodes(this.root, source, '');
    }
    merge(builder) {
        mergeNodes(this.root, builder.root, '');
    }
    build() {
        return materializeNode(this.root, '');
    }
    consumeStructure() {
        this.structureCount += 1;
        if (this.structureCount > this.limits.maxFormFields) {
            throw new errors_1.HttpPayloadTooLargeError('Form contains too many fields');
        }
    }
    static __tsfType = _FormBodyBuilderMetadata_1 = _a.decodeCompactMetadataV1("[1,{\"kind\":16,\"name\":\"FormBodyBuilder\",\"typeName\":\"FormBodyBuilder\",\"classType\":{\"$tsf\":0},\"properties\":[],\"methods\":[{\"name\":\"add\",\"parameters\":[{\"name\":\"name\",\"type\":{\"$tsfType\":0},\"optional\":false,\"default\":false},{\"name\":\"value\",\"type\":{\"$tsfType\":1},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":2}},{\"name\":\"addTopLevelFile\",\"parameters\":[{\"name\":\"name\",\"type\":{\"$tsfType\":0},\"optional\":false,\"default\":false},{\"name\":\"value\",\"type\":{\"$tsfType\":1},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":2}},{\"name\":\"mergeObject\",\"parameters\":[{\"name\":\"value\",\"type\":{\"$tsfType\":3},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":2}},{\"name\":\"merge\",\"parameters\":[{\"name\":\"builder\",\"type\":{\"kind\":16,\"typeName\":\"FormBodyBuilder\",\"classType\":{\"$tsf\":1}},\"optional\":false,\"default\":false}],\"returnType\":{\"$tsfType\":2}},{\"name\":\"build\",\"parameters\":[],\"returnType\":{\"$tsfType\":3}}],\"hasConstructor\":true,\"constructorParameters\":[{\"name\":\"limits\",\"type\":{\"$tsfType\":4},\"optional\":false,\"default\":false}]}]", [() => FormBodyBuilder, () => (typeof FormBodyBuilder !== "undefined" ? FormBodyBuilder : (typeof exports !== "undefined" ? exports.FormBodyBuilder : undefined))], __tsf_metadata_type);
}
exports.FormBodyBuilder = FormBodyBuilder;
_FormBodyBuilderMetadata_1.classType = FormBodyBuilder;
FormBodyBuilder.__tsfType = _FormBodyBuilderMetadata_1;
function parseFormUrlEncodedBody(text, limits) {
    const builder = new FormBodyBuilder(limits);
    for (const [name, value] of new URLSearchParams(text))
        builder.add(name, value);
    return builder.build();
}
function assertTopLevelFormFieldName(name, limits) {
    const tokens = parseFormFieldPath(name, limits);
    if (tokens.length !== 1 || tokens[0].kind !== 'property') {
        throw new errors_1.HttpBadRequestError(`File field "${name}" must be a top-level field`);
    }
}
function parseFormFieldPath(name, limits) {
    if (!name)
        throw new errors_1.HttpBadRequestError('Form field name cannot be empty');
    if (name.length > limits.maxFormFieldNameLength) {
        throw new errors_1.HttpPayloadTooLargeError('Form field name is too long');
    }
    const firstBracket = name.indexOf('[');
    const rootName = firstBracket === -1 ? name : name.slice(0, firstBracket);
    if (!rootName || rootName.includes(']'))
        throw malformedFieldName(name);
    const tokens = [propertyToken(rootName)];
    let offset = firstBracket === -1 ? name.length : firstBracket;
    while (offset < name.length) {
        if (name[offset] !== '[')
            throw malformedFieldName(name);
        const closingBracket = name.indexOf(']', offset + 1);
        if (closingBracket === -1)
            throw malformedFieldName(name);
        const segment = name.slice(offset + 1, closingBracket);
        if (segment.includes('['))
            throw malformedFieldName(name);
        tokens.push(parseBracketToken(segment, name, limits));
        offset = closingBracket + 1;
        if (offset < name.length && name[offset] !== '[')
            throw malformedFieldName(name);
    }
    if (tokens.length > limits.maxFormDepth) {
        throw new errors_1.HttpPayloadTooLargeError('Form field nesting is too deep');
    }
    const appendIndex = tokens.findIndex(token => token.kind === 'append');
    if (appendIndex !== -1 && appendIndex !== tokens.length - 1) {
        throw new errors_1.HttpBadRequestError(`Array append notation must be terminal in form field "${name}"`);
    }
    return tokens;
}
function parseBracketToken(segment, fieldName, limits) {
    if (segment === '')
        return { kind: 'append' };
    if (/^\d+$/.test(segment)) {
        if (!canonicalArrayIndexPattern.test(segment)) {
            throw new errors_1.HttpBadRequestError(`Invalid array index in form field "${fieldName}"`);
        }
        const index = Number(segment);
        if (!Number.isSafeInteger(index) || index > limits.maxFormArrayIndex) {
            throw new errors_1.HttpPayloadTooLargeError(`Array index is too large in form field "${fieldName}"`);
        }
        return { kind: 'index', index };
    }
    if (numericLikePropertyPattern.test(segment)) {
        throw new errors_1.HttpBadRequestError(`Invalid array index in form field "${fieldName}"`);
    }
    return propertyToken(segment);
}
function propertyToken(name) {
    assertSafePropertyName(name);
    return { kind: 'property', name };
}
function assertSafePropertyName(name) {
    if (unsafePropertyNames.has(name))
        throw new errors_1.HttpBadRequestError(`Unsafe form field property "${name}"`);
}
function malformedFieldName(name) {
    return new errors_1.HttpBadRequestError(`Malformed form field name "${name}"`);
}
function insertValue(container, tokens, value, fieldName, offset = 0) {
    const token = tokens[offset];
    const last = offset === tokens.length - 1;
    if (container.kind === 'append-array') {
        if (token.kind !== 'append' || !last)
            throw conflictingStructure(fieldName);
        container.values.push(value);
        return;
    }
    if (container.kind === 'object' && token.kind !== 'property')
        throw conflictingStructure(fieldName);
    if (container.kind === 'indexed-array' && token.kind !== 'index')
        throw conflictingStructure(fieldName);
    if (token.kind === 'append')
        throw conflictingStructure(fieldName);
    const key = token.kind === 'property' ? token.name : token.index;
    const children = container.children;
    const existing = children.get(key);
    if (last) {
        if (!existing) {
            children.set(key, { kind: 'leaf', values: [value] });
            return;
        }
        if (existing.kind !== 'leaf')
            throw conflictingStructure(fieldName);
        existing.values.push(value);
        return;
    }
    const next = tokens[offset + 1];
    const expectedKind = next.kind === 'property' ? 'object' : next.kind === 'index' ? 'indexed-array' : 'append-array';
    let child = existing;
    if (!child) {
        child =
            expectedKind === 'object' ? createObjectNode() : expectedKind === 'indexed-array' ? createIndexedArrayNode() : createAppendArrayNode();
        children.set(key, child);
    }
    else if (child.kind !== expectedKind) {
        throw conflictingStructure(fieldName);
    }
    insertValue(child, tokens, value, fieldName, offset + 1);
}
function conflictingStructure(fieldName) {
    return new errors_1.HttpBadRequestError(`Conflicting form field structure at "${fieldName}"`);
}
function importObjectNode(value, limits, consumeStructure) {
    const root = createObjectNode();
    for (const [name, child] of Object.entries(value)) {
        assertSafePropertyName(name);
        consumeStructure();
        root.children.set(name, importJsonNode(child, limits, consumeStructure, 1));
    }
    return root;
}
function importJsonNode(value, limits, consumeStructure, depth) {
    if (depth > limits.maxFormDepth)
        throw new errors_1.HttpPayloadTooLargeError('Form field nesting is too deep');
    if (Array.isArray(value)) {
        const node = createIndexedArrayNode();
        value.forEach((child, index) => {
            if (index > limits.maxFormArrayIndex)
                throw new errors_1.HttpPayloadTooLargeError('Form array index is too large');
            consumeStructure();
            node.children.set(index, importJsonNode(child, limits, consumeStructure, depth + 1));
        });
        return node;
    }
    if (value !== null && typeof value === 'object') {
        const node = createObjectNode();
        for (const [name, child] of Object.entries(value)) {
            assertSafePropertyName(name);
            consumeStructure();
            node.children.set(name, importJsonNode(child, limits, consumeStructure, depth + 1));
        }
        return node;
    }
    return { kind: 'leaf', values: [value] };
}
function mergeNodes(target, source, path) {
    if (target.kind !== source.kind)
        throw mergeConflict(path);
    if (target.kind === 'leaf' || target.kind === 'append-array')
        throw mergeConflict(path);
    const sourceContainer = source;
    const targetChildren = target.children;
    for (const [key, sourceChild] of sourceContainer.children) {
        const childPath = appendPath(path, key);
        const targetChild = targetChildren.get(key);
        if (!targetChild) {
            targetChildren.set(key, sourceChild);
            continue;
        }
        mergeNodes(targetChild, sourceChild, childPath);
    }
}
function mergeConflict(path) {
    return new errors_1.HttpBadRequestError(`Conflicting form field values at "${path || '<root>'}"`);
}
function materializeNode(node, path) {
    if (node.kind === 'leaf')
        return node.values.length === 1 ? node.values[0] : [...node.values];
    if (node.kind === 'append-array')
        return [...node.values];
    if (node.kind === 'object') {
        const value = Object.create(null);
        for (const [name, child] of node.children)
            value[name] = materializeNode(child, appendPath(path, name));
        return value;
    }
    if (!node.children.size)
        return [];
    let maximumIndex = 0;
    for (const index of node.children.keys())
        maximumIndex = Math.max(maximumIndex, index);
    const value = [];
    for (let index = 0; index <= maximumIndex; index += 1) {
        const child = node.children.get(index);
        if (!child)
            throw new errors_1.HttpBadRequestError(`Sparse form array at "${path}": missing index ${index}`);
        value.push(materializeNode(child, appendPath(path, index)));
    }
    return value;
}
function appendPath(path, key) {
    if (typeof key === 'number')
        return `${path}[${key}]`;
    return path ? `${path}[${key}]` : key;
}
function createObjectNode() {
    return { kind: 'object', children: new Map() };
}
function createIndexedArrayNode() {
    return { kind: 'indexed-array', children: new Map() };
}
function createAppendArrayNode() {
    return { kind: 'append-array', values: [] };
}
exports.__tsfTypeAliases = JSON.parse("[1,{\"FormBodyLimits\":{\"kind\":18,\"typeName\":\"FormBodyLimits\",\"types\":[{\"kind\":20,\"name\":\"maxFormFields\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"maxFormFieldNameLength\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"maxFormDepth\",\"type\":{\"kind\":7},\"optional\":false},{\"kind\":20,\"name\":\"maxFormArrayIndex\",\"type\":{\"kind\":7},\"optional\":false}]}}]")[1];
//# sourceMappingURL=form-body.js.map