use omnizip_filters::Filter;
use crate::codec::composite::{decompress_then_filter, filter_then_compress};
use crate::codec::{Codec, CodecTunables, CoreError, CODEC_LZ4, CODEC_ZSTD};
pub const CODEC_BCJ_X86_LZ4: u8 = 0x20;
pub const CODEC_BCJ_X86_ZSTD: u8 = 0x21;
pub const CODEC_BCJ_ARM64_LZ4: u8 = 0x23;
pub const CODEC_BCJ_ARM64_ZSTD: u8 = 0x24;
const MIN_BCJ_SIZE: usize = 1024;
macro_rules! bcj_composite_codec {
($struct_name:ident, $codec_id:ident, $name:expr, $filter:expr, $inner:ident, $label:expr) => {
pub struct $struct_name;
impl Codec for $struct_name {
fn id(&self) -> u8 {
$codec_id
}
fn name(&self) -> &'static str {
$name
}
fn min_compress_size(&self) -> usize {
MIN_BCJ_SIZE
}
fn compress(&self, plaintext: &[u8]) -> Result<Vec<u8>, CoreError> {
filter_then_compress(plaintext, &$filter, $inner)
}
fn decompress(
&self,
compressed: &[u8],
_expected_len: u32,
) -> Result<Vec<u8>, CoreError> {
decompress_then_filter(compressed, &$filter, $inner, $label)
}
fn compress_with_tunables(
&self,
plaintext: &[u8],
_t: &CodecTunables,
) -> Result<Vec<u8>, CoreError> {
self.compress(plaintext)
}
}
};
}
bcj_composite_codec!(
BcjX86Lz4Codec,
CODEC_BCJ_X86_LZ4,
"bcj-x86+lz4",
omnizip_filters::BcjX86Filter,
CODEC_LZ4,
"bcj-x86+lz4"
);
bcj_composite_codec!(
BcjX86ZstdCodec,
CODEC_BCJ_X86_ZSTD,
"bcj-x86+zstd",
omnizip_filters::BcjX86Filter,
CODEC_ZSTD,
"bcj-x86+zstd"
);
bcj_composite_codec!(
BcjArm64Lz4Codec,
CODEC_BCJ_ARM64_LZ4,
"bcj-arm64+lz4",
omnizip_filters::BcjArm64Filter,
CODEC_LZ4,
"bcj-arm64+lz4"
);
bcj_composite_codec!(
BcjArm64ZstdCodec,
CODEC_BCJ_ARM64_ZSTD,
"bcj-arm64+zstd",
omnizip_filters::BcjArm64Filter,
CODEC_ZSTD,
"bcj-arm64+zstd"
);
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_x86_calls(size_bytes: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(size_bytes);
let targets: [u32; 4] = [0x401000, 0x401234, 0x402000, 0x405060];
let mut i = 0u32;
while out.len() + 5 <= size_bytes {
let opcode = if i & 1 == 0 { 0xE8 } else { 0xE9 };
let target = targets[(i as usize) & 3];
let here = out.len() as u32 + 5;
let rel: i32 = (target as i32) - (here as i32);
out.push(opcode);
out.extend_from_slice(&rel.to_le_bytes());
i += 1;
}
out
}
#[test]
fn bcj_x86_lz4_round_trips() {
let input = synthetic_x86_calls(64 * 1024);
let codec = BcjX86Lz4Codec;
let compressed = codec.compress(&input).expect("compress");
let recovered = codec
.decompress(&compressed, input.len() as u32)
.expect("decompress");
assert_eq!(recovered, input);
}
#[test]
fn bcj_x86_lz4_beats_plain_lz4_on_synthetic_exec() {
let input = synthetic_x86_calls(64 * 1024);
let plain = crate::codec::compress(CODEC_LZ4, &input).expect("plain lz4");
let bcj = BcjX86Lz4Codec.compress(&input).expect("bcj+lz4");
assert!(
bcj.len() < plain.len(),
"BCJ+LZ4 ({}) should beat plain LZ4 ({}) on synthetic x86 calls",
bcj.len(),
plain.len()
);
}
#[test]
fn bcj_x86_zstd_round_trips() {
let input = synthetic_x86_calls(32 * 1024);
let codec = BcjX86ZstdCodec;
let compressed = codec.compress(&input).expect("compress");
let recovered = codec
.decompress(&compressed, input.len() as u32)
.expect("decompress");
assert_eq!(recovered, input);
}
#[test]
fn bcj_arm64_lz4_round_trips() {
let input = synthetic_x86_calls(32 * 1024);
let codec = BcjArm64Lz4Codec;
let compressed = codec.compress(&input).expect("compress");
let recovered = codec
.decompress(&compressed, input.len() as u32)
.expect("decompress");
assert_eq!(recovered, input);
}
#[test]
fn bcj_x86_lz4_handles_tiny_input() {
let input = b"hello world";
let codec = BcjX86Lz4Codec;
let compressed = codec.compress(input).expect("compress");
let recovered = codec
.decompress(&compressed, input.len() as u32)
.expect("decompress");
assert_eq!(recovered.as_slice(), &input[..]);
}
}