registry.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.createConverterRegistry = createConverterRegistry;
  4. function keyOf(name) {
  5. return name.trim().toLowerCase();
  6. }
  7. function toError(error) {
  8. return error instanceof Error ? error : new Error(String(error));
  9. }
  10. function removeConverter(converters, primaryNames, converter) {
  11. for (const alias of [converter.name, ...(converter.aliases ?? [])]) {
  12. converters.delete(keyOf(alias));
  13. }
  14. primaryNames.delete(keyOf(converter.name));
  15. }
  16. function requireCapability(converter, name, capability) {
  17. const method = converter[capability];
  18. if (typeof method !== "function") {
  19. throw new Error(`Converter '${name}' does not support ${capability}()`);
  20. }
  21. return method;
  22. }
  23. function detectConfidence(name, text, converter) {
  24. const normalizedName = keyOf(converter.name || name);
  25. const trimmed = text.trim();
  26. if (!trimmed) {
  27. return 0;
  28. }
  29. let accepted = false;
  30. if (converter.is) {
  31. accepted = converter.is(text);
  32. }
  33. let decodable = false;
  34. try {
  35. converter.decode(text);
  36. decodable = true;
  37. }
  38. catch {
  39. decodable = false;
  40. }
  41. if (!accepted && !decodable) {
  42. return 0;
  43. }
  44. switch (normalizedName) {
  45. case "pem":
  46. return /-----BEGIN [^-]+-----/.test(text) ? 1 : 0;
  47. case "hex": {
  48. const compact = trimmed.replace(/^0x/i, "").replace(/[\s:.-]/g, "");
  49. if (!compact || /[^0-9a-f]/i.test(compact) || compact.length % 2 !== 0) {
  50. return 0;
  51. }
  52. if (/^0x/i.test(trimmed) || /[:\s.-]/.test(trimmed)) {
  53. return 0.95;
  54. }
  55. if (/[a-f]/.test(trimmed) || /[A-F]/.test(trimmed)) {
  56. return 0.8;
  57. }
  58. return 0.45;
  59. }
  60. case "base64url":
  61. if (/[-_]/.test(trimmed)) {
  62. return 0.95;
  63. }
  64. if (/=/.test(trimmed)) {
  65. return 0.1;
  66. }
  67. return 0.6;
  68. case "base64":
  69. if (/[+/=]/.test(trimmed)) {
  70. return 0.9;
  71. }
  72. return 0.55;
  73. case "binary":
  74. case "utf8":
  75. case "utf16be":
  76. case "utf16le":
  77. return 0;
  78. default:
  79. return accepted && decodable ? 0.75 : 0.5;
  80. }
  81. }
  82. function createConverterRegistry(initialConverters = []) {
  83. const converters = new Map();
  84. const primaryNames = new Set();
  85. const api = {
  86. register(converter, options = {}) {
  87. if (!converter.name || !keyOf(converter.name)) {
  88. throw new TypeError("Converter name is required");
  89. }
  90. const names = [...new Set([converter.name, ...(converter.aliases ?? [])].map(keyOf))];
  91. const conflicts = new Set();
  92. for (const name of names) {
  93. const existing = converters.get(name);
  94. if (!existing) {
  95. continue;
  96. }
  97. if (!options.override) {
  98. throw new Error(`Converter '${name}' is already registered`);
  99. }
  100. conflicts.add(existing);
  101. }
  102. for (const conflicting of conflicts) {
  103. removeConverter(converters, primaryNames, conflicting);
  104. }
  105. for (const name of names) {
  106. converters.set(name, converter);
  107. }
  108. primaryNames.add(keyOf(converter.name));
  109. return this;
  110. },
  111. unregister(name) {
  112. const converter = converters.get(keyOf(name));
  113. if (!converter) {
  114. return false;
  115. }
  116. removeConverter(converters, primaryNames, converter);
  117. return true;
  118. },
  119. has(name) {
  120. return converters.has(keyOf(name));
  121. },
  122. get(name) {
  123. const converter = converters.get(keyOf(name));
  124. if (!converter) {
  125. throw new Error(`Converter '${name}' is not registered`);
  126. }
  127. return converter;
  128. },
  129. list() {
  130. return [...primaryNames].map((name) => this.get(name));
  131. },
  132. encode(name, data, options) {
  133. return this.get(name).encode(data, options);
  134. },
  135. decode(name, text, options) {
  136. return this.get(name).decode(text, options);
  137. },
  138. tryDecode(name, text, options) {
  139. try {
  140. return { ok: true, bytes: this.decode(name, text, options) };
  141. }
  142. catch (error) {
  143. return { ok: false, error: toError(error) };
  144. }
  145. },
  146. normalize(name, text, options) {
  147. const converter = this.get(name);
  148. return requireCapability(converter, name, "normalize").call(converter, text, options);
  149. },
  150. parse(name, text, options) {
  151. const converter = this.get(name);
  152. return requireCapability(converter, name, "parse").call(converter, text, options);
  153. },
  154. format(name, data, format) {
  155. const converter = this.get(name);
  156. return requireCapability(converter, name, "format").call(converter, data, format);
  157. },
  158. transcode(text, options) {
  159. const bytes = this.decode(options.from, text, options.fromOptions);
  160. return this.encode(options.to, bytes, options.toOptions);
  161. },
  162. detect(text, options = {}) {
  163. const formatNames = options.formats?.length
  164. ? options.formats.map((name) => String(name))
  165. : this.list()
  166. .map((converter) => converter.name)
  167. .filter((name) => !["binary", "utf8", "utf16be", "utf16le"].includes(keyOf(name)));
  168. const detections = new Map();
  169. for (const requestedName of formatNames) {
  170. const converter = this.get(requestedName);
  171. const confidence = detectConfidence(requestedName, text, converter);
  172. if (confidence <= 0) {
  173. continue;
  174. }
  175. const format = converter.name;
  176. const current = detections.get(format);
  177. if (!current || confidence > current.confidence) {
  178. detections.set(format, { format, confidence });
  179. }
  180. }
  181. return [...detections.values()].sort((left, right) => right.confidence - left.confidence);
  182. },
  183. };
  184. for (const converter of initialConverters) {
  185. api.register(converter);
  186. }
  187. return api;
  188. }