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