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