@ddunigma/node 2.0.1 → 2.1.1

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 (47) hide show
  1. package/LICENCE +1 -1
  2. package/README.md +304 -46
  3. package/dist/cjs/base/BaseDdu.d.ts +35 -9
  4. package/dist/cjs/base/BaseDdu.js +14 -39
  5. package/dist/cjs/charSets/index.d.ts +14 -0
  6. package/dist/cjs/charSets/index.js +4292 -4248
  7. package/dist/cjs/encoders/Ddu64.d.ts +264 -10
  8. package/dist/cjs/encoders/Ddu64.js +1047 -205
  9. package/dist/cjs/encoders/index.js +0 -1
  10. package/dist/cjs/index.d.ts +2 -1
  11. package/dist/cjs/index.js +8 -4
  12. package/dist/cjs/types/DduDefaultTypes.js +5 -0
  13. package/dist/cjs/types/DduInterface.d.ts +72 -5
  14. package/dist/cjs/utils/CharsetBuilder.d.ts +129 -0
  15. package/dist/cjs/utils/CharsetBuilder.js +246 -0
  16. package/dist/cjs/utils/DduPipeline.d.ts +114 -0
  17. package/dist/cjs/utils/DduPipeline.js +229 -0
  18. package/dist/cjs/utils/DduStream.d.ts +64 -0
  19. package/dist/cjs/utils/DduStream.js +221 -0
  20. package/dist/cjs/utils/crypto.d.ts +30 -0
  21. package/dist/cjs/utils/crypto.js +84 -0
  22. package/dist/cjs/utils/index.d.ts +3 -0
  23. package/dist/cjs/utils/index.js +12 -0
  24. package/dist/mjs/base/BaseDdu.d.ts +35 -9
  25. package/dist/mjs/base/BaseDdu.js +14 -39
  26. package/dist/mjs/base/index.js +1 -1
  27. package/dist/mjs/charSets/index.d.ts +14 -0
  28. package/dist/mjs/charSets/index.js +4293 -4249
  29. package/dist/mjs/encoders/Ddu64.d.ts +264 -10
  30. package/dist/mjs/encoders/Ddu64.js +1080 -211
  31. package/dist/mjs/encoders/index.js +1 -2
  32. package/dist/mjs/index.d.ts +2 -1
  33. package/dist/mjs/index.js +4 -6
  34. package/dist/mjs/types/DduDefaultTypes.js +6 -1
  35. package/dist/mjs/types/DduInterface.d.ts +72 -5
  36. package/dist/mjs/types/index.js +3 -3
  37. package/dist/mjs/utils/CharsetBuilder.d.ts +129 -0
  38. package/dist/mjs/utils/CharsetBuilder.js +241 -0
  39. package/dist/mjs/utils/DduPipeline.d.ts +114 -0
  40. package/dist/mjs/utils/DduPipeline.js +223 -0
  41. package/dist/mjs/utils/DduStream.d.ts +64 -0
  42. package/dist/mjs/utils/DduStream.js +225 -0
  43. package/dist/mjs/utils/crypto.d.ts +30 -0
  44. package/dist/mjs/utils/crypto.js +78 -0
  45. package/dist/mjs/utils/index.d.ts +3 -0
  46. package/dist/mjs/utils/index.js +3 -0
  47. package/package.json +15 -8
@@ -1,27 +1,163 @@
1
- import { BaseDdu } from "../base/BaseDdu";
2
- import { DduSetSymbol, dduDefaultConstructorOptions, } from "../types";
3
- import { getCharSet } from "../charSets";
1
+ import { deflateSync } 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
+ import { deriveKey, encryptAes256Gcm, decryptAes256Gcm, inflateWithLimit as inflateWithLimitUtil, } from "../utils/crypto.js";
6
+ // ============================================================================
7
+ // 상수 정의
8
+ // ============================================================================
9
+ /** 바이트당 비트 수 */
10
+ const BYTE_BITS = 8;
11
+ /** 일반 정수 연산이 가능한 최대 비트 길이 (초과 시 BigInt 사용) */
12
+ const MAX_FAST_BITS = 16;
13
+ /** 바이트 마스크 (0xFF) */
14
+ const BYTE_MASK = 0xff;
15
+ /** 압축 데이터 식별 마커 */
16
+ const COMPRESS_MARKER = "ELYSIA";
17
+ /** 체크섬 마커 */
18
+ const CHECKSUM_MARKER = "CHK";
19
+ /** 암호화 마커 */
20
+ const ENCRYPT_MARKER = "ENC";
21
+ /** 기본 최대 디코딩 바이트 수 (64MB) */
22
+ const DEFAULT_MAX_DECODED_BYTES = 64 * 1024 * 1024;
23
+ /** 기본 최대 압축해제 바이트 수 (64MB) */
24
+ const DEFAULT_MAX_DECOMPRESSED_BYTES = 64 * 1024 * 1024;
25
+ /** CRC32 룩업 테이블 (바이트 단위 연산으로 비트 루프 대비 4~8배 빠름) */
26
+ const CRC32_TABLE = (() => {
27
+ const table = new Uint32Array(256);
28
+ for (let i = 0; i < 256; i++) {
29
+ let crc = i;
30
+ for (let j = 0; j < 8; j++) {
31
+ crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
32
+ }
33
+ table[i] = crc;
34
+ }
35
+ return table;
36
+ })();
37
+ /** URL-Safe 문자 매핑 */
38
+ const URL_SAFE_MAP = {
39
+ "+": "-",
40
+ "/": "_",
41
+ "=": ".",
42
+ };
43
+ /** URL-Safe 역방향 매핑 */
44
+ const URL_SAFE_REVERSE_MAP = {
45
+ "-": "+",
46
+ "_": "/",
47
+ ".": "=",
48
+ };
49
+ /** 인코딩 진행률 단계별 퍼센트 */
50
+ const ENCODE_PROGRESS = {
51
+ START: 0,
52
+ ENCRYPT: 15,
53
+ CHECKSUM: 20,
54
+ COMPRESS: 40,
55
+ ENCODE: 50,
56
+ DONE: 100,
57
+ };
58
+ /** 디코딩 진행률 단계별 퍼센트 */
59
+ const DECODE_PROGRESS = {
60
+ START: 0,
61
+ DECODE: 20,
62
+ DECOMPRESS: 50,
63
+ CHECKSUM: 70,
64
+ DECRYPT: 85,
65
+ DONE: 100,
66
+ };
67
+ // ============================================================================
68
+ // Ddu64 클래스
69
+ // ============================================================================
70
+ /**
71
+ * 커스텀 charset을 사용하는 Base64 스타일 인코더
72
+ *
73
+ * @description
74
+ * 바이너리 데이터를 지정된 charset으로 인코딩/디코딩합니다.
75
+ * 2의 제곱수 charset과 가변 길이 charset 모두 지원하며,
76
+ * 압축 옵션을 통해 데이터 크기를 줄일 수 있습니다.
77
+ *
78
+ * @example
79
+ * // 기본 사용
80
+ * const encoder = new Ddu64("우따야", "뭐");
81
+ * const encoded = encoder.encode("Hello");
82
+ * const decoded = encoder.decode(encoded);
83
+ *
84
+ * @example
85
+ * // 압축 사용
86
+ * const encoder = new Ddu64(undefined, undefined, { compress: true });
87
+ * const encoded = encoder.encode(longText);
88
+ */
4
89
  export class Ddu64 extends BaseDdu {
5
- // =========================================================================================
6
- // Properties
7
- // =========================================================================================
90
+ // --------------------------------------------------------------------------
91
+ // 멤버 변수
92
+ // --------------------------------------------------------------------------
93
+ /** 인코딩에 사용할 문자 배열 */
8
94
  dduChar;
95
+ /** 패딩 문자 */
9
96
  paddingChar;
97
+ /** 각 charset 문자의 길이 */
10
98
  charLength;
99
+ /** 비트 길이 (log2) */
11
100
  bitLength;
101
+ /** 2의 제곱수 charset 여부 */
12
102
  usePowerOfTwo;
103
+ /** 문자열 인코딩 방식 */
13
104
  encoding;
105
+ /** 기본 압축 사용 여부 */
106
+ defaultCompress;
107
+ /** 기본 최대 디코딩 바이트 수 */
108
+ defaultMaxDecodedBytes;
109
+ /** 기본 최대 압축해제 바이트 수 */
110
+ defaultMaxDecompressedBytes;
111
+ /** 문자 → 인덱스 역방향 룩업 맵 */
14
112
  dduBinaryLookup = new Map();
113
+ /** 미리 정의된 charset 사용 여부 */
15
114
  isPredefinedCharSet;
115
+ /** 실제 사용되는 비트 길이 */
16
116
  effectiveBitLength;
117
+ /** 최대 바이너리 값 */
17
118
  maxBinaryValue;
18
- // =========================================================================================
19
- // Constructor
20
- // =========================================================================================
119
+ /** ASCII 문자 빠른 룩업 테이블 */
120
+ fastAsciiLookup = null;
121
+ /** ASCII 룩업 사용 여부 */
122
+ useAsciiLookup = false;
123
+ /** URL-Safe 모드 여부 */
124
+ urlSafe;
125
+ /** 암호화 키 해시 (AES-256용 32바이트) */
126
+ encryptionKeyHash;
127
+ /** 기본 체크섬 사용 여부 */
128
+ defaultChecksum;
129
+ /** 기본 청크 크기 */
130
+ defaultChunkSize;
131
+ /** 기본 청크 구분자 */
132
+ defaultChunkSeparator;
133
+ /** 기본 압축 레벨 (1~9) */
134
+ defaultCompressionLevel;
135
+ // --------------------------------------------------------------------------
136
+ // 생성자
137
+ // --------------------------------------------------------------------------
138
+ /**
139
+ * Ddu64 인코더 인스턴스를 생성합니다.
140
+ *
141
+ * @param dduChar - charset 문자열 또는 배열 (미지정 시 옵션의 dduSetSymbol 사용)
142
+ * @param paddingChar - 패딩 문자 (dduChar 지정 시 필수)
143
+ * @param dduOptions - 생성자 옵션
144
+ *
145
+ * @throws dduChar 지정 시 paddingChar가 없으면 에러
146
+ * @throws charset 문자 수가 부족하면 에러
147
+ *
148
+ * @example
149
+ * // 커스텀 charset
150
+ * new Ddu64("우따야", "뭐");
151
+ *
152
+ * @example
153
+ * // 미리 정의된 charset
154
+ * new Ddu64(undefined, undefined, { dduSetSymbol: DduSetSymbol.ONECHARSET });
155
+ */
21
156
  constructor(dduChar, paddingChar, dduOptions) {
22
157
  super();
23
- const shouldThrow = dduOptions?.useBuildErrorReturn ?? false;
24
- // 1. CharSet 초기화 검증
158
+ // throwOnError 우선, useBuildErrorReturn 하위 호환
159
+ const shouldThrow = dduOptions?.throwOnError ?? dduOptions?.useBuildErrorReturn ?? false;
160
+ // charset 초기화
25
161
  const initial = this.resolveInitialCharSet(dduChar, paddingChar, dduOptions, shouldThrow);
26
162
  const normalized = this.normalizeCharSet(initial, shouldThrow, dduOptions);
27
163
  this.dduChar = normalized.charSet;
@@ -29,48 +165,259 @@ export class Ddu64 extends BaseDdu {
29
165
  this.charLength = normalized.charLength;
30
166
  this.isPredefinedCharSet = normalized.isPredefined;
31
167
  this.encoding = dduOptions?.encoding ?? this.defaultEncoding;
32
- // 2. 비트 연산 상수 계산
168
+ this.defaultCompress = dduOptions?.compress ?? false;
169
+ // 제한값 설정
170
+ this.defaultMaxDecodedBytes = this.normalizeLimit(dduOptions?.maxDecodedBytes, DEFAULT_MAX_DECODED_BYTES, shouldThrow, "maxDecodedBytes");
171
+ this.defaultMaxDecompressedBytes = this.normalizeLimit(dduOptions?.maxDecompressedBytes, DEFAULT_MAX_DECOMPRESSED_BYTES, shouldThrow, "maxDecompressedBytes");
172
+ // 비트 길이 계산
33
173
  const dduLength = this.dduChar.length;
34
- this.usePowerOfTwo = dduLength > 0 && (dduLength & (dduLength - 1)) === 0;
174
+ this.usePowerOfTwo =
175
+ dduLength > 0 && (dduLength & (dduLength - 1)) === 0;
35
176
  const computedBitLength = this.getBitLength(dduLength);
36
- this.bitLength = this.usePowerOfTwo ? this.getLargestPowerOfTwoExponent(dduLength) : computedBitLength;
37
- this.effectiveBitLength = this.usePowerOfTwo ? this.bitLength : computedBitLength;
38
- this.maxBinaryValue = 2 ** this.effectiveBitLength;
39
- // 3. Lookup Table 생성 (Decoding용)
177
+ this.bitLength = this.usePowerOfTwo
178
+ ? this.getLargestPowerOfTwoExponent(dduLength)
179
+ : computedBitLength;
180
+ this.effectiveBitLength = this.usePowerOfTwo
181
+ ? this.bitLength
182
+ : computedBitLength;
183
+ this.maxBinaryValue =
184
+ this.effectiveBitLength < 31
185
+ ? 1 << this.effectiveBitLength
186
+ : Math.pow(2, this.effectiveBitLength);
187
+ // ASCII 룩업 테이블 초기화 (성능 최적화)
188
+ if (this.charLength === 1) {
189
+ let allAscii = true;
190
+ for (let i = 0; i < dduLength; i++) {
191
+ if (this.dduChar[i].charCodeAt(0) >= 128) {
192
+ allAscii = false;
193
+ break;
194
+ }
195
+ }
196
+ if (allAscii) {
197
+ this.fastAsciiLookup = new Int16Array(128).fill(-1);
198
+ for (let i = 0; i < dduLength; i++) {
199
+ this.fastAsciiLookup[this.dduChar[i].charCodeAt(0)] = i;
200
+ }
201
+ this.useAsciiLookup = true;
202
+ }
203
+ }
204
+ // 역방향 룩업 맵 생성
40
205
  for (let i = 0; i < dduLength; i++) {
41
206
  this.dduBinaryLookup.set(this.dduChar[i], i);
42
207
  }
43
- // 4. 안전성 검사
208
+ // 커스텀 charset 중복 조합 검증
44
209
  if (this.charLength === 1 && !this.isPredefinedCharSet) {
45
210
  this.validateCombinationDuplicates(this.dduChar, this.paddingChar, dduLength);
46
211
  }
212
+ // 새로운 옵션들 초기화
213
+ const requestUrlSafe = dduOptions?.urlSafe ?? false;
214
+ this.urlSafe = requestUrlSafe
215
+ ? this.isUrlSafeCompatible(this.dduChar, this.paddingChar, shouldThrow)
216
+ : false;
217
+ this.encryptionKeyHash = dduOptions?.encryptionKey
218
+ ? deriveKey(dduOptions.encryptionKey)
219
+ : undefined;
220
+ this.defaultChecksum = dduOptions?.checksum ?? false;
221
+ this.defaultChunkSize = dduOptions?.chunkSize;
222
+ this.defaultChunkSeparator = dduOptions?.chunkSeparator ?? "\n";
223
+ this.defaultCompressionLevel = Math.min(9, Math.max(1, Math.floor(dduOptions?.compressionLevel ?? 6)));
224
+ }
225
+ // --------------------------------------------------------------------------
226
+ // 공개 메서드
227
+ // --------------------------------------------------------------------------
228
+ /**
229
+ * 입력 데이터를 인코딩합니다.
230
+ *
231
+ * @param input - 인코딩할 문자열 또는 Buffer
232
+ * @param options - 인코딩 옵션
233
+ * @param options.compress - 압축 사용 여부 (기본값: 생성자 설정)
234
+ * @param options.checksum - 체크섬 추가 여부
235
+ * @param options.chunkSize - 청크 분할 크기
236
+ * @param options.chunkSeparator - 청크 구분자
237
+ * @param options.onProgress - 진행률 콜백
238
+ * @returns 인코딩된 문자열
239
+ *
240
+ * @example
241
+ * encoder.encode("Hello World!");
242
+ * encoder.encode(buffer, { compress: true });
243
+ * encoder.encode(data, { checksum: true, chunkSize: 76 });
244
+ */
245
+ encode(input, options) {
246
+ return this.encodeInternal(input, options).encoded;
47
247
  }
48
- // =========================================================================================
49
- // Public API
50
- // =========================================================================================
51
248
  /**
52
- * 데이터를 DDU 포맷으로 인코딩합니다.
53
- * 성능을 위해 24비트 이하는 Fast Path(number 연산)를 사용합니다.
249
+ * 인코딩 핵심 로직. encode()와 getStats()가 공유합니다.
250
+ * 압축 크기 메타데이터도 함께 반환하여 중복 deflateSync 호출을 방지합니다.
54
251
  */
55
- encode(input, _options) {
56
- const bufferInput = typeof input === "string" ? Buffer.from(input, this.encoding) : input;
57
- if (this.effectiveBitLength <= 24) {
58
- return this.encodeFast(bufferInput);
252
+ encodeInternal(input, options) {
253
+ const shouldCompress = options?.compress ?? this.defaultCompress;
254
+ const shouldChecksum = options?.checksum ?? this.defaultChecksum;
255
+ const chunkSize = options?.chunkSize ?? this.defaultChunkSize;
256
+ const chunkSeparator = options?.chunkSeparator ?? this.defaultChunkSeparator;
257
+ const onProgress = options?.onProgress;
258
+ let workingBuffer = typeof input === "string" ? Buffer.from(input, this.encoding) : input;
259
+ const totalBytes = workingBuffer.length;
260
+ // 진행률 콜백 호출 (시작)
261
+ if (onProgress) {
262
+ onProgress({ processedBytes: 0, totalBytes, percent: ENCODE_PROGRESS.START, stage: "start" });
59
263
  }
60
- return this.encodeBigInt(bufferInput);
264
+ // 암호화 처리
265
+ let isEncrypted = false;
266
+ if (this.encryptionKeyHash) {
267
+ workingBuffer = this.encryptData(workingBuffer);
268
+ isEncrypted = true;
269
+ if (onProgress) {
270
+ onProgress({ processedBytes: 0, totalBytes, percent: ENCODE_PROGRESS.ENCRYPT, stage: "encrypt" });
271
+ }
272
+ }
273
+ // 체크섬 계산
274
+ let checksum = "";
275
+ if (shouldChecksum) {
276
+ checksum = this.calculateCRC32(workingBuffer);
277
+ if (onProgress) {
278
+ onProgress({ processedBytes: 0, totalBytes, percent: ENCODE_PROGRESS.CHECKSUM, stage: "checksum" });
279
+ }
280
+ }
281
+ // 압축 처리
282
+ let useCompression = false;
283
+ let compressedSize;
284
+ if (shouldCompress) {
285
+ const level = options?.compressionLevel ?? this.defaultCompressionLevel;
286
+ const compressedBuffer = deflateSync(workingBuffer, { level: Math.min(9, Math.max(1, level)) });
287
+ compressedSize = compressedBuffer.length;
288
+ if (compressedBuffer.length < workingBuffer.length) {
289
+ workingBuffer = compressedBuffer;
290
+ useCompression = true;
291
+ }
292
+ if (onProgress) {
293
+ onProgress({ processedBytes: Math.floor(totalBytes * 0.4), totalBytes, percent: ENCODE_PROGRESS.COMPRESS, stage: "compress" });
294
+ }
295
+ }
296
+ // 인코딩 수행
297
+ if (onProgress) {
298
+ onProgress({ processedBytes: Math.floor(totalBytes * 0.5), totalBytes, percent: ENCODE_PROGRESS.ENCODE, stage: "encode" });
299
+ }
300
+ let result = this.effectiveBitLength <= MAX_FAST_BITS
301
+ ? this.encodeFast(workingBuffer, useCompression, isEncrypted)
302
+ : this.encodeBigInt(workingBuffer, useCompression, isEncrypted);
303
+ // 체크섬 추가
304
+ if (shouldChecksum && checksum) {
305
+ result = result + CHECKSUM_MARKER + checksum;
306
+ }
307
+ // URL-Safe 변환
308
+ if (this.urlSafe) {
309
+ result = this.toUrlSafe(result);
310
+ }
311
+ // 청크 분할
312
+ if (chunkSize && chunkSize > 0) {
313
+ result = this.splitIntoChunks(result, chunkSize, chunkSeparator);
314
+ }
315
+ // 진행률 콜백 호출 (완료)
316
+ if (onProgress) {
317
+ onProgress({ processedBytes: totalBytes, totalBytes, percent: ENCODE_PROGRESS.DONE, stage: "done" });
318
+ }
319
+ return { encoded: result, compressedSize };
61
320
  }
62
321
  /**
63
- * DDU 포맷 문자열을 버퍼로 디코딩합니다.
322
+ * 인코딩된 문자열을 Buffer로 디코딩합니다.
323
+ *
324
+ * @param input - 디코딩할 인코딩된 문자열
325
+ * @param options - 디코딩 옵션
326
+ * @param options.maxDecodedBytes - 최대 디코딩 바이트 수
327
+ * @param options.maxDecompressedBytes - 최대 압축해제 바이트 수
328
+ * @param options.onProgress - 진행률 콜백
329
+ * @returns 디코딩된 Buffer
330
+ *
331
+ * @throws 잘못된 문자가 포함된 경우
332
+ * @throws 패딩 형식이 잘못된 경우
333
+ * @throws 크기 제한 초과 시
334
+ * @throws 체크섬 불일치 시
64
335
  */
65
- decodeToBuffer(input, _options) {
66
- if (this.effectiveBitLength <= 24) {
67
- return this.decodeFast(input);
336
+ decodeToBuffer(input, options) {
337
+ const shouldChecksum = options?.checksum ?? this.defaultChecksum;
338
+ const onProgress = options?.onProgress;
339
+ let workingInput = input;
340
+ const inputLength = input.length;
341
+ // 진행률 콜백 호출 (시작)
342
+ if (onProgress) {
343
+ onProgress({ processedBytes: 0, totalBytes: inputLength, percent: DECODE_PROGRESS.START, stage: "start" });
344
+ }
345
+ // 청크 제거 (줄바꿈 등 구분자 제거)
346
+ workingInput = this.removeChunks(workingInput);
347
+ // URL-Safe 역변환
348
+ if (this.urlSafe) {
349
+ workingInput = this.fromUrlSafe(workingInput);
350
+ }
351
+ // 체크섬 추출 (체크섬이 활성화된 경우에만 수행하여 오탐지 방지)
352
+ let extractedChecksum = null;
353
+ if (shouldChecksum) {
354
+ const result = this.extractChecksum(workingInput);
355
+ extractedChecksum = result.checksum;
356
+ workingInput = result.data;
357
+ }
358
+ const { cleanedInput, paddingBits, isCompressed, isEncrypted } = this.parseFooter(workingInput);
359
+ this.assertEncodedInputAligned(cleanedInput);
360
+ // 디코딩 크기 검증
361
+ const maxDecodedBytes = this.normalizeLimit(options?.maxDecodedBytes, this.defaultMaxDecodedBytes, true, "maxDecodedBytes");
362
+ const estimatedDecodedBytes = this.estimateDecodedBytes(cleanedInput.length, paddingBits);
363
+ if (estimatedDecodedBytes > maxDecodedBytes) {
364
+ throw new Error(`[Ddu64 decode] Decoded output exceeds limit. Estimated: ${estimatedDecodedBytes} bytes, Limit: ${maxDecodedBytes} bytes`);
365
+ }
366
+ // 디코딩 수행
367
+ if (onProgress) {
368
+ onProgress({ processedBytes: Math.floor(inputLength * 0.2), totalBytes: inputLength, percent: DECODE_PROGRESS.DECODE, stage: "decode" });
369
+ }
370
+ let decoded = this.effectiveBitLength <= MAX_FAST_BITS
371
+ ? this.decodeFast(cleanedInput, paddingBits)
372
+ : this.decodeBigInt(cleanedInput, paddingBits);
373
+ // 압축 해제
374
+ if (isCompressed) {
375
+ if (onProgress) {
376
+ onProgress({ processedBytes: Math.floor(inputLength * 0.5), totalBytes: inputLength, percent: DECODE_PROGRESS.DECOMPRESS, stage: "decompress" });
377
+ }
378
+ const maxDecompressedBytes = this.normalizeLimit(options?.maxDecompressedBytes, this.defaultMaxDecompressedBytes, true, "maxDecompressedBytes");
379
+ decoded = this.inflateWithLimit(decoded, maxDecompressedBytes);
380
+ }
381
+ // 체크섬 검증
382
+ if (extractedChecksum) {
383
+ if (onProgress) {
384
+ onProgress({ processedBytes: Math.floor(inputLength * 0.7), totalBytes: inputLength, percent: DECODE_PROGRESS.CHECKSUM, stage: "checksum" });
385
+ }
386
+ const calculatedChecksum = this.calculateCRC32(decoded);
387
+ if (calculatedChecksum !== extractedChecksum) {
388
+ throw new Error(`[Ddu64 decode] Checksum mismatch. Expected: ${extractedChecksum}, Got: ${calculatedChecksum}`);
389
+ }
68
390
  }
69
- return this.decodeBigInt(input);
391
+ // 복호화
392
+ if (isEncrypted && this.encryptionKeyHash) {
393
+ if (onProgress) {
394
+ onProgress({ processedBytes: Math.floor(inputLength * 0.85), totalBytes: inputLength, percent: DECODE_PROGRESS.DECRYPT, stage: "decrypt" });
395
+ }
396
+ decoded = this.decryptData(decoded);
397
+ }
398
+ // 진행률 콜백 호출 (완료)
399
+ if (onProgress) {
400
+ onProgress({ processedBytes: inputLength, totalBytes: inputLength, percent: DECODE_PROGRESS.DONE, stage: "done" });
401
+ }
402
+ return decoded;
70
403
  }
71
- decode(input, _options) {
72
- return this.decodeToBuffer(input, _options).toString(this.encoding);
404
+ /**
405
+ * 인코딩된 문자열을 원본 문자열로 디코딩합니다.
406
+ *
407
+ * @param input - 디코딩할 인코딩된 문자열
408
+ * @param options - 디코딩 옵션
409
+ * @returns 디코딩된 문자열
410
+ *
411
+ * @throws 잘못된 문자나 패딩 형식일 경우 에러
412
+ */
413
+ decode(input, options) {
414
+ return this.decodeToBuffer(input, options).toString(this.encoding);
73
415
  }
416
+ /**
417
+ * 현재 인코더의 charset 정보를 반환합니다.
418
+ *
419
+ * @returns charset 설정 정보 객체
420
+ */
74
421
  getCharSetInfo() {
75
422
  return {
76
423
  charSet: [...this.dduChar],
@@ -79,335 +426,805 @@ export class Ddu64 extends BaseDdu {
79
426
  bitLength: this.bitLength,
80
427
  usePowerOfTwo: this.usePowerOfTwo,
81
428
  encoding: this.encoding,
429
+ defaultCompress: this.defaultCompress,
430
+ defaultMaxDecodedBytes: this.defaultMaxDecodedBytes,
431
+ defaultMaxDecompressedBytes: this.defaultMaxDecompressedBytes,
432
+ urlSafe: this.urlSafe,
433
+ hasEncryptionKey: !!this.encryptionKeyHash,
434
+ defaultChecksum: this.defaultChecksum,
435
+ defaultChunkSize: this.defaultChunkSize,
436
+ };
437
+ }
438
+ /**
439
+ * 인코딩 통계 정보를 반환합니다.
440
+ *
441
+ * @param input - 분석할 데이터
442
+ * @param options - 인코딩 옵션
443
+ * @returns 통계 정보 객체
444
+ */
445
+ getStats(input, options) {
446
+ const shouldCompress = options?.compress ?? this.defaultCompress;
447
+ const originalBuffer = typeof input === "string" ? Buffer.from(input, this.encoding) : input;
448
+ const originalSize = originalBuffer.length;
449
+ // encodeInternal을 통해 인코딩과 압축 크기를 한 번에 얻어 중복 deflateSync 방지
450
+ const { encoded, compressedSize } = this.encodeInternal(input, options);
451
+ const encodedSize = encoded.length;
452
+ const expansionRatio = originalSize > 0 ? encodedSize / originalSize : 0;
453
+ const compressionRatio = shouldCompress && compressedSize !== undefined && originalSize > 0
454
+ ? compressedSize / originalSize
455
+ : undefined;
456
+ return {
457
+ originalSize,
458
+ encodedSize,
459
+ compressedSize,
460
+ compressionRatio,
461
+ expansionRatio,
462
+ charsetSize: this.dduChar.length,
463
+ bitLength: this.bitLength,
464
+ };
465
+ }
466
+ /**
467
+ * 비동기 인코딩을 수행합니다.
468
+ *
469
+ * 내부적으로 동기 encode()를 setImmediate로 이벤트 루프에 양보한 뒤 실행합니다.
470
+ * 호출자가 즉시 블로킹되지 않도록 보장하지만, 인코딩 자체는 단일 동기 작업으로
471
+ * 수행되므로 대용량 데이터(수 MB 이상) 처리 시 이벤트 루프가 블로킹될 수 있습니다.
472
+ * 대용량 처리가 필요한 경우 스트림 API(createEncodeStream) 사용을 권장합니다.
473
+ *
474
+ * @param input - 인코딩할 데이터
475
+ * @param options - 인코딩 옵션
476
+ * @returns 인코딩된 문자열 Promise
477
+ */
478
+ async encodeAsync(input, options) {
479
+ return new Promise((resolve, reject) => {
480
+ setImmediate(() => {
481
+ try {
482
+ resolve(this.encode(input, options));
483
+ }
484
+ catch (e) {
485
+ reject(e);
486
+ }
487
+ });
488
+ });
489
+ }
490
+ /**
491
+ * 비동기 디코딩을 수행합니다.
492
+ *
493
+ * 내부적으로 동기 decode()를 setImmediate로 이벤트 루프에 양보한 뒤 실행합니다.
494
+ * 대용량 처리가 필요한 경우 스트림 API(createDecodeStream) 사용을 권장합니다.
495
+ *
496
+ * @param input - 디코딩할 인코딩된 문자열
497
+ * @param options - 디코딩 옵션
498
+ * @returns 디코딩된 문자열 Promise
499
+ */
500
+ async decodeAsync(input, options) {
501
+ return new Promise((resolve, reject) => {
502
+ setImmediate(() => {
503
+ try {
504
+ resolve(this.decode(input, options));
505
+ }
506
+ catch (e) {
507
+ reject(e);
508
+ }
509
+ });
510
+ });
511
+ }
512
+ /**
513
+ * 비동기 디코딩을 Buffer로 수행합니다.
514
+ *
515
+ * 내부적으로 동기 decodeToBuffer()를 setImmediate로 이벤트 루프에 양보한 뒤 실행합니다.
516
+ * 대용량 처리가 필요한 경우 스트림 API(createDecodeStream) 사용을 권장합니다.
517
+ *
518
+ * @param input - 디코딩할 인코딩된 문자열
519
+ * @param options - 디코딩 옵션
520
+ * @returns 디코딩된 Buffer Promise
521
+ */
522
+ async decodeToBufferAsync(input, options) {
523
+ return new Promise((resolve, reject) => {
524
+ setImmediate(() => {
525
+ try {
526
+ resolve(this.decodeToBuffer(input, options));
527
+ }
528
+ catch (e) {
529
+ reject(e);
530
+ }
531
+ });
532
+ });
533
+ }
534
+ // --------------------------------------------------------------------------
535
+ // URL-Safe 메서드
536
+ // --------------------------------------------------------------------------
537
+ /**
538
+ * 문자열을 URL-Safe 형식으로 변환합니다.
539
+ * 단일 정규식 패스로 처리하여 split/join 3회 반복 대비 메모리/속도 개선
540
+ */
541
+ toUrlSafe(input) {
542
+ return input.replace(/[+/=]/g, (c) => URL_SAFE_MAP[c]);
543
+ }
544
+ /**
545
+ * URL-Safe 형식에서 원래 형식으로 복원합니다.
546
+ */
547
+ fromUrlSafe(input) {
548
+ return input.replace(/[-_.]/g, (c) => URL_SAFE_REVERSE_MAP[c]);
549
+ }
550
+ // --------------------------------------------------------------------------
551
+ // 체크섬 메서드
552
+ // --------------------------------------------------------------------------
553
+ /**
554
+ * CRC32 체크섬을 계산합니다. (룩업 테이블 사용)
555
+ */
556
+ calculateCRC32(data) {
557
+ let crc = 0xffffffff;
558
+ for (let i = 0; i < data.length; i++) {
559
+ crc = (crc >>> 8) ^ CRC32_TABLE[(crc ^ data[i]) & 0xff];
560
+ }
561
+ return ((crc ^ 0xffffffff) >>> 0).toString(16).padStart(8, "0");
562
+ }
563
+ /**
564
+ * 인코딩된 문자열에서 체크섬을 추출합니다.
565
+ */
566
+ extractChecksum(input) {
567
+ const markerIndex = input.lastIndexOf(CHECKSUM_MARKER);
568
+ if (markerIndex === -1) {
569
+ return { data: input, checksum: null };
570
+ }
571
+ const checksum = input.slice(markerIndex + CHECKSUM_MARKER.length);
572
+ if (checksum.length !== 8 || !/^[0-9a-f]+$/i.test(checksum)) {
573
+ return { data: input, checksum: null };
574
+ }
575
+ return {
576
+ data: input.slice(0, markerIndex),
577
+ checksum: checksum.toLowerCase(),
82
578
  };
83
579
  }
84
- // =========================================================================================
85
- // Fast Engine (Optimized for Speed)
86
- // Uses standard JS numbers (safe up to ~24 bits per chunk) to avoid BigInt GC overhead.
87
- // =========================================================================================
88
- encodeFast(bufferInput) {
89
- const resultParts = [];
580
+ // --------------------------------------------------------------------------
581
+ // 청크 분할 메서드
582
+ // --------------------------------------------------------------------------
583
+ /**
584
+ * 문자열을 청크로 분할합니다.
585
+ */
586
+ splitIntoChunks(input, chunkSize, separator) {
587
+ if (chunkSize <= 0)
588
+ return input;
589
+ const chunks = [];
590
+ for (let i = 0; i < input.length; i += chunkSize) {
591
+ chunks.push(input.slice(i, i + chunkSize));
592
+ }
593
+ return chunks.join(separator);
594
+ }
595
+ /**
596
+ * 청크 구분자를 제거합니다.
597
+ * 줄바꿈(\r, \n)과 인스턴스에 설정된 청크 구분자만 제거합니다.
598
+ * 공백/탭 등은 charset에 포함될 수 있으므로 제거하지 않습니다.
599
+ */
600
+ removeChunks(input) {
601
+ // 줄바꿈 문자(\r, \n)는 항상 제거 (기본 구분자 및 일반적 라인 구분)
602
+ let result = input.replace(/[\r\n]/g, "");
603
+ // 커스텀 구분자가 줄바꿈이 아닌 경우 추가로 제거
604
+ const sep = this.defaultChunkSeparator;
605
+ if (sep && sep !== "\n" && sep !== "\r\n" && sep !== "\r") {
606
+ result = result.split(sep).join("");
607
+ }
608
+ return result;
609
+ }
610
+ // --------------------------------------------------------------------------
611
+ // 암호화 메서드
612
+ // --------------------------------------------------------------------------
613
+ /**
614
+ * 데이터를 AES-256-GCM으로 암호화합니다.
615
+ */
616
+ encryptData(data) {
617
+ if (!this.encryptionKeyHash) {
618
+ throw new Error("[Ddu64 encrypt] Encryption key is not set");
619
+ }
620
+ return encryptAes256Gcm(data, this.encryptionKeyHash);
621
+ }
622
+ /**
623
+ * AES-256-GCM으로 암호화된 데이터를 복호화합니다.
624
+ */
625
+ decryptData(data) {
626
+ if (!this.encryptionKeyHash) {
627
+ throw new Error("[Ddu64 decrypt] Encryption key is not set");
628
+ }
629
+ return decryptAes256Gcm(data, this.encryptionKeyHash);
630
+ }
631
+ // --------------------------------------------------------------------------
632
+ // 유틸리티 메서드
633
+ // --------------------------------------------------------------------------
634
+ /**
635
+ * 옵션 값을 정규화합니다.
636
+ */
637
+ normalizeLimit(value, fallback, shouldThrow, name) {
638
+ if (value === undefined)
639
+ return fallback;
640
+ if (value === Number.POSITIVE_INFINITY)
641
+ return Number.POSITIVE_INFINITY;
642
+ if (!Number.isFinite(value) || value <= 0) {
643
+ if (shouldThrow) {
644
+ throw new Error(`[Ddu64 options] Invalid ${name}. Must be a positive finite number or Infinity.`);
645
+ }
646
+ return fallback;
647
+ }
648
+ return Math.floor(value);
649
+ }
650
+ /**
651
+ * 디코딩 결과 바이트 수를 추정합니다.
652
+ */
653
+ estimateDecodedBytes(cleanedInputLen, paddingBits) {
654
+ if (cleanedInputLen === 0)
655
+ return 0;
656
+ if (paddingBits < 0 || paddingBits >= this.effectiveBitLength) {
657
+ throw new Error(`[Ddu64 decode] Invalid padding bits: ${paddingBits}`);
658
+ }
659
+ const chunkSize = this.usePowerOfTwo ? this.charLength : this.charLength * 2;
660
+ const numChunks = Math.ceil(cleanedInputLen / chunkSize);
661
+ const bits = numChunks * this.effectiveBitLength - paddingBits;
662
+ if (bits < 0)
663
+ throw new Error(`[Ddu64 decode] Invalid decoded bit length`);
664
+ return Math.ceil(bits / BYTE_BITS);
665
+ }
666
+ /**
667
+ * 인코딩된 입력의 정렬을 검증합니다.
668
+ */
669
+ assertEncodedInputAligned(cleanedInput) {
670
+ const { charLength } = this;
671
+ if (charLength <= 0)
672
+ return;
673
+ if (cleanedInput.length % charLength !== 0) {
674
+ throw new Error(`[Ddu64 decode] Invalid encoded length. Expected multiple of ${charLength}, got ${cleanedInput.length}`);
675
+ }
676
+ if (!this.usePowerOfTwo) {
677
+ const chunkSize = charLength * 2;
678
+ if (cleanedInput.length % chunkSize !== 0) {
679
+ throw new Error(`[Ddu64 decode] Invalid encoded length for variable charset. Expected multiple of ${chunkSize}, got ${cleanedInput.length}`);
680
+ }
681
+ }
682
+ }
683
+ /**
684
+ * 크기 제한을 적용하여 압축을 해제합니다.
685
+ */
686
+ inflateWithLimit(data, maxBytes) {
687
+ return inflateWithLimitUtil(data, maxBytes, "Ddu64 decode");
688
+ }
689
+ // --------------------------------------------------------------------------
690
+ // 푸터 파싱
691
+ // --------------------------------------------------------------------------
692
+ /**
693
+ * 인코딩된 문자열의 푸터(패딩 정보)를 파싱합니다.
694
+ *
695
+ * 푸터 형식: {encodedData}{paddingChar}[ELYSIA][ENC]{digits}
696
+ * 끝에서부터 역순으로 파싱하여 paddingChar가 숫자인 경우도 안전하게 처리합니다.
697
+ */
698
+ parseFooter(input) {
699
+ const inputLen = input.length;
700
+ const pad = this.paddingChar;
701
+ const padLen = pad.length;
702
+ const noFooter = { cleanedInput: input, paddingBits: 0, isCompressed: false, isEncrypted: false };
703
+ if (inputLen < padLen)
704
+ return noFooter;
705
+ const maxPaddingBits = Math.max(0, this.effectiveBitLength - 1);
706
+ const maxDigits = maxPaddingBits.toString().length;
707
+ // 끝에서부터 역순 파싱: digits → ENC → ELYSIA → paddingChar
708
+ for (let digitCount = Math.min(maxDigits, inputLen); digitCount >= 1; digitCount--) {
709
+ const digitsStart = inputLen - digitCount;
710
+ // 1) trailing digits 확인
711
+ let allDigits = true;
712
+ for (let i = digitsStart; i < inputLen; i++) {
713
+ const c = input.charCodeAt(i);
714
+ if (c < 48 || c > 57) {
715
+ allDigits = false;
716
+ break;
717
+ }
718
+ }
719
+ if (!allDigits)
720
+ continue;
721
+ const digitStr = input.substring(digitsStart);
722
+ const paddingBits = parseInt(digitStr, 10);
723
+ if (Number.isNaN(paddingBits) ||
724
+ paddingBits < 0 ||
725
+ paddingBits >= this.effectiveBitLength ||
726
+ digitStr !== paddingBits.toString())
727
+ continue;
728
+ // 2) digits 앞에서 마커들 역순 확인
729
+ let pos = digitsStart;
730
+ let isEncrypted = false;
731
+ let isCompressed = false;
732
+ if (pos >= ENCRYPT_MARKER.length && input.substring(pos - ENCRYPT_MARKER.length, pos) === ENCRYPT_MARKER) {
733
+ isEncrypted = true;
734
+ pos -= ENCRYPT_MARKER.length;
735
+ }
736
+ if (pos >= COMPRESS_MARKER.length && input.substring(pos - COMPRESS_MARKER.length, pos) === COMPRESS_MARKER) {
737
+ isCompressed = true;
738
+ pos -= COMPRESS_MARKER.length;
739
+ }
740
+ // 3) 마커 앞에서 padding 문자 확인
741
+ const padStart = pos - padLen;
742
+ if (padStart >= 0 && input.substring(padStart, pos) === pad) {
743
+ if (padStart % this.charLength !== 0) {
744
+ throw new Error(`[Ddu64 decode] Invalid padding format. Misaligned padding marker`);
745
+ }
746
+ return {
747
+ cleanedInput: input.substring(0, padStart),
748
+ paddingBits,
749
+ isCompressed,
750
+ isEncrypted,
751
+ };
752
+ }
753
+ }
754
+ // Fallback: 역순 탐색이 유효한 패딩을 찾지 못한 경우,
755
+ // padding 문자가 존재하지만 tail이 잘못된 형식인지 확인하여 에러 보고
756
+ const lastPadIdx = input.lastIndexOf(pad);
757
+ if (lastPadIdx >= 0 && lastPadIdx % this.charLength === 0) {
758
+ const tailStart = lastPadIdx + padLen;
759
+ if (tailStart >= inputLen) {
760
+ throw new Error(`[Ddu64 decode] Invalid padding format. Missing padding length`);
761
+ }
762
+ const tail = input.substring(tailStart);
763
+ throw new Error(`[Ddu64 decode] Invalid padding format. Got: "${tail}"`);
764
+ }
765
+ return noFooter;
766
+ }
767
+ // --------------------------------------------------------------------------
768
+ // 인코딩 (Fast 모드 - 16비트 이하)
769
+ // --------------------------------------------------------------------------
770
+ /**
771
+ * 일반 정수 연산을 사용한 빠른 인코딩
772
+ */
773
+ encodeFast(bufferInput, compress, encrypt) {
774
+ const inputLen = bufferInput.length;
775
+ if (inputLen === 0)
776
+ return "";
90
777
  const { dduChar, effectiveBitLength: bitLength, paddingChar } = this;
91
778
  const dduLength = dduChar.length;
779
+ const totalBits = inputLen * BYTE_BITS;
780
+ const estimatedChunks = Math.ceil(totalBits / bitLength);
781
+ const estimatedSymbols = this.usePowerOfTwo
782
+ ? estimatedChunks
783
+ : estimatedChunks * 2;
784
+ const resultParts = new Array(estimatedSymbols + 3);
785
+ let resultIdx = 0;
92
786
  let accumulator = 0;
93
787
  let accumulatorBits = 0;
94
- // Loop Unswitching: 조건문을 루프 밖으로 빼서 CPU 분기 예측 효율 향상
95
788
  if (this.usePowerOfTwo) {
96
- for (const byte of bufferInput) {
97
- accumulator = (accumulator << 8) | byte;
98
- accumulatorBits += 8;
789
+ const mask = (1 << bitLength) - 1;
790
+ for (let i = 0; i < inputLen; i++) {
791
+ accumulator = (accumulator << BYTE_BITS) | bufferInput[i];
792
+ accumulatorBits += BYTE_BITS;
99
793
  while (accumulatorBits >= bitLength) {
100
- const shift = accumulatorBits - bitLength;
101
- const index = accumulator >> shift;
102
- resultParts.push(dduChar[index]);
103
794
  accumulatorBits -= bitLength;
795
+ resultParts[resultIdx++] =
796
+ dduChar[(accumulator >> accumulatorBits) & mask];
104
797
  accumulator &= (1 << accumulatorBits) - 1;
105
798
  }
106
799
  }
107
800
  }
108
801
  else {
109
- for (const byte of bufferInput) {
110
- accumulator = (accumulator << 8) | byte;
111
- accumulatorBits += 8;
802
+ for (let i = 0; i < inputLen; i++) {
803
+ accumulator = (accumulator << BYTE_BITS) | bufferInput[i];
804
+ accumulatorBits += BYTE_BITS;
112
805
  while (accumulatorBits >= bitLength) {
113
- const shift = accumulatorBits - bitLength;
114
- const index = accumulator >> shift;
115
- // 비 2의 제곱수는 나눗셈으로 인덱스 계산
116
- const div = (index / dduLength) | 0;
117
- const mod = index % dduLength;
118
- resultParts.push(dduChar[div] + dduChar[mod]);
119
806
  accumulatorBits -= bitLength;
807
+ const index = accumulator >> accumulatorBits;
808
+ const div = (index / dduLength) | 0;
809
+ resultParts[resultIdx++] = dduChar[div];
810
+ resultParts[resultIdx++] = dduChar[index - div * dduLength];
120
811
  accumulator &= (1 << accumulatorBits) - 1;
121
812
  }
122
813
  }
123
814
  }
124
- // 남은 비트 패딩 처리
815
+ // 남은 비트 처리 (패딩)
125
816
  if (accumulatorBits > 0) {
126
817
  const paddingBits = bitLength - accumulatorBits;
127
818
  const index = accumulator << paddingBits;
128
819
  if (this.usePowerOfTwo) {
129
- resultParts.push(dduChar[index]);
820
+ resultParts[resultIdx++] = dduChar[index];
130
821
  }
131
822
  else {
132
823
  const div = (index / dduLength) | 0;
133
- const mod = index % dduLength;
134
- resultParts.push(dduChar[div] + dduChar[mod]);
824
+ resultParts[resultIdx++] = dduChar[div];
825
+ resultParts[resultIdx++] = dduChar[index - div * dduLength];
135
826
  }
136
- return resultParts.join("") + paddingChar + paddingBits.toString();
827
+ resultParts[resultIdx++] = paddingChar;
828
+ resultParts[resultIdx++] = (compress ? COMPRESS_MARKER : "") + (encrypt ? ENCRYPT_MARKER : "") + paddingBits.toString();
829
+ }
830
+ else if (compress || encrypt) {
831
+ resultParts[resultIdx++] = paddingChar;
832
+ resultParts[resultIdx++] = (compress ? COMPRESS_MARKER : "") + (encrypt ? ENCRYPT_MARKER : "") + "0";
137
833
  }
834
+ resultParts.length = resultIdx;
138
835
  return resultParts.join("");
139
836
  }
140
- decodeFast(input) {
141
- const { cleanedInput, paddingBits } = this.parsePaddingAndGetInput(input);
837
+ // --------------------------------------------------------------------------
838
+ // 디코딩 (Fast 모드 - 16비트 이하)
839
+ // --------------------------------------------------------------------------
840
+ /**
841
+ * 일반 정수 연산을 사용한 빠른 디코딩
842
+ */
843
+ decodeFast(cleanedInput, paddingBits) {
142
844
  const inputLen = cleanedInput.length;
143
- const buffer = [];
845
+ if (inputLen === 0)
846
+ return Buffer.alloc(0);
847
+ const { effectiveBitLength: bitLength, charLength } = this;
848
+ const dduLength = this.dduChar.length;
849
+ const chunkSize = this.usePowerOfTwo ? charLength : charLength * 2;
850
+ const numChunks = Math.ceil(inputLen / chunkSize);
851
+ const estimatedBytes = Math.ceil((numChunks * bitLength - paddingBits) / BYTE_BITS);
852
+ const buffer = new Uint8Array(estimatedBytes + 1);
853
+ let bufIdx = 0;
144
854
  let accumulator = 0;
145
855
  let accumulatorBits = 0;
146
- const { effectiveBitLength: bitLength, dduBinaryLookup: lookup, charLength, maxBinaryValue } = this;
147
- const dduLength = this.dduChar.length;
148
856
  if (this.usePowerOfTwo) {
149
- const chunkSize = charLength;
857
+ if (this.useAsciiLookup && this.fastAsciiLookup) {
858
+ // ASCII 최적화 경로
859
+ const lookup = this.fastAsciiLookup;
860
+ for (let i = 0; i < inputLen; i += chunkSize) {
861
+ const code = cleanedInput.charCodeAt(i);
862
+ const val = code < 128 ? lookup[code] : -1;
863
+ if (val < 0) {
864
+ throw new Error(`[Ddu64 decode] Invalid character "${cleanedInput[i]}" at ${i}`);
865
+ }
866
+ accumulator = (accumulator << bitLength) | val;
867
+ accumulatorBits += bitLength;
868
+ if (i + chunkSize >= inputLen && paddingBits > 0) {
869
+ accumulator >>= paddingBits;
870
+ accumulatorBits -= paddingBits;
871
+ }
872
+ while (accumulatorBits >= BYTE_BITS) {
873
+ accumulatorBits -= BYTE_BITS;
874
+ buffer[bufIdx++] = (accumulator >> accumulatorBits) & BYTE_MASK;
875
+ accumulator &= (1 << accumulatorBits) - 1;
876
+ }
877
+ }
878
+ }
879
+ else {
880
+ // 일반 룩업 경로
881
+ const lookup = this.dduBinaryLookup;
882
+ for (let i = 0; i < inputLen; i += chunkSize) {
883
+ const chunk = cleanedInput.slice(i, i + charLength);
884
+ const val = lookup.get(chunk);
885
+ if (val === undefined) {
886
+ throw new Error(`[Ddu64 decode] Invalid character "${chunk}" at ${i}`);
887
+ }
888
+ accumulator = (accumulator << bitLength) | val;
889
+ accumulatorBits += bitLength;
890
+ if (i + chunkSize >= inputLen && paddingBits > 0) {
891
+ accumulator >>= paddingBits;
892
+ accumulatorBits -= paddingBits;
893
+ }
894
+ while (accumulatorBits >= BYTE_BITS) {
895
+ accumulatorBits -= BYTE_BITS;
896
+ buffer[bufIdx++] = (accumulator >> accumulatorBits) & BYTE_MASK;
897
+ accumulator &= (1 << accumulatorBits) - 1;
898
+ }
899
+ }
900
+ }
901
+ }
902
+ else if (this.useAsciiLookup && this.fastAsciiLookup && charLength === 1) {
903
+ // 가변 길이 charset - ASCII 최적화 경로
904
+ const asciiLookup = this.fastAsciiLookup;
905
+ const maxVal = this.maxBinaryValue;
150
906
  for (let i = 0; i < inputLen; i += chunkSize) {
151
- const chunk = cleanedInput.slice(i, i + charLength);
152
- const val = lookup.get(chunk);
153
- if (val === undefined)
154
- throw new Error(`[Ddu64 decode] Invalid character "${chunk}" at ${i}`);
155
- if (val >= maxBinaryValue)
156
- throw new Error(`[Ddu64 decode] Value ${val} exceeds range`);
157
- accumulator = (accumulator << bitLength) | val;
907
+ const code1 = cleanedInput.charCodeAt(i);
908
+ const code2 = cleanedInput.charCodeAt(i + 1);
909
+ const v1 = code1 < 128 ? asciiLookup[code1] : -1;
910
+ const v2 = code2 < 128 ? asciiLookup[code2] : -1;
911
+ if (v1 < 0) {
912
+ throw new Error(`[Ddu64 decode] Invalid character "${cleanedInput[i]}" at ${i}`);
913
+ }
914
+ if (v2 < 0) {
915
+ throw new Error(`[Ddu64 decode] Invalid character "${cleanedInput[i + 1]}" at ${i + charLength}`);
916
+ }
917
+ const value = v1 * dduLength + v2;
918
+ if (value >= maxVal) {
919
+ throw new Error(`[Ddu64 decode] Value ${value} exceeds range`);
920
+ }
921
+ accumulator = (accumulator << bitLength) | value;
158
922
  accumulatorBits += bitLength;
159
- // 마지막 청크 패딩 비트 제거
160
923
  if (i + chunkSize >= inputLen && paddingBits > 0) {
161
924
  accumulator >>= paddingBits;
162
925
  accumulatorBits -= paddingBits;
163
926
  }
164
- while (accumulatorBits >= 8) {
165
- const shift = accumulatorBits - 8;
166
- buffer.push((accumulator >> shift) & 0xFF);
167
- accumulatorBits -= 8;
927
+ while (accumulatorBits >= BYTE_BITS) {
928
+ accumulatorBits -= BYTE_BITS;
929
+ buffer[bufIdx++] = (accumulator >> accumulatorBits) & BYTE_MASK;
168
930
  accumulator &= (1 << accumulatorBits) - 1;
169
931
  }
170
932
  }
171
933
  }
172
934
  else {
173
- const chunkSize = charLength * 2;
935
+ // 가변 길이 charset - 일반 룩업 경로
936
+ const lookup = this.dduBinaryLookup;
174
937
  for (let i = 0; i < inputLen; i += chunkSize) {
175
938
  const c1 = cleanedInput.slice(i, i + charLength);
176
939
  const c2 = cleanedInput.slice(i + charLength, i + chunkSize);
177
940
  const v1 = lookup.get(c1);
178
941
  const v2 = lookup.get(c2);
179
- if (v1 === undefined)
942
+ if (v1 === undefined) {
180
943
  throw new Error(`[Ddu64 decode] Invalid character "${c1}" at ${i}`);
181
- if (v2 === undefined)
944
+ }
945
+ if (v2 === undefined) {
182
946
  throw new Error(`[Ddu64 decode] Invalid character "${c2}" at ${i + charLength}`);
947
+ }
183
948
  const value = v1 * dduLength + v2;
184
- if (value >= maxBinaryValue)
949
+ if (value >= this.maxBinaryValue) {
185
950
  throw new Error(`[Ddu64 decode] Value ${value} exceeds range`);
951
+ }
186
952
  accumulator = (accumulator << bitLength) | value;
187
953
  accumulatorBits += bitLength;
188
954
  if (i + chunkSize >= inputLen && paddingBits > 0) {
189
955
  accumulator >>= paddingBits;
190
956
  accumulatorBits -= paddingBits;
191
957
  }
192
- while (accumulatorBits >= 8) {
193
- const shift = accumulatorBits - 8;
194
- buffer.push((accumulator >> shift) & 0xFF);
195
- accumulatorBits -= 8;
958
+ while (accumulatorBits >= BYTE_BITS) {
959
+ accumulatorBits -= BYTE_BITS;
960
+ buffer[bufIdx++] = (accumulator >> accumulatorBits) & BYTE_MASK;
196
961
  accumulator &= (1 << accumulatorBits) - 1;
197
962
  }
198
963
  }
199
964
  }
200
- return Buffer.from(buffer);
965
+ return Buffer.from(buffer.subarray(0, bufIdx));
201
966
  }
202
- // =========================================================================================
203
- // Safe Engine (BigInt)
204
- // Fallback for huge charsets (> 24 bits per chunk) where 32-bit integers overflow.
205
- // =========================================================================================
206
- encodeBigInt(bufferInput) {
207
- const resultParts = [];
967
+ // --------------------------------------------------------------------------
968
+ // 인코딩 (BigInt 모드 - 17비트 이상)
969
+ // --------------------------------------------------------------------------
970
+ /**
971
+ * BigInt를 사용한 대형 비트 인코딩
972
+ */
973
+ encodeBigInt(bufferInput, compress, encrypt) {
974
+ const inputLen = bufferInput.length;
975
+ if (inputLen === 0)
976
+ return "";
208
977
  const { dduChar, effectiveBitLength: bitLength } = this;
209
978
  const dduLength = dduChar.length;
210
- const bigBitLength = BigInt(bitLength);
979
+ const estimatedChunks = Math.ceil((inputLen * BYTE_BITS) / bitLength);
980
+ const estimatedSymbols = this.usePowerOfTwo
981
+ ? estimatedChunks
982
+ : estimatedChunks * 2;
983
+ const resultParts = new Array(estimatedSymbols + 3);
984
+ let resultIdx = 0;
211
985
  let accumulator = 0n;
212
986
  let accumulatorBits = 0;
213
987
  if (this.usePowerOfTwo) {
214
- for (const byte of bufferInput) {
215
- accumulator = (accumulator << 8n) | BigInt(byte);
216
- accumulatorBits += 8;
988
+ for (let i = 0; i < inputLen; i++) {
989
+ accumulator = (accumulator << 8n) | BigInt(bufferInput[i]);
990
+ accumulatorBits += BYTE_BITS;
217
991
  while (accumulatorBits >= bitLength) {
218
992
  const shift = accumulatorBits - bitLength;
219
- const value = accumulator >> BigInt(shift);
220
- resultParts.push(dduChar[Number(value)]);
221
- accumulator &= ((1n << BigInt(shift)) - 1n);
993
+ resultParts[resultIdx++] =
994
+ dduChar[Number(accumulator >> BigInt(shift))];
995
+ accumulator &= (1n << BigInt(shift)) - 1n;
222
996
  accumulatorBits -= bitLength;
223
997
  }
224
998
  }
225
999
  }
226
1000
  else {
227
- for (const byte of bufferInput) {
228
- accumulator = (accumulator << 8n) | BigInt(byte);
229
- accumulatorBits += 8;
1001
+ for (let i = 0; i < inputLen; i++) {
1002
+ accumulator = (accumulator << 8n) | BigInt(bufferInput[i]);
1003
+ accumulatorBits += BYTE_BITS;
230
1004
  while (accumulatorBits >= bitLength) {
231
1005
  const shift = accumulatorBits - bitLength;
232
- const value = accumulator >> BigInt(shift);
233
- const idx = Number(value);
234
- resultParts.push(dduChar[Math.floor(idx / dduLength)] + dduChar[idx % dduLength]);
235
- accumulator &= ((1n << BigInt(shift)) - 1n);
1006
+ const idx = Number(accumulator >> BigInt(shift));
1007
+ const div = Math.floor(idx / dduLength);
1008
+ resultParts[resultIdx++] = dduChar[div];
1009
+ resultParts[resultIdx++] = dduChar[idx - div * dduLength];
1010
+ accumulator &= (1n << BigInt(shift)) - 1n;
236
1011
  accumulatorBits -= bitLength;
237
1012
  }
238
1013
  }
239
1014
  }
1015
+ // 남은 비트 처리 (패딩)
240
1016
  if (accumulatorBits > 0) {
241
1017
  const paddingBits = bitLength - accumulatorBits;
242
1018
  const index = Number(accumulator << BigInt(paddingBits));
243
1019
  if (this.usePowerOfTwo) {
244
- resultParts.push(dduChar[index]);
1020
+ resultParts[resultIdx++] = dduChar[index];
245
1021
  }
246
1022
  else {
247
- resultParts.push(dduChar[Math.floor(index / dduLength)] + dduChar[index % dduLength]);
1023
+ const div = Math.floor(index / dduLength);
1024
+ resultParts[resultIdx++] = dduChar[div];
1025
+ resultParts[resultIdx++] = dduChar[index - div * dduLength];
248
1026
  }
249
- return resultParts.join("") + this.paddingChar + paddingBits.toString();
1027
+ resultParts[resultIdx++] = this.paddingChar;
1028
+ resultParts[resultIdx++] = (compress ? COMPRESS_MARKER : "") + (encrypt ? ENCRYPT_MARKER : "") + paddingBits.toString();
250
1029
  }
1030
+ else if (compress || encrypt) {
1031
+ resultParts[resultIdx++] = this.paddingChar;
1032
+ resultParts[resultIdx++] = (compress ? COMPRESS_MARKER : "") + (encrypt ? ENCRYPT_MARKER : "") + "0";
1033
+ }
1034
+ resultParts.length = resultIdx;
251
1035
  return resultParts.join("");
252
1036
  }
253
- decodeBigInt(input) {
254
- const { cleanedInput, paddingBits } = this.parsePaddingAndGetInput(input);
255
- const buffer = [];
256
- let accumulator = 0n;
257
- let accumulatorBits = 0;
258
- const { effectiveBitLength: bitLength, dduBinaryLookup: lookup } = this;
1037
+ // --------------------------------------------------------------------------
1038
+ // 디코딩 (BigInt 모드 - 17비트 이상)
1039
+ // --------------------------------------------------------------------------
1040
+ /**
1041
+ * BigInt를 사용한 대형 비트 디코딩
1042
+ */
1043
+ decodeBigInt(cleanedInput, paddingBits) {
1044
+ const inputLen = cleanedInput.length;
1045
+ if (inputLen === 0)
1046
+ return Buffer.alloc(0);
1047
+ const { effectiveBitLength: bitLength, dduBinaryLookup: lookup, charLength, } = this;
259
1048
  const bigBitLength = BigInt(bitLength);
260
1049
  const dduLength = this.dduChar.length;
261
- const chunkSize = this.usePowerOfTwo ? this.charLength : this.charLength * 2;
1050
+ const chunkSize = this.usePowerOfTwo ? charLength : charLength * 2;
1051
+ const numChunks = Math.ceil(inputLen / chunkSize);
1052
+ const estimatedBytes = Math.ceil((numChunks * bitLength - paddingBits) / BYTE_BITS);
1053
+ const buffer = new Uint8Array(estimatedBytes + 1);
1054
+ let bufIdx = 0;
1055
+ let accumulator = 0n;
1056
+ let accumulatorBits = 0;
262
1057
  if (this.usePowerOfTwo) {
263
- for (let i = 0; i < cleanedInput.length; i += chunkSize) {
264
- const chunk = cleanedInput.slice(i, i + this.charLength);
1058
+ for (let i = 0; i < inputLen; i += chunkSize) {
1059
+ const chunk = cleanedInput.slice(i, i + charLength);
265
1060
  const val = lookup.get(chunk);
266
- if (val === undefined)
1061
+ if (val === undefined) {
267
1062
  throw new Error(`[Ddu64 decode] Invalid character "${chunk}" at ${i}`);
1063
+ }
268
1064
  accumulator = (accumulator << bigBitLength) | BigInt(val);
269
1065
  accumulatorBits += bitLength;
270
- if (i + chunkSize >= cleanedInput.length && paddingBits > 0) {
1066
+ if (i + chunkSize >= inputLen && paddingBits > 0) {
271
1067
  accumulator >>= BigInt(paddingBits);
272
1068
  accumulatorBits -= paddingBits;
273
1069
  }
274
- while (accumulatorBits >= 8) {
275
- const shift = accumulatorBits - 8;
276
- buffer.push(Number((accumulator >> BigInt(shift)) & 0xffn));
277
- accumulator &= ((1n << BigInt(shift)) - 1n);
278
- accumulatorBits -= 8;
1070
+ while (accumulatorBits >= BYTE_BITS) {
1071
+ const shift = accumulatorBits - BYTE_BITS;
1072
+ buffer[bufIdx++] = Number((accumulator >> BigInt(shift)) & 0xffn);
1073
+ accumulator &= (1n << BigInt(shift)) - 1n;
1074
+ accumulatorBits -= BYTE_BITS;
279
1075
  }
280
1076
  }
281
1077
  }
282
1078
  else {
283
- for (let i = 0; i < cleanedInput.length; i += chunkSize) {
284
- const c1 = cleanedInput.slice(i, i + this.charLength);
285
- const c2 = cleanedInput.slice(i + this.charLength, i + chunkSize);
1079
+ for (let i = 0; i < inputLen; i += chunkSize) {
1080
+ const c1 = cleanedInput.slice(i, i + charLength);
1081
+ const c2 = cleanedInput.slice(i + charLength, i + chunkSize);
286
1082
  const v1 = lookup.get(c1);
287
1083
  const v2 = lookup.get(c2);
288
- if (v1 === undefined)
1084
+ if (v1 === undefined) {
289
1085
  throw new Error(`[Ddu64 decode] Invalid character "${c1}" at ${i}`);
290
- if (v2 === undefined)
291
- throw new Error(`[Ddu64 decode] Invalid character "${c2}" at ${i + this.charLength}`);
1086
+ }
1087
+ if (v2 === undefined) {
1088
+ throw new Error(`[Ddu64 decode] Invalid character "${c2}" at ${i + charLength}`);
1089
+ }
292
1090
  const value = v1 * dduLength + v2;
293
- if (value >= this.maxBinaryValue)
1091
+ if (value >= this.maxBinaryValue) {
294
1092
  throw new Error(`[Ddu64 decode] Value ${value} exceeds range`);
1093
+ }
295
1094
  accumulator = (accumulator << bigBitLength) | BigInt(value);
296
1095
  accumulatorBits += bitLength;
297
- if (i + chunkSize >= cleanedInput.length && paddingBits > 0) {
1096
+ if (i + chunkSize >= inputLen && paddingBits > 0) {
298
1097
  accumulator >>= BigInt(paddingBits);
299
1098
  accumulatorBits -= paddingBits;
300
1099
  }
301
- while (accumulatorBits >= 8) {
302
- const shift = accumulatorBits - 8;
303
- buffer.push(Number((accumulator >> BigInt(shift)) & 0xffn));
304
- accumulator &= ((1n << BigInt(shift)) - 1n);
305
- accumulatorBits -= 8;
1100
+ while (accumulatorBits >= BYTE_BITS) {
1101
+ const shift = accumulatorBits - BYTE_BITS;
1102
+ buffer[bufIdx++] = Number((accumulator >> BigInt(shift)) & 0xffn);
1103
+ accumulator &= (1n << BigInt(shift)) - 1n;
1104
+ accumulatorBits -= BYTE_BITS;
306
1105
  }
307
1106
  }
308
1107
  }
309
- return Buffer.from(buffer);
310
- }
311
- // =========================================================================================
312
- // Internal Helpers (Normalization & Validation)
313
- // =========================================================================================
314
- parsePaddingAndGetInput(input) {
315
- const padLen = this.paddingChar.length;
316
- if (input.length < padLen)
317
- return { cleanedInput: input, paddingBits: 0 };
318
- const padIdx = input.lastIndexOf(this.paddingChar);
319
- // 패딩 문자가 존재하고, 위치가 올바른지(chunk 단위) 확인
320
- if (padIdx >= 0 && padIdx % this.charLength === 0 && padIdx + padLen <= input.length) {
321
- const paddingSection = input.slice(padIdx + padLen);
322
- if (!paddingSection)
323
- throw new Error(`[Ddu64 decode] Invalid padding format. Missing padding length`);
324
- const paddingBits = parseInt(paddingSection, 10);
325
- if (isNaN(paddingBits) || paddingSection !== paddingBits.toString() || paddingBits < 0 || paddingBits >= this.effectiveBitLength) {
326
- throw new Error(`[Ddu64 decode] Invalid padding format. Got: "${paddingSection}"`);
327
- }
328
- return { cleanedInput: input.substring(0, padIdx), paddingBits };
329
- }
330
- return { cleanedInput: input, paddingBits: 0 };
1108
+ return Buffer.from(buffer.subarray(0, bufIdx));
331
1109
  }
1110
+ // --------------------------------------------------------------------------
1111
+ // Charset 초기화 메서드
1112
+ // --------------------------------------------------------------------------
332
1113
  /**
333
- * 입력된 CharSet검증하고 정리(Normalization)합니다.
334
- * 문제가 발생하면 옵션에 따라 Error를 던지거나 Fallback CharSet을 반환합니다.
1114
+ * Charset정규화합니다.
335
1115
  */
336
1116
  normalizeCharSet(current, shouldThrow, dduOptions) {
337
- let state = { ...current };
338
- // 재시도 루프 (Fallback 로직 포함)
339
- while (true) {
1117
+ // 1차 시도: 주어진 charset으로 정규화
1118
+ // 실패 1회만 fallback 시도 (동일 fallback을 반복해도 결과는 동일)
1119
+ const attempts = [current, null];
1120
+ for (const attempt of attempts) {
1121
+ const state = attempt ? { ...attempt } : this.getFallbackCharSet(dduOptions);
340
1122
  try {
341
- // 1. 중복 제거
342
- const uniqueChars = Array.from(new Set(state.charSet));
343
- if (uniqueChars.length !== state.charSet.length) {
1123
+ // 중복 문자 제거
1124
+ let charSet = state.charSet;
1125
+ let requiredLength = state.requiredLength;
1126
+ const uniqueChars = Array.from(new Set(charSet));
1127
+ if (uniqueChars.length !== charSet.length) {
344
1128
  if (shouldThrow) {
345
- const duplicates = state.charSet.filter((c, i) => state.charSet.indexOf(c) !== i);
1129
+ const duplicates = charSet.filter((c, i) => charSet.indexOf(c) !== i);
346
1130
  throw new Error(`[Ddu64 normalizeCharSet] Character set contains duplicate characters: [${[...new Set(duplicates)].join(", ")}]`);
347
1131
  }
348
- state.charSet = uniqueChars;
1132
+ charSet = uniqueChars;
349
1133
  if (!state.isPredefined)
350
- state.requiredLength = state.charSet.length;
1134
+ requiredLength = charSet.length;
1135
+ }
1136
+ // 문자 수 검증
1137
+ if (charSet.length < requiredLength) {
1138
+ throw new Error(`[Ddu64 normalizeCharSet] Insufficient characters. Required: ${requiredLength}, Has: ${charSet.length}`);
351
1139
  }
352
- // 2. 기본 조건 검사
353
- if (state.charSet.length < state.requiredLength)
354
- throw new Error(`[Ddu64 normalizeCharSet] Insufficient characters. Required: ${state.requiredLength}, Has: ${state.charSet.length}`);
355
- if (state.requiredLength < 2)
1140
+ if (requiredLength < 2) {
356
1141
  throw new Error(`[Ddu64 normalizeCharSet] At least 2 unique characters required.`);
357
- if (state.charSet.length === 0)
1142
+ }
1143
+ if (charSet.length === 0) {
358
1144
  throw new Error(`[Ddu64 normalizeCharSet] Empty charset.`);
359
- // 3. 문자 길이 일관성 검사
360
- const charLength = state.charSet[0].length;
361
- const invalidChar = state.charSet.find(c => c.length !== charLength);
1145
+ }
1146
+ // 문자 길이 일관성 검증
1147
+ const charLength = charSet[0].length;
1148
+ const invalidChar = charSet.find((c) => c.length !== charLength);
362
1149
  if (invalidChar) {
363
- if (shouldThrow)
1150
+ if (shouldThrow) {
364
1151
  throw new Error(`[Ddu64 normalizeCharSet] Inconsistent char length. Expected ${charLength}, found "${invalidChar}" (${invalidChar.length})`);
365
- // 필터링 후 재검증을 위해 예외 발생시켜 Fallback 또는 재시도 유도
366
- throw new Error("Filtered inconsistent chars (internal retry)");
1152
+ }
1153
+ continue; // fallback으로 재시도
367
1154
  }
368
- // 4. 패딩 충돌 검사
369
- if (state.padding.length !== charLength)
1155
+ // 패딩 검증
1156
+ if (state.padding.length !== charLength) {
370
1157
  throw new Error(`[Ddu64 normalizeCharSet] Padding length mismatch. Expected ${charLength}, got ${state.padding.length}`);
371
- if (state.charSet.includes(state.padding)) {
372
- if (shouldThrow)
1158
+ }
1159
+ if (charSet.includes(state.padding)) {
1160
+ if (shouldThrow) {
373
1161
  throw new Error(`[Ddu64 normalizeCharSet] Padding character "${state.padding}" conflicts with charset.`);
374
- state.charSet = state.charSet.filter(c => c !== state.padding);
1162
+ }
1163
+ charSet = charSet.filter((c) => c !== state.padding);
375
1164
  }
1165
+ // 불필요한 배열 복사 방지
1166
+ const finalSet = charSet.length === requiredLength
1167
+ ? charSet
1168
+ : charSet.slice(0, requiredLength);
376
1169
  return {
377
- charSet: state.charSet.slice(0, state.requiredLength),
1170
+ charSet: finalSet,
378
1171
  padding: state.padding,
379
1172
  charLength,
380
1173
  isPredefined: state.isPredefined,
381
1174
  };
382
1175
  }
383
1176
  catch (e) {
384
- // 사용자가 명시적으로 에러를 요청했거나, 복구 불가능한 에러인 경우
385
- if (shouldThrow && !e.message.includes("internal retry"))
1177
+ if (shouldThrow)
386
1178
  throw e;
387
- // 외에는 Fallback CharSet 사용
388
- state = this.getFallbackCharSet(dduOptions);
1179
+ // fallback 시도로 continue
389
1180
  }
390
1181
  }
1182
+ // 최종 fallback (이론상 도달 불가, 방어 코드)
1183
+ const fallback = this.getFallbackCharSet(dduOptions);
1184
+ return {
1185
+ charSet: fallback.charSet.slice(0, fallback.requiredLength),
1186
+ padding: fallback.padding,
1187
+ charLength: fallback.charSet[0]?.length ?? 1,
1188
+ isPredefined: true,
1189
+ };
391
1190
  }
1191
+ /**
1192
+ * 초기 charset을 결정합니다.
1193
+ */
392
1194
  resolveInitialCharSet(dduChar, paddingChar, dduOptions, shouldThrow) {
393
- // 내부 헬퍼: 길이와 옵션에 따라 최종 메타데이터 생성
394
1195
  const buildMeta = (set, padding, length, isPredefined) => {
395
1196
  const usePow2 = this.shouldUsePowerOfTwo(length, dduOptions?.usePowerOfTwo);
396
1197
  if (usePow2 && length > 0) {
397
1198
  const exponent = this.getLargestPowerOfTwoExponent(length);
398
1199
  const pow2Length = 1 << exponent;
399
- return { charSet: set.slice(0, pow2Length), padding, requiredLength: pow2Length, bitLength: exponent, isPredefined };
1200
+ const selected = set.length === pow2Length ? set : set.slice(0, pow2Length);
1201
+ return {
1202
+ charSet: selected,
1203
+ padding,
1204
+ requiredLength: pow2Length,
1205
+ bitLength: exponent,
1206
+ isPredefined,
1207
+ };
400
1208
  }
401
- return { charSet: set.slice(0, length), padding, requiredLength: length, bitLength: length > 0 ? this.getBitLength(length) : 0, isPredefined };
1209
+ const selected = set.length === length ? set : set.slice(0, length);
1210
+ return {
1211
+ charSet: selected,
1212
+ padding,
1213
+ requiredLength: length,
1214
+ bitLength: length > 0 ? this.getBitLength(length) : 0,
1215
+ isPredefined,
1216
+ };
402
1217
  };
403
1218
  try {
404
1219
  const finalDduChar = dduChar ?? dduOptions?.dduChar;
405
1220
  const finalPadding = paddingChar ?? dduOptions?.paddingChar;
406
- // Case A: 사용자 제공 CharSet
407
1221
  if (finalDduChar) {
408
- if (!finalPadding)
1222
+ if (!finalPadding) {
409
1223
  throw new Error(`[Ddu64 Constructor] paddingChar is required when dduChar is provided.`);
410
- const arr = typeof finalDduChar === "string" ? [...finalDduChar.trim()] : [...finalDduChar];
1224
+ }
1225
+ const arr = typeof finalDduChar === "string"
1226
+ ? [...finalDduChar.trim()]
1227
+ : [...finalDduChar];
411
1228
  if (shouldThrow) {
412
1229
  const uniqueSize = new Set(arr).size;
413
1230
  if (uniqueSize !== arr.length) {
@@ -416,12 +1233,14 @@ export class Ddu64 extends BaseDdu {
416
1233
  }
417
1234
  }
418
1235
  const reqLen = dduOptions?.requiredLength ?? arr.length;
419
- if (arr.length < reqLen)
1236
+ if (arr.length < reqLen) {
420
1237
  throw new Error(`[Ddu64 Constructor] Insufficient characters.`);
1238
+ }
421
1239
  return buildMeta(arr, finalPadding, reqLen, false);
422
1240
  }
423
- // Case B: 심볼(Enum)로 지정된 CharSet
424
- const symbol = dduOptions?.dduSetSymbol ?? dduDefaultConstructorOptions.dduSetSymbol ?? DduSetSymbol.DDU;
1241
+ const symbol = dduOptions?.dduSetSymbol ??
1242
+ dduDefaultConstructorOptions.dduSetSymbol ??
1243
+ DduSetSymbol.DDU;
425
1244
  const cs = this.getCharSetOrThrow(symbol);
426
1245
  return buildMeta(cs.charSet, cs.paddingChar, cs.maxRequiredLength, true);
427
1246
  }
@@ -431,26 +1250,72 @@ export class Ddu64 extends BaseDdu {
431
1250
  return this.getFallbackCharSet(dduOptions);
432
1251
  }
433
1252
  }
1253
+ /**
1254
+ * Fallback charset을 반환합니다.
1255
+ */
434
1256
  getFallbackCharSet(dduOptions) {
435
- const symbol = dduOptions?.dduSetSymbol ?? dduDefaultConstructorOptions.dduSetSymbol ?? DduSetSymbol.ONECHARSET;
1257
+ const symbol = dduOptions?.dduSetSymbol ??
1258
+ dduDefaultConstructorOptions.dduSetSymbol ??
1259
+ DduSetSymbol.ONECHARSET;
436
1260
  const cs = getCharSet(symbol) ?? getCharSet(DduSetSymbol.ONECHARSET);
437
1261
  if (!cs)
438
1262
  throw new Error(`Critical: No fallback CharSet available`);
439
- return { charSet: cs.charSet, padding: cs.paddingChar, requiredLength: cs.maxRequiredLength, bitLength: cs.bitLength, isPredefined: true };
1263
+ return {
1264
+ charSet: cs.charSet,
1265
+ padding: cs.paddingChar,
1266
+ requiredLength: cs.maxRequiredLength,
1267
+ bitLength: cs.bitLength,
1268
+ isPredefined: true,
1269
+ };
440
1270
  }
1271
+ /**
1272
+ * 2의 제곱수 사용 여부를 결정합니다.
1273
+ */
441
1274
  shouldUsePowerOfTwo(length, preference) {
442
1275
  if (preference !== undefined)
443
1276
  return preference ? length > 0 : false;
444
1277
  return length > 0 && (length & (length - 1)) === 0;
445
1278
  }
1279
+ /**
1280
+ * charset을 가져오거나 에러를 발생시킵니다.
1281
+ */
446
1282
  getCharSetOrThrow(symbol) {
447
1283
  const cs = getCharSet(symbol);
448
1284
  if (!cs)
449
1285
  throw new Error(`CharSet with symbol ${symbol} not found`);
450
1286
  return cs;
451
1287
  }
1288
+ /**
1289
+ * URL-Safe 모드 시 charset/padding이 역변환 대상 문자를 포함하지 않는지 검증합니다.
1290
+ * 역변환 대상 문자("-", "_", ".")가 charset이나 padding에 있으면
1291
+ * fromUrlSafe 시 해당 문자가 "+", "/", "="로 변환되어 데이터가 손상됩니다.
1292
+ *
1293
+ * @returns URL-Safe 모드를 활성화해도 안전한 경우 true
1294
+ */
1295
+ isUrlSafeCompatible(charSet, paddingChar, shouldThrow) {
1296
+ const conflictChars = Object.keys(URL_SAFE_REVERSE_MAP); // ["-", "_", "."]
1297
+ for (const ch of conflictChars) {
1298
+ for (const c of charSet) {
1299
+ if (c.includes(ch)) {
1300
+ const msg = `[Ddu64 Constructor] URL-Safe mode conflict: charset character "${c}" contains "${ch}" which would be transformed to "${URL_SAFE_REVERSE_MAP[ch]}" during decoding.`;
1301
+ if (shouldThrow)
1302
+ throw new Error(msg);
1303
+ return false;
1304
+ }
1305
+ }
1306
+ if (paddingChar.includes(ch)) {
1307
+ const msg = `[Ddu64 Constructor] URL-Safe mode conflict: padding character "${paddingChar}" contains "${ch}" which would be transformed to "${URL_SAFE_REVERSE_MAP[ch]}" during decoding.`;
1308
+ if (shouldThrow)
1309
+ throw new Error(msg);
1310
+ return false;
1311
+ }
1312
+ }
1313
+ return true;
1314
+ }
1315
+ /**
1316
+ * 커스텀 charset의 조합 중복을 검증합니다.
1317
+ */
452
1318
  validateCombinationDuplicates(charSet, paddingChar, requiredLength) {
453
- // 작은 크기의 단일 문자 집합에 대해서만 '조합 충돌' 검사를 수행 (안전장치)
454
1319
  if (charSet[0].length !== 1 || requiredLength > 256)
455
1320
  return;
456
1321
  const limit = Math.min(charSet.length, requiredLength);
@@ -461,11 +1326,15 @@ export class Ddu64 extends BaseDdu {
461
1326
  throw new Error(`Combination conflict: ${context}`);
462
1327
  combinations.add(s);
463
1328
  };
464
- targetChars.forEach(c => combinations.add(c));
1329
+ for (let i = 0; i < targetChars.length; i++) {
1330
+ combinations.add(targetChars[i]);
1331
+ }
465
1332
  combinations.add(paddingChar);
466
- for (const c1 of targetChars) {
467
- for (const c2 of targetChars)
468
- add(c1 + c2, `"${c1}" + "${c2}"`);
1333
+ for (let i = 0; i < targetChars.length; i++) {
1334
+ const c1 = targetChars[i];
1335
+ for (let j = 0; j < targetChars.length; j++) {
1336
+ add(c1 + targetChars[j], `"${c1}" + "${targetChars[j]}"`);
1337
+ }
469
1338
  add(c1 + paddingChar, `"${c1}" + padding`);
470
1339
  add(paddingChar + c1, `padding + "${c1}"`);
471
1340
  }