ohmu
·
2026-10-15
1#include "common/base64.h"
2#include "common/strutil.h"
3#include "common/textModifier.h"
4
5#include <stdint.h>
6#include <stdio.h>
7#include <stdlib.h>
8#include <string.h>
9
10static void verify(bool expression, const char* expressionStr, int lineNo) {
11 if (expression) {
12 return;
13 }
14
15 fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s\n" FASTFETCH_TEXT_MODIFIER_RESET, lineNo, expressionStr);
16 exit(1);
17}
18
19#define VERIFY(expression) verify((expression), #expression, __LINE__)
20
21// The empty string is a valid case and yields no bytes.
22static uint32_t fromHex(const char* hex, uint8_t* out) {
23 uint32_t length = 0;
24 for (const char* p = hex; p[0] != '\0' && p[1] != '\0'; p += 2) {
25 unsigned value = 0;
26 if (sscanf(p, "%2x", &value) != 1) {
27 return 0;
28 }
29 out[length++] = (uint8_t) value;
30 }
31 return length;
32}
33
34typedef struct FFBase64EncodingCase {
35 const char* name;
36 const char* hex;
37 const char* expected;
38} FFBase64EncodingCase;
39
40// The RFC 4648 vectors cover the whole groups. The rest cover the partial group at the end of a
41// source whose length is not a multiple of three, which is where the encoder used to go wrong: a
42// plain `char` is signed on the supported platforms, so widening a byte with the high bit set into
43// the 64 bit accumulator sign extended it, and the six bit fields read back out of that accumulator
44// no longer held the byte's own bits.
45//
46// Only the two byte partial group was affected. One byte left over is shifted into place and never
47// OR-ed into, so its sign extension lands above the fields that are read; two bytes left over OR the
48// second byte in after shifting it by 8, which drags the sign bits down into them.
49static const FFBase64EncodingCase ENCODING_CASES[] = {
50 // RFC 4648 test vectors
51 { "rfc4648-empty", "", "" },
52 { "rfc4648-f", "66", "Zg==" },
53 { "rfc4648-fo", "666f", "Zm8=" },
54 { "rfc4648-foo", "666f6f", "Zm9v" },
55 { "rfc4648-foob", "666f6f62", "Zm9vYg==" },
56 { "rfc4648-fooba", "666f6f6261", "Zm9vYmE=" },
57 { "rfc4648-foobar", "666f6f626172", "Zm9vYmFy" },
58
59 // One byte left over (length % 3 == 1)
60 { "single-80", "41414180", "QUFBgA==" },
61 { "single-b9", "414141b9", "QUFBuQ==" },
62 { "single-c6", "414141c6", "QUFBxg==" },
63 { "single-ff", "414141ff", "QUFB/w==" },
64
65 // Two bytes left over (length % 3 == 2), with the high bit on the second byte
66 { "pair-second-80", "4141414180", "QUFBQYA=" },
67 { "pair-second-9c", "414141419c", "QUFBQZw=" },
68 { "pair-second-b9", "41414141b9", "QUFBQbk=" },
69 { "pair-second-c6", "41414141c6", "QUFBQcY=" },
70 { "pair-second-ff", "41414141ff", "QUFBQf8=" },
71
72 // Two bytes left over (length % 3 == 2), with the high bit on the first byte
73 { "pair-first-80", "4141418041", "QUFBgEE=" },
74 { "pair-first-9c", "4141419c41", "QUFBnEE=" },
75 { "pair-first-b9", "414141b941", "QUFBuUE=" },
76 { "pair-first-c6", "414141c641", "QUFBxkE=" },
77 { "pair-first-ff", "414141ff41", "QUFB/0E=" },
78
79 // Two bytes left over (length % 3 == 2), with the high bit on both
80 { "pair-both-80", "4141418080", "QUFBgIA=" },
81 { "pair-both-b9", "414141b9b9", "QUFBubk=" },
82 { "pair-both-c6", "414141c6c6", "QUFBxsY=" },
83 { "pair-both-ff", "414141ffff", "QUFB//8=" },
84
85 // The shape the kitty animation encoder hands over: a zlib stream for one 100x100 RGBA frame.
86 // Its last two bytes (0x79 0xb9) are what the sign extension used to mangle. The terminal then
87 // rejected the frame with a zlib checksum error, so the animation was transmitted correctly but
88 // never played.
89 { "kitty-animation-frame", "78daedd1310d00000cc3b0f227dd91a876f9308124490300000000000000000000000000000000000000231dd3d40f3ffcf0c30f3ffcf0c30f3ff0c30ffcf0c30f3ffcf0c30f3ffcf0033ffcc00f0000000000000000000000f876fa5e79b9", "eNrt0TENAAAMw7DyJ92RqHb5MIEkSQMAAAAAAAAAAAAAAAAAAAAAAAAAIx3T1A8//PDDDz/88MMPP/DDD/zwww8//PDDDz/88AM//MAPAAAAAAAAAAAAAAD4dvpeebk=" },
90};
91
92static void verifyEncodingCase(const FFBase64EncodingCase* testCase, int lineNo) {
93 static uint8_t source[512];
94 const uint32_t sourceLength = fromHex(testCase->hex, source);
95 VERIFY(sourceLength <= sizeof(source));
96
97 static char encoded[1024];
98 uint32_t encodedLength = 0;
99 memset(encoded, 0, sizeof(encoded));
100 ffBase64EncodeRaw(sourceLength, (const char*) source, &encodedLength, encoded);
101
102 if (!ffStrEquals(encoded, testCase->expected)) {
103 fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s: expected \"%s\", got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET,
104 lineNo, testCase->name, testCase->expected, encoded);
105 exit(1);
106 }
107
108 // The strbuf wrapper has to agree with the raw function
109 FF_STRBUF_AUTO_DESTROY input = ffStrbufCreateNS(sourceLength, (const char*) source);
110 FF_STRBUF_AUTO_DESTROY wrapped = ffBase64EncodeStrbuf(&input);
111 if (!ffStrbufEqualS(&wrapped, testCase->expected)) {
112 fprintf(stderr, FASTFETCH_TEXT_MODIFIER_ERROR "[%d] %s: strbuf wrapper got \"%s\"\n" FASTFETCH_TEXT_MODIFIER_RESET,
113 lineNo, testCase->name, wrapped.chars);
114 exit(1);
115 }
116}
117
118// Covers every length, so a break in any of the three code paths (whole groups, one byte left
119// over, two bytes left over) shows up at the length that reaches it. The pattern puts the high bit
120// on roughly half of the bytes, which is what the partial groups are sensitive to.
121static void verifyRoundTrip(void) {
122 static uint8_t source[512];
123 static char encoded[1024];
124 static char decoded[512];
125
126 for (uint32_t length = 0; length <= 400; ++length) {
127 for (uint32_t i = 0; i < length; ++i) {
128 source[i] = (uint8_t) (i * 37 + 0x80);
129 }
130
131 uint32_t encodedLength = 0;
132 memset(encoded, 0, sizeof(encoded));
133 ffBase64EncodeRaw(length, (const char*) source, &encodedLength, encoded);
134 VERIFY(encodedLength == (length + 2) / 3 * 4);
135
136 uint32_t decodedLength = 0;
137 memset(decoded, 0, sizeof(decoded));
138 ffBase64DecodeRaw(encodedLength, encoded, &decodedLength, decoded);
139 VERIFY(decodedLength == length);
140 VERIFY(memcmp(decoded, source, length) == 0);
141 }
142}
143
144int main(void) {
145 for (size_t i = 0; i < sizeof(ENCODING_CASES) / sizeof(ENCODING_CASES[0]); ++i) {
146 verifyEncodingCase(&ENCODING_CASES[i], __LINE__);
147 }
148
149 verifyRoundTrip();
150
151 puts("\033[32mAll tests passed!" FASTFETCH_TEXT_MODIFIER_RESET);
152 return 0;
153}