Skip to main content

lz4/
lib.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use std::num::NonZero;
6use std::os::raw::{c_char, c_int};
7
8unsafe extern "C" {
9    fn LZ4_compress_default(
10        src: *const c_char,
11        dst: *mut c_char,
12        srcSize: c_int,
13        dstCapacity: c_int,
14    ) -> c_int;
15    fn LZ4_decompress_safe(
16        src: *const c_char,
17        dst: *mut c_char,
18        compressedSize: c_int,
19        dstCapacity: c_int,
20    ) -> c_int;
21    fn LZ4_compressBound(inputSize: c_int) -> c_int;
22    fn LZ4_compress_HC(
23        src: *const c_char,
24        dst: *mut c_char,
25        srcSize: c_int,
26        dstCapacity: c_int,
27        compressionLevel: c_int,
28    ) -> c_int;
29}
30
31#[derive(thiserror::Error, Debug)]
32pub enum Error {
33    #[error("LZ4 only supports compressing up to 2016MiB")]
34    InputTooLarge,
35    #[error("LZ4 decompression failed")]
36    DecompressionFailed,
37}
38
39fn compress_bound(input_size: c_int) -> Result<NonZero<c_int>, Error> {
40    // SAFETY: no unsafe parameters.
41    let bound = unsafe { LZ4_compressBound(input_size) };
42    NonZero::<c_int>::new(bound).ok_or(Error::InputTooLarge)
43}
44
45/// Compresses the given data using LZ4.
46pub fn compress(data: &[u8]) -> Vec<u8> {
47    if data.is_empty() {
48        return Vec::new();
49    }
50    let src_size = data.len() as c_int;
51    let bound = unsafe { LZ4_compressBound(src_size) };
52    let mut compressed = vec![0u8; bound as usize];
53    let compressed_size = unsafe {
54        LZ4_compress_default(
55            data.as_ptr() as *const c_char,
56            compressed.as_mut_ptr() as *mut c_char,
57            src_size,
58            bound,
59        )
60    };
61    assert!(compressed_size > 0, "LZ4 compression failed");
62    compressed.truncate(compressed_size as usize);
63    compressed
64}
65
66/// Decompresses the given data using LZ4, expecting the given uncompressed size.
67pub fn decompress(data: &[u8], uncompressed_size: usize) -> Result<Vec<u8>, i32> {
68    let mut decompressed = vec![0u8; uncompressed_size];
69    let bytes_written = decompress_into(data, &mut decompressed).map_err(|_| -1)?;
70    if bytes_written != uncompressed_size {
71        return Err(-1);
72    }
73    Ok(decompressed)
74}
75
76/// Decompresses the given data into `destination` using LZ4. The number of bytes written to
77/// `destination` is returned.
78pub fn decompress_into(data: &[u8], destination: &mut [u8]) -> Result<usize, Error> {
79    if data.is_empty() {
80        return Ok(0);
81    }
82    if destination.is_empty() {
83        // The pointer of an empty byte slice in rust points to 0x1. LZ4 subtracts some constants
84        // from the pointer before checking if the output size is 0. The subtraction causes an
85        // overflow which is undefined behaviour and gets caught by asan.
86        //
87        // `data` is not empty and `destination` is empty so LZ4 would return an error because not
88        // all of `data` could be decompressed into `destination`.
89        return Err(Error::DecompressionFailed);
90    }
91    let result = unsafe {
92        LZ4_decompress_safe(
93            data.as_ptr() as *const c_char,
94            destination.as_mut_ptr() as *mut c_char,
95            data.len().try_into().map_err(|_| Error::InputTooLarge)?,
96            destination.len().try_into().map_err(|_| Error::InputTooLarge)?,
97        )
98    };
99    if result < 0 { Err(Error::DecompressionFailed) } else { Ok(result as usize) }
100}
101
102/// The compression level to use with `compress_hc`.
103#[derive(Copy, Clone)]
104pub struct HcCompressionLevel(i32);
105
106impl HcCompressionLevel {
107    pub const MIN: Self = Self(3);
108    pub const DEFAULT: Self = Self(9);
109    pub const OPT_MIN: Self = Self(10);
110    pub const MAX: Self = Self(12);
111
112    pub fn custom(level: i32) -> Self {
113        Self(level)
114    }
115}
116
117impl From<HcCompressionLevel> for i32 {
118    fn from(level: HcCompressionLevel) -> Self {
119        level.0
120    }
121}
122
123/// Compresses the given data using LZ4 HC.
124pub fn compress_hc(data: &[u8], compression_level: HcCompressionLevel) -> Result<Vec<u8>, Error> {
125    if data.is_empty() {
126        return Ok(Vec::new());
127    }
128    let data_size = i32::try_from(data.len()).map_err(|_| Error::InputTooLarge)?;
129    let bound = compress_bound(data_size)?.get();
130    let mut compressed = Vec::with_capacity(bound as usize);
131    // SAFETY:
132    //  1. u8, and MaybeUninit<u8> have the same size and alignment as c_char which makes reading
133    //     from and writing to the casted pointers safe.
134    //  2. When compression succeeds, LZ4 will have initialized all of the bytes up to
135    //     `compressed_size` making the `set_len` call safe.
136    unsafe {
137        let dst = compressed.spare_capacity_mut()[0..bound as usize].as_mut_ptr().cast::<c_char>();
138        let src = data.as_ptr().cast::<c_char>();
139        let compressed_size = LZ4_compress_HC(src, dst, data_size, bound, compression_level.into());
140        // Compression is guaranteed to succeed when the size of the destination buffer is at least
141        // `LZ4_compressBound(src_size)`.
142        assert!(compressed_size > 0, "LZ4 compression failed");
143        compressed.set_len(compressed_size as usize);
144    }
145    Ok(compressed)
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn test_roundtrip() {
154        let data = b"Hello, world! LZ4 compression test.";
155        let compressed = compress(data);
156        let decompressed = decompress(&compressed, data.len()).unwrap();
157        assert_eq!(data.as_slice(), decompressed.as_slice());
158    }
159
160    #[test]
161    fn test_empty() {
162        let data = b"";
163        let compressed = compress(data);
164        let decompressed = decompress(&compressed, 0).unwrap();
165        assert_eq!(data.as_slice(), decompressed.as_slice());
166    }
167
168    #[test]
169    fn test_compress_bound() {
170        assert!(compress_bound(0).is_ok());
171        assert!(compress_bound(20).is_ok());
172        assert!(compress_bound(i32::MAX).is_err());
173        assert!(compress_bound(0x7E000000).is_ok());
174        assert!(compress_bound(0x7E000000 + 1).is_err());
175    }
176
177    #[test]
178    fn test_hc_roundtrip() {
179        let data = b"Hello, world! LZ4 compression test.";
180        let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
181        let decompressed = decompress(&compressed, data.len()).unwrap();
182        assert_eq!(data.as_slice(), decompressed.as_slice());
183    }
184
185    #[test]
186    fn test_hc_empty_roundtrip() {
187        let data = b"";
188        let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
189        let decompressed = decompress(&compressed, data.len()).unwrap();
190        assert_eq!(data.as_slice(), decompressed.as_slice());
191    }
192    #[test]
193    fn test_decompress_into_zero_buffer() {
194        let data = b"Hello, world! LZ4 compression test.";
195        let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
196        decompress_into(&compressed, &mut Vec::new()).expect_err("buf should be too small");
197    }
198
199    #[test]
200    fn test_decompress_into_buffer_too_small() {
201        let data = b"Hello, world! LZ4 compression test.";
202        let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
203        let mut buf = vec![0; data.len() - 1];
204        decompress_into(&compressed, &mut buf).expect_err("buf should be too small");
205    }
206
207    #[test]
208    fn test_decompress_into_large_buffer() {
209        let data = b"Hello, world! LZ4 compression test.";
210        let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
211        let mut buf = vec![0; data.len() + 1];
212        let bytes_written = decompress_into(&compressed, &mut buf).unwrap();
213        assert_eq!(data.as_slice(), &buf[0..bytes_written]);
214    }
215
216    #[test]
217    fn test_decompress_into_exact_sized_buffer() {
218        let data = b"Hello, world! LZ4 compression test.";
219        let compressed = compress_hc(data, HcCompressionLevel::MAX).unwrap();
220        let mut buf = vec![0; data.len()];
221        let bytes_written = decompress_into(&compressed, &mut buf).unwrap();
222        assert_eq!(data.len(), bytes_written);
223        assert_eq!(data.as_slice(), buf.as_slice());
224    }
225
226    #[test]
227    fn test_empty_decompress_into_large_buffer() {
228        let mut buf = vec![0; 100];
229        let decompressed_bytes =
230            decompress_into(&[], &mut buf).expect("decompression should succeed");
231        assert_eq!(decompressed_bytes, 0);
232    }
233}