registry.js 6.4 KB

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