Something went wrong. Try again.
A Tangled knot on CF Workers, backed by CF Artifacts
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379import { Buffer } from 'node:buffer';import { inflateSync } from 'node:zlib';import { parsePatch, applyPatch } from 'diff';import { filePath, fail } from './types';const BASE85 = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~';export interface PatchChange { old: string | null; next: string | null; oldMode?: string; newMode?: string; oldOid?: string; newOid?: string; copy: boolean; binary?: { kind: 'literal' | 'delta'; size: number; data: Uint8Array }; text?: ReturnType<typeof parsePatch>[number];}function pathName(value: string, prefix: boolean) { if (value === '/dev/null') return null; let bytes: Buffer; if (value.startsWith('"')) { if (!value.endsWith('"')) fail(400, 'InvalidPatch', 'invalid quoted path'); const parts: number[] = []; for (let i = 1; i < value.length - 1; i++) { if (value[i] !== '\\') { const code = value.codePointAt(i)!; parts.push(...Buffer.from(String.fromCodePoint(code))); if (code > 65535) i++; continue; } const next = value[++i], simple: Record<string, number> = { t: 9, n: 10, r: 13, b: 8, f: 12, v: 11, '"': 34, '\\': 92, }; if (next in simple) parts.push(simple[next]); else if (/[0-7]/.test(next)) { const octal = next + value.slice(i + 1, i + 3); if (!/^[0-7]{3}$/.test(octal)) fail(400, 'InvalidPatch', 'invalid octal path'); parts.push(parseInt(octal, 8)); i += 2; } else fail(400, 'InvalidPatch', 'invalid path escape'); } bytes = Buffer.from(parts); } else bytes = Buffer.from(value); let decoded: string; try { decoded = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode( bytes, ); } catch { return fail(400, 'UnsupportedPatch', 'non-UTF8 paths are unsupported'); } return filePath(prefix ? decoded.replace(/^[ab]\//, '') : decoded);}function binaryBlock(lines: string[], position: number, max: number) { const header = /^(literal|delta) (\d+)$/.exec(lines[position]); if (!header) fail(400, 'InvalidPatch', 'invalid binary patch header'); const size = Number(header[2]); if (!Number.isSafeInteger(size) || size < 0 || size > max) fail(413, 'OperationTooLarge', 'binary patch expansion exceeds limit'); const chunks: Buffer[] = []; let compressed = 0; for (position++; position < lines.length && lines[position]; position++) { const line = lines[position], code = line.charCodeAt(0), length = code >= 65 && code <= 90 ? code - 64 : code >= 97 && code <= 122 ? code - 70 : 0; if (!length || line.length !== 1 + Math.ceil(length / 4) * 5) fail(400, 'InvalidPatch', 'invalid Git base85 line'); const bytes = Buffer.alloc(Math.ceil(length / 4) * 4); for ( let start = 1, offset = 0; start < line.length; start += 5, offset += 4 ) { let number = 0; for (let i = 0; i < 5; i++) { const digit = BASE85.indexOf(line[start + i]); if (digit < 0) fail(400, 'InvalidPatch', 'invalid Git base85 digit'); number = number * 85 + digit; } if (number > 0xffffffff) fail(400, 'InvalidPatch', 'Git base85 integer overflow'); bytes.writeUInt32BE(number, offset); } const chunk = bytes.subarray(0, length); compressed += length; if (compressed > max) fail(413, 'OperationTooLarge', 'binary compressed patch exceeds limit'); chunks.push(chunk); } let data: Buffer; try { const compressedBytes = Buffer.concat(chunks), inflated = inflateSync(compressedBytes, { maxOutputLength: Math.max(size, 1), info: true, }) as unknown as { buffer: Buffer; engine: { bytesWritten: number } }; data = inflated.buffer; if (inflated.engine.bytesWritten !== compressedBytes.length) fail(400, 'InvalidPatch', 'trailing binary compressed data'); } catch { return fail( 400, 'InvalidPatch', 'invalid or oversized binary deflate stream', ); } if (data.length !== size) fail(400, 'InvalidPatch', 'binary patch length mismatch'); return { value: { kind: header[1] as 'literal' | 'delta', size, data }, position, };}export function applyBinaryDelta( base: Uint8Array, instructions: Uint8Array, max: number,) { const data = Buffer.from(instructions); let position = 0, count = 0; function size() { let value = 0, factor = 1; for (let i = 0; i < 8; i++) { if (position >= data.length) fail(400, 'InvalidPatch', 'truncated binary delta'); const byte = data[position++]; value += (byte & 127) * factor; if (!Number.isSafeInteger(value) || value > max) fail(413, 'OperationTooLarge', 'binary delta expansion exceeds limit'); if (!(byte & 128)) return value; factor *= 128; } return fail(400, 'InvalidPatch', 'invalid binary delta size'); } const oldSize = size(), newSize = size(); if (oldSize !== base.length) fail(409, 'PatchConflict', 'binary delta base size mismatch'); const output = Buffer.alloc(newSize); let written = 0; while (position < data.length) { if (++count > 100000) fail(413, 'OperationTooLarge', 'binary delta instruction limit'); const code = data[position++]; if (code & 128) { let offset = 0, length = 0; for (let bit = 0; bit < 4; bit++) if (code & (1 << bit)) { if (position >= data.length) fail(400, 'InvalidPatch', 'truncated delta offset'); offset += data[position++] * 2 ** (8 * bit); } for (let bit = 0; bit < 3; bit++) if (code & (1 << (bit + 4))) { if (position >= data.length) fail(400, 'InvalidPatch', 'truncated delta length'); length += data[position++] * 2 ** (8 * bit); } if (!length) length = 65536; if (offset + length > base.length || written + length > newSize) fail(400, 'InvalidPatch', 'delta copy outside bounds'); output.set(base.subarray(offset, offset + length), written); written += length; } else { if (!code || position + code > data.length || written + code > newSize) fail(400, 'InvalidPatch', 'invalid delta insertion'); output.set(data.subarray(position, position + code), written); position += code; written += code; } } if (written !== newSize) fail(400, 'InvalidPatch', 'binary delta output mismatch'); return output;}export function parseGitPatch( patch: string, max = 16 * 1024 * 1024,): PatchChange[] { const starts = [...patch.matchAll(/^diff --git (.+)$/gm)]; if (!starts.length) { let parsed; try { parsed = parsePatch(patch); } catch { return fail(400, 'InvalidPatch', 'invalid unified diff'); } if (!parsed.length || parsed.length > 100) fail(400, 'InvalidPatch', 'patch must change 1 to 100 files'); const paths = new Set<string>(); return parsed.map((text) => { const old = pathName(text.oldFileName ?? '', true), next = pathName(text.newFileName ?? '', true), path = next ?? old; if (!path || paths.has(path)) fail(400, 'InvalidPatch', 'duplicate patch destination'); paths.add(path); return { old, next, copy: false, text }; }); } if (starts.length > 100) fail(413, 'OperationTooLarge', 'patch changes too many files'); const result: PatchChange[] = []; for (let i = 0; i < starts.length; i++) { const block = patch.slice( starts[i].index, starts[i + 1]?.index ?? patch.length, ), lines = block.split('\n'); let tokens = /^("(?:[^"\\]|\\.)*"|\S+) ("(?:[^"\\]|\\.)*"|\S+)$/.exec( starts[i][1], ); if (!tokens && !starts[i][1].startsWith('"')) tokens = /^(a\/.*?) (b\/.*)$/.exec(starts[i][1]); if (!tokens) fail(400, 'InvalidPatch', 'invalid diff paths'); const change: PatchChange = { old: pathName(tokens[1], true), next: pathName(tokens[2], true), copy: false, }; let body = -1; for (let line = 1; line < lines.length; line++) { const value = lines[line]; let match: RegExpExecArray | null; if (value.startsWith('--- ')) { body = line; break; } if (value === 'GIT binary patch') { const forward = binaryBlock(lines, line + 1, max); change.binary = forward.value; let next = forward.position + 1; if (/^(literal|delta) /.test(lines[next] ?? '')) next = binaryBlock(lines, next, max).position + 1; for (const trailing of lines.slice(next)) if (trailing && !/^(-- |knot|[0-9]+\.[0-9]+.*)$/.test(trailing)) fail(400, 'InvalidPatch', 'trailing binary patch data'); body = -2; break; } if ( (match = /^(old mode|new mode|new file mode|deleted file mode) (\d{6})$/.exec( value, )) ) { if ( !['100644', '100664', '100755', '120000', '160000'].includes(match[2]) ) fail(400, 'UnsupportedPatch', 'unsupported Git mode'); if (match[1] === 'old mode' || match[1] === 'deleted file mode') change.oldMode = match[2]; else change.newMode = match[2]; if (match[1] === 'new file mode') change.old = null; if (match[1] === 'deleted file mode') change.next = null; } else if ( (match = /^index ([a-f0-9]{4,40})\.\.([a-f0-9]{4,40})(?: (\d{6}))?$/.exec( value, )) ) { change.oldOid = match[1]; change.newOid = match[2]; if (match[3]) { if ( !['100644', '100664', '100755', '120000', '160000'].includes( match[3], ) ) fail(400, 'UnsupportedPatch', 'unsupported Git mode'); change.oldMode = change.newMode = match[3]; } } else if ( (match = /^(rename from|rename to|copy from|copy to) (.+)$/.exec(value)) ) { if (match[1].endsWith('from')) change.old = pathName(match[2], false); else change.next = pathName(match[2], false); if (match[1].startsWith('copy')) change.copy = true; } else if (value.startsWith('Binary files ')) fail(400, 'UnsupportedPatch', 'binary patch has no content'); else if ( value && !/^(similarity index \d+%|dissimilarity index \d+%|-- |knot|[0-9]+\.[0-9]+.*)$/.test( value, ) ) fail(400, 'InvalidPatch', 'unrecognized Git patch header'); } if (body >= 0) { let text; try { text = parsePatch(lines.slice(body).join('\n')); } catch { return fail(400, 'InvalidPatch', 'invalid text hunks'); } if (text.length !== 1) fail(400, 'InvalidPatch', 'invalid text patch'); change.text = text[0]; change.old = pathName(text[0].oldFileName ?? '', true); change.next = pathName(text[0].newFileName ?? '', true); } if (!change.old && !change.next) fail(400, 'InvalidPatch', 'patch has no path'); if ( body === -1 && !change.oldMode && !change.newMode && change.old === change.next ) fail(400, 'InvalidPatch', 'patch contains no change'); result.push(change); } const destinations = new Set<string>(), sources = new Set<string>(); for (const change of result) { if (change.next) { if (destinations.has(change.next)) fail(400, 'InvalidPatch', 'duplicate patch destination'); destinations.add(change.next); } if (change.old && !change.copy) { if (sources.has(change.old)) fail(400, 'InvalidPatch', 'duplicate patch source'); sources.add(change.old); } } return result;}export function applyTextPatch(original: Uint8Array, change: PatchChange) { let lines = 1; for (const byte of original) if (byte === 10 && ++lines > 200000) fail(413, 'OperationTooLarge', 'text patch source exceeds line limit'); let comparisons = 0; let text: string; try { text = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode( original, ); } catch { return fail(400, 'UnsupportedPatch', 'text patch targets non-UTF8 content'); } if (text.includes('\0')) fail(400, 'UnsupportedPatch', 'text patch targets binary content'); const updated = change.text ? applyPatch(text, change.text, { fuzzFactor: 0, compareLine: (_number, line, _operation, content) => { if (++comparisons > 500000) fail( 413, 'OperationTooLarge', 'text patch context search exceeds comparison limit', ); return line === content; }, }) : text; if (updated === false) fail(409, 'PatchConflict', 'text patch context does not match'); return Buffer.from(updated);}