Spaces:
Sleeping
Sleeping
| import { toUint8Array } from "../bytes/index.js"; | |
| const HEX_CHARACTER_REGEX = /^[0-9a-f]$/i; | |
| const COMMON_SEPARATORS = [" ", "\t", "\n", "\r", ":", "-", "."]; | |
| function resolveSeparators(options) { | |
| if (options.separators === "none") { | |
| return []; | |
| } | |
| if (!options.separators || options.separators === "common") { | |
| return COMMON_SEPARATORS; | |
| } | |
| return options.separators; | |
| } | |
| function validateSeparator(separator) { | |
| if (!separator) { | |
| throw new TypeError("Hex separators must be non-empty strings"); | |
| } | |
| } | |
| function matchSeparator(text, index, separators) { | |
| for (const separator of separators) { | |
| if (text.startsWith(separator, index)) { | |
| return separator; | |
| } | |
| } | |
| return undefined; | |
| } | |
| function detectCase(text) { | |
| const hasUpper = /[A-F]/.test(text); | |
| const hasLower = /[a-f]/.test(text); | |
| return hasUpper && !hasLower ? "upper" : "lower"; | |
| } | |
| function detectLineSeparator(text) { | |
| const match = /\r\n|\n/.exec(text); | |
| if (!match) { | |
| return undefined; | |
| } | |
| return match[0] === "\r\n" ? "\r\n" : "\n"; | |
| } | |
| function compactForDetection(text) { | |
| return text.replace(/[^0-9a-f]/gi, ""); | |
| } | |
| function detectGroup(text) { | |
| const segments = text.match(/[0-9A-Fa-f]+|[^0-9A-Fa-f]+/g) ?? []; | |
| if (segments.length < 3) { | |
| return undefined; | |
| } | |
| const hexSegments = segments.filter((_, index) => index % 2 === 0); | |
| const separators = segments.filter((_, index) => index % 2 === 1); | |
| const separator = separators[0]; | |
| if (!separator || separators.some((item) => item !== separator)) { | |
| return undefined; | |
| } | |
| if (hexSegments.some((segment) => segment.length === 0 || segment.length % 2 !== 0)) { | |
| return undefined; | |
| } | |
| const firstLength = hexSegments[0]?.length ?? 0; | |
| if (!firstLength) { | |
| return undefined; | |
| } | |
| if (hexSegments.slice(0, -1).some((segment) => segment.length !== firstLength)) { | |
| return undefined; | |
| } | |
| if ((hexSegments[hexSegments.length - 1]?.length ?? 0) > firstLength) { | |
| return undefined; | |
| } | |
| return { | |
| size: firstLength / 2, | |
| separator, | |
| }; | |
| } | |
| function detectFormat(text) { | |
| const trimmed = text.trim(); | |
| const prefix = /^0x/i.test(trimmed) ? "0x" : ""; | |
| const body = prefix ? trimmed.slice(2) : trimmed; | |
| const lineSeparator = detectLineSeparator(body); | |
| const lines = body.split(/\r\n|\n/).filter((line) => line.length > 0); | |
| const sampleLine = lines[0]?.trim() ?? ""; | |
| const group = detectGroup(sampleLine); | |
| const format = { | |
| case: detectCase(trimmed), | |
| prefix, | |
| }; | |
| if (group) { | |
| format.group = group; | |
| } | |
| if (lineSeparator && lines.length > 1) { | |
| const firstLineBytes = compactForDetection(lines[0] ?? "").length / 2; | |
| if (firstLineBytes > 0 && lines.slice(0, -1).every((line) => compactForDetection(line).length / 2 === firstLineBytes)) { | |
| format.line = { | |
| bytesPerLine: firstLineBytes, | |
| separator: lineSeparator, | |
| }; | |
| } | |
| } | |
| return format; | |
| } | |
| function normalizeText(text, options) { | |
| const allowPrefix = options.allowPrefix ?? true; | |
| const separators = [...resolveSeparators(options)].sort((left, right) => right.length - left.length); | |
| for (const separator of separators) { | |
| validateSeparator(separator); | |
| } | |
| let working = text.trim(); | |
| if (/^0x/i.test(working)) { | |
| if (!allowPrefix) { | |
| throw new TypeError("Hexadecimal text must not include a 0x prefix"); | |
| } | |
| working = working.slice(2); | |
| } | |
| let normalized = ""; | |
| let lastTokenWasSeparator = false; | |
| for (let index = 0; index < working.length;) { | |
| const character = working[index] ?? ""; | |
| if (HEX_CHARACTER_REGEX.test(character)) { | |
| normalized += character; | |
| lastTokenWasSeparator = false; | |
| index += 1; | |
| continue; | |
| } | |
| const separator = matchSeparator(working, index, separators); | |
| if (!separator) { | |
| throw new TypeError("Input is not valid hexadecimal text"); | |
| } | |
| if (options.strict && (lastTokenWasSeparator || normalized.length === 0)) { | |
| throw new TypeError("Hexadecimal text contains misplaced separators"); | |
| } | |
| lastTokenWasSeparator = true; | |
| index += separator.length; | |
| } | |
| if (options.strict && lastTokenWasSeparator && normalized.length > 0) { | |
| throw new TypeError("Hexadecimal text must not end with a separator"); | |
| } | |
| if (normalized.length % 2 !== 0) { | |
| if (!options.allowOddLength) { | |
| throw new TypeError("Hexadecimal text must contain an even number of characters"); | |
| } | |
| normalized = `0${normalized}`; | |
| } | |
| return normalized.toLowerCase(); | |
| } | |
| function groupPairs(pairs, group) { | |
| if (!group) { | |
| return pairs.join(""); | |
| } | |
| if (!Number.isInteger(group.size) || group.size < 1) { | |
| throw new RangeError("Hex group size must be a positive integer"); | |
| } | |
| const chunks = []; | |
| for (let index = 0; index < pairs.length; index += group.size) { | |
| chunks.push(pairs.slice(index, index + group.size).join("")); | |
| } | |
| return chunks.join(group.separator); | |
| } | |
| export function normalize(text, options = {}) { | |
| return normalizeText(text, options); | |
| } | |
| export function is(text, options = {}) { | |
| if (typeof text !== "string") { | |
| return false; | |
| } | |
| try { | |
| normalize(text, options); | |
| return true; | |
| } | |
| catch { | |
| return false; | |
| } | |
| } | |
| export function encode(data, options = {}) { | |
| const bytes = toUint8Array(data); | |
| const casing = options.case ?? "lower"; | |
| const pairs = Array.from(bytes, (byte) => { | |
| const text = byte.toString(16).padStart(2, "0"); | |
| return casing === "upper" ? text.toUpperCase() : text; | |
| }); | |
| let body = ""; | |
| if (options.line) { | |
| const bytesPerLine = options.line.bytesPerLine; | |
| if (!Number.isInteger(bytesPerLine) || bytesPerLine < 1) { | |
| throw new RangeError("Hex bytesPerLine must be a positive integer"); | |
| } | |
| const separator = options.line.separator ?? "\n"; | |
| const lines = []; | |
| for (let index = 0; index < pairs.length; index += bytesPerLine) { | |
| lines.push(groupPairs(pairs.slice(index, index + bytesPerLine), options.group)); | |
| } | |
| body = lines.join(separator); | |
| } | |
| else { | |
| body = groupPairs(pairs, options.group); | |
| } | |
| return `${options.prefix ?? ""}${body}`; | |
| } | |
| export function decode(text, options = {}) { | |
| const normalized = normalize(text, options); | |
| const result = new Uint8Array(normalized.length / 2); | |
| for (let i = 0; i < normalized.length; i += 2) { | |
| result[i / 2] = Number.parseInt(normalized.slice(i, i + 2), 16); | |
| } | |
| return result; | |
| } | |
| export function parse(text, options = {}) { | |
| const normalized = normalize(text, options); | |
| return { | |
| bytes: decode(normalized), | |
| format: detectFormat(text), | |
| normalized, | |
| }; | |
| } | |
| export function format(data, value) { | |
| return encode(data, value); | |
| } | |
| export const formats = { | |
| compact: Object.freeze({}), | |
| upper: Object.freeze({ case: "upper" }), | |
| colon: Object.freeze({ group: { size: 1, separator: ":" } }), | |
| colonUpper: Object.freeze({ case: "upper", group: { size: 1, separator: ":" } }), | |
| groupsOf4: Object.freeze({ group: { size: 4, separator: " " } }), | |
| prefixed: Object.freeze({ prefix: "0x" }), | |
| }; | |
| export const hex = { encode, decode, format, formats, is, normalize, parse }; | |