@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,27 +1,103 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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 =
|
|
114
|
+
this.usePowerOfTwo =
|
|
115
|
+
dduLength > 0 && (dduLength & (dduLength - 1)) === 0;
|
|
35
116
|
const computedBitLength = this.getBitLength(dduLength);
|
|
36
|
-
this.bitLength = this.usePowerOfTwo
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
50
|
-
//
|
|
153
|
+
// --------------------------------------------------------------------------
|
|
154
|
+
// 공개 메서드
|
|
155
|
+
// --------------------------------------------------------------------------
|
|
51
156
|
/**
|
|
52
|
-
* 데이터를
|
|
53
|
-
*
|
|
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,
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
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.
|
|
185
|
+
return this.effectiveBitLength <= MAX_FAST_BITS
|
|
186
|
+
? this.encodeFast(compressedBuffer, true)
|
|
187
|
+
: this.encodeBigInt(compressedBuffer, true);
|
|
61
188
|
}
|
|
62
189
|
/**
|
|
63
|
-
*
|
|
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,
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 (
|
|
110
|
-
accumulator = (accumulator <<
|
|
111
|
-
accumulatorBits +=
|
|
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
|
|
498
|
+
resultParts[resultIdx++] = dduChar[index];
|
|
130
499
|
}
|
|
131
500
|
else {
|
|
132
501
|
const div = (index / dduLength) | 0;
|
|
133
|
-
|
|
134
|
-
resultParts
|
|
502
|
+
resultParts[resultIdx++] = dduChar[div];
|
|
503
|
+
resultParts[resultIdx++] = dduChar[index - div * dduLength];
|
|
135
504
|
}
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
517
|
+
// --------------------------------------------------------------------------
|
|
518
|
+
// 디코딩 (Fast 모드 - 16비트 이하)
|
|
519
|
+
// --------------------------------------------------------------------------
|
|
520
|
+
/**
|
|
521
|
+
* 일반 정수 연산을 사용한 빠른 디코딩
|
|
522
|
+
*/
|
|
523
|
+
decodeFast(cleanedInput, paddingBits) {
|
|
142
524
|
const inputLen = cleanedInput.length;
|
|
143
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >=
|
|
193
|
-
|
|
194
|
-
buffer
|
|
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
|
-
//
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
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 (
|
|
215
|
-
accumulator = (accumulator << 8n) | BigInt(
|
|
216
|
-
accumulatorBits +=
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
accumulator &= (
|
|
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 (
|
|
228
|
-
accumulator = (accumulator << 8n) | BigInt(
|
|
229
|
-
accumulatorBits +=
|
|
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
|
|
233
|
-
const
|
|
234
|
-
resultParts
|
|
235
|
-
|
|
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
|
|
668
|
+
resultParts[resultIdx++] = dduChar[index];
|
|
245
669
|
}
|
|
246
670
|
else {
|
|
247
|
-
|
|
671
|
+
const div = Math.floor(index / dduLength);
|
|
672
|
+
resultParts[resultIdx++] = dduChar[div];
|
|
673
|
+
resultParts[resultIdx++] = dduChar[index - div * dduLength];
|
|
248
674
|
}
|
|
249
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 ?
|
|
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 <
|
|
264
|
-
const chunk = cleanedInput.slice(i, i +
|
|
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 >=
|
|
716
|
+
if (i + chunkSize >= inputLen && paddingBits > 0) {
|
|
271
717
|
accumulator >>= BigInt(paddingBits);
|
|
272
718
|
accumulatorBits -= paddingBits;
|
|
273
719
|
}
|
|
274
|
-
while (accumulatorBits >=
|
|
275
|
-
const shift = accumulatorBits -
|
|
276
|
-
buffer
|
|
277
|
-
accumulator &= (
|
|
278
|
-
accumulatorBits -=
|
|
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 <
|
|
284
|
-
const c1 = cleanedInput.slice(i, i +
|
|
285
|
-
const c2 = cleanedInput.slice(i +
|
|
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
|
-
|
|
291
|
-
|
|
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 >=
|
|
746
|
+
if (i + chunkSize >= inputLen && paddingBits > 0) {
|
|
298
747
|
accumulator >>= BigInt(paddingBits);
|
|
299
748
|
accumulatorBits -= paddingBits;
|
|
300
749
|
}
|
|
301
|
-
while (accumulatorBits >=
|
|
302
|
-
const shift = accumulatorBits -
|
|
303
|
-
buffer
|
|
304
|
-
accumulator &= (
|
|
305
|
-
accumulatorBits -=
|
|
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
|
-
*
|
|
334
|
-
* 문제가 발생하면 옵션에 따라 Error를 던지거나 Fallback CharSet을 반환합니다.
|
|
764
|
+
* Charset을 정규화합니다.
|
|
335
765
|
*/
|
|
336
766
|
normalizeCharSet(current, shouldThrow, dduOptions) {
|
|
337
767
|
let state = { ...current };
|
|
338
|
-
|
|
339
|
-
|
|
768
|
+
let retryCount = 0;
|
|
769
|
+
const maxRetries = 3;
|
|
770
|
+
while (retryCount < maxRetries) {
|
|
340
771
|
try {
|
|
341
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
786
|
+
}
|
|
787
|
+
if (state.requiredLength < 2) {
|
|
356
788
|
throw new Error(`[Ddu64 normalizeCharSet] At least 2 unique characters required.`);
|
|
357
|
-
|
|
789
|
+
}
|
|
790
|
+
if (state.charSet.length === 0) {
|
|
358
791
|
throw new Error(`[Ddu64 normalizeCharSet] Empty charset.`);
|
|
359
|
-
|
|
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
|
-
|
|
366
|
-
throw new Error("
|
|
799
|
+
}
|
|
800
|
+
throw new Error("internal retry");
|
|
367
801
|
}
|
|
368
|
-
//
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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 ??
|
|
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 {
|
|
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
|
-
|
|
950
|
+
for (let i = 0; i < targetChars.length; i++) {
|
|
951
|
+
combinations.add(targetChars[i]);
|
|
952
|
+
}
|
|
465
953
|
combinations.add(paddingChar);
|
|
466
|
-
for (
|
|
467
|
-
|
|
468
|
-
|
|
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
|
}
|