twonly-app-dependencies/hashlib_codecs/test/core/alphabet_test.dart
2025-12-07 16:10:41 +01:00

172 lines
6.4 KiB
Dart

import 'package:test/test.dart';
import 'package:hashlib_codecs/src/core/alphabet.dart';
final b64codes =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
.codeUnits;
void main() {
group('AlphabetEncoder / AlphabetDecoder', () {
test('8-bit identity alphabet without padding', () {
// bits = 8 so encoder should behave like identity before alphabet map
final encodeAlphabet = List<int>.generate(256, (i) => i); // identity
final enc = AlphabetEncoder(bits: 8, alphabet: encodeAlphabet);
final data = List<int>.generate(256, (i) => i); // 0..255
final out = enc.convert(data);
expect(out, data);
// Decode back
final decodeAlphabet =
List<int>.generate(256, (i) => i); // inverse (identity)
final dec = AlphabetDecoder(bits: 8, alphabet: decodeAlphabet);
final back = dec.convert(out);
expect(back, data);
});
test('5-bit encoding with padding to full byte boundary', () {
final bits = 5;
final encodeAlphabet = List<int>.generate(32, (i) => i); // identity
const pad = 255;
final enc =
AlphabetEncoder(bits: bits, alphabet: encodeAlphabet, padding: pad);
final input = [0xAB]; // single byte
final encoded = enc.convert(input);
// Expect length padded so that length * bits is multiple of 8.
expect((encoded.length * bits) % 8, 0);
// Without padding we would have ceil(8/5)=2 symbols; padding extends to 8 symbols (like base32 style)
expect(encoded.length, 8);
// First two symbols are from data, remaining are padding
for (int i = 2; i < encoded.length; i++) {
expect(encoded[i], pad);
}
// Build decoder inverse alphabet (identity) with padding
final decodeAlphabet = List<int>.generate(256, (i) => i < 32 ? i : -1);
final dec =
AlphabetDecoder(bits: bits, alphabet: decodeAlphabet, padding: pad);
final decoded = dec.convert(encoded);
expect(decoded, input);
});
test('Decoder stops at padding', () {
final bits = 5;
const pad = 200;
final encodeAlphabet = List<int>.generate(32, (i) => i);
final enc =
AlphabetEncoder(bits: bits, alphabet: encodeAlphabet, padding: pad);
final input = [1, 2, 3];
final encoded = enc.convert(input);
// Manually append extra data after padding that should be ignored
final withJunk = [
...encoded,
pad, // an explicit early padding (should stop here)
10, 11, 12
];
final decodeAlphabet = List<int>.generate(256, (i) => i < 32 ? i : -1);
final dec =
AlphabetDecoder(bits: bits, alphabet: decodeAlphabet, padding: pad);
final decoded = dec.convert(withJunk);
expect(decoded, input);
});
test('Decoder throws on invalid character (out of range)', () {
final dec = AlphabetDecoder(
bits: 5,
alphabet: List<int>.generate(32, (i) => i),
);
expect(() => dec.convert([40]), throwsFormatException);
});
test('Decoder throws on invalid character (negative mapping)', () {
// alphabet[y] < 0 triggers FormatException
final badAlphabet = List<int>.generate(32, (i) => i == 10 ? -1 : i);
final dec = AlphabetDecoder(bits: 5, alphabet: badAlphabet);
expect(() => dec.convert([10]), throwsFormatException);
});
test('Base64 encode/decode (no padding)', () {
final enc = AlphabetEncoder(
bits: 6,
alphabet: b64codes,
padding: '='.codeUnitAt(0),
);
final input = 'Man'.codeUnits; // 3 bytes -> 4 chars, no padding
final encoded = enc.convert(input);
expect(String.fromCharCodes(encoded), 'TWFu');
final decodeAlphabet = List<int>.filled(256, -1);
for (var i = 0; i < b64codes.length; i++) {
decodeAlphabet[b64codes[i]] = i;
}
final dec = AlphabetDecoder(
bits: 6,
alphabet: decodeAlphabet,
padding: '='.codeUnitAt(0),
);
final decoded = dec.convert(encoded);
expect(decoded, input);
});
test('Base64 encode/decode (single padding)', () {
final pad = '='.codeUnitAt(0);
final enc = AlphabetEncoder(bits: 6, alphabet: b64codes, padding: pad);
final input = 'Ma'.codeUnits; // 2 bytes -> 3 data chars + 1 pad
final encoded = enc.convert(input);
expect(String.fromCharCodes(encoded), 'TWE=');
final decodeAlphabet = List<int>.filled(256, -1);
for (var i = 0; i < b64codes.length; i++) {
decodeAlphabet[b64codes[i]] = i;
}
final dec =
AlphabetDecoder(bits: 6, alphabet: decodeAlphabet, padding: pad);
final decoded = dec.convert(encoded);
expect(decoded, input);
});
test('Base64 encode/decode (double padding)', () {
final pad = '='.codeUnitAt(0);
final enc = AlphabetEncoder(bits: 6, alphabet: b64codes, padding: pad);
final input = 'M'.codeUnits; // 1 byte -> 2 data chars + 2 pads
final encoded = enc.convert(input);
expect(String.fromCharCodes(encoded), 'TQ==');
final decodeAlphabet = List<int>.filled(256, -1);
for (var i = 0; i < b64codes.length; i++) {
decodeAlphabet[b64codes[i]] = i;
}
final dec =
AlphabetDecoder(bits: 6, alphabet: decodeAlphabet, padding: pad);
final decoded = dec.convert(encoded);
expect(decoded, input);
});
test('Base64 decoder ignores data after padding', () {
final pad = '='.codeUnitAt(0);
final enc = AlphabetEncoder(bits: 6, alphabet: b64codes, padding: pad);
final input = 'Ma'.codeUnits;
final encoded = enc.convert(input); // TWE=
final withJunk = [...encoded, 'A'.codeUnitAt(0), 'B'.codeUnitAt(0)];
final decodeAlphabet = List<int>.filled(256, -1);
for (var i = 0; i < b64codes.length; i++) {
decodeAlphabet[b64codes[i]] = i;
}
final dec =
AlphabetDecoder(bits: 6, alphabet: decodeAlphabet, padding: pad);
final decoded = dec.convert(withJunk);
expect(decoded, input);
});
test('Base64 decoder rejects invalid character', () {
final decodeAlphabet = List<int>.filled(256, -1);
for (var i = 0; i < b64codes.length; i++) {
decodeAlphabet[b64codes[i]] = i;
}
final dec = AlphabetDecoder(
bits: 6,
alphabet: decodeAlphabet,
padding: '='.codeUnitAt(0),
);
expect(() => dec.convert('?'.codeUnits), throwsFormatException);
});
});
}