hex.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. import { toUint8Array } from "../bytes/index.js";
  2. const HEX_CHARACTER_REGEX = /^[0-9a-f]$/i;
  3. const COMMON_SEPARATORS = [" ", "\t", "\n", "\r", ":", "-", "."];
  4. function resolveSeparators(options) {
  5. if (options.separators === "none") {
  6. return [];
  7. }
  8. if (!options.separators || options.separators === "common") {
  9. return COMMON_SEPARATORS;
  10. }
  11. return options.separators;
  12. }
  13. function validateSeparator(separator) {
  14. if (!separator) {
  15. throw new TypeError("Hex separators must be non-empty strings");
  16. }
  17. }
  18. function matchSeparator(text, index, separators) {
  19. for (const separator of separators) {
  20. if (text.startsWith(separator, index)) {
  21. return separator;
  22. }
  23. }
  24. return undefined;
  25. }
  26. function detectCase(text) {
  27. const hasUpper = /[A-F]/.test(text);
  28. const hasLower = /[a-f]/.test(text);
  29. return hasUpper && !hasLower ? "upper" : "lower";
  30. }
  31. function detectLineSeparator(text) {
  32. const match = /\r\n|\n/.exec(text);
  33. if (!match) {
  34. return undefined;
  35. }
  36. return match[0] === "\r\n" ? "\r\n" : "\n";
  37. }
  38. function compactForDetection(text) {
  39. return text.replace(/[^0-9a-f]/gi, "");
  40. }
  41. function detectGroup(text) {
  42. const segments = text.match(/[0-9A-Fa-f]+|[^0-9A-Fa-f]+/g) ?? [];
  43. if (segments.length < 3) {
  44. return undefined;
  45. }
  46. const hexSegments = segments.filter((_, index) => index % 2 === 0);
  47. const separators = segments.filter((_, index) => index % 2 === 1);
  48. const separator = separators[0];
  49. if (!separator || separators.some((item) => item !== separator)) {
  50. return undefined;
  51. }
  52. if (hexSegments.some((segment) => segment.length === 0 || segment.length % 2 !== 0)) {
  53. return undefined;
  54. }
  55. const firstLength = hexSegments[0]?.length ?? 0;
  56. if (!firstLength) {
  57. return undefined;
  58. }
  59. if (hexSegments.slice(0, -1).some((segment) => segment.length !== firstLength)) {
  60. return undefined;
  61. }
  62. if ((hexSegments[hexSegments.length - 1]?.length ?? 0) > firstLength) {
  63. return undefined;
  64. }
  65. return {
  66. size: firstLength / 2,
  67. separator,
  68. };
  69. }
  70. function detectFormat(text) {
  71. const trimmed = text.trim();
  72. const prefix = /^0x/i.test(trimmed) ? "0x" : "";
  73. const body = prefix ? trimmed.slice(2) : trimmed;
  74. const lineSeparator = detectLineSeparator(body);
  75. const lines = body.split(/\r\n|\n/).filter((line) => line.length > 0);
  76. const sampleLine = lines[0]?.trim() ?? "";
  77. const group = detectGroup(sampleLine);
  78. const format = {
  79. case: detectCase(trimmed),
  80. prefix,
  81. };
  82. if (group) {
  83. format.group = group;
  84. }
  85. if (lineSeparator && lines.length > 1) {
  86. const firstLineBytes = compactForDetection(lines[0] ?? "").length / 2;
  87. if (firstLineBytes > 0 && lines.slice(0, -1).every((line) => compactForDetection(line).length / 2 === firstLineBytes)) {
  88. format.line = {
  89. bytesPerLine: firstLineBytes,
  90. separator: lineSeparator,
  91. };
  92. }
  93. }
  94. return format;
  95. }
  96. function normalizeText(text, options) {
  97. const allowPrefix = options.allowPrefix ?? true;
  98. const separators = [...resolveSeparators(options)].sort((left, right) => right.length - left.length);
  99. for (const separator of separators) {
  100. validateSeparator(separator);
  101. }
  102. let working = text.trim();
  103. if (/^0x/i.test(working)) {
  104. if (!allowPrefix) {
  105. throw new TypeError("Hexadecimal text must not include a 0x prefix");
  106. }
  107. working = working.slice(2);
  108. }
  109. let normalized = "";
  110. let lastTokenWasSeparator = false;
  111. for (let index = 0; index < working.length;) {
  112. const character = working[index] ?? "";
  113. if (HEX_CHARACTER_REGEX.test(character)) {
  114. normalized += character;
  115. lastTokenWasSeparator = false;
  116. index += 1;
  117. continue;
  118. }
  119. const separator = matchSeparator(working, index, separators);
  120. if (!separator) {
  121. throw new TypeError("Input is not valid hexadecimal text");
  122. }
  123. if (options.strict && (lastTokenWasSeparator || normalized.length === 0)) {
  124. throw new TypeError("Hexadecimal text contains misplaced separators");
  125. }
  126. lastTokenWasSeparator = true;
  127. index += separator.length;
  128. }
  129. if (options.strict && lastTokenWasSeparator && normalized.length > 0) {
  130. throw new TypeError("Hexadecimal text must not end with a separator");
  131. }
  132. if (normalized.length % 2 !== 0) {
  133. if (!options.allowOddLength) {
  134. throw new TypeError("Hexadecimal text must contain an even number of characters");
  135. }
  136. normalized = `0${normalized}`;
  137. }
  138. return normalized.toLowerCase();
  139. }
  140. function groupPairs(pairs, group) {
  141. if (!group) {
  142. return pairs.join("");
  143. }
  144. if (!Number.isInteger(group.size) || group.size < 1) {
  145. throw new RangeError("Hex group size must be a positive integer");
  146. }
  147. const chunks = [];
  148. for (let index = 0; index < pairs.length; index += group.size) {
  149. chunks.push(pairs.slice(index, index + group.size).join(""));
  150. }
  151. return chunks.join(group.separator);
  152. }
  153. export function normalize(text, options = {}) {
  154. return normalizeText(text, options);
  155. }
  156. export function is(text, options = {}) {
  157. if (typeof text !== "string") {
  158. return false;
  159. }
  160. try {
  161. normalize(text, options);
  162. return true;
  163. }
  164. catch {
  165. return false;
  166. }
  167. }
  168. export function encode(data, options = {}) {
  169. const bytes = toUint8Array(data);
  170. const casing = options.case ?? "lower";
  171. const pairs = Array.from(bytes, (byte) => {
  172. const text = byte.toString(16).padStart(2, "0");
  173. return casing === "upper" ? text.toUpperCase() : text;
  174. });
  175. let body = "";
  176. if (options.line) {
  177. const bytesPerLine = options.line.bytesPerLine;
  178. if (!Number.isInteger(bytesPerLine) || bytesPerLine < 1) {
  179. throw new RangeError("Hex bytesPerLine must be a positive integer");
  180. }
  181. const separator = options.line.separator ?? "\n";
  182. const lines = [];
  183. for (let index = 0; index < pairs.length; index += bytesPerLine) {
  184. lines.push(groupPairs(pairs.slice(index, index + bytesPerLine), options.group));
  185. }
  186. body = lines.join(separator);
  187. }
  188. else {
  189. body = groupPairs(pairs, options.group);
  190. }
  191. return `${options.prefix ?? ""}${body}`;
  192. }
  193. export function decode(text, options = {}) {
  194. const normalized = normalize(text, options);
  195. const result = new Uint8Array(normalized.length / 2);
  196. for (let i = 0; i < normalized.length; i += 2) {
  197. result[i / 2] = Number.parseInt(normalized.slice(i, i + 2), 16);
  198. }
  199. return result;
  200. }
  201. export function parse(text, options = {}) {
  202. const normalized = normalize(text, options);
  203. return {
  204. bytes: decode(normalized),
  205. format: detectFormat(text),
  206. normalized,
  207. };
  208. }
  209. export function format(data, value) {
  210. return encode(data, value);
  211. }
  212. export const formats = {
  213. compact: Object.freeze({}),
  214. upper: Object.freeze({ case: "upper" }),
  215. colon: Object.freeze({ group: { size: 1, separator: ":" } }),
  216. colonUpper: Object.freeze({ case: "upper", group: { size: 1, separator: ":" } }),
  217. groupsOf4: Object.freeze({ group: { size: 4, separator: " " } }),
  218. prefixed: Object.freeze({ prefix: "0x" }),
  219. };
  220. export const hex = { encode, decode, format, formats, is, normalize, parse };