@synet/encoder 1.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/MANIFESTO.md +220 -0
- package/MANUAL.md +735 -0
- package/README.md +235 -0
- package/biome.json +37 -0
- package/dist/encoder.unit.d.ts +137 -0
- package/dist/encoder.unit.js +517 -0
- package/dist/functions.d.ts +74 -0
- package/dist/functions.js +243 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +38 -0
- package/dist/result.d.ts +23 -0
- package/dist/result.js +69 -0
- package/package.json +55 -0
- package/src/encoder.unit.ts +654 -0
- package/src/functions.ts +252 -0
- package/src/index.ts +49 -0
- package/src/result.ts +81 -0
- package/test/encoder-unit.test.ts +424 -0
- package/test/functions.test.ts +386 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +12 -0
package/src/functions.ts
ADDED
@@ -0,0 +1,252 @@
|
|
1
|
+
/**
|
2
|
+
* Pure Encoding Functions - Serverless Ready
|
3
|
+
*
|
4
|
+
* Simple, stateless functions for encoding/decoding operations.
|
5
|
+
* These throw on error (simple operations) following Doctrine #14.
|
6
|
+
*/
|
7
|
+
|
8
|
+
/**
|
9
|
+
* Encoding format types
|
10
|
+
*/
|
11
|
+
export type EncodingFormat = 'base64' | 'base64url' | 'hex' | 'uri' | 'ascii';
|
12
|
+
|
13
|
+
/**
|
14
|
+
* Encode string to Base64
|
15
|
+
*/
|
16
|
+
export function encodeBase64(data: string): string {
|
17
|
+
if (typeof Buffer !== 'undefined') {
|
18
|
+
return Buffer.from(data, 'utf8').toString('base64');
|
19
|
+
}
|
20
|
+
if (typeof btoa !== 'undefined') {
|
21
|
+
// Modern Unicode-safe base64 encoding without deprecated unescape()
|
22
|
+
const bytes = new TextEncoder().encode(data);
|
23
|
+
const binaryString = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
|
24
|
+
return btoa(binaryString);
|
25
|
+
}
|
26
|
+
throw new Error('No base64 encoding available');
|
27
|
+
}
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Decode Base64 string
|
31
|
+
*/
|
32
|
+
export function decodeBase64(data: string): string {
|
33
|
+
if (typeof Buffer !== 'undefined') {
|
34
|
+
return Buffer.from(data, 'base64').toString('utf8');
|
35
|
+
}
|
36
|
+
if (typeof atob !== 'undefined') {
|
37
|
+
// Modern Unicode-safe base64 decoding without deprecated escape()
|
38
|
+
const binaryString = atob(data);
|
39
|
+
const bytes = new Uint8Array(binaryString.length);
|
40
|
+
for (let i = 0; i < binaryString.length; i++) {
|
41
|
+
bytes[i] = binaryString.charCodeAt(i);
|
42
|
+
}
|
43
|
+
return new TextDecoder('utf-8').decode(bytes);
|
44
|
+
}
|
45
|
+
throw new Error('No base64 decoding available');
|
46
|
+
}
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Encode string to Base64URL (URL-safe)
|
50
|
+
*/
|
51
|
+
export function encodeBase64URL(data: string): string {
|
52
|
+
return encodeBase64(data)
|
53
|
+
.replace(/\+/g, '-')
|
54
|
+
.replace(/\//g, '_')
|
55
|
+
.replace(/=/g, '');
|
56
|
+
}
|
57
|
+
|
58
|
+
/**
|
59
|
+
* Decode Base64URL string
|
60
|
+
*/
|
61
|
+
export function decodeBase64URL(data: string): string {
|
62
|
+
let base64 = data.replace(/-/g, '+').replace(/_/g, '/');
|
63
|
+
while (base64.length % 4) {
|
64
|
+
base64 += '=';
|
65
|
+
}
|
66
|
+
return decodeBase64(base64);
|
67
|
+
}
|
68
|
+
|
69
|
+
/**
|
70
|
+
* Encode string to hexadecimal
|
71
|
+
*/
|
72
|
+
export function encodeHex(data: string): string {
|
73
|
+
return Array.from(data)
|
74
|
+
.map(char => char.charCodeAt(0).toString(16).padStart(2, '0'))
|
75
|
+
.join('');
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Decode hexadecimal string
|
80
|
+
*/
|
81
|
+
export function decodeHex(data: string): string {
|
82
|
+
if (data.length % 2 !== 0) {
|
83
|
+
throw new Error('Invalid hex string: length must be even');
|
84
|
+
}
|
85
|
+
|
86
|
+
if (!/^[0-9a-fA-F]*$/.test(data)) {
|
87
|
+
throw new Error('Invalid hex string: contains non-hex characters');
|
88
|
+
}
|
89
|
+
|
90
|
+
let result = '';
|
91
|
+
for (let i = 0; i < data.length; i += 2) {
|
92
|
+
result += String.fromCharCode(Number.parseInt(data.slice(i, i + 2), 16));
|
93
|
+
}
|
94
|
+
return result;
|
95
|
+
}
|
96
|
+
|
97
|
+
/**
|
98
|
+
* Encode string for URI
|
99
|
+
*/
|
100
|
+
export function encodeURIString(data: string): string {
|
101
|
+
return encodeURIComponent(data);
|
102
|
+
}
|
103
|
+
|
104
|
+
/**
|
105
|
+
* Decode URI-encoded string
|
106
|
+
*/
|
107
|
+
export function decodeURIString(data: string): string {
|
108
|
+
return decodeURIComponent(data);
|
109
|
+
}
|
110
|
+
|
111
|
+
/**
|
112
|
+
* Encode string as ASCII (validates ASCII-only)
|
113
|
+
*/
|
114
|
+
export function encodeASCII(data: string): string {
|
115
|
+
for (let i = 0; i < data.length; i++) {
|
116
|
+
const code = data.charCodeAt(i);
|
117
|
+
if (code > 127) {
|
118
|
+
throw new Error(`Non-ASCII character found at position ${i}: '${data[i]}' (code: ${code})`);
|
119
|
+
}
|
120
|
+
}
|
121
|
+
return data;
|
122
|
+
}
|
123
|
+
|
124
|
+
/**
|
125
|
+
* Decode ASCII string (no-op, validates printable ASCII)
|
126
|
+
*/
|
127
|
+
export function decodeASCII(data: string): string {
|
128
|
+
for (let i = 0; i < data.length; i++) {
|
129
|
+
const code = data.charCodeAt(i);
|
130
|
+
if (code < 32 || code > 126) {
|
131
|
+
throw new Error(`Non-printable ASCII character at position ${i}: code ${code}`);
|
132
|
+
}
|
133
|
+
}
|
134
|
+
return data;
|
135
|
+
}
|
136
|
+
|
137
|
+
/**
|
138
|
+
* Generic encode function
|
139
|
+
*/
|
140
|
+
export function encode(data: string, format: EncodingFormat): string {
|
141
|
+
switch (format) {
|
142
|
+
case 'base64':
|
143
|
+
return encodeBase64(data);
|
144
|
+
case 'base64url':
|
145
|
+
return encodeBase64URL(data);
|
146
|
+
case 'hex':
|
147
|
+
return encodeHex(data);
|
148
|
+
case 'uri':
|
149
|
+
return encodeURIString(data);
|
150
|
+
case 'ascii':
|
151
|
+
return encodeASCII(data);
|
152
|
+
default:
|
153
|
+
throw new Error(`Unsupported encoding format: ${format}`);
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
/**
|
158
|
+
* Generic decode function
|
159
|
+
*/
|
160
|
+
export function decode(data: string, format: EncodingFormat): string {
|
161
|
+
switch (format) {
|
162
|
+
case 'base64':
|
163
|
+
return decodeBase64(data);
|
164
|
+
case 'base64url':
|
165
|
+
return decodeBase64URL(data);
|
166
|
+
case 'hex':
|
167
|
+
return decodeHex(data);
|
168
|
+
case 'uri':
|
169
|
+
return decodeURIString(data);
|
170
|
+
case 'ascii':
|
171
|
+
return decodeASCII(data);
|
172
|
+
default:
|
173
|
+
throw new Error(`Unsupported decoding format: ${format}`);
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
/**
|
178
|
+
* Auto-detect encoding format
|
179
|
+
*/
|
180
|
+
export function detectFormat(data: string): EncodingFormat {
|
181
|
+
// Test patterns in order of specificity/confidence
|
182
|
+
if (/^[0-9a-fA-F]+$/.test(data) && data.length % 2 === 0) {
|
183
|
+
return 'hex';
|
184
|
+
}
|
185
|
+
|
186
|
+
// Base64url should be tested before base64 since it's more restrictive
|
187
|
+
if (/^[A-Za-z0-9\-_]*$/.test(data) && !data.includes('+') && !data.includes('/') && !data.includes('=') && data.length > 0) {
|
188
|
+
return 'base64url';
|
189
|
+
}
|
190
|
+
|
191
|
+
// Base64 with standard characters or padding
|
192
|
+
if (/^[A-Za-z0-9+/]*={0,2}$/.test(data) && data.length % 4 === 0 && (data.includes('+') || data.includes('/') || data.includes('='))) {
|
193
|
+
return 'base64';
|
194
|
+
}
|
195
|
+
|
196
|
+
if (data.includes('%') && /^[A-Za-z0-9\-_.~%!*'()]+$/.test(data)) {
|
197
|
+
return 'uri';
|
198
|
+
}
|
199
|
+
|
200
|
+
if (/^[\x20-\x7E]*$/.test(data)) {
|
201
|
+
return 'ascii';
|
202
|
+
}
|
203
|
+
|
204
|
+
throw new Error(`Cannot detect encoding format for: ${data.slice(0, 50)}...`);
|
205
|
+
}
|
206
|
+
|
207
|
+
/**
|
208
|
+
* Validate format of encoded data
|
209
|
+
*/
|
210
|
+
export function validateFormat(data: string, format: EncodingFormat): boolean {
|
211
|
+
try {
|
212
|
+
switch (format) {
|
213
|
+
case 'base64':
|
214
|
+
return /^[A-Za-z0-9+/]*={0,2}$/.test(data) && data.length % 4 === 0;
|
215
|
+
case 'base64url':
|
216
|
+
return /^[A-Za-z0-9\-_]*$/.test(data);
|
217
|
+
case 'hex':
|
218
|
+
return /^[0-9a-fA-F]*$/.test(data) && data.length % 2 === 0;
|
219
|
+
case 'uri':
|
220
|
+
decodeURIComponent(data);
|
221
|
+
return true;
|
222
|
+
case 'ascii':
|
223
|
+
return /^[\x20-\x7E]*$/.test(data);
|
224
|
+
default:
|
225
|
+
return false;
|
226
|
+
}
|
227
|
+
} catch {
|
228
|
+
return false;
|
229
|
+
}
|
230
|
+
}
|
231
|
+
|
232
|
+
/**
|
233
|
+
* Chain multiple encodings
|
234
|
+
*/
|
235
|
+
export function chain(data: string, formats: EncodingFormat[]): string {
|
236
|
+
let result = data;
|
237
|
+
for (const format of formats) {
|
238
|
+
result = encode(result, format);
|
239
|
+
}
|
240
|
+
return result;
|
241
|
+
}
|
242
|
+
|
243
|
+
/**
|
244
|
+
* Reverse chain decodings
|
245
|
+
*/
|
246
|
+
export function reverseChain(data: string, formats: EncodingFormat[]): string {
|
247
|
+
let result = data;
|
248
|
+
for (const format of [...formats].reverse()) {
|
249
|
+
result = decode(result, format);
|
250
|
+
}
|
251
|
+
return result;
|
252
|
+
}
|
package/src/index.ts
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
/**
|
2
|
+
* @synet/encoder - Conscious Encoding/Decoding Unit
|
3
|
+
*
|
4
|
+
* Zero-dependency encoding operations following Unit Architecture doctrine.
|
5
|
+
*
|
6
|
+
* EXPORTS:
|
7
|
+
* - Encoder: Complete conscious encoder unit with teach/learn capabilities
|
8
|
+
* - Pure functions: Simple functional encoding operations
|
9
|
+
* - Result: Foundational error handling pattern
|
10
|
+
* - Types: All encoding-related interfaces
|
11
|
+
*/
|
12
|
+
|
13
|
+
// Core Unit
|
14
|
+
export { Encoder } from './encoder.unit.js';
|
15
|
+
|
16
|
+
// Result pattern (foundational)
|
17
|
+
export { Result } from './result.js';
|
18
|
+
|
19
|
+
// Types
|
20
|
+
export type {
|
21
|
+
EncoderConfig,
|
22
|
+
EncoderProps,
|
23
|
+
EncodingFormat,
|
24
|
+
EncodingResult,
|
25
|
+
DecodingResult,
|
26
|
+
DetectionResult,
|
27
|
+
ValidationResult
|
28
|
+
} from './encoder.unit.js';
|
29
|
+
|
30
|
+
// Pure function exports for simple use cases
|
31
|
+
export {
|
32
|
+
encode,
|
33
|
+
decode,
|
34
|
+
encodeBase64,
|
35
|
+
decodeBase64,
|
36
|
+
encodeBase64URL,
|
37
|
+
decodeBase64URL,
|
38
|
+
encodeHex,
|
39
|
+
decodeHex,
|
40
|
+
encodeURIString,
|
41
|
+
decodeURIString,
|
42
|
+
encodeASCII,
|
43
|
+
decodeASCII,
|
44
|
+
detectFormat,
|
45
|
+
validateFormat,
|
46
|
+
chain,
|
47
|
+
reverseChain,
|
48
|
+
type EncodingFormat as FunctionEncodingFormat
|
49
|
+
} from './functions.js';
|
package/src/result.ts
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
/**
|
2
|
+
* Foundational Result Pattern for Encoder Operations
|
3
|
+
*
|
4
|
+
* Local copy following Unit Architecture Doctrine #14: ERROR BOUNDARY CLARITY
|
5
|
+
* Simple operations throw, complex operations use Result pattern
|
6
|
+
*/
|
7
|
+
|
8
|
+
export class Result<T> {
|
9
|
+
private constructor(
|
10
|
+
private readonly _value: T | null,
|
11
|
+
private readonly _error: string | null,
|
12
|
+
private readonly _errorCause?: unknown
|
13
|
+
) {}
|
14
|
+
|
15
|
+
static success<T>(value: T): Result<T> {
|
16
|
+
return new Result(value, null);
|
17
|
+
}
|
18
|
+
|
19
|
+
static fail<T>(message: string, cause?: unknown): Result<T> {
|
20
|
+
return new Result<T>(null, message, cause);
|
21
|
+
}
|
22
|
+
|
23
|
+
get isSuccess(): boolean {
|
24
|
+
return this._error === null;
|
25
|
+
}
|
26
|
+
|
27
|
+
get isFailure(): boolean {
|
28
|
+
return this._error !== null;
|
29
|
+
}
|
30
|
+
|
31
|
+
get value(): T {
|
32
|
+
if (this._error !== null) {
|
33
|
+
throw new Error(`Attempted to get value from failed Result: ${this._error}`);
|
34
|
+
}
|
35
|
+
return this._value as T;
|
36
|
+
}
|
37
|
+
|
38
|
+
get error(): string {
|
39
|
+
return this._error || '';
|
40
|
+
}
|
41
|
+
|
42
|
+
get errorCause(): unknown {
|
43
|
+
return this._errorCause;
|
44
|
+
}
|
45
|
+
|
46
|
+
map<U>(fn: (value: T) => U): Result<U> {
|
47
|
+
if (this.isFailure) {
|
48
|
+
return Result.fail<U>(this._error || 'Unknown error', this._errorCause);
|
49
|
+
}
|
50
|
+
try {
|
51
|
+
return Result.success(fn(this.value));
|
52
|
+
} catch (error) {
|
53
|
+
return Result.fail<U>(
|
54
|
+
`Map operation failed: ${error instanceof Error ? error.message : String(error)}`,
|
55
|
+
error
|
56
|
+
);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
|
60
|
+
flatMap<U>(fn: (value: T) => Result<U>): Result<U> {
|
61
|
+
if (this.isFailure) {
|
62
|
+
return Result.fail<U>(this._error || 'Unknown error', this._errorCause);
|
63
|
+
}
|
64
|
+
try {
|
65
|
+
return fn(this.value);
|
66
|
+
} catch (error) {
|
67
|
+
return Result.fail<U>(
|
68
|
+
`FlatMap operation failed: ${error instanceof Error ? error.message : String(error)}`,
|
69
|
+
error
|
70
|
+
);
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
getOrElse(defaultValue: T): T {
|
75
|
+
return this.isSuccess ? this.value : defaultValue;
|
76
|
+
}
|
77
|
+
|
78
|
+
match<U>(onSuccess: (value: T) => U, onFailure: (error: string) => U): U {
|
79
|
+
return this.isSuccess ? onSuccess(this.value) : onFailure(this.error);
|
80
|
+
}
|
81
|
+
}
|