@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
@@ -0,0 +1,654 @@
|
|
1
|
+
/**
|
2
|
+
* Encoder Unit - Conscious Encoding/Decoding Operations
|
3
|
+
*
|
4
|
+
* SYNET Unit Architecture v1.0.6 Implementation
|
5
|
+
*
|
6
|
+
* Philosophy: One unit, one goal - reliable data transformation
|
7
|
+
*
|
8
|
+
* Native Capabilities:
|
9
|
+
* - encode() - Transform data to specified format
|
10
|
+
* - decode() - Reverse transformation with validation
|
11
|
+
* - detect() - Auto-detect encoding format
|
12
|
+
* - validate() - Verify encoded data integrity
|
13
|
+
* - chain() - Sequential encoding operations
|
14
|
+
*
|
15
|
+
* Supported Formats: Base64, Base64URL, Hex, URI, ASCII
|
16
|
+
*
|
17
|
+
* @author SYNET ALPHA
|
18
|
+
* @version 1.0.0
|
19
|
+
* @follows Unit Architecture Doctrine v1.0.6
|
20
|
+
*/
|
21
|
+
|
22
|
+
import { Unit, createUnitSchema, type UnitProps, type TeachingContract } from '@synet/unit';
|
23
|
+
import { Result } from './result.js';
|
24
|
+
|
25
|
+
// Doctrine #13: TYPE HIERARCHY CONSISTENCY (Config → Props → State → Output)
|
26
|
+
|
27
|
+
/**
|
28
|
+
* External input configuration for static create()
|
29
|
+
*/
|
30
|
+
export interface EncoderConfig {
|
31
|
+
defaultFormat?: EncodingFormat;
|
32
|
+
strictMode?: boolean;
|
33
|
+
autoDetect?: boolean;
|
34
|
+
maxInputSize?: number;
|
35
|
+
validateOutput?: boolean;
|
36
|
+
}
|
37
|
+
|
38
|
+
/**
|
39
|
+
* Internal state after validation (implements UnitProps)
|
40
|
+
*/
|
41
|
+
export interface EncoderProps extends UnitProps {
|
42
|
+
defaultFormat: EncodingFormat;
|
43
|
+
strictMode: boolean;
|
44
|
+
autoDetect: boolean;
|
45
|
+
maxInputSize: number;
|
46
|
+
validateOutput: boolean;
|
47
|
+
readonly created: Date;
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* Encoding format types
|
52
|
+
*/
|
53
|
+
export type EncodingFormat = 'base64' | 'base64url' | 'hex' | 'uri' | 'ascii';
|
54
|
+
|
55
|
+
/**
|
56
|
+
* Encoding operation result
|
57
|
+
*/
|
58
|
+
export interface EncodingResult {
|
59
|
+
readonly encoded: string;
|
60
|
+
readonly originalSize: number;
|
61
|
+
readonly encodedSize: number;
|
62
|
+
readonly format: EncodingFormat;
|
63
|
+
readonly compressionRatio: number;
|
64
|
+
readonly timestamp: Date;
|
65
|
+
}
|
66
|
+
|
67
|
+
/**
|
68
|
+
* Decoding operation result
|
69
|
+
*/
|
70
|
+
export interface DecodingResult {
|
71
|
+
readonly decoded: string;
|
72
|
+
readonly detectedFormat: EncodingFormat;
|
73
|
+
readonly isValid: boolean;
|
74
|
+
readonly originalSize: number;
|
75
|
+
readonly timestamp: Date;
|
76
|
+
}
|
77
|
+
|
78
|
+
/**
|
79
|
+
* Format detection result
|
80
|
+
*/
|
81
|
+
export interface DetectionResult {
|
82
|
+
readonly format: EncodingFormat;
|
83
|
+
readonly confidence: number;
|
84
|
+
readonly reasons: string[];
|
85
|
+
readonly timestamp: Date;
|
86
|
+
}
|
87
|
+
|
88
|
+
/**
|
89
|
+
* Validation result
|
90
|
+
*/
|
91
|
+
export interface ValidationResult {
|
92
|
+
readonly isValid: boolean;
|
93
|
+
readonly format: EncodingFormat;
|
94
|
+
readonly errors: string[];
|
95
|
+
readonly suggestions: string[];
|
96
|
+
}
|
97
|
+
|
98
|
+
/**
|
99
|
+
* Encoder Implementation
|
100
|
+
*
|
101
|
+
* Doctrine #1: ZERO DEPENDENCY (only Node.js/browser native APIs)
|
102
|
+
* Doctrine #17: VALUE OBJECT FOUNDATION (immutable with identity and capabilities)
|
103
|
+
*/
|
104
|
+
export class Encoder extends Unit<EncoderProps> {
|
105
|
+
|
106
|
+
// Doctrine #4: CREATE NOT CONSTRUCT (protected constructor)
|
107
|
+
protected constructor(props: EncoderProps) {
|
108
|
+
super(props);
|
109
|
+
}
|
110
|
+
|
111
|
+
// Doctrine #4: CREATE NOT CONSTRUCT (static create with validation)
|
112
|
+
static create(config: EncoderConfig = {}): Encoder {
|
113
|
+
// Doctrine #3: PROPS CONTAIN EVERYTHING (validate and transform config to props)
|
114
|
+
const props: EncoderProps = {
|
115
|
+
// Doctrine #7: EVERY UNIT MUST HAVE DNA
|
116
|
+
dna: createUnitSchema({
|
117
|
+
id: 'encoder',
|
118
|
+
version: '1.0.0'
|
119
|
+
}),
|
120
|
+
defaultFormat: config.defaultFormat || 'base64',
|
121
|
+
strictMode: config.strictMode ?? false,
|
122
|
+
autoDetect: config.autoDetect ?? true,
|
123
|
+
maxInputSize: config.maxInputSize || 10 * 1024 * 1024, // 10MB default
|
124
|
+
validateOutput: config.validateOutput ?? true,
|
125
|
+
created: new Date()
|
126
|
+
};
|
127
|
+
|
128
|
+
return new Encoder(props);
|
129
|
+
}
|
130
|
+
|
131
|
+
// Doctrine #11: ALWAYS HELP (living documentation)
|
132
|
+
help(): void {
|
133
|
+
console.log(`
|
134
|
+
|
135
|
+
|
136
|
+
Hi, I am Encoder Unit [${this.dna.id}] v${this.dna.version} - Data Transformation Service
|
137
|
+
|
138
|
+
IDENTITY: ${this.whoami()}
|
139
|
+
DEFAULT FORMAT: ${this.props.defaultFormat}
|
140
|
+
STRICT MODE: ${this.props.strictMode}
|
141
|
+
AUTO DETECT: ${this.props.autoDetect}
|
142
|
+
MAX INPUT: ${(this.props.maxInputSize / 1024 / 1024).toFixed(1)}MB
|
143
|
+
STATUS: IMMUTABLE (stateless operations)
|
144
|
+
|
145
|
+
NATIVE CAPABILITIES:
|
146
|
+
• encode(data, format?) - Transform data to specified format (Result for validation)
|
147
|
+
• decode(data, format?) - Reverse transformation with auto-detection (Result)
|
148
|
+
• detect(data) - Auto-detect encoding format with confidence (throws on error)
|
149
|
+
• validate(data, format) - Verify encoded data integrity (throws on error)
|
150
|
+
• chain(data, formats) - Sequential encoding operations (Result)
|
151
|
+
• reverse(data, formats) - Reverse sequential decoding (Result)
|
152
|
+
|
153
|
+
SUPPORTED FORMATS:
|
154
|
+
• base64: Standard Base64 encoding (RFC 4648)
|
155
|
+
• base64url: URL-safe Base64 encoding
|
156
|
+
• hex: Hexadecimal encoding (lowercase)
|
157
|
+
• uri: URI component encoding
|
158
|
+
• ascii: ASCII text encoding
|
159
|
+
|
160
|
+
USAGE EXAMPLES:
|
161
|
+
const encoder = Encoder.create();
|
162
|
+
|
163
|
+
// Simple operations (Result pattern for validation)
|
164
|
+
const encoded = encoder.encode('Hello World', 'base64');
|
165
|
+
if (encoded.isSuccess) {
|
166
|
+
console.log(encoded.value.encoded); // SGVsbG8gV29ybGQ=
|
167
|
+
}
|
168
|
+
|
169
|
+
// Auto-detection decoding
|
170
|
+
const decoded = encoder.decode('SGVsbG8gV29ybGQ=');
|
171
|
+
if (decoded.isSuccess) {
|
172
|
+
console.log(decoded.value.decoded); // Hello World
|
173
|
+
}
|
174
|
+
|
175
|
+
// Format detection
|
176
|
+
const format = encoder.detect('48656c6c6f'); // hex
|
177
|
+
console.log(format.format); // 'hex'
|
178
|
+
|
179
|
+
LEARNING CAPABILITIES:
|
180
|
+
Other units can learn from me:
|
181
|
+
unit.learn([encoder.teach()]);
|
182
|
+
unit.execute('encoder.encode', data, format);
|
183
|
+
|
184
|
+
I TEACH:
|
185
|
+
• encode(data, format) - Encoding capability
|
186
|
+
• decode(data, format) - Decoding capability
|
187
|
+
• detect(data) - Format detection capability
|
188
|
+
• validate(data, format) - Validation capability
|
189
|
+
• chain(data, formats) - Sequential encoding capability
|
190
|
+
|
191
|
+
`);
|
192
|
+
}
|
193
|
+
|
194
|
+
// Doctrine #2: TEACH/LEARN PARADIGM (every unit must teach)
|
195
|
+
// Doctrine #9: ALWAYS TEACH (explicit capability binding)
|
196
|
+
// Doctrine #19: CAPABILITY LEAKAGE PREVENTION (teach only native capabilities)
|
197
|
+
teach(): TeachingContract {
|
198
|
+
return {
|
199
|
+
// Doctrine #12: NAMESPACE EVERYTHING (unitId for namespacing)
|
200
|
+
unitId: this.dna.id,
|
201
|
+
capabilities: {
|
202
|
+
// Native encoding capabilities only - wrapped for unknown[] compatibility
|
203
|
+
encode: ((...args: unknown[]) => this.encode(args[0] as string, args[1] as EncodingFormat)) as (...args: unknown[]) => unknown,
|
204
|
+
decode: ((...args: unknown[]) => this.decode(args[0] as string, args[1] as EncodingFormat)) as (...args: unknown[]) => unknown,
|
205
|
+
detect: ((...args: unknown[]) => this.detect(args[0] as string)) as (...args: unknown[]) => unknown,
|
206
|
+
validate: ((...args: unknown[]) => this.validate(args[0] as string, args[1] as EncodingFormat)) as (...args: unknown[]) => unknown,
|
207
|
+
chain: ((...args: unknown[]) => this.chain(args[0] as string, args[1] as EncodingFormat[])) as (...args: unknown[]) => unknown,
|
208
|
+
|
209
|
+
// Metadata access
|
210
|
+
getDefaultFormat: (() => this.props.defaultFormat) as (...args: unknown[]) => unknown,
|
211
|
+
isStrictMode: (() => this.props.strictMode) as (...args: unknown[]) => unknown,
|
212
|
+
getMaxInputSize: (() => this.props.maxInputSize) as (...args: unknown[]) => unknown
|
213
|
+
}
|
214
|
+
};
|
215
|
+
}
|
216
|
+
|
217
|
+
// Doctrine #14: ERROR BOUNDARY CLARITY (Result for complex operations)
|
218
|
+
|
219
|
+
/**
|
220
|
+
* Encode data to specified format (Result - complex validation operation)
|
221
|
+
*/
|
222
|
+
encode(data: string, format?: EncodingFormat): Result<EncodingResult> {
|
223
|
+
try {
|
224
|
+
const encodingFormat = format || this.props.defaultFormat;
|
225
|
+
|
226
|
+
// Input validation
|
227
|
+
if (data.length > this.props.maxInputSize) {
|
228
|
+
return Result.fail(`Input too large: ${data.length} bytes (max: ${this.props.maxInputSize})`);
|
229
|
+
}
|
230
|
+
|
231
|
+
if (this.props.strictMode && !this.isValidInput(data)) {
|
232
|
+
return Result.fail('Invalid input for strict mode');
|
233
|
+
}
|
234
|
+
|
235
|
+
const encoded = this.performEncode(data, encodingFormat);
|
236
|
+
|
237
|
+
// Output validation if enabled
|
238
|
+
if (this.props.validateOutput) {
|
239
|
+
const validation = this.validate(encoded, encodingFormat);
|
240
|
+
if (!validation.isValid) {
|
241
|
+
return Result.fail(`Output validation failed: ${validation.errors.join(', ')}`);
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
245
|
+
const result: EncodingResult = {
|
246
|
+
encoded,
|
247
|
+
originalSize: data.length,
|
248
|
+
encodedSize: encoded.length,
|
249
|
+
format: encodingFormat,
|
250
|
+
compressionRatio: encoded.length / data.length,
|
251
|
+
timestamp: new Date()
|
252
|
+
};
|
253
|
+
|
254
|
+
return Result.success(result);
|
255
|
+
} catch (error) {
|
256
|
+
return Result.fail(
|
257
|
+
`Encoding failed: ${error instanceof Error ? error.message : String(error)}`,
|
258
|
+
error
|
259
|
+
);
|
260
|
+
}
|
261
|
+
}
|
262
|
+
|
263
|
+
/**
|
264
|
+
* Decode data with optional format hint (Result - complex auto-detection operation)
|
265
|
+
*/
|
266
|
+
decode(data: string, format?: EncodingFormat): Result<DecodingResult> {
|
267
|
+
try {
|
268
|
+
// Auto-detect format if not provided and auto-detection is enabled
|
269
|
+
let detectedFormat: EncodingFormat;
|
270
|
+
if (format) {
|
271
|
+
detectedFormat = format;
|
272
|
+
} else if (this.props.autoDetect) {
|
273
|
+
detectedFormat = this.detect(data).format;
|
274
|
+
} else {
|
275
|
+
detectedFormat = this.props.defaultFormat;
|
276
|
+
}
|
277
|
+
|
278
|
+
// Input validation
|
279
|
+
const validation = this.validate(data, detectedFormat);
|
280
|
+
if (!validation.isValid) {
|
281
|
+
if (this.props.strictMode) {
|
282
|
+
return Result.fail(`Invalid ${detectedFormat} format: ${validation.errors.join(', ')}`);
|
283
|
+
}
|
284
|
+
// In non-strict mode, still fail if the format is completely wrong
|
285
|
+
if (validation.errors.some(err => err.includes('invalid') || err.includes('Invalid'))) {
|
286
|
+
return Result.fail(
|
287
|
+
`Invalid ${detectedFormat} format: ${validation.errors.join(', ')}`,
|
288
|
+
new Error(`Validation failed for ${detectedFormat}`)
|
289
|
+
);
|
290
|
+
}
|
291
|
+
}
|
292
|
+
|
293
|
+
const decoded = this.performDecode(data, detectedFormat);
|
294
|
+
|
295
|
+
const result: DecodingResult = {
|
296
|
+
decoded,
|
297
|
+
detectedFormat,
|
298
|
+
isValid: validation.isValid,
|
299
|
+
originalSize: data.length,
|
300
|
+
timestamp: new Date()
|
301
|
+
};
|
302
|
+
|
303
|
+
return Result.success(result);
|
304
|
+
} catch (error) {
|
305
|
+
return Result.fail(
|
306
|
+
`Decoding failed: ${error instanceof Error ? error.message : String(error)}`,
|
307
|
+
error
|
308
|
+
);
|
309
|
+
}
|
310
|
+
}
|
311
|
+
|
312
|
+
/**
|
313
|
+
* Chain multiple encoding operations (Result - complex multi-step operation)
|
314
|
+
*/
|
315
|
+
chain(data: string, formats: EncodingFormat[]): Result<EncodingResult> {
|
316
|
+
try {
|
317
|
+
let result = data;
|
318
|
+
let totalRatio = 1;
|
319
|
+
|
320
|
+
for (const format of formats) {
|
321
|
+
const encoded = this.encode(result, format);
|
322
|
+
if (encoded.isFailure) {
|
323
|
+
return Result.fail(`Chain failed at ${format}: ${encoded.error}`);
|
324
|
+
}
|
325
|
+
result = encoded.value.encoded;
|
326
|
+
totalRatio *= encoded.value.compressionRatio;
|
327
|
+
}
|
328
|
+
|
329
|
+
const chainResult: EncodingResult = {
|
330
|
+
encoded: result,
|
331
|
+
originalSize: data.length,
|
332
|
+
encodedSize: result.length,
|
333
|
+
format: formats[formats.length - 1], // Final format
|
334
|
+
compressionRatio: totalRatio,
|
335
|
+
timestamp: new Date()
|
336
|
+
};
|
337
|
+
|
338
|
+
return Result.success(chainResult);
|
339
|
+
} catch (error) {
|
340
|
+
return Result.fail(
|
341
|
+
`Chain encoding failed: ${error instanceof Error ? error.message : String(error)}`,
|
342
|
+
error
|
343
|
+
);
|
344
|
+
}
|
345
|
+
}
|
346
|
+
|
347
|
+
/**
|
348
|
+
* Reverse chain decoding (Result - complex multi-step operation)
|
349
|
+
*/
|
350
|
+
reverse(data: string, formats: EncodingFormat[]): Result<DecodingResult> {
|
351
|
+
try {
|
352
|
+
let result = data;
|
353
|
+
const reversedFormats = [...formats].reverse();
|
354
|
+
|
355
|
+
for (const format of reversedFormats) {
|
356
|
+
const decoded = this.decode(result, format);
|
357
|
+
if (decoded.isFailure) {
|
358
|
+
return Result.fail(`Reverse chain failed at ${format}: ${decoded.error}`);
|
359
|
+
}
|
360
|
+
result = decoded.value.decoded;
|
361
|
+
}
|
362
|
+
|
363
|
+
const reverseResult: DecodingResult = {
|
364
|
+
decoded: result,
|
365
|
+
detectedFormat: formats[0], // Original format
|
366
|
+
isValid: true,
|
367
|
+
originalSize: data.length,
|
368
|
+
timestamp: new Date()
|
369
|
+
};
|
370
|
+
|
371
|
+
return Result.success(reverseResult);
|
372
|
+
} catch (error) {
|
373
|
+
return Result.fail(
|
374
|
+
`Reverse chain failed: ${error instanceof Error ? error.message : String(error)}`,
|
375
|
+
error
|
376
|
+
);
|
377
|
+
}
|
378
|
+
}
|
379
|
+
|
380
|
+
// Doctrine #14: ERROR BOUNDARY CLARITY (throws for simple operations)
|
381
|
+
|
382
|
+
/**
|
383
|
+
* Auto-detect encoding format (throw on error - simple classification operation)
|
384
|
+
*/
|
385
|
+
detect(data: string): DetectionResult {
|
386
|
+
const patterns: Array<{ format: EncodingFormat; test: (s: string) => boolean; confidence: number; reason: string }> = [
|
387
|
+
{
|
388
|
+
format: 'hex',
|
389
|
+
test: (s) => /^[0-9a-fA-F]+$/.test(s) && s.length % 2 === 0,
|
390
|
+
confidence: 0.95,
|
391
|
+
reason: 'Matches hexadecimal pattern with even length'
|
392
|
+
},
|
393
|
+
{
|
394
|
+
format: 'base64url',
|
395
|
+
test: (s) => /^[A-Za-z0-9\-_]*$/.test(s) && !s.includes('+') && !s.includes('/') && !s.includes('=') && s.length > 0,
|
396
|
+
confidence: 0.9,
|
397
|
+
reason: 'Matches base64url character set (URL-safe, no padding)'
|
398
|
+
},
|
399
|
+
{
|
400
|
+
format: 'base64',
|
401
|
+
test: (s) => /^[A-Za-z0-9+/]*={0,2}$/.test(s) && s.length % 4 === 0 && (s.includes('+') || s.includes('/') || s.includes('=')),
|
402
|
+
confidence: 0.85,
|
403
|
+
reason: 'Matches base64 character set with standard chars or padding'
|
404
|
+
},
|
405
|
+
{
|
406
|
+
format: 'uri',
|
407
|
+
test: (s) => s.includes('%') && /^[A-Za-z0-9\-_.~%!*'()]+$/.test(s),
|
408
|
+
confidence: 0.8,
|
409
|
+
reason: 'Contains URI percent-encoding characters'
|
410
|
+
},
|
411
|
+
{
|
412
|
+
format: 'ascii',
|
413
|
+
test: (s) => /^[\x20-\x7E]*$/.test(s),
|
414
|
+
confidence: 0.7,
|
415
|
+
reason: 'Contains only printable ASCII characters'
|
416
|
+
}
|
417
|
+
];
|
418
|
+
|
419
|
+
const matches = patterns.filter(p => p.test(data));
|
420
|
+
|
421
|
+
if (matches.length === 0) {
|
422
|
+
throw new Error(`Cannot detect encoding format for data: ${data.slice(0, 50)}...`);
|
423
|
+
}
|
424
|
+
|
425
|
+
// Return highest confidence match
|
426
|
+
const bestMatch = matches.reduce((best, current) =>
|
427
|
+
current.confidence > best.confidence ? current : best
|
428
|
+
);
|
429
|
+
|
430
|
+
return {
|
431
|
+
format: bestMatch.format,
|
432
|
+
confidence: bestMatch.confidence,
|
433
|
+
reasons: matches.map(m => m.reason),
|
434
|
+
timestamp: new Date()
|
435
|
+
};
|
436
|
+
}
|
437
|
+
|
438
|
+
/**
|
439
|
+
* Validate encoded data format (throw on error - simple validation operation)
|
440
|
+
*/
|
441
|
+
validate(data: string, format: EncodingFormat): ValidationResult {
|
442
|
+
const errors: string[] = [];
|
443
|
+
const suggestions: string[] = [];
|
444
|
+
|
445
|
+
try {
|
446
|
+
switch (format) {
|
447
|
+
case 'base64':
|
448
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
|
449
|
+
errors.push('Contains invalid base64 characters');
|
450
|
+
suggestions.push('Remove invalid characters or try base64url format');
|
451
|
+
}
|
452
|
+
if (data.length % 4 !== 0) {
|
453
|
+
errors.push('Invalid base64 length (must be multiple of 4)');
|
454
|
+
suggestions.push('Add padding with = characters');
|
455
|
+
}
|
456
|
+
break;
|
457
|
+
|
458
|
+
case 'base64url':
|
459
|
+
if (!/^[A-Za-z0-9\-_]*$/.test(data)) {
|
460
|
+
errors.push('Contains invalid base64url characters');
|
461
|
+
suggestions.push('Use only A-Z, a-z, 0-9, -, _ characters');
|
462
|
+
}
|
463
|
+
break;
|
464
|
+
|
465
|
+
case 'hex':
|
466
|
+
if (!/^[0-9a-fA-F]*$/.test(data)) {
|
467
|
+
errors.push('Contains invalid hexadecimal characters');
|
468
|
+
suggestions.push('Use only 0-9, a-f, A-F characters');
|
469
|
+
}
|
470
|
+
if (data.length % 2 !== 0) {
|
471
|
+
errors.push('Invalid hex length (must be even)');
|
472
|
+
suggestions.push('Add leading zero or remove extra character');
|
473
|
+
}
|
474
|
+
break;
|
475
|
+
|
476
|
+
case 'uri':
|
477
|
+
try {
|
478
|
+
decodeURIComponent(data);
|
479
|
+
} catch {
|
480
|
+
errors.push('Invalid URI encoding');
|
481
|
+
suggestions.push('Check percent-encoding format');
|
482
|
+
}
|
483
|
+
break;
|
484
|
+
|
485
|
+
case 'ascii':
|
486
|
+
if (!/^[\x20-\x7E]*$/.test(data)) {
|
487
|
+
errors.push('Contains non-ASCII characters');
|
488
|
+
suggestions.push('Use only printable ASCII characters (32-126)');
|
489
|
+
}
|
490
|
+
break;
|
491
|
+
|
492
|
+
default:
|
493
|
+
throw new Error(`Unknown format: ${format}`);
|
494
|
+
}
|
495
|
+
|
496
|
+
return {
|
497
|
+
isValid: errors.length === 0,
|
498
|
+
format,
|
499
|
+
errors,
|
500
|
+
suggestions
|
501
|
+
};
|
502
|
+
} catch (error) {
|
503
|
+
throw new Error(`Validation failed: ${error instanceof Error ? error.message : String(error)}`);
|
504
|
+
}
|
505
|
+
}
|
506
|
+
|
507
|
+
// Doctrine #8: PURE FUNCTION HEARTS (core logic as pure functions)
|
508
|
+
|
509
|
+
private performEncode(data: string, format: EncodingFormat): string {
|
510
|
+
switch (format) {
|
511
|
+
case 'base64':
|
512
|
+
return this.encodeBase64(data);
|
513
|
+
case 'base64url':
|
514
|
+
return this.encodeBase64URL(data);
|
515
|
+
case 'hex':
|
516
|
+
return this.encodeHex(data);
|
517
|
+
case 'uri':
|
518
|
+
return this.encodeURI(data);
|
519
|
+
case 'ascii':
|
520
|
+
return this.encodeASCII(data);
|
521
|
+
default:
|
522
|
+
throw new Error(`Unsupported encoding format: ${format}`);
|
523
|
+
}
|
524
|
+
}
|
525
|
+
|
526
|
+
private performDecode(data: string, format: EncodingFormat): string {
|
527
|
+
switch (format) {
|
528
|
+
case 'base64':
|
529
|
+
return this.decodeBase64(data);
|
530
|
+
case 'base64url':
|
531
|
+
return this.decodeBase64URL(data);
|
532
|
+
case 'hex':
|
533
|
+
return this.decodeHex(data);
|
534
|
+
case 'uri':
|
535
|
+
return this.decodeURI(data);
|
536
|
+
case 'ascii':
|
537
|
+
return this.decodeASCII(data);
|
538
|
+
default:
|
539
|
+
throw new Error(`Unsupported decoding format: ${format}`);
|
540
|
+
}
|
541
|
+
}
|
542
|
+
|
543
|
+
// Base64 implementation (cross-platform Node.js/Browser)
|
544
|
+
private encodeBase64(data: string): string {
|
545
|
+
if (typeof Buffer !== 'undefined') {
|
546
|
+
return Buffer.from(data, 'utf8').toString('base64');
|
547
|
+
}
|
548
|
+
if (typeof btoa !== 'undefined') {
|
549
|
+
// Modern Unicode-safe base64 encoding without deprecated unescape()
|
550
|
+
const bytes = new TextEncoder().encode(data);
|
551
|
+
const binaryString = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
|
552
|
+
return btoa(binaryString);
|
553
|
+
}
|
554
|
+
throw new Error('No base64 encoding available');
|
555
|
+
}
|
556
|
+
|
557
|
+
private decodeBase64(data: string): string {
|
558
|
+
if (typeof Buffer !== 'undefined') {
|
559
|
+
return Buffer.from(data, 'base64').toString('utf8');
|
560
|
+
}
|
561
|
+
if (typeof atob !== 'undefined') {
|
562
|
+
// Modern Unicode-safe base64 decoding without deprecated escape()
|
563
|
+
const binaryString = atob(data);
|
564
|
+
const bytes = new Uint8Array(binaryString.length);
|
565
|
+
for (let i = 0; i < binaryString.length; i++) {
|
566
|
+
bytes[i] = binaryString.charCodeAt(i);
|
567
|
+
}
|
568
|
+
return new TextDecoder('utf-8').decode(bytes);
|
569
|
+
}
|
570
|
+
throw new Error('No base64 decoding available');
|
571
|
+
}
|
572
|
+
|
573
|
+
// Base64URL implementation
|
574
|
+
private encodeBase64URL(data: string): string {
|
575
|
+
return this.encodeBase64(data)
|
576
|
+
.replace(/\+/g, '-')
|
577
|
+
.replace(/\//g, '_')
|
578
|
+
.replace(/=/g, '');
|
579
|
+
}
|
580
|
+
|
581
|
+
private decodeBase64URL(data: string): string {
|
582
|
+
let base64 = data.replace(/-/g, '+').replace(/_/g, '/');
|
583
|
+
while (base64.length % 4) {
|
584
|
+
base64 += '=';
|
585
|
+
}
|
586
|
+
return this.decodeBase64(base64);
|
587
|
+
}
|
588
|
+
|
589
|
+
// Hex implementation
|
590
|
+
private encodeHex(data: string): string {
|
591
|
+
return Array.from(data)
|
592
|
+
.map(char => char.charCodeAt(0).toString(16).padStart(2, '0'))
|
593
|
+
.join('');
|
594
|
+
}
|
595
|
+
|
596
|
+
private decodeHex(data: string): string {
|
597
|
+
const hex = data.replace(/[^0-9a-fA-F]/g, '');
|
598
|
+
let result = '';
|
599
|
+
for (let i = 0; i < hex.length; i += 2) {
|
600
|
+
result += String.fromCharCode(Number.parseInt(hex.slice(i, i + 2), 16));
|
601
|
+
}
|
602
|
+
return result;
|
603
|
+
}
|
604
|
+
|
605
|
+
// URI implementation
|
606
|
+
private encodeURI(data: string): string {
|
607
|
+
return encodeURIComponent(data);
|
608
|
+
}
|
609
|
+
|
610
|
+
private decodeURI(data: string): string {
|
611
|
+
return decodeURIComponent(data);
|
612
|
+
}
|
613
|
+
|
614
|
+
// ASCII implementation
|
615
|
+
private encodeASCII(data: string): string {
|
616
|
+
return Array.from(data)
|
617
|
+
.map(char => {
|
618
|
+
const code = char.charCodeAt(0);
|
619
|
+
if (code > 127) {
|
620
|
+
throw new Error(`Non-ASCII character found: ${char} (code: ${code})`);
|
621
|
+
}
|
622
|
+
return char;
|
623
|
+
})
|
624
|
+
.join('');
|
625
|
+
}
|
626
|
+
|
627
|
+
private decodeASCII(data: string): string {
|
628
|
+
return data; // ASCII is already decoded
|
629
|
+
}
|
630
|
+
|
631
|
+
// Utility methods
|
632
|
+
private isValidInput(data: string): boolean {
|
633
|
+
// Basic input validation for strict mode
|
634
|
+
return typeof data === 'string' && data.length > 0;
|
635
|
+
}
|
636
|
+
|
637
|
+
// Standard unit identification
|
638
|
+
whoami(): string {
|
639
|
+
return `EncoderUnit[${this.dna.id}@${this.dna.version}]`;
|
640
|
+
}
|
641
|
+
|
642
|
+
// JSON serialization (no sensitive data exposed)
|
643
|
+
toJSON(): Record<string, unknown> {
|
644
|
+
return {
|
645
|
+
type: 'EncoderUnit',
|
646
|
+
dna: this.dna,
|
647
|
+
defaultFormat: this.props.defaultFormat,
|
648
|
+
strictMode: this.props.strictMode,
|
649
|
+
autoDetect: this.props.autoDetect,
|
650
|
+
learnedCapabilities: this.capabilities(), // This calls the base Unit class method
|
651
|
+
created: this.props.created
|
652
|
+
};
|
653
|
+
}
|
654
|
+
}
|