@ddunigma/node 1.1.9 → 2.0.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/README.md +4 -0
- package/dist/cjs/base/BaseDdu.js +1 -1
- package/dist/cjs/encoders/Ddu64.d.ts +22 -13
- package/dist/cjs/encoders/Ddu64.js +391 -288
- package/dist/mjs/encoders/Ddu64.d.ts +22 -13
- package/dist/mjs/encoders/Ddu64.js +389 -283
- package/package.json +4 -1
|
@@ -2,6 +2,9 @@ import { BaseDdu } from "../base/BaseDdu";
|
|
|
2
2
|
import { DduSetSymbol, dduDefaultConstructorOptions, } from "../types";
|
|
3
3
|
import { getCharSet } from "../charSets";
|
|
4
4
|
export class Ddu64 extends BaseDdu {
|
|
5
|
+
// =========================================================================================
|
|
6
|
+
// Properties
|
|
7
|
+
// =========================================================================================
|
|
5
8
|
dduChar;
|
|
6
9
|
paddingChar;
|
|
7
10
|
charLength;
|
|
@@ -12,215 +15,432 @@ export class Ddu64 extends BaseDdu {
|
|
|
12
15
|
isPredefinedCharSet;
|
|
13
16
|
effectiveBitLength;
|
|
14
17
|
maxBinaryValue;
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
// =========================================================================================
|
|
19
|
+
// Constructor
|
|
20
|
+
// =========================================================================================
|
|
17
21
|
constructor(dduChar, paddingChar, dduOptions) {
|
|
18
22
|
super();
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const normalized = this.normalizeCharSet(
|
|
23
|
+
const shouldThrow = dduOptions?.useBuildErrorReturn ?? false;
|
|
24
|
+
// 1. CharSet 초기화 및 검증
|
|
25
|
+
const initial = this.resolveInitialCharSet(dduChar, paddingChar, dduOptions, shouldThrow);
|
|
26
|
+
const normalized = this.normalizeCharSet(initial, shouldThrow, dduOptions);
|
|
23
27
|
this.dduChar = normalized.charSet;
|
|
24
28
|
this.paddingChar = normalized.padding;
|
|
25
29
|
this.charLength = normalized.charLength;
|
|
26
30
|
this.isPredefinedCharSet = normalized.isPredefined;
|
|
27
31
|
this.encoding = dduOptions?.encoding ?? this.defaultEncoding;
|
|
32
|
+
// 2. 비트 연산 상수 계산
|
|
28
33
|
const dduLength = this.dduChar.length;
|
|
29
|
-
const recalculatedBitLength = this.getBitLength(dduLength);
|
|
30
34
|
this.usePowerOfTwo = dduLength > 0 && (dduLength & (dduLength - 1)) === 0;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.effectiveBitLength = this.usePowerOfTwo ? this.bitLength : recalculatedBitLength;
|
|
35
|
+
const computedBitLength = this.getBitLength(dduLength);
|
|
36
|
+
this.bitLength = this.usePowerOfTwo ? this.getLargestPowerOfTwoExponent(dduLength) : computedBitLength;
|
|
37
|
+
this.effectiveBitLength = this.usePowerOfTwo ? this.bitLength : computedBitLength;
|
|
35
38
|
this.maxBinaryValue = 2 ** this.effectiveBitLength;
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
value = value * 2 + (chunk.charCodeAt(i) & 1);
|
|
42
|
-
}
|
|
43
|
-
return value;
|
|
44
|
-
};
|
|
45
|
-
this.dduChar.forEach((char, index) => this.dduBinaryLookup.set(char, index));
|
|
39
|
+
// 3. Lookup Table 생성 (Decoding용)
|
|
40
|
+
for (let i = 0; i < dduLength; i++) {
|
|
41
|
+
this.dduBinaryLookup.set(this.dduChar[i], i);
|
|
42
|
+
}
|
|
43
|
+
// 4. 안전성 검사
|
|
46
44
|
if (this.charLength === 1 && !this.isPredefinedCharSet) {
|
|
47
45
|
this.validateCombinationDuplicates(this.dduChar, this.paddingChar, dduLength);
|
|
48
46
|
}
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
}
|
|
48
|
+
// =========================================================================================
|
|
49
|
+
// Public API
|
|
50
|
+
// =========================================================================================
|
|
51
|
+
/**
|
|
52
|
+
* 데이터를 DDU 포맷으로 인코딩합니다.
|
|
53
|
+
* 성능을 위해 24비트 이하는 Fast Path(number 연산)를 사용합니다.
|
|
54
|
+
*/
|
|
55
|
+
encode(input, _options) {
|
|
56
|
+
const bufferInput = typeof input === "string" ? Buffer.from(input, this.encoding) : input;
|
|
57
|
+
if (this.effectiveBitLength <= 24) {
|
|
58
|
+
return this.encodeFast(bufferInput);
|
|
59
|
+
}
|
|
60
|
+
return this.encodeBigInt(bufferInput);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* DDU 포맷 문자열을 버퍼로 디코딩합니다.
|
|
64
|
+
*/
|
|
65
|
+
decodeToBuffer(input, _options) {
|
|
66
|
+
if (this.effectiveBitLength <= 24) {
|
|
67
|
+
return this.decodeFast(input);
|
|
68
|
+
}
|
|
69
|
+
return this.decodeBigInt(input);
|
|
70
|
+
}
|
|
71
|
+
decode(input, _options) {
|
|
72
|
+
return this.decodeToBuffer(input, _options).toString(this.encoding);
|
|
73
|
+
}
|
|
74
|
+
getCharSetInfo() {
|
|
75
|
+
return {
|
|
76
|
+
charSet: [...this.dduChar],
|
|
77
|
+
paddingChar: this.paddingChar,
|
|
78
|
+
charLength: this.charLength,
|
|
79
|
+
bitLength: this.bitLength,
|
|
80
|
+
usePowerOfTwo: this.usePowerOfTwo,
|
|
81
|
+
encoding: this.encoding,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
// =========================================================================================
|
|
85
|
+
// Fast Engine (Optimized for Speed)
|
|
86
|
+
// Uses standard JS numbers (safe up to ~24 bits per chunk) to avoid BigInt GC overhead.
|
|
87
|
+
// =========================================================================================
|
|
88
|
+
encodeFast(bufferInput) {
|
|
89
|
+
const resultParts = [];
|
|
90
|
+
const { dduChar, effectiveBitLength: bitLength, paddingChar } = this;
|
|
91
|
+
const dduLength = dduChar.length;
|
|
92
|
+
let accumulator = 0;
|
|
93
|
+
let accumulatorBits = 0;
|
|
94
|
+
// Loop Unswitching: 조건문을 루프 밖으로 빼서 CPU 분기 예측 효율 향상
|
|
95
|
+
if (this.usePowerOfTwo) {
|
|
96
|
+
for (const byte of bufferInput) {
|
|
97
|
+
accumulator = (accumulator << 8) | byte;
|
|
98
|
+
accumulatorBits += 8;
|
|
99
|
+
while (accumulatorBits >= bitLength) {
|
|
100
|
+
const shift = accumulatorBits - bitLength;
|
|
101
|
+
const index = accumulator >> shift;
|
|
102
|
+
resultParts.push(dduChar[index]);
|
|
103
|
+
accumulatorBits -= bitLength;
|
|
104
|
+
accumulator &= (1 << accumulatorBits) - 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
51
107
|
}
|
|
52
108
|
else {
|
|
53
|
-
|
|
109
|
+
for (const byte of bufferInput) {
|
|
110
|
+
accumulator = (accumulator << 8) | byte;
|
|
111
|
+
accumulatorBits += 8;
|
|
112
|
+
while (accumulatorBits >= bitLength) {
|
|
113
|
+
const shift = accumulatorBits - bitLength;
|
|
114
|
+
const index = accumulator >> shift;
|
|
115
|
+
// 비 2의 제곱수는 나눗셈으로 인덱스 계산
|
|
116
|
+
const div = (index / dduLength) | 0;
|
|
117
|
+
const mod = index % dduLength;
|
|
118
|
+
resultParts.push(dduChar[div] + dduChar[mod]);
|
|
119
|
+
accumulatorBits -= bitLength;
|
|
120
|
+
accumulator &= (1 << accumulatorBits) - 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
54
123
|
}
|
|
124
|
+
// 남은 비트 패딩 처리
|
|
125
|
+
if (accumulatorBits > 0) {
|
|
126
|
+
const paddingBits = bitLength - accumulatorBits;
|
|
127
|
+
const index = accumulator << paddingBits;
|
|
128
|
+
if (this.usePowerOfTwo) {
|
|
129
|
+
resultParts.push(dduChar[index]);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
const div = (index / dduLength) | 0;
|
|
133
|
+
const mod = index % dduLength;
|
|
134
|
+
resultParts.push(dduChar[div] + dduChar[mod]);
|
|
135
|
+
}
|
|
136
|
+
return resultParts.join("") + paddingChar + paddingBits.toString();
|
|
137
|
+
}
|
|
138
|
+
return resultParts.join("");
|
|
55
139
|
}
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
140
|
+
decodeFast(input) {
|
|
141
|
+
const { cleanedInput, paddingBits } = this.parsePaddingAndGetInput(input);
|
|
142
|
+
const inputLen = cleanedInput.length;
|
|
143
|
+
const buffer = [];
|
|
144
|
+
let accumulator = 0;
|
|
145
|
+
let accumulatorBits = 0;
|
|
146
|
+
const { effectiveBitLength: bitLength, dduBinaryLookup: lookup, charLength, maxBinaryValue } = this;
|
|
147
|
+
const dduLength = this.dduChar.length;
|
|
148
|
+
if (this.usePowerOfTwo) {
|
|
149
|
+
const chunkSize = charLength;
|
|
150
|
+
for (let i = 0; i < inputLen; i += chunkSize) {
|
|
151
|
+
const chunk = cleanedInput.slice(i, i + charLength);
|
|
152
|
+
const val = lookup.get(chunk);
|
|
153
|
+
if (val === undefined)
|
|
154
|
+
throw new Error(`[Ddu64 decode] Invalid character "${chunk}" at ${i}`);
|
|
155
|
+
if (val >= maxBinaryValue)
|
|
156
|
+
throw new Error(`[Ddu64 decode] Value ${val} exceeds range`);
|
|
157
|
+
accumulator = (accumulator << bitLength) | val;
|
|
158
|
+
accumulatorBits += bitLength;
|
|
159
|
+
// 마지막 청크 패딩 비트 제거
|
|
160
|
+
if (i + chunkSize >= inputLen && paddingBits > 0) {
|
|
161
|
+
accumulator >>= paddingBits;
|
|
162
|
+
accumulatorBits -= paddingBits;
|
|
163
|
+
}
|
|
164
|
+
while (accumulatorBits >= 8) {
|
|
165
|
+
const shift = accumulatorBits - 8;
|
|
166
|
+
buffer.push((accumulator >> shift) & 0xFF);
|
|
167
|
+
accumulatorBits -= 8;
|
|
168
|
+
accumulator &= (1 << accumulatorBits) - 1;
|
|
84
169
|
}
|
|
85
170
|
}
|
|
86
|
-
|
|
87
|
-
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
const chunkSize = charLength * 2;
|
|
174
|
+
for (let i = 0; i < inputLen; i += chunkSize) {
|
|
175
|
+
const c1 = cleanedInput.slice(i, i + charLength);
|
|
176
|
+
const c2 = cleanedInput.slice(i + charLength, i + chunkSize);
|
|
177
|
+
const v1 = lookup.get(c1);
|
|
178
|
+
const v2 = lookup.get(c2);
|
|
179
|
+
if (v1 === undefined)
|
|
180
|
+
throw new Error(`[Ddu64 decode] Invalid character "${c1}" at ${i}`);
|
|
181
|
+
if (v2 === undefined)
|
|
182
|
+
throw new Error(`[Ddu64 decode] Invalid character "${c2}" at ${i + charLength}`);
|
|
183
|
+
const value = v1 * dduLength + v2;
|
|
184
|
+
if (value >= maxBinaryValue)
|
|
185
|
+
throw new Error(`[Ddu64 decode] Value ${value} exceeds range`);
|
|
186
|
+
accumulator = (accumulator << bitLength) | value;
|
|
187
|
+
accumulatorBits += bitLength;
|
|
188
|
+
if (i + chunkSize >= inputLen && paddingBits > 0) {
|
|
189
|
+
accumulator >>= paddingBits;
|
|
190
|
+
accumulatorBits -= paddingBits;
|
|
191
|
+
}
|
|
192
|
+
while (accumulatorBits >= 8) {
|
|
193
|
+
const shift = accumulatorBits - 8;
|
|
194
|
+
buffer.push((accumulator >> shift) & 0xFF);
|
|
195
|
+
accumulatorBits -= 8;
|
|
196
|
+
accumulator &= (1 << accumulatorBits) - 1;
|
|
197
|
+
}
|
|
88
198
|
}
|
|
89
|
-
|
|
90
|
-
|
|
199
|
+
}
|
|
200
|
+
return Buffer.from(buffer);
|
|
201
|
+
}
|
|
202
|
+
// =========================================================================================
|
|
203
|
+
// Safe Engine (BigInt)
|
|
204
|
+
// Fallback for huge charsets (> 24 bits per chunk) where 32-bit integers overflow.
|
|
205
|
+
// =========================================================================================
|
|
206
|
+
encodeBigInt(bufferInput) {
|
|
207
|
+
const resultParts = [];
|
|
208
|
+
const { dduChar, effectiveBitLength: bitLength } = this;
|
|
209
|
+
const dduLength = dduChar.length;
|
|
210
|
+
const bigBitLength = BigInt(bitLength);
|
|
211
|
+
let accumulator = 0n;
|
|
212
|
+
let accumulatorBits = 0;
|
|
213
|
+
if (this.usePowerOfTwo) {
|
|
214
|
+
for (const byte of bufferInput) {
|
|
215
|
+
accumulator = (accumulator << 8n) | BigInt(byte);
|
|
216
|
+
accumulatorBits += 8;
|
|
217
|
+
while (accumulatorBits >= bitLength) {
|
|
218
|
+
const shift = accumulatorBits - bitLength;
|
|
219
|
+
const value = accumulator >> BigInt(shift);
|
|
220
|
+
resultParts.push(dduChar[Number(value)]);
|
|
221
|
+
accumulator &= ((1n << BigInt(shift)) - 1n);
|
|
222
|
+
accumulatorBits -= bitLength;
|
|
223
|
+
}
|
|
91
224
|
}
|
|
92
|
-
|
|
93
|
-
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
for (const byte of bufferInput) {
|
|
228
|
+
accumulator = (accumulator << 8n) | BigInt(byte);
|
|
229
|
+
accumulatorBits += 8;
|
|
230
|
+
while (accumulatorBits >= bitLength) {
|
|
231
|
+
const shift = accumulatorBits - bitLength;
|
|
232
|
+
const value = accumulator >> BigInt(shift);
|
|
233
|
+
const idx = Number(value);
|
|
234
|
+
resultParts.push(dduChar[Math.floor(idx / dduLength)] + dduChar[idx % dduLength]);
|
|
235
|
+
accumulator &= ((1n << BigInt(shift)) - 1n);
|
|
236
|
+
accumulatorBits -= bitLength;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (accumulatorBits > 0) {
|
|
241
|
+
const paddingBits = bitLength - accumulatorBits;
|
|
242
|
+
const index = Number(accumulator << BigInt(paddingBits));
|
|
243
|
+
if (this.usePowerOfTwo) {
|
|
244
|
+
resultParts.push(dduChar[index]);
|
|
94
245
|
}
|
|
95
|
-
|
|
96
|
-
|
|
246
|
+
else {
|
|
247
|
+
resultParts.push(dduChar[Math.floor(index / dduLength)] + dduChar[index % dduLength]);
|
|
97
248
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
249
|
+
return resultParts.join("") + this.paddingChar + paddingBits.toString();
|
|
250
|
+
}
|
|
251
|
+
return resultParts.join("");
|
|
252
|
+
}
|
|
253
|
+
decodeBigInt(input) {
|
|
254
|
+
const { cleanedInput, paddingBits } = this.parsePaddingAndGetInput(input);
|
|
255
|
+
const buffer = [];
|
|
256
|
+
let accumulator = 0n;
|
|
257
|
+
let accumulatorBits = 0;
|
|
258
|
+
const { effectiveBitLength: bitLength, dduBinaryLookup: lookup } = this;
|
|
259
|
+
const bigBitLength = BigInt(bitLength);
|
|
260
|
+
const dduLength = this.dduChar.length;
|
|
261
|
+
const chunkSize = this.usePowerOfTwo ? this.charLength : this.charLength * 2;
|
|
262
|
+
if (this.usePowerOfTwo) {
|
|
263
|
+
for (let i = 0; i < cleanedInput.length; i += chunkSize) {
|
|
264
|
+
const chunk = cleanedInput.slice(i, i + this.charLength);
|
|
265
|
+
const val = lookup.get(chunk);
|
|
266
|
+
if (val === undefined)
|
|
267
|
+
throw new Error(`[Ddu64 decode] Invalid character "${chunk}" at ${i}`);
|
|
268
|
+
accumulator = (accumulator << bigBitLength) | BigInt(val);
|
|
269
|
+
accumulatorBits += bitLength;
|
|
270
|
+
if (i + chunkSize >= cleanedInput.length && paddingBits > 0) {
|
|
271
|
+
accumulator >>= BigInt(paddingBits);
|
|
272
|
+
accumulatorBits -= paddingBits;
|
|
273
|
+
}
|
|
274
|
+
while (accumulatorBits >= 8) {
|
|
275
|
+
const shift = accumulatorBits - 8;
|
|
276
|
+
buffer.push(Number((accumulator >> BigInt(shift)) & 0xffn));
|
|
277
|
+
accumulator &= ((1n << BigInt(shift)) - 1n);
|
|
278
|
+
accumulatorBits -= 8;
|
|
103
279
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
for (let i = 0; i < cleanedInput.length; i += chunkSize) {
|
|
284
|
+
const c1 = cleanedInput.slice(i, i + this.charLength);
|
|
285
|
+
const c2 = cleanedInput.slice(i + this.charLength, i + chunkSize);
|
|
286
|
+
const v1 = lookup.get(c1);
|
|
287
|
+
const v2 = lookup.get(c2);
|
|
288
|
+
if (v1 === undefined)
|
|
289
|
+
throw new Error(`[Ddu64 decode] Invalid character "${c1}" at ${i}`);
|
|
290
|
+
if (v2 === undefined)
|
|
291
|
+
throw new Error(`[Ddu64 decode] Invalid character "${c2}" at ${i + this.charLength}`);
|
|
292
|
+
const value = v1 * dduLength + v2;
|
|
293
|
+
if (value >= this.maxBinaryValue)
|
|
294
|
+
throw new Error(`[Ddu64 decode] Value ${value} exceeds range`);
|
|
295
|
+
accumulator = (accumulator << bigBitLength) | BigInt(value);
|
|
296
|
+
accumulatorBits += bitLength;
|
|
297
|
+
if (i + chunkSize >= cleanedInput.length && paddingBits > 0) {
|
|
298
|
+
accumulator >>= BigInt(paddingBits);
|
|
299
|
+
accumulatorBits -= paddingBits;
|
|
107
300
|
}
|
|
108
|
-
|
|
109
|
-
|
|
301
|
+
while (accumulatorBits >= 8) {
|
|
302
|
+
const shift = accumulatorBits - 8;
|
|
303
|
+
buffer.push(Number((accumulator >> BigInt(shift)) & 0xffn));
|
|
304
|
+
accumulator &= ((1n << BigInt(shift)) - 1n);
|
|
305
|
+
accumulatorBits -= 8;
|
|
110
306
|
}
|
|
111
|
-
charLength = charSet[0].length;
|
|
112
307
|
}
|
|
113
|
-
|
|
114
|
-
|
|
308
|
+
}
|
|
309
|
+
return Buffer.from(buffer);
|
|
310
|
+
}
|
|
311
|
+
// =========================================================================================
|
|
312
|
+
// Internal Helpers (Normalization & Validation)
|
|
313
|
+
// =========================================================================================
|
|
314
|
+
parsePaddingAndGetInput(input) {
|
|
315
|
+
const padLen = this.paddingChar.length;
|
|
316
|
+
if (input.length < padLen)
|
|
317
|
+
return { cleanedInput: input, paddingBits: 0 };
|
|
318
|
+
const padIdx = input.lastIndexOf(this.paddingChar);
|
|
319
|
+
// 패딩 문자가 존재하고, 위치가 올바른지(chunk 단위) 확인
|
|
320
|
+
if (padIdx >= 0 && padIdx % this.charLength === 0 && padIdx + padLen <= input.length) {
|
|
321
|
+
const paddingSection = input.slice(padIdx + padLen);
|
|
322
|
+
if (!paddingSection)
|
|
323
|
+
throw new Error(`[Ddu64 decode] Invalid padding format. Missing padding length`);
|
|
324
|
+
const paddingBits = parseInt(paddingSection, 10);
|
|
325
|
+
if (isNaN(paddingBits) || paddingSection !== paddingBits.toString() || paddingBits < 0 || paddingBits >= this.effectiveBitLength) {
|
|
326
|
+
throw new Error(`[Ddu64 decode] Invalid padding format. Got: "${paddingSection}"`);
|
|
115
327
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
328
|
+
return { cleanedInput: input.substring(0, padIdx), paddingBits };
|
|
329
|
+
}
|
|
330
|
+
return { cleanedInput: input, paddingBits: 0 };
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 입력된 CharSet을 검증하고 정리(Normalization)합니다.
|
|
334
|
+
* 문제가 발생하면 옵션에 따라 Error를 던지거나 Fallback CharSet을 반환합니다.
|
|
335
|
+
*/
|
|
336
|
+
normalizeCharSet(current, shouldThrow, dduOptions) {
|
|
337
|
+
let state = { ...current };
|
|
338
|
+
// 재시도 루프 (Fallback 로직 포함)
|
|
339
|
+
while (true) {
|
|
340
|
+
try {
|
|
341
|
+
// 1. 중복 제거
|
|
342
|
+
const uniqueChars = Array.from(new Set(state.charSet));
|
|
343
|
+
if (uniqueChars.length !== state.charSet.length) {
|
|
344
|
+
if (shouldThrow) {
|
|
345
|
+
const duplicates = state.charSet.filter((c, i) => state.charSet.indexOf(c) !== i);
|
|
346
|
+
throw new Error(`[Ddu64 normalizeCharSet] Character set contains duplicate characters: [${[...new Set(duplicates)].join(", ")}]`);
|
|
347
|
+
}
|
|
348
|
+
state.charSet = uniqueChars;
|
|
349
|
+
if (!state.isPredefined)
|
|
350
|
+
state.requiredLength = state.charSet.length;
|
|
119
351
|
}
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
352
|
+
// 2. 기본 조건 검사
|
|
353
|
+
if (state.charSet.length < state.requiredLength)
|
|
354
|
+
throw new Error(`[Ddu64 normalizeCharSet] Insufficient characters. Required: ${state.requiredLength}, Has: ${state.charSet.length}`);
|
|
355
|
+
if (state.requiredLength < 2)
|
|
356
|
+
throw new Error(`[Ddu64 normalizeCharSet] At least 2 unique characters required.`);
|
|
357
|
+
if (state.charSet.length === 0)
|
|
358
|
+
throw new Error(`[Ddu64 normalizeCharSet] Empty charset.`);
|
|
359
|
+
// 3. 문자 길이 일관성 검사
|
|
360
|
+
const charLength = state.charSet[0].length;
|
|
361
|
+
const invalidChar = state.charSet.find(c => c.length !== charLength);
|
|
362
|
+
if (invalidChar) {
|
|
363
|
+
if (shouldThrow)
|
|
364
|
+
throw new Error(`[Ddu64 normalizeCharSet] Inconsistent char length. Expected ${charLength}, found "${invalidChar}" (${invalidChar.length})`);
|
|
365
|
+
// 필터링 후 재검증을 위해 예외 발생시켜 Fallback 또는 재시도 유도
|
|
366
|
+
throw new Error("Filtered inconsistent chars (internal retry)");
|
|
123
367
|
}
|
|
124
|
-
|
|
125
|
-
|
|
368
|
+
// 4. 패딩 충돌 검사
|
|
369
|
+
if (state.padding.length !== charLength)
|
|
370
|
+
throw new Error(`[Ddu64 normalizeCharSet] Padding length mismatch. Expected ${charLength}, got ${state.padding.length}`);
|
|
371
|
+
if (state.charSet.includes(state.padding)) {
|
|
372
|
+
if (shouldThrow)
|
|
373
|
+
throw new Error(`[Ddu64 normalizeCharSet] Padding character "${state.padding}" conflicts with charset.`);
|
|
374
|
+
state.charSet = state.charSet.filter(c => c !== state.padding);
|
|
126
375
|
}
|
|
127
|
-
|
|
376
|
+
return {
|
|
377
|
+
charSet: state.charSet.slice(0, state.requiredLength),
|
|
378
|
+
padding: state.padding,
|
|
379
|
+
charLength,
|
|
380
|
+
isPredefined: state.isPredefined,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
// 사용자가 명시적으로 에러를 요청했거나, 복구 불가능한 에러인 경우
|
|
385
|
+
if (shouldThrow && !e.message.includes("internal retry"))
|
|
386
|
+
throw e;
|
|
387
|
+
// 그 외에는 Fallback CharSet 사용
|
|
388
|
+
state = this.getFallbackCharSet(dduOptions);
|
|
128
389
|
}
|
|
129
|
-
return {
|
|
130
|
-
charSet: charSet.slice(0, requiredLength),
|
|
131
|
-
padding,
|
|
132
|
-
requiredLength,
|
|
133
|
-
bitLength,
|
|
134
|
-
isPredefined,
|
|
135
|
-
charLength,
|
|
136
|
-
};
|
|
137
390
|
}
|
|
138
391
|
}
|
|
139
|
-
resolveInitialCharSet(dduChar, paddingChar, dduOptions,
|
|
140
|
-
|
|
392
|
+
resolveInitialCharSet(dduChar, paddingChar, dduOptions, shouldThrow) {
|
|
393
|
+
// 내부 헬퍼: 길이와 옵션에 따라 최종 메타데이터 생성
|
|
394
|
+
const buildMeta = (set, padding, length, isPredefined) => {
|
|
141
395
|
const usePow2 = this.shouldUsePowerOfTwo(length, dduOptions?.usePowerOfTwo);
|
|
142
396
|
if (usePow2 && length > 0) {
|
|
143
397
|
const exponent = this.getLargestPowerOfTwoExponent(length);
|
|
144
398
|
const pow2Length = 1 << exponent;
|
|
145
|
-
return {
|
|
146
|
-
charSet: set.slice(0, pow2Length),
|
|
147
|
-
padding,
|
|
148
|
-
requiredLength: pow2Length,
|
|
149
|
-
bitLength: exponent,
|
|
150
|
-
isPredefined: predefined,
|
|
151
|
-
};
|
|
399
|
+
return { charSet: set.slice(0, pow2Length), padding, requiredLength: pow2Length, bitLength: exponent, isPredefined };
|
|
152
400
|
}
|
|
153
|
-
|
|
154
|
-
return {
|
|
155
|
-
charSet: set.slice(0, length),
|
|
156
|
-
padding,
|
|
157
|
-
requiredLength: length,
|
|
158
|
-
bitLength: computedBitLength,
|
|
159
|
-
isPredefined: predefined,
|
|
160
|
-
};
|
|
401
|
+
return { charSet: set.slice(0, length), padding, requiredLength: length, bitLength: length > 0 ? this.getBitLength(length) : 0, isPredefined };
|
|
161
402
|
};
|
|
162
|
-
const fallback = () => this.getFallbackCharSet(dduOptions);
|
|
163
403
|
try {
|
|
164
404
|
const finalDduChar = dduChar ?? dduOptions?.dduChar;
|
|
165
405
|
const finalPadding = paddingChar ?? dduOptions?.paddingChar;
|
|
406
|
+
// Case A: 사용자 제공 CharSet
|
|
166
407
|
if (finalDduChar) {
|
|
167
|
-
if (!finalPadding)
|
|
168
|
-
throw new Error(`[Ddu64 Constructor] paddingChar is required when dduChar is provided
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (uniqueChars.size !== arr.length) {
|
|
176
|
-
const duplicates = arr.filter((char, index) => arr.indexOf(char) !== index);
|
|
177
|
-
throw new Error(`[Ddu64 Constructor] Character set contains duplicate characters. Total: ${arr.length}, Unique: ${uniqueChars.size}, Duplicates: [${[
|
|
178
|
-
...new Set(duplicates),
|
|
179
|
-
].join(", ")}]`);
|
|
408
|
+
if (!finalPadding)
|
|
409
|
+
throw new Error(`[Ddu64 Constructor] paddingChar is required when dduChar is provided.`);
|
|
410
|
+
const arr = typeof finalDduChar === "string" ? [...finalDduChar.trim()] : [...finalDduChar];
|
|
411
|
+
if (shouldThrow) {
|
|
412
|
+
const uniqueSize = new Set(arr).size;
|
|
413
|
+
if (uniqueSize !== arr.length) {
|
|
414
|
+
const duplicates = arr.filter((c, i) => arr.indexOf(c) !== i);
|
|
415
|
+
throw new Error(`[Ddu64 Constructor] Character set contains duplicate characters: [${[...new Set(duplicates)].join(", ")}]`);
|
|
180
416
|
}
|
|
181
417
|
}
|
|
182
|
-
const
|
|
183
|
-
if (arr.length <
|
|
184
|
-
throw new Error(`[Ddu64 Constructor] Insufficient characters
|
|
185
|
-
|
|
186
|
-
return finalize(arr, finalPadding, len, undefined, false);
|
|
187
|
-
}
|
|
188
|
-
if (dduOptions?.dduSetSymbol) {
|
|
189
|
-
const cs = this.getCharSetOrThrow(dduOptions.dduSetSymbol);
|
|
190
|
-
return finalize(cs.charSet, cs.paddingChar, cs.maxRequiredLength, cs.bitLength, true);
|
|
418
|
+
const reqLen = dduOptions?.requiredLength ?? arr.length;
|
|
419
|
+
if (arr.length < reqLen)
|
|
420
|
+
throw new Error(`[Ddu64 Constructor] Insufficient characters.`);
|
|
421
|
+
return buildMeta(arr, finalPadding, reqLen, false);
|
|
191
422
|
}
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
423
|
+
// Case B: 심볼(Enum)로 지정된 CharSet
|
|
424
|
+
const symbol = dduOptions?.dduSetSymbol ?? dduDefaultConstructorOptions.dduSetSymbol ?? DduSetSymbol.DDU;
|
|
425
|
+
const cs = this.getCharSetOrThrow(symbol);
|
|
426
|
+
return buildMeta(cs.charSet, cs.paddingChar, cs.maxRequiredLength, true);
|
|
195
427
|
}
|
|
196
428
|
catch (error) {
|
|
197
|
-
if (
|
|
429
|
+
if (shouldThrow)
|
|
198
430
|
throw error;
|
|
199
|
-
return
|
|
431
|
+
return this.getFallbackCharSet(dduOptions);
|
|
200
432
|
}
|
|
201
433
|
}
|
|
202
434
|
getFallbackCharSet(dduOptions) {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
DduSetSymbol.ONECHARSET;
|
|
206
|
-
const cs = getCharSet(fallbackSymbol) ?? getCharSet(DduSetSymbol.ONECHARSET);
|
|
435
|
+
const symbol = dduOptions?.dduSetSymbol ?? dduDefaultConstructorOptions.dduSetSymbol ?? DduSetSymbol.ONECHARSET;
|
|
436
|
+
const cs = getCharSet(symbol) ?? getCharSet(DduSetSymbol.ONECHARSET);
|
|
207
437
|
if (!cs)
|
|
208
438
|
throw new Error(`Critical: No fallback CharSet available`);
|
|
209
|
-
return {
|
|
210
|
-
charSet: cs.charSet,
|
|
211
|
-
padding: cs.paddingChar,
|
|
212
|
-
requiredLength: cs.maxRequiredLength,
|
|
213
|
-
bitLength: cs.bitLength,
|
|
214
|
-
isPredefined: true,
|
|
215
|
-
};
|
|
439
|
+
return { charSet: cs.charSet, padding: cs.paddingChar, requiredLength: cs.maxRequiredLength, bitLength: cs.bitLength, isPredefined: true };
|
|
216
440
|
}
|
|
217
441
|
shouldUsePowerOfTwo(length, preference) {
|
|
218
|
-
if (preference
|
|
219
|
-
return length > 0;
|
|
220
|
-
}
|
|
221
|
-
if (preference === false) {
|
|
222
|
-
return false;
|
|
223
|
-
}
|
|
442
|
+
if (preference !== undefined)
|
|
443
|
+
return preference ? length > 0 : false;
|
|
224
444
|
return length > 0 && (length & (length - 1)) === 0;
|
|
225
445
|
}
|
|
226
446
|
getCharSetOrThrow(symbol) {
|
|
@@ -229,140 +449,26 @@ export class Ddu64 extends BaseDdu {
|
|
|
229
449
|
throw new Error(`CharSet with symbol ${symbol} not found`);
|
|
230
450
|
return cs;
|
|
231
451
|
}
|
|
232
|
-
getBinaryFromIndex(index) {
|
|
233
|
-
if (index < 0 || index >= this.maxBinaryValue) {
|
|
234
|
-
throw new Error(`[Ddu64] Binary index overflow. Received: ${index}, Allowed range: 0-${this.maxBinaryValue - 1}`);
|
|
235
|
-
}
|
|
236
|
-
return this.indexToBinaryCache?.[index] ?? index.toString(2).padStart(this.effectiveBitLength, "0");
|
|
237
|
-
}
|
|
238
452
|
validateCombinationDuplicates(charSet, paddingChar, requiredLength) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (charLength !== 1 || requiredLength > 256) {
|
|
453
|
+
// 작은 크기의 단일 문자 집합에 대해서만 '조합 충돌' 검사를 수행 (안전장치)
|
|
454
|
+
if (charSet[0].length !== 1 || requiredLength > 256)
|
|
242
455
|
return;
|
|
243
|
-
}
|
|
244
|
-
const allStrings = new Set([...charSet.slice(0, requiredLength), paddingChar]);
|
|
245
456
|
const limit = Math.min(charSet.length, requiredLength);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
}
|
|
253
|
-
const charPad = charSet[i] + paddingChar;
|
|
254
|
-
const padChar = paddingChar + charSet[i];
|
|
255
|
-
if (allStrings.has(charPad)) {
|
|
256
|
-
throw new Error(`Combination conflict: "${charSet[i]}" + padding "${paddingChar}" = "${charPad}"`);
|
|
257
|
-
}
|
|
258
|
-
if (allStrings.has(padChar)) {
|
|
259
|
-
throw new Error(`Combination conflict: padding "${paddingChar}" + "${charSet[i]}" = "${padChar}"`);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const doublePad = paddingChar + paddingChar;
|
|
263
|
-
if (allStrings.has(doublePad)) {
|
|
264
|
-
throw new Error(`Combination conflict: double padding "${doublePad}" already exists`);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
encode(input, _options) {
|
|
268
|
-
// options는 구버전 호환성을 위해 유지하지만 사용하지 않음
|
|
269
|
-
const bufferInput = typeof input === "string" ? Buffer.from(input, this.encoding) : input;
|
|
270
|
-
const { dduBinary, padding } = this.bufferToDduBinary(bufferInput, this.effectiveBitLength);
|
|
271
|
-
const resultParts = new Array(dduBinary.length);
|
|
272
|
-
if (this.usePowerOfTwo) {
|
|
273
|
-
for (let i = 0; i < dduBinary.length; i++) {
|
|
274
|
-
const charInt = this.binaryChunkToIntFn(dduBinary[i]);
|
|
275
|
-
resultParts[i] = this.dduChar[charInt];
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
279
|
-
const dduLength = this.dduChar.length;
|
|
280
|
-
for (let i = 0; i < dduBinary.length; i++) {
|
|
281
|
-
const value = this.binaryChunkToIntFn(dduBinary[i]);
|
|
282
|
-
const quotient = Math.floor(value / dduLength);
|
|
283
|
-
const remainder = value % dduLength;
|
|
284
|
-
resultParts[i] = this.dduChar[quotient] + this.dduChar[remainder];
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
let resultString = resultParts.join("");
|
|
288
|
-
// 패딩 비트 정보를 padChar + 패딩비트수 형태로 추가
|
|
289
|
-
if (padding > 0) {
|
|
290
|
-
resultString += this.paddingChar + padding;
|
|
291
|
-
}
|
|
292
|
-
return resultString;
|
|
293
|
-
}
|
|
294
|
-
decodeToBuffer(input, _options) {
|
|
295
|
-
let paddingBits = 0;
|
|
296
|
-
if (input.length >= this.paddingChar.length) {
|
|
297
|
-
const padCharIndex = input.lastIndexOf(this.paddingChar);
|
|
298
|
-
if (padCharIndex >= 0 &&
|
|
299
|
-
padCharIndex % this.charLength === 0 &&
|
|
300
|
-
padCharIndex + this.paddingChar.length <= input.length) {
|
|
301
|
-
const paddingSection = input.slice(padCharIndex + this.paddingChar.length);
|
|
302
|
-
if (paddingSection.length === 0) {
|
|
303
|
-
throw new Error(`[Ddu64 decode] Invalid padding format. Missing padding length after "${this.paddingChar}"`);
|
|
304
|
-
}
|
|
305
|
-
paddingBits = parseInt(paddingSection, 10);
|
|
306
|
-
if (isNaN(paddingBits) ||
|
|
307
|
-
paddingSection !== paddingBits.toString() ||
|
|
308
|
-
paddingBits < 0 ||
|
|
309
|
-
paddingBits >= this.effectiveBitLength) {
|
|
310
|
-
throw new Error(`[Ddu64 decode] Invalid padding format. Expected integer between 0 and ${this.effectiveBitLength - 1}, Got: "${paddingSection}"`);
|
|
311
|
-
}
|
|
312
|
-
input = input.substring(0, padCharIndex);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
const binaryParts = [];
|
|
316
|
-
const charLength = this.charLength;
|
|
317
|
-
const dduLength = this.dduChar.length;
|
|
318
|
-
if (!this.usePowerOfTwo) {
|
|
319
|
-
const chunkSize = charLength * 2;
|
|
320
|
-
for (let i = 0; i < input.length; i += chunkSize) {
|
|
321
|
-
const firstChar = input.slice(i, i + charLength);
|
|
322
|
-
const secondChar = input.slice(i + charLength, i + chunkSize);
|
|
323
|
-
const firstIndex = this.dduBinaryLookup.get(firstChar);
|
|
324
|
-
const secondIndex = this.dduBinaryLookup.get(secondChar);
|
|
325
|
-
if (firstIndex === undefined || secondIndex === undefined) {
|
|
326
|
-
const invalidChar = firstIndex === undefined ? firstChar : secondChar;
|
|
327
|
-
throw new Error(`[Ddu64 decode] Invalid character in encoded string. Character: "${invalidChar}", Position: ${i}, Expected charset size: ${dduLength}`);
|
|
328
|
-
}
|
|
329
|
-
const value = firstIndex * dduLength + secondIndex;
|
|
330
|
-
if (value >= this.maxBinaryValue) {
|
|
331
|
-
throw new Error(`[Ddu64 decode] Invalid character combination detected. Calculated value ${value} exceeds binary range ${this.maxBinaryValue - 1}.`);
|
|
332
|
-
}
|
|
333
|
-
binaryParts.push(this.getBinaryFromIndex(value));
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
for (let i = 0; i < input.length; i += charLength) {
|
|
338
|
-
const charChunk = input.slice(i, i + charLength);
|
|
339
|
-
const charIndex = this.dduBinaryLookup.get(charChunk);
|
|
340
|
-
if (charIndex === undefined) {
|
|
341
|
-
throw new Error(`[Ddu64 decode] Invalid character in encoded string. Character: "${charChunk}", Position: ${i}, Charset size: ${dduLength}, Character length: ${charLength}`);
|
|
342
|
-
}
|
|
343
|
-
if (charIndex >= this.maxBinaryValue) {
|
|
344
|
-
throw new Error(`[Ddu64 decode] Invalid binary index ${charIndex}. Allowed range: 0-${this.maxBinaryValue - 1}`);
|
|
345
|
-
}
|
|
346
|
-
binaryParts.push(this.getBinaryFromIndex(charIndex));
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
return this.dduBinaryToBuffer(binaryParts.join(""), paddingBits);
|
|
350
|
-
}
|
|
351
|
-
decode(input, _options) {
|
|
352
|
-
return this.decodeToBuffer(input, _options).toString(this.encoding);
|
|
353
|
-
}
|
|
354
|
-
/**
|
|
355
|
-
* 테스트 및 디버깅용 getter 메서드
|
|
356
|
-
* 인코더의 내부 상태 정보를 반환
|
|
357
|
-
*/
|
|
358
|
-
getCharSetInfo() {
|
|
359
|
-
return {
|
|
360
|
-
charSet: [...this.dduChar],
|
|
361
|
-
paddingChar: this.paddingChar,
|
|
362
|
-
charLength: this.charLength,
|
|
363
|
-
bitLength: this.bitLength,
|
|
364
|
-
usePowerOfTwo: this.usePowerOfTwo,
|
|
365
|
-
encoding: this.encoding,
|
|
457
|
+
const targetChars = charSet.slice(0, limit);
|
|
458
|
+
const combinations = new Set();
|
|
459
|
+
const add = (s, context) => {
|
|
460
|
+
if (combinations.has(s))
|
|
461
|
+
throw new Error(`Combination conflict: ${context}`);
|
|
462
|
+
combinations.add(s);
|
|
366
463
|
};
|
|
464
|
+
targetChars.forEach(c => combinations.add(c));
|
|
465
|
+
combinations.add(paddingChar);
|
|
466
|
+
for (const c1 of targetChars) {
|
|
467
|
+
for (const c2 of targetChars)
|
|
468
|
+
add(c1 + c2, `"${c1}" + "${c2}"`);
|
|
469
|
+
add(c1 + paddingChar, `"${c1}" + padding`);
|
|
470
|
+
add(paddingChar + c1, `padding + "${c1}"`);
|
|
471
|
+
}
|
|
472
|
+
add(paddingChar + paddingChar, "double padding");
|
|
367
473
|
}
|
|
368
474
|
}
|