master ohmu/fastfetch-openbsd / tests / base64.c
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}