hex.js 7.6 KB

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