UNPKG

locutus

Version:

Locutus other languages' standard libraries to JavaScript for fun and educational purposes

212 lines (211 loc) 8.06 kB
const STRING_CODES = new Set(['a', 'A', 'h', 'H']); function toByteArray(input) { return Uint8Array.from(Array.from(input, (char) => char.charCodeAt(0) & 0xff)); } function parseFormat(format) { const segments = String(format) .split('/') .map((segment) => segment.trim()) .filter(Boolean); const parsed = []; for (const segment of segments) { const code = segment[0] ?? ''; if (!/[aAhHcCsSvVnNiIlLfdxX@CN]/.test(code)) { return null; } let cursor = 1; let quantifierText = ''; while (cursor < segment.length && /[\d*]/.test(segment[cursor] ?? '')) { quantifierText += segment[cursor]; cursor += 1; } const label = segment.slice(cursor); const quantifier = quantifierText === '' ? 1 : quantifierText === '*' ? '*' : Number.parseInt(quantifierText, 10); if (typeof quantifier === 'number' && (!Number.isFinite(quantifier) || quantifier < 0)) { return null; } parsed.push({ code, quantifier, label }); } return parsed; } function resolveCount(quantifier, cursor, bytesPerValue, totalLength) { if (quantifier !== '*') { return quantifier; } if (bytesPerValue <= 0) { return 0; } return Math.floor((totalLength - cursor) / bytesPerValue); } function addResult(target, nextNumericKey, label, index, count, value) { if (label) { const key = count === 1 && index === 0 ? label : `${label}${index + 1}`; target[key] = value; return; } target[String(nextNumericKey.value)] = value; nextNumericKey.value += 1; } function readUInt16(bytes, cursor, littleEndian) { return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint16(cursor, littleEndian); } function readInt16(bytes, cursor, littleEndian) { return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getInt16(cursor, littleEndian); } function readUInt32(bytes, cursor, littleEndian) { return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(cursor, littleEndian); } function readInt32(bytes, cursor, littleEndian) { return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getInt32(cursor, littleEndian); } export function unpack(format, data) { // discuss at: https://locutus.io/php/unpack/ // parity verified: PHP 8.3 // original by: Kevin van Zonneveld (https://kvz.io) // note 1: Supports pragmatic unpack templates for strings, integers, floats, skips, rewind, and absolute seek. // note 2: Machine-order numeric templates follow the project's existing little-endian pack/unpack convention. // example 1: unpack('nversion/Ctype', "\x12\x34\x7f") // returns 1: {version: 4660, type: 127} // example 2: unpack('a5label/x/Cflag', "hello\x00\x01") // returns 2: {label: 'hello', flag: 1} // example 3: unpack('H4hex/Cvalue', "\x23\x45\x21") // returns 3: {hex: '2345', value: 33} const parsed = parseFormat(format); if (!parsed) { return false; } const bytes = toByteArray(String(data)); const out = {}; const nextNumericKey = { value: 1 }; let cursor = 0; for (const segment of parsed) { const { code, quantifier, label } = segment; if (code === 'x') { const count = quantifier === '*' ? bytes.length - cursor : quantifier; cursor += count; if (cursor > bytes.length) { return false; } continue; } if (code === 'X') { const count = quantifier === '*' ? cursor : quantifier; cursor -= count; if (cursor < 0) { return false; } continue; } if (code === '@') { cursor = quantifier === '*' ? bytes.length : quantifier; if (cursor < 0 || cursor > bytes.length) { return false; } continue; } if (STRING_CODES.has(code)) { const nibbleMode = code === 'h' || code === 'H'; const unitSize = nibbleMode ? 0.5 : 1; const requested = quantifier === '*' ? (nibbleMode ? (bytes.length - cursor) * 2 : bytes.length - cursor) : quantifier; if (!Number.isFinite(requested) || requested < 0) { return false; } if (nibbleMode) { const byteCount = Math.ceil(requested * unitSize); if (cursor + byteCount > bytes.length) { return false; } let hex = ''; for (let i = 0; i < byteCount; i++) { const pair = (bytes[cursor + i] ?? 0).toString(16).padStart(2, '0'); hex += code === 'h' ? `${pair[1] ?? '0'}${pair[0] ?? '0'}` : pair; } addResult(out, nextNumericKey, label, 0, 1, hex.slice(0, requested)); cursor += byteCount; continue; } if (cursor + requested > bytes.length) { return false; } const chunk = String.fromCharCode(...bytes.slice(cursor, cursor + requested)); const normalized = code === 'a' ? chunk.replace(/\0+$/g, '') : chunk.replace(/[\0 ]+$/g, ''); addResult(out, nextNumericKey, label, 0, 1, normalized); cursor += requested; continue; } let bytesPerValue = 1; switch (code) { case 's': case 'S': case 'v': case 'n': bytesPerValue = 2; break; case 'i': case 'I': case 'l': case 'L': case 'V': case 'N': case 'f': bytesPerValue = 4; break; case 'd': bytesPerValue = 8; break; default: bytesPerValue = 1; break; } const count = resolveCount(quantifier, cursor, bytesPerValue, bytes.length); for (let i = 0; i < count; i++) { if (cursor + bytesPerValue > bytes.length) { return false; } let value; switch (code) { case 'c': { const byte = bytes[cursor] ?? 0; value = byte > 0x7f ? byte - 0x100 : byte; break; } case 'C': value = bytes[cursor] ?? 0; break; case 's': value = readInt16(bytes, cursor, true); break; case 'S': case 'v': value = readUInt16(bytes, cursor, true); break; case 'n': value = readUInt16(bytes, cursor, false); break; case 'i': case 'l': value = readInt32(bytes, cursor, true); break; case 'I': case 'L': case 'V': value = readUInt32(bytes, cursor, true); break; case 'N': value = readUInt32(bytes, cursor, false); break; case 'f': value = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getFloat32(cursor, true); break; case 'd': value = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getFloat64(cursor, true); break; default: return false; } addResult(out, nextNumericKey, label, i, count, value); cursor += bytesPerValue; } } return out; }