@bigdreamsweb3/wordbin 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,7 @@ console.log("Dictionary Version used (header byte):", encoded.dictVersion);
47
47
  // → Dictionary Version used (header byte):: 2
48
48
 
49
49
  console.log("Encoded bytes:", encoded.encodedBytes);
50
- // → Base64 payload: Encoded bytes: 34
50
+ // → Encoded bytes: 34
51
51
 
52
52
  console.log("Original bytes:", encoded.originalBytes);
53
53
  // → Original bytes: 70
@@ -90,21 +90,21 @@ npx wordbin build --custom ./my-local-words.txt
90
90
 
91
91
  ## Dictionary Versions
92
92
 
93
- | Version | Filename | Words | Source | ID bytes | Best for |
94
- | ------- | --------------------- | ----- | ------------------ | -------- | --------------------------------------- |
95
- | v1 | wordbin-v1-bip39.json | 2,048 | BIP-39 English | ~2 | Crypto recovery seeds, high reliability |
96
- | v2 | wordbin-v2-dwyl.json | ~466k | dwyl/english-words | 2–4 | General English, tags, keywords |
97
-
98
- > **v2 note**: The dwyl list contains capitalized words. Encoding/decoding is case-sensitive. Normalize to lowercase for case-insensitive behavior.
93
+ | Version | Filename | Words | Source | ID bytes | Best for |
94
+ | ------- | --------------------- | -------- | ------------------ | -------- | --------------------------------------- |
95
+ | v1 | wordbin-v1-bip39.json | 2,048 | BIP-39 English | ~2 | Crypto recovery seeds, high reliability |
96
+ | v2 | wordbin-v2-dwyl.json | ~479,000 | dwyl/english-words | 2–4 | General English, tags, keywords |
99
97
 
100
98
  ---
101
99
 
102
100
  ## How It Works
103
101
 
104
- 1. Each word SHA-256 → first **2–4 bytes** = deterministic ID.
105
- 2. Encoded payload: `[MAGIC_BYTES ('WB') | VERSION | WORD_IDS...]`
106
- 3. Decoding reverses IDs original words using the same dictionary.
107
- 4. Optimized for **short sequences**, not arbitrary long text.
102
+ 1. Each word is hashed → first **2–4 bytes** = its fixed ID.
103
+ 2. If the ID is in the dictionary → use the short ID.
104
+ Otherwise store the word as a compact literal block.
105
+ 3. Payload begins with one byte: **dictionary version**.
106
+ 4. Decoding uses the same dictionary to reverse IDs → original words.
107
+ 5. Backtracking handles any ID length ambiguity → guaranteed correct output.
108
108
 
109
109
  ---
110
110
 
@@ -85,10 +85,19 @@ async function buildDictionary(words, options = {}) {
85
85
  const normalizedWords = words.map((w) => w.trim().toLowerCase()).filter((w) => w);
86
86
  await Promise.all(
87
87
  normalizedWords.map(async (word) => {
88
- const id = await generateWordId(word);
89
- const key = toHex(id);
90
- if (!map[key]) map[key] = [];
91
- map[key].push(word);
88
+ let attempt = 0;
89
+ let key;
90
+ while (true) {
91
+ const id = await generateWordId(
92
+ attempt === 0 ? word : `${word}:${attempt}`
93
+ );
94
+ key = toHex(id);
95
+ if (!map[key]) {
96
+ map[key] = [word];
97
+ break;
98
+ }
99
+ attempt++;
100
+ }
92
101
  })
93
102
  );
94
103
  Object.values(map).forEach((collisions) => {
@@ -101,14 +110,13 @@ async function buildDictionary(words, options = {}) {
101
110
  };
102
111
  }
103
112
  export {
104
- toHex as a,
113
+ utf8Decode as a,
105
114
  buildDictionary as b,
106
- utf8Decode as c,
115
+ toHex as c,
107
116
  decodeVarint as d,
108
117
  encodeVarint as e,
109
118
  fromBase64 as f,
110
- generateWordId as g,
111
119
  toBase64 as t,
112
120
  utf8Encode as u
113
121
  };
114
- //# sourceMappingURL=dictionary-D3gr2Ala.js.map
122
+ //# sourceMappingURL=builder-e2OwBYJh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"builder-e2OwBYJh.js","sources":["../src/core/tiers.ts","../src/core/id.ts","../src/utils/buffer.ts","../src/dict/builder.ts"],"sourcesContent":["export function getIdByteLength(wordLength: number): number {\r\n if (wordLength <= 4) return 2;\r\n if (wordLength <= 9) return 3;\r\n return 4;\r\n}\r\n\r\nexport function getWrapByteLength(wordLength: number): number {\r\n if (wordLength <= 4) return 2;\r\n if (wordLength <= 9) return 3;\r\n return 4;\r\n}\r\n\r\nexport async function getTextEncoder(): Promise<TextEncoder> {\r\n if (typeof TextEncoder !== \"undefined\") return new TextEncoder();\r\n const { TextEncoder: NodeTextEncoder } = await import(\"node:util\");\r\n // @ts-ignore Node typings\r\n return new NodeTextEncoder();\r\n}\r\n\r\nexport async function wrapBase64(data: string): Promise<Uint8Array> {\r\n const normalized = data.trim().toLowerCase();\r\n if (!normalized) throw new Error(\"Cannot generate ID for empty string\");\r\n\r\n const encoder = await getTextEncoder();\r\n const result = encoder.encode(normalized);\r\n\r\n // Browser + Node compatible SHA-256\r\n let hash: ArrayBuffer;\r\n const anyCrypto: any = (globalThis as any).crypto;\r\n if (anyCrypto && anyCrypto.subtle) {\r\n hash = await anyCrypto.subtle.digest(\"SHA-256\", result);\r\n } else {\r\n const { createHash } = await import(\"node:crypto\");\r\n hash = createHash(\"sha256\").update(Buffer.from(result)).digest().buffer;\r\n }\r\n\r\n const hashBytes = new Uint8Array(hash);\r\n const size = getWrapByteLength(normalized.length);\r\n return hashBytes.slice(0, size);\r\n}\r\n","import { getIdByteLength } from './tiers.js'\r\n\r\n/**\r\n * Deterministic word \t ID generator\r\n * Same output on browser and node (when using compatible input)\r\n */\r\nexport async function generateWordId(word: string): Promise<Uint8Array> {\r\n const normalized = word.trim().toLowerCase()\r\n if (!normalized) throw new Error('Cannot generate ID for empty string')\r\n\r\n const encoder = await getTextEncoder()\r\n const data = encoder.encode(normalized)\r\n\r\n // Browser + Node compatible SHA-256\r\n let hash: ArrayBuffer\r\n const anyCrypto: any = (globalThis as any).crypto\r\n if (anyCrypto && anyCrypto.subtle) {\r\n hash = await anyCrypto.subtle.digest('SHA-256', data)\r\n } else {\r\n const { createHash } = await import('node:crypto')\r\n hash = createHash('sha256').update(Buffer.from(data)).digest().buffer\r\n }\r\n\r\n const hashBytes = new Uint8Array(hash)\r\n const size = getIdByteLength(normalized.length)\r\n return hashBytes.slice(0, size)\r\n}\r\n\r\nasync function getTextEncoder(): Promise<TextEncoder> {\r\n if (typeof TextEncoder !== 'undefined') return new TextEncoder()\r\n const { TextEncoder: NodeTextEncoder } = await import('node:util')\r\n // @ts-ignore Node typings\r\n return new NodeTextEncoder()\r\n}\r\n","export function toHex(bytes: Uint8Array): string {\r\n return Array.from(bytes)\r\n .map((b) => b.toString(16).padStart(2, '0'))\r\n .join('')\r\n}\r\n\r\nexport function fromHex(hex: string): Uint8Array {\r\n if (hex.length % 2 !== 0) throw new Error('Invalid hex string length')\r\n const bytes = new Uint8Array(hex.length / 2)\r\n for (let i = 0; i < hex.length; i += 2) {\r\n bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16)\r\n }\r\n return bytes\r\n}\r\n\r\nexport function toBase64(bytes: Uint8Array): string {\r\n const b64 = (globalThis as any).btoa\r\n if (typeof b64 === 'function') {\r\n return b64(String.fromCharCode(...bytes))\r\n }\r\n // Node fallback\r\n return Buffer.from(bytes).toString('base64')\r\n}\r\n\r\nexport function fromBase64(base64: string): Uint8Array {\r\n const at = (globalThis as any).atob\r\n if (typeof at === 'function') {\r\n const binary = at(base64)\r\n const out = new Uint8Array(binary.length)\r\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)\r\n return out\r\n }\r\n // Node fallback\r\n return new Uint8Array(Buffer.from(base64, 'base64'))\r\n}\r\n\r\n// UTF-8 helpers\r\nexport function utf8Encode(str: string): Uint8Array {\r\n if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(str)\r\n // Node fallback\r\n return new Uint8Array(Buffer.from(str, 'utf8'))\r\n}\r\n\r\nexport function utf8Decode(bytes: Uint8Array): string {\r\n if (typeof TextDecoder !== 'undefined') return new TextDecoder().decode(bytes)\r\n // Node fallback\r\n return Buffer.from(bytes).toString('utf8')\r\n}\r\n\r\n// Varint (LEB128 7-bit groups) helpers\r\nexport function encodeVarint(n: number): Uint8Array {\r\n if (n < 0) throw new Error('Varint cannot encode negative numbers')\r\n const out: number[] = []\r\n do {\r\n let byte = n & 0x7f\r\n n >>>= 7\r\n if (n !== 0) byte |= 0x80\r\n out.push(byte)\r\n } while (n !== 0)\r\n return new Uint8Array(out)\r\n}\r\n\r\nexport function decodeVarint(bytes: Uint8Array, offset: number): { value: number; bytesRead: number } {\r\n let result = 0\r\n let shift = 0\r\n let pos = offset\r\n while (pos < bytes.length) {\r\n const byte = bytes[pos++]\r\n result |= (byte & 0x7f) << shift\r\n if ((byte & 0x80) === 0) {\r\n return { value: result, bytesRead: pos - offset }\r\n }\r\n shift += 7\r\n if (shift > 35) throw new Error('Varint too large')\r\n }\r\n throw new Error('Truncated varint')\r\n}\r\n","// File: src\\dict\\builder.ts\r\n\r\nimport type { WordBinDictionary } from \"../types\";\r\nimport { generateWordId } from \"../core/id.js\";\r\nimport { toHex } from \"../utils/buffer.js\";\r\n\r\nexport interface BuildDictionaryOptions {\r\n /**\r\n * Dictionary version number (used in header and for format compatibility)\r\n * @default 1\r\n */\r\n version?: number;\r\n\r\n /**\r\n * Human-readable description of this dictionary\r\n * @default \"WordBin dictionary v${version}\"\r\n */\r\n description?: string;\r\n\r\n /**\r\n * Optional: custom prefix or identifier for this dictionary build\r\n * (can be used in logs, filenames, etc.)\r\n */\r\n name?: string;\r\n}\r\n\r\nexport async function buildDictionary(\r\n words: string[],\r\n options: BuildDictionaryOptions = {},\r\n): Promise<WordBinDictionary> {\r\n const { version = 1, description = `WordBin dictionary v${version}` } =\r\n options;\r\n\r\n const map: Record<string, string[]> = {};\r\n\r\n const normalizedWords = words\r\n .map((w) => w.trim().toLowerCase())\r\n .filter((w) => w);\r\n\r\n await Promise.all(\r\n normalizedWords.map(async (word) => {\r\n let attempt = 0;\r\n let key: string;\r\n\r\n while (true) {\r\n const id = await generateWordId(\r\n attempt === 0 ? word : `${word}:${attempt}`,\r\n );\r\n\r\n key = toHex(id);\r\n\r\n // If no collision, break\r\n if (!map[key]) {\r\n map[key] = [word];\r\n break;\r\n }\r\n\r\n // Collision detected → try again\r\n attempt++;\r\n }\r\n }),\r\n );\r\n\r\n Object.values(map).forEach((collisions) => {\r\n collisions.sort((a, b) => a.localeCompare(b));\r\n });\r\n\r\n return {\r\n version,\r\n description,\r\n words: map,\r\n };\r\n}\r\n"],"names":[],"mappings":"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"}
package/dist/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import { resolve } from "node:path";
4
4
  import { createInterface } from "node:readline";
5
5
  import { stdout, stdin } from "node:process";
6
6
  import { wordlists } from "bip39";
7
- import { b as buildDictionary } from "./dictionary-D3gr2Ala.js";
7
+ import { b as buildDictionary } from "./builder-e2OwBYJh.js";
8
8
  const rl = createInterface({ input: stdin, output: stdout });
9
9
  const help = `
10
10
  WordBin CLI – Dictionary Builder
package/dist/core.d.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import { EncodeResult, WordBinDictionary } from './types';
2
2
  export declare class WordBin {
3
3
  private primaryDictVersion;
4
+ private log;
4
5
  constructor(initialDict?: WordBinDictionary, options?: {
5
6
  debug?: boolean;
6
7
  });
7
- private log;
8
8
  static createFromWords(words: string[]): Promise<WordBin>;
9
9
  static createFromJson(dictJson: WordBinDictionary): Promise<WordBin>;
10
10
  static create(options?: {
11
11
  debug?: boolean;
12
12
  }): Promise<WordBin>;
13
- private getReverseMapForVersion;
13
+ private getMapsForVersion;
14
14
  encode(text: string | EncodeResult | Uint8Array, options?: {
15
15
  dictVersion?: number;
16
16
  }): Promise<EncodeResult>;
@@ -1,4 +1,4 @@
1
- import { WordBinDictionary } from './types';
1
+ import { WordBinDictionary } from '../types';
2
2
  export interface BuildDictionaryOptions {
3
3
  /**
4
4
  * Dictionary version number (used in header and for format compatibility)
@@ -1,4 +1,4 @@
1
- import { WordBinDictionary } from './types.js';
1
+ import { WordBinDictionary } from '../types.js';
2
2
  /**
3
3
  * Scan all available dictionary versions from all data dirs
4
4
  */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { MAGIC } from './constants.js';
2
- export { buildDictionary } from './dictionary';
2
+ export { buildDictionary } from './dict/builder';
3
3
  export { WordBin } from './core.js';
4
4
  export type { EncodeResult, WordBinDictionary } from './types';
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { b as buildDictionary, t as toBase64, a as toHex, g as generateWordId, u as utf8Encode, e as encodeVarint, f as fromBase64, d as decodeVarint, c as utf8Decode } from "./dictionary-D3gr2Ala.js";
1
+ import { b as buildDictionary, t as toBase64, u as utf8Encode, e as encodeVarint, f as fromBase64, d as decodeVarint, a as utf8Decode, c as toHex } from "./builder-e2OwBYJh.js";
2
2
  import fs from "fs/promises";
3
3
  import path from "path";
4
4
  import { fileURLToPath } from "url";
@@ -88,7 +88,9 @@ class WordBin {
88
88
  };
89
89
  }
90
90
  static async createFromWords(words) {
91
- console.warn("Building dictionary from scratch – consider pre-built files");
91
+ console.warn(
92
+ "Building dictionary from scratch – consider using pre-built files"
93
+ );
92
94
  const dict = await buildDictionary(words);
93
95
  return new WordBin(dict);
94
96
  }
@@ -99,23 +101,41 @@ class WordBin {
99
101
  const latestDict = await loadLatestDictionary();
100
102
  return new WordBin(latestDict, options);
101
103
  }
102
- async getReverseMapForVersion(version) {
104
+ async getMapsForVersion(version) {
103
105
  const dict = await loadDictionaryByVersion(version);
104
106
  const reverseMap = /* @__PURE__ */ new Map();
107
+ const forwardMap = /* @__PURE__ */ new Map();
108
+ const idLengths = /* @__PURE__ */ new Set();
105
109
  for (const [hex, words] of Object.entries(dict.words)) {
106
- if (words.length > 0) reverseMap.set(hex, words[0]);
110
+ if (!words.length) continue;
111
+ if (words.length > 1) {
112
+ throw new Error(
113
+ `Dictionary corruption: ID ${hex} maps to multiple words`
114
+ );
115
+ }
116
+ const word = words[0];
117
+ const bytes = Buffer.from(hex, "hex");
118
+ idLengths.add(bytes.length);
119
+ reverseMap.set(hex, word);
120
+ forwardMap.set(word, bytes);
107
121
  }
108
- return reverseMap;
122
+ const sortedIdLengths = Array.from(idLengths).sort((a, b) => b - a);
123
+ return { reverseMap, forwardMap, sortedIdLengths };
109
124
  }
110
- async encode(text, options = {}) {
125
+ async encode(text, options) {
111
126
  let textStr;
112
- if (typeof text === "string") textStr = text;
113
- else if (text instanceof Uint8Array) textStr = toBase64(text);
114
- else textStr = text.encodedBase64;
115
- if (!textStr.trim()) {
127
+ if (typeof text === "string") {
128
+ textStr = text;
129
+ } else if (text instanceof Uint8Array) {
130
+ textStr = toBase64(text);
131
+ } else {
132
+ textStr = text.encodedBase64;
133
+ }
134
+ const trimmed = textStr.trim();
135
+ if (!trimmed) {
116
136
  return {
117
137
  originalText: "",
118
- dictVersion: 0,
138
+ dictVersion: this.primaryDictVersion,
119
139
  encoded: new Uint8Array(0),
120
140
  payload: "",
121
141
  encodedBase64: "",
@@ -125,181 +145,122 @@ class WordBin {
125
145
  ratioPercent: 100
126
146
  };
127
147
  }
128
- const words = textStr.split(/\s+/).filter(Boolean);
129
- this.log(`[encode] Input words (${words.length}):`, words);
130
- const useVersion = options.dictVersion ?? this.primaryDictVersion;
131
- this.log(`[encode] Using dictionary version: ${useVersion}`);
148
+ const words = trimmed.split(/\s+/).filter(Boolean);
149
+ const useVersion = options?.dictVersion ?? this.primaryDictVersion;
132
150
  const header = new Uint8Array([useVersion]);
133
- this.log(`[encode] Header bytes: [${[...header].join(", ")}]`);
134
- this.log(`[encode] Header hex: ${toHex(header)}`);
135
151
  const chunks = [header];
136
- const reverseMap = await this.getReverseMapForVersion(useVersion);
137
- this.log(`[encode] Reverse map loaded — size: ${reverseMap.size} entries`);
138
- this.log("[encode] Word → ID mapping:");
152
+ const { forwardMap } = await this.getMapsForVersion(useVersion);
139
153
  for (const w of words) {
140
- const id = await generateWordId(w);
141
- const key = toHex(id);
142
- this.log(` "${w}" → ID bytes: [${[...id].join(", ")}] | hex: ${key}`);
143
- if (reverseMap.has(key)) {
144
- reverseMap.get(key);
145
- this.log(` → Found in dictionary → using ${id.length}-byte ID`);
154
+ const id = forwardMap.get(w);
155
+ if (id) {
146
156
  chunks.push(id);
147
157
  } else {
148
158
  const utf8 = utf8Encode(w);
149
159
  const lenVarint = encodeVarint(utf8.length);
150
- this.log(` → NOT in dictionary → literal mode`);
151
- this.log(
152
- ` Literal length varint bytes: [${[...lenVarint].join(", ")}] (value = ${utf8.length})`
153
- );
154
- this.log(` Word UTF-8 bytes length: ${utf8.length}`);
155
160
  const out = new Uint8Array(1 + lenVarint.length + utf8.length);
156
161
  out[0] = LITERAL;
157
162
  out.set(lenVarint, 1);
158
163
  out.set(utf8, 1 + lenVarint.length);
159
- this.log(` Literal chunk bytes: [${[...out].join(", ")}]`);
160
164
  chunks.push(out);
161
165
  }
162
166
  }
163
- const totalLength = chunks.reduce((n, c) => n + c.length, 0);
167
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
164
168
  const result = new Uint8Array(totalLength);
165
- this.log(`[encode] Total encoded length: ${totalLength} bytes`);
166
- let off = 0;
167
- chunks.forEach((chunk, i) => {
168
- result.set(chunk, off);
169
- off += chunk.length;
170
- this.log(
171
- ` Chunk ${i}: ${chunk.length} bytes → offset ${off - chunk.length}`
172
- );
173
- });
174
- this.log(
175
- `[encode] Final encoded bytes (first 32): [${[...result.subarray(0, Math.min(32, result.length))].join(", ")}]`
176
- );
169
+ let offset = 0;
170
+ for (const chunk of chunks) {
171
+ result.set(chunk, offset);
172
+ offset += chunk.length;
173
+ }
177
174
  const originalBytes = new TextEncoder().encode(textStr).length;
178
175
  const base64Result = toBase64(result);
179
- this.log(`[encode] Base64 starts with: ${base64Result.slice(0, 12)}...`);
180
176
  return {
181
177
  originalText: textStr,
182
- dictVersion: result[0],
178
+ dictVersion: useVersion,
183
179
  encoded: result,
184
180
  payload: base64Result,
185
181
  encodedBase64: base64Result,
186
182
  originalBytes,
187
183
  encodedBytes: totalLength,
188
184
  bytesSaved: originalBytes - totalLength,
189
- ratioPercent: totalLength === 0 ? 100 : Math.round(totalLength / originalBytes * 100)
185
+ ratioPercent: totalLength === 0 ? 100 : Math.round(totalLength / originalBytes * 100 * 100) / 100
190
186
  };
191
187
  }
192
188
  async decode(data) {
193
189
  let buffer;
194
190
  if (typeof data === "string") {
195
- this.log(
196
- `[decode] Input is base64 string: "${data.substring(0, 20)}..."`
197
- );
198
191
  buffer = fromBase64(data);
199
- this.log(`[decode] Decoded to ${buffer.length} bytes`);
200
192
  } else {
201
193
  buffer = data;
202
- this.log(`[decode] Input is Uint8Array with ${buffer.length} bytes`);
203
194
  }
204
- this.log(`[decode] Full buffer hex: ${toHex(buffer)}`);
205
- this.log(
206
- `[decode] First 16 bytes: [${[...buffer.subarray(0, Math.min(16, buffer.length))].join(", ")}]`
207
- );
208
195
  if (buffer.length < 1) {
209
- throw new Error("Data too short");
196
+ throw new Error("Data too short to contain version byte");
210
197
  }
211
198
  const version = buffer[0];
212
- this.log(`[decode] Dictionary version from header: ${version}`);
213
- if (version < 1 || version > 100) {
214
- this.log(`[decode] Warning: unusual dictionary version ${version}`);
215
- }
216
199
  let pos = 1;
217
- this.log(`[decode] Starting decode at position ${pos}`);
218
- const reverseMap = await this.getReverseMapForVersion(version);
219
- this.log(
220
- `[decode] Reverse map loaded for v${version} — size: ${reverseMap.size} entries`
221
- );
222
- this.log(`[decode] ===== STARTING DECODE LOOP =====`);
200
+ const { reverseMap, sortedIdLengths } = await this.getMapsForVersion(version);
223
201
  const result = [];
224
- const decoded = this.tryDecode(pos, buffer, reverseMap, result, 0);
202
+ const decoded = this.tryDecode(
203
+ pos,
204
+ buffer,
205
+ reverseMap,
206
+ result,
207
+ 0,
208
+ sortedIdLengths
209
+ );
225
210
  if (decoded === null) {
226
211
  throw new Error(
227
- "No valid decode path found — possible corruption or dictionary mismatch"
212
+ "Decode failed — possible data corruption, wrong dictionary version, or unsupported format"
228
213
  );
229
214
  }
230
- this.log(`
231
- [decode] ===== DECODE COMPLETE =====`);
232
- this.log(`[decode] Total words decoded: ${result.length}`);
233
- this.log(`[decode] Final result: "${decoded}"`);
234
215
  return decoded;
235
216
  }
236
- tryDecode(pos, buffer, reverseMap, result, depth) {
237
- const indent = " ".repeat(depth);
238
- this.log(`${indent}[tryDecode] At position ${pos} (depth ${depth})`);
217
+ tryDecode(pos, buffer, reverseMap, result, depth, sortedIdLengths) {
239
218
  if (pos === buffer.length) {
240
- this.log(`${indent}[tryDecode] Reached end successfully`);
241
219
  return result.join(" ");
242
220
  }
243
- const previewLen = Math.min(8, buffer.length - pos);
244
- const preview = [...buffer.subarray(pos, pos + previewLen)].map((b) => `0x${b.toString(16).padStart(2, "0")}`).join(" ");
245
- this.log(`${indent}[tryDecode] Next ${previewLen} bytes: ${preview}`);
246
221
  if (buffer[pos] === LITERAL) {
247
- this.log(
248
- `${indent}[tryDecode] Found LITERAL marker (0x${LITERAL.toString(16)})`
249
- );
250
222
  const { value: byteLen, bytesRead } = decodeVarint(buffer, pos + 1);
251
- this.log(
252
- `${indent}[tryDecode] Varint: value=${byteLen}, bytesRead=${bytesRead}`
253
- );
223
+ if (byteLen > 1e6 || byteLen < 0) {
224
+ return null;
225
+ }
254
226
  const start = pos + 1 + bytesRead;
255
227
  const end = start + byteLen;
256
- this.log(
257
- `${indent}[tryDecode] Literal range: [${start}..${end}) (${byteLen} bytes)`
258
- );
259
228
  if (end > buffer.length) {
260
- this.log(`${indent}[tryDecode] Truncated literal — failing path`);
261
229
  return null;
262
230
  }
263
231
  const literalBytes = buffer.subarray(start, end);
264
232
  const word = utf8Decode(literalBytes);
265
- this.log(`${indent}[tryDecode] Decoded literal: "${word}"`);
266
233
  result.push(word);
267
- const res = this.tryDecode(end, buffer, reverseMap, result, depth + 1);
234
+ const res = this.tryDecode(
235
+ end,
236
+ buffer,
237
+ reverseMap,
238
+ result,
239
+ depth + 1,
240
+ sortedIdLengths
241
+ );
268
242
  if (res !== null) return res;
269
243
  result.pop();
270
- this.log(`${indent}[tryDecode] Backtracking from literal`);
271
- return null;
272
244
  }
273
- for (const len of [4, 3, 2]) {
274
- if (pos + len > buffer.length) {
275
- this.log(`${indent}[tryDecode] Skipping ${len}-byte (would exceed)`);
276
- continue;
277
- }
245
+ for (const len of sortedIdLengths) {
246
+ if (pos + len > buffer.length) continue;
278
247
  const slice = buffer.subarray(pos, pos + len);
279
248
  const key = toHex(slice);
280
- const keyBytes = [...slice].map((b) => `0x${b.toString(16).padStart(2, "0")}`).join(" ");
281
- this.log(
282
- `${indent}[tryDecode] Trying ${len}-byte: [${keyBytes}] hex=${key}`
283
- );
284
249
  if (reverseMap.has(key)) {
285
250
  const word = reverseMap.get(key);
286
- this.log(`${indent}[tryDecode] Match: "${word}" (ID: ${key})`);
287
251
  result.push(word);
288
252
  const res = this.tryDecode(
289
253
  pos + len,
290
254
  buffer,
291
255
  reverseMap,
292
256
  result,
293
- depth + 1
257
+ depth + 1,
258
+ sortedIdLengths
294
259
  );
295
260
  if (res !== null) return res;
296
261
  result.pop();
297
- this.log(`${indent}[tryDecode] Backtracking from "${word}"`);
298
- } else {
299
- this.log(`${indent}[tryDecode] No match for ${key}`);
300
262
  }
301
263
  }
302
- this.log(`${indent}[tryDecode] No valid branches — failing path`);
303
264
  return null;
304
265
  }
305
266
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":["../src/constants.ts","../src/dictionary-loader.ts","../src/core.ts"],"sourcesContent":["// wordbin\\src\\constants.ts\r\nexport const MAGIC = new Uint8Array([87, 66]) // 'W''B'\r\nexport const LITERAL = 0xff\r\n","import fs from \"fs/promises\";\r\nimport path from \"path\";\r\nimport { fileURLToPath } from \"url\";\r\nimport type { WordBinDictionary } from \"./types.js\";\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\n// 1️⃣ Bundled dictionaries inside the package (dist/data)\r\nconst PACKAGE_DATA_DIR = path.join(__dirname, \"data\");\r\n\r\n// 2️⃣ User project dictionaries (built via CLI)\r\nconst LOCAL_DATA_DIR = path.join(process.cwd(), \"data\");\r\n\r\n/**\r\n * Get all dictionary directories that exist\r\n * Priority: LOCAL first, then PACKAGE fallback\r\n */\r\nasync function getExistingDataDirs(): Promise<string[]> {\r\n const dirs: string[] = [];\r\n\r\n try {\r\n await fs.access(LOCAL_DATA_DIR);\r\n dirs.push(LOCAL_DATA_DIR);\r\n } catch {}\r\n\r\n try {\r\n await fs.access(PACKAGE_DATA_DIR);\r\n dirs.push(PACKAGE_DATA_DIR);\r\n } catch {}\r\n\r\n return dirs;\r\n}\r\n\r\n/**\r\n * Scan all available dictionary versions from all data dirs\r\n */\r\nexport async function getAllAvailableDictionaryVersions(): Promise<number[]> {\r\n const dirs = await getExistingDataDirs();\r\n const versions = new Set<number>();\r\n\r\n for (const dir of dirs) {\r\n try {\r\n const files = await fs.readdir(dir);\r\n\r\n for (const file of files) {\r\n const match = file.match(/wordbin-v(\\d+)/i);\r\n if (match) {\r\n versions.add(parseInt(match[1], 10));\r\n }\r\n }\r\n } catch {\r\n // Ignore invalid dirs silently\r\n }\r\n }\r\n\r\n return Array.from(versions).sort((a, b) => a - b);\r\n}\r\n\r\n/**\r\n * Load a specific dictionary version\r\n * LOCAL dictionaries override PACKAGE ones\r\n */\r\nexport async function loadDictionaryByVersion(\r\n version: number,\r\n): Promise<WordBinDictionary> {\r\n const dirs = await getExistingDataDirs();\r\n\r\n if (dirs.length === 0) {\r\n throw new Error(\r\n `No dictionary directories found. Expected ./data or bundled package data.`,\r\n );\r\n }\r\n\r\n for (const dir of dirs) {\r\n const files = await fs.readdir(dir);\r\n\r\n const versionFile = files.find((f) =>\r\n f.match(new RegExp(`wordbin-v${version}(?:\\\\.|-)`, \"i\")),\r\n );\r\n\r\n if (versionFile) {\r\n const filePath = path.join(dir, versionFile);\r\n const data = await fs.readFile(filePath, \"utf-8\");\r\n const dict = JSON.parse(data) as WordBinDictionary;\r\n\r\n if (dict.version !== version) {\r\n throw new Error(\r\n `Version mismatch: file ${versionFile} claims v${dict.version} but expected v${version}`,\r\n );\r\n }\r\n\r\n return dict;\r\n }\r\n }\r\n\r\n const available = await getAllAvailableDictionaryVersions();\r\n\r\n throw new Error(\r\n `Dictionary version ${version} not found. Available versions: ${available.join(\", \")}`,\r\n );\r\n}\r\n\r\n/**\r\n * Load the latest available dictionary version\r\n */\r\nexport async function loadLatestDictionary(): Promise<WordBinDictionary> {\r\n const versions = await getAllAvailableDictionaryVersions();\r\n\r\n if (versions.length === 0) {\r\n throw new Error(\r\n `No dictionary files found. Run \"npx wordbin build\" or use bundled v1.`,\r\n );\r\n }\r\n\r\n const latestVersion = Math.max(...versions);\r\n\r\n console.log(\r\n `Loading latest dictionary: v${latestVersion} (available: ${versions.join(\", \")})`,\r\n );\r\n\r\n return loadDictionaryByVersion(latestVersion);\r\n}\r\n\r\n/**\r\n * Check if a specific version exists\r\n */\r\nexport async function hasDictionaryVersion(version: number): Promise<boolean> {\r\n const versions = await getAllAvailableDictionaryVersions();\r\n return versions.includes(version);\r\n}\r\n","import { LITERAL } from \"./constants.js\";\r\nimport { generateWordId } from \"./core/id.js\";\r\nimport {\r\n toHex,\r\n toBase64,\r\n fromBase64,\r\n encodeVarint,\r\n decodeVarint,\r\n utf8Encode,\r\n utf8Decode,\r\n} from \"./utils/buffer.js\";\r\nimport type { EncodeResult, WordBinDictionary } from \"./types\";\r\nimport { buildDictionary } from \"./dictionary.js\";\r\nimport {\r\n loadDictionaryByVersion,\r\n loadLatestDictionary,\r\n} from \"./dictionary-loader.js\";\r\n\r\nexport class WordBin {\r\n private primaryDictVersion: number;\r\n\r\n constructor(initialDict?: WordBinDictionary, options?: { debug?: boolean }) {\r\n this.primaryDictVersion = initialDict?.version ?? 2;\r\n this.log = options?.debug\r\n ? (...args) => console.log(\"[WordBin]\", ...args)\r\n : () => {};\r\n }\r\n\r\n private log: (...args: any[]) => void;\r\n\r\n static async createFromWords(words: string[]): Promise<WordBin> {\r\n console.warn(\"Building dictionary from scratch – consider pre-built files\");\r\n const dict = await buildDictionary(words);\r\n return new WordBin(dict);\r\n }\r\n\r\n static async createFromJson(dictJson: WordBinDictionary): Promise<WordBin> {\r\n return new WordBin(dictJson);\r\n }\r\n\r\n static async create(options?: { debug?: boolean }): Promise<WordBin> {\r\n const latestDict = await loadLatestDictionary();\r\n return new WordBin(latestDict, options);\r\n }\r\n\r\n private async getReverseMapForVersion(\r\n version: number,\r\n ): Promise<Map<string, string>> {\r\n const dict = await loadDictionaryByVersion(version);\r\n const reverseMap = new Map<string, string>();\r\n for (const [hex, words] of Object.entries(dict.words)) {\r\n if (words.length > 0) reverseMap.set(hex, words[0]);\r\n }\r\n return reverseMap;\r\n }\r\n\r\n async encode(\r\n text: string | EncodeResult | Uint8Array,\r\n options: { dictVersion?: number } = {},\r\n ): Promise<EncodeResult> {\r\n let textStr: string;\r\n if (typeof text === \"string\") textStr = text;\r\n else if (text instanceof Uint8Array) textStr = toBase64(text);\r\n else textStr = text.encodedBase64;\r\n\r\n if (!textStr.trim()) {\r\n return {\r\n originalText: \"\",\r\n dictVersion: 0,\r\n encoded: new Uint8Array(0),\r\n payload: \"\",\r\n encodedBase64: \"\",\r\n originalBytes: 0,\r\n encodedBytes: 0,\r\n bytesSaved: 0,\r\n ratioPercent: 100,\r\n };\r\n }\r\n\r\n const words = textStr.split(/\\s+/).filter(Boolean);\r\n this.log(`[encode] Input words (${words.length}):`, words);\r\n\r\n const useVersion = options.dictVersion ?? this.primaryDictVersion;\r\n this.log(`[encode] Using dictionary version: ${useVersion}`);\r\n\r\n // ──────────────────────────────────────────────\r\n // Header: just the version byte (no magic bytes)\r\n // ──────────────────────────────────────────────\r\n const header = new Uint8Array([useVersion]);\r\n\r\n this.log(`[encode] Header bytes: [${[...header].join(\", \")}]`);\r\n this.log(`[encode] Header hex: ${toHex(header)}`);\r\n\r\n const chunks: Uint8Array[] = [header];\r\n\r\n const reverseMap = await this.getReverseMapForVersion(useVersion);\r\n this.log(`[encode] Reverse map loaded — size: ${reverseMap.size} entries`);\r\n\r\n // ──────────────────────────────────────────────\r\n // Process each word\r\n // ──────────────────────────────────────────────\r\n this.log(\"[encode] Word → ID mapping:\");\r\n\r\n for (const w of words) {\r\n const id = await generateWordId(w);\r\n const key = toHex(id);\r\n\r\n this.log(` \"${w}\" → ID bytes: [${[...id].join(\", \")}] | hex: ${key}`);\r\n\r\n if (reverseMap.has(key)) {\r\n const mappedWord = reverseMap.get(key);\r\n this.log(` → Found in dictionary → using ${id.length}-byte ID`);\r\n chunks.push(id);\r\n } else {\r\n const utf8 = utf8Encode(w);\r\n const lenVarint = encodeVarint(utf8.length);\r\n this.log(` → NOT in dictionary → literal mode`);\r\n this.log(\r\n ` Literal length varint bytes: [${[...lenVarint].join(\", \")}] (value = ${utf8.length})`,\r\n );\r\n this.log(` Word UTF-8 bytes length: ${utf8.length}`);\r\n\r\n const out = new Uint8Array(1 + lenVarint.length + utf8.length);\r\n out[0] = LITERAL;\r\n out.set(lenVarint, 1);\r\n out.set(utf8, 1 + lenVarint.length);\r\n\r\n this.log(` Literal chunk bytes: [${[...out].join(\", \")}]`);\r\n chunks.push(out);\r\n }\r\n }\r\n\r\n // ──────────────────────────────────────────────\r\n // Final assembly\r\n // ──────────────────────────────────────────────\r\n const totalLength = chunks.reduce((n, c) => n + c.length, 0);\r\n const result = new Uint8Array(totalLength);\r\n\r\n this.log(`[encode] Total encoded length: ${totalLength} bytes`);\r\n\r\n let off = 0;\r\n chunks.forEach((chunk, i) => {\r\n result.set(chunk, off);\r\n off += chunk.length;\r\n this.log(\r\n ` Chunk ${i}: ${chunk.length} bytes → offset ${off - chunk.length}`,\r\n );\r\n });\r\n\r\n this.log(\r\n `[encode] Final encoded bytes (first 32): [${[...result.subarray(0, Math.min(32, result.length))].join(\", \")}]`,\r\n );\r\n\r\n const originalBytes = new TextEncoder().encode(textStr).length;\r\n\r\n const base64Result = toBase64(result);\r\n this.log(`[encode] Base64 starts with: ${base64Result.slice(0, 12)}...`);\r\n\r\n return {\r\n originalText: textStr,\r\n dictVersion: result[0],\r\n encoded: result,\r\n payload: base64Result,\r\n encodedBase64: base64Result,\r\n originalBytes,\r\n encodedBytes: totalLength,\r\n bytesSaved: originalBytes - totalLength,\r\n ratioPercent:\r\n totalLength === 0\r\n ? 100\r\n : Math.round((totalLength / originalBytes) * 100),\r\n };\r\n }\r\n\r\n async decode(data: Uint8Array | string): Promise<string> {\r\n let buffer: Uint8Array;\r\n if (typeof data === \"string\") {\r\n this.log(\r\n `[decode] Input is base64 string: \"${data.substring(0, 20)}...\"`,\r\n );\r\n buffer = fromBase64(data);\r\n this.log(`[decode] Decoded to ${buffer.length} bytes`);\r\n } else {\r\n buffer = data;\r\n this.log(`[decode] Input is Uint8Array with ${buffer.length} bytes`);\r\n }\r\n\r\n this.log(`[decode] Full buffer hex: ${toHex(buffer)}`);\r\n this.log(\r\n `[decode] First 16 bytes: [${[...buffer.subarray(0, Math.min(16, buffer.length))].join(\", \")}]`,\r\n );\r\n\r\n if (buffer.length < 1) {\r\n throw new Error(\"Data too short\");\r\n }\r\n\r\n const version = buffer[0];\r\n this.log(`[decode] Dictionary version from header: ${version}`);\r\n\r\n // Optional: basic sanity check (you can adjust or remove)\r\n if (version < 1 || version > 100) {\r\n this.log(`[decode] Warning: unusual dictionary version ${version}`);\r\n // You can throw here if you want strict validation:\r\n // throw new Error(`Unreasonable dictionary version: ${version}`);\r\n }\r\n\r\n let pos = 1; // payload starts right after the version byte\r\n this.log(`[decode] Starting decode at position ${pos}`);\r\n\r\n const reverseMap = await this.getReverseMapForVersion(version);\r\n this.log(\r\n `[decode] Reverse map loaded for v${version} — size: ${reverseMap.size} entries`,\r\n );\r\n\r\n this.log(`[decode] ===== STARTING DECODE LOOP =====`);\r\n\r\n const result: string[] = [];\r\n const decoded = this.tryDecode(pos, buffer, reverseMap, result, 0);\r\n\r\n if (decoded === null) {\r\n throw new Error(\r\n \"No valid decode path found — possible corruption or dictionary mismatch\",\r\n );\r\n }\r\n\r\n this.log(`\\n[decode] ===== DECODE COMPLETE =====`);\r\n this.log(`[decode] Total words decoded: ${result.length}`);\r\n this.log(`[decode] Final result: \"${decoded}\"`);\r\n\r\n return decoded;\r\n }\r\n\r\n private tryDecode(\r\n pos: number,\r\n buffer: Uint8Array,\r\n reverseMap: Map<string, string>,\r\n result: string[],\r\n depth: number,\r\n ): string | null {\r\n const indent = \" \".repeat(depth);\r\n this.log(`${indent}[tryDecode] At position ${pos} (depth ${depth})`);\r\n\r\n if (pos === buffer.length) {\r\n this.log(`${indent}[tryDecode] Reached end successfully`);\r\n return result.join(\" \");\r\n }\r\n\r\n const previewLen = Math.min(8, buffer.length - pos);\r\n const preview = [...buffer.subarray(pos, pos + previewLen)]\r\n .map((b) => `0x${b.toString(16).padStart(2, \"0\")}`)\r\n .join(\" \");\r\n this.log(`${indent}[tryDecode] Next ${previewLen} bytes: ${preview}`);\r\n\r\n // Try literal first\r\n if (buffer[pos] === LITERAL) {\r\n this.log(\r\n `${indent}[tryDecode] Found LITERAL marker (0x${LITERAL.toString(16)})`,\r\n );\r\n\r\n const { value: byteLen, bytesRead } = decodeVarint(buffer, pos + 1);\r\n this.log(\r\n `${indent}[tryDecode] Varint: value=${byteLen}, bytesRead=${bytesRead}`,\r\n );\r\n\r\n const start = pos + 1 + bytesRead;\r\n const end = start + byteLen;\r\n this.log(\r\n `${indent}[tryDecode] Literal range: [${start}..${end}) (${byteLen} bytes)`,\r\n );\r\n\r\n if (end > buffer.length) {\r\n this.log(`${indent}[tryDecode] Truncated literal — failing path`);\r\n return null;\r\n }\r\n\r\n const literalBytes = buffer.subarray(start, end);\r\n const word = utf8Decode(literalBytes);\r\n this.log(`${indent}[tryDecode] Decoded literal: \"${word}\"`);\r\n\r\n result.push(word);\r\n const res = this.tryDecode(end, buffer, reverseMap, result, depth + 1);\r\n if (res !== null) return res;\r\n result.pop();\r\n this.log(`${indent}[tryDecode] Backtracking from literal`);\r\n return null;\r\n }\r\n\r\n // Try dictionary matches (longest first)\r\n for (const len of [4, 3, 2]) {\r\n if (pos + len > buffer.length) {\r\n this.log(`${indent}[tryDecode] Skipping ${len}-byte (would exceed)`);\r\n continue;\r\n }\r\n\r\n const slice = buffer.subarray(pos, pos + len);\r\n const key = toHex(slice);\r\n const keyBytes = [...slice]\r\n .map((b) => `0x${b.toString(16).padStart(2, \"0\")}`)\r\n .join(\" \");\r\n\r\n this.log(\r\n `${indent}[tryDecode] Trying ${len}-byte: [${keyBytes}] hex=${key}`,\r\n );\r\n\r\n if (reverseMap.has(key)) {\r\n const word = reverseMap.get(key)!;\r\n this.log(`${indent}[tryDecode] Match: \"${word}\" (ID: ${key})`);\r\n\r\n result.push(word);\r\n const res = this.tryDecode(\r\n pos + len,\r\n buffer,\r\n reverseMap,\r\n result,\r\n depth + 1,\r\n );\r\n if (res !== null) return res;\r\n result.pop();\r\n this.log(`${indent}[tryDecode] Backtracking from \"${word}\"`);\r\n } else {\r\n this.log(`${indent}[tryDecode] No match for ${key}`);\r\n }\r\n }\r\n\r\n this.log(`${indent}[tryDecode] No valid branches — failing path`);\r\n return null;\r\n }\r\n}\r\n"],"names":["__filename","__dirname"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","sources":["../src/constants.ts","../src/dict/dictionary-loader.ts","../src/core.ts"],"sourcesContent":["// wordbin\\src\\constants.ts\r\nexport const MAGIC = new Uint8Array([87, 66]) // 'W''B'\r\nexport const LITERAL = 0xff\r\n","import fs from \"fs/promises\";\r\nimport path from \"path\";\r\nimport { fileURLToPath } from \"url\";\r\nimport type { WordBinDictionary } from \"../types.js\";\r\n\r\nconst __filename = fileURLToPath(import.meta.url);\r\nconst __dirname = path.dirname(__filename);\r\n\r\n// 1️⃣ Bundled dictionaries inside the package (dist/data)\r\nconst PACKAGE_DATA_DIR = path.join(__dirname, \"data\");\r\n\r\n// 2️⃣ User project dictionaries (built via CLI)\r\nconst LOCAL_DATA_DIR = path.join(process.cwd(), \"data\");\r\n\r\n/**\r\n * Get all dictionary directories that exist\r\n * Priority: LOCAL first, then PACKAGE fallback\r\n */\r\nasync function getExistingDataDirs(): Promise<string[]> {\r\n const dirs: string[] = [];\r\n\r\n try {\r\n await fs.access(LOCAL_DATA_DIR);\r\n dirs.push(LOCAL_DATA_DIR);\r\n } catch {}\r\n\r\n try {\r\n await fs.access(PACKAGE_DATA_DIR);\r\n dirs.push(PACKAGE_DATA_DIR);\r\n } catch {}\r\n\r\n return dirs;\r\n}\r\n\r\n/**\r\n * Scan all available dictionary versions from all data dirs\r\n */\r\nexport async function getAllAvailableDictionaryVersions(): Promise<number[]> {\r\n const dirs = await getExistingDataDirs();\r\n const versions = new Set<number>();\r\n\r\n for (const dir of dirs) {\r\n try {\r\n const files = await fs.readdir(dir);\r\n\r\n for (const file of files) {\r\n const match = file.match(/wordbin-v(\\d+)/i);\r\n if (match) {\r\n versions.add(parseInt(match[1], 10));\r\n }\r\n }\r\n } catch {\r\n // Ignore invalid dirs silently\r\n }\r\n }\r\n\r\n return Array.from(versions).sort((a, b) => a - b);\r\n}\r\n\r\n/**\r\n * Load a specific dictionary version\r\n * LOCAL dictionaries override PACKAGE ones\r\n */\r\nexport async function loadDictionaryByVersion(\r\n version: number,\r\n): Promise<WordBinDictionary> {\r\n const dirs = await getExistingDataDirs();\r\n\r\n if (dirs.length === 0) {\r\n throw new Error(\r\n `No dictionary directories found. Expected ./data or bundled package data.`,\r\n );\r\n }\r\n\r\n for (const dir of dirs) {\r\n const files = await fs.readdir(dir);\r\n\r\n const versionFile = files.find((f) =>\r\n f.match(new RegExp(`wordbin-v${version}(?:\\\\.|-)`, \"i\")),\r\n );\r\n\r\n if (versionFile) {\r\n const filePath = path.join(dir, versionFile);\r\n const data = await fs.readFile(filePath, \"utf-8\");\r\n const dict = JSON.parse(data) as WordBinDictionary;\r\n\r\n if (dict.version !== version) {\r\n throw new Error(\r\n `Version mismatch: file ${versionFile} claims v${dict.version} but expected v${version}`,\r\n );\r\n }\r\n\r\n return dict;\r\n }\r\n }\r\n\r\n const available = await getAllAvailableDictionaryVersions();\r\n\r\n throw new Error(\r\n `Dictionary version ${version} not found. Available versions: ${available.join(\", \")}`,\r\n );\r\n}\r\n\r\n/**\r\n * Load the latest available dictionary version\r\n */\r\nexport async function loadLatestDictionary(): Promise<WordBinDictionary> {\r\n const versions = await getAllAvailableDictionaryVersions();\r\n\r\n if (versions.length === 0) {\r\n throw new Error(\r\n `No dictionary files found. Run \"npx wordbin build\" or use bundled v1.`,\r\n );\r\n }\r\n\r\n const latestVersion = Math.max(...versions);\r\n\r\n console.log(\r\n `Loading latest dictionary: v${latestVersion} (available: ${versions.join(\", \")})`,\r\n );\r\n\r\n return loadDictionaryByVersion(latestVersion);\r\n}\r\n\r\n/**\r\n * Check if a specific version exists\r\n */\r\nexport async function hasDictionaryVersion(version: number): Promise<boolean> {\r\n const versions = await getAllAvailableDictionaryVersions();\r\n return versions.includes(version);\r\n}\r\n","import { LITERAL } from \"./constants.js\";\r\nimport {\r\n toHex,\r\n toBase64,\r\n fromBase64,\r\n encodeVarint,\r\n decodeVarint,\r\n utf8Encode,\r\n utf8Decode,\r\n} from \"./utils/buffer.js\";\r\nimport type { EncodeResult, WordBinDictionary } from \"./types\";\r\nimport { buildDictionary } from \"./dict/builder\";\r\nimport {\r\n loadDictionaryByVersion,\r\n loadLatestDictionary,\r\n} from \"./dict/dictionary-loader.js\";\r\n\r\nexport class WordBin {\r\n private primaryDictVersion: number;\r\n private log: (...args: any[]) => void;\r\n\r\n constructor(initialDict?: WordBinDictionary, options?: { debug?: boolean }) {\r\n this.primaryDictVersion = initialDict?.version ?? 2;\r\n this.log = options?.debug\r\n ? (...args) => console.log(\"[WordBin]\", ...args)\r\n : () => {};\r\n }\r\n\r\n static async createFromWords(words: string[]): Promise<WordBin> {\r\n console.warn(\r\n \"Building dictionary from scratch – consider using pre-built files\",\r\n );\r\n const dict = await buildDictionary(words);\r\n return new WordBin(dict);\r\n }\r\n\r\n static async createFromJson(dictJson: WordBinDictionary): Promise<WordBin> {\r\n return new WordBin(dictJson);\r\n }\r\n\r\n static async create(options?: { debug?: boolean }): Promise<WordBin> {\r\n const latestDict = await loadLatestDictionary();\r\n return new WordBin(latestDict, options);\r\n }\r\n\r\n private async getMapsForVersion(version: number): Promise<{\r\n reverseMap: Map<string, string>;\r\n forwardMap: Map<string, Uint8Array>;\r\n sortedIdLengths: number[];\r\n }> {\r\n const dict = await loadDictionaryByVersion(version);\r\n\r\n const reverseMap = new Map<string, string>();\r\n const forwardMap = new Map<string, Uint8Array>();\r\n const idLengths = new Set<number>();\r\n\r\n for (const [hex, words] of Object.entries(dict.words)) {\r\n if (!words.length) continue;\r\n if (words.length > 1) {\r\n throw new Error(\r\n `Dictionary corruption: ID ${hex} maps to multiple words`,\r\n );\r\n }\r\n\r\n const word = words[0];\r\n const bytes = Buffer.from(hex, \"hex\"); // Buffer is a Uint8Array\r\n idLengths.add(bytes.length);\r\n\r\n reverseMap.set(hex, word);\r\n forwardMap.set(word, bytes);\r\n }\r\n\r\n const sortedIdLengths = Array.from(idLengths).sort((a, b) => b - a); // longest first\r\n\r\n return { reverseMap, forwardMap, sortedIdLengths };\r\n }\r\n\r\n async encode(\r\n text: string | EncodeResult | Uint8Array,\r\n options?: { dictVersion?: number },\r\n ): Promise<EncodeResult> {\r\n let textStr: string;\r\n if (typeof text === \"string\") {\r\n textStr = text;\r\n } else if (text instanceof Uint8Array) {\r\n textStr = toBase64(text);\r\n } else {\r\n textStr = text.encodedBase64;\r\n }\r\n\r\n const trimmed = textStr.trim();\r\n if (!trimmed) {\r\n return {\r\n originalText: \"\",\r\n dictVersion: this.primaryDictVersion,\r\n encoded: new Uint8Array(0),\r\n payload: \"\",\r\n encodedBase64: \"\",\r\n originalBytes: 0,\r\n encodedBytes: 0,\r\n bytesSaved: 0,\r\n ratioPercent: 100,\r\n };\r\n }\r\n\r\n const words = trimmed.split(/\\s+/).filter(Boolean);\r\n const useVersion = options?.dictVersion ?? this.primaryDictVersion;\r\n\r\n const header = new Uint8Array([useVersion]);\r\n const chunks: Uint8Array[] = [header];\r\n\r\n const { forwardMap } = await this.getMapsForVersion(useVersion);\r\n\r\n for (const w of words) {\r\n const id = forwardMap.get(w);\r\n if (id) {\r\n chunks.push(id);\r\n } else {\r\n const utf8 = utf8Encode(w);\r\n const lenVarint = encodeVarint(utf8.length);\r\n const out = new Uint8Array(1 + lenVarint.length + utf8.length);\r\n out[0] = LITERAL;\r\n out.set(lenVarint, 1);\r\n out.set(utf8, 1 + lenVarint.length);\r\n chunks.push(out);\r\n }\r\n }\r\n\r\n const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);\r\n const result = new Uint8Array(totalLength);\r\n let offset = 0;\r\n for (const chunk of chunks) {\r\n result.set(chunk, offset);\r\n offset += chunk.length;\r\n }\r\n\r\n const originalBytes = new TextEncoder().encode(textStr).length;\r\n const base64Result = toBase64(result);\r\n\r\n return {\r\n originalText: textStr,\r\n dictVersion: useVersion,\r\n encoded: result,\r\n payload: base64Result,\r\n encodedBase64: base64Result,\r\n originalBytes,\r\n encodedBytes: totalLength,\r\n bytesSaved: originalBytes - totalLength,\r\n ratioPercent:\r\n totalLength === 0\r\n ? 100\r\n : Math.round((totalLength / originalBytes) * 100 * 100) / 100,\r\n };\r\n }\r\n\r\n async decode(data: Uint8Array | string): Promise<string> {\r\n let buffer: Uint8Array;\r\n if (typeof data === \"string\") {\r\n buffer = fromBase64(data);\r\n } else {\r\n buffer = data;\r\n }\r\n\r\n if (buffer.length < 1) {\r\n throw new Error(\"Data too short to contain version byte\");\r\n }\r\n\r\n const version = buffer[0];\r\n let pos = 1;\r\n\r\n const { reverseMap, sortedIdLengths } =\r\n await this.getMapsForVersion(version);\r\n\r\n const result: string[] = [];\r\n const decoded = this.tryDecode(\r\n pos,\r\n buffer,\r\n reverseMap,\r\n result,\r\n 0,\r\n sortedIdLengths,\r\n );\r\n\r\n if (decoded === null) {\r\n throw new Error(\r\n \"Decode failed — possible data corruption, wrong dictionary version, or unsupported format\",\r\n );\r\n }\r\n\r\n return decoded;\r\n }\r\n\r\n private tryDecode(\r\n pos: number,\r\n buffer: Uint8Array,\r\n reverseMap: Map<string, string>,\r\n result: string[],\r\n depth: number,\r\n sortedIdLengths: number[],\r\n ): string | null {\r\n if (pos === buffer.length) {\r\n return result.join(\" \");\r\n }\r\n\r\n // 1. Try literal block\r\n if (buffer[pos] === LITERAL) {\r\n const { value: byteLen, bytesRead } = decodeVarint(buffer, pos + 1);\r\n\r\n // Basic sanity check — very large literals are suspicious\r\n if (byteLen > 1_000_000 || byteLen < 0) {\r\n return null;\r\n }\r\n\r\n const start = pos + 1 + bytesRead;\r\n const end = start + byteLen;\r\n\r\n if (end > buffer.length) {\r\n return null;\r\n }\r\n\r\n const literalBytes = buffer.subarray(start, end);\r\n const word = utf8Decode(literalBytes);\r\n\r\n result.push(word);\r\n const res = this.tryDecode(\r\n end,\r\n buffer,\r\n reverseMap,\r\n result,\r\n depth + 1,\r\n sortedIdLengths,\r\n );\r\n if (res !== null) return res;\r\n result.pop();\r\n }\r\n\r\n // 2. Try dictionary IDs — longest first\r\n for (const len of sortedIdLengths) {\r\n if (pos + len > buffer.length) continue;\r\n\r\n const slice = buffer.subarray(pos, pos + len);\r\n const key = toHex(slice);\r\n\r\n if (reverseMap.has(key)) {\r\n const word = reverseMap.get(key)!;\r\n result.push(word);\r\n\r\n const res = this.tryDecode(\r\n pos + len,\r\n buffer,\r\n reverseMap,\r\n result,\r\n depth + 1,\r\n sortedIdLengths,\r\n );\r\n if (res !== null) return res;\r\n\r\n result.pop();\r\n }\r\n }\r\n\r\n // No valid continuation found on this path\r\n return null;\r\n }\r\n}\r\n"],"names":["__filename","__dirname"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bigdreamsweb3/wordbin",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "WordBin – Encode words & short text into tiny, reversible binary for storage, URLs, IoT, QR codes, metadata, blockchain, or Web3 apps.",
5
5
  "author": "Agbaka Daniel Ugonna <99cratson@gmail.com>",
6
6
  "license": "MIT",
@@ -39,6 +39,9 @@
39
39
  },
40
40
  "scripts": {
41
41
  "build": "vite build",
42
+ "build:dict:v1": "npx tsx scripts/dictionaries/build-v1-bip39.mts",
43
+ "build:dict:v2": "npx tsx scripts/dictionaries/build-v2-dwyl.ts",
44
+ "build:dict:all": "npm run build:dict:v1 && npm run build:dict:v2",
42
45
  "clean": "rimraf dist",
43
46
  "prepack": "npm run build",
44
47
  "dev": "vite",
@@ -1 +0,0 @@
1
- {"version":3,"file":"dictionary-D3gr2Ala.js","sources":["../src/core/tiers.ts","../src/core/id.ts","../src/utils/buffer.ts","../src/dictionary.ts"],"sourcesContent":["export function getIdByteLength(wordLength: number): number {\r\n if (wordLength <= 4) return 2;\r\n if (wordLength <= 9) return 3;\r\n return 4;\r\n}\r\n\r\nexport function getWrapByteLength(wordLength: number): number {\r\n if (wordLength <= 4) return 2;\r\n if (wordLength <= 9) return 3;\r\n return 4;\r\n}\r\n\r\nexport async function getTextEncoder(): Promise<TextEncoder> {\r\n if (typeof TextEncoder !== \"undefined\") return new TextEncoder();\r\n const { TextEncoder: NodeTextEncoder } = await import(\"node:util\");\r\n // @ts-ignore Node typings\r\n return new NodeTextEncoder();\r\n}\r\n\r\nexport async function wrapBase64(data: string): Promise<Uint8Array> {\r\n const normalized = data.trim().toLowerCase();\r\n if (!normalized) throw new Error(\"Cannot generate ID for empty string\");\r\n\r\n const encoder = await getTextEncoder();\r\n const result = encoder.encode(normalized);\r\n\r\n // Browser + Node compatible SHA-256\r\n let hash: ArrayBuffer;\r\n const anyCrypto: any = (globalThis as any).crypto;\r\n if (anyCrypto && anyCrypto.subtle) {\r\n hash = await anyCrypto.subtle.digest(\"SHA-256\", result);\r\n } else {\r\n const { createHash } = await import(\"node:crypto\");\r\n hash = createHash(\"sha256\").update(Buffer.from(result)).digest().buffer;\r\n }\r\n\r\n const hashBytes = new Uint8Array(hash);\r\n const size = getWrapByteLength(normalized.length);\r\n return hashBytes.slice(0, size);\r\n}\r\n","import { getIdByteLength } from './tiers.js'\r\n\r\n/**\r\n * Deterministic word \t ID generator\r\n * Same output on browser and node (when using compatible input)\r\n */\r\nexport async function generateWordId(word: string): Promise<Uint8Array> {\r\n const normalized = word.trim().toLowerCase()\r\n if (!normalized) throw new Error('Cannot generate ID for empty string')\r\n\r\n const encoder = await getTextEncoder()\r\n const data = encoder.encode(normalized)\r\n\r\n // Browser + Node compatible SHA-256\r\n let hash: ArrayBuffer\r\n const anyCrypto: any = (globalThis as any).crypto\r\n if (anyCrypto && anyCrypto.subtle) {\r\n hash = await anyCrypto.subtle.digest('SHA-256', data)\r\n } else {\r\n const { createHash } = await import('node:crypto')\r\n hash = createHash('sha256').update(Buffer.from(data)).digest().buffer\r\n }\r\n\r\n const hashBytes = new Uint8Array(hash)\r\n const size = getIdByteLength(normalized.length)\r\n return hashBytes.slice(0, size)\r\n}\r\n\r\nasync function getTextEncoder(): Promise<TextEncoder> {\r\n if (typeof TextEncoder !== 'undefined') return new TextEncoder()\r\n const { TextEncoder: NodeTextEncoder } = await import('node:util')\r\n // @ts-ignore Node typings\r\n return new NodeTextEncoder()\r\n}\r\n","export function toHex(bytes: Uint8Array): string {\r\n return Array.from(bytes)\r\n .map((b) => b.toString(16).padStart(2, '0'))\r\n .join('')\r\n}\r\n\r\nexport function fromHex(hex: string): Uint8Array {\r\n if (hex.length % 2 !== 0) throw new Error('Invalid hex string length')\r\n const bytes = new Uint8Array(hex.length / 2)\r\n for (let i = 0; i < hex.length; i += 2) {\r\n bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16)\r\n }\r\n return bytes\r\n}\r\n\r\nexport function toBase64(bytes: Uint8Array): string {\r\n const b64 = (globalThis as any).btoa\r\n if (typeof b64 === 'function') {\r\n return b64(String.fromCharCode(...bytes))\r\n }\r\n // Node fallback\r\n return Buffer.from(bytes).toString('base64')\r\n}\r\n\r\nexport function fromBase64(base64: string): Uint8Array {\r\n const at = (globalThis as any).atob\r\n if (typeof at === 'function') {\r\n const binary = at(base64)\r\n const out = new Uint8Array(binary.length)\r\n for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i)\r\n return out\r\n }\r\n // Node fallback\r\n return new Uint8Array(Buffer.from(base64, 'base64'))\r\n}\r\n\r\n// UTF-8 helpers\r\nexport function utf8Encode(str: string): Uint8Array {\r\n if (typeof TextEncoder !== 'undefined') return new TextEncoder().encode(str)\r\n // Node fallback\r\n return new Uint8Array(Buffer.from(str, 'utf8'))\r\n}\r\n\r\nexport function utf8Decode(bytes: Uint8Array): string {\r\n if (typeof TextDecoder !== 'undefined') return new TextDecoder().decode(bytes)\r\n // Node fallback\r\n return Buffer.from(bytes).toString('utf8')\r\n}\r\n\r\n// Varint (LEB128 7-bit groups) helpers\r\nexport function encodeVarint(n: number): Uint8Array {\r\n if (n < 0) throw new Error('Varint cannot encode negative numbers')\r\n const out: number[] = []\r\n do {\r\n let byte = n & 0x7f\r\n n >>>= 7\r\n if (n !== 0) byte |= 0x80\r\n out.push(byte)\r\n } while (n !== 0)\r\n return new Uint8Array(out)\r\n}\r\n\r\nexport function decodeVarint(bytes: Uint8Array, offset: number): { value: number; bytesRead: number } {\r\n let result = 0\r\n let shift = 0\r\n let pos = offset\r\n while (pos < bytes.length) {\r\n const byte = bytes[pos++]\r\n result |= (byte & 0x7f) << shift\r\n if ((byte & 0x80) === 0) {\r\n return { value: result, bytesRead: pos - offset }\r\n }\r\n shift += 7\r\n if (shift > 35) throw new Error('Varint too large')\r\n }\r\n throw new Error('Truncated varint')\r\n}\r\n","// File: src\\dictionary.ts\r\n\r\nimport type { WordBinDictionary } from \"./types\";\r\nimport { generateWordId } from \"./core/id.js\";\r\nimport { toHex } from \"./utils/buffer.js\";\r\n\r\nexport interface BuildDictionaryOptions {\r\n /**\r\n * Dictionary version number (used in header and for format compatibility)\r\n * @default 1\r\n */\r\n version?: number;\r\n\r\n /**\r\n * Human-readable description of this dictionary\r\n * @default \"WordBin dictionary v${version}\"\r\n */\r\n description?: string;\r\n\r\n /**\r\n * Optional: custom prefix or identifier for this dictionary build\r\n * (can be used in logs, filenames, etc.)\r\n */\r\n name?: string;\r\n}\r\n\r\nexport async function buildDictionary(\r\n words: string[],\r\n options: BuildDictionaryOptions = {},\r\n): Promise<WordBinDictionary> {\r\n const { version = 1, description = `WordBin dictionary v${version}` } =\r\n options;\r\n\r\n const map: Record<string, string[]> = {};\r\n\r\n const normalizedWords = words\r\n .map((w) => w.trim().toLowerCase())\r\n .filter((w) => w);\r\n\r\n await Promise.all(\r\n normalizedWords.map(async (word) => {\r\n const id = await generateWordId(word);\r\n const key = toHex(id);\r\n if (!map[key]) map[key] = [];\r\n map[key].push(word);\r\n }),\r\n );\r\n\r\n Object.values(map).forEach((collisions) => {\r\n collisions.sort((a, b) => a.localeCompare(b));\r\n });\r\n\r\n return {\r\n version,\r\n description,\r\n words: map,\r\n };\r\n}\r\n"],"names":[],"mappings":"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"}