@ddunigma/node 2.0.0 → 2.0.2

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.
@@ -1,27 +1,103 @@
1
- import { BaseDdu } from "../base/BaseDdu";
2
- import { DduSetSymbol, dduDefaultConstructorOptions, } from "../types";
3
- import { getCharSet } from "../charSets";
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
+ */
4
42
  export class Ddu64 extends BaseDdu {
5
- // =========================================================================================
6
- // Properties
7
- // =========================================================================================
43
+ // --------------------------------------------------------------------------
44
+ // 멤버 변수
45
+ // --------------------------------------------------------------------------
46
+ /** 인코딩에 사용할 문자 배열 */
8
47
  dduChar;
48
+ /** 패딩 문자 */
9
49
  paddingChar;
50
+ /** 각 charset 문자의 길이 */
10
51
  charLength;
52
+ /** 비트 길이 (log2) */
11
53
  bitLength;
54
+ /** 2의 제곱수 charset 여부 */
12
55
  usePowerOfTwo;
56
+ /** 문자열 인코딩 방식 */
13
57
  encoding;
58
+ /** 기본 압축 사용 여부 */
59
+ defaultCompress;
60
+ /** 기본 최대 디코딩 바이트 수 */
61
+ defaultMaxDecodedBytes;
62
+ /** 기본 최대 압축해제 바이트 수 */
63
+ defaultMaxDecompressedBytes;
64
+ /** 문자 → 인덱스 역방향 룩업 맵 */
14
65
  dduBinaryLookup = new Map();
66
+ /** 미리 정의된 charset 사용 여부 */
15
67
  isPredefinedCharSet;
68
+ /** 실제 사용되는 비트 길이 */
16
69
  effectiveBitLength;
70
+ /** 최대 바이너리 값 */
17
71
  maxBinaryValue;
18
- // =========================================================================================
19
- // Constructor
20
- // =========================================================================================
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
+ */
21
97
  constructor(dduChar, paddingChar, dduOptions) {
22
98
  super();
23
99
  const shouldThrow = dduOptions?.useBuildErrorReturn ?? false;
24
- // 1. CharSet 초기화 및 검증
100
+ // charset 초기화
25
101
  const initial = this.resolveInitialCharSet(dduChar, paddingChar, dduOptions, shouldThrow);
26
102
  const normalized = this.normalizeCharSet(initial, shouldThrow, dduOptions);
27
103
  this.dduChar = normalized.charSet;
@@ -29,48 +105,136 @@ export class Ddu64 extends BaseDdu {
29
105
  this.charLength = normalized.charLength;
30
106
  this.isPredefinedCharSet = normalized.isPredefined;
31
107
  this.encoding = dduOptions?.encoding ?? this.defaultEncoding;
32
- // 2. 비트 연산 상수 계산
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
+ // 비트 길이 계산
33
113
  const dduLength = this.dduChar.length;
34
- this.usePowerOfTwo = dduLength > 0 && (dduLength & (dduLength - 1)) === 0;
114
+ this.usePowerOfTwo =
115
+ dduLength > 0 && (dduLength & (dduLength - 1)) === 0;
35
116
  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용)
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
+ // 역방향 룩업 맵 생성
40
145
  for (let i = 0; i < dduLength; i++) {
41
146
  this.dduBinaryLookup.set(this.dduChar[i], i);
42
147
  }
43
- // 4. 안전성 검사
148
+ // 커스텀 charset 중복 조합 검증
44
149
  if (this.charLength === 1 && !this.isPredefinedCharSet) {
45
150
  this.validateCombinationDuplicates(this.dduChar, this.paddingChar, dduLength);
46
151
  }
47
152
  }
48
- // =========================================================================================
49
- // Public API
50
- // =========================================================================================
153
+ // --------------------------------------------------------------------------
154
+ // 공개 메서드
155
+ // --------------------------------------------------------------------------
51
156
  /**
52
- * 데이터를 DDU 포맷으로 인코딩합니다.
53
- * 성능을 위해 24비트 이하는 Fast Path(number 연산)를 사용합니다.
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 });
54
167
  */
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);
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);
59
184
  }
60
- return this.encodeBigInt(bufferInput);
185
+ return this.effectiveBitLength <= MAX_FAST_BITS
186
+ ? this.encodeFast(compressedBuffer, true)
187
+ : this.encodeBigInt(compressedBuffer, true);
61
188
  }
62
189
  /**
63
- * DDU 포맷 문자열을 버퍼로 디코딩합니다.
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 크기 제한 초과 시
64
201
  */
65
- decodeToBuffer(input, _options) {
66
- if (this.effectiveBitLength <= 24) {
67
- return this.decodeFast(input);
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`);
68
210
  }
69
- return this.decodeBigInt(input);
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);
70
220
  }
71
- decode(input, _options) {
72
- return this.decodeToBuffer(input, _options).toString(this.encoding);
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);
73
232
  }
233
+ /**
234
+ * 현재 인코더의 charset 정보를 반환합니다.
235
+ *
236
+ * @returns charset 설정 정보 객체
237
+ */
74
238
  getCharSetInfo() {
75
239
  return {
76
240
  charSet: [...this.dduChar],
@@ -79,266 +243,533 @@ export class Ddu64 extends BaseDdu {
79
243
  bitLength: this.bitLength,
80
244
  usePowerOfTwo: this.usePowerOfTwo,
81
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;
82
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 };
83
444
  }
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 = [];
445
+ // --------------------------------------------------------------------------
446
+ // 인코딩 (Fast 모드 - 16비트 이하)
447
+ // --------------------------------------------------------------------------
448
+ /**
449
+ * 일반 정수 연산을 사용한 빠른 인코딩
450
+ */
451
+ encodeFast(bufferInput, compress) {
452
+ const inputLen = bufferInput.length;
453
+ if (inputLen === 0)
454
+ return "";
90
455
  const { dduChar, effectiveBitLength: bitLength, paddingChar } = this;
91
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;
92
464
  let accumulator = 0;
93
465
  let accumulatorBits = 0;
94
- // Loop Unswitching: 조건문을 루프 밖으로 빼서 CPU 분기 예측 효율 향상
95
466
  if (this.usePowerOfTwo) {
96
- for (const byte of bufferInput) {
97
- accumulator = (accumulator << 8) | byte;
98
- accumulatorBits += 8;
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;
99
471
  while (accumulatorBits >= bitLength) {
100
- const shift = accumulatorBits - bitLength;
101
- const index = accumulator >> shift;
102
- resultParts.push(dduChar[index]);
103
472
  accumulatorBits -= bitLength;
473
+ resultParts[resultIdx++] =
474
+ dduChar[(accumulator >> accumulatorBits) & mask];
104
475
  accumulator &= (1 << accumulatorBits) - 1;
105
476
  }
106
477
  }
107
478
  }
108
479
  else {
109
- for (const byte of bufferInput) {
110
- accumulator = (accumulator << 8) | byte;
111
- accumulatorBits += 8;
480
+ for (let i = 0; i < inputLen; i++) {
481
+ accumulator = (accumulator << BYTE_BITS) | bufferInput[i];
482
+ accumulatorBits += BYTE_BITS;
112
483
  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
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];
120
489
  accumulator &= (1 << accumulatorBits) - 1;
121
490
  }
122
491
  }
123
492
  }
124
- // 남은 비트 패딩 처리
493
+ // 남은 비트 처리 (패딩)
125
494
  if (accumulatorBits > 0) {
126
495
  const paddingBits = bitLength - accumulatorBits;
127
496
  const index = accumulator << paddingBits;
128
497
  if (this.usePowerOfTwo) {
129
- resultParts.push(dduChar[index]);
498
+ resultParts[resultIdx++] = dduChar[index];
130
499
  }
131
500
  else {
132
501
  const div = (index / dduLength) | 0;
133
- const mod = index % dduLength;
134
- resultParts.push(dduChar[div] + dduChar[mod]);
502
+ resultParts[resultIdx++] = dduChar[div];
503
+ resultParts[resultIdx++] = dduChar[index - div * dduLength];
135
504
  }
136
- return resultParts.join("") + paddingChar + paddingBits.toString();
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";
137
513
  }
514
+ resultParts.length = resultIdx;
138
515
  return resultParts.join("");
139
516
  }
140
- decodeFast(input) {
141
- const { cleanedInput, paddingBits } = this.parsePaddingAndGetInput(input);
517
+ // --------------------------------------------------------------------------
518
+ // 디코딩 (Fast 모드 - 16비트 이하)
519
+ // --------------------------------------------------------------------------
520
+ /**
521
+ * 일반 정수 연산을 사용한 빠른 디코딩
522
+ */
523
+ decodeFast(cleanedInput, paddingBits) {
142
524
  const inputLen = cleanedInput.length;
143
- const buffer = [];
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;
144
534
  let accumulator = 0;
145
535
  let accumulatorBits = 0;
146
- const { effectiveBitLength: bitLength, dduBinaryLookup: lookup, charLength, maxBinaryValue } = this;
147
- const dduLength = this.dduChar.length;
148
536
  if (this.usePowerOfTwo) {
149
- const chunkSize = charLength;
150
- 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;
158
- accumulatorBits += bitLength;
159
- // 마지막 청크 패딩 비트 제거
160
- if (i + chunkSize >= inputLen && paddingBits > 0) {
161
- accumulator >>= paddingBits;
162
- accumulatorBits -= paddingBits;
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
+ }
163
557
  }
164
- while (accumulatorBits >= 8) {
165
- const shift = accumulatorBits - 8;
166
- buffer.push((accumulator >> shift) & 0xFF);
167
- accumulatorBits -= 8;
168
- accumulator &= (1 << accumulatorBits) - 1;
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
+ }
169
579
  }
170
580
  }
171
581
  }
172
582
  else {
173
- const chunkSize = charLength * 2;
583
+ // 가변 길이 charset
584
+ const lookup = this.dduBinaryLookup;
174
585
  for (let i = 0; i < inputLen; i += chunkSize) {
175
586
  const c1 = cleanedInput.slice(i, i + charLength);
176
587
  const c2 = cleanedInput.slice(i + charLength, i + chunkSize);
177
588
  const v1 = lookup.get(c1);
178
589
  const v2 = lookup.get(c2);
179
- if (v1 === undefined)
590
+ if (v1 === undefined) {
180
591
  throw new Error(`[Ddu64 decode] Invalid character "${c1}" at ${i}`);
181
- if (v2 === undefined)
592
+ }
593
+ if (v2 === undefined) {
182
594
  throw new Error(`[Ddu64 decode] Invalid character "${c2}" at ${i + charLength}`);
595
+ }
183
596
  const value = v1 * dduLength + v2;
184
- if (value >= maxBinaryValue)
597
+ if (value >= this.maxBinaryValue) {
185
598
  throw new Error(`[Ddu64 decode] Value ${value} exceeds range`);
599
+ }
186
600
  accumulator = (accumulator << bitLength) | value;
187
601
  accumulatorBits += bitLength;
188
602
  if (i + chunkSize >= inputLen && paddingBits > 0) {
189
603
  accumulator >>= paddingBits;
190
604
  accumulatorBits -= paddingBits;
191
605
  }
192
- while (accumulatorBits >= 8) {
193
- const shift = accumulatorBits - 8;
194
- buffer.push((accumulator >> shift) & 0xFF);
195
- accumulatorBits -= 8;
606
+ while (accumulatorBits >= BYTE_BITS) {
607
+ accumulatorBits -= BYTE_BITS;
608
+ buffer[bufIdx++] = (accumulator >> accumulatorBits) & BYTE_MASK;
196
609
  accumulator &= (1 << accumulatorBits) - 1;
197
610
  }
198
611
  }
199
612
  }
200
- return Buffer.from(buffer);
613
+ return Buffer.from(buffer.subarray(0, bufIdx));
201
614
  }
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 = [];
615
+ // --------------------------------------------------------------------------
616
+ // 인코딩 (BigInt 모드 - 17비트 이상)
617
+ // --------------------------------------------------------------------------
618
+ /**
619
+ * BigInt를 사용한 대형 비트 인코딩
620
+ */
621
+ encodeBigInt(bufferInput, compress) {
622
+ const inputLen = bufferInput.length;
623
+ if (inputLen === 0)
624
+ return "";
208
625
  const { dduChar, effectiveBitLength: bitLength } = this;
209
626
  const dduLength = dduChar.length;
210
- const bigBitLength = BigInt(bitLength);
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;
211
633
  let accumulator = 0n;
212
634
  let accumulatorBits = 0;
213
635
  if (this.usePowerOfTwo) {
214
- for (const byte of bufferInput) {
215
- accumulator = (accumulator << 8n) | BigInt(byte);
216
- accumulatorBits += 8;
636
+ for (let i = 0; i < inputLen; i++) {
637
+ accumulator = (accumulator << 8n) | BigInt(bufferInput[i]);
638
+ accumulatorBits += BYTE_BITS;
217
639
  while (accumulatorBits >= bitLength) {
218
640
  const shift = accumulatorBits - bitLength;
219
- const value = accumulator >> BigInt(shift);
220
- resultParts.push(dduChar[Number(value)]);
221
- accumulator &= ((1n << BigInt(shift)) - 1n);
641
+ resultParts[resultIdx++] =
642
+ dduChar[Number(accumulator >> BigInt(shift))];
643
+ accumulator &= (1n << BigInt(shift)) - 1n;
222
644
  accumulatorBits -= bitLength;
223
645
  }
224
646
  }
225
647
  }
226
648
  else {
227
- for (const byte of bufferInput) {
228
- accumulator = (accumulator << 8n) | BigInt(byte);
229
- accumulatorBits += 8;
649
+ for (let i = 0; i < inputLen; i++) {
650
+ accumulator = (accumulator << 8n) | BigInt(bufferInput[i]);
651
+ accumulatorBits += BYTE_BITS;
230
652
  while (accumulatorBits >= bitLength) {
231
653
  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);
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;
236
659
  accumulatorBits -= bitLength;
237
660
  }
238
661
  }
239
662
  }
663
+ // 남은 비트 처리 (패딩)
240
664
  if (accumulatorBits > 0) {
241
665
  const paddingBits = bitLength - accumulatorBits;
242
666
  const index = Number(accumulator << BigInt(paddingBits));
243
667
  if (this.usePowerOfTwo) {
244
- resultParts.push(dduChar[index]);
668
+ resultParts[resultIdx++] = dduChar[index];
245
669
  }
246
670
  else {
247
- resultParts.push(dduChar[Math.floor(index / dduLength)] + dduChar[index % dduLength]);
671
+ const div = Math.floor(index / dduLength);
672
+ resultParts[resultIdx++] = dduChar[div];
673
+ resultParts[resultIdx++] = dduChar[index - div * dduLength];
248
674
  }
249
- return resultParts.join("") + this.paddingChar + paddingBits.toString();
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";
250
683
  }
684
+ resultParts.length = resultIdx;
251
685
  return resultParts.join("");
252
686
  }
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;
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;
259
698
  const bigBitLength = BigInt(bitLength);
260
699
  const dduLength = this.dduChar.length;
261
- const chunkSize = this.usePowerOfTwo ? this.charLength : this.charLength * 2;
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;
262
707
  if (this.usePowerOfTwo) {
263
- for (let i = 0; i < cleanedInput.length; i += chunkSize) {
264
- const chunk = cleanedInput.slice(i, i + this.charLength);
708
+ for (let i = 0; i < inputLen; i += chunkSize) {
709
+ const chunk = cleanedInput.slice(i, i + charLength);
265
710
  const val = lookup.get(chunk);
266
- if (val === undefined)
711
+ if (val === undefined) {
267
712
  throw new Error(`[Ddu64 decode] Invalid character "${chunk}" at ${i}`);
713
+ }
268
714
  accumulator = (accumulator << bigBitLength) | BigInt(val);
269
715
  accumulatorBits += bitLength;
270
- if (i + chunkSize >= cleanedInput.length && paddingBits > 0) {
716
+ if (i + chunkSize >= inputLen && paddingBits > 0) {
271
717
  accumulator >>= BigInt(paddingBits);
272
718
  accumulatorBits -= paddingBits;
273
719
  }
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;
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;
279
725
  }
280
726
  }
281
727
  }
282
728
  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);
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);
286
732
  const v1 = lookup.get(c1);
287
733
  const v2 = lookup.get(c2);
288
- if (v1 === undefined)
734
+ if (v1 === undefined) {
289
735
  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}`);
736
+ }
737
+ if (v2 === undefined) {
738
+ throw new Error(`[Ddu64 decode] Invalid character "${c2}" at ${i + charLength}`);
739
+ }
292
740
  const value = v1 * dduLength + v2;
293
- if (value >= this.maxBinaryValue)
741
+ if (value >= this.maxBinaryValue) {
294
742
  throw new Error(`[Ddu64 decode] Value ${value} exceeds range`);
743
+ }
295
744
  accumulator = (accumulator << bigBitLength) | BigInt(value);
296
745
  accumulatorBits += bitLength;
297
- if (i + chunkSize >= cleanedInput.length && paddingBits > 0) {
746
+ if (i + chunkSize >= inputLen && paddingBits > 0) {
298
747
  accumulator >>= BigInt(paddingBits);
299
748
  accumulatorBits -= paddingBits;
300
749
  }
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;
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;
306
755
  }
307
756
  }
308
757
  }
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 };
758
+ return Buffer.from(buffer.subarray(0, bufIdx));
331
759
  }
760
+ // --------------------------------------------------------------------------
761
+ // Charset 초기화 메서드
762
+ // --------------------------------------------------------------------------
332
763
  /**
333
- * 입력된 CharSet검증하고 정리(Normalization)합니다.
334
- * 문제가 발생하면 옵션에 따라 Error를 던지거나 Fallback CharSet을 반환합니다.
764
+ * Charset정규화합니다.
335
765
  */
336
766
  normalizeCharSet(current, shouldThrow, dduOptions) {
337
767
  let state = { ...current };
338
- // 재시도 루프 (Fallback 로직 포함)
339
- while (true) {
768
+ let retryCount = 0;
769
+ const maxRetries = 3;
770
+ while (retryCount < maxRetries) {
340
771
  try {
341
- // 1. 중복 제거
772
+ // 중복 문자 제거
342
773
  const uniqueChars = Array.from(new Set(state.charSet));
343
774
  if (uniqueChars.length !== state.charSet.length) {
344
775
  if (shouldThrow) {
@@ -349,65 +780,99 @@ export class Ddu64 extends BaseDdu {
349
780
  if (!state.isPredefined)
350
781
  state.requiredLength = state.charSet.length;
351
782
  }
352
- // 2. 기본 조건 검사
353
- if (state.charSet.length < state.requiredLength)
783
+ // 문자 검증
784
+ if (state.charSet.length < state.requiredLength) {
354
785
  throw new Error(`[Ddu64 normalizeCharSet] Insufficient characters. Required: ${state.requiredLength}, Has: ${state.charSet.length}`);
355
- if (state.requiredLength < 2)
786
+ }
787
+ if (state.requiredLength < 2) {
356
788
  throw new Error(`[Ddu64 normalizeCharSet] At least 2 unique characters required.`);
357
- if (state.charSet.length === 0)
789
+ }
790
+ if (state.charSet.length === 0) {
358
791
  throw new Error(`[Ddu64 normalizeCharSet] Empty charset.`);
359
- // 3. 문자 길이 일관성 검사
792
+ }
793
+ // 문자 길이 일관성 검증
360
794
  const charLength = state.charSet[0].length;
361
- const invalidChar = state.charSet.find(c => c.length !== charLength);
795
+ const invalidChar = state.charSet.find((c) => c.length !== charLength);
362
796
  if (invalidChar) {
363
- if (shouldThrow)
797
+ if (shouldThrow) {
364
798
  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)");
799
+ }
800
+ throw new Error("internal retry");
367
801
  }
368
- // 4. 패딩 충돌 검사
369
- if (state.padding.length !== charLength)
802
+ // 패딩 검증
803
+ if (state.padding.length !== charLength) {
370
804
  throw new Error(`[Ddu64 normalizeCharSet] Padding length mismatch. Expected ${charLength}, got ${state.padding.length}`);
805
+ }
371
806
  if (state.charSet.includes(state.padding)) {
372
- if (shouldThrow)
807
+ if (shouldThrow) {
373
808
  throw new Error(`[Ddu64 normalizeCharSet] Padding character "${state.padding}" conflicts with charset.`);
374
- state.charSet = state.charSet.filter(c => c !== state.padding);
809
+ }
810
+ state.charSet = state.charSet.filter((c) => c !== state.padding);
375
811
  }
812
+ // 불필요한 배열 복사 방지
813
+ const finalSet = state.charSet.length === state.requiredLength
814
+ ? state.charSet
815
+ : state.charSet.slice(0, state.requiredLength);
376
816
  return {
377
- charSet: state.charSet.slice(0, state.requiredLength),
817
+ charSet: finalSet,
378
818
  padding: state.padding,
379
819
  charLength,
380
820
  isPredefined: state.isPredefined,
381
821
  };
382
822
  }
383
823
  catch (e) {
384
- // 사용자가 명시적으로 에러를 요청했거나, 복구 불가능한 에러인 경우
385
824
  if (shouldThrow && !e.message.includes("internal retry"))
386
825
  throw e;
387
- // 그 외에는 Fallback CharSet 사용
388
826
  state = this.getFallbackCharSet(dduOptions);
827
+ retryCount++;
389
828
  }
390
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
+ };
391
838
  }
839
+ /**
840
+ * 초기 charset을 결정합니다.
841
+ */
392
842
  resolveInitialCharSet(dduChar, paddingChar, dduOptions, shouldThrow) {
393
- // 내부 헬퍼: 길이와 옵션에 따라 최종 메타데이터 생성
394
843
  const buildMeta = (set, padding, length, isPredefined) => {
395
844
  const usePow2 = this.shouldUsePowerOfTwo(length, dduOptions?.usePowerOfTwo);
396
845
  if (usePow2 && length > 0) {
397
846
  const exponent = this.getLargestPowerOfTwoExponent(length);
398
847
  const pow2Length = 1 << exponent;
399
- return { charSet: set.slice(0, pow2Length), padding, requiredLength: pow2Length, bitLength: exponent, isPredefined };
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
+ };
400
856
  }
401
- return { charSet: set.slice(0, length), padding, requiredLength: length, bitLength: length > 0 ? this.getBitLength(length) : 0, isPredefined };
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
+ };
402
865
  };
403
866
  try {
404
867
  const finalDduChar = dduChar ?? dduOptions?.dduChar;
405
868
  const finalPadding = paddingChar ?? dduOptions?.paddingChar;
406
- // Case A: 사용자 제공 CharSet
407
869
  if (finalDduChar) {
408
- if (!finalPadding)
870
+ if (!finalPadding) {
409
871
  throw new Error(`[Ddu64 Constructor] paddingChar is required when dduChar is provided.`);
410
- const arr = typeof finalDduChar === "string" ? [...finalDduChar.trim()] : [...finalDduChar];
872
+ }
873
+ const arr = typeof finalDduChar === "string"
874
+ ? [...finalDduChar.trim()]
875
+ : [...finalDduChar];
411
876
  if (shouldThrow) {
412
877
  const uniqueSize = new Set(arr).size;
413
878
  if (uniqueSize !== arr.length) {
@@ -416,12 +881,14 @@ export class Ddu64 extends BaseDdu {
416
881
  }
417
882
  }
418
883
  const reqLen = dduOptions?.requiredLength ?? arr.length;
419
- if (arr.length < reqLen)
884
+ if (arr.length < reqLen) {
420
885
  throw new Error(`[Ddu64 Constructor] Insufficient characters.`);
886
+ }
421
887
  return buildMeta(arr, finalPadding, reqLen, false);
422
888
  }
423
- // Case B: 심볼(Enum)로 지정된 CharSet
424
- const symbol = dduOptions?.dduSetSymbol ?? dduDefaultConstructorOptions.dduSetSymbol ?? DduSetSymbol.DDU;
889
+ const symbol = dduOptions?.dduSetSymbol ??
890
+ dduDefaultConstructorOptions.dduSetSymbol ??
891
+ DduSetSymbol.DDU;
425
892
  const cs = this.getCharSetOrThrow(symbol);
426
893
  return buildMeta(cs.charSet, cs.paddingChar, cs.maxRequiredLength, true);
427
894
  }
@@ -431,26 +898,45 @@ export class Ddu64 extends BaseDdu {
431
898
  return this.getFallbackCharSet(dduOptions);
432
899
  }
433
900
  }
901
+ /**
902
+ * Fallback charset을 반환합니다.
903
+ */
434
904
  getFallbackCharSet(dduOptions) {
435
- const symbol = dduOptions?.dduSetSymbol ?? dduDefaultConstructorOptions.dduSetSymbol ?? DduSetSymbol.ONECHARSET;
905
+ const symbol = dduOptions?.dduSetSymbol ??
906
+ dduDefaultConstructorOptions.dduSetSymbol ??
907
+ DduSetSymbol.ONECHARSET;
436
908
  const cs = getCharSet(symbol) ?? getCharSet(DduSetSymbol.ONECHARSET);
437
909
  if (!cs)
438
910
  throw new Error(`Critical: No fallback CharSet available`);
439
- return { charSet: cs.charSet, padding: cs.paddingChar, requiredLength: cs.maxRequiredLength, bitLength: cs.bitLength, isPredefined: true };
911
+ return {
912
+ charSet: cs.charSet,
913
+ padding: cs.paddingChar,
914
+ requiredLength: cs.maxRequiredLength,
915
+ bitLength: cs.bitLength,
916
+ isPredefined: true,
917
+ };
440
918
  }
919
+ /**
920
+ * 2의 제곱수 사용 여부를 결정합니다.
921
+ */
441
922
  shouldUsePowerOfTwo(length, preference) {
442
923
  if (preference !== undefined)
443
924
  return preference ? length > 0 : false;
444
925
  return length > 0 && (length & (length - 1)) === 0;
445
926
  }
927
+ /**
928
+ * charset을 가져오거나 에러를 발생시킵니다.
929
+ */
446
930
  getCharSetOrThrow(symbol) {
447
931
  const cs = getCharSet(symbol);
448
932
  if (!cs)
449
933
  throw new Error(`CharSet with symbol ${symbol} not found`);
450
934
  return cs;
451
935
  }
936
+ /**
937
+ * 커스텀 charset의 조합 중복을 검증합니다.
938
+ */
452
939
  validateCombinationDuplicates(charSet, paddingChar, requiredLength) {
453
- // 작은 크기의 단일 문자 집합에 대해서만 '조합 충돌' 검사를 수행 (안전장치)
454
940
  if (charSet[0].length !== 1 || requiredLength > 256)
455
941
  return;
456
942
  const limit = Math.min(charSet.length, requiredLength);
@@ -461,11 +947,15 @@ export class Ddu64 extends BaseDdu {
461
947
  throw new Error(`Combination conflict: ${context}`);
462
948
  combinations.add(s);
463
949
  };
464
- targetChars.forEach(c => combinations.add(c));
950
+ for (let i = 0; i < targetChars.length; i++) {
951
+ combinations.add(targetChars[i]);
952
+ }
465
953
  combinations.add(paddingChar);
466
- for (const c1 of targetChars) {
467
- for (const c2 of targetChars)
468
- add(c1 + c2, `"${c1}" + "${c2}"`);
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
+ }
469
959
  add(c1 + paddingChar, `"${c1}" + padding`);
470
960
  add(paddingChar + c1, `padding + "${c1}"`);
471
961
  }