Add CompactPro compression algorithms.

This commit is contained in:
2026-04-16 22:07:24 +01:00
parent eff7df7ce7
commit e3e6a874fd
9 changed files with 4191 additions and 2 deletions

View File

@@ -236,7 +236,9 @@ add_library("Aaru.Compression.Native" SHARED library.c apple_rle.c apple_rle.h a
ppmd/SubAllocatorVariantH.c
ppmd/SubAllocatorVariantH.h
ppmd/VariantH.c
ppmd/VariantH.h)
ppmd/VariantH.h
cpt/cpt.c
cpt/cpt.h)
include(3rdparty/bzip2.cmake)
include(3rdparty/flac.cmake)

370
cpt/cpt.c Normal file
View File

@@ -0,0 +1,370 @@
/*
* cpt.c - Compact Pro archive decompression
*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2026 Natalia Portillo.
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/
#include "cpt.h"
#include <stdlib.h>
#include <string.h>
#include "../pak/bitstream.h"
#include "../pak/prefixcode.h"
/* ============ Constants ============ */
#define CPT_WINDOW_SIZE 8192
#define CPT_BLOCK_SIZE 0x1fff0
#define CPT_RLE_ESCAPE 0x81
#define CPT_LITERAL_SYMS 256
#define CPT_LENGTH_SYMS 64
#define CPT_OFFSET_SYMS 128
#define CPT_MAX_CODELEN 15
#define CPT_OFFSET_LOW 6
/* ============ Bitstream helpers ============ */
/** Discard remaining bits in the current byte to align to a byte boundary. */
static void bs_align_to_byte(BitStream *bs)
{
int remainder = bs->bitcount & 7;
if(remainder > 0)
{
bs->bitbuffer <<= remainder;
bs->bitcount -= remainder;
}
}
/** Return the logical byte offset of consumed data (bytes read from source). */
static size_t bs_byte_offset(const BitStream *bs) { return bs->pos - (size_t)((bs->bitcount + 7) / 8); }
/** Skip n bytes through the bit buffer (must be at a byte boundary). */
static void bs_skip_bytes(BitStream *bs, int n)
{
for(int i = 0; i < n; i++) bitstream_read_bits(bs, 8);
}
/* ============ Huffman table reading ============ */
/**
* Read a canonical Huffman code table from the bitstream.
*
* Format: one byte giving the number of packed byte pairs, then that many
* bytes each containing two 4-bit code lengths (high nibble first).
* Maximum code length is 15. Uses shortest-code-is-zeros convention.
*
* @param bs Bitstream positioned at the start of the code table.
* @param size Number of symbols in the code alphabet.
* @return Allocated PrefixCode on success, NULL on error.
*/
static PrefixCode *cpt_read_code_table(BitStream *bs, int size)
{
int numbytes = (int)bitstream_read_bits(bs, 8);
if(numbytes * 2 > size) return NULL;
int *lengths = (int *)calloc((size_t)size, sizeof(int));
if(!lengths) return NULL;
for(int i = 0; i < numbytes; i++)
{
int val = (int)bitstream_read_bits(bs, 8);
lengths[2 * i] = val >> 4;
lengths[2 * i + 1] = val & 0x0f;
}
/* Remaining symbols have length 0 (no code assigned). */
PrefixCode *code = prefix_code_alloc_with_lengths(lengths, size, CPT_MAX_CODELEN, true);
free(lengths);
return code;
}
/* ============ LZH decoder ============ */
/**
* Decompress LZH-encoded data (block-based Huffman LZSS, 8 KB window).
*
* The output is the intermediate RLE-encoded stream that must be further
* processed by the RLE decoder to produce the final data.
*
* @param dst Output buffer.
* @param dst_cap Capacity of output buffer.
* @param src Input compressed data.
* @param src_size Size of input data.
* @param out_len Receives the number of bytes actually written.
* @return 0 on success, -1 on error.
*/
static int cpt_lzh_decode(uint8_t *dst, size_t dst_cap, const uint8_t *src, size_t src_size, size_t *out_len)
{
BitStream bs;
bitstream_init(&bs, src, src_size);
uint8_t window[CPT_WINDOW_SIZE];
memset(window, 0, sizeof(window));
size_t win_pos = 0;
size_t out_pos = 0;
int block_cost = CPT_BLOCK_SIZE; /* Force first block read immediately. */
size_t block_start = 0;
PrefixCode *literal_code = NULL;
PrefixCode *length_code = NULL;
PrefixCode *offset_code = NULL;
while(!bitstream_eof(&bs))
{
/* New block required? */
if(block_cost >= CPT_BLOCK_SIZE)
{
if(block_start)
{
/* Inter-block alignment padding:
* Align to byte boundary, then skip 2 or 3 bytes depending on
* whether the consumed byte count since block_start is odd or even. */
bs_align_to_byte(&bs);
size_t consumed = bs_byte_offset(&bs) - block_start;
if(consumed & 1)
bs_skip_bytes(&bs, 3);
else
bs_skip_bytes(&bs, 2);
}
/* Free previous tables. */
prefix_code_free(literal_code);
prefix_code_free(length_code);
prefix_code_free(offset_code);
literal_code = length_code = offset_code = NULL;
/* Read three Huffman code tables for this block. */
literal_code = cpt_read_code_table(&bs, CPT_LITERAL_SYMS);
length_code = cpt_read_code_table(&bs, CPT_LENGTH_SYMS);
offset_code = cpt_read_code_table(&bs, CPT_OFFSET_SYMS);
if(!literal_code || !length_code || !offset_code) goto fail;
block_cost = 0;
block_start = bs_byte_offset(&bs);
}
if(bitstream_eof(&bs)) break;
/* Read next symbol: bit=1 means literal, bit=0 means match. */
uint32_t flag = bitstream_read_bit(&bs);
if(flag)
{
/* Literal byte. */
int sym = prefix_code_read_symbol(&bs, literal_code);
if(sym < 0) goto fail;
if(out_pos >= dst_cap) goto fail;
uint8_t byte = (uint8_t)sym;
dst[out_pos++] = byte;
window[win_pos] = byte;
win_pos = (win_pos + 1) & (CPT_WINDOW_SIZE - 1);
block_cost += 2;
}
else
{
/* LZSS match. */
int match_len = prefix_code_read_symbol(&bs, length_code);
if(match_len < 0) goto fail;
int offset_high = prefix_code_read_symbol(&bs, offset_code);
if(offset_high < 0) goto fail;
int offset = (offset_high << CPT_OFFSET_LOW) | (int)bitstream_read_bits(&bs, CPT_OFFSET_LOW);
/* Copy from window. */
size_t copy_pos = (win_pos - offset) & (CPT_WINDOW_SIZE - 1);
for(int i = 0; i < match_len; i++)
{
if(out_pos >= dst_cap) goto fail;
uint8_t byte = window[copy_pos];
dst[out_pos++] = byte;
window[win_pos] = byte;
win_pos = (win_pos + 1) & (CPT_WINDOW_SIZE - 1);
copy_pos = (copy_pos + 1) & (CPT_WINDOW_SIZE - 1);
}
block_cost += 3;
}
}
*out_len = out_pos;
prefix_code_free(literal_code);
prefix_code_free(length_code);
prefix_code_free(offset_code);
return 0;
fail:
prefix_code_free(literal_code);
prefix_code_free(length_code);
prefix_code_free(offset_code);
return -1;
}
/* ============ RLE decoder ============ */
int cpt_rle_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) return -1;
size_t dst_cap = *dst_size;
size_t out_pos = 0;
size_t in_pos = 0;
uint8_t saved = 0;
int repeat = 0;
int halfescaped = 0;
while(out_pos < dst_cap)
{
/* Emit pending repeats first. */
if(repeat > 0)
{
repeat--;
if(out_pos >= dst_cap) break;
dst_buffer[out_pos++] = saved;
continue;
}
int byte;
if(halfescaped)
{
byte = CPT_RLE_ESCAPE;
halfescaped = 0;
}
else
{
if(in_pos >= src_size) break;
byte = src_buffer[in_pos++];
}
if(byte == CPT_RLE_ESCAPE)
{
if(in_pos >= src_size) break;
byte = src_buffer[in_pos++];
if(byte == 0x82)
{
if(in_pos >= src_size) break;
byte = src_buffer[in_pos++];
if(byte != 0)
{
/* Repeat previous byte: byte-2 additional copies. */
repeat = byte - 2;
if(out_pos >= dst_cap) break;
dst_buffer[out_pos++] = saved;
}
else
{
/* Literal 0x81 0x82 sequence. */
repeat = 1;
saved = 0x82;
if(out_pos >= dst_cap) break;
dst_buffer[out_pos++] = CPT_RLE_ESCAPE;
}
}
else if(byte == CPT_RLE_ESCAPE)
{
/* Escaped 0x81: output 0x81 and set half-escaped state. */
halfescaped = 1;
saved = CPT_RLE_ESCAPE;
if(out_pos >= dst_cap) break;
dst_buffer[out_pos++] = saved;
}
else
{
/* Literal 0x81 followed by this byte. */
repeat = 1;
saved = (uint8_t)byte;
if(out_pos >= dst_cap) break;
dst_buffer[out_pos++] = CPT_RLE_ESCAPE;
}
}
else
{
saved = (uint8_t)byte;
if(out_pos >= dst_cap) break;
dst_buffer[out_pos++] = saved;
}
}
*dst_size = out_pos;
return 0;
}
/* ============ Combined LZH + RLE decoder ============ */
int cpt_lzh_rle_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) return -1;
size_t final_size = *dst_size;
/* Intermediate buffer for LZH output (RLE-encoded data).
* The RLE encoding can at most double the data size (every 0x81 byte in
* the original needs escaping), so 2 × final_size is a safe upper bound. */
size_t intermediate_cap = final_size * 2 + 4096;
uint8_t *intermediate = (uint8_t *)malloc(intermediate_cap);
if(!intermediate) return -1;
size_t intermediate_len = 0;
int ret = cpt_lzh_decode(intermediate, intermediate_cap, src_buffer, src_size, &intermediate_len);
if(ret != 0)
{
free(intermediate);
return -1;
}
/* Now RLE-decode the intermediate data to produce the final output. */
*dst_size = final_size;
ret = cpt_rle_decode_buffer(dst_buffer, dst_size, intermediate, intermediate_len);
free(intermediate);
return ret;
}

54
cpt/cpt.h Normal file
View File

@@ -0,0 +1,54 @@
/*
* cpt.h - Compact Pro archive decompression
*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2026 Natalia Portillo.
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/
#ifndef AARU_CPT_H
#define AARU_CPT_H
#include <stddef.h>
#include <stdint.h>
/**
* Decompress Compact Pro RLE-encoded data.
*
* All Compact Pro entries use RLE encoding with escape byte 0x81.
*
* @param dst_buffer Output buffer for decompressed data.
* @param dst_size On input, size of dst_buffer. On output, actual bytes written.
* @param src_buffer Input RLE-encoded data.
* @param src_size Size of input data.
* @return 0 on success, -1 on error.
*/
int cpt_rle_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, const uint8_t *src_buffer, size_t src_size);
/**
* Decompress Compact Pro LZH+RLE-encoded data.
*
* LZH-compressed entries use block-based canonical Huffman coding with an
* 8192-byte LZSS sliding window, followed by RLE decoding.
*
* @param dst_buffer Output buffer for decompressed data.
* @param dst_size On input, size of dst_buffer. On output, actual bytes written.
* @param src_buffer Input LZH+RLE compressed data.
* @param src_size Size of input data.
* @return 0 on success, -1 on error.
*/
int cpt_lzh_rle_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, const uint8_t *src_buffer, size_t src_size);
#endif /* AARU_CPT_H */

View File

@@ -48,6 +48,7 @@
#include "3rdparty/lzo-2.10/include/lzo/lzodefs.h"
#include "3rdparty/zstd/lib/zstd.h"
#include "ace/ace.h"
#include "cpt/cpt.h"
#include "zip/zip.h"
AARU_EXPORT int32_t AARU_CALL AARU_bzip2_decode_buffer(uint8_t *dst_buffer, uint32_t *dst_size,
@@ -555,4 +556,14 @@ AARU_EXPORT int AARU_CALL AARU_zip_winzipjpeg_decode_buffer(uint8_t *dst_buffer,
const uint8_t *src_buffer, size_t src_size)
{ return zip_winzipjpeg_decompress(dst_buffer, dst_size, src_buffer, src_size); }
/* ============== Compact Pro Wrappers ============== */
AARU_EXPORT int AARU_CALL AARU_cpt_rle_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, const uint8_t *src_buffer,
size_t src_size)
{ return cpt_rle_decode_buffer(dst_buffer, dst_size, src_buffer, src_size); }
AARU_EXPORT int AARU_CALL AARU_cpt_lzh_rle_decode_buffer(uint8_t *dst_buffer, size_t *dst_size,
const uint8_t *src_buffer, size_t src_size)
{ return cpt_lzh_rle_decode_buffer(dst_buffer, dst_size, src_buffer, src_size); }
AARU_EXPORT uint64_t AARU_CALL AARU_get_acn_version() { return AARU_CHECKUMS_NATIVE_VERSION; }

View File

@@ -332,4 +332,12 @@ AARU_EXPORT int AARU_CALL rar30_decompress(const uint8_t *in_buf, size_t in_len,
AARU_EXPORT int AARU_CALL rar50_decompress(const uint8_t *in_buf, size_t in_len, uint8_t *out_buf, size_t *out_len,
size_t window_size);
// Compact Pro: RLE decompression (always applied)
AARU_EXPORT int AARU_CALL AARU_cpt_rle_decode_buffer(uint8_t *dst_buffer, size_t *dst_size, const uint8_t *src_buffer,
size_t src_size);
// Compact Pro: LZH + RLE decompression (block Huffman LZSS + RLE)
AARU_EXPORT int AARU_CALL AARU_cpt_lzh_rle_decode_buffer(uint8_t *dst_buffer, size_t *dst_size,
const uint8_t *src_buffer, size_t src_size);
#endif // AARU_COMPRESSION_NATIVE_LIBRARY_H

View File

@@ -147,6 +147,12 @@ file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/rar50_m4.bin
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/rar50_m5.bin
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/cpt_rle.bin
DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/data/)
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/data/cpt_lzh_rle.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
@@ -157,5 +163,6 @@ add_executable(tests_run apple_rle.cpp crc32.c crc32.h adc.cpp bzip2.cpp lzip.cp
arj/arj.cpp
arjz/arjz.cpp
zip/zip.cpp
rar/rar.cpp)
rar/rar.cpp
cpt/cpt.cpp)
target_link_libraries(tests_run gtest gtest_main "Aaru.Compression.Native")

109
tests/cpt/cpt.cpp Normal file
View File

@@ -0,0 +1,109 @@
/*
* This file is part of the Aaru Data Preservation Suite.
* Copyright (c) 2019-2026 Natalia Portillo.
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/
#include <unistd.h>
#include <climits>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include "../../library.h"
#include "../crc32.h"
#include "gtest/gtest.h"
#define EXPECTED_CRC32 0x66007DBA
#define EXPECTED_ORIGSIZE 152089
/* ---- RLE only (STORE.CPT data fork) ---- */
#define RLE_COMPRESSED_SIZE 150125
static const uint8_t *rle_buffer;
class CptRleFixture : public ::testing::Test
{
protected:
void SetUp()
{
char path[PATH_MAX];
char filename[PATH_MAX];
getcwd(path, PATH_MAX);
snprintf(filename, PATH_MAX, "%s/data/cpt_rle.bin", path);
FILE *file = fopen(filename, "rb");
rle_buffer = (const uint8_t *)malloc(RLE_COMPRESSED_SIZE);
fread((void *)rle_buffer, 1, RLE_COMPRESSED_SIZE, file);
fclose(file);
}
void TearDown() { free((void *)rle_buffer); }
};
TEST_F(CptRleFixture, cpt_rle)
{
size_t destLen = EXPECTED_ORIGSIZE;
auto *outBuf = (uint8_t *)malloc(EXPECTED_ORIGSIZE);
auto err = AARU_cpt_rle_decode_buffer(outBuf, &destLen, rle_buffer, RLE_COMPRESSED_SIZE);
EXPECT_EQ(err, 0);
EXPECT_EQ(destLen, (size_t)EXPECTED_ORIGSIZE);
auto crc = crc32_data(outBuf, EXPECTED_ORIGSIZE);
free(outBuf);
EXPECT_EQ(crc, EXPECTED_CRC32);
}
/* ---- LZH + RLE (DEFAULTS.CPT data fork) ---- */
#define LZH_RLE_COMPRESSED_SIZE 61101
static const uint8_t *lzh_rle_buffer;
class CptLzhRleFixture : public ::testing::Test
{
protected:
void SetUp()
{
char path[PATH_MAX];
char filename[PATH_MAX];
getcwd(path, PATH_MAX);
snprintf(filename, PATH_MAX, "%s/data/cpt_lzh_rle.bin", path);
FILE *file = fopen(filename, "rb");
lzh_rle_buffer = (const uint8_t *)malloc(LZH_RLE_COMPRESSED_SIZE);
fread((void *)lzh_rle_buffer, 1, LZH_RLE_COMPRESSED_SIZE, file);
fclose(file);
}
void TearDown() { free((void *)lzh_rle_buffer); }
};
TEST_F(CptLzhRleFixture, cpt_lzh_rle)
{
size_t destLen = EXPECTED_ORIGSIZE;
auto *outBuf = (uint8_t *)malloc(EXPECTED_ORIGSIZE);
auto err = AARU_cpt_lzh_rle_decode_buffer(outBuf, &destLen, lzh_rle_buffer, LZH_RLE_COMPRESSED_SIZE);
EXPECT_EQ(err, 0);
EXPECT_EQ(destLen, (size_t)EXPECTED_ORIGSIZE);
auto crc = crc32_data(outBuf, EXPECTED_ORIGSIZE);
free(outBuf);
EXPECT_EQ(crc, EXPECTED_CRC32);
}

BIN
tests/data/cpt_lzh_rle.bin Normal file

Binary file not shown.

3628
tests/data/cpt_rle.bin Normal file

File diff suppressed because it is too large Load Diff