| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- 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 };
|