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