@ddunigma/node 2.0.2 → 2.2.0

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.
Files changed (51) hide show
  1. package/LICENCE +1 -1
  2. package/README.md +342 -78
  3. package/dist/DduStream-KO75MEEM.js +2 -0
  4. package/dist/chunk-CZMUPXSK.js +5 -0
  5. package/dist/index.cjs +8 -0
  6. package/dist/index.d.cts +813 -0
  7. package/dist/index.d.ts +813 -0
  8. package/dist/index.js +5 -0
  9. package/package.json +34 -15
  10. package/dist/cjs/base/BaseDdu.d.ts +0 -53
  11. package/dist/cjs/base/BaseDdu.js +0 -29
  12. package/dist/cjs/base/index.d.ts +0 -1
  13. package/dist/cjs/base/index.js +0 -5
  14. package/dist/cjs/charSets/index.d.ts +0 -18
  15. package/dist/cjs/charSets/index.js +0 -4385
  16. package/dist/cjs/encoders/Ddu64.d.ts +0 -185
  17. package/dist/cjs/encoders/Ddu64.js +0 -941
  18. package/dist/cjs/encoders/index.d.ts +0 -1
  19. package/dist/cjs/encoders/index.js +0 -5
  20. package/dist/cjs/index.d.ts +0 -4
  21. package/dist/cjs/index.js +0 -9
  22. package/dist/cjs/package.json +0 -3
  23. package/dist/cjs/types/DduDefaultTypes.d.ts +0 -3
  24. package/dist/cjs/types/DduDefaultTypes.js +0 -17
  25. package/dist/cjs/types/DduEnums.d.ts +0 -6
  26. package/dist/cjs/types/DduEnums.js +0 -10
  27. package/dist/cjs/types/DduInterface.d.ts +0 -39
  28. package/dist/cjs/types/DduInterface.js +0 -2
  29. package/dist/cjs/types/index.d.ts +0 -3
  30. package/dist/cjs/types/index.js +0 -19
  31. package/dist/mjs/base/BaseDdu.d.ts +0 -53
  32. package/dist/mjs/base/BaseDdu.js +0 -23
  33. package/dist/mjs/base/index.d.ts +0 -1
  34. package/dist/mjs/base/index.js +0 -1
  35. package/dist/mjs/charSets/index.d.ts +0 -18
  36. package/dist/mjs/charSets/index.js +0 -4380
  37. package/dist/mjs/encoders/Ddu64.d.ts +0 -185
  38. package/dist/mjs/encoders/Ddu64.js +0 -964
  39. package/dist/mjs/encoders/index.d.ts +0 -1
  40. package/dist/mjs/encoders/index.js +0 -1
  41. package/dist/mjs/index.d.ts +0 -4
  42. package/dist/mjs/index.js +0 -3
  43. package/dist/mjs/package.json +0 -3
  44. package/dist/mjs/types/DduDefaultTypes.d.ts +0 -3
  45. package/dist/mjs/types/DduDefaultTypes.js +0 -14
  46. package/dist/mjs/types/DduEnums.d.ts +0 -6
  47. package/dist/mjs/types/DduEnums.js +0 -7
  48. package/dist/mjs/types/DduInterface.d.ts +0 -39
  49. package/dist/mjs/types/DduInterface.js +0 -1
  50. package/dist/mjs/types/index.d.ts +0 -3
  51. package/dist/mjs/types/index.js +0 -3
@@ -1,964 +0,0 @@
1
- import { deflateSync, inflateSync } from "zlib";
2
- import { BaseDdu } from "../base/BaseDdu.js";
3
- import { DduSetSymbol, dduDefaultConstructorOptions, } from "../types/index.js";
4
- import { getCharSet } from "../charSets/index.js";
5
- // ============================================================================
6
- // 상수 정의
7
- // ============================================================================
8
- /** 바이트당 비트 수 */
9
- const BYTE_BITS = 8;
10
- /** 일반 정수 연산이 가능한 최대 비트 길이 (초과 시 BigInt 사용) */
11
- const MAX_FAST_BITS = 16;
12
- /** 바이트 마스크 (0xFF) */
13
- const BYTE_MASK = 0xff;
14
- /** 압축 데이터 식별 마커 */
15
- const COMPRESS_MARKER = "ELYSIA";
16
- /** 기본 최대 디코딩 바이트 수 (64MB) */
17
- const DEFAULT_MAX_DECODED_BYTES = 64 * 1024 * 1024;
18
- /** 기본 최대 압축해제 바이트 수 (64MB) */
19
- const DEFAULT_MAX_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
20
- // ============================================================================
21
- // Ddu64 클래스
22
- // ============================================================================
23
- /**
24
- * 커스텀 charset을 사용하는 Base64 스타일 인코더
25
- *
26
- * @description
27
- * 바이너리 데이터를 지정된 charset으로 인코딩/디코딩합니다.
28
- * 2의 제곱수 charset과 가변 길이 charset 모두 지원하며,
29
- * 압축 옵션을 통해 데이터 크기를 줄일 수 있습니다.
30
- *
31
- * @example
32
- * // 기본 사용
33
- * const encoder = new Ddu64("우따야", "뭐");
34
- * const encoded = encoder.encode("Hello");
35
- * const decoded = encoder.decode(encoded);
36
- *
37
- * @example
38
- * // 압축 사용
39
- * const encoder = new Ddu64(undefined, undefined, { compress: true });
40
- * const encoded = encoder.encode(longText);
41
- */
42
- export class Ddu64 extends BaseDdu {
43
- // --------------------------------------------------------------------------
44
- // 멤버 변수
45
- // --------------------------------------------------------------------------
46
- /** 인코딩에 사용할 문자 배열 */
47
- dduChar;
48
- /** 패딩 문자 */
49
- paddingChar;
50
- /** 각 charset 문자의 길이 */
51
- charLength;
52
- /** 비트 길이 (log2) */
53
- bitLength;
54
- /** 2의 제곱수 charset 여부 */
55
- usePowerOfTwo;
56
- /** 문자열 인코딩 방식 */
57
- encoding;
58
- /** 기본 압축 사용 여부 */
59
- defaultCompress;
60
- /** 기본 최대 디코딩 바이트 수 */
61
- defaultMaxDecodedBytes;
62
- /** 기본 최대 압축해제 바이트 수 */
63
- defaultMaxDecompressedBytes;
64
- /** 문자 → 인덱스 역방향 룩업 맵 */
65
- dduBinaryLookup = new Map();
66
- /** 미리 정의된 charset 사용 여부 */
67
- isPredefinedCharSet;
68
- /** 실제 사용되는 비트 길이 */
69
- effectiveBitLength;
70
- /** 최대 바이너리 값 */
71
- maxBinaryValue;
72
- /** ASCII 문자 빠른 룩업 테이블 */
73
- fastAsciiLookup = null;
74
- /** ASCII 룩업 사용 여부 */
75
- useAsciiLookup = false;
76
- // --------------------------------------------------------------------------
77
- // 생성자
78
- // --------------------------------------------------------------------------
79
- /**
80
- * Ddu64 인코더 인스턴스를 생성합니다.
81
- *
82
- * @param dduChar - charset 문자열 또는 배열 (미지정 시 옵션의 dduSetSymbol 사용)
83
- * @param paddingChar - 패딩 문자 (dduChar 지정 시 필수)
84
- * @param dduOptions - 생성자 옵션
85
- *
86
- * @throws dduChar 지정 시 paddingChar가 없으면 에러
87
- * @throws charset 문자 수가 부족하면 에러
88
- *
89
- * @example
90
- * // 커스텀 charset
91
- * new Ddu64("우따야", "뭐");
92
- *
93
- * @example
94
- * // 미리 정의된 charset
95
- * new Ddu64(undefined, undefined, { dduSetSymbol: DduSetSymbol.ONECHARSET });
96
- */
97
- constructor(dduChar, paddingChar, dduOptions) {
98
- super();
99
- const shouldThrow = dduOptions?.useBuildErrorReturn ?? false;
100
- // charset 초기화
101
- const initial = this.resolveInitialCharSet(dduChar, paddingChar, dduOptions, shouldThrow);
102
- const normalized = this.normalizeCharSet(initial, shouldThrow, dduOptions);
103
- this.dduChar = normalized.charSet;
104
- this.paddingChar = normalized.padding;
105
- this.charLength = normalized.charLength;
106
- this.isPredefinedCharSet = normalized.isPredefined;
107
- this.encoding = dduOptions?.encoding ?? this.defaultEncoding;
108
- this.defaultCompress = dduOptions?.compress ?? false;
109
- // 제한값 설정
110
- this.defaultMaxDecodedBytes = this.normalizeLimit(dduOptions?.maxDecodedBytes, DEFAULT_MAX_DECODED_BYTES, shouldThrow, "maxDecodedBytes");
111
- this.defaultMaxDecompressedBytes = this.normalizeLimit(dduOptions?.maxDecompressedBytes, DEFAULT_MAX_DECOMPRESSED_BYTES, shouldThrow, "maxDecompressedBytes");
112
- // 비트 길이 계산
113
- const dduLength = this.dduChar.length;
114
- this.usePowerOfTwo =
115
- dduLength > 0 && (dduLength & (dduLength - 1)) === 0;
116
- const computedBitLength = this.getBitLength(dduLength);
117
- this.bitLength = this.usePowerOfTwo
118
- ? this.getLargestPowerOfTwoExponent(dduLength)
119
- : computedBitLength;
120
- this.effectiveBitLength = this.usePowerOfTwo
121
- ? this.bitLength
122
- : computedBitLength;
123
- this.maxBinaryValue =
124
- this.effectiveBitLength < 31
125
- ? 1 << this.effectiveBitLength
126
- : Math.pow(2, this.effectiveBitLength);
127
- // ASCII 룩업 테이블 초기화 (성능 최적화)
128
- if (this.charLength === 1) {
129
- let allAscii = true;
130
- for (let i = 0; i < dduLength; i++) {
131
- if (this.dduChar[i].charCodeAt(0) >= 128) {
132
- allAscii = false;
133
- break;
134
- }
135
- }
136
- if (allAscii) {
137
- this.fastAsciiLookup = new Int16Array(128).fill(-1);
138
- for (let i = 0; i < dduLength; i++) {
139
- this.fastAsciiLookup[this.dduChar[i].charCodeAt(0)] = i;
140
- }
141
- this.useAsciiLookup = true;
142
- }
143
- }
144
- // 역방향 룩업 맵 생성
145
- for (let i = 0; i < dduLength; i++) {
146
- this.dduBinaryLookup.set(this.dduChar[i], i);
147
- }
148
- // 커스텀 charset 중복 조합 검증
149
- if (this.charLength === 1 && !this.isPredefinedCharSet) {
150
- this.validateCombinationDuplicates(this.dduChar, this.paddingChar, dduLength);
151
- }
152
- }
153
- // --------------------------------------------------------------------------
154
- // 공개 메서드
155
- // --------------------------------------------------------------------------
156
- /**
157
- * 입력 데이터를 인코딩합니다.
158
- *
159
- * @param input - 인코딩할 문자열 또는 Buffer
160
- * @param options - 인코딩 옵션
161
- * @param options.compress - 압축 사용 여부 (기본값: 생성자 설정)
162
- * @returns 인코딩된 문자열
163
- *
164
- * @example
165
- * encoder.encode("Hello World!");
166
- * encoder.encode(buffer, { compress: true });
167
- */
168
- encode(input, options) {
169
- const shouldCompress = options?.compress ?? this.defaultCompress;
170
- const originalBuffer = typeof input === "string" ? Buffer.from(input, this.encoding) : input;
171
- // 비압축 인코딩
172
- if (!shouldCompress) {
173
- return this.effectiveBitLength <= MAX_FAST_BITS
174
- ? this.encodeFast(originalBuffer, false)
175
- : this.encodeBigInt(originalBuffer, false);
176
- }
177
- // 압축 시도
178
- const compressedBuffer = deflateSync(originalBuffer, { level: 9 });
179
- // 압축 효과가 없으면 원본 사용
180
- if (compressedBuffer.length >= originalBuffer.length) {
181
- return this.effectiveBitLength <= MAX_FAST_BITS
182
- ? this.encodeFast(originalBuffer, false)
183
- : this.encodeBigInt(originalBuffer, false);
184
- }
185
- return this.effectiveBitLength <= MAX_FAST_BITS
186
- ? this.encodeFast(compressedBuffer, true)
187
- : this.encodeBigInt(compressedBuffer, true);
188
- }
189
- /**
190
- * 인코딩된 문자열을 Buffer로 디코딩합니다.
191
- *
192
- * @param input - 디코딩할 인코딩된 문자열
193
- * @param options - 디코딩 옵션
194
- * @param options.maxDecodedBytes - 최대 디코딩 바이트 수
195
- * @param options.maxDecompressedBytes - 최대 압축해제 바이트 수
196
- * @returns 디코딩된 Buffer
197
- *
198
- * @throws 잘못된 문자가 포함된 경우
199
- * @throws 패딩 형식이 잘못된 경우
200
- * @throws 크기 제한 초과 시
201
- */
202
- decodeToBuffer(input, options) {
203
- const { cleanedInput, paddingBits, isCompressed } = this.parseFooter(input);
204
- this.assertEncodedInputAligned(cleanedInput);
205
- // 디코딩 크기 검증
206
- const maxDecodedBytes = this.normalizeLimit(options?.maxDecodedBytes, this.defaultMaxDecodedBytes, true, "maxDecodedBytes");
207
- const estimatedDecodedBytes = this.estimateDecodedBytes(cleanedInput.length, paddingBits);
208
- if (estimatedDecodedBytes > maxDecodedBytes) {
209
- throw new Error(`[Ddu64 decode] Decoded output exceeds limit. Estimated: ${estimatedDecodedBytes} bytes, Limit: ${maxDecodedBytes} bytes`);
210
- }
211
- // 디코딩 수행
212
- const decoded = this.effectiveBitLength <= MAX_FAST_BITS
213
- ? this.decodeFast(cleanedInput, paddingBits)
214
- : this.decodeBigInt(cleanedInput, paddingBits);
215
- if (!isCompressed)
216
- return decoded;
217
- // 압축 해제
218
- const maxDecompressedBytes = this.normalizeLimit(options?.maxDecompressedBytes, this.defaultMaxDecompressedBytes, true, "maxDecompressedBytes");
219
- return this.inflateWithLimit(decoded, maxDecompressedBytes);
220
- }
221
- /**
222
- * 인코딩된 문자열을 원본 문자열로 디코딩합니다.
223
- *
224
- * @param input - 디코딩할 인코딩된 문자열
225
- * @param options - 디코딩 옵션
226
- * @returns 디코딩된 문자열
227
- *
228
- * @throws 잘못된 문자나 패딩 형식일 경우 에러
229
- */
230
- decode(input, options) {
231
- return this.decodeToBuffer(input, options).toString(this.encoding);
232
- }
233
- /**
234
- * 현재 인코더의 charset 정보를 반환합니다.
235
- *
236
- * @returns charset 설정 정보 객체
237
- */
238
- getCharSetInfo() {
239
- return {
240
- charSet: [...this.dduChar],
241
- paddingChar: this.paddingChar,
242
- charLength: this.charLength,
243
- bitLength: this.bitLength,
244
- usePowerOfTwo: this.usePowerOfTwo,
245
- encoding: this.encoding,
246
- defaultCompress: this.defaultCompress,
247
- defaultMaxDecodedBytes: this.defaultMaxDecodedBytes,
248
- defaultMaxDecompressedBytes: this.defaultMaxDecompressedBytes,
249
- };
250
- }
251
- // --------------------------------------------------------------------------
252
- // 유틸리티 메서드
253
- // --------------------------------------------------------------------------
254
- /**
255
- * 옵션 값을 정규화합니다.
256
- */
257
- normalizeLimit(value, fallback, shouldThrow, name) {
258
- if (value === undefined)
259
- return fallback;
260
- if (value === Number.POSITIVE_INFINITY)
261
- return Number.POSITIVE_INFINITY;
262
- if (!Number.isFinite(value) || value <= 0) {
263
- if (shouldThrow) {
264
- throw new Error(`[Ddu64 options] Invalid ${name}. Must be a positive finite number or Infinity.`);
265
- }
266
- return fallback;
267
- }
268
- return Math.floor(value);
269
- }
270
- /**
271
- * 디코딩 결과 바이트 수를 추정합니다.
272
- */
273
- estimateDecodedBytes(cleanedInputLen, paddingBits) {
274
- if (cleanedInputLen === 0)
275
- return 0;
276
- if (paddingBits < 0 || paddingBits >= this.effectiveBitLength) {
277
- throw new Error(`[Ddu64 decode] Invalid padding bits: ${paddingBits}`);
278
- }
279
- const chunkSize = this.usePowerOfTwo ? this.charLength : this.charLength * 2;
280
- const numChunks = Math.ceil(cleanedInputLen / chunkSize);
281
- const bits = numChunks * this.effectiveBitLength - paddingBits;
282
- if (bits < 0)
283
- throw new Error(`[Ddu64 decode] Invalid decoded bit length`);
284
- return Math.ceil(bits / BYTE_BITS);
285
- }
286
- /**
287
- * 인코딩된 입력의 정렬을 검증합니다.
288
- */
289
- assertEncodedInputAligned(cleanedInput) {
290
- const { charLength } = this;
291
- if (charLength <= 0)
292
- return;
293
- if (cleanedInput.length % charLength !== 0) {
294
- throw new Error(`[Ddu64 decode] Invalid encoded length. Expected multiple of ${charLength}, got ${cleanedInput.length}`);
295
- }
296
- if (!this.usePowerOfTwo) {
297
- const chunkSize = charLength * 2;
298
- if (cleanedInput.length % chunkSize !== 0) {
299
- throw new Error(`[Ddu64 decode] Invalid encoded length for variable charset. Expected multiple of ${chunkSize}, got ${cleanedInput.length}`);
300
- }
301
- }
302
- }
303
- /**
304
- * 크기 제한을 적용하여 압축을 해제합니다.
305
- */
306
- inflateWithLimit(data, maxBytes) {
307
- if (maxBytes === Number.POSITIVE_INFINITY)
308
- return inflateSync(data);
309
- try {
310
- return inflateSync(data, { maxOutputLength: maxBytes });
311
- }
312
- catch (e) {
313
- const msg = String(e?.message ?? "");
314
- const code = String(e?.code ?? "");
315
- // maxOutputLength 미지원 시 fallback
316
- if (msg.toLowerCase().includes("maxoutputlength") ||
317
- msg.toLowerCase().includes("unknown option") ||
318
- code === "ERR_INVALID_ARG_VALUE") {
319
- const inflated = inflateSync(data);
320
- if (inflated.length > maxBytes) {
321
- throw new Error(`[Ddu64 decode] Decompressed data exceeds limit. Size: ${inflated.length} bytes, Limit: ${maxBytes} bytes`);
322
- }
323
- return inflated;
324
- }
325
- // 출력 제한 초과
326
- if (code === "ERR_BUFFER_TOO_LARGE" ||
327
- msg.toLowerCase().includes("output length") ||
328
- msg.toLowerCase().includes("buffer too large")) {
329
- throw new Error(`[Ddu64 decode] Decompressed data exceeds limit. Limit: ${maxBytes} bytes`);
330
- }
331
- throw e;
332
- }
333
- }
334
- // --------------------------------------------------------------------------
335
- // 푸터 파싱
336
- // --------------------------------------------------------------------------
337
- /**
338
- * 인코딩된 문자열의 푸터(패딩 정보)를 파싱합니다.
339
- */
340
- parseFooter(input) {
341
- const inputLen = input.length;
342
- const pad = this.paddingChar;
343
- const padLen = pad.length;
344
- if (inputLen < padLen)
345
- return { cleanedInput: input, paddingBits: 0, isCompressed: false };
346
- const markerLen = COMPRESS_MARKER.length;
347
- const maxPaddingBits = Math.max(0, this.effectiveBitLength - 1);
348
- const maxDigits = maxPaddingBits.toString().length;
349
- // 인덱스 기반 문자열 비교
350
- const matchesAt = (str, pattern, start) => {
351
- const pLen = pattern.length;
352
- if (start < 0 || start + pLen > str.length)
353
- return false;
354
- for (let i = 0; i < pLen; i++) {
355
- if (str.charCodeAt(start + i) !== pattern.charCodeAt(i))
356
- return false;
357
- }
358
- return true;
359
- };
360
- const isDigitCode = (code) => code >= 48 && code <= 57;
361
- // 푸터 패턴 탐색
362
- for (let digitCount = Math.min(maxDigits, inputLen); digitCount >= 1; digitCount--) {
363
- const digitsStart = inputLen - digitCount;
364
- const firstCode = input.charCodeAt(digitsStart);
365
- if (!isDigitCode(firstCode))
366
- continue;
367
- let allDigits = true;
368
- for (let i = digitsStart + 1; i < inputLen; i++) {
369
- if (!isDigitCode(input.charCodeAt(i))) {
370
- allDigits = false;
371
- break;
372
- }
373
- }
374
- if (!allDigits)
375
- continue;
376
- const digitSuffix = input.substring(digitsStart);
377
- const paddingBits = parseInt(digitSuffix, 10);
378
- if (Number.isNaN(paddingBits) ||
379
- paddingBits < 0 ||
380
- paddingBits >= this.effectiveBitLength ||
381
- digitSuffix !== paddingBits.toString())
382
- continue;
383
- // 압축 푸터: pad + marker + digits
384
- if (digitsStart >= padLen + markerLen) {
385
- const markerStart = digitsStart - markerLen;
386
- const padStart = markerStart - padLen;
387
- if (matchesAt(input, COMPRESS_MARKER, markerStart) &&
388
- matchesAt(input, pad, padStart)) {
389
- if (padStart % this.charLength !== 0) {
390
- throw new Error(`[Ddu64 decode] Invalid padding format. Misaligned padding marker`);
391
- }
392
- return {
393
- cleanedInput: input.substring(0, padStart),
394
- paddingBits,
395
- isCompressed: true,
396
- };
397
- }
398
- }
399
- // 일반 푸터: pad + digits
400
- const padStart = digitsStart - padLen;
401
- if (padStart >= 0 && matchesAt(input, pad, padStart)) {
402
- if (padStart % this.charLength !== 0) {
403
- throw new Error(`[Ddu64 decode] Invalid padding format. Misaligned padding marker`);
404
- }
405
- return {
406
- cleanedInput: input.substring(0, padStart),
407
- paddingBits,
408
- isCompressed: false,
409
- };
410
- }
411
- }
412
- // Fallback: lastIndexOf 기반
413
- const lastPadIdx = input.lastIndexOf(pad);
414
- if (lastPadIdx >= 0 && lastPadIdx % this.charLength === 0) {
415
- const tailStart = lastPadIdx + padLen;
416
- if (tailStart >= inputLen) {
417
- throw new Error(`[Ddu64 decode] Invalid padding format. Missing padding length`);
418
- }
419
- let tail = input.substring(tailStart);
420
- const hasMarker = tail.length >= markerLen && matchesAt(tail, COMPRESS_MARKER, 0);
421
- if (hasMarker) {
422
- tail = tail.substring(markerLen);
423
- if (!tail) {
424
- throw new Error(`[Ddu64 decode] Invalid padding format. Missing padding length`);
425
- }
426
- }
427
- const paddingBits = parseInt(tail, 10);
428
- if (Number.isNaN(paddingBits) ||
429
- tail !== paddingBits.toString() ||
430
- paddingBits < 0 ||
431
- paddingBits >= this.effectiveBitLength) {
432
- throw new Error(`[Ddu64 decode] Invalid padding format. Got: "${tail}"`);
433
- }
434
- const isCompressed = input
435
- .slice(lastPadIdx + padLen)
436
- .startsWith(COMPRESS_MARKER);
437
- return {
438
- cleanedInput: input.substring(0, lastPadIdx),
439
- paddingBits,
440
- isCompressed,
441
- };
442
- }
443
- return { cleanedInput: input, paddingBits: 0, isCompressed: false };
444
- }
445
- // --------------------------------------------------------------------------
446
- // 인코딩 (Fast 모드 - 16비트 이하)
447
- // --------------------------------------------------------------------------
448
- /**
449
- * 일반 정수 연산을 사용한 빠른 인코딩
450
- */
451
- encodeFast(bufferInput, compress) {
452
- const inputLen = bufferInput.length;
453
- if (inputLen === 0)
454
- return "";
455
- const { dduChar, effectiveBitLength: bitLength, paddingChar } = this;
456
- const dduLength = dduChar.length;
457
- const totalBits = inputLen * BYTE_BITS;
458
- const estimatedChunks = Math.ceil(totalBits / bitLength);
459
- const estimatedSymbols = this.usePowerOfTwo
460
- ? estimatedChunks
461
- : estimatedChunks * 2;
462
- const resultParts = new Array(estimatedSymbols + 3);
463
- let resultIdx = 0;
464
- let accumulator = 0;
465
- let accumulatorBits = 0;
466
- if (this.usePowerOfTwo) {
467
- const mask = (1 << bitLength) - 1;
468
- for (let i = 0; i < inputLen; i++) {
469
- accumulator = (accumulator << BYTE_BITS) | bufferInput[i];
470
- accumulatorBits += BYTE_BITS;
471
- while (accumulatorBits >= bitLength) {
472
- accumulatorBits -= bitLength;
473
- resultParts[resultIdx++] =
474
- dduChar[(accumulator >> accumulatorBits) & mask];
475
- accumulator &= (1 << accumulatorBits) - 1;
476
- }
477
- }
478
- }
479
- else {
480
- for (let i = 0; i < inputLen; i++) {
481
- accumulator = (accumulator << BYTE_BITS) | bufferInput[i];
482
- accumulatorBits += BYTE_BITS;
483
- while (accumulatorBits >= bitLength) {
484
- accumulatorBits -= bitLength;
485
- const index = accumulator >> accumulatorBits;
486
- const div = (index / dduLength) | 0;
487
- resultParts[resultIdx++] = dduChar[div];
488
- resultParts[resultIdx++] = dduChar[index - div * dduLength];
489
- accumulator &= (1 << accumulatorBits) - 1;
490
- }
491
- }
492
- }
493
- // 남은 비트 처리 (패딩)
494
- if (accumulatorBits > 0) {
495
- const paddingBits = bitLength - accumulatorBits;
496
- const index = accumulator << paddingBits;
497
- if (this.usePowerOfTwo) {
498
- resultParts[resultIdx++] = dduChar[index];
499
- }
500
- else {
501
- const div = (index / dduLength) | 0;
502
- resultParts[resultIdx++] = dduChar[div];
503
- resultParts[resultIdx++] = dduChar[index - div * dduLength];
504
- }
505
- resultParts[resultIdx++] = paddingChar;
506
- resultParts[resultIdx++] = compress
507
- ? COMPRESS_MARKER + paddingBits.toString()
508
- : paddingBits.toString();
509
- }
510
- else if (compress) {
511
- resultParts[resultIdx++] = paddingChar;
512
- resultParts[resultIdx++] = COMPRESS_MARKER + "0";
513
- }
514
- resultParts.length = resultIdx;
515
- return resultParts.join("");
516
- }
517
- // --------------------------------------------------------------------------
518
- // 디코딩 (Fast 모드 - 16비트 이하)
519
- // --------------------------------------------------------------------------
520
- /**
521
- * 일반 정수 연산을 사용한 빠른 디코딩
522
- */
523
- decodeFast(cleanedInput, paddingBits) {
524
- const inputLen = cleanedInput.length;
525
- if (inputLen === 0)
526
- return Buffer.alloc(0);
527
- const { effectiveBitLength: bitLength, charLength } = this;
528
- const dduLength = this.dduChar.length;
529
- const chunkSize = this.usePowerOfTwo ? charLength : charLength * 2;
530
- const numChunks = Math.ceil(inputLen / chunkSize);
531
- const estimatedBytes = Math.ceil((numChunks * bitLength - paddingBits) / BYTE_BITS);
532
- const buffer = new Uint8Array(estimatedBytes + 1);
533
- let bufIdx = 0;
534
- let accumulator = 0;
535
- let accumulatorBits = 0;
536
- if (this.usePowerOfTwo) {
537
- if (this.useAsciiLookup && this.fastAsciiLookup) {
538
- // ASCII 최적화 경로
539
- const lookup = this.fastAsciiLookup;
540
- for (let i = 0; i < inputLen; i += chunkSize) {
541
- const code = cleanedInput.charCodeAt(i);
542
- const val = code < 128 ? lookup[code] : -1;
543
- if (val < 0) {
544
- throw new Error(`[Ddu64 decode] Invalid character "${cleanedInput[i]}" at ${i}`);
545
- }
546
- accumulator = (accumulator << bitLength) | val;
547
- accumulatorBits += bitLength;
548
- if (i + chunkSize >= inputLen && paddingBits > 0) {
549
- accumulator >>= paddingBits;
550
- accumulatorBits -= paddingBits;
551
- }
552
- while (accumulatorBits >= BYTE_BITS) {
553
- accumulatorBits -= BYTE_BITS;
554
- buffer[bufIdx++] = (accumulator >> accumulatorBits) & BYTE_MASK;
555
- accumulator &= (1 << accumulatorBits) - 1;
556
- }
557
- }
558
- }
559
- else {
560
- // 일반 룩업 경로
561
- const lookup = this.dduBinaryLookup;
562
- for (let i = 0; i < inputLen; i += chunkSize) {
563
- const chunk = cleanedInput.slice(i, i + charLength);
564
- const val = lookup.get(chunk);
565
- if (val === undefined) {
566
- throw new Error(`[Ddu64 decode] Invalid character "${chunk}" at ${i}`);
567
- }
568
- accumulator = (accumulator << bitLength) | val;
569
- accumulatorBits += bitLength;
570
- if (i + chunkSize >= inputLen && paddingBits > 0) {
571
- accumulator >>= paddingBits;
572
- accumulatorBits -= paddingBits;
573
- }
574
- while (accumulatorBits >= BYTE_BITS) {
575
- accumulatorBits -= BYTE_BITS;
576
- buffer[bufIdx++] = (accumulator >> accumulatorBits) & BYTE_MASK;
577
- accumulator &= (1 << accumulatorBits) - 1;
578
- }
579
- }
580
- }
581
- }
582
- else {
583
- // 가변 길이 charset
584
- const lookup = this.dduBinaryLookup;
585
- for (let i = 0; i < inputLen; i += chunkSize) {
586
- const c1 = cleanedInput.slice(i, i + charLength);
587
- const c2 = cleanedInput.slice(i + charLength, i + chunkSize);
588
- const v1 = lookup.get(c1);
589
- const v2 = lookup.get(c2);
590
- if (v1 === undefined) {
591
- throw new Error(`[Ddu64 decode] Invalid character "${c1}" at ${i}`);
592
- }
593
- if (v2 === undefined) {
594
- throw new Error(`[Ddu64 decode] Invalid character "${c2}" at ${i + charLength}`);
595
- }
596
- const value = v1 * dduLength + v2;
597
- if (value >= this.maxBinaryValue) {
598
- throw new Error(`[Ddu64 decode] Value ${value} exceeds range`);
599
- }
600
- accumulator = (accumulator << bitLength) | value;
601
- accumulatorBits += bitLength;
602
- if (i + chunkSize >= inputLen && paddingBits > 0) {
603
- accumulator >>= paddingBits;
604
- accumulatorBits -= paddingBits;
605
- }
606
- while (accumulatorBits >= BYTE_BITS) {
607
- accumulatorBits -= BYTE_BITS;
608
- buffer[bufIdx++] = (accumulator >> accumulatorBits) & BYTE_MASK;
609
- accumulator &= (1 << accumulatorBits) - 1;
610
- }
611
- }
612
- }
613
- return Buffer.from(buffer.subarray(0, bufIdx));
614
- }
615
- // --------------------------------------------------------------------------
616
- // 인코딩 (BigInt 모드 - 17비트 이상)
617
- // --------------------------------------------------------------------------
618
- /**
619
- * BigInt를 사용한 대형 비트 인코딩
620
- */
621
- encodeBigInt(bufferInput, compress) {
622
- const inputLen = bufferInput.length;
623
- if (inputLen === 0)
624
- return "";
625
- const { dduChar, effectiveBitLength: bitLength } = this;
626
- const dduLength = dduChar.length;
627
- const estimatedChunks = Math.ceil((inputLen * BYTE_BITS) / bitLength);
628
- const estimatedSymbols = this.usePowerOfTwo
629
- ? estimatedChunks
630
- : estimatedChunks * 2;
631
- const resultParts = new Array(estimatedSymbols + 3);
632
- let resultIdx = 0;
633
- let accumulator = 0n;
634
- let accumulatorBits = 0;
635
- if (this.usePowerOfTwo) {
636
- for (let i = 0; i < inputLen; i++) {
637
- accumulator = (accumulator << 8n) | BigInt(bufferInput[i]);
638
- accumulatorBits += BYTE_BITS;
639
- while (accumulatorBits >= bitLength) {
640
- const shift = accumulatorBits - bitLength;
641
- resultParts[resultIdx++] =
642
- dduChar[Number(accumulator >> BigInt(shift))];
643
- accumulator &= (1n << BigInt(shift)) - 1n;
644
- accumulatorBits -= bitLength;
645
- }
646
- }
647
- }
648
- else {
649
- for (let i = 0; i < inputLen; i++) {
650
- accumulator = (accumulator << 8n) | BigInt(bufferInput[i]);
651
- accumulatorBits += BYTE_BITS;
652
- while (accumulatorBits >= bitLength) {
653
- const shift = accumulatorBits - bitLength;
654
- const idx = Number(accumulator >> BigInt(shift));
655
- const div = Math.floor(idx / dduLength);
656
- resultParts[resultIdx++] = dduChar[div];
657
- resultParts[resultIdx++] = dduChar[idx - div * dduLength];
658
- accumulator &= (1n << BigInt(shift)) - 1n;
659
- accumulatorBits -= bitLength;
660
- }
661
- }
662
- }
663
- // 남은 비트 처리 (패딩)
664
- if (accumulatorBits > 0) {
665
- const paddingBits = bitLength - accumulatorBits;
666
- const index = Number(accumulator << BigInt(paddingBits));
667
- if (this.usePowerOfTwo) {
668
- resultParts[resultIdx++] = dduChar[index];
669
- }
670
- else {
671
- const div = Math.floor(index / dduLength);
672
- resultParts[resultIdx++] = dduChar[div];
673
- resultParts[resultIdx++] = dduChar[index - div * dduLength];
674
- }
675
- resultParts[resultIdx++] = this.paddingChar;
676
- resultParts[resultIdx++] = compress
677
- ? COMPRESS_MARKER + paddingBits.toString()
678
- : paddingBits.toString();
679
- }
680
- else if (compress) {
681
- resultParts[resultIdx++] = this.paddingChar;
682
- resultParts[resultIdx++] = COMPRESS_MARKER + "0";
683
- }
684
- resultParts.length = resultIdx;
685
- return resultParts.join("");
686
- }
687
- // --------------------------------------------------------------------------
688
- // 디코딩 (BigInt 모드 - 17비트 이상)
689
- // --------------------------------------------------------------------------
690
- /**
691
- * BigInt를 사용한 대형 비트 디코딩
692
- */
693
- decodeBigInt(cleanedInput, paddingBits) {
694
- const inputLen = cleanedInput.length;
695
- if (inputLen === 0)
696
- return Buffer.alloc(0);
697
- const { effectiveBitLength: bitLength, dduBinaryLookup: lookup, charLength, } = this;
698
- const bigBitLength = BigInt(bitLength);
699
- const dduLength = this.dduChar.length;
700
- const chunkSize = this.usePowerOfTwo ? charLength : charLength * 2;
701
- const numChunks = Math.ceil(inputLen / chunkSize);
702
- const estimatedBytes = Math.ceil((numChunks * bitLength - paddingBits) / BYTE_BITS);
703
- const buffer = new Uint8Array(estimatedBytes + 1);
704
- let bufIdx = 0;
705
- let accumulator = 0n;
706
- let accumulatorBits = 0;
707
- if (this.usePowerOfTwo) {
708
- for (let i = 0; i < inputLen; i += chunkSize) {
709
- const chunk = cleanedInput.slice(i, i + charLength);
710
- const val = lookup.get(chunk);
711
- if (val === undefined) {
712
- throw new Error(`[Ddu64 decode] Invalid character "${chunk}" at ${i}`);
713
- }
714
- accumulator = (accumulator << bigBitLength) | BigInt(val);
715
- accumulatorBits += bitLength;
716
- if (i + chunkSize >= inputLen && paddingBits > 0) {
717
- accumulator >>= BigInt(paddingBits);
718
- accumulatorBits -= paddingBits;
719
- }
720
- while (accumulatorBits >= BYTE_BITS) {
721
- const shift = accumulatorBits - BYTE_BITS;
722
- buffer[bufIdx++] = Number((accumulator >> BigInt(shift)) & 0xffn);
723
- accumulator &= (1n << BigInt(shift)) - 1n;
724
- accumulatorBits -= BYTE_BITS;
725
- }
726
- }
727
- }
728
- else {
729
- for (let i = 0; i < inputLen; i += chunkSize) {
730
- const c1 = cleanedInput.slice(i, i + charLength);
731
- const c2 = cleanedInput.slice(i + charLength, i + chunkSize);
732
- const v1 = lookup.get(c1);
733
- const v2 = lookup.get(c2);
734
- if (v1 === undefined) {
735
- throw new Error(`[Ddu64 decode] Invalid character "${c1}" at ${i}`);
736
- }
737
- if (v2 === undefined) {
738
- throw new Error(`[Ddu64 decode] Invalid character "${c2}" at ${i + charLength}`);
739
- }
740
- const value = v1 * dduLength + v2;
741
- if (value >= this.maxBinaryValue) {
742
- throw new Error(`[Ddu64 decode] Value ${value} exceeds range`);
743
- }
744
- accumulator = (accumulator << bigBitLength) | BigInt(value);
745
- accumulatorBits += bitLength;
746
- if (i + chunkSize >= inputLen && paddingBits > 0) {
747
- accumulator >>= BigInt(paddingBits);
748
- accumulatorBits -= paddingBits;
749
- }
750
- while (accumulatorBits >= BYTE_BITS) {
751
- const shift = accumulatorBits - BYTE_BITS;
752
- buffer[bufIdx++] = Number((accumulator >> BigInt(shift)) & 0xffn);
753
- accumulator &= (1n << BigInt(shift)) - 1n;
754
- accumulatorBits -= BYTE_BITS;
755
- }
756
- }
757
- }
758
- return Buffer.from(buffer.subarray(0, bufIdx));
759
- }
760
- // --------------------------------------------------------------------------
761
- // Charset 초기화 메서드
762
- // --------------------------------------------------------------------------
763
- /**
764
- * Charset을 정규화합니다.
765
- */
766
- normalizeCharSet(current, shouldThrow, dduOptions) {
767
- let state = { ...current };
768
- let retryCount = 0;
769
- const maxRetries = 3;
770
- while (retryCount < maxRetries) {
771
- try {
772
- // 중복 문자 제거
773
- const uniqueChars = Array.from(new Set(state.charSet));
774
- if (uniqueChars.length !== state.charSet.length) {
775
- if (shouldThrow) {
776
- const duplicates = state.charSet.filter((c, i) => state.charSet.indexOf(c) !== i);
777
- throw new Error(`[Ddu64 normalizeCharSet] Character set contains duplicate characters: [${[...new Set(duplicates)].join(", ")}]`);
778
- }
779
- state.charSet = uniqueChars;
780
- if (!state.isPredefined)
781
- state.requiredLength = state.charSet.length;
782
- }
783
- // 문자 수 검증
784
- if (state.charSet.length < state.requiredLength) {
785
- throw new Error(`[Ddu64 normalizeCharSet] Insufficient characters. Required: ${state.requiredLength}, Has: ${state.charSet.length}`);
786
- }
787
- if (state.requiredLength < 2) {
788
- throw new Error(`[Ddu64 normalizeCharSet] At least 2 unique characters required.`);
789
- }
790
- if (state.charSet.length === 0) {
791
- throw new Error(`[Ddu64 normalizeCharSet] Empty charset.`);
792
- }
793
- // 문자 길이 일관성 검증
794
- const charLength = state.charSet[0].length;
795
- const invalidChar = state.charSet.find((c) => c.length !== charLength);
796
- if (invalidChar) {
797
- if (shouldThrow) {
798
- throw new Error(`[Ddu64 normalizeCharSet] Inconsistent char length. Expected ${charLength}, found "${invalidChar}" (${invalidChar.length})`);
799
- }
800
- throw new Error("internal retry");
801
- }
802
- // 패딩 검증
803
- if (state.padding.length !== charLength) {
804
- throw new Error(`[Ddu64 normalizeCharSet] Padding length mismatch. Expected ${charLength}, got ${state.padding.length}`);
805
- }
806
- if (state.charSet.includes(state.padding)) {
807
- if (shouldThrow) {
808
- throw new Error(`[Ddu64 normalizeCharSet] Padding character "${state.padding}" conflicts with charset.`);
809
- }
810
- state.charSet = state.charSet.filter((c) => c !== state.padding);
811
- }
812
- // 불필요한 배열 복사 방지
813
- const finalSet = state.charSet.length === state.requiredLength
814
- ? state.charSet
815
- : state.charSet.slice(0, state.requiredLength);
816
- return {
817
- charSet: finalSet,
818
- padding: state.padding,
819
- charLength,
820
- isPredefined: state.isPredefined,
821
- };
822
- }
823
- catch (e) {
824
- if (shouldThrow && !e.message.includes("internal retry"))
825
- throw e;
826
- state = this.getFallbackCharSet(dduOptions);
827
- retryCount++;
828
- }
829
- }
830
- // 최종 fallback
831
- const fallback = this.getFallbackCharSet(dduOptions);
832
- return {
833
- charSet: fallback.charSet.slice(0, fallback.requiredLength),
834
- padding: fallback.padding,
835
- charLength: fallback.charSet[0]?.length ?? 1,
836
- isPredefined: true,
837
- };
838
- }
839
- /**
840
- * 초기 charset을 결정합니다.
841
- */
842
- resolveInitialCharSet(dduChar, paddingChar, dduOptions, shouldThrow) {
843
- const buildMeta = (set, padding, length, isPredefined) => {
844
- const usePow2 = this.shouldUsePowerOfTwo(length, dduOptions?.usePowerOfTwo);
845
- if (usePow2 && length > 0) {
846
- const exponent = this.getLargestPowerOfTwoExponent(length);
847
- const pow2Length = 1 << exponent;
848
- const selected = set.length === pow2Length ? set : set.slice(0, pow2Length);
849
- return {
850
- charSet: selected,
851
- padding,
852
- requiredLength: pow2Length,
853
- bitLength: exponent,
854
- isPredefined,
855
- };
856
- }
857
- const selected = set.length === length ? set : set.slice(0, length);
858
- return {
859
- charSet: selected,
860
- padding,
861
- requiredLength: length,
862
- bitLength: length > 0 ? this.getBitLength(length) : 0,
863
- isPredefined,
864
- };
865
- };
866
- try {
867
- const finalDduChar = dduChar ?? dduOptions?.dduChar;
868
- const finalPadding = paddingChar ?? dduOptions?.paddingChar;
869
- if (finalDduChar) {
870
- if (!finalPadding) {
871
- throw new Error(`[Ddu64 Constructor] paddingChar is required when dduChar is provided.`);
872
- }
873
- const arr = typeof finalDduChar === "string"
874
- ? [...finalDduChar.trim()]
875
- : [...finalDduChar];
876
- if (shouldThrow) {
877
- const uniqueSize = new Set(arr).size;
878
- if (uniqueSize !== arr.length) {
879
- const duplicates = arr.filter((c, i) => arr.indexOf(c) !== i);
880
- throw new Error(`[Ddu64 Constructor] Character set contains duplicate characters: [${[...new Set(duplicates)].join(", ")}]`);
881
- }
882
- }
883
- const reqLen = dduOptions?.requiredLength ?? arr.length;
884
- if (arr.length < reqLen) {
885
- throw new Error(`[Ddu64 Constructor] Insufficient characters.`);
886
- }
887
- return buildMeta(arr, finalPadding, reqLen, false);
888
- }
889
- const symbol = dduOptions?.dduSetSymbol ??
890
- dduDefaultConstructorOptions.dduSetSymbol ??
891
- DduSetSymbol.DDU;
892
- const cs = this.getCharSetOrThrow(symbol);
893
- return buildMeta(cs.charSet, cs.paddingChar, cs.maxRequiredLength, true);
894
- }
895
- catch (error) {
896
- if (shouldThrow)
897
- throw error;
898
- return this.getFallbackCharSet(dduOptions);
899
- }
900
- }
901
- /**
902
- * Fallback charset을 반환합니다.
903
- */
904
- getFallbackCharSet(dduOptions) {
905
- const symbol = dduOptions?.dduSetSymbol ??
906
- dduDefaultConstructorOptions.dduSetSymbol ??
907
- DduSetSymbol.ONECHARSET;
908
- const cs = getCharSet(symbol) ?? getCharSet(DduSetSymbol.ONECHARSET);
909
- if (!cs)
910
- throw new Error(`Critical: No fallback CharSet available`);
911
- return {
912
- charSet: cs.charSet,
913
- padding: cs.paddingChar,
914
- requiredLength: cs.maxRequiredLength,
915
- bitLength: cs.bitLength,
916
- isPredefined: true,
917
- };
918
- }
919
- /**
920
- * 2의 제곱수 사용 여부를 결정합니다.
921
- */
922
- shouldUsePowerOfTwo(length, preference) {
923
- if (preference !== undefined)
924
- return preference ? length > 0 : false;
925
- return length > 0 && (length & (length - 1)) === 0;
926
- }
927
- /**
928
- * charset을 가져오거나 에러를 발생시킵니다.
929
- */
930
- getCharSetOrThrow(symbol) {
931
- const cs = getCharSet(symbol);
932
- if (!cs)
933
- throw new Error(`CharSet with symbol ${symbol} not found`);
934
- return cs;
935
- }
936
- /**
937
- * 커스텀 charset의 조합 중복을 검증합니다.
938
- */
939
- validateCombinationDuplicates(charSet, paddingChar, requiredLength) {
940
- if (charSet[0].length !== 1 || requiredLength > 256)
941
- return;
942
- const limit = Math.min(charSet.length, requiredLength);
943
- const targetChars = charSet.slice(0, limit);
944
- const combinations = new Set();
945
- const add = (s, context) => {
946
- if (combinations.has(s))
947
- throw new Error(`Combination conflict: ${context}`);
948
- combinations.add(s);
949
- };
950
- for (let i = 0; i < targetChars.length; i++) {
951
- combinations.add(targetChars[i]);
952
- }
953
- combinations.add(paddingChar);
954
- for (let i = 0; i < targetChars.length; i++) {
955
- const c1 = targetChars[i];
956
- for (let j = 0; j < targetChars.length; j++) {
957
- add(c1 + targetChars[j], `"${c1}" + "${targetChars[j]}"`);
958
- }
959
- add(c1 + paddingChar, `"${c1}" + padding`);
960
- add(paddingChar + c1, `padding + "${c1}"`);
961
- }
962
- add(paddingChar + paddingChar, "double padding");
963
- }
964
- }