blob: 66d48691c5704fb80d00cd7c5f993ac134fde822 [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::num::NonZero;
use std::os::raw::{c_char, c_int};
unsafe extern "C" {
fn LZ4_compress_default(
src: *const c_char,
dst: *mut c_char,
srcSize: c_int,
dstCapacity: c_int,
) -> c_int;
fn LZ4_decompress_safe(
src: *const c_char,
dst: *mut c_char,
compressedSize: c_int,
dstCapacity: c_int,
) -> c_int;
fn LZ4_compressBound(inputSize: c_int) -> c_int;
fn LZ4_compress_HC(
src: *const c_char,
dst: *mut c_char,
srcSize: c_int,
dstCapacity: c_int,
compressionLevel: c_int,
) -> c_int;
}
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
pub enum Error {
#[error("LZ4 only supports compressing up to 2016MiB")]
InputTooLarge,
#[error("LZ4 decompression failed")]
DecompressionFailed,
}
fn compress_bound(input_size: c_int) -> Result<NonZero<c_int>, Error> {
// SAFETY: no unsafe parameters.
let bound = unsafe { LZ4_compressBound(input_size) };
NonZero::<c_int>::new(bound).ok_or(Error::InputTooLarge)
}
/// Compresses the given data using LZ4.
pub fn compress(data: &[u8]) -> Vec<u8> {
if data.is_empty() {
return Vec::new();
}
let src_size = data.len() as c_int;
let bound = unsafe { LZ4_compressBound(src_size) };
let mut compressed = vec![0u8; bound as usize];
let compressed_size = unsafe {
LZ4_compress_default(
data.as_ptr() as *const c_char,
compressed.as_mut_ptr() as *mut c_char,
src_size,
bound,
)
};
assert!(compressed_size > 0, "LZ4 compression failed");
compressed.truncate(compressed_size as usize);
compressed
}
/// Decompresses the given data using LZ4, expecting the given uncompressed size.
pub fn decompress(data: &[u8], uncompressed_size: usize) -> Result<Vec<u8>, i32> {
let mut decompressed = vec![0u8; uncompressed_size];
let bytes_written = decompress_into(data, &mut decompressed).map_err(|_| -1)?;
if bytes_written != uncompressed_size {
return Err(-1);
}
Ok(decompressed)
}
/// Decompresses the given data into `destination` using LZ4. The number of bytes written to
/// `destination` is returned.
pub fn decompress_into(data: &[u8], destination: &mut [u8]) -> Result<usize, Error> {
if data.is_empty() {
return Ok(0);
}
if destination.is_empty() {
// The pointer of an empty byte slice in rust points to 0x1. LZ4 subtracts some constants
// from the pointer before checking if the output size is 0. The subtraction causes an
// overflow which is undefined behaviour and gets caught by asan.
//
// `data` is not empty and `destination` is empty so LZ4 would return an error because not
// all of `data` could be decompressed into `destination`.
return Err(Error::DecompressionFailed);
}
let result = unsafe {
LZ4_decompress_safe(
data.as_ptr() as *const c_char,
destination.as_mut_ptr() as *mut c_char,
data.len().try_into().map_err(|_| Error::InputTooLarge)?,
destination.len().try_into().map_err(|_| Error::InputTooLarge)?,
)
};
if result < 0 { Err(Error::DecompressionFailed) } else { Ok(result as usize) }
}
/// The compression level to use with `compress_hc`.
#[derive(Copy, Clone)]
pub struct HcCompressionLevel(i32);
impl HcCompressionLevel {
pub const MIN: Self = Self(3);
pub const DEFAULT: Self = Self(9);
pub const OPT_MIN: Self = Self(10);
pub const MAX: Self = Self(12);
pub fn custom(level: i32) -> Self {
Self(level)
}
}
impl From<HcCompressionLevel> for i32 {
fn from(level: HcCompressionLevel) -> Self {
level.0
}
}
/// Compresses the given data using LZ4 HC.
pub fn compress_hc(data: &[u8], compression_level: HcCompressionLevel) -> Result<Vec<u8>, Error> {
if data.is_empty() {
return Ok(Vec::new());
}
let data_size = i32::try_from(data.len()).map_err(|_| Error::InputTooLarge)?;
let bound = compress_bound(data_size)?.get();
let mut compressed = Vec::with_capacity(bound as usize);
// SAFETY:
// 1. u8, and MaybeUninit<u8> have the same size and alignment as c_char which makes reading
// from and writing to the casted pointers safe.
// 2. When compression succeeds, LZ4 will have initialized all of the bytes up to
// `compressed_size` making the `set_len` call safe.
unsafe {
let dst = compressed.spare_capacity_mut()[0..bound as usize].as_mut_ptr().cast::<c_char>();
let src = data.as_ptr().cast::<c_char>();
let compressed_size = LZ4_compress_HC(src, dst, data_size, bound, compression_level.into());
// Compression is guaranteed to succeed when the size of the destination buffer is at least
// `LZ4_compressBound(src_size)`.
assert!(compressed_size > 0, "LZ4 compression failed");
compressed.set_len(compressed_size as usize);
}
Ok(compressed)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_roundtrip() {
let data = b"Hello, world! LZ4 compression test.";
let compressed = compress(data);
let decompressed = decompress(&compressed, data.len()).unwrap();
assert_eq!(data.as_slice(), decompressed.as_slice());
}
#[test]
fn test_empty() {
let data = b"";
let compressed = compress(data);
let decompressed = decompress(&compressed, 0).unwrap();
assert_eq!(data.as_slice(), decompressed.as_slice());
}
#[test]
fn test_compress_bound() {
assert!(compress_bound(0).is_ok());
assert!(compress_bound(20).is_ok());
assert!(compress_bound(i32::MAX).is_err());
assert!(compress_bound(0x7E000000).is_ok());
assert!(compress_bound(0x7E000000 + 1).is_err());
}
#[test]
fn test_hc_roundtrip() {
let data = b"Hello, world! LZ4 compression test.";
let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
let decompressed = decompress(&compressed, data.len()).unwrap();
assert_eq!(data.as_slice(), decompressed.as_slice());
}
#[test]
fn test_hc_empty_roundtrip() {
let data = b"";
let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
let decompressed = decompress(&compressed, data.len()).unwrap();
assert_eq!(data.as_slice(), decompressed.as_slice());
}
#[test]
fn test_decompress_into_zero_buffer() {
let data = b"Hello, world! LZ4 compression test.";
let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
decompress_into(&compressed, &mut Vec::new()).expect_err("buf should be too small");
}
#[test]
fn test_decompress_into_buffer_too_small() {
let data = b"Hello, world! LZ4 compression test.";
let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
let mut buf = vec![0; data.len() - 1];
decompress_into(&compressed, &mut buf).expect_err("buf should be too small");
}
#[test]
fn test_decompress_into_large_buffer() {
let data = b"Hello, world! LZ4 compression test.";
let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
let mut buf = vec![0; data.len() + 1];
let bytes_written = decompress_into(&compressed, &mut buf).unwrap();
assert_eq!(data.as_slice(), &buf[0..bytes_written]);
}
#[test]
fn test_decompress_into_exact_sized_buffer() {
let data = b"Hello, world! LZ4 compression test.";
let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
let mut buf = vec![0; data.len()];
let bytes_written = decompress_into(&compressed, &mut buf).unwrap();
assert_eq!(data.len(), bytes_written);
assert_eq!(data.as_slice(), buf.as_slice());
}
#[test]
fn test_empty_decompress_into_large_buffer() {
let mut buf = vec![0; 100];
let decompressed_bytes =
decompress_into(&[], &mut buf).expect("decompression should succeed");
assert_eq!(decompressed_bytes, 0);
}
}