1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.codec.binary;
19
20 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
21 import static org.junit.jupiter.api.Assertions.assertEquals;
22 import static org.junit.jupiter.api.Assertions.assertFalse;
23 import static org.junit.jupiter.api.Assertions.assertNotNull;
24 import static org.junit.jupiter.api.Assertions.assertThrows;
25 import static org.junit.jupiter.api.Assertions.assertTrue;
26
27 import java.io.ByteArrayInputStream;
28 import java.io.ByteArrayOutputStream;
29 import java.io.IOException;
30 import java.nio.charset.StandardCharsets;
31 import java.util.Arrays;
32 import java.util.Random;
33
34 import org.apache.commons.codec.CodecPolicy;
35 import org.apache.commons.codec.DecoderException;
36 import org.apache.commons.codec.EncoderException;
37 import org.apache.commons.io.IOUtils;
38 import org.junit.jupiter.api.Test;
39 import org.junit.jupiter.params.ParameterizedTest;
40 import org.junit.jupiter.params.provider.ValueSource;
41
42
43
44
45
46
47
48 class Base45Test {
49
50
51
52
53
54 private static final String[][] RFC9285_TEST_VECTORS = {
55
56 { "", "" },
57 { "ietf!", "QED8WEX0" },
58 { "base-45", "UJCLQE7W581" },
59
60 { "AB", "BB8" },
61 };
62
63
64 private void compare(final byte[] input, final ByteArrayOutputStream baos) throws IOException {
65 final byte[] encoded = baos.toByteArray();
66 final ByteArrayInputStream bais = new ByteArrayInputStream(encoded);
67 final ByteArrayOutputStream decodedBaos = new ByteArrayOutputStream();
68 try (Base45InputStream in = new Base45InputStream(bais)) {
69 IOUtils.copy(in, decodedBaos);
70 }
71 assertArrayEquals(input, decodedBaos.toByteArray());
72 }
73
74
75
76
77 @Test
78 void testBuilder() {
79 final Base45 codec = Base45.builder().get();
80 assertNotNull(codec);
81 assertEquals("QED8WEX0", codec.encodeToString("ietf!".getBytes(StandardCharsets.US_ASCII)));
82 }
83
84 @Test
85 void testBuilderLenientDecodingPolicyThrows() {
86 assertThrows(IllegalArgumentException.class, () -> Base45.builder().setDecodingPolicy(CodecPolicy.LENIENT));
87 assertEquals(CodecPolicy.STRICT, Base45.builder().setDecodingPolicy(null).get().getCodecPolicy());
88 }
89
90
91
92
93 @Test
94 void testBuilderSetEncodeTableDerivesDecodeTable() {
95 final byte[] custom = new byte[45];
96 int k = 0;
97 for (char c = 'a'; c <= 'z'; c++) {
98 custom[k++] = (byte) c;
99 }
100 for (char c = 'A'; c <= 'I'; c++) {
101 custom[k++] = (byte) c;
102 }
103 for (char c = '0'; c <= '9'; c++) {
104 custom[k++] = (byte) c;
105 }
106 final Base45 codec = Base45.builder().setEncodeTable(custom).get();
107 final byte[] input = "hello world".getBytes(StandardCharsets.US_ASCII);
108 final byte[] encoded = codec.encode(input);
109 for (final byte b : encoded) {
110 assertTrue(codec.isInAlphabet(b), "Encoded byte '" + (char) b + "' must be in the custom alphabet");
111 }
112 assertArrayEquals(input, codec.decode(encoded), "A custom-alphabet codec must be able to decode its own output");
113
114 assertArrayEquals("QED8WEX0".getBytes(StandardCharsets.US_ASCII),
115 Base45.builder().setEncodeTable((byte[]) null).get().encode("ietf!".getBytes(StandardCharsets.US_ASCII)));
116 }
117
118
119
120
121 @Test
122 void testBuilderSetEncodeTableRejectsInvalidTable() {
123 assertThrows(IllegalArgumentException.class, () -> Base45.builder().setEncodeTable(new byte[] { 'a', 'b', 'c' }), "wrong size");
124 assertThrows(IllegalArgumentException.class, () -> Base45.builder().setEncodeTable(), "zero entries");
125 final byte[] duplicates = new byte[45];
126 Arrays.fill(duplicates, (byte) 'a');
127 assertThrows(IllegalArgumentException.class, () -> Base45.builder().setEncodeTable(duplicates), "duplicates");
128 }
129
130 @Test
131 void testBuilderSetLineLength() {
132 assertThrows(UnsupportedOperationException.class, () -> Base45.builder().setLineLength(0));
133 }
134
135 @Test
136 void testBuilderSetLineSeparator() {
137 assertThrows(UnsupportedOperationException.class, () -> Base45.builder().setLineSeparator((byte) 0));
138 }
139
140 @Test
141 void testBuilderSetPadding() {
142 assertThrows(UnsupportedOperationException.class, () -> Base45.builder().setPadding((byte) 0));
143 }
144
145
146
147
148 @Test
149 void testBuilderStrictDecoding() {
150 final Base45 strict = Base45.builder().setDecodingPolicy(CodecPolicy.STRICT).get();
151 assertTrue(strict.isStrictDecoding());
152 assertTrue(new Base45().isStrictDecoding());
153
154 assertArrayEquals("ietf!".getBytes(StandardCharsets.US_ASCII), strict.decode("QED8WEX0"));
155 }
156
157
158
159
160 @Test
161 void testBuilderStrictDecodingRejectsHighPair() {
162 final Base45 strict = Base45.builder().setDecodingPolicy(CodecPolicy.STRICT).get();
163
164 assertThrows(IllegalArgumentException.class, () -> strict.decode("::"), "Strict mode: ':' ':' decodes to 2024 > 255, should be rejected");
165 }
166
167
168
169
170 @Test
171 void testCodecConstants() {
172 assertEquals(3, Base45.BYTES_PER_ENCODED_BLOCK, "BYTES_PER_ENCODED_BLOCK should be 3 (3 Base45 chars per 2 bytes)");
173 assertEquals(2, Base45.BYTES_PER_UNENCODED_BLOCK, "BYTES_PER_UNENCODED_BLOCK should be 2 (2 bytes per 3 Base45 chars)");
174 }
175
176
177
178
179 @Test
180 void testDecodeEmpty() {
181 final Base45 codec = new Base45();
182 assertArrayEquals(new byte[0], codec.decode(""));
183 assertArrayEquals(new byte[0], codec.decode(new byte[0]));
184 }
185
186
187
188
189 @ParameterizedTest
190 @ValueSource(chars = { '!', '"', '#', '&', '\'', '(', ')', ',', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', 'a', 'z' })
191 void testDecodeInvalidCharacters(final char c) {
192 final Base45 codec = new Base45();
193
194 final String input = "Q" + c + "D";
195 assertThrows(IllegalArgumentException.class, () -> codec.decode(input), () -> "Should reject character '" + c + "' (ASCII " + (int) c + ")");
196 }
197
198
199
200
201 @Test
202 void testDecodeInvalidLengthMod3Equals1() {
203 final Base45 codec = new Base45();
204 assertThrows(IllegalArgumentException.class, () -> codec.decode("Q"), "Single character input should be rejected");
205 assertThrows(IllegalArgumentException.class, () -> codec.decode("QEDB"), "4-character input (length % 3 == 1) should be rejected");
206 assertThrows(IllegalArgumentException.class, () -> codec.decode("QEDBWEC"), "7-character input (length % 3 == 1) should be rejected");
207 }
208
209
210
211
212 @Test
213 void testDecodeMaxMax() {
214 assertArrayEquals(new byte[] { (byte) 0xFF, (byte) 0xFF }, new Base45().decode("FGW"));
215 }
216
217
218
219
220 @Test
221 void testDecodeObject() throws DecoderException {
222 final Base45 codec = new Base45();
223 final byte[] encoded = "QED8WEX0".getBytes(StandardCharsets.US_ASCII);
224 Object result = codec.decode((Object) encoded);
225 assertArrayEquals("ietf!".getBytes(StandardCharsets.US_ASCII), (byte[]) result);
226
227 result = codec.decode((Object) "QED8WEX0");
228 assertArrayEquals("ietf!".getBytes(StandardCharsets.US_ASCII), (byte[]) result);
229 }
230
231
232
233
234 @Test
235 void testDecodeObjectThrowsForUnsupportedType() {
236 final Base45 codec = new Base45();
237 assertThrows(DecoderException.class, () -> codec.decode(Integer.valueOf(42)));
238 }
239
240
241
242
243
244 @Test
245 void testDecodePairExceedingMaxByte() {
246 final Base45 codec = new Base45();
247
248 assertThrows(IllegalArgumentException.class, () -> codec.decode("::"), "Pair ':' ':' decodes to 2024 which exceeds 255 and should be rejected");
249
250 assertThrows(IllegalArgumentException.class, () -> codec.decode(":6"), "Pair ':' '6' decodes to 314 which exceeds 255 and should be rejected");
251 }
252
253
254
255
256 @ParameterizedTest
257 @ValueSource(strings = { "\r", "\n", "\t", "\r\n", "\u000B", "\f", "\u001C", "\u001D", "\u001E", "\u001F" })
258 void testDecodeRejectsNonAlphabetWhitespace(final String whitespace) {
259 final Base45 codec = new Base45();
260 assertThrows(IllegalArgumentException.class, () -> codec.decode(whitespace));
261 final String encoded = "QED8WEX0";
262 for (int i = 0; i <= encoded.length(); i++) {
263 final String input = encoded.substring(0, i) + whitespace + encoded.substring(i);
264 assertThrows(IllegalArgumentException.class, () -> codec.decode(input));
265 assertThrows(IllegalArgumentException.class, () -> codec.decode(input.getBytes(StandardCharsets.US_ASCII)));
266 }
267 }
268
269
270
271
272 @Test
273 void testDecodeRfc9285TestVectors() {
274 final Base45 codec = new Base45();
275 for (final String[] testCase : RFC9285_TEST_VECTORS) {
276 final String plainText = testCase[0];
277 final String encoded = testCase[1];
278 final byte[] expected = plainText.getBytes(StandardCharsets.ISO_8859_1);
279 final byte[] actual = codec.decode(encoded.getBytes(StandardCharsets.US_ASCII));
280 assertArrayEquals(expected, actual, "RFC 9285 decode failed for: '" + encoded + "'");
281 }
282 }
283
284
285
286
287 @Test
288 void testDecodeSingleMax() {
289 assertArrayEquals(new byte[] { (byte) 0xFF }, new Base45().decode("U5"));
290 }
291
292
293
294
295 @Test
296 void testDecodeSingleZero() {
297 assertArrayEquals(new byte[] { 0 }, new Base45().decode("00"));
298 }
299
300
301
302
303 @Test
304 void testDecodeSpaceIsInAlphabet() {
305 final Base45 codec = new Base45();
306
307
308 assertTrue(codec.isInAlphabet((byte) ' '), "Space should be in Base45 alphabet");
309 assertArrayEquals("Hello!!".getBytes(StandardCharsets.US_ASCII), codec.decode("%69 VD92EX0"));
310
311 final byte[] input = { (byte) 0xF0, (byte) 0xF0 };
312 final byte[] encoded = codec.encode(input);
313 final byte[] decoded = codec.decode(encoded);
314 assertArrayEquals(input, decoded, "Round-trip with space in encoding failed");
315 }
316
317
318
319
320 @Test
321 void testDecodeStringRfc9285TestVectors() {
322 final Base45 codec = new Base45();
323 for (final String[] testCase : RFC9285_TEST_VECTORS) {
324 final String plainText = testCase[0];
325 final String encoded = testCase[1];
326 final byte[] expected = plainText.getBytes(StandardCharsets.ISO_8859_1);
327 final byte[] actual = codec.decode(encoded);
328 assertArrayEquals(expected, actual, () -> "RFC 9285 decode(String) failed for: '" + encoded + "'");
329 }
330 }
331
332
333
334
335 @Test
336 void testDecodeTableLength() {
337 assertEquals(128, Base45.DECODE_TABLE.length);
338 }
339
340
341
342
343
344 @Test
345 void testDecodeTripleExceedingMaxValue() {
346 final Base45 codec = new Base45();
347
348
349 assertThrows(IllegalArgumentException.class, () -> codec.decode(":::"),
350 "Triple ':' ':' ':' decodes to 91124 which exceeds 65535 and should be rejected");
351 }
352
353
354
355
356 @Test
357 void testDecodeZeroZero() {
358 assertArrayEquals(new byte[] { 0, 0 }, new Base45().decode("000"));
359 }
360
361
362
363
364 @Test
365 void testEncodeAB() {
366 final byte[] input = "AB".getBytes(StandardCharsets.US_ASCII);
367 assertEquals("BB8", new Base45().encodeToString(input));
368 assertArrayEquals(input, new Base45().decode("BB8"));
369 }
370
371
372
373
374 @Test
375 void testEncodeAsString() {
376 final Base45 codec = new Base45();
377 final byte[] input = "ietf!".getBytes(StandardCharsets.US_ASCII);
378 assertEquals("QED8WEX0", codec.encodeAsString(input));
379 assertEquals(codec.encodeToString(input), codec.encodeAsString(input));
380 }
381
382
383
384
385 @Test
386 void testEncodeDecodeSingleByteRoundTrip() {
387 final Base45 codec = new Base45();
388 for (int i = 0; i <= 255; i++) {
389 final byte[] input = { (byte) i };
390 final byte[] encoded = codec.encode(input);
391 assertEquals(2, encoded.length, "Single byte should encode to 2 chars, byte value: " + i);
392 final byte[] decoded = codec.decode(encoded);
393 assertArrayEquals(input, decoded, "Round-trip failed for byte value: " + i);
394 }
395 }
396
397
398
399
400 @Test
401 void testEncodeDecodeTableConsistency() {
402 final byte[] encodeTable = Base45.ENCODE_TABLE;
403 final byte[] decodeTable = Base45.DECODE_TABLE;
404 for (int i = 0; i < encodeTable.length; i++) {
405 final int encoded = encodeTable[i] & 0xFF;
406 assertTrue(encoded < decodeTable.length, "Encode table char " + (char) encoded + " at index " + i + " exceeds decode table length");
407 assertEquals(i, decodeTable[encoded], "Decode table mismatch for char '" + (char) encoded + "' at encode index " + i);
408 }
409 }
410
411
412
413
414 @Test
415 void testEncodeDecodeTwoByteRoundTrip() {
416 final Base45 codec = new Base45();
417
418 final int[] interestingValues = { 0, 1, 44, 45, 254, 255, 256, 2024, 2025, 65534, 65535 };
419 for (final int n : interestingValues) {
420 final byte[] input = { (byte) (n >> 8), (byte) (n & 0xFF) };
421 final byte[] encoded = codec.encode(input);
422 assertEquals(3, encoded.length, "Two bytes should encode to 3 chars, n=" + n);
423 final byte[] decoded = codec.decode(encoded);
424 assertArrayEquals(input, decoded, "Round-trip failed for two-byte value n=" + n);
425 }
426 }
427
428
429
430
431
432 @Test
433 void testEncodeDecodeWithSpaceInOutput() {
434 final Base45 codec = new Base45();
435
436
437
438 final byte[] input = { 0, 36 };
439 final byte[] encoded = codec.encode(input);
440 assertTrue(codec.isInAlphabet(encoded[0]), "First encoded char should be in alphabet");
441 assertEquals((byte) ' ', encoded[0], "First encoded char should be space (value 36)");
442 assertArrayEquals(input, codec.decode(encoded));
443 }
444
445
446
447
448 @Test
449 void testEncodedLength() {
450 final Base45 codec = new Base45();
451 final int[] inputLengths = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 1000 };
452 for (final int n : inputLengths) {
453 final byte[] input = new byte[n];
454 final long expected = (long) n / 2 * 3 + (n % 2 != 0 ? 2 : 0);
455 assertEquals(expected, codec.getEncodedLength(input), "getEncodedLength incorrect for input length " + n);
456 }
457 }
458
459
460
461
462 @Test
463 void testEncodedOutputIsInAlphabet() {
464 final Base45 codec = new Base45();
465 final Random rng = new Random(42L);
466 for (int len = 0; len <= 100; len++) {
467 final byte[] input = new byte[len];
468 rng.nextBytes(input);
469 final byte[] encoded = codec.encode(input);
470 for (final byte b : encoded) {
471 assertTrue(codec.isInAlphabet(b), "Encoded byte " + b + " ('" + (char) b + "') is not in Base45 alphabet");
472 }
473 }
474 }
475
476
477
478
479 @Test
480 void testEncodeEmpty() {
481 final Base45 codec = new Base45();
482 assertArrayEquals(new byte[0], codec.encode(new byte[0]));
483 assertEquals("", codec.encodeToString(new byte[0]));
484 }
485
486
487
488
489 @Test
490 void testEncodeMaxMax() {
491 final byte[] input = { (byte) 0xFF, (byte) 0xFF };
492 assertEquals("FGW", new Base45().encodeToString(input));
493 }
494
495
496
497
498 @Test
499 void testEncodeObject() throws EncoderException {
500 final Base45 codec = new Base45();
501 final byte[] input = "ietf!".getBytes(StandardCharsets.US_ASCII);
502 final Object result = codec.encode((Object) input);
503 assertArrayEquals("QED8WEX0".getBytes(StandardCharsets.US_ASCII), (byte[]) result);
504 }
505
506
507
508
509 @Test
510 void testEncodeObjectThrowsForNonByteArray() {
511 final Base45 codec = new Base45();
512 assertThrows(EncoderException.class, () -> codec.encode("not a byte array"));
513 }
514
515
516
517
518
519
520
521
522 @Test
523 void testEncodeResetsWorkAreaAfterCompleteGroup() {
524 final Base45 codec = new Base45();
525 final BaseNCodec.Context context = new BaseNCodec.Context();
526 final byte[] data = { 1, 2, 3, 4, 5, 6 };
527 codec.encode(data, 0, data.length, context);
528 assertEquals(0, context.modulus, "modulus must be 0 after an even number of encoded bytes");
529 assertEquals(0, context.ibitWorkArea, "encode() must reset ibitWorkArea after emitting each complete group, not leave stale accumulator data");
530 }
531
532
533
534
535 @Test
536 void testEncodeRfc9285TestVectors() {
537 final Base45 codec = new Base45();
538 for (final String[] testCase : RFC9285_TEST_VECTORS) {
539 final String plainText = testCase[0];
540 final String expected = testCase[1];
541 final byte[] input = plainText.getBytes(StandardCharsets.ISO_8859_1);
542 final String actual = codec.encodeToString(input);
543 assertEquals(expected, actual, "RFC 9285 encode failed for: '" + plainText + "'");
544 }
545 }
546
547
548
549
550 @Test
551 void testEncodeSingleByteLetterA() {
552 final byte[] input = "A".getBytes(StandardCharsets.US_ASCII);
553 assertEquals("K1", new Base45().encodeToString(input));
554 assertArrayEquals(input, new Base45().decode("K1"));
555 }
556
557
558
559
560 @Test
561 void testEncodeSingleByteOne() {
562 final byte[] input = { 1 };
563 assertEquals("10", new Base45().encodeToString(input));
564 assertArrayEquals(input, new Base45().decode("10"));
565 }
566
567
568
569
570 @Test
571 void testEncodeSingleMaxByte() {
572 final byte[] input = { (byte) 0xFF };
573 assertEquals("U5", new Base45().encodeToString(input));
574 }
575
576
577
578
579 @Test
580 void testEncodeSingleZeroByte() {
581 final byte[] input = { 0 };
582 assertEquals("00", new Base45().encodeToString(input));
583 }
584
585
586
587
588 @Test
589 void testEncodeTableDigits() {
590 for (int i = 0; i <= 9; i++) {
591 assertEquals((byte) ('0' + i), Base45.ENCODE_TABLE[i], "ENCODE_TABLE[" + i + "] should be digit '" + (char) ('0' + i) + "'");
592 }
593 }
594
595
596
597
598 @Test
599 void testEncodeTableHas45Entries() {
600 assertEquals(45, Base45.ENCODE_TABLE.length);
601 }
602
603
604
605
606 @Test
607 void testEncodeTableSpecialChars() {
608 assertEquals((byte) ' ', Base45.ENCODE_TABLE[36], "ENCODE_TABLE[36] should be space");
609 assertEquals((byte) '$', Base45.ENCODE_TABLE[37], "ENCODE_TABLE[37] should be '$'");
610 assertEquals((byte) '%', Base45.ENCODE_TABLE[38], "ENCODE_TABLE[38] should be '%'");
611 assertEquals((byte) '*', Base45.ENCODE_TABLE[39], "ENCODE_TABLE[39] should be '*'");
612 assertEquals((byte) '+', Base45.ENCODE_TABLE[40], "ENCODE_TABLE[40] should be '+'");
613 assertEquals((byte) '-', Base45.ENCODE_TABLE[41], "ENCODE_TABLE[41] should be '-'");
614 assertEquals((byte) '.', Base45.ENCODE_TABLE[42], "ENCODE_TABLE[42] should be '.'");
615 assertEquals((byte) '/', Base45.ENCODE_TABLE[43], "ENCODE_TABLE[43] should be '/'");
616 assertEquals((byte) ':', Base45.ENCODE_TABLE[44], "ENCODE_TABLE[44] should be ':'");
617 }
618
619
620
621
622 @Test
623 void testEncodeTableUppercase() {
624 for (int i = 0; i < 26; i++) {
625 assertEquals((byte) ('A' + i), Base45.ENCODE_TABLE[10 + i], "ENCODE_TABLE[" + (10 + i) + "] should be letter '" + (char) ('A' + i) + "'");
626 }
627 }
628
629
630
631
632 @Test
633 void testEncodeWithOffsetAndLength() {
634 final Base45 codec = new Base45();
635 final byte[] buffer = new byte[10];
636
637 final byte[] input = "ietf!".getBytes(StandardCharsets.US_ASCII);
638 System.arraycopy(input, 0, buffer, 2, input.length);
639 final byte[] encoded = codec.encode(buffer, 2, 5);
640 assertEquals("QED8WEX0", new String(encoded, StandardCharsets.US_ASCII));
641 }
642
643
644
645
646
647
648
649
650 @Test
651 void testEncodeWorkAreaHoldsOnlyPendingByte() {
652 final Base45 codec = new Base45();
653 final BaseNCodec.Context context = new BaseNCodec.Context();
654 final byte[] data = { 1, 2, 3, 4, 99 };
655 codec.encode(data, 0, data.length, context);
656 assertEquals(1, context.modulus, "one pending byte must leave modulus == 1");
657 assertEquals(99, context.ibitWorkArea, "after two complete groups, ibitWorkArea must hold only the pending byte, not stale high bits");
658 }
659
660
661
662
663 @Test
664 void testEncodeZeroZero() {
665 final byte[] input = { 0, 0 };
666 assertEquals("000", new Base45().encodeToString(input));
667 }
668
669
670
671
672 @Test
673 void testGetEncodedLength() {
674 final Base45 codec = new Base45();
675 assertEquals(0L, codec.getEncodedLength(new byte[0]));
676 assertEquals(2L, codec.getEncodedLength(new byte[1]));
677 assertEquals(3L, codec.getEncodedLength(new byte[2]));
678 assertEquals(5L, codec.getEncodedLength(new byte[3]));
679 assertEquals(6L, codec.getEncodedLength(new byte[4]));
680 assertEquals(8L, codec.getEncodedLength(new byte[5]));
681 assertEquals(9L, codec.getEncodedLength(new byte[6]));
682 assertEquals(11L, codec.getEncodedLength(new byte[7]));
683 }
684
685
686
687
688 @Test
689 void testGetEncodedLengthMatchesActual() {
690 final Base45 codec = new Base45();
691 for (int len = 0; len <= 30; len++) {
692 final byte[] input = new byte[len];
693 Arrays.fill(input, (byte) 0xAB);
694 final byte[] encoded = codec.encode(input);
695 assertEquals(codec.getEncodedLength(input), encoded.length, "getEncodedLength disagrees with actual length for input length " + len);
696 }
697 }
698
699
700
701
702 @Test
703 void testIsInAlphabet_allValidChars() {
704 final Base45 codec = new Base45();
705 final String alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:";
706 assertEquals(45, alphabet.length());
707 for (final char c : alphabet.toCharArray()) {
708 assertTrue(codec.isInAlphabet((byte) c), "Character '" + c + "' (ASCII " + (int) c + ") should be in Base45 alphabet");
709 }
710 }
711
712
713
714
715 @Test
716 void testIsInAlphabet_invalidChars() {
717 final Base45 codec = new Base45();
718
719 for (int i = 0; i < 32; i++) {
720 if (i != ' ') {
721
722 assertFalse(codec.isInAlphabet((byte) i), "Control char " + i + " should not be in Base45 alphabet");
723 }
724 }
725
726 assertFalse(codec.isInAlphabet((byte) '!'), "! should not be in Base45 alphabet");
727 assertFalse(codec.isInAlphabet((byte) '"'), "\" should not be in Base45 alphabet");
728 assertFalse(codec.isInAlphabet((byte) '#'), "# should not be in Base45 alphabet");
729 assertFalse(codec.isInAlphabet((byte) '&'), "& should not be in Base45 alphabet");
730 assertFalse(codec.isInAlphabet((byte) '\''), "' should not be in Base45 alphabet");
731 assertFalse(codec.isInAlphabet((byte) '('), "( should not be in Base45 alphabet");
732 assertFalse(codec.isInAlphabet((byte) ')'), ") should not be in Base45 alphabet");
733 assertFalse(codec.isInAlphabet((byte) ','), ", should not be in Base45 alphabet");
734 assertFalse(codec.isInAlphabet((byte) ';'), "; should not be in Base45 alphabet");
735 assertFalse(codec.isInAlphabet((byte) '<'), "< should not be in Base45 alphabet");
736 assertFalse(codec.isInAlphabet((byte) '='), "= should not be in Base45 alphabet");
737 assertFalse(codec.isInAlphabet((byte) '>'), "> should not be in Base45 alphabet");
738 assertFalse(codec.isInAlphabet((byte) '?'), "? should not be in Base45 alphabet");
739 assertFalse(codec.isInAlphabet((byte) '@'), "@ should not be in Base45 alphabet");
740 assertFalse(codec.isInAlphabet((byte) '['), "[ should not be in Base45 alphabet");
741 assertFalse(codec.isInAlphabet((byte) '\\'), "\\ should not be in Base45 alphabet");
742 assertFalse(codec.isInAlphabet((byte) ']'), "] should not be in Base45 alphabet");
743 assertFalse(codec.isInAlphabet((byte) '^'), "^ should not be in Base45 alphabet");
744 assertFalse(codec.isInAlphabet((byte) '_'), "_ should not be in Base45 alphabet");
745 assertFalse(codec.isInAlphabet((byte) '`'), "` should not be in Base45 alphabet");
746
747 for (char c = 'a'; c <= 'z'; c++) {
748 assertFalse(codec.isInAlphabet((byte) c), "Lowercase '" + c + "' should not be in Base45 alphabet");
749 }
750
751 assertFalse(codec.isInAlphabet((byte) 0x80), "Byte 0x80 should not be in Base45 alphabet");
752 assertFalse(codec.isInAlphabet((byte) 0xFF), "Byte 0xFF should not be in Base45 alphabet");
753 }
754
755
756
757
758
759 @Test
760 void testIsInAlphabetArrayDoesNotAllowPad() {
761 final Base45 codec = new Base45();
762 assertFalse(codec.isInAlphabet("QED="), "'=' (inherited pad) must not be treated as in-alphabet for padding-less Base45");
763 assertFalse(codec.isInAlphabet("QED=".getBytes(StandardCharsets.US_ASCII), true));
764 assertFalse(codec.isInAlphabet("QED=".getBytes(StandardCharsets.US_ASCII), false));
765 assertTrue(codec.isInAlphabet("QED\t8WE".getBytes(StandardCharsets.US_ASCII), true));
766 assertFalse(codec.isInAlphabet("QED\t8WE".getBytes(StandardCharsets.US_ASCII), false));
767 assertTrue(codec.isInAlphabet("QED8WEX0".getBytes(StandardCharsets.US_ASCII), true));
768 }
769
770
771
772
773 @Test
774 void testIsInAlphabetByteArray() {
775 final Base45 codec = new Base45();
776 assertTrue(codec.isInAlphabet("QED8WEX0".getBytes(StandardCharsets.US_ASCII), false));
777 assertTrue(codec.isInAlphabet(new byte[0], false));
778 assertFalse(codec.isInAlphabet("QED!WEX0".getBytes(StandardCharsets.US_ASCII), false));
779 assertFalse(codec.isInAlphabet("abc".getBytes(StandardCharsets.US_ASCII), false));
780 }
781
782
783
784
785 @Test
786 void testLongInputRoundTrip() {
787 final Base45 codec = new Base45();
788 final byte[] input = new byte[1000];
789 new Random(99999L).nextBytes(input);
790 final byte[] encoded = codec.encode(input);
791 assertEquals(codec.getEncodedLength(input), encoded.length);
792 assertArrayEquals(input, codec.decode(encoded));
793 }
794
795
796
797
798
799
800
801
802
803
804
805 @Test
806 void testRfc9285TestVector1_ietf() {
807 final byte[] input = "ietf!".getBytes(StandardCharsets.US_ASCII);
808 assertEquals("QED8WEX0", new Base45().encodeToString(input));
809 assertArrayEquals(input, new Base45().decode("QED8WEX0"));
810 }
811
812
813
814
815
816
817
818
819
820
821
822
823 @Test
824 void testRfc9285TestVector2_base45() {
825 final byte[] input = "base-45".getBytes(StandardCharsets.US_ASCII);
826 assertEquals("UJCLQE7W581", new Base45().encodeToString(input));
827 assertArrayEquals(input, new Base45().decode("UJCLQE7W581"));
828 }
829
830
831
832
833 @Test
834 void testRoundTripAllByteValues() {
835 final Base45 codec = new Base45();
836 final byte[] allBytes = new byte[256];
837 for (int i = 0; i < 256; i++) {
838 allBytes[i] = (byte) i;
839 }
840 final byte[] encoded = codec.encode(allBytes);
841 final byte[] decoded = codec.decode(encoded);
842 assertArrayEquals(allBytes, decoded, "Round-trip failed for all byte values");
843 }
844
845
846
847
848 @Test
849 void testRoundTripAllLengths() {
850 final Base45 codec = new Base45();
851 final Random rng = new Random(12345L);
852 for (int len = 0; len <= 50; len++) {
853 final byte[] input = new byte[len];
854 rng.nextBytes(input);
855 final byte[] encoded = codec.encode(input);
856 final byte[] decoded = codec.decode(encoded);
857 assertArrayEquals(input, decoded, "Round-trip failed for length " + len);
858 }
859 }
860
861
862
863
864 @Test
865 void testRoundTripStrings() {
866 final Base45 codec = new Base45();
867
868 final String[] inputs = {
869 "Hello, World!",
870 "The quick brown fox jumps over the lazy dog",
871 "Apache Commons Codec",
872 "0123456789",
873 "\u0000\u0001\u0002",
874 "Base45 (RFC 9285)",
875 };
876
877 for (final String input : inputs) {
878 final byte[] bytes = input.getBytes(StandardCharsets.UTF_8);
879 final byte[] decoded = codec.decode(codec.encode(bytes));
880 assertArrayEquals(bytes, decoded, () -> "Round-trip failed for: " + input);
881 }
882 }
883
884
885
886
887 @Test
888 void testSingleByteAroundBase45Multiples() {
889 final Base45 codec = new Base45();
890
891 for (int i = 0; i <= 255; i += 45) {
892 final byte[] input = { (byte) i };
893 final byte[] encoded = codec.encode(input);
894 assertEquals(2, encoded.length, "Encoded length should be 2 for single byte, value=" + i);
895 assertArrayEquals(input, codec.decode(encoded), "Round-trip failed for single byte value=" + i);
896 }
897 }
898
899
900
901
902 @Test
903 void testStreamingEncodeDecodeIncremental() throws IOException {
904 final byte[] input = "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8);
905 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
906 try (Base45OutputStream out = new Base45OutputStream(baos)) {
907
908 for (int i = 0; i < input.length; i += 3) {
909 final int len = Math.min(3, input.length - i);
910 out.write(input, i, len);
911 }
912 }
913 compare(input, baos);
914 }
915
916
917
918
919 @Test
920 void testStreamingEncodeDecodeOneByteChunks() throws IOException {
921 final byte[] input = "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8);
922 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
923 try (Base45OutputStream out = new Base45OutputStream(baos)) {
924 for (final byte element : input) {
925 out.write(element);
926 }
927 }
928 compare(input, baos);
929 }
930
931
932
933
934 @Test
935 void testTwoByteExtremeValues() {
936 final Base45 codec = new Base45();
937
938 assertArrayEquals(new byte[] { 0, 0 }, codec.decode("000"));
939 assertEquals("000", codec.encodeToString(new byte[] { 0, 0 }));
940
941 assertArrayEquals(new byte[] { (byte) 0xFF, (byte) 0xFF }, codec.decode("FGW"));
942 assertEquals("FGW", codec.encodeToString(new byte[] { (byte) 0xFF, (byte) 0xFF }));
943 }
944 }