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