bzip3_rs/lib.rs
1//! Pure-Rust port of the bzip3 compressor.
2//!
3//! This crate reads and writes the bzip3 format. It has no C code and no
4//! unsafe code. Output is compatible with C bzip3 in both directions.
5//!
6//! Each block passes through four stages: run-length coding, LZP,
7//! a Burrows-Wheeler transform, and an order-0 context-mixing coder.
8//! The crate checks a CRC32 value for each block.
9//!
10//! # Example
11//!
12//! ```
13//! use bzip3_rs::{BlockSize, compress, decompress};
14//!
15//! # fn main() -> Result<(), bzip3_rs::error::Error> {
16//! let data = b"hello world ".repeat(1000);
17//! let coded = compress(&data, BlockSize::DEFAULT)?;
18//! let plain = decompress(&coded)?;
19//! assert_eq!(plain, data);
20//! # Ok(())
21//! # }
22//! ```
23//!
24//! # Where to start
25//!
26//! - [`compress`] and [`decompress`] process a whole buffer.
27//! - [`compress_with_jobs`] and [`decompress_with_jobs`] process blocks in parallel. See [`Jobs`].
28//! - Module [`stream`] processes [`std::io::Read`] and [`std::io::Write`] values.
29//! - Modules [`mod@read`] and [`mod@write`] hold encoder and decoder adapters.
30//! - [`BlockEncoder`] and [`BlockDecoder`] process one block at a time.
31//! [`BlockDecoder::recover_block_into`] keeps the data of a damaged block
32//! and returns a [`BlockStatus`] value.
33//! - [`BlockSize`] holds the checked block size.
34//!
35//! # Frame formats
36//!
37//! The crate uses two frame formats. They do not mix.
38//!
39//! - The one-shot frame has a 13-byte header: magic, block size, and block count.
40//! [`compress`] and [`decompress`] use it.
41//! - The streaming frame has a 9-byte header: magic and block size.
42//! Blocks follow until the input ends.
43//! Module [`stream`], modules [`mod@read`] and [`mod@write`], and the `bzip3` tool use it.
44//!
45//! Each decoder reads only its own frame. Use the decoder that matches the encoder.
46//!
47//! ```
48//! use bzip3_rs::{BlockSize, decompress, stream};
49//!
50//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
51//! let mut coded = Vec::new();
52//! stream::compress_slice(b"hello world", &mut coded, BlockSize::MIN, bzip3_rs::Jobs::SERIAL)?;
53//!
54//! // `decompress` reads the one-shot frame, so it rejects a streaming frame.
55//! assert!(decompress(&coded).is_err());
56//! # Ok(())
57//! # }
58//! ```
59//!
60//! # Errors
61//!
62//! Functions, constructors, and `finish` methods return [`error::Result`].
63//!
64//! The `read` and `write` adapters implement [`std::io::Read`] and [`std::io::Write`].
65//! Their `read` and `write` methods return [`std::io::Error`].
66//! A codec fault has the kind [`std::io::ErrorKind::InvalidData`].
67//! The [`error::Error`] value is the source of that I/O error.
68//!
69//! ```
70//! use std::io::{Cursor, ErrorKind, Read};
71//! use bzip3_rs::error::Error;
72//! use bzip3_rs::{BlockSize, Jobs, read, stream};
73//!
74//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
75//! let mut coded = Vec::new();
76//! stream::compress_slice(&b"hello world ".repeat(1000), &mut coded, BlockSize::MIN, Jobs::SERIAL)?;
77//! let middle = coded.len() / 2;
78//! coded[middle] ^= 0xFF; // Damage one payload byte.
79//!
80//! let mut decoder = read::Decoder::new(Cursor::new(coded.as_slice()))?;
81//! let fault = decoder.read_to_end(&mut Vec::new()).unwrap_err();
82//! assert_eq!(fault.kind(), ErrorKind::InvalidData);
83//! let codec = fault.get_ref().and_then(|source| source.downcast_ref::<Error>());
84//! assert!(codec.is_some());
85//! # Ok(())
86//! # }
87//! ```
88//!
89//! # Parallelism
90//!
91//! Every [`Jobs`] value gives the same output bytes.
92//! More threads use more memory, because each thread holds one block.
93//!
94//! # Cargo features
95//!
96//! The library needs no feature. Feature `cli` builds the `bzip3` tool.
97
98#![forbid(unsafe_code)]
99// Decision on the review `deny(warnings)` claim: ticket 01 asks for
100// a "deny-by-default lint set", which is exactly the clippy denies
101// below. No crate-wide `deny(warnings)` follows on purpose: a
102// library deny would break downstream builds on newer toolchains.
103#![deny(clippy::all, clippy::pedantic)]
104#![warn(clippy::nursery)]
105#![warn(clippy::cognitive_complexity)]
106#![warn(missing_debug_implementations)]
107#![warn(missing_docs)]
108
109mod batch;
110mod block;
111mod block_size;
112mod buffer;
113mod cm;
114mod crc32;
115pub mod error;
116mod frame;
117mod jobs;
118mod lzp;
119pub mod read;
120mod rle;
121mod sais;
122pub mod stream;
123pub mod write;
124
125/// Run-length stage access for benches. Not part of the public API.
126#[cfg(feature = "bench-internals")]
127#[doc(hidden)]
128pub mod rle_bench {
129 pub use crate::rle::decode_into;
130 pub use crate::rle::encode_into;
131}
132
133/// Suffix-array stage access for benches. Not part of the public API.
134#[cfg(feature = "bench-internals")]
135#[doc(hidden)]
136pub mod sais_bench {
137 pub use crate::sais::BwtScratch;
138 pub use crate::sais::UnbwtScratch;
139 pub use crate::sais::bwt_with;
140 pub use crate::sais::unbwt_with;
141}
142
143pub use block::BlockDecoder;
144pub use block::BlockEncoder;
145pub use block::BlockStatus;
146pub use block_size::BlockSize;
147pub use frame::BlockHeader;
148pub use frame::StreamHeader;
149pub use jobs::Jobs;
150pub use jobs::default_workers;
151
152/// Magic bytes opening every frame.
153///
154/// # Examples
155///
156/// ```
157/// use bzip3_rs::{BlockSize, MAGIC_NUMBER, compress};
158///
159/// # fn main() -> Result<(), bzip3_rs::error::Error> {
160/// let coded = compress(b"hello world", BlockSize::DEFAULT)?;
161/// assert!(coded.starts_with(&MAGIC_NUMBER));
162/// # Ok(())
163/// # }
164/// ```
165pub const MAGIC_NUMBER: [u8; 5] = *b"BZ3v1";
166
167/// Compress `data` into one-shot framed output.
168///
169/// The header records the effective block size. This is the requested size.
170/// When the input is smaller, it is `compress_bound(data.len())` with a minimum of 65 KiB.
171/// Every block runs the full encode pipeline through one reused
172/// encoder state. The output grows as needed, so no capacity error occurs.
173///
174/// # Errors
175///
176/// Returns [`Error::InvalidBlockSize`](error::Error::InvalidBlockSize)
177/// when the clamped size leaves the valid range.
178/// Returns [`Error::OutOfMemory`](error::Error::OutOfMemory)
179/// when the encoder state allocation fails.
180/// Returns [`Error::BlockTooLarge`](error::Error::BlockTooLarge)
181/// when a block exceeds the effective size.
182/// Returns [`Error::BwtFailed`](error::Error::BwtFailed)
183/// when a block transform fails.
184///
185/// # Examples
186///
187/// ```
188/// use bzip3_rs::{BlockSize, compress, decompress};
189///
190/// # fn main() -> Result<(), bzip3_rs::error::Error> {
191/// let data = b"hello world ".repeat(1000);
192/// let coded = compress(&data, BlockSize::DEFAULT)?;
193/// assert!(coded.len() < data.len());
194/// assert_eq!(decompress(&coded)?, data);
195/// # Ok(())
196/// # }
197/// ```
198#[must_use = "compressed output must be used or stored"]
199#[inline]
200pub fn compress(data: &[u8], block_size: BlockSize) -> error::Result<Vec<u8>> {
201 frame::encode_frame(data, block_size, Jobs::SERIAL)
202}
203
204/// Compress `data` into one-shot framed output on a thread pool.
205///
206/// Output bytes are identical to [`compress`] at every job count.
207/// `jobs` selects the thread count. See [`Jobs`].
208///
209/// # Errors
210///
211/// Returns the errors of [`compress`]. Returns
212/// [`Error::ThreadPoolFailed`](error::Error::ThreadPoolFailed) when the private pool
213/// fails to build.
214///
215/// # Examples
216///
217/// ```
218/// use bzip3_rs::{BlockSize, Jobs, compress, compress_with_jobs};
219///
220/// # fn main() -> Result<(), bzip3_rs::error::Error> {
221/// let data = b"hello world ".repeat(1000);
222/// let serial = compress(&data, BlockSize::DEFAULT)?;
223/// let parallel = compress_with_jobs(&data, BlockSize::DEFAULT, Jobs::from_count(2))?;
224/// assert_eq!(serial, parallel);
225/// # Ok(())
226/// # }
227/// ```
228#[must_use = "compressed output must be used or stored"]
229#[inline]
230pub fn compress_with_jobs(
231 data: &[u8],
232 block_size: BlockSize,
233 jobs: Jobs,
234) -> error::Result<Vec<u8>> {
235 frame::encode_frame(data, block_size, jobs)
236}
237
238/// Decompress one-shot framed input.
239///
240/// Trailing bytes after the last block are ignored. The output grows
241/// as needed, so no capacity error occurs.
242///
243/// # Errors
244///
245/// Returns [`error::Error::TruncatedHeader`] on a short header,
246/// [`error::Error::InvalidMagic`] on bad magic,
247/// [`error::Error::InvalidHeader`] on bad sizes,
248/// [`error::Error::TruncatedData`] on a short payload,
249/// [`error::Error::InvalidBlockSize`] on an invalid header block size, and the
250/// block error when a payload fails to decode.
251///
252/// # Examples
253///
254/// ```
255/// use bzip3_rs::error::Error;
256/// use bzip3_rs::{BlockSize, compress, decompress};
257///
258/// # fn main() -> Result<(), Error> {
259/// let data = b"hello world ".repeat(1000);
260/// let coded = compress(&data, BlockSize::DEFAULT)?;
261/// assert_eq!(decompress(&coded)?, data);
262///
263/// // Input with a wrong magic number fails.
264/// let bad = [0_u8; 13];
265/// assert!(matches!(decompress(&bad), Err(Error::InvalidMagic)));
266/// # Ok(())
267/// # }
268/// ```
269#[must_use = "decompressed output must be used or stored"]
270#[inline]
271pub fn decompress(data: &[u8]) -> error::Result<Vec<u8>> {
272 frame::decode_frame(data, Jobs::SERIAL)
273}
274
275/// Decompress one-shot framed input on a thread pool.
276///
277/// Output bytes and errors are identical to [`decompress`] at every
278/// job count. `jobs` follows the rule of [`compress_with_jobs`].
279///
280/// # Errors
281///
282/// Returns the errors of [`decompress`]. Returns
283/// [`error::Error::ThreadPoolFailed`] when the private pool fails to build.
284///
285/// # Examples
286///
287/// ```
288/// use bzip3_rs::{BlockSize, Jobs, compress, decompress_with_jobs};
289///
290/// # fn main() -> Result<(), bzip3_rs::error::Error> {
291/// let data = b"hello world ".repeat(1000);
292/// let coded = compress(&data, BlockSize::DEFAULT)?;
293/// assert_eq!(decompress_with_jobs(&coded, Jobs::Global)?, data);
294/// # Ok(())
295/// # }
296/// ```
297#[must_use = "decompressed output must be used or stored"]
298#[inline]
299pub fn decompress_with_jobs(data: &[u8], jobs: Jobs) -> error::Result<Vec<u8>> {
300 frame::decode_frame(data, jobs)
301}
302
303/// Estimate the largest compressed output for an input of `input_size` bytes.
304///
305/// Matches the reference bound: `input_size + input_size / 50 + 32`. Saturates
306/// instead of wrapping on huge inputs, so the call never panics.
307///
308/// # Examples
309///
310/// ```
311/// use bzip3_rs::{BlockSize, compress, compress_bound};
312///
313/// # fn main() -> Result<(), bzip3_rs::error::Error> {
314/// let data = b"hello world ".repeat(1000);
315/// let coded = compress(&data, BlockSize::DEFAULT)?;
316/// assert!(coded.len() <= compress_bound(data.len()));
317/// assert_eq!(compress_bound(1000), 1052);
318/// # Ok(())
319/// # }
320/// ```
321#[must_use]
322#[inline]
323pub const fn compress_bound(input_size: usize) -> usize {
324 input_size
325 .saturating_add(input_size / 50)
326 .saturating_add(32)
327}
328
329#[cfg(test)]
330mod tests {
331 use super::compress_bound;
332
333 #[test]
334 fn compress_bound_adds_fiftieth_plus_32() {
335 assert_eq!(compress_bound(0), 32);
336 assert_eq!(compress_bound(1), 33);
337 assert_eq!(compress_bound(50), 83);
338 assert_eq!(compress_bound(100), 134);
339 assert_eq!(compress_bound(1_000_000), 1_020_032);
340 }
341
342 #[test]
343 fn compress_bound_saturates_on_huge_input() {
344 assert_eq!(compress_bound(usize::MAX), usize::MAX);
345 }
346}
347
348#[cfg(doctest)]
349#[doc = include_str!("../README.md")]
350struct ReadmeDoctests;