@ddunigma/node 1.1.6 → 1.1.7
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 +44 -0
- package/dist/cjs/base/BaseDdu.d.ts +13 -24
- package/dist/cjs/base/BaseDdu.js +0 -24
- package/dist/cjs/encoders/Ddu64.d.ts +14 -0
- package/dist/cjs/encoders/Ddu64.js +123 -74
- package/dist/mjs/base/BaseDdu.d.ts +13 -24
- package/dist/mjs/base/BaseDdu.js +0 -24
- package/dist/mjs/encoders/Ddu64.d.ts +14 -0
- package/dist/mjs/encoders/Ddu64.js +124 -74
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -64,6 +64,7 @@ const encoder4 = new Ddu64(undefined, undefined, {
|
|
|
64
64
|
const text = "안녕하세요";
|
|
65
65
|
const encoded = encoder1.encode(text);
|
|
66
66
|
const decoded = encoder1.decode(encoded);
|
|
67
|
+
const decodedBuffer = encoder1.decodeToBuffer(encoded);
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
### 커스텀 Charset 사용
|
|
@@ -82,4 +83,47 @@ const encoder = new Ddu64(koreanChars, "뭐");
|
|
|
82
83
|
const text = "안녕하세요12";
|
|
83
84
|
const encoded = encoder.encode(text);
|
|
84
85
|
const decoded = encoder.decode(encoded);
|
|
86
|
+
const decodedBuffer = encoder.decodeToBuffer(encoded);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## API
|
|
90
|
+
|
|
91
|
+
### `new Ddu64(dduChar?, paddingChar?, options?)`
|
|
92
|
+
|
|
93
|
+
인코더 인스턴스를 생성합니다.
|
|
94
|
+
|
|
95
|
+
**Parameters:**
|
|
96
|
+
- `dduChar` (string | string[]): charset 문자열 또는 배열
|
|
97
|
+
- `paddingChar` (string): 패딩 문자
|
|
98
|
+
- `options` (DduOptions): 옵션 객체
|
|
99
|
+
- `dduSetSymbol`: 미리 정의된 charset 사용
|
|
100
|
+
- `encoding`: Buffer encoding (기본값: 'utf-8')
|
|
101
|
+
- `usePowerOfTwo`: 2의 제곱수 강제 여부
|
|
102
|
+
- `useBuildErrorReturn`: 에러 발생 시 throw 여부
|
|
103
|
+
|
|
104
|
+
### `encode(data: string | Buffer): string`
|
|
105
|
+
|
|
106
|
+
데이터를 인코딩
|
|
107
|
+
|
|
108
|
+
### `decode(encoded: string): string`
|
|
109
|
+
|
|
110
|
+
인코딩된 문자열을 디코딩
|
|
111
|
+
|
|
112
|
+
### `decodeToBuffer(encoded: string): Buffer`
|
|
113
|
+
|
|
114
|
+
인코딩된 문자열을 Buffer로 직접 디코딩
|
|
115
|
+
|
|
116
|
+
### `getCharSetInfo()`
|
|
117
|
+
|
|
118
|
+
현재 charset 정보를 반환
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
{
|
|
122
|
+
charSet: string[];
|
|
123
|
+
paddingChar: string;
|
|
124
|
+
charLength: number;
|
|
125
|
+
bitLength: number;
|
|
126
|
+
usePowerOfTwo: boolean;
|
|
127
|
+
encoding: BufferEncoding;
|
|
128
|
+
}
|
|
85
129
|
```
|
|
@@ -1,38 +1,27 @@
|
|
|
1
1
|
import { DduOptions, BufferToDduBinaryResult } from "../types";
|
|
2
2
|
export declare abstract class BaseDdu {
|
|
3
3
|
protected readonly defaultEncoding: BufferEncoding;
|
|
4
|
-
/**
|
|
5
|
-
* 이진수 변환 룩업 테이블 (0-255 -> 8비트 이진 문자열)
|
|
6
|
-
*/
|
|
7
4
|
protected readonly binaryLookup: string[];
|
|
8
|
-
/**
|
|
9
|
-
* 정규표현식 특수문자 이스케이프
|
|
10
|
-
*/
|
|
11
5
|
protected escapeRegExp(str: string): string;
|
|
12
|
-
/**
|
|
13
|
-
* 문자열을 지정된 길이로 분할하는 제너레이터
|
|
14
|
-
*/
|
|
15
6
|
protected splitString(s: string, length: number): Generator<string>;
|
|
16
|
-
/**
|
|
17
|
-
* 2의 거듭제곱 길이 계산
|
|
18
|
-
*/
|
|
19
7
|
protected getLargestPowerOfTwo(n: number): number;
|
|
20
|
-
/**
|
|
21
|
-
* 2의 거듭제곱 지수 계산
|
|
22
|
-
*/
|
|
23
8
|
protected getLargestPowerOfTwoExponent(n: number): number;
|
|
24
|
-
/**
|
|
25
|
-
* 비트 길이 계산
|
|
26
|
-
*/
|
|
27
9
|
protected getBitLength(setLength: number): number;
|
|
28
|
-
/**
|
|
29
|
-
* Buffer를 DDU Binary 배열로 변환
|
|
30
|
-
*/
|
|
31
10
|
protected bufferToDduBinary(input: Buffer, bitLength: number): BufferToDduBinaryResult;
|
|
32
|
-
/**
|
|
33
|
-
* DDU Binary를 Buffer로 변환
|
|
34
|
-
*/
|
|
35
11
|
protected dduBinaryToBuffer(decodedBin: string, paddingBits: number): Buffer;
|
|
36
12
|
abstract encode(input: Buffer | string, options?: DduOptions): string;
|
|
13
|
+
abstract decodeToBuffer(input: string, options?: DduOptions): Buffer;
|
|
37
14
|
abstract decode(input: string, options?: DduOptions): string;
|
|
15
|
+
/**
|
|
16
|
+
* 테스트 및 디버깅용 추상 메서드
|
|
17
|
+
* 구현 클래스의 내부 상태 정보를 반환
|
|
18
|
+
*/
|
|
19
|
+
abstract getCharSetInfo(): {
|
|
20
|
+
charSet: string[];
|
|
21
|
+
paddingChar: string;
|
|
22
|
+
charLength: number;
|
|
23
|
+
bitLength: number;
|
|
24
|
+
usePowerOfTwo: boolean;
|
|
25
|
+
encoding: BufferEncoding;
|
|
26
|
+
};
|
|
38
27
|
}
|
package/dist/cjs/base/BaseDdu.js
CHANGED
|
@@ -4,46 +4,25 @@ exports.BaseDdu = void 0;
|
|
|
4
4
|
class BaseDdu {
|
|
5
5
|
constructor() {
|
|
6
6
|
this.defaultEncoding = "utf-8";
|
|
7
|
-
/**
|
|
8
|
-
* 이진수 변환 룩업 테이블 (0-255 -> 8비트 이진 문자열)
|
|
9
|
-
*/
|
|
10
7
|
this.binaryLookup = Array.from({ length: 256 }, (_, i) => i.toString(2).padStart(8, "0"));
|
|
11
8
|
}
|
|
12
|
-
/**
|
|
13
|
-
* 정규표현식 특수문자 이스케이프
|
|
14
|
-
*/
|
|
15
9
|
escapeRegExp(str) {
|
|
16
10
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17
11
|
}
|
|
18
|
-
/**
|
|
19
|
-
* 문자열을 지정된 길이로 분할하는 제너레이터
|
|
20
|
-
*/
|
|
21
12
|
*splitString(s, length) {
|
|
22
13
|
for (let i = 0; i < s.length; i += length) {
|
|
23
14
|
yield s.slice(i, Math.min(i + length, s.length));
|
|
24
15
|
}
|
|
25
16
|
}
|
|
26
|
-
/**
|
|
27
|
-
* 2의 거듭제곱 길이 계산
|
|
28
|
-
*/
|
|
29
17
|
getLargestPowerOfTwo(n) {
|
|
30
18
|
return Math.pow(2, Math.floor(Math.log2(n)));
|
|
31
19
|
}
|
|
32
|
-
/**
|
|
33
|
-
* 2의 거듭제곱 지수 계산
|
|
34
|
-
*/
|
|
35
20
|
getLargestPowerOfTwoExponent(n) {
|
|
36
21
|
return Math.floor(Math.log2(n));
|
|
37
22
|
}
|
|
38
|
-
/**
|
|
39
|
-
* 비트 길이 계산
|
|
40
|
-
*/
|
|
41
23
|
getBitLength(setLength) {
|
|
42
24
|
return Math.ceil(Math.log2(setLength));
|
|
43
25
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Buffer를 DDU Binary 배열로 변환
|
|
46
|
-
*/
|
|
47
26
|
bufferToDduBinary(input, bitLength) {
|
|
48
27
|
// 성능 최적화: reduce 대신 for loop + join 사용
|
|
49
28
|
if (input.length === 0) {
|
|
@@ -61,9 +40,6 @@ class BaseDdu {
|
|
|
61
40
|
}
|
|
62
41
|
return { dduBinary, padding };
|
|
63
42
|
}
|
|
64
|
-
/**
|
|
65
|
-
* DDU Binary를 Buffer로 변환
|
|
66
|
-
*/
|
|
67
43
|
dduBinaryToBuffer(decodedBin, paddingBits) {
|
|
68
44
|
if (paddingBits > 0) {
|
|
69
45
|
decodedBin = decodedBin.slice(0, -paddingBits);
|
|
@@ -10,6 +10,7 @@ export declare class Ddu64 extends BaseDdu {
|
|
|
10
10
|
protected readonly dduBinaryLookup: Map<string, number>;
|
|
11
11
|
private readonly isPredefinedCharSet;
|
|
12
12
|
private readonly effectiveBitLength;
|
|
13
|
+
private readonly maxBinaryValue;
|
|
13
14
|
private readonly binaryChunkToIntFn;
|
|
14
15
|
private readonly indexToBinaryCache;
|
|
15
16
|
constructor(dduChar?: string[] | string, paddingChar?: string, dduOptions?: DduConstructorOptions);
|
|
@@ -17,5 +18,18 @@ export declare class Ddu64 extends BaseDdu {
|
|
|
17
18
|
private getBinaryFromIndex;
|
|
18
19
|
private validateCombinationDuplicates;
|
|
19
20
|
encode(input: Buffer | string, _options?: DduOptions): string;
|
|
21
|
+
decodeToBuffer(input: string, _options?: DduOptions): Buffer;
|
|
20
22
|
decode(input: string, _options?: DduOptions): string;
|
|
23
|
+
/**
|
|
24
|
+
* 테스트 및 디버깅용 getter 메서드
|
|
25
|
+
* 인코더의 내부 상태 정보를 반환
|
|
26
|
+
*/
|
|
27
|
+
getCharSetInfo(): {
|
|
28
|
+
charSet: string[];
|
|
29
|
+
paddingChar: string;
|
|
30
|
+
charLength: number;
|
|
31
|
+
bitLength: number;
|
|
32
|
+
usePowerOfTwo: boolean;
|
|
33
|
+
encoding: BufferEncoding;
|
|
34
|
+
};
|
|
21
35
|
}
|
|
@@ -23,12 +23,23 @@ class Ddu64 extends BaseDdu_1.BaseDdu {
|
|
|
23
23
|
const finalPadding = paddingChar !== null && paddingChar !== void 0 ? paddingChar : dduOptions === null || dduOptions === void 0 ? void 0 : dduOptions.paddingChar;
|
|
24
24
|
if (finalDduChar) {
|
|
25
25
|
// 커스텀 charset
|
|
26
|
-
if (!finalPadding)
|
|
27
|
-
throw new Error(
|
|
28
|
-
|
|
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
|
+
}
|
|
29
39
|
const len = (_b = dduOptions === null || dduOptions === void 0 ? void 0 : dduOptions.requiredLength) !== null && _b !== void 0 ? _b : arr.length;
|
|
30
|
-
if (arr.length < len)
|
|
31
|
-
throw new Error(`
|
|
40
|
+
if (arr.length < len) {
|
|
41
|
+
throw new Error(`[Ddu64 Constructor] Insufficient characters in charset. Required: ${len}, Provided: ${arr.length}`);
|
|
42
|
+
}
|
|
32
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);
|
|
33
44
|
if (usePow2) {
|
|
34
45
|
const exp = this.getLargestPowerOfTwoExponent(len);
|
|
@@ -73,22 +84,25 @@ class Ddu64 extends BaseDdu_1.BaseDdu {
|
|
|
73
84
|
this.dduChar = normalized.charSet;
|
|
74
85
|
this.paddingChar = normalized.padding;
|
|
75
86
|
this.charLength = normalized.charLength;
|
|
76
|
-
this.bitLength = normalized.bitLength;
|
|
77
87
|
this.isPredefinedCharSet = normalized.isPredefined;
|
|
78
88
|
this.encoding = (_g = dduOptions === null || dduOptions === void 0 ? void 0 : dduOptions.encoding) !== null && _g !== void 0 ? _g : this.defaultEncoding;
|
|
79
89
|
const dduLength = this.dduChar.length;
|
|
90
|
+
const recalculatedBitLength = this.getBitLength(dduLength);
|
|
80
91
|
this.usePowerOfTwo = dduLength > 0 && (dduLength & (dduLength - 1)) === 0;
|
|
81
|
-
this.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
92
|
+
this.bitLength = this.usePowerOfTwo
|
|
93
|
+
? this.getLargestPowerOfTwoExponent(dduLength)
|
|
94
|
+
: recalculatedBitLength;
|
|
95
|
+
this.effectiveBitLength = this.usePowerOfTwo ? this.bitLength : recalculatedBitLength;
|
|
96
|
+
this.maxBinaryValue = Math.pow(2, this.effectiveBitLength);
|
|
97
|
+
// 성능 최적화: 단일 구현 사용 (벤치마크 결과 Method2가 더 빠르고 안정적)
|
|
98
|
+
// Method2는 32비트 이상에서도 오버플로우 없이 정확한 결과 제공
|
|
99
|
+
this.binaryChunkToIntFn = (chunk) => chunk.split('').reduce((v, c) => v * 2 + (c.charCodeAt(0) & 1), 0);
|
|
85
100
|
this.dduChar.forEach((char, index) => this.dduBinaryLookup.set(char, index));
|
|
86
|
-
if (this.charLength
|
|
101
|
+
if (this.charLength === 1 && !this.isPredefinedCharSet) {
|
|
87
102
|
this.validateCombinationDuplicates(this.dduChar, this.paddingChar, dduLength);
|
|
88
103
|
}
|
|
89
104
|
if (this.effectiveBitLength <= 16) {
|
|
90
|
-
|
|
91
|
-
this.indexToBinaryCache = Array.from({ length: size }, (_, i) => i.toString(2).padStart(this.effectiveBitLength, "0"));
|
|
105
|
+
this.indexToBinaryCache = Array.from({ length: this.maxBinaryValue }, (_, i) => i.toString(2).padStart(this.effectiveBitLength, "0"));
|
|
92
106
|
}
|
|
93
107
|
else {
|
|
94
108
|
this.indexToBinaryCache = null;
|
|
@@ -114,50 +128,67 @@ class Ddu64 extends BaseDdu_1.BaseDdu {
|
|
|
114
128
|
}
|
|
115
129
|
return false;
|
|
116
130
|
};
|
|
117
|
-
// 중복 제거 (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
131
|
+
// 중복 제거 (모든 charset에 적용)
|
|
132
|
+
const uniqueChars = new Set(charSet);
|
|
133
|
+
if (uniqueChars.size !== charSet.length) {
|
|
134
|
+
const duplicates = charSet.filter((char, index) => charSet.indexOf(char) !== index);
|
|
135
|
+
const errorMsg = `[Ddu64 normalizeCharSet] Character set contains duplicate characters. Total: ${charSet.length}, Unique: ${uniqueChars.size}, Duplicates: [${[...new Set(duplicates)].join(', ')}]`;
|
|
136
|
+
if (shouldThrowError)
|
|
137
|
+
throw new Error(errorMsg);
|
|
138
|
+
// 중복 제거 후 계속 진행
|
|
139
|
+
charSet = Array.from(uniqueChars);
|
|
140
|
+
if (!isPredefined) {
|
|
124
141
|
requiredLength = charSet.length;
|
|
125
142
|
}
|
|
126
143
|
}
|
|
127
144
|
// 검증
|
|
128
|
-
if (validate(charSet.length < requiredLength,
|
|
145
|
+
if (validate(charSet.length < requiredLength, `[Ddu64 normalizeCharSet] Insufficient characters. Required: ${requiredLength}, Provided: ${charSet.length}`)) {
|
|
146
|
+
return this.normalizeCharSet(charSet, padding, requiredLength, bitLength, isPredefined, shouldThrowError, dduOptions);
|
|
147
|
+
}
|
|
148
|
+
if (validate(requiredLength < 2, `[Ddu64 normalizeCharSet] At least 2 unique characters are required. Provided: ${requiredLength}`)) {
|
|
149
|
+
return this.normalizeCharSet(charSet, padding, requiredLength, bitLength, isPredefined, shouldThrowError, dduOptions);
|
|
150
|
+
}
|
|
151
|
+
if (validate(bitLength <= 0, `[Ddu64 normalizeCharSet] Invalid bit length (${bitLength}) for charset size ${requiredLength}`)) {
|
|
129
152
|
return this.normalizeCharSet(charSet, padding, requiredLength, bitLength, isPredefined, shouldThrowError, dduOptions);
|
|
130
153
|
}
|
|
131
154
|
let charLength = (_b = (_a = charSet[0]) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
|
|
132
|
-
validate(charLength === 0,
|
|
155
|
+
validate(charLength === 0, `[Ddu64 normalizeCharSet] Empty charset. Required: ${requiredLength} characters`);
|
|
133
156
|
const invalidIndex = charSet.findIndex(char => char.length !== charLength);
|
|
134
157
|
if (invalidIndex !== -1) {
|
|
135
|
-
if (shouldThrowError)
|
|
136
|
-
throw new Error(`
|
|
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}`);
|
|
160
|
+
}
|
|
137
161
|
charSet = charSet.filter(char => char.length === charLength);
|
|
138
|
-
validate(charSet.length < requiredLength,
|
|
162
|
+
validate(charSet.length < requiredLength, `[Ddu64 normalizeCharSet] Insufficient characters after filtering. Required: ${requiredLength}, Remaining: ${charSet.length}`);
|
|
139
163
|
charLength = (_d = (_c = charSet[0]) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0;
|
|
140
164
|
}
|
|
141
|
-
validate(!charSet.length,
|
|
142
|
-
validate(padding.length !== charLength, `Padding character
|
|
165
|
+
validate(!charSet.length, `[Ddu64 normalizeCharSet] Empty charset after validation`);
|
|
166
|
+
validate(padding.length !== charLength, `[Ddu64 normalizeCharSet] Padding character length mismatch. Expected: ${charLength}, Got: ${padding.length} (padding: "${padding}")`);
|
|
143
167
|
if (new Set(charSet).has(padding)) {
|
|
144
|
-
if (shouldThrowError)
|
|
145
|
-
throw new Error(`Padding character "${padding}"
|
|
168
|
+
if (shouldThrowError) {
|
|
169
|
+
throw new Error(`[Ddu64 normalizeCharSet] Padding character "${padding}" conflicts with charset. Padding must not be in the character set.`);
|
|
170
|
+
}
|
|
146
171
|
charSet = charSet.filter(char => char !== padding);
|
|
147
|
-
validate(charSet.length < requiredLength,
|
|
172
|
+
validate(charSet.length < requiredLength, `[Ddu64 normalizeCharSet] Insufficient characters after removing padding conflict. Required: ${requiredLength}, Remaining: ${charSet.length}`);
|
|
148
173
|
charLength = (_f = (_e = charSet[0]) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0;
|
|
149
174
|
}
|
|
150
175
|
return { charSet: charSet.slice(0, requiredLength), padding, requiredLength, bitLength, isPredefined, charLength };
|
|
151
176
|
}
|
|
152
177
|
getBinaryFromIndex(index) {
|
|
153
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}`);
|
|
181
|
+
}
|
|
154
182
|
return (_b = (_a = this.indexToBinaryCache) === null || _a === void 0 ? void 0 : _a[index]) !== null && _b !== void 0 ? _b : index.toString(2).padStart(this.effectiveBitLength, "0");
|
|
155
183
|
}
|
|
156
184
|
validateCombinationDuplicates(charSet, paddingChar, requiredLength) {
|
|
157
185
|
const charLength = charSet[0].length;
|
|
186
|
+
// 조합 충돌 검사는 단일 문자 집합이면서 비교적 작은 경우(<=256)에만 적용한다.
|
|
187
|
+
if (charLength !== 1 || requiredLength > 256) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
158
190
|
const allStrings = new Set([...charSet.slice(0, requiredLength), paddingChar]);
|
|
159
191
|
const limit = Math.min(charSet.length, requiredLength);
|
|
160
|
-
// 2개 조합 검사
|
|
161
192
|
for (let i = 0; i < limit; i++) {
|
|
162
193
|
for (let j = 0; j < limit; j++) {
|
|
163
194
|
const combo = charSet[i] + charSet[j];
|
|
@@ -167,32 +198,17 @@ class Ddu64 extends BaseDdu_1.BaseDdu {
|
|
|
167
198
|
}
|
|
168
199
|
const charPad = charSet[i] + paddingChar;
|
|
169
200
|
const padChar = paddingChar + charSet[i];
|
|
170
|
-
if (allStrings.has(charPad))
|
|
201
|
+
if (allStrings.has(charPad)) {
|
|
171
202
|
throw new Error(`Combination conflict: "${charSet[i]}" + padding "${paddingChar}" = "${charPad}"`);
|
|
172
|
-
|
|
203
|
+
}
|
|
204
|
+
if (allStrings.has(padChar)) {
|
|
173
205
|
throw new Error(`Combination conflict: padding "${paddingChar}" + "${charSet[i]}" = "${padChar}"`);
|
|
206
|
+
}
|
|
174
207
|
}
|
|
175
|
-
// 패딩 + 패딩 조합 검사
|
|
176
208
|
const doublePad = paddingChar + paddingChar;
|
|
177
209
|
if (allStrings.has(doublePad)) {
|
|
178
210
|
throw new Error(`Combination conflict: double padding "${doublePad}" already exists`);
|
|
179
211
|
}
|
|
180
|
-
// 3개 조합 검사 (100개 이하만)
|
|
181
|
-
if (charLength >= 2 && requiredLength <= 100) {
|
|
182
|
-
for (let i = 0; i < limit; i++) {
|
|
183
|
-
for (let j = 0; j < limit; j++) {
|
|
184
|
-
for (let k = 0; k < limit; k++) {
|
|
185
|
-
const combo = charSet[i] + charSet[j] + charSet[k];
|
|
186
|
-
for (let start = 0; start <= combo.length - charLength; start++) {
|
|
187
|
-
const sub = combo.substring(start, start + charLength);
|
|
188
|
-
if (allStrings.has(sub) && sub !== charSet[i] && sub !== charSet[j] && sub !== charSet[k]) {
|
|
189
|
-
throw new Error(`Combination conflict: substring "${sub}" from "${charSet[i]}" + "${charSet[j]}" + "${charSet[k]}"`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
212
|
}
|
|
197
213
|
encode(input, _options) {
|
|
198
214
|
// options는 구버전 호환성을 위해 유지하지만 사용하지 않음
|
|
@@ -207,8 +223,8 @@ class Ddu64 extends BaseDdu_1.BaseDdu {
|
|
|
207
223
|
for (let i = 0; i < dduBinary.length; i++) {
|
|
208
224
|
const binaryChunk = dduBinary[i];
|
|
209
225
|
const charInt = this.binaryChunkToIntFn(binaryChunk);
|
|
210
|
-
if (
|
|
211
|
-
// 가변 길이 조합 인코딩
|
|
226
|
+
if (!this.usePowerOfTwo) {
|
|
227
|
+
// 가변 길이 조합 인코딩 (멀티바이트 문자도 지원)
|
|
212
228
|
const quotient = Math.floor(charInt / dduLength);
|
|
213
229
|
const remainder = charInt % dduLength;
|
|
214
230
|
resultParts[i] = this.dduChar[quotient] + this.dduChar[remainder];
|
|
@@ -225,45 +241,78 @@ class Ddu64 extends BaseDdu_1.BaseDdu {
|
|
|
225
241
|
}
|
|
226
242
|
return resultString;
|
|
227
243
|
}
|
|
228
|
-
|
|
229
|
-
// options는 구버전 호환성을 위해 유지하지만 사용하지 않음
|
|
230
|
-
// 패딩 정보 추출
|
|
244
|
+
decodeToBuffer(input, _options) {
|
|
231
245
|
let paddingBits = 0;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
246
|
+
if (input.length >= this.paddingChar.length) {
|
|
247
|
+
const padCharIndex = input.lastIndexOf(this.paddingChar);
|
|
248
|
+
if (padCharIndex >= 0 &&
|
|
249
|
+
padCharIndex % this.charLength === 0 &&
|
|
250
|
+
padCharIndex + this.paddingChar.length <= input.length) {
|
|
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
|
+
}
|
|
237
264
|
}
|
|
238
265
|
let dduBinary = "";
|
|
239
|
-
// 생성자에서 설정된 값 사용
|
|
240
266
|
const dduLength = this.dduChar.length;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
const secondChar = input[i + 1];
|
|
267
|
+
if (!this.usePowerOfTwo) {
|
|
268
|
+
const chunkSize = this.charLength * 2;
|
|
269
|
+
for (let i = 0; i < input.length; i += chunkSize) {
|
|
270
|
+
const firstChar = input.slice(i, i + this.charLength);
|
|
271
|
+
const secondChar = input.slice(i + this.charLength, i + chunkSize);
|
|
247
272
|
const firstIndex = this.dduBinaryLookup.get(firstChar);
|
|
248
273
|
const secondIndex = this.dduBinaryLookup.get(secondChar);
|
|
249
|
-
if (firstIndex === undefined || secondIndex === undefined)
|
|
250
|
-
|
|
274
|
+
if (firstIndex === undefined || secondIndex === undefined) {
|
|
275
|
+
const invalidChar = firstIndex === undefined ? firstChar : secondChar;
|
|
276
|
+
throw new Error(`[Ddu64 decode] Invalid character in encoded string. Character: "${invalidChar}", Position: ${i}, Expected charset size: ${dduLength}`);
|
|
277
|
+
}
|
|
251
278
|
const value = firstIndex * dduLength + secondIndex;
|
|
279
|
+
if (value >= this.maxBinaryValue) {
|
|
280
|
+
throw new Error(`[Ddu64 decode] Invalid character combination detected. Calculated value ${value} exceeds binary range ${this.maxBinaryValue - 1}.`);
|
|
281
|
+
}
|
|
252
282
|
dduBinary += this.getBinaryFromIndex(value);
|
|
253
283
|
}
|
|
254
284
|
}
|
|
255
285
|
else {
|
|
256
|
-
// 고정 길이 직접 매핑 (charLength만큼씩 읽음)
|
|
257
286
|
for (let i = 0; i < input.length; i += this.charLength) {
|
|
258
287
|
const charChunk = input.slice(i, i + this.charLength);
|
|
259
288
|
const charIndex = this.dduBinaryLookup.get(charChunk);
|
|
260
|
-
if (charIndex === undefined)
|
|
261
|
-
throw new Error(`Invalid character: ${charChunk}`);
|
|
289
|
+
if (charIndex === undefined) {
|
|
290
|
+
throw new Error(`[Ddu64 decode] Invalid character in encoded string. Character: "${charChunk}", Position: ${i}, Charset size: ${dduLength}, Character length: ${this.charLength}`);
|
|
291
|
+
}
|
|
292
|
+
if (charIndex >= this.maxBinaryValue) {
|
|
293
|
+
throw new Error(`[Ddu64 decode] Invalid binary index ${charIndex}. Allowed range: 0-${this.maxBinaryValue - 1}`);
|
|
294
|
+
}
|
|
262
295
|
dduBinary += this.getBinaryFromIndex(charIndex);
|
|
263
296
|
}
|
|
264
297
|
}
|
|
265
|
-
|
|
266
|
-
|
|
298
|
+
return this.dduBinaryToBuffer(dduBinary, paddingBits);
|
|
299
|
+
}
|
|
300
|
+
decode(input, _options) {
|
|
301
|
+
return this.decodeToBuffer(input, _options).toString(this.encoding);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* 테스트 및 디버깅용 getter 메서드
|
|
305
|
+
* 인코더의 내부 상태 정보를 반환
|
|
306
|
+
*/
|
|
307
|
+
getCharSetInfo() {
|
|
308
|
+
return {
|
|
309
|
+
charSet: [...this.dduChar],
|
|
310
|
+
paddingChar: this.paddingChar,
|
|
311
|
+
charLength: this.charLength,
|
|
312
|
+
bitLength: this.bitLength,
|
|
313
|
+
usePowerOfTwo: this.usePowerOfTwo,
|
|
314
|
+
encoding: this.encoding,
|
|
315
|
+
};
|
|
267
316
|
}
|
|
268
317
|
}
|
|
269
318
|
exports.Ddu64 = Ddu64;
|
|
@@ -1,38 +1,27 @@
|
|
|
1
1
|
import { DduOptions, BufferToDduBinaryResult } from "../types";
|
|
2
2
|
export declare abstract class BaseDdu {
|
|
3
3
|
protected readonly defaultEncoding: BufferEncoding;
|
|
4
|
-
/**
|
|
5
|
-
* 이진수 변환 룩업 테이블 (0-255 -> 8비트 이진 문자열)
|
|
6
|
-
*/
|
|
7
4
|
protected readonly binaryLookup: string[];
|
|
8
|
-
/**
|
|
9
|
-
* 정규표현식 특수문자 이스케이프
|
|
10
|
-
*/
|
|
11
5
|
protected escapeRegExp(str: string): string;
|
|
12
|
-
/**
|
|
13
|
-
* 문자열을 지정된 길이로 분할하는 제너레이터
|
|
14
|
-
*/
|
|
15
6
|
protected splitString(s: string, length: number): Generator<string>;
|
|
16
|
-
/**
|
|
17
|
-
* 2의 거듭제곱 길이 계산
|
|
18
|
-
*/
|
|
19
7
|
protected getLargestPowerOfTwo(n: number): number;
|
|
20
|
-
/**
|
|
21
|
-
* 2의 거듭제곱 지수 계산
|
|
22
|
-
*/
|
|
23
8
|
protected getLargestPowerOfTwoExponent(n: number): number;
|
|
24
|
-
/**
|
|
25
|
-
* 비트 길이 계산
|
|
26
|
-
*/
|
|
27
9
|
protected getBitLength(setLength: number): number;
|
|
28
|
-
/**
|
|
29
|
-
* Buffer를 DDU Binary 배열로 변환
|
|
30
|
-
*/
|
|
31
10
|
protected bufferToDduBinary(input: Buffer, bitLength: number): BufferToDduBinaryResult;
|
|
32
|
-
/**
|
|
33
|
-
* DDU Binary를 Buffer로 변환
|
|
34
|
-
*/
|
|
35
11
|
protected dduBinaryToBuffer(decodedBin: string, paddingBits: number): Buffer;
|
|
36
12
|
abstract encode(input: Buffer | string, options?: DduOptions): string;
|
|
13
|
+
abstract decodeToBuffer(input: string, options?: DduOptions): Buffer;
|
|
37
14
|
abstract decode(input: string, options?: DduOptions): string;
|
|
15
|
+
/**
|
|
16
|
+
* 테스트 및 디버깅용 추상 메서드
|
|
17
|
+
* 구현 클래스의 내부 상태 정보를 반환
|
|
18
|
+
*/
|
|
19
|
+
abstract getCharSetInfo(): {
|
|
20
|
+
charSet: string[];
|
|
21
|
+
paddingChar: string;
|
|
22
|
+
charLength: number;
|
|
23
|
+
bitLength: number;
|
|
24
|
+
usePowerOfTwo: boolean;
|
|
25
|
+
encoding: BufferEncoding;
|
|
26
|
+
};
|
|
38
27
|
}
|
package/dist/mjs/base/BaseDdu.js
CHANGED
|
@@ -1,44 +1,23 @@
|
|
|
1
1
|
export class BaseDdu {
|
|
2
2
|
defaultEncoding = "utf-8";
|
|
3
|
-
/**
|
|
4
|
-
* 이진수 변환 룩업 테이블 (0-255 -> 8비트 이진 문자열)
|
|
5
|
-
*/
|
|
6
3
|
binaryLookup = Array.from({ length: 256 }, (_, i) => i.toString(2).padStart(8, "0"));
|
|
7
|
-
/**
|
|
8
|
-
* 정규표현식 특수문자 이스케이프
|
|
9
|
-
*/
|
|
10
4
|
escapeRegExp(str) {
|
|
11
5
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
12
6
|
}
|
|
13
|
-
/**
|
|
14
|
-
* 문자열을 지정된 길이로 분할하는 제너레이터
|
|
15
|
-
*/
|
|
16
7
|
*splitString(s, length) {
|
|
17
8
|
for (let i = 0; i < s.length; i += length) {
|
|
18
9
|
yield s.slice(i, Math.min(i + length, s.length));
|
|
19
10
|
}
|
|
20
11
|
}
|
|
21
|
-
/**
|
|
22
|
-
* 2의 거듭제곱 길이 계산
|
|
23
|
-
*/
|
|
24
12
|
getLargestPowerOfTwo(n) {
|
|
25
13
|
return 2 ** Math.floor(Math.log2(n));
|
|
26
14
|
}
|
|
27
|
-
/**
|
|
28
|
-
* 2의 거듭제곱 지수 계산
|
|
29
|
-
*/
|
|
30
15
|
getLargestPowerOfTwoExponent(n) {
|
|
31
16
|
return Math.floor(Math.log2(n));
|
|
32
17
|
}
|
|
33
|
-
/**
|
|
34
|
-
* 비트 길이 계산
|
|
35
|
-
*/
|
|
36
18
|
getBitLength(setLength) {
|
|
37
19
|
return Math.ceil(Math.log2(setLength));
|
|
38
20
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Buffer를 DDU Binary 배열로 변환
|
|
41
|
-
*/
|
|
42
21
|
bufferToDduBinary(input, bitLength) {
|
|
43
22
|
// 성능 최적화: reduce 대신 for loop + join 사용
|
|
44
23
|
if (input.length === 0) {
|
|
@@ -56,9 +35,6 @@ export class BaseDdu {
|
|
|
56
35
|
}
|
|
57
36
|
return { dduBinary, padding };
|
|
58
37
|
}
|
|
59
|
-
/**
|
|
60
|
-
* DDU Binary를 Buffer로 변환
|
|
61
|
-
*/
|
|
62
38
|
dduBinaryToBuffer(decodedBin, paddingBits) {
|
|
63
39
|
if (paddingBits > 0) {
|
|
64
40
|
decodedBin = decodedBin.slice(0, -paddingBits);
|
|
@@ -10,6 +10,7 @@ export declare class Ddu64 extends BaseDdu {
|
|
|
10
10
|
protected readonly dduBinaryLookup: Map<string, number>;
|
|
11
11
|
private readonly isPredefinedCharSet;
|
|
12
12
|
private readonly effectiveBitLength;
|
|
13
|
+
private readonly maxBinaryValue;
|
|
13
14
|
private readonly binaryChunkToIntFn;
|
|
14
15
|
private readonly indexToBinaryCache;
|
|
15
16
|
constructor(dduChar?: string[] | string, paddingChar?: string, dduOptions?: DduConstructorOptions);
|
|
@@ -17,5 +18,18 @@ export declare class Ddu64 extends BaseDdu {
|
|
|
17
18
|
private getBinaryFromIndex;
|
|
18
19
|
private validateCombinationDuplicates;
|
|
19
20
|
encode(input: Buffer | string, _options?: DduOptions): string;
|
|
21
|
+
decodeToBuffer(input: string, _options?: DduOptions): Buffer;
|
|
20
22
|
decode(input: string, _options?: DduOptions): string;
|
|
23
|
+
/**
|
|
24
|
+
* 테스트 및 디버깅용 getter 메서드
|
|
25
|
+
* 인코더의 내부 상태 정보를 반환
|
|
26
|
+
*/
|
|
27
|
+
getCharSetInfo(): {
|
|
28
|
+
charSet: string[];
|
|
29
|
+
paddingChar: string;
|
|
30
|
+
charLength: number;
|
|
31
|
+
bitLength: number;
|
|
32
|
+
usePowerOfTwo: boolean;
|
|
33
|
+
encoding: BufferEncoding;
|
|
34
|
+
};
|
|
21
35
|
}
|
|
@@ -11,6 +11,7 @@ export class Ddu64 extends BaseDdu {
|
|
|
11
11
|
dduBinaryLookup = new Map();
|
|
12
12
|
isPredefinedCharSet;
|
|
13
13
|
effectiveBitLength;
|
|
14
|
+
maxBinaryValue;
|
|
14
15
|
binaryChunkToIntFn;
|
|
15
16
|
indexToBinaryCache;
|
|
16
17
|
constructor(dduChar, paddingChar, dduOptions) {
|
|
@@ -29,12 +30,23 @@ export class Ddu64 extends BaseDdu {
|
|
|
29
30
|
const finalPadding = paddingChar ?? dduOptions?.paddingChar;
|
|
30
31
|
if (finalDduChar) {
|
|
31
32
|
// 커스텀 charset
|
|
32
|
-
if (!finalPadding)
|
|
33
|
-
throw new Error(
|
|
34
|
-
|
|
33
|
+
if (!finalPadding) {
|
|
34
|
+
throw new Error(`[Ddu64 Constructor] paddingChar is required when dduChar is provided. Received: dduChar=${typeof finalDduChar}, paddingChar=${finalPadding}`);
|
|
35
|
+
}
|
|
36
|
+
// 문자열을 배열로 변환 (중복 제거 없이)
|
|
37
|
+
const arr = typeof finalDduChar === "string" ? [...finalDduChar.trim()] : finalDduChar;
|
|
38
|
+
// 중복 검사 (useBuildErrorReturn이 true일 때만)
|
|
39
|
+
if (shouldThrowError) {
|
|
40
|
+
const uniqueChars = new Set(arr);
|
|
41
|
+
if (uniqueChars.size !== arr.length) {
|
|
42
|
+
const duplicates = arr.filter((char, index) => arr.indexOf(char) !== index);
|
|
43
|
+
throw new Error(`[Ddu64 Constructor] Character set contains duplicate characters. Total: ${arr.length}, Unique: ${uniqueChars.size}, Duplicates: [${[...new Set(duplicates)].join(', ')}]`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
35
46
|
const len = dduOptions?.requiredLength ?? arr.length;
|
|
36
|
-
if (arr.length < len)
|
|
37
|
-
throw new Error(`
|
|
47
|
+
if (arr.length < len) {
|
|
48
|
+
throw new Error(`[Ddu64 Constructor] Insufficient characters in charset. Required: ${len}, Provided: ${arr.length}`);
|
|
49
|
+
}
|
|
38
50
|
const usePow2 = dduOptions?.usePowerOfTwo === true || (dduOptions?.usePowerOfTwo === undefined && len > 0 && (len & (len - 1)) === 0);
|
|
39
51
|
if (usePow2) {
|
|
40
52
|
const exp = this.getLargestPowerOfTwoExponent(len);
|
|
@@ -79,22 +91,25 @@ export class Ddu64 extends BaseDdu {
|
|
|
79
91
|
this.dduChar = normalized.charSet;
|
|
80
92
|
this.paddingChar = normalized.padding;
|
|
81
93
|
this.charLength = normalized.charLength;
|
|
82
|
-
this.bitLength = normalized.bitLength;
|
|
83
94
|
this.isPredefinedCharSet = normalized.isPredefined;
|
|
84
95
|
this.encoding = dduOptions?.encoding ?? this.defaultEncoding;
|
|
85
96
|
const dduLength = this.dduChar.length;
|
|
97
|
+
const recalculatedBitLength = this.getBitLength(dduLength);
|
|
86
98
|
this.usePowerOfTwo = dduLength > 0 && (dduLength & (dduLength - 1)) === 0;
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
this.bitLength = this.usePowerOfTwo
|
|
100
|
+
? this.getLargestPowerOfTwoExponent(dduLength)
|
|
101
|
+
: recalculatedBitLength;
|
|
102
|
+
this.effectiveBitLength = this.usePowerOfTwo ? this.bitLength : recalculatedBitLength;
|
|
103
|
+
this.maxBinaryValue = 2 ** this.effectiveBitLength;
|
|
104
|
+
// 성능 최적화: 단일 구현 사용 (벤치마크 결과 Method2가 더 빠르고 안정적)
|
|
105
|
+
// Method2는 32비트 이상에서도 오버플로우 없이 정확한 결과 제공
|
|
106
|
+
this.binaryChunkToIntFn = (chunk) => chunk.split('').reduce((v, c) => v * 2 + (c.charCodeAt(0) & 1), 0);
|
|
91
107
|
this.dduChar.forEach((char, index) => this.dduBinaryLookup.set(char, index));
|
|
92
|
-
if (this.charLength
|
|
108
|
+
if (this.charLength === 1 && !this.isPredefinedCharSet) {
|
|
93
109
|
this.validateCombinationDuplicates(this.dduChar, this.paddingChar, dduLength);
|
|
94
110
|
}
|
|
95
111
|
if (this.effectiveBitLength <= 16) {
|
|
96
|
-
|
|
97
|
-
this.indexToBinaryCache = Array.from({ length: size }, (_, i) => i.toString(2).padStart(this.effectiveBitLength, "0"));
|
|
112
|
+
this.indexToBinaryCache = Array.from({ length: this.maxBinaryValue }, (_, i) => i.toString(2).padStart(this.effectiveBitLength, "0"));
|
|
98
113
|
}
|
|
99
114
|
else {
|
|
100
115
|
this.indexToBinaryCache = null;
|
|
@@ -118,49 +133,66 @@ export class Ddu64 extends BaseDdu {
|
|
|
118
133
|
}
|
|
119
134
|
return false;
|
|
120
135
|
};
|
|
121
|
-
// 중복 제거 (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
136
|
+
// 중복 제거 (모든 charset에 적용)
|
|
137
|
+
const uniqueChars = new Set(charSet);
|
|
138
|
+
if (uniqueChars.size !== charSet.length) {
|
|
139
|
+
const duplicates = charSet.filter((char, index) => charSet.indexOf(char) !== index);
|
|
140
|
+
const errorMsg = `[Ddu64 normalizeCharSet] Character set contains duplicate characters. Total: ${charSet.length}, Unique: ${uniqueChars.size}, Duplicates: [${[...new Set(duplicates)].join(', ')}]`;
|
|
141
|
+
if (shouldThrowError)
|
|
142
|
+
throw new Error(errorMsg);
|
|
143
|
+
// 중복 제거 후 계속 진행
|
|
144
|
+
charSet = Array.from(uniqueChars);
|
|
145
|
+
if (!isPredefined) {
|
|
128
146
|
requiredLength = charSet.length;
|
|
129
147
|
}
|
|
130
148
|
}
|
|
131
149
|
// 검증
|
|
132
|
-
if (validate(charSet.length < requiredLength,
|
|
150
|
+
if (validate(charSet.length < requiredLength, `[Ddu64 normalizeCharSet] Insufficient characters. Required: ${requiredLength}, Provided: ${charSet.length}`)) {
|
|
151
|
+
return this.normalizeCharSet(charSet, padding, requiredLength, bitLength, isPredefined, shouldThrowError, dduOptions);
|
|
152
|
+
}
|
|
153
|
+
if (validate(requiredLength < 2, `[Ddu64 normalizeCharSet] At least 2 unique characters are required. Provided: ${requiredLength}`)) {
|
|
154
|
+
return this.normalizeCharSet(charSet, padding, requiredLength, bitLength, isPredefined, shouldThrowError, dduOptions);
|
|
155
|
+
}
|
|
156
|
+
if (validate(bitLength <= 0, `[Ddu64 normalizeCharSet] Invalid bit length (${bitLength}) for charset size ${requiredLength}`)) {
|
|
133
157
|
return this.normalizeCharSet(charSet, padding, requiredLength, bitLength, isPredefined, shouldThrowError, dduOptions);
|
|
134
158
|
}
|
|
135
159
|
let charLength = charSet[0]?.length ?? 0;
|
|
136
|
-
validate(charLength === 0,
|
|
160
|
+
validate(charLength === 0, `[Ddu64 normalizeCharSet] Empty charset. Required: ${requiredLength} characters`);
|
|
137
161
|
const invalidIndex = charSet.findIndex(char => char.length !== charLength);
|
|
138
162
|
if (invalidIndex !== -1) {
|
|
139
|
-
if (shouldThrowError)
|
|
140
|
-
throw new Error(`
|
|
163
|
+
if (shouldThrowError) {
|
|
164
|
+
throw new Error(`[Ddu64 normalizeCharSet] Inconsistent character length. Expected: ${charLength}, but character at index ${invalidIndex} ("${charSet[invalidIndex]}") has length ${charSet[invalidIndex].length}`);
|
|
165
|
+
}
|
|
141
166
|
charSet = charSet.filter(char => char.length === charLength);
|
|
142
|
-
validate(charSet.length < requiredLength,
|
|
167
|
+
validate(charSet.length < requiredLength, `[Ddu64 normalizeCharSet] Insufficient characters after filtering. Required: ${requiredLength}, Remaining: ${charSet.length}`);
|
|
143
168
|
charLength = charSet[0]?.length ?? 0;
|
|
144
169
|
}
|
|
145
|
-
validate(!charSet.length,
|
|
146
|
-
validate(padding.length !== charLength, `Padding character
|
|
170
|
+
validate(!charSet.length, `[Ddu64 normalizeCharSet] Empty charset after validation`);
|
|
171
|
+
validate(padding.length !== charLength, `[Ddu64 normalizeCharSet] Padding character length mismatch. Expected: ${charLength}, Got: ${padding.length} (padding: "${padding}")`);
|
|
147
172
|
if (new Set(charSet).has(padding)) {
|
|
148
|
-
if (shouldThrowError)
|
|
149
|
-
throw new Error(`Padding character "${padding}"
|
|
173
|
+
if (shouldThrowError) {
|
|
174
|
+
throw new Error(`[Ddu64 normalizeCharSet] Padding character "${padding}" conflicts with charset. Padding must not be in the character set.`);
|
|
175
|
+
}
|
|
150
176
|
charSet = charSet.filter(char => char !== padding);
|
|
151
|
-
validate(charSet.length < requiredLength,
|
|
177
|
+
validate(charSet.length < requiredLength, `[Ddu64 normalizeCharSet] Insufficient characters after removing padding conflict. Required: ${requiredLength}, Remaining: ${charSet.length}`);
|
|
152
178
|
charLength = charSet[0]?.length ?? 0;
|
|
153
179
|
}
|
|
154
180
|
return { charSet: charSet.slice(0, requiredLength), padding, requiredLength, bitLength, isPredefined, charLength };
|
|
155
181
|
}
|
|
156
182
|
getBinaryFromIndex(index) {
|
|
183
|
+
if (index < 0 || index >= this.maxBinaryValue) {
|
|
184
|
+
throw new Error(`[Ddu64] Binary index overflow. Received: ${index}, Allowed range: 0-${this.maxBinaryValue - 1}`);
|
|
185
|
+
}
|
|
157
186
|
return this.indexToBinaryCache?.[index] ?? index.toString(2).padStart(this.effectiveBitLength, "0");
|
|
158
187
|
}
|
|
159
188
|
validateCombinationDuplicates(charSet, paddingChar, requiredLength) {
|
|
160
189
|
const charLength = charSet[0].length;
|
|
190
|
+
// 조합 충돌 검사는 단일 문자 집합이면서 비교적 작은 경우(<=256)에만 적용한다.
|
|
191
|
+
if (charLength !== 1 || requiredLength > 256) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
161
194
|
const allStrings = new Set([...charSet.slice(0, requiredLength), paddingChar]);
|
|
162
195
|
const limit = Math.min(charSet.length, requiredLength);
|
|
163
|
-
// 2개 조합 검사
|
|
164
196
|
for (let i = 0; i < limit; i++) {
|
|
165
197
|
for (let j = 0; j < limit; j++) {
|
|
166
198
|
const combo = charSet[i] + charSet[j];
|
|
@@ -170,32 +202,17 @@ export class Ddu64 extends BaseDdu {
|
|
|
170
202
|
}
|
|
171
203
|
const charPad = charSet[i] + paddingChar;
|
|
172
204
|
const padChar = paddingChar + charSet[i];
|
|
173
|
-
if (allStrings.has(charPad))
|
|
205
|
+
if (allStrings.has(charPad)) {
|
|
174
206
|
throw new Error(`Combination conflict: "${charSet[i]}" + padding "${paddingChar}" = "${charPad}"`);
|
|
175
|
-
|
|
207
|
+
}
|
|
208
|
+
if (allStrings.has(padChar)) {
|
|
176
209
|
throw new Error(`Combination conflict: padding "${paddingChar}" + "${charSet[i]}" = "${padChar}"`);
|
|
210
|
+
}
|
|
177
211
|
}
|
|
178
|
-
// 패딩 + 패딩 조합 검사
|
|
179
212
|
const doublePad = paddingChar + paddingChar;
|
|
180
213
|
if (allStrings.has(doublePad)) {
|
|
181
214
|
throw new Error(`Combination conflict: double padding "${doublePad}" already exists`);
|
|
182
215
|
}
|
|
183
|
-
// 3개 조합 검사 (100개 이하만)
|
|
184
|
-
if (charLength >= 2 && requiredLength <= 100) {
|
|
185
|
-
for (let i = 0; i < limit; i++) {
|
|
186
|
-
for (let j = 0; j < limit; j++) {
|
|
187
|
-
for (let k = 0; k < limit; k++) {
|
|
188
|
-
const combo = charSet[i] + charSet[j] + charSet[k];
|
|
189
|
-
for (let start = 0; start <= combo.length - charLength; start++) {
|
|
190
|
-
const sub = combo.substring(start, start + charLength);
|
|
191
|
-
if (allStrings.has(sub) && sub !== charSet[i] && sub !== charSet[j] && sub !== charSet[k]) {
|
|
192
|
-
throw new Error(`Combination conflict: substring "${sub}" from "${charSet[i]}" + "${charSet[j]}" + "${charSet[k]}"`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
216
|
}
|
|
200
217
|
encode(input, _options) {
|
|
201
218
|
// options는 구버전 호환성을 위해 유지하지만 사용하지 않음
|
|
@@ -210,8 +227,8 @@ export class Ddu64 extends BaseDdu {
|
|
|
210
227
|
for (let i = 0; i < dduBinary.length; i++) {
|
|
211
228
|
const binaryChunk = dduBinary[i];
|
|
212
229
|
const charInt = this.binaryChunkToIntFn(binaryChunk);
|
|
213
|
-
if (
|
|
214
|
-
// 가변 길이 조합 인코딩
|
|
230
|
+
if (!this.usePowerOfTwo) {
|
|
231
|
+
// 가변 길이 조합 인코딩 (멀티바이트 문자도 지원)
|
|
215
232
|
const quotient = Math.floor(charInt / dduLength);
|
|
216
233
|
const remainder = charInt % dduLength;
|
|
217
234
|
resultParts[i] = this.dduChar[quotient] + this.dduChar[remainder];
|
|
@@ -228,44 +245,77 @@ export class Ddu64 extends BaseDdu {
|
|
|
228
245
|
}
|
|
229
246
|
return resultString;
|
|
230
247
|
}
|
|
231
|
-
|
|
232
|
-
// options는 구버전 호환성을 위해 유지하지만 사용하지 않음
|
|
233
|
-
// 패딩 정보 추출
|
|
248
|
+
decodeToBuffer(input, _options) {
|
|
234
249
|
let paddingBits = 0;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
if (input.length >= this.paddingChar.length) {
|
|
251
|
+
const padCharIndex = input.lastIndexOf(this.paddingChar);
|
|
252
|
+
if (padCharIndex >= 0 &&
|
|
253
|
+
padCharIndex % this.charLength === 0 &&
|
|
254
|
+
padCharIndex + this.paddingChar.length <= input.length) {
|
|
255
|
+
const paddingSection = input.slice(padCharIndex + this.paddingChar.length);
|
|
256
|
+
if (paddingSection.length === 0) {
|
|
257
|
+
throw new Error(`[Ddu64 decode] Invalid padding format. Missing padding length after "${this.paddingChar}"`);
|
|
258
|
+
}
|
|
259
|
+
paddingBits = parseInt(paddingSection, 10);
|
|
260
|
+
if (isNaN(paddingBits) ||
|
|
261
|
+
paddingSection !== paddingBits.toString() ||
|
|
262
|
+
paddingBits < 0 ||
|
|
263
|
+
paddingBits >= this.effectiveBitLength) {
|
|
264
|
+
throw new Error(`[Ddu64 decode] Invalid padding format. Expected integer between 0 and ${this.effectiveBitLength - 1}, Got: "${paddingSection}"`);
|
|
265
|
+
}
|
|
266
|
+
input = input.substring(0, padCharIndex);
|
|
267
|
+
}
|
|
240
268
|
}
|
|
241
269
|
let dduBinary = "";
|
|
242
|
-
// 생성자에서 설정된 값 사용
|
|
243
270
|
const dduLength = this.dduChar.length;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
const secondChar = input[i + 1];
|
|
271
|
+
if (!this.usePowerOfTwo) {
|
|
272
|
+
const chunkSize = this.charLength * 2;
|
|
273
|
+
for (let i = 0; i < input.length; i += chunkSize) {
|
|
274
|
+
const firstChar = input.slice(i, i + this.charLength);
|
|
275
|
+
const secondChar = input.slice(i + this.charLength, i + chunkSize);
|
|
250
276
|
const firstIndex = this.dduBinaryLookup.get(firstChar);
|
|
251
277
|
const secondIndex = this.dduBinaryLookup.get(secondChar);
|
|
252
|
-
if (firstIndex === undefined || secondIndex === undefined)
|
|
253
|
-
|
|
278
|
+
if (firstIndex === undefined || secondIndex === undefined) {
|
|
279
|
+
const invalidChar = firstIndex === undefined ? firstChar : secondChar;
|
|
280
|
+
throw new Error(`[Ddu64 decode] Invalid character in encoded string. Character: "${invalidChar}", Position: ${i}, Expected charset size: ${dduLength}`);
|
|
281
|
+
}
|
|
254
282
|
const value = firstIndex * dduLength + secondIndex;
|
|
283
|
+
if (value >= this.maxBinaryValue) {
|
|
284
|
+
throw new Error(`[Ddu64 decode] Invalid character combination detected. Calculated value ${value} exceeds binary range ${this.maxBinaryValue - 1}.`);
|
|
285
|
+
}
|
|
255
286
|
dduBinary += this.getBinaryFromIndex(value);
|
|
256
287
|
}
|
|
257
288
|
}
|
|
258
289
|
else {
|
|
259
|
-
// 고정 길이 직접 매핑 (charLength만큼씩 읽음)
|
|
260
290
|
for (let i = 0; i < input.length; i += this.charLength) {
|
|
261
291
|
const charChunk = input.slice(i, i + this.charLength);
|
|
262
292
|
const charIndex = this.dduBinaryLookup.get(charChunk);
|
|
263
|
-
if (charIndex === undefined)
|
|
264
|
-
throw new Error(`Invalid character: ${charChunk}`);
|
|
293
|
+
if (charIndex === undefined) {
|
|
294
|
+
throw new Error(`[Ddu64 decode] Invalid character in encoded string. Character: "${charChunk}", Position: ${i}, Charset size: ${dduLength}, Character length: ${this.charLength}`);
|
|
295
|
+
}
|
|
296
|
+
if (charIndex >= this.maxBinaryValue) {
|
|
297
|
+
throw new Error(`[Ddu64 decode] Invalid binary index ${charIndex}. Allowed range: 0-${this.maxBinaryValue - 1}`);
|
|
298
|
+
}
|
|
265
299
|
dduBinary += this.getBinaryFromIndex(charIndex);
|
|
266
300
|
}
|
|
267
301
|
}
|
|
268
|
-
|
|
269
|
-
|
|
302
|
+
return this.dduBinaryToBuffer(dduBinary, paddingBits);
|
|
303
|
+
}
|
|
304
|
+
decode(input, _options) {
|
|
305
|
+
return this.decodeToBuffer(input, _options).toString(this.encoding);
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* 테스트 및 디버깅용 getter 메서드
|
|
309
|
+
* 인코더의 내부 상태 정보를 반환
|
|
310
|
+
*/
|
|
311
|
+
getCharSetInfo() {
|
|
312
|
+
return {
|
|
313
|
+
charSet: [...this.dduChar],
|
|
314
|
+
paddingChar: this.paddingChar,
|
|
315
|
+
charLength: this.charLength,
|
|
316
|
+
bitLength: this.bitLength,
|
|
317
|
+
usePowerOfTwo: this.usePowerOfTwo,
|
|
318
|
+
encoding: this.encoding,
|
|
319
|
+
};
|
|
270
320
|
}
|
|
271
321
|
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ddunigma/node",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"main": "dist/cjs/index.js",
|
|
5
5
|
"module": "dist/mjs/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "rm -rf dist/* && tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && sh postProcess.sh",
|
|
10
|
-
"test": "
|
|
11
|
-
"test:
|
|
10
|
+
"test": "tsx ./src/test/test-quick.ts",
|
|
11
|
+
"test:comprehensive": "tsx ./src/test/test-comprehensive.ts",
|
|
12
|
+
"test:all": "tsx ./src/test/test-all-integrated.ts",
|
|
12
13
|
"dev": "tsx ./src/test/test.ts"
|
|
13
14
|
},
|
|
14
15
|
"exports": {
|