From 5c39fda7be044ea713f8ebe35bd15de4d7e11418 Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Mon, 20 Apr 2026 21:57:52 +0100 Subject: [PATCH] Add KenCode decompressor implementation and tests --- CMakeLists.txt | 2 +- kencode.c | 314 +++++++++++++++++++++++++++++++++++ kencode.h | 12 ++ library.h | 6 +- tests/CMakeLists.txt | 18 +- tests/data/kencode_boot.bin | Bin 0 -> 2495 bytes tests/data/kencode_hfs.bin | Bin 0 -> 4579 bytes tests/data/kencode_large.bin | Bin 0 -> 2017 bytes tests/data/kencode_last.bin | Bin 0 -> 3076 bytes tests/data/kencode_zeros.bin | Bin 0 -> 442 bytes tests/kencode.cpp | 211 +++++++++++++++++++++++ 11 files changed, 560 insertions(+), 3 deletions(-) create mode 100644 kencode.c create mode 100644 kencode.h create mode 100644 tests/data/kencode_boot.bin create mode 100644 tests/data/kencode_hfs.bin create mode 100644 tests/data/kencode_large.bin create mode 100644 tests/data/kencode_last.bin create mode 100644 tests/data/kencode_zeros.bin create mode 100644 tests/kencode.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 46de9f6..ddce114 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,7 +131,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) add_subdirectory(3rdparty) -add_library("Aaru.Compression.Native" SHARED library.c apple_rle.c apple_rle.h apple_lzh.c apple_lzh.h adc.c adc.h lzip.c flac.c flac.h +add_library("Aaru.Compression.Native" SHARED library.c apple_rle.c apple_rle.h apple_lzh.c apple_lzh.h adc.c adc.h kencode.c kencode.h lzip.c flac.c flac.h zoo/lzd.c zoo/lzd.h zoo/lzh.c zoo/decode.c zoo/huf.c zoo/io.c zoo/lh5.c zoo/lh5.h zoo/lzh.h zoo/ar.h zoo/maketbl.c arc/pack.c arc/squeeze.c arc/crunch.c arc/lzw.c pak/crush.c pak/distill.c pak/bitstream.c pak/bitstream.h pak/lzw.c pak/lzw.h pak/prefixcode.c diff --git a/kencode.c b/kencode.c new file mode 100644 index 0000000..9ae643a --- /dev/null +++ b/kencode.c @@ -0,0 +1,314 @@ +// KenCode decompressor +// Reverse-engineered from Apple DiskCopy 6.3.3 resource fork hdi2 ID 128 +// Algorithm: LZSS with MSB-first bitstream and variable-length prefix codes +// +// Function map from PEF disassembly: +// 0x0000 plugin_entry Component Manager dispatcher (selectors 0-3) +// 0x00CC read_match_info Reads one token (match or literal run) from bitstream +// 0x01AC decompress Main decompression loop +// 0x03E4 bs_init Initialize bitstream reader +// 0x03F4 read_unary Count consecutive 1-bits (MSB-first) +// 0x046C read_bits Read N bits from bitstream +// 0x0510 read_literals Read N literal bytes from bitstream +// 0x05C0 decode_lit_count Decode literal run length +// 0x0750 decode_match_len Decode match length with prefix code +// 0x0A84 decode_offset Decode match offset (multi-level) +// 0x0C2C offset_dispatch Select distance class based on output position + +#include +#include +#include + +#include "library.h" +#include "kencode.h" + +// ============================================================================ +// MSB-first bitstream reader +// ============================================================================ + +typedef struct +{ + const uint8_t *data; + uint32_t bit_pos; + uint32_t bit_end; +} kc_bs; + +static inline void bs_init(kc_bs *bs, const uint8_t *data, size_t size) +{ + bs->data = data; + bs->bit_pos = 0; + bs->bit_end = (uint32_t)(size * 8); +} + +// PEF 0x046C: Read N bits MSB-first. +// The PEF reads a 32-bit big-endian word at (bit_pos / 8), shifts right +// by (32 - bit_within_byte - n), and masks to n bits. +static inline uint32_t bs_read(kc_bs *bs, int n) +{ + if(n <= 0 || bs->bit_pos + (uint32_t)n > bs->bit_end) return 0; + + uint32_t byte_off = bs->bit_pos >> 3; + int bit_off = bs->bit_pos & 7; + int shift = 32 - bit_off - n; + + // Read a 32-bit big-endian word starting at byte_off + // Need to handle reads near end of buffer + uint32_t word = 0; + size_t max_byte = (bs->bit_end + 7) / 8; + for(int i = 0; i < 4; i++) + { + word <<= 8; + if(byte_off + i < max_byte) word |= bs->data[byte_off + i]; + } + + uint32_t mask = ((uint32_t)1 << n) - 1; + bs->bit_pos += n; + + if(shift >= 0) return (word >> shift) & mask; + + // shift < 0: need bits from next word (rare: n > 24 or misaligned) + word <<= (-shift); + if(byte_off + 4 < max_byte) word |= (bs->data[byte_off + 4] >> (8 + shift)); + return word & mask; +} + +// PEF 0x03F4: Count consecutive 1-bits, stopping at first 0-bit or max_count. +static inline uint32_t bs_read_unary(kc_bs *bs, int max_count) +{ + uint32_t count = 0; + while((int)count < max_count && bs->bit_pos < bs->bit_end) + { + uint32_t byte_off = bs->bit_pos >> 3; + int bit_off = bs->bit_pos & 7; + bs->bit_pos++; + if(!(bs->data[byte_off] & (0x80 >> bit_off))) return count; // hit 0 + count++; + } + return count; +} + +// PEF 0x0510: Read N literal bytes from bitstream. Each byte is 8 bits +// at the current (possibly unaligned) bit position. +static void bs_read_bytes(kc_bs *bs, uint8_t *dst, int count) +{ + uint32_t byte_off = bs->bit_pos >> 3; + int bit_off = bs->bit_pos & 7; + int rshift = 8 - bit_off; + + for(int i = 0; i < count; i++) + { + uint16_t w = ((uint16_t)bs->data[byte_off] << 8) | bs->data[byte_off + 1]; + dst[i] = (uint8_t)(w >> rshift); + byte_off++; + } + bs->bit_pos += count * 8; +} + +// ============================================================================ +// VLC decoders +// ============================================================================ + +// PEF 0x05C0: Decode literal run length (1..63) +static int decode_lit_count(kc_bs *bs) +{ + // "0" → 1 + if(bs_read(bs, 1) == 0) return 1; + + // "1" + 2 bits + uint32_t v = bs_read(bs, 2); + if(v == 0) return 2; + if(v == 1) return 3; + if(v == 2) return (int)bs_read(bs, 2) + 4; // 4-7 + + // v==3: 4 more bits + uint32_t e = bs_read(bs, 4); + if(e <= 7) return (int)e + 8; // 8-15 + if(e <= 11) return (int)(e * 4 + bs_read(bs, 2)) - 16; // 16-31 + return (int)(e * 8 + bs_read(bs, 3)) - 64; // 32-63 +} + +// PEF 0x0750: Decode match length raw value (0..2042+) +// Value 0 with had_literals=1 → literal run marker +// Otherwise: actual match length = raw + 2 (or +3 if previous was literals) +static int decode_match_len(kc_bs *bs) +{ + uint32_t pfx = bs_read_unary(bs, 10); + + switch(pfx) + { + case 0: + return (int)bs_read(bs, 1); // 0-1 + case 1: + { + uint32_t b = bs_read(bs, 1); + if(b == 0) return 2; + return (int)bs_read(bs, 1) + 3; // 3-4 + } + case 2: + { + uint32_t b = bs_read(bs, 1); + if(b == 0) return (int)bs_read(bs, 1) + 5; // 5-6 + return (int)bs_read(bs, 2) + 7; // 7-10 + } + case 3: + return (int)bs_read(bs, 3) + 11; // 11-18 + case 4: + return (int)bs_read(bs, 3) + 19; // 19-26 + case 5: + return (int)bs_read(bs, 5) + 27; // 27-58 + case 6: + return (int)bs_read(bs, 6) + 59; // 59-122 + case 7: + return (int)bs_read(bs, 7) + 123; // 123-250 + case 8: + return (int)bs_read(bs, 8) + 251; // 251-506 + case 9: + return (int)bs_read(bs, 9) + 507; // 507-1018 + default: + return (int)bs_read(bs, 10) + 1019; // 1019+ + } +} + +// PEF 0x0A84: Decode match offset given distance class (dist_bits) +static int decode_offset(kc_bs *bs, int dist_bits, int hash_size) +{ + int pw = 1 << dist_bits; + + // Prefix "0" + dist_bits → short offset + if(bs_read(bs, 1) == 0) return (int)bs_read(bs, dist_bits) + 1; + + // Prefix "10" + (dist_bits+2) bits → medium offset + if(bs_read(bs, 1) == 0) return pw + 1 + (int)bs_read(bs, dist_bits + 2); + + // Prefix "11" → long offset, escalating levels + int pw5 = pw * 5; + + if(hash_size <= pw5 + 2) return pw5 + 1 + (int)bs_read(bs, 1); + + if(hash_size <= pw5 + 4) return pw5 + 1 + (int)bs_read(bs, 2); + + // Escalating loop + int max_level = dist_bits + 4; + int scale = 4; + int level = 3; + int threshold = pw5 + 4; + + if(max_level >= 3) + { + while(level <= max_level) + { + threshold += scale; + // 68K helper at 0x0ABA: clamp ONLY the exact value 0x680 to 0x66C + int clamped = (threshold == 0x680) ? 0x66C : threshold; + if(hash_size <= clamped || level == max_level) break; + scale <<= 1; + level++; + } + } + + return pw5 + 1 + (int)bs_read(bs, level); +} + +// PEF 0x0C2C / 68K 0x0D48: Select distance class based on output position and call decode_offset +// out_pos determines the distance class, hash_size (from state init) caps it for large positions +static int offset_dispatch(kc_bs *bs, int out_pos, int hash_size) +{ + int dist_bits; + + if(out_pos <= 0xa) + dist_bits = 0; + else if(out_pos <= 0x14) + dist_bits = 1; + else if(out_pos <= 0x28) + dist_bits = 2; + else if(out_pos <= 0x50) + dist_bits = 3; + else if(out_pos <= 0xa0) + dist_bits = 4; + else if(out_pos <= 0x2a0) + dist_bits = 5; + else if(out_pos <= 0x3e8) + dist_bits = 6; + else if(out_pos <= 0xa80 || hash_size <= 0x800) + dist_bits = 7; + else if(out_pos <= 0x1500 || hash_size <= 0x1000) + dist_bits = 8; + else if(out_pos <= 0x2a00 || hash_size <= 0x2000) + dist_bits = 9; + else if(out_pos <= 0x5400 || hash_size <= 0x4000) + dist_bits = 10; + else if(out_pos <= 0xa800 || hash_size <= 0x8000) + dist_bits = 11; + else if(out_pos <= 0x11170 || hash_size <= 0x10000) + dist_bits = 12; + else if(out_pos <= 0x2a000 || hash_size <= 0x20000) + dist_bits = 13; + else + dist_bits = 14; + + // 68K passes (out_pos, dist_bits, bitstream) to decode_offset + // but decode_offset internally uses out_pos for hash_size comparisons too + return decode_offset(bs, dist_bits, out_pos); +} + +// ============================================================================ +// Main decompressor — PEF 0x01AC +// ============================================================================ + +AARU_EXPORT int32_t AARU_CALL AARU_kencode_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, + const uint8_t *src_buffer, size_t src_size) +{ + if(!dst_buffer || !dst_size || !src_buffer || src_size == 0) return -1; + + size_t max_out = *dst_size; + kc_bs bs; + bs_init(&bs, src_buffer, src_size); + + size_t out_pos = 0; + uint8_t had_literals = 1; // PEF 0x01C0-0x01C4 + + // Plugin init: hash_size from clamp_hash_size(0x2800) → 0x2800 (in range [0x800, 0x30000]) + int hash_size = 0x2800; + + while(out_pos < max_out) + { + // PEF 0x00CC: read_match_info + int raw = decode_match_len(&bs); + + if(raw <= 0 && had_literals) + { + // Literal run + int lit_count = decode_lit_count(&bs); + if(out_pos + lit_count > max_out) return -4; + bs_read_bytes(&bs, dst_buffer + out_pos, lit_count); + out_pos += lit_count; + + // 68K 0x03C4-0x03D2: had_literals = (lit_count >= 63) ? 1 : 0 + // PPC 0x0180-0x0194: equivalent via subfc/adde/subfe/clrlwi + had_literals = (lit_count >= 63) ? 1 : 0; + } + else + { + // Match reference + int match_len; + if(had_literals == 0) + match_len = raw + 3; // PEF 0x0130: +3 when coming after non-literal + else + match_len = raw + 2; // PEF 0x011C: +2 when had_literals is set + + had_literals = 1; // PEF 0x0140 + + int offset = offset_dispatch(&bs, (int)out_pos, hash_size); + + if(out_pos + match_len > max_out) return -4; + if(offset > (int)out_pos) return -4; + + // Byte-by-byte copy (may overlap) + for(int i = 0; i < match_len; i++) dst_buffer[out_pos + i] = dst_buffer[out_pos - offset + i]; + out_pos += match_len; + } + } + + *dst_size = out_pos; + return 0; +} diff --git a/kencode.h b/kencode.h new file mode 100644 index 0000000..7a0997e --- /dev/null +++ b/kencode.h @@ -0,0 +1,12 @@ +// KenCode decompressor +// Reverse-engineered from Apple DiskCopy 6.3.3 hdi2 resource ID 128 + +#ifndef KENCODE_H +#define KENCODE_H + +#include +#include + +int kencode_decode_buffer(uint8_t *dst_buffer, size_t dst_size, const uint8_t *src_buffer, size_t src_size); + +#endif // KENCODE_H diff --git a/library.h b/library.h index 0cc4a1c..55dd4ef 100644 --- a/library.h +++ b/library.h @@ -57,6 +57,10 @@ AARU_EXPORT int32_t AARU_CALL AARU_adc_decode_buffer(uint8_t *dst_buffer, int32_t dst_size, const uint8_t *src_buffer, int32_t src_size); +// KenCode decompression (NDIF chunk type 0x80) +AARU_EXPORT int32_t AARU_CALL AARU_kencode_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, + const uint8_t *src_buffer, size_t src_size); + AARU_EXPORT int32_t AARU_CALL AARU_apple_rle_decode_buffer(uint8_t *dst_buffer, int32_t dst_size, const uint8_t *src_buffer, int32_t src_size); @@ -402,7 +406,7 @@ AARU_EXPORT int AARU_CALL AARU_stuffit_arsenic_decode_buffer(uint8_t *dst_buffer // ShrinkWrap 3 SIT2 compression (NDIF chunk type 0xF0) AARU_EXPORT int AARU_CALL AARU_stuffit_shrinkwrap_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, - const uint8_t *src_buffer, size_t src_size); + const uint8_t *src_buffer, size_t src_size); // StuffIt X method 0: Brimstone (PPMd Variant G) AARU_EXPORT int AARU_CALL AARU_stuffitx_brimstone_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5fb17bf..36fae71 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -222,6 +222,21 @@ file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/mf1dd_mfs_fast.dart.lz file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/mf1dd_mfs_best.dart.lz DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/) +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/kencode_boot.bin + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/) + +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/kencode_zeros.bin + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/) + +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/kencode_hfs.bin + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/) + +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/kencode_last.bin + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/) + +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/kencode_large.bin + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/) + # 'Google_Tests_run' is the target name # 'test1.cpp tests2.cpp' are source files with tests add_executable(tests_run apple_rle.cpp crc32.c crc32.h adc.cpp bzip2.cpp lzip.cpp lzfse.cpp zstd.cpp lzma.cpp flac.cpp lz4.cpp @@ -236,5 +251,6 @@ add_executable(tests_run apple_rle.cpp crc32.c crc32.h adc.cpp bzip2.cpp lzip.cp cpt/cpt.cpp dd/dd.cpp stuffit/stuffit.cpp - dart/dart.cpp) + dart/dart.cpp + kencode.cpp) target_link_libraries(tests_run gtest gtest_main "Aaru.Compression.Native") diff --git a/tests/data/kencode_boot.bin b/tests/data/kencode_boot.bin new file mode 100644 index 0000000000000000000000000000000000000000..20e8faa8a7252ace1434dbe3e49eff1325fc5eeb GIT binary patch literal 2495 zcmcDWn?8w)Bb)nK>M<4u1_uWP20sQ7hW!c&j0y}69I6c3f)|!BFdA(YwDq}j;y@#_ z#tE%9H5cLkzl=}zUEK4hT8w>f1;f!Y2Dvb?(EUoRoplVIeoPy=|9oD;Ccl@V=9ue0 z{%$vk4%eR+ek!#}-3MpBC}%8^YPSEkd0Cd=@ngvwDwftC56IwIpwXJ3!DGm7ciCpf zl0?q_vcv?F#Dk`Z4@?ps+DdFs6yunZe`0F>>m~;Nx{R$4HveZn?D73GquU=rzkiAQ z8Q5PaGZtA|=$42lY~P7yW|$-agKri>VL4Wqk0tIQxb8jqMj#T#0|N)jWbw8XBIHAimdURr7m?s;m{M2hF452YDh}@w;WDf$~?RNkBO#$#1n@}aq}EbJ$Z8Pi9&HuYC%q7a;ic}M(PA()fOJzGo8zB zcgV=hjJb2TFJFZ>XT9P=2L?{f&n;34!I8nf!4_wyRLUQ45Xd{g_&~m)c~^k_0S3{m zef1Am>Sf&RKYU=|=l}oj1H%UfK_Lb~69&OB2Ehsj!C4G~dl&@oF$jKOP!eKLGGR~( zV^FGKP@2V{w1+|I9)r>c24f)xW8?o0%%%y<<_*l23z)4BFxx(0wr5~*RA6y-U~x@g zac^MpT)^UefW`L#i$4QvpaN^K18ZmkYj^`||5NZZ4Zc8?+L14FqGL%9hl63}@~!ocX|TSBT-R3B%nmhD-nLWM?tl-NSJA9>d)a4DW;(-kC7G3uAa!!SHSt z!@E5U@9r@?X7T@jq{03ygZFCGuVG`u>Zs0z|!Kt%i8^0;KHzf3!{y8cm$MNrXCJtn%WygO!R35~%lQMY0v@gc7hL)OinavGu>`8M1nRQ{ znzsbnu!Op_g!;0ChPH&pv4p0zgyyq^mbZk~utc`BME0^oPHl;t#}c`$C2~DWshk4w`A>M z$ve`Lca|mZYD?Zdmb_;zdGA^BzPIH4VJT*5DR|!2WZ8Ad^1qzr(hC0TKc_3OO<+$FJgxx4AENvR8|0fA?AaLVco_ajF#a%L z{*b`_-~c(ob`B;?+I=e@Fo<0!c3{{iIe}sKEdjgM&^dL-zZNmAb9l~R$H03)jX|J% z!a{=!MpiSKF26e5!eGerNyp&-|Nn32xwNlfh|-7+TsSpy@5ZZJr}|F4cyZ~@xo0K! zZk#yf_>OX>7knp_HiWoFgrwvbCkA{lRM@7tRqXh`9ly>oD7@%dmz&r7&gPZo*(C}L zObqe7OD4Q8m>mCCz{u-Tlv$RlAa2|HrGRr7-(T#a0v_`MWduaZGc%gAl8EI6K?1 zuxnw@omDis?HPC_JPIqms93gZ8SSwWP}**|eUrXoB7g5K^fhy+H)utUgHqDlC z{~D!P{O(1mso59eek*BiWN8aiR|~J@ir?fqRdwg&Wa~E_%LP@IiL;443pb13*F9Un zJHIG3G4BuOBJR}-&2x@_pYumNL!csU{^R-eHSeFYG3>Bbn8!gM8Zl|6W^avj3GzFi6_9Mf7AIU=U?6nEF%q!{x?kkZESB{70t#Kf650><@+iv2gyGF9rZh#u_F7 literal 0 HcmV?d00001 diff --git a/tests/data/kencode_hfs.bin b/tests/data/kencode_hfs.bin new file mode 100644 index 0000000000000000000000000000000000000000..9af471b7db0774242969f47dff41f34114fcd645 GIT binary patch literal 4579 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. + */ + +#include +#include +#include +#include +#include +#include + +#include "../library.h" +#include "../kencode.h" +#include "crc32.h" +#include "gtest/gtest.h" + +// DOS_720.img chunk 0: boot sector with mixed data/matches (2495 → 10240) +#define KC_BOOT_COMP_SIZE 2495 +#define KC_BOOT_DECOMP_SIZE 10240 +#define KC_BOOT_CRC32 0x18DDE60Cu + +// DOS_720.img chunk 47: mostly zero data, high compression (442 → 10240) +#define KC_ZEROS_COMP_SIZE 442 +#define KC_ZEROS_DECOMP_SIZE 10240 +#define KC_ZEROS_CRC32 0x271DDE9Au + +// HFS_800.img chunk 2: HFS filesystem data (4579 → 10240) +#define KC_HFS_COMP_SIZE 4579 +#define KC_HFS_DECOMP_SIZE 10240 +#define KC_HFS_CRC32 0x72B8E8F1u + +// HFS_DMF.img chunk 77: last chunk, non-standard 6144 byte output (3076 → 6144) +#define KC_LAST_COMP_SIZE 3076 +#define KC_LAST_DECOMP_SIZE 6144 +#define KC_LAST_CRC32 0xA0B2EAEFu + +// DOS_SP_5Mb.img chunk 0: 5MB disk image first chunk (2017 → 10240) +#define KC_LARGE_COMP_SIZE 2017 +#define KC_LARGE_DECOMP_SIZE 10240 +#define KC_LARGE_CRC32 0xA584FF1Au + +static uint8_t *read_test_file(const char *name, size_t expected_size) +{ + char path[PATH_MAX]; + char filename[PATH_MAX]; + + getcwd(path, PATH_MAX); + snprintf(filename, PATH_MAX, "%s/data/%s", path, name); + + FILE *file = fopen(filename, "rb"); + if(file == nullptr) return nullptr; + + auto *buf = (uint8_t *)malloc(expected_size); + fread(buf, 1, expected_size, file); + fclose(file); + return buf; +} + +/* ---- Boot sector: mixed data and matches ---- */ + +static const uint8_t *kc_boot_buffer; + +class KencodeBootFixture : public ::testing::Test +{ +protected: + void SetUp() { kc_boot_buffer = read_test_file("kencode_boot.bin", KC_BOOT_COMP_SIZE); } + + void TearDown() { free((void *)kc_boot_buffer); } +}; + +TEST_F(KencodeBootFixture, kencode_boot) +{ + auto *outBuf = (uint8_t *)malloc(KC_BOOT_DECOMP_SIZE); + size_t destLen = KC_BOOT_DECOMP_SIZE; + + auto err = AARU_kencode_decode_buffer(outBuf, &destLen, kc_boot_buffer, KC_BOOT_COMP_SIZE); + + EXPECT_EQ(err, 0); + EXPECT_EQ(destLen, (size_t)KC_BOOT_DECOMP_SIZE); + + auto crc = crc32_data(outBuf, KC_BOOT_DECOMP_SIZE); + free(outBuf); + + EXPECT_EQ(crc, KC_BOOT_CRC32); +} + +/* ---- Mostly zeros: high compression ratio (23:1) ---- */ + +static const uint8_t *kc_zeros_buffer; + +class KencodeZerosFixture : public ::testing::Test +{ +protected: + void SetUp() { kc_zeros_buffer = read_test_file("kencode_zeros.bin", KC_ZEROS_COMP_SIZE); } + + void TearDown() { free((void *)kc_zeros_buffer); } +}; + +TEST_F(KencodeZerosFixture, kencode_zeros) +{ + auto *outBuf = (uint8_t *)malloc(KC_ZEROS_DECOMP_SIZE); + size_t destLen = KC_ZEROS_DECOMP_SIZE; + + auto err = AARU_kencode_decode_buffer(outBuf, &destLen, kc_zeros_buffer, KC_ZEROS_COMP_SIZE); + + EXPECT_EQ(err, 0); + EXPECT_EQ(destLen, (size_t)KC_ZEROS_DECOMP_SIZE); + + auto crc = crc32_data(outBuf, KC_ZEROS_DECOMP_SIZE); + free(outBuf); + + EXPECT_EQ(crc, KC_ZEROS_CRC32); +} + +/* ---- HFS filesystem data ---- */ + +static const uint8_t *kc_hfs_buffer; + +class KencodeHfsFixture : public ::testing::Test +{ +protected: + void SetUp() { kc_hfs_buffer = read_test_file("kencode_hfs.bin", KC_HFS_COMP_SIZE); } + + void TearDown() { free((void *)kc_hfs_buffer); } +}; + +TEST_F(KencodeHfsFixture, kencode_hfs) +{ + auto *outBuf = (uint8_t *)malloc(KC_HFS_DECOMP_SIZE); + size_t destLen = KC_HFS_DECOMP_SIZE; + + auto err = AARU_kencode_decode_buffer(outBuf, &destLen, kc_hfs_buffer, KC_HFS_COMP_SIZE); + + EXPECT_EQ(err, 0); + EXPECT_EQ(destLen, (size_t)KC_HFS_DECOMP_SIZE); + + auto crc = crc32_data(outBuf, KC_HFS_DECOMP_SIZE); + free(outBuf); + + EXPECT_EQ(crc, KC_HFS_CRC32); +} + +/* ---- Last chunk: non-standard 6144-byte output ---- */ + +static const uint8_t *kc_last_buffer; + +class KencodeLastFixture : public ::testing::Test +{ +protected: + void SetUp() { kc_last_buffer = read_test_file("kencode_last.bin", KC_LAST_COMP_SIZE); } + + void TearDown() { free((void *)kc_last_buffer); } +}; + +TEST_F(KencodeLastFixture, kencode_last) +{ + auto *outBuf = (uint8_t *)malloc(KC_LAST_DECOMP_SIZE); + size_t destLen = KC_LAST_DECOMP_SIZE; + + auto err = AARU_kencode_decode_buffer(outBuf, &destLen, kc_last_buffer, KC_LAST_COMP_SIZE); + + EXPECT_EQ(err, 0); + EXPECT_EQ(destLen, (size_t)KC_LAST_DECOMP_SIZE); + + auto crc = crc32_data(outBuf, KC_LAST_DECOMP_SIZE); + free(outBuf); + + EXPECT_EQ(crc, KC_LAST_CRC32); +} + +/* ---- 5MB disk image chunk ---- */ + +static const uint8_t *kc_large_buffer; + +class KencodeLargeFixture : public ::testing::Test +{ +protected: + void SetUp() { kc_large_buffer = read_test_file("kencode_large.bin", KC_LARGE_COMP_SIZE); } + + void TearDown() { free((void *)kc_large_buffer); } +}; + +TEST_F(KencodeLargeFixture, kencode_large) +{ + auto *outBuf = (uint8_t *)malloc(KC_LARGE_DECOMP_SIZE); + size_t destLen = KC_LARGE_DECOMP_SIZE; + + auto err = AARU_kencode_decode_buffer(outBuf, &destLen, kc_large_buffer, KC_LARGE_COMP_SIZE); + + EXPECT_EQ(err, 0); + EXPECT_EQ(destLen, (size_t)KC_LARGE_DECOMP_SIZE); + + auto crc = crc32_data(outBuf, KC_LARGE_DECOMP_SIZE); + free(outBuf); + + EXPECT_EQ(crc, KC_LARGE_CRC32); +}