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