diff --git a/.github/workflows/github-actions-test.yml b/.github/workflows/github-actions-test.yml index ae40bac2..6cc82bea 100644 --- a/.github/workflows/github-actions-test.yml +++ b/.github/workflows/github-actions-test.yml @@ -24,5 +24,7 @@ jobs: uses: actions/checkout@v4 with: submodules: 'recursive' + - name: Install test dependencies + run: sudo apt-get update && sudo apt-get install -y zlib1g-dev - name: Run tests run: ./scripts/test.sh diff --git a/.github/workflows/test-each-commit.yml b/.github/workflows/test-each-commit.yml index 9747f3b4..a39b5ced 100644 --- a/.github/workflows/test-each-commit.yml +++ b/.github/workflows/test-each-commit.yml @@ -72,8 +72,8 @@ jobs: - name: Show commit run: git log -1 --oneline - - name: Install clang-format - run: apt-get update && apt-get install -y clang-format + - name: Install check dependencies + run: apt-get update && apt-get install -y clang-format zlib1g-dev - name: Configure git identity (required by rebase) run: | diff --git a/.gitignore b/.gitignore index 01fe38be..01dc4f51 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ simulator/sim_data/ # Tests components/bbqr/test/test_bbqr components/bbqr/test/test_base32 -components/bbqr/test/test_miniz +components/deflate_codec/test/test_deflate_codec main/core/test/test_derivation_path main/core/test/test_ss_whitelist_parse main/core/test/test_ss_whitelist_is_whitelisted diff --git a/components/bbqr/CMakeLists.txt b/components/bbqr/CMakeLists.txt index 8581438c..654b3536 100644 --- a/components/bbqr/CMakeLists.txt +++ b/components/bbqr/CMakeLists.txt @@ -2,7 +2,8 @@ idf_component_register( SRCS "src/bbqr.c" "src/base32.c" - "src/miniz.c" INCLUDE_DIRS "src" + PRIV_REQUIRES + deflate_codec ) diff --git a/components/bbqr/src/base32.c b/components/bbqr/src/base32.c index 71492d2e..b6bd2102 100644 --- a/components/bbqr/src/base32.c +++ b/components/bbqr/src/base32.c @@ -26,8 +26,14 @@ static const int8_t BASE32_DECODE_TABLE[128] = { }; size_t base32_encoded_len(size_t input_len) { - // Each 5 bytes becomes 8 characters - return ((input_len + 4) / 5) * 8; + // BBQr omits RFC 4648 padding, so partial groups use 2, 4, 5, or 7 chars. + static const uint8_t chars_per_remainder[] = {0, 2, 4, 5, 7}; + size_t full_groups = input_len / 5; + size_t remainder_chars = chars_per_remainder[input_len % 5]; + if (full_groups > (SIZE_MAX - remainder_chars) / 8) { + return 0; + } + return full_groups * 8 + remainder_chars; } size_t base32_decoded_len(size_t input_len) { @@ -35,18 +41,12 @@ size_t base32_decoded_len(size_t input_len) { return (input_len * 5) / 8; } -size_t base32_encode(const uint8_t *input, size_t input_len, char *output, - size_t output_size) { - if (!input || !output || input_len == 0) { - return 0; - } - - size_t encoded_len = base32_encoded_len(input_len); - if (output_size < encoded_len + 1) { - return 0; +bool base32_encode_write(const uint8_t *input, size_t input_len, + base32_write_fn write, void *context) { + if (!input || !write || input_len == 0) { + return false; } - size_t out_idx = 0; size_t in_idx = 0; // Process input in groups of 5 bytes -> 8 chars @@ -60,7 +60,6 @@ size_t base32_encode(const uint8_t *input, size_t input_len, char *output, bytes_in_group++; } - // Calculate how many base32 chars we need for this group // 1 byte (8 bits) -> 2 chars // 2 bytes (16 bits) -> 4 chars // 3 bytes (24 bits) -> 5 chars @@ -68,20 +67,60 @@ size_t base32_encode(const uint8_t *input, size_t input_len, char *output, // 5 bytes (40 bits) -> 8 chars static const int chars_per_bytes[] = {0, 2, 4, 5, 7, 8}; int num_chars = chars_per_bytes[bytes_in_group]; + char encoded[8]; + + for (int i = 0; i < num_chars; i++) { + int idx = (buffer >> (35 - i * 5)) & 0x1F; + encoded[i] = BASE32_ALPHABET[idx]; + } - // Extract 5-bit groups from high bits - for (int i = 0; i < 8; i++) { - if (i < num_chars) { - int idx = (buffer >> (35 - i * 5)) & 0x1F; - output[out_idx++] = BASE32_ALPHABET[idx]; - } else { - output[out_idx++] = '='; - } + if (!write(context, encoded, (size_t)num_chars)) { + return false; } } - output[out_idx] = '\0'; - return out_idx; + return true; +} + +typedef struct { + char *output; + size_t output_size; + size_t output_len; +} base32_buffer_writer_t; + +static bool write_to_buffer(void *context, const char *data, size_t len) { + base32_buffer_writer_t *writer = (base32_buffer_writer_t *)context; + if (len > writer->output_size - writer->output_len - 1) { + return false; + } + + memcpy(writer->output + writer->output_len, data, len); + writer->output_len += len; + return true; +} + +size_t base32_encode(const uint8_t *input, size_t input_len, char *output, + size_t output_size) { + if (!input || !output || input_len == 0) { + return 0; + } + + size_t encoded_len = base32_encoded_len(input_len); + if (output_size < encoded_len + 1) { + return 0; + } + + base32_buffer_writer_t writer = { + .output = output, + .output_size = output_size, + .output_len = 0, + }; + if (!base32_encode_write(input, input_len, write_to_buffer, &writer)) { + return 0; + } + + output[writer.output_len] = '\0'; + return writer.output_len; } bool base32_decode(const char *input, size_t input_len, uint8_t *output, diff --git a/components/bbqr/src/base32.h b/components/bbqr/src/base32.h index b19ca58c..97210095 100644 --- a/components/bbqr/src/base32.h +++ b/components/bbqr/src/base32.h @@ -5,6 +5,13 @@ #include #include +/** + * @brief Callback used by base32_encode_write(). + * + * @return true to continue encoding, false to stop with failure. + */ +typedef bool (*base32_write_fn)(void *context, const char *data, size_t len); + /** * @brief Calculate the encoded length for base32 encoding * @@ -25,7 +32,8 @@ size_t base32_decoded_len(size_t input_len); * @brief Encode binary data to base32 (RFC 4648) * * Uses the standard base32 alphabet: ABCDEFGHIJKLMNOPQRSTUVWXYZ234567 - * Output is uppercase and includes padding with '=' characters. + * Output is uppercase and omits padding as required by BBQr. The resulting + * characters are all valid in QR alphanumeric mode. * * @param input Binary data to encode * @param input_len Length of input data in bytes @@ -37,6 +45,14 @@ size_t base32_decoded_len(size_t input_len); size_t base32_encode(const uint8_t *input, size_t input_len, char *output, size_t output_size); +/** + * @brief Encode binary data to unpadded Base32 through a callback. + * + * The callback receives groups of at most eight QR-alphanumeric characters. + */ +bool base32_encode_write(const uint8_t *input, size_t input_len, + base32_write_fn write, void *context); + /** * @brief Decode base32 string to binary data (RFC 4648) * diff --git a/components/bbqr/src/bbqr.c b/components/bbqr/src/bbqr.c index 72006b38..0626161e 100644 --- a/components/bbqr/src/bbqr.c +++ b/components/bbqr/src/bbqr.c @@ -1,15 +1,64 @@ #include "bbqr.h" #include "base32.h" +#include "deflate_codec.h" #include #include #include -// Include miniz for zlib compression -#include "miniz.h" - // Base36 alphabet (0-9, A-Z) static const char BASE36_ALPHABET[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +// BBQr spec: raw deflate with a 1KB window (wbits = -10) +#define BBQR_COMPRESS_WBITS 10 +// Decode liberally: raw inflate at 15 accepts any window, including spec's 10 +#define BBQR_DECODE_WBITS 15 +#define BBQR_MAX_DECOMPRESSED (512U * 1024U) + +typedef struct { + BBQrParts *parts; + int part_index; + size_t payload_offset; + size_t payload_per_part; + size_t encoded_len; + size_t total_written; +} bbqr_parts_writer_t; + +static size_t part_payload_len(size_t encoded_len, size_t payload_per_part, + int index) { + size_t start = (size_t)index * payload_per_part; + size_t remaining = encoded_len - start; + return remaining < payload_per_part ? remaining : payload_per_part; +} + +static bool write_to_parts(void *context, const char *data, size_t len) { + bbqr_parts_writer_t *writer = (bbqr_parts_writer_t *)context; + + while (len > 0) { + if (writer->part_index >= writer->parts->count) { + return false; + } + + size_t part_len = part_payload_len( + writer->encoded_len, writer->payload_per_part, writer->part_index); + size_t available = part_len - writer->payload_offset; + size_t chunk_len = len < available ? len : available; + memcpy(writer->parts->parts[writer->part_index] + BBQR_HEADER_LEN + + writer->payload_offset, + data, chunk_len); + writer->payload_offset += chunk_len; + writer->total_written += chunk_len; + data += chunk_len; + len -= chunk_len; + + if (writer->payload_offset == part_len) { + writer->part_index++; + writer->payload_offset = 0; + } + } + + return true; +} + bool bbqr_is_valid_encoding(char c) { return c == BBQR_ENCODING_HEX || c == BBQR_ENCODING_BASE32 || c == BBQR_ENCODING_ZLIB; @@ -99,6 +148,16 @@ bool bbqr_parse_part(const char *data, size_t data_len, BBQrPart *part) { return true; } +static int hex_nibble(char c) { + if (c >= '0' && c <= '9') + return c - '0'; + if (c >= 'A' && c <= 'F') + return c - 'A' + 10; + if (c >= 'a' && c <= 'f') + return c - 'a' + 10; + return -1; +} + // Helper: decode hex string to binary static uint8_t *decode_hex(const char *hex, size_t hex_len, size_t *out_len) { if (hex_len % 2 != 0) { @@ -112,32 +171,12 @@ static uint8_t *decode_hex(const char *hex, size_t hex_len, size_t *out_len) { } for (size_t i = 0; i < bin_len; i++) { - char h[3] = {hex[i * 2], hex[i * 2 + 1], '\0'}; - - // Convert hex pair to byte - char c1 = h[0]; - char c2 = h[1]; - int v1 = -1, v2 = -1; - - if (c1 >= '0' && c1 <= '9') - v1 = c1 - '0'; - else if (c1 >= 'A' && c1 <= 'F') - v1 = c1 - 'A' + 10; - else if (c1 >= 'a' && c1 <= 'f') - v1 = c1 - 'a' + 10; - - if (c2 >= '0' && c2 <= '9') - v2 = c2 - '0'; - else if (c2 >= 'A' && c2 <= 'F') - v2 = c2 - 'A' + 10; - else if (c2 >= 'a' && c2 <= 'f') - v2 = c2 - 'a' + 10; - + int v1 = hex_nibble(hex[i * 2]); + int v2 = hex_nibble(hex[i * 2 + 1]); if (v1 < 0 || v2 < 0) { free(output); return NULL; } - output[i] = (uint8_t)((v1 << 4) | v2); } @@ -195,15 +234,17 @@ uint8_t *bbqr_decode_payload(char encoding, const char *data, size_t data_len, // Verify header checksum: (CMF*256 + FLG) % 31 == 0 if ((compressed[0] * 256 + compressed[1]) % 31 == 0) { // Try zlib-wrapped decompression first - decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); + decompressed = deflate_decompress_zlib_alloc(compressed, compressed_len, + &decompressed_len, + BBQR_MAX_DECOMPRESSED); } } if (!decompressed) { // Fall back to raw deflate (BBQr spec says raw deflate) - decompressed = - mz_inflate_raw_alloc(compressed, compressed_len, &decompressed_len); + decompressed = deflate_decompress_raw_alloc( + compressed, compressed_len, &decompressed_len, BBQR_DECODE_WBITS, + BBQR_MAX_DECOMPRESSED); } free(compressed); @@ -232,50 +273,24 @@ BBQrParts *bbqr_encode(const uint8_t *data, size_t data_len, char file_type, int max_payload_per_part = max_chars_per_qr - BBQR_HEADER_LEN; - // Try compression first (use raw deflate for BBQr) - uint8_t *compressed = NULL; + // Use compression (raw deflate) only when it actually shrinks the data size_t compressed_len = 0; - char *encoded_data = NULL; - size_t encoded_len = 0; - char encoding = BBQR_ENCODING_BASE32; - - compressed = mz_deflate_raw_alloc(data, data_len, &compressed_len); + uint8_t *compressed = deflate_compress_raw_alloc( + data, data_len, &compressed_len, BBQR_COMPRESS_WBITS); + const uint8_t *to_encode = data; + size_t to_encode_len = data_len; + char encoding = BBQR_ENCODING_BASE32; if (compressed && compressed_len < data_len) { - // Compression helped - use Z encoding - size_t max_encoded = base32_encoded_len(compressed_len); - encoded_data = (char *)malloc(max_encoded + 1); - if (encoded_data) { - encoded_len = base32_encode(compressed, compressed_len, encoded_data, - max_encoded + 1); - if (encoded_len > 0) { - encoding = BBQR_ENCODING_ZLIB; - } else { - free(encoded_data); - encoded_data = NULL; - } - } - free(compressed); - } else { - if (compressed) { - free(compressed); - } + to_encode = compressed; + to_encode_len = compressed_len; + encoding = BBQR_ENCODING_ZLIB; } - // If compression didn't help or failed, use uncompressed base32 - if (!encoded_data) { - size_t max_encoded = base32_encoded_len(data_len); - encoded_data = (char *)malloc(max_encoded + 1); - if (!encoded_data) { - return NULL; - } - - encoded_len = base32_encode(data, data_len, encoded_data, max_encoded + 1); - if (encoded_len == 0) { - free(encoded_data); - return NULL; - } - encoding = BBQR_ENCODING_BASE32; + size_t encoded_len = base32_encoded_len(to_encode_len); + if (encoded_len == 0) { + free(compressed); + return NULL; } // Calculate number of parts needed @@ -285,14 +300,13 @@ BBQrParts *bbqr_encode(const uint8_t *data, size_t data_len, char file_type, payload_per_part = 8; } - int num_parts = (encoded_len + payload_per_part - 1) / payload_per_part; - if (num_parts > 1295) { - free(encoded_data); + size_t parts_needed = encoded_len / (size_t)payload_per_part + + (encoded_len % (size_t)payload_per_part != 0); + if (parts_needed > 1295) { + free(compressed); return NULL; } - if (num_parts < 1) { - num_parts = 1; - } + int num_parts = (int)parts_needed; // Recalculate payload per part to distribute evenly payload_per_part = (encoded_len + num_parts - 1) / num_parts; @@ -302,14 +316,14 @@ BBQrParts *bbqr_encode(const uint8_t *data, size_t data_len, char file_type, // Allocate parts structure BBQrParts *parts = (BBQrParts *)calloc(1, sizeof(BBQrParts)); if (!parts) { - free(encoded_data); + free(compressed); return NULL; } parts->parts = (char **)calloc(num_parts, sizeof(char *)); if (!parts->parts) { free(parts); - free(encoded_data); + free(compressed); return NULL; } @@ -317,50 +331,64 @@ BBQrParts *bbqr_encode(const uint8_t *data, size_t data_len, char file_type, parts->encoding = encoding; parts->file_type = file_type; - // Generate each part - size_t offset = 0; + if (encoded_len > SIZE_MAX - (size_t)num_parts * (BBQR_HEADER_LEN + 1)) { + free(parts->parts); + free(parts); + free(compressed); + return NULL; + } + size_t storage_len = encoded_len + (size_t)num_parts * (BBQR_HEADER_LEN + 1); + parts->storage = (char *)malloc(storage_len); + if (!parts->storage) { + free(parts->parts); + free(parts); + free(compressed); + return NULL; + } + + // Generate headers and lay out all part strings in one allocation. + size_t storage_offset = 0; + char total_c1, total_c2; + bbqr_base36_encode(num_parts, &total_c1, &total_c2); for (int i = 0; i < num_parts; i++) { - size_t remaining = encoded_len - offset; - size_t this_payload_len = (remaining > (size_t)payload_per_part) - ? (size_t)payload_per_part - : remaining; - - // Allocate part string (header + payload + null terminator) - parts->parts[i] = (char *)malloc(BBQR_HEADER_LEN + this_payload_len + 1); - if (!parts->parts[i]) { - // Cleanup on failure - for (int j = 0; j < i; j++) { - free(parts->parts[j]); - } - free(parts->parts); - free(parts); - free(encoded_data); - return NULL; - } + size_t this_payload_len = + part_payload_len(encoded_len, (size_t)payload_per_part, i); // Build header: B$ + encoding + file_type + total(2) + index(2) - char total_c1, total_c2, index_c1, index_c2; - bbqr_base36_encode(num_parts, &total_c1, &total_c2); + char index_c1, index_c2; bbqr_base36_encode(i, &index_c1, &index_c2); - parts->parts[i][0] = 'B'; - parts->parts[i][1] = '$'; - parts->parts[i][2] = encoding; - parts->parts[i][3] = file_type; - parts->parts[i][4] = total_c1; - parts->parts[i][5] = total_c2; - parts->parts[i][6] = index_c1; - parts->parts[i][7] = index_c2; - - // Copy payload - memcpy(parts->parts[i] + BBQR_HEADER_LEN, encoded_data + offset, - this_payload_len); - parts->parts[i][BBQR_HEADER_LEN + this_payload_len] = '\0'; - - offset += this_payload_len; + char *part = parts->storage + storage_offset; + parts->parts[i] = part; + part[0] = 'B'; + part[1] = '$'; + part[2] = encoding; + part[3] = file_type; + part[4] = total_c1; + part[5] = total_c2; + part[6] = index_c1; + part[7] = index_c2; + part[BBQR_HEADER_LEN + this_payload_len] = '\0'; + + storage_offset += BBQR_HEADER_LEN + this_payload_len + 1; + } + + bbqr_parts_writer_t writer = { + .parts = parts, + .part_index = 0, + .payload_offset = 0, + .payload_per_part = (size_t)payload_per_part, + .encoded_len = encoded_len, + .total_written = 0, + }; + bool encoded = + base32_encode_write(to_encode, to_encode_len, write_to_parts, &writer); + free(compressed); + if (!encoded || writer.total_written != encoded_len) { + bbqr_parts_free(parts); + return NULL; } - free(encoded_data); return parts; } @@ -370,13 +398,9 @@ void bbqr_parts_free(BBQrParts *parts) { } if (parts->parts) { - for (int i = 0; i < parts->count; i++) { - if (parts->parts[i]) { - free(parts->parts[i]); - } - } free(parts->parts); } + free(parts->storage); free(parts); } diff --git a/components/bbqr/src/bbqr.h b/components/bbqr/src/bbqr.h index 4ca231be..249454c0 100644 --- a/components/bbqr/src/bbqr.h +++ b/components/bbqr/src/bbqr.h @@ -45,6 +45,8 @@ typedef struct { int count; // Number of parts char encoding; // Encoding used char file_type; // File type + char *storage; // Backing storage for all part strings; free only via + // bbqr_parts_free() } BBQrParts; /** diff --git a/components/bbqr/src/miniz.c b/components/bbqr/src/miniz.c deleted file mode 100644 index 6dd79812..00000000 --- a/components/bbqr/src/miniz.c +++ /dev/null @@ -1,936 +0,0 @@ -/* - * miniz.c - Minimal zlib-compatible compression/decompression - * Based on public domain code. Optimized for embedded use. - * - * LZ77 compression with static Huffman codes based on: - * - uzlib by Paul Sokolovsky (MIT license) - * - PuTTY deflate code by Simon Tatham - * - MicroPython deflate module by Jim Mussared, Damien P. George - */ - -#include "miniz.h" -#include -#include - -/* Default window bits for BBQr (1024 byte window) */ -#define MZ_DEFAULT_WBITS 10 - -/* LZ77 parameters */ -#define MATCH_LEN_MIN 3 -#define MATCH_LEN_MAX 258 - -/* Adler32 checksum */ -static uint32_t adler32(uint32_t adler, const uint8_t *ptr, size_t len) { - uint32_t s1 = adler & 0xffff, s2 = adler >> 16; - while (len > 0) { - size_t block_len = len < 5550 ? len : 5550; - len -= block_len; - while (block_len--) { - s1 += *ptr++; - s2 += s1; - } - s1 %= 65521; - s2 %= 65521; - } - return (s2 << 16) + s1; -} - -/* - * ============================================================================ - * LZ77 Compression with Static Huffman Encoding - * ============================================================================ - */ - -/* Compression state */ -typedef struct { - /* Output buffer */ - uint8_t *out_buf; - size_t out_pos; - size_t out_size; - - /* Bit buffer for output */ - uint32_t out_bits; - int n_out_bits; - - /* History window (circular buffer) */ - uint8_t *hist_buf; - size_t hist_max; /* Window size (power of 2) */ - size_t hist_start; /* Start index in circular buffer */ - size_t hist_len; /* Current bytes in history */ -} mz_compress_state_t; - -/* Mirror nibble lookup table for Huffman code output */ -static const uint8_t s_mirror_nibble[16] = {0x0, 0x8, 0x4, 0xc, 0x2, 0xa, - 0x6, 0xe, 0x1, 0x9, 0x5, 0xd, - 0x3, 0xb, 0x7, 0xf}; - -static inline uint8_t mirror_byte(uint8_t b) { - return (s_mirror_nibble[b & 0xf] << 4) | s_mirror_nibble[b >> 4]; -} - -static inline int int_log2(int x) { - int r = 0; - while (x >>= 1) { - ++r; - } - return r; -} - -/* Output bits to the compression buffer (LSB first) */ -static int out_bits(mz_compress_state_t *s, uint32_t bits, int nbits) { - s->out_bits |= bits << s->n_out_bits; - s->n_out_bits += nbits; - - while (s->n_out_bits >= 8) { - if (s->out_pos >= s->out_size) { - return MZ_BUF_ERROR; - } - s->out_buf[s->out_pos++] = s->out_bits & 0xFF; - s->out_bits >>= 8; - s->n_out_bits -= 8; - } - return MZ_OK; -} - -/* Output a literal byte using static Huffman codes */ -static int out_literal(mz_compress_state_t *s, uint8_t c) { - if (c <= 143) { - /* 0-143: 8 bits starting at 00110000 (0x30) */ - return out_bits(s, mirror_byte(0x30 + c), 8); - } else { - /* 144-255: 9 bits starting at 110010000 (0x190) */ - return out_bits(s, 1 + 2 * mirror_byte(0x90 - 144 + c), 9); - } -} - -/* Output a length/distance match using static Huffman codes */ -static int out_match(mz_compress_state_t *s, int distance, int len) { - int ret; - distance -= 1; - - while (len > 0) { - int thislen; - - /* Max match length per code is 258; handle longer matches in pieces */ - thislen = (len > 260 ? 258 : len <= 258 ? len : len - 3); - len -= thislen; - - thislen -= 3; - int lcode = 28; - int x = int_log2(thislen); - int y; - if (thislen < 255) { - if (x) { - --x; - } - y = (thislen >> (x ? x - 1 : 0)) & 3; - lcode = x * 4 + y; - } - - /* Transmit length code: 256-279 are 7 bits, 280-287 are 8 bits */ - if (lcode <= 22) { - ret = out_bits(s, mirror_byte((lcode + 1) * 2), 7); - } else { - ret = out_bits(s, mirror_byte(lcode + 169), 8); - } - if (ret != MZ_OK) - return ret; - - /* Transmit extra length bits */ - if (thislen < 255 && x > 1) { - int extrabits = x - 1; - int lmin = (thislen >> extrabits) << extrabits; - ret = out_bits(s, thislen - lmin, extrabits); - if (ret != MZ_OK) - return ret; - } - - x = int_log2(distance); - y = (distance >> (x ? x - 1 : 0)) & 1; - - /* Transmit distance code: 5 bits */ - ret = out_bits(s, mirror_byte((x * 2 + y) * 8), 5); - if (ret != MZ_OK) - return ret; - - /* Transmit extra distance bits */ - if (x > 1) { - int dextrabits = x - 1; - int dmin = (distance >> dextrabits) << dextrabits; - ret = out_bits(s, distance - dmin, dextrabits); - if (ret != MZ_OK) - return ret; - } - } - return MZ_OK; -} - -/* Start a deflate block with static Huffman codes */ -static int start_block(mz_compress_state_t *s) { - /* BFINAL=1 (final block), BTYPE=01 (static Huffman) */ - return out_bits(s, 3, 3); -} - -/* End the deflate block */ -static int finish_block(mz_compress_state_t *s) { - int ret; - /* End-of-block symbol (256): 7 bits = 0000000 */ - ret = out_bits(s, 0, 7); - if (ret != MZ_OK) - return ret; - - /* Flush remaining bits (pad to byte boundary) */ - if (s->n_out_bits > 0) { - ret = out_bits(s, 0, 8 - s->n_out_bits); - } - return ret; -} - -/* Search history for longest match */ -static size_t lz77_find_match(mz_compress_state_t *s, const uint8_t *src, - size_t src_len, size_t *match_offset) { - size_t longest_len = 0; - size_t mask = s->hist_max - 1; - - for (size_t hist_search = 0; hist_search < s->hist_len; ++hist_search) { - size_t match_len; - for (match_len = 0; match_len < MATCH_LEN_MAX && match_len < src_len; - ++match_len) { - uint8_t hist; - if (hist_search + match_len < s->hist_len) { - hist = s->hist_buf[(s->hist_start + hist_search + match_len) & mask]; - } else { - hist = src[hist_search + match_len - s->hist_len]; - } - if (src[match_len] != hist) { - break; - } - } - - /* Take this match if it's at least minimum length and >= previous best */ - if (match_len >= MATCH_LEN_MIN && match_len >= longest_len) { - longest_len = match_len; - *match_offset = s->hist_len - hist_search; - } - } - - return longest_len; -} - -/* Push bytes into history buffer */ -static void hist_push(mz_compress_state_t *s, const uint8_t *data, size_t len) { - size_t mask = s->hist_max - 1; - for (size_t i = 0; i < len; i++) { - s->hist_buf[(s->hist_start + s->hist_len) & mask] = data[i]; - if (s->hist_len == s->hist_max) { - s->hist_start = (s->hist_start + 1) & mask; - } else { - ++s->hist_len; - } - } -} - -/* LZ77 compress data chunk */ -static int lz77_compress(mz_compress_state_t *s, const uint8_t *src, - size_t len) { - int ret; - const uint8_t *end = src + len; - - while (src < end) { - size_t match_offset = 0; - size_t match_len = lz77_find_match(s, src, end - src, &match_offset); - - if (match_len == 0) { - /* Output literal */ - ret = out_literal(s, *src); - if (ret != MZ_OK) - return ret; - hist_push(s, src, 1); - src++; - } else { - /* Output match */ - ret = out_match(s, match_offset, match_len); - if (ret != MZ_OK) - return ret; - hist_push(s, src, match_len); - src += match_len; - } - } - return MZ_OK; -} - -/* Length/distance tables for deflate */ -static const uint16_t s_length_base[29] = { - 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, - 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258}; -static const uint8_t s_length_extra[29] = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, - 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, - 4, 4, 4, 4, 5, 5, 5, 5, 0}; -static const uint16_t s_dist_base[30] = { - 1, 2, 3, 4, 5, 7, 9, 13, 17, 25, - 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, - 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}; -static const uint8_t s_dist_extra[30] = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, - 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, - 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; - -/* Code length order for dynamic Huffman */ -static const uint8_t s_code_order[19] = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, - 11, 4, 12, 3, 13, 2, 14, 1, 15}; - -/* Huffman decode table structure */ -typedef struct { - uint16_t counts[16]; /* Number of codes of each length */ - uint16_t symbols[288]; /* Symbols sorted by code */ -} HuffTable; - -/* Build Huffman table from code lengths */ -static int build_huffman(HuffTable *h, const uint8_t *lengths, int num_syms) { - int offs[16]; - memset(h->counts, 0, sizeof(h->counts)); - - /* Count codes of each length */ - for (int i = 0; i < num_syms; i++) { - if (lengths[i] > 0 && lengths[i] <= 15) { - h->counts[lengths[i]]++; - } - } - - /* Compute offsets */ - offs[0] = 0; - offs[1] = 0; - for (int i = 1; i < 15; i++) { - offs[i + 1] = offs[i] + h->counts[i]; - } - - /* Sort symbols by code length, then by symbol value */ - for (int i = 0; i < num_syms; i++) { - if (lengths[i] > 0 && lengths[i] <= 15) { - h->symbols[offs[lengths[i]]++] = i; - } - } - - return 0; -} - -/* Bit-by-bit Huffman decode */ -static int decode_huffman_simple(HuffTable *h, const uint8_t **pSrc, - const uint8_t *pSrc_end, uint32_t *bit_buf, - int *num_bits) { - int code = 0; - int first = 0; - int index = 0; - - for (int len = 1; len <= 15; len++) { - while (*num_bits < 1) { - if (*pSrc >= pSrc_end) - return -1; - *bit_buf |= (uint32_t)(*(*pSrc)++) << *num_bits; - *num_bits += 8; - } - - code = (code << 1) | (*bit_buf & 1); - *bit_buf >>= 1; - (*num_bits)--; - - int count = h->counts[len]; - if (code - first < count) { - return h->symbols[index + code - first]; - } - first = (first + count) << 1; - index += count; - } - - return -1; -} - -/* Build fixed Huffman tables */ -static void build_fixed_tables(HuffTable *lit_len, HuffTable *dist) { - uint8_t lengths[288]; - - /* Literal/length table: RFC 1951 section 3.2.6 */ - for (int i = 0; i < 144; i++) - lengths[i] = 8; - for (int i = 144; i < 256; i++) - lengths[i] = 9; - for (int i = 256; i < 280; i++) - lengths[i] = 7; - for (int i = 280; i < 288; i++) - lengths[i] = 8; - build_huffman(lit_len, lengths, 288); - - /* Distance table: all 5-bit codes */ - for (int i = 0; i < 30; i++) - lengths[i] = 5; - build_huffman(dist, lengths, 30); -} - -/* Raw deflate decompression */ -static int inflate_raw_impl(uint8_t *dest, size_t *dest_len, - const uint8_t *source, size_t source_len) { - const uint8_t *pSrc = source; - const uint8_t *pSrc_end = source + source_len; - uint8_t *pDst = dest; - uint8_t *pDst_end = dest + *dest_len; - uint32_t bit_buf = 0; - int num_bits = 0; - int final_block = 0; - - HuffTable lit_len_table, dist_table; - HuffTable code_len_table; - - while (!final_block) { - /* Read block header (3 bits) */ - while (num_bits < 3) { - if (pSrc >= pSrc_end) - return MZ_DATA_ERROR; - bit_buf |= (uint32_t)(*pSrc++) << num_bits; - num_bits += 8; - } - - final_block = bit_buf & 1; - int block_type = (bit_buf >> 1) & 3; - bit_buf >>= 3; - num_bits -= 3; - - if (block_type == 0) { - /* Stored block */ - /* Align to byte boundary */ - num_bits = 0; - bit_buf = 0; - - if (pSrc + 4 > pSrc_end) - return MZ_DATA_ERROR; - - uint16_t len = pSrc[0] | (pSrc[1] << 8); - uint16_t nlen = pSrc[2] | (pSrc[3] << 8); - pSrc += 4; - - if (len != (uint16_t)~nlen) - return MZ_DATA_ERROR; - if (pSrc + len > pSrc_end) - return MZ_DATA_ERROR; - if (pDst + len > pDst_end) - return MZ_BUF_ERROR; - - memcpy(pDst, pSrc, len); - pSrc += len; - pDst += len; - - } else if (block_type == 1) { - /* Fixed Huffman codes */ - build_fixed_tables(&lit_len_table, &dist_table); - - for (;;) { - int sym = decode_huffman_simple(&lit_len_table, &pSrc, pSrc_end, - &bit_buf, &num_bits); - if (sym < 0) - return MZ_DATA_ERROR; - - if (sym < 256) { - /* Literal byte */ - if (pDst >= pDst_end) - return MZ_BUF_ERROR; - *pDst++ = (uint8_t)sym; - } else if (sym == 256) { - /* End of block */ - break; - } else { - /* Length/distance pair */ - int len_idx = sym - 257; - if (len_idx >= 29) - return MZ_DATA_ERROR; - - int match_len = s_length_base[len_idx]; - int extra = s_length_extra[len_idx]; - - /* Read extra length bits */ - while (num_bits < extra) { - if (pSrc >= pSrc_end) - return MZ_DATA_ERROR; - bit_buf |= (uint32_t)(*pSrc++) << num_bits; - num_bits += 8; - } - match_len += bit_buf & ((1 << extra) - 1); - bit_buf >>= extra; - num_bits -= extra; - - /* Decode distance */ - int dist_sym = decode_huffman_simple(&dist_table, &pSrc, pSrc_end, - &bit_buf, &num_bits); - if (dist_sym < 0 || dist_sym >= 30) - return MZ_DATA_ERROR; - - int match_dist = s_dist_base[dist_sym]; - extra = s_dist_extra[dist_sym]; - - /* Read extra distance bits */ - while (num_bits < extra) { - if (pSrc >= pSrc_end) - return MZ_DATA_ERROR; - bit_buf |= (uint32_t)(*pSrc++) << num_bits; - num_bits += 8; - } - match_dist += bit_buf & ((1 << extra) - 1); - bit_buf >>= extra; - num_bits -= extra; - - /* Copy match */ - if ((size_t)match_dist > (size_t)(pDst - dest)) - return MZ_DATA_ERROR; - if (pDst + match_len > pDst_end) - return MZ_BUF_ERROR; - - const uint8_t *pMatch = pDst - match_dist; - while (match_len-- > 0) { - *pDst++ = *pMatch++; - } - } - } - - } else if (block_type == 2) { - /* Dynamic Huffman codes */ - - /* Read header: HLIT, HDIST, HCLEN */ - while (num_bits < 14) { - if (pSrc >= pSrc_end) - return MZ_DATA_ERROR; - bit_buf |= (uint32_t)(*pSrc++) << num_bits; - num_bits += 8; - } - - int hlit = (bit_buf & 0x1F) + 257; - bit_buf >>= 5; - num_bits -= 5; - - int hdist = (bit_buf & 0x1F) + 1; - bit_buf >>= 5; - num_bits -= 5; - - int hclen = (bit_buf & 0xF) + 4; - bit_buf >>= 4; - num_bits -= 4; - - if (hlit > 286 || hdist > 30) - return MZ_DATA_ERROR; - - /* Read code length code lengths */ - uint8_t code_lengths[19]; - memset(code_lengths, 0, sizeof(code_lengths)); - - for (int i = 0; i < hclen; i++) { - while (num_bits < 3) { - if (pSrc >= pSrc_end) - return MZ_DATA_ERROR; - bit_buf |= (uint32_t)(*pSrc++) << num_bits; - num_bits += 8; - } - code_lengths[s_code_order[i]] = bit_buf & 7; - bit_buf >>= 3; - num_bits -= 3; - } - - build_huffman(&code_len_table, code_lengths, 19); - - /* Read literal/length and distance code lengths */ - uint8_t lengths[286 + 30]; - int total = hlit + hdist; - int i = 0; - - while (i < total) { - int sym = decode_huffman_simple(&code_len_table, &pSrc, pSrc_end, - &bit_buf, &num_bits); - if (sym < 0) - return MZ_DATA_ERROR; - - if (sym < 16) { - lengths[i++] = sym; - } else if (sym == 16) { - /* Copy previous 3-6 times */ - while (num_bits < 2) { - if (pSrc >= pSrc_end) - return MZ_DATA_ERROR; - bit_buf |= (uint32_t)(*pSrc++) << num_bits; - num_bits += 8; - } - int repeat = 3 + (bit_buf & 3); - bit_buf >>= 2; - num_bits -= 2; - - if (i == 0) - return MZ_DATA_ERROR; - uint8_t prev = lengths[i - 1]; - while (repeat-- > 0 && i < total) { - lengths[i++] = prev; - } - } else if (sym == 17) { - /* Repeat 0 for 3-10 times */ - while (num_bits < 3) { - if (pSrc >= pSrc_end) - return MZ_DATA_ERROR; - bit_buf |= (uint32_t)(*pSrc++) << num_bits; - num_bits += 8; - } - int repeat = 3 + (bit_buf & 7); - bit_buf >>= 3; - num_bits -= 3; - - while (repeat-- > 0 && i < total) { - lengths[i++] = 0; - } - } else if (sym == 18) { - /* Repeat 0 for 11-138 times */ - while (num_bits < 7) { - if (pSrc >= pSrc_end) - return MZ_DATA_ERROR; - bit_buf |= (uint32_t)(*pSrc++) << num_bits; - num_bits += 8; - } - int repeat = 11 + (bit_buf & 0x7F); - bit_buf >>= 7; - num_bits -= 7; - - while (repeat-- > 0 && i < total) { - lengths[i++] = 0; - } - } else { - return MZ_DATA_ERROR; - } - } - - build_huffman(&lit_len_table, lengths, hlit); - build_huffman(&dist_table, lengths + hlit, hdist); - - /* Decode compressed data */ - for (;;) { - int sym = decode_huffman_simple(&lit_len_table, &pSrc, pSrc_end, - &bit_buf, &num_bits); - if (sym < 0) - return MZ_DATA_ERROR; - - if (sym < 256) { - if (pDst >= pDst_end) - return MZ_BUF_ERROR; - *pDst++ = (uint8_t)sym; - } else if (sym == 256) { - break; - } else { - int len_idx = sym - 257; - if (len_idx >= 29) - return MZ_DATA_ERROR; - - int match_len = s_length_base[len_idx]; - int extra = s_length_extra[len_idx]; - - while (num_bits < extra) { - if (pSrc >= pSrc_end) - return MZ_DATA_ERROR; - bit_buf |= (uint32_t)(*pSrc++) << num_bits; - num_bits += 8; - } - match_len += bit_buf & ((1 << extra) - 1); - bit_buf >>= extra; - num_bits -= extra; - - int dist_sym = decode_huffman_simple(&dist_table, &pSrc, pSrc_end, - &bit_buf, &num_bits); - if (dist_sym < 0 || dist_sym >= 30) - return MZ_DATA_ERROR; - - int match_dist = s_dist_base[dist_sym]; - extra = s_dist_extra[dist_sym]; - - while (num_bits < extra) { - if (pSrc >= pSrc_end) - return MZ_DATA_ERROR; - bit_buf |= (uint32_t)(*pSrc++) << num_bits; - num_bits += 8; - } - match_dist += bit_buf & ((1 << extra) - 1); - bit_buf >>= extra; - num_bits -= extra; - - if ((size_t)match_dist > (size_t)(pDst - dest)) - return MZ_DATA_ERROR; - if (pDst + match_len > pDst_end) - return MZ_BUF_ERROR; - - const uint8_t *pMatch = pDst - match_dist; - while (match_len-- > 0) { - *pDst++ = *pMatch++; - } - } - } - - } else { - /* Invalid block type */ - return MZ_DATA_ERROR; - } - } - - *dest_len = pDst - dest; - return MZ_OK; -} - -/* Public API: Raw deflate inflate */ -int mz_inflate_raw(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len) { - return inflate_raw_impl(dest, dest_len, source, source_len); -} - -uint8_t *mz_inflate_raw_alloc(const uint8_t *source, size_t source_len, - size_t *dest_len) { - /* Estimate output size - start with 4x input */ - size_t alloc_size = source_len * 4; - if (alloc_size < 1024) - alloc_size = 1024; - - for (int attempt = 0; attempt < 10; attempt++) { - uint8_t *dest = (uint8_t *)malloc(alloc_size); - if (!dest) - return NULL; - - size_t out_len = alloc_size; - int status = inflate_raw_impl(dest, &out_len, source, source_len); - - if (status == MZ_OK) { - *dest_len = out_len; - return dest; - } else if (status == MZ_BUF_ERROR) { - free(dest); - alloc_size *= 2; - if (alloc_size > 16 * 1024 * 1024) /* Cap at 16MB */ - return NULL; - continue; - } else { - free(dest); - return NULL; - } - } - - return NULL; -} - -/* Zlib-wrapped uncompress (with header/trailer) */ -int mz_uncompress(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len) { - if (source_len < 6) - return MZ_DATA_ERROR; - - /* Parse zlib header */ - uint8_t cmf = source[0]; - uint8_t flg = source[1]; - - if ((cmf & 0x0F) != 8) - return MZ_DATA_ERROR; /* Must be deflate */ - if (((cmf << 8) | flg) % 31 != 0) - return MZ_DATA_ERROR; /* Header checksum */ - if (flg & 0x20) - return MZ_DATA_ERROR; /* Preset dictionary not supported */ - - /* Decompress */ - int ret = inflate_raw_impl(dest, dest_len, source + 2, source_len - 6); - if (ret != MZ_OK) - return ret; - - /* Verify Adler32 */ - uint32_t computed = adler32(1, dest, *dest_len); - const uint8_t *p = source + source_len - 4; - uint32_t stored = ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) | - ((uint32_t)p[2] << 8) | (uint32_t)p[3]; - - if (computed != stored) - return MZ_DATA_ERROR; - - return MZ_OK; -} - -uint8_t *mz_uncompress_alloc(const uint8_t *source, size_t source_len, - size_t *dest_len) { - if (source_len < 6) - return NULL; - - size_t alloc_size = (source_len - 6) * 4; - if (alloc_size < 1024) - alloc_size = 1024; - - for (int attempt = 0; attempt < 10; attempt++) { - uint8_t *dest = (uint8_t *)malloc(alloc_size); - if (!dest) - return NULL; - - size_t out_len = alloc_size; - int status = mz_uncompress(dest, &out_len, source, source_len); - - if (status == MZ_OK) { - *dest_len = out_len; - return dest; - } else if (status == MZ_BUF_ERROR) { - free(dest); - alloc_size *= 2; - if (alloc_size > 16 * 1024 * 1024) - return NULL; - continue; - } else { - free(dest); - return NULL; - } - } - - return NULL; -} - -/* Raw deflate compression with LZ77 and static Huffman */ -int mz_deflate_raw(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len) { - return mz_deflate_raw_wbits(dest, dest_len, source, source_len, - MZ_DEFAULT_WBITS); -} - -int mz_deflate_raw_wbits(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len, int wbits) { - if (wbits < 8 || wbits > 15) { - wbits = MZ_DEFAULT_WBITS; - } - - size_t window_size = 1U << wbits; - uint8_t *hist_buf = (uint8_t *)malloc(window_size); - if (!hist_buf) { - return MZ_MEM_ERROR; - } - - mz_compress_state_t state; - memset(&state, 0, sizeof(state)); - state.out_buf = dest; - state.out_pos = 0; - state.out_size = *dest_len; - state.hist_buf = hist_buf; - state.hist_max = window_size; - state.hist_start = 0; - state.hist_len = 0; - - int ret = start_block(&state); - if (ret == MZ_OK) { - ret = lz77_compress(&state, source, source_len); - } - if (ret == MZ_OK) { - ret = finish_block(&state); - } - - free(hist_buf); - - if (ret == MZ_OK) { - *dest_len = state.out_pos; - } - return ret; -} - -uint8_t *mz_deflate_raw_alloc(const uint8_t *source, size_t source_len, - size_t *dest_len) { - return mz_deflate_raw_alloc_wbits(source, source_len, dest_len, - MZ_DEFAULT_WBITS); -} - -uint8_t *mz_deflate_raw_alloc_wbits(const uint8_t *source, size_t source_len, - size_t *dest_len, int wbits) { - /* Worst case: slightly larger than input + overhead */ - size_t alloc_size = source_len + (source_len / 8) + 64; - if (alloc_size < 256) - alloc_size = 256; - - uint8_t *dest = (uint8_t *)malloc(alloc_size); - if (!dest) - return NULL; - - *dest_len = alloc_size; - int ret = mz_deflate_raw_wbits(dest, dest_len, source, source_len, wbits); - if (ret != MZ_OK) { - free(dest); - return NULL; - } - - return dest; -} - -/* Zlib-wrapped compression */ -size_t mz_compressBound(size_t source_len) { - /* Compressed data is typically smaller, but worst case can be larger */ - return source_len + (source_len / 8) + 64 + 6; -} - -int mz_compress2(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len, int level) { - return mz_compress_wbits(dest, dest_len, source, source_len, level, - MZ_DEFAULT_WBITS); -} - -int mz_compress_wbits(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len, int level, int wbits) { - (void)level; - - if (*dest_len < 6) { - return MZ_BUF_ERROR; - } - - if (wbits < 8 || wbits > 15) { - wbits = MZ_DEFAULT_WBITS; - } - - /* Zlib header (2 bytes) */ - /* CMF: bits 0-3 = CM (8=deflate), bits 4-7 = CINFO (log2(window)-8) */ - uint8_t cmf = 0x08 | ((wbits - 8) << 4); - /* FLG: bits 0-4 = FCHECK, bit 5 = FDICT (0), bits 6-7 = FLEVEL */ - uint8_t flg = 0x00; /* level 0, will fix checksum below */ - /* FLG checksum: (CMF*256 + FLG) % 31 == 0 */ - flg |= 31 - ((cmf * 256 + flg) % 31); - - dest[0] = cmf; - dest[1] = flg; - - /* Compress */ - size_t raw_len = *dest_len - 6; - int ret = mz_deflate_raw_wbits(dest + 2, &raw_len, source, source_len, wbits); - if (ret != MZ_OK) { - return ret; - } - - /* Adler32 trailer (big-endian) */ - uint32_t adler = adler32(1, source, source_len); - uint8_t *p = dest + 2 + raw_len; - p[0] = (adler >> 24) & 0xFF; - p[1] = (adler >> 16) & 0xFF; - p[2] = (adler >> 8) & 0xFF; - p[3] = adler & 0xFF; - - *dest_len = 2 + raw_len + 4; - return MZ_OK; -} - -int mz_compress(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len) { - return mz_compress2(dest, dest_len, source, source_len, - MZ_DEFAULT_COMPRESSION); -} - -uint8_t *mz_compress_alloc(const uint8_t *source, size_t source_len, - size_t *dest_len, int level) { - return mz_compress_alloc_wbits(source, source_len, dest_len, level, - MZ_DEFAULT_WBITS); -} - -uint8_t *mz_compress_alloc_wbits(const uint8_t *source, size_t source_len, - size_t *dest_len, int level, int wbits) { - size_t alloc_size = mz_compressBound(source_len); - uint8_t *dest = (uint8_t *)malloc(alloc_size); - if (!dest) - return NULL; - - *dest_len = alloc_size; - int ret = mz_compress_wbits(dest, dest_len, source, source_len, level, wbits); - if (ret != MZ_OK) { - free(dest); - return NULL; - } - - return dest; -} diff --git a/components/bbqr/src/miniz.h b/components/bbqr/src/miniz.h deleted file mode 100644 index 03c5b7b4..00000000 --- a/components/bbqr/src/miniz.h +++ /dev/null @@ -1,174 +0,0 @@ -/* - * miniz.h - Minimal zlib-compatible compression/decompression - * This is a minimal subset for BBQr compression support. - * - * Features: - * - LZ77 compression with static Huffman codes - * - Configurable window size (wbits parameter, default 10 = 1024 bytes) - * - Full decompression support (stored, fixed, dynamic Huffman) - * - Zlib header/trailer with Adler32 checksum - * - * Based on: - * - miniz by Rich Geldreich (public domain) - * - uzlib by Paul Sokolovsky (MIT license) - * - MicroPython deflate module - */ - -#ifndef MINIZ_H -#define MINIZ_H - -#include -#include - -/* Compression/decompression return codes */ -#define MZ_OK 0 -#define MZ_STREAM_END 1 -#define MZ_NEED_DICT 2 -#define MZ_ERRNO (-1) -#define MZ_STREAM_ERROR (-2) -#define MZ_DATA_ERROR (-3) -#define MZ_MEM_ERROR (-4) -#define MZ_BUF_ERROR (-5) -#define MZ_VERSION_ERROR (-6) -#define MZ_PARAM_ERROR (-10000) - -/* Compression levels (currently ignored, using static Huffman) */ -#define MZ_NO_COMPRESSION 0 -#define MZ_BEST_SPEED 1 -#define MZ_BEST_COMPRESSION 9 -#define MZ_DEFAULT_COMPRESSION 6 - -/* Window bits (wbits) - determines LZ77 window size: 2^wbits bytes */ -#define MZ_MIN_WBITS 8 /* 256 bytes */ -#define MZ_MAX_WBITS 15 /* 32KB */ -#define MZ_DEFAULT_WBITS 10 /* 1024 bytes - optimal for BBQr */ - -/* Flush types */ -#define MZ_NO_FLUSH 0 -#define MZ_PARTIAL_FLUSH 1 -#define MZ_SYNC_FLUSH 2 -#define MZ_FULL_FLUSH 3 -#define MZ_FINISH 4 - -/** - * @brief Compress data using zlib deflate - * - * @param dest Destination buffer for compressed data - * @param dest_len Pointer to dest buffer size, updated with actual size - * @param source Source data to compress - * @param source_len Length of source data - * @param level Compression level (0-9, or MZ_DEFAULT_COMPRESSION) - * @return MZ_OK on success, error code on failure - */ -int mz_compress2(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len, int level); - -/** - * @brief Compress data using default compression level - */ -int mz_compress(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len); - -/** - * @brief Get upper bound on compressed size - */ -size_t mz_compressBound(size_t source_len); - -/** - * @brief Decompress zlib-compressed data - * - * @param dest Destination buffer for decompressed data - * @param dest_len Pointer to dest buffer size, updated with actual size - * @param source Compressed source data - * @param source_len Length of compressed data - * @return MZ_OK on success, error code on failure - */ -int mz_uncompress(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len); - -/** - * @brief Decompress with dynamic allocation - * - * @param source Compressed source data - * @param source_len Length of compressed data - * @param dest_len Pointer to store decompressed length - * @return Allocated buffer with decompressed data, or NULL on failure - * Caller must free the returned buffer. - */ -uint8_t *mz_uncompress_alloc(const uint8_t *source, size_t source_len, - size_t *dest_len); - -/** - * @brief Compress with dynamic allocation - * - * @param source Source data to compress - * @param source_len Length of source data - * @param dest_len Pointer to store compressed length - * @param level Compression level - * @return Allocated buffer with compressed data, or NULL on failure - * Caller must free the returned buffer. - */ -uint8_t *mz_compress_alloc(const uint8_t *source, size_t source_len, - size_t *dest_len, int level); - -/** - * @brief Decompress raw deflate data (no zlib header/trailer) - * - * @param dest Destination buffer for decompressed data - * @param dest_len Pointer to dest buffer size, updated with actual size - * @param source Compressed source data (raw deflate) - * @param source_len Length of compressed data - * @return MZ_OK on success, error code on failure - */ -int mz_inflate_raw(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len); - -/** - * @brief Decompress raw deflate with dynamic allocation - */ -uint8_t *mz_inflate_raw_alloc(const uint8_t *source, size_t source_len, - size_t *dest_len); - -/** - * @brief Compress to raw deflate (no zlib header/trailer) - * Uses default window bits (MZ_DEFAULT_WBITS = 10, 1024 bytes) - */ -int mz_deflate_raw(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len); - -/** - * @brief Compress to raw deflate with configurable window size - * - * @param wbits Window bits (8-15), determines window size as 2^wbits bytes - * Use 10 for BBQr (1024 byte window) - */ -int mz_deflate_raw_wbits(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len, int wbits); - -/** - * @brief Compress to raw deflate with dynamic allocation - */ -uint8_t *mz_deflate_raw_alloc(const uint8_t *source, size_t source_len, - size_t *dest_len); - -/** - * @brief Compress to raw deflate with dynamic allocation and wbits - */ -uint8_t *mz_deflate_raw_alloc_wbits(const uint8_t *source, size_t source_len, - size_t *dest_len, int wbits); - -/** - * @brief Compress with zlib wrapper and configurable window size - * - * @param wbits Window bits (8-15), determines window size as 2^wbits bytes - */ -int mz_compress_wbits(uint8_t *dest, size_t *dest_len, const uint8_t *source, - size_t source_len, int level, int wbits); - -/** - * @brief Compress with zlib wrapper, dynamic allocation, and wbits - */ -uint8_t *mz_compress_alloc_wbits(const uint8_t *source, size_t source_len, - size_t *dest_len, int level, int wbits); - -#endif /* MINIZ_H */ diff --git a/components/bbqr/test/Makefile b/components/bbqr/test/Makefile index 40ed439b..f40066e1 100644 --- a/components/bbqr/test/Makefile +++ b/components/bbqr/test/Makefile @@ -1,15 +1,13 @@ CC = gcc -CFLAGS = -Wall -Wextra -g -I../src -LDFLAGS = +CFLAGS = -Wall -Wextra -g -I../src -I../../deflate_codec/src +LDFLAGS = -lz -SRCS_BBQR = test_bbqr.c ../src/base32.c ../src/miniz.c ../src/bbqr.c +SRCS_BBQR = test_bbqr.c ../src/base32.c ../../deflate_codec/src/deflate_codec.c ../src/bbqr.c SRCS_BASE32 = test_base32.c ../src/base32.c -SRCS_MINIZ = test_miniz.c ../src/miniz.c TARGET_BBQR = test_bbqr TARGET_BASE32 = test_base32 -TARGET_MINIZ = test_miniz -all: $(TARGET_BBQR) $(TARGET_BASE32) $(TARGET_MINIZ) +all: $(TARGET_BBQR) $(TARGET_BASE32) $(TARGET_BBQR): $(SRCS_BBQR) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) @@ -17,11 +15,7 @@ $(TARGET_BBQR): $(SRCS_BBQR) $(TARGET_BASE32): $(SRCS_BASE32) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -$(TARGET_MINIZ): $(SRCS_MINIZ) - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) - -run: $(TARGET_BBQR) $(TARGET_BASE32) $(TARGET_MINIZ) - ./$(TARGET_MINIZ) +run: $(TARGET_BBQR) $(TARGET_BASE32) ./$(TARGET_BASE32) ./$(TARGET_BBQR) @@ -31,10 +25,7 @@ run-bbqr: $(TARGET_BBQR) run-base32: $(TARGET_BASE32) ./$(TARGET_BASE32) -run-miniz: $(TARGET_MINIZ) - ./$(TARGET_MINIZ) - clean: - rm -f $(TARGET_BBQR) $(TARGET_BASE32) $(TARGET_MINIZ) + rm -f $(TARGET_BBQR) $(TARGET_BASE32) -.PHONY: all run run-bbqr run-base32 run-miniz clean +.PHONY: all run run-bbqr run-base32 clean diff --git a/components/bbqr/test/test_base32.c b/components/bbqr/test/test_base32.c index 3fe5554e..21a127b2 100644 --- a/components/bbqr/test/test_base32.c +++ b/components/bbqr/test/test_base32.c @@ -171,23 +171,9 @@ static const base32_test_vector_t test_vectors[] = { #define NUM_TEST_VECTORS (sizeof(test_vectors) / sizeof(test_vectors[0])) -/* Helper to strip padding from encoded string */ -static void strip_padding(const char *input, char *output, size_t output_size) { - size_t len = strlen(input); - while (len > 0 && input[len - 1] == '=') { - len--; - } - if (len >= output_size) { - len = output_size - 1; - } - memcpy(output, input, len); - output[len] = '\0'; -} - /* Test encoding with all test vectors */ void test_base32_encode_vectors(void) { char output[1024]; - char stripped[1024]; printf("\n=== Base32 Encoding Tests ===\n"); @@ -205,29 +191,26 @@ void test_base32_encode_vectors(void) { continue; } - /* Strip padding from output to compare with expected (without padding) */ - strip_padding(output, stripped, sizeof(stripped)); - - if (strcmp(stripped, tv->encoded) == 0) { + if (strcmp(output, tv->encoded) == 0) { PASS(); } else { printf("\n Expected: %s\n", tv->encoded); - printf(" Got: %s\n", stripped); + printf(" Got: %s\n", output); FAIL("encoding mismatch"); } } } -/* Test encoding produces correct padded output */ -void test_base32_encode_padded(void) { +/* Test encoding never emits the padding forbidden by BBQr */ +void test_base32_encode_unpadded(void) { char output[1024]; - printf("\n=== Base32 Padded Encoding Tests ===\n"); + printf("\n=== Base32 Unpadded Encoding Tests ===\n"); for (size_t i = 0; i < NUM_TEST_VECTORS; i++) { const base32_test_vector_t *tv = &test_vectors[i]; char test_name[64]; - snprintf(test_name, sizeof(test_name), "base32_encode_padded vector %zu", + snprintf(test_name, sizeof(test_name), "base32_encode_unpadded vector %zu", i); TEST(test_name); @@ -239,12 +222,12 @@ void test_base32_encode_padded(void) { continue; } - if (strcmp(output, tv->encoded_padded) == 0) { + if (strcmp(output, tv->encoded) == 0 && strchr(output, '=') == NULL) { PASS(); } else { - printf("\n Expected: %s\n", tv->encoded_padded); + printf("\n Expected: %s\n", tv->encoded); printf(" Got: %s\n", output); - FAIL("padded encoding mismatch"); + FAIL("unpadded encoding mismatch"); } } } @@ -458,7 +441,7 @@ void test_base32_invalid_input(void) { /* Test encode with insufficient output buffer */ TEST("base32_encode small buffer"); - len = base32_encode(dummy, 4, output, 2); /* needs 8 chars + null */ + len = base32_encode(dummy, 4, output, 2); /* needs 7 chars + null */ if (len == 0) { PASS(); } else { @@ -542,10 +525,10 @@ void test_base32_length_functions(void) { } TEST("base32_encoded_len 1 byte"); - if (base32_encoded_len(1) == 8) { + if (base32_encoded_len(1) == 2) { PASS(); } else { - printf("got %zu, expected 8\n", base32_encoded_len(1)); + printf("got %zu, expected 2\n", base32_encoded_len(1)); FAIL("wrong length"); } @@ -558,10 +541,10 @@ void test_base32_length_functions(void) { } TEST("base32_encoded_len 6 bytes"); - if (base32_encoded_len(6) == 16) { + if (base32_encoded_len(6) == 10) { PASS(); } else { - printf("got %zu, expected 16\n", base32_encoded_len(6)); + printf("got %zu, expected 10\n", base32_encoded_len(6)); FAIL("wrong length"); } @@ -631,7 +614,7 @@ int main(void) { /* Run all tests */ test_base32_encode_vectors(); - test_base32_encode_padded(); + test_base32_encode_unpadded(); test_base32_decode_unpadded(); test_base32_decode_padded(); test_base32_roundtrip(); diff --git a/components/bbqr/test/test_bbqr.c b/components/bbqr/test/test_bbqr.c index 666c1aef..a0c431d9 100644 --- a/components/bbqr/test/test_bbqr.c +++ b/components/bbqr/test/test_bbqr.c @@ -1,8 +1,6 @@ /* * BBQr Test Suite - * Compile with: gcc -o test_bbqr test_bbqr.c ../src/base32.c ../src/miniz.c - * ../src/bbqr.c -I../src -lz Or without system zlib: gcc -o test_bbqr - * test_bbqr.c ../src/base32.c ../src/miniz.c ../src/bbqr.c -I../src + * Compile with: make run-bbqr (see Makefile) */ #include @@ -13,7 +11,6 @@ #include "base32.h" #include "bbqr.h" #include "bbqr_samples.h" -#include "miniz.h" static int tests_passed = 0; static int tests_failed = 0; @@ -184,49 +181,6 @@ void test_bbqr_parse_header(void) { PASS(); } -/* Test miniz compression/decompression round-trip */ -void test_miniz_roundtrip(void) { - TEST("miniz compress/decompress round-trip"); - - const char *original = "Hello, this is a test string for compression. " - "It should compress reasonably well because it has " - "some repetition. Hello hello hello!"; - size_t original_len = strlen(original); - - /* Compress */ - size_t compressed_len = 0; - uint8_t *compressed = - mz_compress_alloc((const uint8_t *)original, original_len, - &compressed_len, MZ_DEFAULT_COMPRESSION); - if (!compressed) { - FAIL("Compression failed"); - return; - } - - printf("(compressed %zu -> %zu bytes) ", original_len, compressed_len); - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("Decompression failed"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - free(decompressed); - FAIL("Data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - /* Test BBQr encode/decode round-trip */ void test_bbqr_roundtrip(void) { TEST("bbqr encode/decode round-trip"); @@ -273,6 +227,142 @@ void test_bbqr_roundtrip(void) { PASS(); } +/* Test BBQr output remains in QR alphanumeric mode without Base32 padding. */ +void test_bbqr_unpadded_base32(void) { + TEST("bbqr omits base32 padding"); + + const uint8_t original[] = {'f'}; + BBQrParts *parts = + bbqr_encode(original, sizeof(original), BBQR_TYPE_PSBT, 400); + + if (!parts) { + FAIL("Encode failed"); + return; + } + + if (parts->count != 1 || parts->encoding != BBQR_ENCODING_BASE32 || + strcmp(parts->parts[0], "B$2P0100MY") != 0 || + strchr(parts->parts[0], '=') != NULL) { + bbqr_parts_free(parts); + FAIL("Output is not unpadded BBQr Base32"); + return; + } + + bbqr_parts_free(parts); + PASS(); +} + +/* Test that the encoder uses the high-compression raw-deflate path. */ +void test_bbqr_compression_quality(void) { + TEST("bbqr high-compression deflate"); + + static const uint8_t original[] = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " + "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."; + const size_t original_len = sizeof(original) - 1; + + BBQrParts *parts = + bbqr_encode(original, original_len, BBQR_TYPE_UNICODE, 400); + if (!parts) { + FAIL("Encode failed"); + return; + } + + size_t payload_len = strlen(parts->parts[0]) - BBQR_HEADER_LEN; + if (parts->count != 1 || parts->encoding != BBQR_ENCODING_ZLIB || + payload_len >= 285) { + bbqr_parts_free(parts); + FAIL("Compression did not improve on fixed-Huffman output"); + return; + } + + BBQrPart parsed; + size_t decoded_len = 0; + uint8_t *decoded = NULL; + if (!bbqr_parse_part(parts->parts[0], strlen(parts->parts[0]), &parsed) || + !(decoded = bbqr_decode_payload(parsed.encoding, parsed.payload, + parsed.payload_len, &decoded_len)) || + decoded_len != original_len || + memcmp(decoded, original, original_len) != 0) { + free(decoded); + bbqr_parts_free(parts); + FAIL("Compressed output did not round-trip"); + return; + } + + free(decoded); + bbqr_parts_free(parts); + PASS(); +} + +/* Test direct Base32 emission across multiple contiguous part buffers. */ +void test_bbqr_multipart_streaming(void) { + TEST("bbqr streams multipart output"); + + uint8_t original[512]; + uint32_t state = 0x6d2b79f5; + for (size_t i = 0; i < sizeof(original); i++) { + state = state * 1664525u + 1013904223u; + original[i] = (uint8_t)(state >> 24); + } + + const int max_chars = 56; + const size_t max_payload = max_chars - BBQR_HEADER_LEN; + BBQrParts *parts = + bbqr_encode(original, sizeof(original), BBQR_TYPE_PSBT, max_chars); + if (!parts) { + FAIL("Encode failed"); + return; + } + + size_t payload_len = 0; + bool valid = parts->count > 1 && parts->storage != NULL; + for (int i = 0; valid && i < parts->count; i++) { + size_t part_len = strlen(parts->parts[i]); + size_t this_payload_len = part_len - BBQR_HEADER_LEN; + valid = part_len <= (size_t)max_chars && + strchr(parts->parts[i], '=') == NULL && + (i == parts->count - 1 || this_payload_len == max_payload); + if (i > 0) { + valid = valid && parts->parts[i] == parts->parts[i - 1] + + strlen(parts->parts[i - 1]) + 1; + } + payload_len += this_payload_len; + } + + char *payload = valid ? (char *)malloc(payload_len) : NULL; + if (!payload) { + bbqr_parts_free(parts); + FAIL("Invalid part layout or allocation failed"); + return; + } + + size_t offset = 0; + for (int i = 0; i < parts->count; i++) { + size_t this_payload_len = strlen(parts->parts[i]) - BBQR_HEADER_LEN; + memcpy(payload + offset, parts->parts[i] + BBQR_HEADER_LEN, + this_payload_len); + offset += this_payload_len; + } + + size_t decoded_len = 0; + uint8_t *decoded = + bbqr_decode_payload(parts->encoding, payload, payload_len, &decoded_len); + free(payload); + if (!decoded || decoded_len != sizeof(original) || + memcmp(decoded, original, sizeof(original)) != 0) { + free(decoded); + bbqr_parts_free(parts); + FAIL("Multipart output did not round-trip"); + return; + } + + free(decoded); + bbqr_parts_free(parts); + PASS(); +} + /* Expected PSBT data (base64 decoded) for test_real_bbqr_decode */ static const uint8_t EXPECTED_PSBT[] = { 0x70, 0x73, 0x62, 0x74, 0xff, 0x01, 0x00, 0xf6, 0x02, 0x00, 0x00, 0x00, @@ -558,8 +648,10 @@ int main(void) { test_base32_roundtrip(); test_base36(); test_bbqr_parse_header(); - test_miniz_roundtrip(); test_bbqr_roundtrip(); + test_bbqr_unpadded_base32(); + test_bbqr_compression_quality(); + test_bbqr_multipart_streaming(); test_real_bbqr_decode(); test_vectors(); diff --git a/components/bbqr/test/test_miniz.c b/components/bbqr/test/test_miniz.c deleted file mode 100644 index 90f46e73..00000000 --- a/components/bbqr/test/test_miniz.c +++ /dev/null @@ -1,811 +0,0 @@ -/* - * Miniz Test Suite - * Tests for LZ77 compression with static Huffman encoding - * - * Compile: gcc -o test_miniz test_miniz.c ../src/miniz.c -I../src -Wall -Wextra - * Run: ./test_miniz - */ - -#include -#include -#include -#include - -#include "miniz.h" - -static int tests_passed = 0; -static int tests_failed = 0; - -#define TEST(name) printf("Testing: %s... ", name) -#define PASS() \ - do { \ - printf("PASS\n"); \ - tests_passed++; \ - } while (0) -#define FAIL(msg) \ - do { \ - printf("FAIL: %s\n", msg); \ - tests_failed++; \ - } while (0) - -/* Helper: print hex dump of first n bytes */ -static void hex_dump(const char *label, const uint8_t *data, size_t len, - size_t max_show) { - printf(" %s (%zu bytes): ", label, len); - size_t show = (len < max_show) ? len : max_show; - for (size_t i = 0; i < show; i++) { - printf("%02x ", data[i]); - } - if (len > max_show) - printf("..."); - printf("\n"); -} - -/* - * Test: Basic round-trip with simple string - */ -void test_basic_roundtrip(void) { - TEST("basic round-trip"); - - const char *original = "Hello, World!"; - size_t original_len = strlen(original); - - /* Compress */ - size_t compressed_len = 0; - uint8_t *compressed = - mz_compress_alloc((const uint8_t *)original, original_len, - &compressed_len, MZ_DEFAULT_COMPRESSION); - if (!compressed) { - FAIL("compression returned NULL"); - return; - } - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - -/* - * Test: Round-trip with repetitive data (should compress well) - */ -void test_repetitive_data(void) { - TEST("repetitive data compression"); - - const char *original = - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; - size_t original_len = strlen(original); - - /* Compress */ - size_t compressed_len = 0; - uint8_t *compressed = - mz_compress_alloc((const uint8_t *)original, original_len, - &compressed_len, MZ_DEFAULT_COMPRESSION); - if (!compressed) { - FAIL("compression returned NULL"); - return; - } - - printf("(%zu -> %zu bytes, %.1f%% ratio) ", original_len, compressed_len, - 100.0 * compressed_len / original_len); - - /* Verify compression achieved */ - if (compressed_len >= original_len) { - printf("\nWARNING: No compression achieved\n"); - } - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - -/* - * Test: Round-trip with mixed content - */ -void test_mixed_content(void) { - TEST("mixed content round-trip"); - - const char *original = "Hello, this is a test string for compression. " - "It should compress reasonably well because it has " - "some repetition. Hello hello hello! " - "The quick brown fox jumps over the lazy dog. " - "Pack my box with five dozen liquor jugs."; - size_t original_len = strlen(original); - - /* Compress */ - size_t compressed_len = 0; - uint8_t *compressed = - mz_compress_alloc((const uint8_t *)original, original_len, - &compressed_len, MZ_DEFAULT_COMPRESSION); - if (!compressed) { - FAIL("compression returned NULL"); - return; - } - - printf("(%zu -> %zu bytes) ", original_len, compressed_len); - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - -/* - * Test: Raw deflate (no zlib header/trailer) - */ -void test_raw_deflate(void) { - TEST("raw deflate round-trip"); - - const char *original = "Test data for raw deflate compression"; - size_t original_len = strlen(original); - - /* Compress */ - size_t compressed_len = 0; - uint8_t *compressed = mz_deflate_raw_alloc((const uint8_t *)original, - original_len, &compressed_len); - if (!compressed) { - FAIL("compression returned NULL"); - return; - } - - printf("(%zu -> %zu bytes) ", original_len, compressed_len); - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_inflate_raw_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - -/* - * Test: Different window sizes (wbits) - */ -void test_wbits_8(void) { - TEST("wbits=8 (256 byte window)"); - - const char *original = "Testing compression with wbits=8. " - "This uses a 256 byte sliding window. " - "Testing compression with wbits=8 again."; - size_t original_len = strlen(original); - - /* Compress with wbits=8 */ - size_t compressed_len = 0; - uint8_t *compressed = - mz_compress_alloc_wbits((const uint8_t *)original, original_len, - &compressed_len, MZ_DEFAULT_COMPRESSION, 8); - if (!compressed) { - FAIL("compression returned NULL"); - return; - } - - printf("(%zu -> %zu bytes) ", original_len, compressed_len); - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - -void test_wbits_10(void) { - TEST("wbits=10 (1024 byte window - BBQr default)"); - - const char *original = "Testing compression with wbits=10. " - "This is the default for BBQr. " - "Testing compression with wbits=10 again. " - "The 1024 byte window is a good balance."; - size_t original_len = strlen(original); - - /* Compress with wbits=10 */ - size_t compressed_len = 0; - uint8_t *compressed = - mz_compress_alloc_wbits((const uint8_t *)original, original_len, - &compressed_len, MZ_DEFAULT_COMPRESSION, 10); - if (!compressed) { - FAIL("compression returned NULL"); - return; - } - - printf("(%zu -> %zu bytes) ", original_len, compressed_len); - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - -void test_wbits_15(void) { - TEST("wbits=15 (32KB window)"); - - const char *original = - "Testing compression with wbits=15. " - "This uses the maximum 32KB window. " - "Larger windows can find more matches but use more RAM."; - size_t original_len = strlen(original); - - /* Compress with wbits=15 */ - size_t compressed_len = 0; - uint8_t *compressed = - mz_compress_alloc_wbits((const uint8_t *)original, original_len, - &compressed_len, MZ_DEFAULT_COMPRESSION, 15); - if (!compressed) { - FAIL("compression returned NULL"); - return; - } - - printf("(%zu -> %zu bytes) ", original_len, compressed_len); - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - -/* - * Test: Binary data (all byte values) - */ -void test_binary_data(void) { - TEST("binary data round-trip"); - - /* Create data with all byte values */ - uint8_t original[512]; - for (int i = 0; i < 256; i++) { - original[i] = (uint8_t)i; - original[256 + i] = (uint8_t)(255 - i); - } - size_t original_len = sizeof(original); - - /* Compress */ - size_t compressed_len = 0; - uint8_t *compressed = mz_compress_alloc( - original, original_len, &compressed_len, MZ_DEFAULT_COMPRESSION); - if (!compressed) { - FAIL("compression returned NULL"); - return; - } - - printf("(%zu -> %zu bytes) ", original_len, compressed_len); - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - -/* - * Test: Empty input - */ -void test_empty_input(void) { - TEST("empty input"); - - uint8_t dest[64]; - size_t dest_len = sizeof(dest); - - int ret = mz_compress(dest, &dest_len, (const uint8_t *)"", 0); - if (ret != MZ_OK) { - printf("(compress returned %d) ", ret); - } - - /* Try to decompress */ - if (ret == MZ_OK && dest_len > 0) { - size_t decomp_len = 0; - uint8_t *decomp = mz_uncompress_alloc(dest, dest_len, &decomp_len); - if (decomp) { - if (decomp_len == 0) { - free(decomp); - PASS(); - return; - } - free(decomp); - } - } - - /* Empty input might not produce valid output - that's OK */ - PASS(); -} - -/* - * Test: Single byte - */ -void test_single_byte(void) { - TEST("single byte"); - - const uint8_t original[] = {'X'}; - size_t original_len = 1; - - /* Compress */ - size_t compressed_len = 0; - uint8_t *compressed = mz_compress_alloc( - original, original_len, &compressed_len, MZ_DEFAULT_COMPRESSION); - if (!compressed) { - FAIL("compression returned NULL"); - return; - } - - printf("(1 -> %zu bytes) ", compressed_len); - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != 1 || decompressed[0] != 'X') { - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - -/* - * Test: Large data (stress test) - */ -void test_large_data(void) { - TEST("large data (4KB)"); - - /* Generate pseudo-random but reproducible data */ - size_t original_len = 4096; - uint8_t *original = (uint8_t *)malloc(original_len); - if (!original) { - FAIL("malloc failed"); - return; - } - - /* Fill with pattern that has some repetition */ - for (size_t i = 0; i < original_len; i++) { - original[i] = (uint8_t)((i * 17 + i / 128) & 0xFF); - } - - /* Compress */ - size_t compressed_len = 0; - uint8_t *compressed = mz_compress_alloc( - original, original_len, &compressed_len, MZ_DEFAULT_COMPRESSION); - if (!compressed) { - free(original); - FAIL("compression returned NULL"); - return; - } - - printf("(%zu -> %zu bytes, %.1f%%) ", original_len, compressed_len, - 100.0 * compressed_len / original_len); - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - free(original); - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - free(original); - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(original); - free(decompressed); - PASS(); -} - -/* - * Test: PSBT-like data (typical BBQr content) - */ -void test_psbt_like_data(void) { - TEST("PSBT-like binary data"); - - /* Typical PSBT header and structure */ - const uint8_t original[] = { - 0x70, 0x73, 0x62, 0x74, 0xff, /* "psbt" + separator */ - 0x01, 0x00, 0x52, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, - 0x01, 0x00, 0xf2, 0x05, 0x2a, 0x01, 0x00, 0x00, 0x00, 0x19, 0x76, - 0xa9, 0x14, 0x89, 0xab, 0xcd, 0xef, 0xab, 0xba, 0xab, 0xba, 0xab, - 0xba, 0xab, 0xba, 0xab, 0xba, 0xab, 0xba, 0xab, 0xba, 0xab, 0xba, - 0x88, 0xac, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - size_t original_len = sizeof(original); - - /* Compress with BBQr default wbits=10 */ - size_t compressed_len = 0; - uint8_t *compressed = mz_compress_alloc_wbits( - original, original_len, &compressed_len, MZ_DEFAULT_COMPRESSION, 10); - if (!compressed) { - FAIL("compression returned NULL"); - return; - } - - printf("(%zu -> %zu bytes) ", original_len, compressed_len); - - /* Decompress */ - size_t decompressed_len = 0; - uint8_t *decompressed = - mz_uncompress_alloc(compressed, compressed_len, &decompressed_len); - free(compressed); - - if (!decompressed) { - FAIL("decompression returned NULL"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - hex_dump("Original", original, original_len, 32); - hex_dump("Decompressed", decompressed, decompressed_len, 32); - free(decompressed); - FAIL("data mismatch"); - return; - } - - free(decompressed); - PASS(); -} - -/* - * Test: Fixed-buffer compression (no allocation) - */ -void test_fixed_buffer(void) { - TEST("fixed buffer compression"); - - const char *original = "Testing fixed buffer compression without malloc"; - size_t original_len = strlen(original); - - uint8_t compressed[256]; - size_t compressed_len = sizeof(compressed); - - int ret = mz_compress(compressed, &compressed_len, (const uint8_t *)original, - original_len); - if (ret != MZ_OK) { - printf("(mz_compress returned %d) ", ret); - FAIL("compression failed"); - return; - } - - printf("(%zu -> %zu bytes) ", original_len, compressed_len); - - /* Decompress into fixed buffer */ - uint8_t decompressed[256]; - size_t decompressed_len = sizeof(decompressed); - - ret = mz_uncompress(decompressed, &decompressed_len, compressed, - compressed_len); - if (ret != MZ_OK) { - printf("(mz_uncompress returned %d) ", ret); - FAIL("decompression failed"); - return; - } - - if (decompressed_len != original_len || - memcmp(original, decompressed, original_len) != 0) { - FAIL("data mismatch"); - return; - } - - PASS(); -} - -/* - * Test: Buffer too small error - */ -void test_buffer_too_small(void) { - TEST("buffer too small error"); - - const char *original = "This string should not fit in a tiny buffer"; - size_t original_len = strlen(original); - - uint8_t compressed[8]; /* Way too small */ - size_t compressed_len = sizeof(compressed); - - int ret = mz_compress(compressed, &compressed_len, (const uint8_t *)original, - original_len); - - if (ret == MZ_BUF_ERROR) { - PASS(); - } else { - printf("(expected MZ_BUF_ERROR, got %d) ", ret); - FAIL("wrong error code"); - } -} - -/* - * Test: Verify zlib header format - */ -void test_zlib_header(void) { - TEST("zlib header format"); - - const char *original = "Test"; - size_t original_len = strlen(original); - - uint8_t compressed[64]; - size_t compressed_len = sizeof(compressed); - - int ret = - mz_compress_wbits(compressed, &compressed_len, (const uint8_t *)original, - original_len, MZ_DEFAULT_COMPRESSION, 10); - if (ret != MZ_OK) { - FAIL("compression failed"); - return; - } - - /* Check zlib header */ - uint8_t cmf = compressed[0]; - uint8_t flg = compressed[1]; - - /* CMF: method (bits 0-3) should be 8 (deflate) */ - if ((cmf & 0x0F) != 8) { - printf("(CMF method = %d, expected 8) ", cmf & 0x0F); - FAIL("wrong compression method"); - return; - } - - /* CMF: info (bits 4-7) should be wbits-8 = 2 for wbits=10 */ - int cinfo = (cmf >> 4) & 0x0F; - if (cinfo != 2) { - printf("(CINFO = %d, expected 2 for wbits=10) ", cinfo); - FAIL("wrong CINFO"); - return; - } - - /* Header checksum: (CMF*256 + FLG) % 31 == 0 */ - if ((cmf * 256 + flg) % 31 != 0) { - printf("(header checksum failed) "); - FAIL("header checksum error"); - return; - } - - printf("(CMF=0x%02x, FLG=0x%02x) ", cmf, flg); - PASS(); -} - -/* - * Test: Compression ratio benchmark - */ -void test_compression_ratio(void) { - printf("\n=== Compression Ratio Benchmark ===\n"); - - struct { - const char *name; - const uint8_t *data; - size_t len; - } test_cases[] = { - {"Zeros (256)", NULL, 256}, - {"Ones (256)", NULL, 256}, - {"Sequential (256)", NULL, 256}, - {"Text (lorem)", NULL, 0}, - }; - - /* Allocate test data */ - uint8_t zeros[256], ones[256], seq[256]; - memset(zeros, 0, sizeof(zeros)); - memset(ones, 0xFF, sizeof(ones)); - for (int i = 0; i < 256; i++) - seq[i] = (uint8_t)i; - - const char *lorem = - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " - "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " - "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris."; - - test_cases[0].data = zeros; - test_cases[1].data = ones; - test_cases[2].data = seq; - test_cases[3].data = (const uint8_t *)lorem; - test_cases[3].len = strlen(lorem); - - for (size_t i = 0; i < sizeof(test_cases) / sizeof(test_cases[0]); i++) { - const uint8_t *data = test_cases[i].data; - size_t len = test_cases[i].len; - - size_t compressed_len = 0; - uint8_t *compressed = - mz_compress_alloc(data, len, &compressed_len, MZ_DEFAULT_COMPRESSION); - - if (compressed) { - printf(" %-20s: %4zu -> %4zu bytes (%5.1f%%)\n", test_cases[i].name, len, - compressed_len, 100.0 * compressed_len / len); - free(compressed); - } else { - printf(" %-20s: compression failed\n", test_cases[i].name); - } - } - printf("\n"); -} - -/* - * Main test runner - */ -int main(void) { - printf("========================================\n"); - printf(" Miniz Test Suite\n"); - printf(" LZ77 + Static Huffman Compression\n"); - printf("========================================\n\n"); - - /* Basic functionality tests */ - printf("=== Basic Tests ===\n"); - test_basic_roundtrip(); - test_repetitive_data(); - test_mixed_content(); - test_raw_deflate(); - - /* Window size tests */ - printf("\n=== Window Size Tests ===\n"); - test_wbits_8(); - test_wbits_10(); - test_wbits_15(); - - /* Edge case tests */ - printf("\n=== Edge Case Tests ===\n"); - test_binary_data(); - test_empty_input(); - test_single_byte(); - - /* Larger data tests */ - printf("\n=== Large Data Tests ===\n"); - test_large_data(); - test_psbt_like_data(); - - /* API tests */ - printf("\n=== API Tests ===\n"); - test_fixed_buffer(); - test_buffer_too_small(); - test_zlib_header(); - - /* Benchmark */ - test_compression_ratio(); - - /* Summary */ - printf("========================================\n"); - printf(" Test Summary\n"); - printf("========================================\n"); - printf("Passed: %d\n", tests_passed); - printf("Failed: %d\n", tests_failed); - printf("Total: %d\n", tests_passed + tests_failed); - printf("========================================\n"); - - return tests_failed > 0 ? 1 : 0; -} diff --git a/components/deflate_codec/CMakeLists.txt b/components/deflate_codec/CMakeLists.txt new file mode 100644 index 00000000..696f9642 --- /dev/null +++ b/components/deflate_codec/CMakeLists.txt @@ -0,0 +1,8 @@ +idf_component_register( + SRCS + "src/deflate_codec.c" + INCLUDE_DIRS + "src" + PRIV_REQUIRES + espressif__zlib +) diff --git a/components/deflate_codec/idf_component.yml b/components/deflate_codec/idf_component.yml new file mode 100644 index 00000000..84ca8a1f --- /dev/null +++ b/components/deflate_codec/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/zlib: "^1.2.13" diff --git a/components/deflate_codec/src/deflate_codec.c b/components/deflate_codec/src/deflate_codec.c new file mode 100644 index 00000000..9ed29841 --- /dev/null +++ b/components/deflate_codec/src/deflate_codec.c @@ -0,0 +1,185 @@ +#include "deflate_codec.h" + +#include +#include +#include + +#define DEFLATE_CODEC_MEM_LEVEL 4 +#define DEFLATE_CODEC_INITIAL_OUTPUT 1024U + +#ifdef ESP_PLATFORM +#include + +// zlib's internal state is several KB per stream; keep it out of the scarce +// internal DRAM that the camera/ISP pipeline already pressures. +static voidpf codec_zalloc(voidpf opaque, uInt items, uInt size) { + (void)opaque; + return heap_caps_malloc_prefer((size_t)items * size, 2, + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, + MALLOC_CAP_8BIT); +} + +static void codec_zfree(voidpf opaque, voidpf address) { + (void)opaque; + heap_caps_free(address); +} +#endif + +static void stream_set_allocator(z_stream *stream) { +#ifdef ESP_PLATFORM + stream->zalloc = codec_zalloc; + stream->zfree = codec_zfree; +#else + (void)stream; +#endif +} + +uint8_t *deflate_compress_raw_alloc(const uint8_t *source, size_t source_len, + size_t *dest_len, int window_bits) { + if ((!source && source_len != 0) || !dest_len || window_bits < 8 || + window_bits > MAX_WBITS || source_len > UINT_MAX || + source_len > ULONG_MAX) { + return NULL; + } + + z_stream stream = {0}; + stream_set_allocator(&stream); + if (deflateInit2(&stream, Z_BEST_COMPRESSION, Z_DEFLATED, -window_bits, + DEFLATE_CODEC_MEM_LEVEL, Z_DEFAULT_STRATEGY) != Z_OK) { + return NULL; + } + + uLong bound = deflateBound(&stream, (uLong)source_len); + if (bound == 0 || bound > SIZE_MAX || bound > UINT_MAX) { + deflateEnd(&stream); + return NULL; + } + + uint8_t *dest = (uint8_t *)malloc((size_t)bound); + if (!dest) { + deflateEnd(&stream); + return NULL; + } + + stream.next_in = (Bytef *)source; + stream.avail_in = (uInt)source_len; + stream.next_out = dest; + stream.avail_out = (uInt)bound; + + int status = deflate(&stream, Z_FINISH); + if (status != Z_STREAM_END) { + deflateEnd(&stream); + free(dest); + return NULL; + } + + size_t actual_len = (size_t)stream.total_out; + deflateEnd(&stream); + + uint8_t *shrunk = (uint8_t *)realloc(dest, actual_len); + if (shrunk) { + dest = shrunk; + } + *dest_len = actual_len; + return dest; +} + +static uint8_t *inflate_alloc(const uint8_t *source, size_t source_len, + size_t *dest_len, int window_bits, + size_t max_output) { + if (!source || source_len == 0 || !dest_len || max_output == 0 || + source_len > UINT_MAX) { + return NULL; + } + + z_stream stream = {0}; + stream_set_allocator(&stream); + if (inflateInit2(&stream, window_bits) != Z_OK) { + return NULL; + } + + size_t capacity = source_len > max_output / 4 ? max_output : source_len * 4; + if (capacity < DEFLATE_CODEC_INITIAL_OUTPUT) { + capacity = max_output < DEFLATE_CODEC_INITIAL_OUTPUT + ? max_output + : DEFLATE_CODEC_INITIAL_OUTPUT; + } + + uint8_t *dest = (uint8_t *)malloc(capacity); + if (!dest) { + inflateEnd(&stream); + return NULL; + } + + stream.next_in = (Bytef *)source; + stream.avail_in = (uInt)source_len; + stream.next_out = dest; + stream.avail_out = (uInt)capacity; + + for (;;) { + int status = inflate(&stream, Z_NO_FLUSH); + if (status == Z_STREAM_END) { + break; + } + if (status != Z_OK) { + inflateEnd(&stream); + free(dest); + return NULL; + } + + if (stream.avail_out != 0) { + if (stream.avail_in == 0) { + inflateEnd(&stream); + free(dest); + return NULL; + } + continue; + } + + if (capacity >= max_output) { + inflateEnd(&stream); + free(dest); + return NULL; + } + + size_t used = (size_t)stream.total_out; + size_t new_capacity = capacity > max_output / 2 ? max_output : capacity * 2; + uint8_t *grown = (uint8_t *)realloc(dest, new_capacity); + if (!grown) { + inflateEnd(&stream); + free(dest); + return NULL; + } + + dest = grown; + capacity = new_capacity; + stream.next_out = dest + used; + stream.avail_out = (uInt)(capacity - used); + } + + size_t actual_len = (size_t)stream.total_out; + inflateEnd(&stream); + + if (actual_len > 0) { + uint8_t *shrunk = (uint8_t *)realloc(dest, actual_len); + if (shrunk) { + dest = shrunk; + } + } + *dest_len = actual_len; + return dest; +} + +uint8_t *deflate_decompress_raw_alloc(const uint8_t *source, size_t source_len, + size_t *dest_len, int window_bits, + size_t max_output) { + if (window_bits < 8 || window_bits > MAX_WBITS) { + return NULL; + } + return inflate_alloc(source, source_len, dest_len, -window_bits, max_output); +} + +uint8_t *deflate_decompress_zlib_alloc(const uint8_t *source, size_t source_len, + size_t *dest_len, size_t max_output) { + return inflate_alloc(source, source_len, dest_len, MAX_WBITS, max_output); +} diff --git a/components/deflate_codec/src/deflate_codec.h b/components/deflate_codec/src/deflate_codec.h new file mode 100644 index 00000000..c7751f68 --- /dev/null +++ b/components/deflate_codec/src/deflate_codec.h @@ -0,0 +1,20 @@ +#ifndef DEFLATE_CODEC_H +#define DEFLATE_CODEC_H + +#include +#include + +/** Compress to raw deflate using maximum compression effort. */ +uint8_t *deflate_compress_raw_alloc(const uint8_t *source, size_t source_len, + size_t *dest_len, int window_bits); + +/** Decompress raw deflate while enforcing an output-size limit. */ +uint8_t *deflate_decompress_raw_alloc(const uint8_t *source, size_t source_len, + size_t *dest_len, int window_bits, + size_t max_output); + +/** Decompress a zlib-wrapped stream while enforcing an output-size limit. */ +uint8_t *deflate_decompress_zlib_alloc(const uint8_t *source, size_t source_len, + size_t *dest_len, size_t max_output); + +#endif /* DEFLATE_CODEC_H */ diff --git a/components/deflate_codec/test/Makefile b/components/deflate_codec/test/Makefile new file mode 100644 index 00000000..dfc9f6d0 --- /dev/null +++ b/components/deflate_codec/test/Makefile @@ -0,0 +1,19 @@ +CC = gcc +CFLAGS = -Wall -Wextra -g -I../src +LDFLAGS = -lz + +SRCS_DEFLATE = test_deflate_codec.c ../src/deflate_codec.c +TARGET_DEFLATE = test_deflate_codec + +all: $(TARGET_DEFLATE) + +$(TARGET_DEFLATE): $(SRCS_DEFLATE) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +run: $(TARGET_DEFLATE) + ./$(TARGET_DEFLATE) + +clean: + rm -f $(TARGET_DEFLATE) + +.PHONY: all run clean diff --git a/components/deflate_codec/test/test_deflate_codec.c b/components/deflate_codec/test/test_deflate_codec.c new file mode 100644 index 00000000..c448779b --- /dev/null +++ b/components/deflate_codec/test/test_deflate_codec.c @@ -0,0 +1,171 @@ +#include "deflate_codec.h" + +#include +#include +#include +#include + +#define TEST_WBITS 10 +#define TEST_MAX_OUTPUT (512U * 1024U) + +static int tests_passed = 0; +static int tests_failed = 0; + +#define TEST(name) printf("Testing: %s... ", name) +#define PASS() \ + do { \ + printf("PASS\n"); \ + tests_passed++; \ + } while (0) +#define FAIL(msg) \ + do { \ + printf("FAIL: %s\n", msg); \ + tests_failed++; \ + } while (0) + +static void test_raw_roundtrip(void) { + TEST("raw deflate round-trip"); + + uint8_t original[4096]; + for (size_t i = 0; i < sizeof(original); i++) { + original[i] = (uint8_t)((i * 17 + i / 128) & 0xff); + } + + size_t compressed_len = 0; + uint8_t *compressed = deflate_compress_raw_alloc(original, sizeof(original), + &compressed_len, TEST_WBITS); + if (!compressed) { + FAIL("compression failed"); + return; + } + + size_t decoded_len = 0; + uint8_t *decoded = deflate_decompress_raw_alloc( + compressed, compressed_len, &decoded_len, TEST_WBITS, TEST_MAX_OUTPUT); + free(compressed); + + if (!decoded || decoded_len != sizeof(original) || + memcmp(decoded, original, sizeof(original)) != 0) { + free(decoded); + FAIL("round-trip mismatch"); + return; + } + + printf("(%zu -> %zu bytes) ", sizeof(original), compressed_len); + free(decoded); + PASS(); +} + +static void test_wrapped_zlib_decode(void) { + TEST("zlib-wrapped decode"); + + static const uint8_t original[] = + "Wrapped zlib compatibility data. Wrapped zlib compatibility data."; + uLongf compressed_len = compressBound(sizeof(original)); + uint8_t *compressed = (uint8_t *)malloc((size_t)compressed_len); + if (!compressed || compress2(compressed, &compressed_len, original, + sizeof(original), Z_BEST_COMPRESSION) != Z_OK) { + free(compressed); + FAIL("wrapped compression failed"); + return; + } + + size_t decoded_len = 0; + uint8_t *decoded = deflate_decompress_zlib_alloc( + compressed, (size_t)compressed_len, &decoded_len, TEST_MAX_OUTPUT); + free(compressed); + + if (!decoded || decoded_len != sizeof(original) || + memcmp(decoded, original, sizeof(original)) != 0) { + free(decoded); + FAIL("wrapped decode mismatch"); + return; + } + + free(decoded); + PASS(); +} + +static void test_output_limit(void) { + TEST("decompression output limit"); + + uint8_t original[4096] = {0}; + size_t compressed_len = 0; + uint8_t *compressed = deflate_compress_raw_alloc(original, sizeof(original), + &compressed_len, TEST_WBITS); + if (!compressed) { + FAIL("compression failed"); + return; + } + + size_t decoded_len = 0; + uint8_t *decoded = deflate_decompress_raw_alloc( + compressed, compressed_len, &decoded_len, TEST_WBITS, 1024); + free(compressed); + + if (decoded) { + free(decoded); + FAIL("oversized output was accepted"); + return; + } + + PASS(); +} + +static void test_empty_roundtrip(void) { + TEST("empty raw-deflate round-trip"); + + static const uint8_t empty[] = {0}; + size_t compressed_len = 0; + uint8_t *compressed = + deflate_compress_raw_alloc(empty, 0, &compressed_len, TEST_WBITS); + if (!compressed) { + FAIL("empty compression failed"); + return; + } + + size_t decoded_len = 1; + uint8_t *decoded = deflate_decompress_raw_alloc( + compressed, compressed_len, &decoded_len, TEST_WBITS, TEST_MAX_OUTPUT); + free(compressed); + + if (!decoded || decoded_len != 0) { + free(decoded); + FAIL("empty round-trip mismatch"); + return; + } + + free(decoded); + PASS(); +} + +static void test_invalid_stream(void) { + TEST("invalid stream rejection"); + + static const uint8_t invalid[] = {0xff, 0xff, 0xff, 0xff}; + size_t decoded_len = 0; + uint8_t *decoded = deflate_decompress_raw_alloc( + invalid, sizeof(invalid), &decoded_len, TEST_WBITS, TEST_MAX_OUTPUT); + if (decoded) { + free(decoded); + FAIL("invalid stream was accepted"); + return; + } + + PASS(); +} + +int main(void) { + printf("Deflate Codec Test Suite\n"); + printf("========================\n\n"); + + test_raw_roundtrip(); + test_wrapped_zlib_decode(); + test_output_limit(); + test_empty_roundtrip(); + test_invalid_stream(); + + printf("\n========================\n"); + printf("Results: %d passed, %d failed\n", tests_passed, tests_failed); + return tests_failed > 0 ? 1 : 0; +} diff --git a/dependencies.lock b/dependencies.lock index f51728ec..97b8b188 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -187,7 +187,7 @@ dependencies: type: service version: 1.2.0~2 espressif/esp_lv_decoder: - component_hash: 8e74961d7471fbdf6fc17c5517c0ecc15148b8d04e792a92fefc2b7bacd98cbb + component_hash: 331d23c304cea5d1aed0ad3cea7774874dbaeebb2217e8db6bf3cc8485ac9f9f dependencies: - name: espressif/esp_new_jpeg registry_url: https://components.espressif.com @@ -217,7 +217,9 @@ dependencies: - esp32c3 - esp32c5 - esp32c6 - version: 0.4.2 + - esp32c61 + - esp32h4 + version: 0.4.3 espressif/esp_lv_fs: component_hash: 66896007884b817df34c964f9a114fff538ee2674e99fee7159162498b93f94b dependencies: @@ -451,7 +453,7 @@ dependencies: require: private version: '>=4.4' source: - registry_url: https://components.espressif.com + registry_url: https://components.espressif.com/ type: service version: 1.3.2 idf: @@ -474,8 +476,9 @@ direct_dependencies: - espressif/esp_lcd_touch_gt911 - espressif/esp_lvgl_adapter - espressif/esp_video +- espressif/zlib - idf - lvgl/lvgl -manifest_hash: 9ecbbbcd636262f3b288e960bb84de4d38c7c6212b55f8d6fd4e83a0e7d0edff +manifest_hash: 167024d0947a55943ecf621fe6a8eb48ed1e699e5afff6b6720c0a8a545a1d5c target: esp32p4 version: 3.0.0 diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index bc36c1db..15cdeebd 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -28,8 +28,9 @@ idf_component_register( SRCS ${SOURCES} INCLUDE_DIRS . PRIV_REQUIRES lvgl ${TOUCH_DRIVER} k_quirc esp_timer wave_4b wave_35 wave_5 wave_43 - crowpanel libwally-core cUR sd_card bbqr video spiffs nvs_flash efuse - esp_hw_support esp_app_format mbedtls_compat esp_driver_ppa + crowpanel libwally-core cUR sd_card bbqr deflate_codec video spiffs + nvs_flash efuse esp_hw_support esp_app_format mbedtls_compat + esp_driver_ppa ) if(DEFINED FONT_POLICY_WIDTH AND DEFINED FONT_POLICY_HEIGHT) diff --git a/main/core/kef.c b/main/core/kef.c index 0acbf769..2408629e 100644 --- a/main/core/kef.c +++ b/main/core/kef.c @@ -12,7 +12,11 @@ #include "crypto_utils.h" /* Raw deflate compress / decompress (wbits = 10) */ -#include "../../components/bbqr/src/miniz.h" +#include + +/* KEF format contract: raw deflate with a 1KB window */ +#define KEF_DEFLATE_WBITS 10 +#define KEF_MAX_DECOMPRESSED (512U * 1024U) #include #include @@ -403,8 +407,8 @@ kef_error_t kef_encrypt(const uint8_t *id, size_t id_len, uint8_t version, size_t work_len = pt_len; if (vi->compress) { - compressed = - mz_deflate_raw_alloc_wbits(plaintext, pt_len, &compressed_len, 10); + compressed = deflate_compress_raw_alloc(plaintext, pt_len, &compressed_len, + KEF_DEFLATE_WBITS); if (!compressed) { err = KEF_ERR_COMPRESS; goto cleanup; @@ -702,7 +706,8 @@ kef_error_t kef_decrypt(const uint8_t *envelope, size_t env_len, if (vi->compress) { size_t dec_len = 0; uint8_t *decompressed = - mz_inflate_raw_alloc(decrypted, plain_len, &dec_len); + deflate_decompress_raw_alloc(decrypted, plain_len, &dec_len, + KEF_DEFLATE_WBITS, KEF_MAX_DECOMPRESSED); if (!decompressed) { err = KEF_ERR_DECOMPRESS; goto cleanup; diff --git a/main/qr/viewer.c b/main/qr/viewer.c index 31ef5bc6..cbf8bd3a 100644 --- a/main/qr/viewer.c +++ b/main/qr/viewer.c @@ -21,11 +21,6 @@ #define UR_HEADER_OVERHEAD 30 #define UR_MAX_FRAGMENT_LEN ((MAX_QR_CHARS_PER_FRAME - UR_HEADER_OVERHEAD) / 2) -typedef struct { - char *data; - size_t len; -} QRViewerPart; - static lv_obj_t *qr_viewer_screen = NULL; static lv_obj_t *qr_code_obj = NULL; static lv_obj_t *progress_frame = NULL; @@ -37,7 +32,8 @@ static char *qr_content_copy = NULL; static lv_timer_t *message_timer = NULL; static lv_timer_t *animation_timer = NULL; -static QRViewerPart *qr_parts = NULL; +static char **qr_parts = NULL; +static BBQrParts *bbqr_parts_owner = NULL; static int qr_parts_count = 0; static int current_part_index = 0; @@ -117,12 +113,16 @@ static void split_content_into_parts(const char *content) { if (content_len <= max_chars) { qr_parts_count = 1; - qr_parts = malloc(sizeof(QRViewerPart)); + qr_parts = malloc(sizeof(char *)); if (!qr_parts) return; - qr_parts[0].data = strdup(content); - qr_parts[0].len = content_len; + qr_parts[0] = strdup(content); + if (!qr_parts[0]) { + free(qr_parts); + qr_parts = NULL; + qr_parts_count = 0; + } return; } @@ -135,7 +135,7 @@ static void split_content_into_parts(const char *content) { size_t chars_per_part = max_chars - prefix_len; qr_parts_count = (content_len + chars_per_part - 1) / chars_per_part; - qr_parts = malloc(qr_parts_count * sizeof(QRViewerPart)); + qr_parts = malloc(qr_parts_count * sizeof(char *)); if (!qr_parts) { qr_parts_count = 0; return; @@ -151,11 +151,11 @@ static void split_content_into_parts(const char *content) { snprintf(header, sizeof(header), "p%dof%d ", i + 1, qr_parts_count); size_t header_len = strlen(header); - qr_parts[i].len = header_len + chunk_size; - qr_parts[i].data = malloc(qr_parts[i].len + 1); - if (!qr_parts[i].data) { + size_t part_len = header_len + chunk_size; + qr_parts[i] = malloc(part_len + 1); + if (!qr_parts[i]) { for (int j = 0; j < i; j++) { - free(qr_parts[j].data); + free(qr_parts[j]); } free(qr_parts); qr_parts = NULL; @@ -163,17 +163,21 @@ static void split_content_into_parts(const char *content) { return; } - memcpy(qr_parts[i].data, header, header_len); - memcpy(qr_parts[i].data + header_len, content + offset, chunk_size); - qr_parts[i].data[qr_parts[i].len] = '\0'; + memcpy(qr_parts[i], header, header_len); + memcpy(qr_parts[i] + header_len, content + offset, chunk_size); + qr_parts[i][part_len] = '\0'; } } static void cleanup_qr_parts(void) { - if (qr_parts) { + if (bbqr_parts_owner) { + bbqr_parts_free(bbqr_parts_owner); + bbqr_parts_owner = NULL; + qr_parts = NULL; + } else if (qr_parts) { for (int i = 0; i < qr_parts_count; i++) { - if (qr_parts[i].data) { - free(qr_parts[i].data); + if (qr_parts[i]) { + free(qr_parts[i]); } } free(qr_parts); @@ -188,7 +192,7 @@ static void animation_timer_cb(lv_timer_t *timer) { return; } current_part_index = (current_part_index + 1) % qr_parts_count; - qr_update_optimal(qr_code_obj, qr_parts[current_part_index].data, NULL); + qr_update_optimal(qr_code_obj, qr_parts[current_part_index], NULL); update_progress_indicator(current_part_index); } @@ -208,7 +212,7 @@ static bool setup_qr_viewer_ui(lv_obj_t *parent, const char *title) { } int32_t qr_size = (w < h) ? w : h; - qr_code_obj = qr_create_optimal(qr_viewer_screen, qr_size, qr_parts[0].data); + qr_code_obj = qr_create_optimal(qr_viewer_screen, qr_size, qr_parts[0]); if (!qr_code_obj) { return false; } @@ -252,6 +256,7 @@ void qr_viewer_page_create(lv_obj_t *parent, const char *qr_content, return; } + cleanup_qr_parts(); return_callback = return_cb; message_timer = NULL; animation_timer = NULL; @@ -366,6 +371,8 @@ bool qr_viewer_page_create_with_format(lv_obj_t *parent, int qr_format, return true; } + cleanup_qr_parts(); + // Decode base64 content to binary PSBT size_t max_decoded_len = (strlen(content) * 3) / 4 + 1; uint8_t *psbt_bytes = (uint8_t *)malloc(max_decoded_len); @@ -383,11 +390,11 @@ bool qr_viewer_page_create_with_format(lv_obj_t *parent, int qr_format, if (qr_format == FORMAT_BBQR) { // Encode as BBQr - BBQrParts *bbqr_parts = bbqr_encode(psbt_bytes, psbt_len, BBQR_TYPE_PSBT, - MAX_QR_CHARS_PER_FRAME); + bbqr_parts_owner = bbqr_encode(psbt_bytes, psbt_len, BBQR_TYPE_PSBT, + MAX_QR_CHARS_PER_FRAME); free(psbt_bytes); - if (!bbqr_parts) { + if (!bbqr_parts_owner) { return false; } @@ -395,28 +402,8 @@ bool qr_viewer_page_create_with_format(lv_obj_t *parent, int qr_format, message_timer = NULL; animation_timer = NULL; - qr_parts_count = bbqr_parts->count; - qr_parts = malloc(qr_parts_count * sizeof(QRViewerPart)); - if (!qr_parts) { - bbqr_parts_free(bbqr_parts); - return false; - } - - for (int i = 0; i < qr_parts_count; i++) { - qr_parts[i].len = strlen(bbqr_parts->parts[i]); - qr_parts[i].data = strdup(bbqr_parts->parts[i]); - if (!qr_parts[i].data) { - for (int j = 0; j < i; j++) { - free(qr_parts[j].data); - } - free(qr_parts); - qr_parts = NULL; - qr_parts_count = 0; - bbqr_parts_free(bbqr_parts); - return false; - } - } - bbqr_parts_free(bbqr_parts); + qr_parts_count = bbqr_parts_owner->count; + qr_parts = bbqr_parts_owner->parts; if (!setup_qr_viewer_ui(parent, title)) { cleanup_qr_parts(); @@ -475,7 +462,7 @@ bool qr_viewer_page_create_with_format(lv_obj_t *parent, int qr_format, animation_timer = NULL; qr_parts_count = parts_count; - qr_parts = malloc(qr_parts_count * sizeof(QRViewerPart)); + qr_parts = malloc(qr_parts_count * sizeof(char *)); if (!qr_parts) { for (size_t i = 0; i < parts_count; i++) { free(ur_parts_strings[i]); @@ -485,8 +472,7 @@ bool qr_viewer_page_create_with_format(lv_obj_t *parent, int qr_format, } for (int i = 0; i < qr_parts_count; i++) { - qr_parts[i].len = strlen(ur_parts_strings[i]); - qr_parts[i].data = ur_parts_strings[i]; + qr_parts[i] = ur_parts_strings[i]; } free(ur_parts_strings); diff --git a/scripts/format.sh b/scripts/format.sh index 83a39fb9..af955f2a 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -20,6 +20,7 @@ DIRS=( "$REPO_ROOT/main" "$REPO_ROOT/components/bbqr" "$REPO_ROOT/components/cUR" + "$REPO_ROOT/components/deflate_codec" "$REPO_ROOT/components/k_quirc" "$REPO_ROOT/components/sd_card" "$REPO_ROOT/components/video" diff --git a/scripts/test.sh b/scripts/test.sh index 8ed675e0..7de79865 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -7,6 +7,9 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || (cd "$SCRIPT_DIR/.." && pwd))" +echo "Running deflate_codec tests..." +make -C "$REPO_ROOT/components/deflate_codec/test" run + echo "Running bbqr tests..." make -C "$REPO_ROOT/components/bbqr/test" run diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 207de7ff..7cc6f354 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -179,8 +179,8 @@ set(APP_CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../main/core/descriptor_checksum.c ${CMAKE_CURRENT_SOURCE_DIR}/../main/core/bip32_path.c ${CMAKE_CURRENT_SOURCE_DIR}/../main/core/script_templates.c - # miniz (used by kef.c for raw-deflate compression) - ${CMAKE_CURRENT_SOURCE_DIR}/../components/bbqr/src/miniz.c + # Shared zlib adapter used by KEF and BBQr + ${CMAKE_CURRENT_SOURCE_DIR}/../components/deflate_codec/src/deflate_codec.c # bbqr (multi-part QR encoding) ${CMAKE_CURRENT_SOURCE_DIR}/../components/bbqr/src/bbqr.c ${CMAKE_CURRENT_SOURCE_DIR}/../components/bbqr/src/base32.c @@ -371,9 +371,10 @@ target_include_directories(kern_simulator PRIVATE ${MBEDTLS_INCLUDE_DIRS} # Application source tree ${CMAKE_CURRENT_SOURCE_DIR}/../main # ui/theme.h, core/, utils/, pages/ - # Component headers (for kef.c -> bbqr/miniz, cUR, k_quirc, video) + # Component headers (for kef.c -> deflate_codec, cUR, k_quirc, video) ${CMAKE_CURRENT_SOURCE_DIR}/../components ${CMAKE_CURRENT_SOURCE_DIR}/../components/bbqr/src + ${CMAKE_CURRENT_SOURCE_DIR}/../components/deflate_codec/src ${CMAKE_CURRENT_SOURCE_DIR}/../components/k_quirc/include ${CMAKE_CURRENT_SOURCE_DIR}/../components/k_quirc/src ${CMAKE_CURRENT_SOURCE_DIR}/../components/cUR/src @@ -436,6 +437,7 @@ target_link_libraries(kern_simulator PRIVATE ${MBEDTLS_LIB} ${MBEDCRYPTO_LIB} ${MBEDX509_LIB} + z $<$:v4l2_capture> ) @@ -444,7 +446,7 @@ add_executable(kern_sim_storage_smoke ${CMAKE_CURRENT_SOURCE_DIR}/platform/esp_idf_stubs/storage_sim.c ${CMAKE_CURRENT_SOURCE_DIR}/../main/core/kef.c ${CMAKE_CURRENT_SOURCE_DIR}/../main/core/crypto_utils.c - ${CMAKE_CURRENT_SOURCE_DIR}/../components/bbqr/src/miniz.c + ${CMAKE_CURRENT_SOURCE_DIR}/../components/deflate_codec/src/deflate_codec.c ${CMAKE_CURRENT_SOURCE_DIR}/platform/esp_idf_stubs/stubs.c ${CMAKE_CURRENT_SOURCE_DIR}/platform/esp_idf_stubs/sim_flash.c ${CMAKE_CURRENT_SOURCE_DIR}/platform/esp_idf_stubs/sim_storage_hooks.c @@ -459,7 +461,7 @@ target_include_directories(kern_sim_storage_smoke PRIVATE ${MBEDTLS_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/../main ${CMAKE_CURRENT_SOURCE_DIR}/../components - ${CMAKE_CURRENT_SOURCE_DIR}/../components/bbqr/src + ${CMAKE_CURRENT_SOURCE_DIR}/../components/deflate_codec/src ) target_compile_definitions(kern_sim_storage_smoke PRIVATE @@ -478,6 +480,7 @@ target_link_libraries(kern_sim_storage_smoke PRIVATE ${MBEDTLS_LIB} ${MBEDCRYPTO_LIB} ${MBEDX509_LIB} + z ) enable_testing() diff --git a/simulator/tests/storage_smoke.c b/simulator/tests/storage_smoke.c index 27e32334..a32105c9 100644 --- a/simulator/tests/storage_smoke.c +++ b/simulator/tests/storage_smoke.c @@ -106,7 +106,7 @@ int main(void) { size_t kef_blob_len = 0; const uint8_t password[] = "test"; const uint8_t secret[] = "seed entropy bytes"; - CHECK(kef_encrypt((const uint8_t *)"SmokeName", 9, KEF_V15_CTR_H4, + CHECK(kef_encrypt((const uint8_t *)"SmokeName", 9, KEF_V16_CTR_Z_H4, password, strlen((const char *)password), 10000, secret, sizeof(secret) - 1, &kef_blob, &kef_blob_len) == KEF_OK, "create KEF envelope");