{{ message }}
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
common: share the bzip2 stream engine #8638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,324 @@ | ||
| // spell-checker:ignore chunker libbz | ||
| //! VM-independent bzip2 stream engine. | ||
| //! | ||
| //! The engine owns its native stream state and reports plain Rust errors so | ||
| //! interpreter and embedding layers can provide their own object and exception | ||
| //! adapters. | ||
|
|
||
| use bzip2::{Action, Compress, Compression, Decompress, Error, Status}; | ||
|
|
||
| use super::Chunker; | ||
|
|
||
| const INITIAL_BUFFER_SIZE: usize = 8192; | ||
| const BIGCHUNK: usize = 512 * 1024; | ||
|
|
||
| /// Double the output block until `BIGCHUNK`, then keep the size fixed. | ||
| const fn new_buffer_size(current_size: usize) -> usize { | ||
| if current_size < BIGCHUNK { | ||
| current_size + current_size | ||
| } else { | ||
| current_size | ||
| } | ||
| } | ||
|
|
||
| /// Stream errors reported by the bzip2 engine. Exception mapping belongs to | ||
| /// the interpreter adapter. | ||
| #[derive(Clone, Copy, PartialEq, Eq, Debug)] | ||
| pub enum Bz2Error { | ||
| /// `BZ_PARAM_ERROR` | ||
| Param, | ||
| /// `BZ_DATA_ERROR` / `BZ_DATA_ERROR_MAGIC` | ||
| Data, | ||
| /// `BZ_SEQUENCE_ERROR` | ||
| Sequence, | ||
| /// `BZ_MEM_ERROR` | ||
| Mem, | ||
| } | ||
|
|
||
| impl From<Error> for Bz2Error { | ||
| fn from(error: Error) -> Self { | ||
| match error { | ||
| Error::Param => Self::Param, | ||
| Error::Data | Error::DataMagic => Self::Data, | ||
| Error::Sequence => Self::Sequence, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Incremental compressor. After `flush`, the stream is finished and must | ||
| /// not be used again. | ||
| pub struct Compressor { | ||
| compress: Compress, | ||
| flushed: bool, | ||
| } | ||
|
|
||
| impl Compressor { | ||
| /// Create a compressor. Returns `None` when `compresslevel` is outside | ||
| /// `1..=9`. A zero work factor selects libbz2's default of 30. | ||
| pub fn new(compresslevel: i64) -> Option<Self> { | ||
| let level = u32::try_from(compresslevel) | ||
| .ok() | ||
| .and_then(Compression::try_new)?; | ||
| Some(Self { | ||
| compress: Compress::new(level, 0), | ||
| flushed: false, | ||
| }) | ||
| } | ||
|
|
||
| #[must_use] | ||
| pub fn is_flushed(&self) -> bool { | ||
| self.flushed | ||
| } | ||
|
|
||
| /// Compress `data` without finishing the stream. | ||
| pub fn compress(&mut self, data: &[u8]) -> Result<Vec<u8>, Bz2Error> { | ||
| self.run(data, Action::Run) | ||
| } | ||
|
|
||
| /// Finish the stream. The compressor must not be used again. | ||
| pub fn flush(&mut self) -> Result<Vec<u8>, Bz2Error> { | ||
| self.flushed = true; | ||
| self.run(&[], Action::Finish) | ||
| } | ||
|
|
||
| /// One pass over the input with the requested action, growing the output | ||
| /// block whenever libbz2 fills it. | ||
| fn run(&mut self, mut input: &[u8], action: Action) -> Result<Vec<u8>, Bz2Error> { | ||
| let mut out = Vec::new(); | ||
| let mut block = vec![0u8; INITIAL_BUFFER_SIZE]; | ||
| loop { | ||
| // In regular compression mode, stop when input data is exhausted. | ||
| if action == Action::Run && input.is_empty() { | ||
| break; | ||
| } | ||
| let previous_in = self.compress.total_in(); | ||
| let previous_out = self.compress.total_out(); | ||
| let status = self.compress.compress(input, &mut block, action)?; | ||
| let consumed = (self.compress.total_in() - previous_in) as usize; | ||
| let produced = (self.compress.total_out() - previous_out) as usize; | ||
| out.extend_from_slice(&block[..produced]); | ||
| input = &input[consumed..]; | ||
| // In flushing mode, stop when all buffered data has been flushed. | ||
| if action == Action::Finish && status == Status::StreamEnd { | ||
| break; | ||
| } | ||
| if produced == block.len() { | ||
| block = vec![0u8; new_buffer_size(block.len())]; | ||
| } | ||
| } | ||
| out.shrink_to_fit(); | ||
| Ok(out) | ||
| } | ||
| } | ||
|
|
||
| /// Incremental decompressor. The first failure is latched so later calls | ||
| /// can be refused by the caller; re-entering the native stream after a | ||
| /// failure can write out of bounds. | ||
| pub struct Decompressor { | ||
| decompress: Decompress, | ||
| eof: bool, | ||
| failed: bool, | ||
| needs_input: bool, | ||
| unused_data: Vec<u8>, | ||
| /// Input handed to `decompress` that libbz2 has not consumed yet. | ||
| input_buffer: Vec<u8>, | ||
| } | ||
|
|
||
| impl Decompressor { | ||
| /// Create a decompressor using the fast (non-`small`) algorithm. | ||
| #[must_use] | ||
| pub fn new() -> Self { | ||
| Self { | ||
| decompress: Decompress::new(false), | ||
| eof: false, | ||
| failed: false, | ||
| needs_input: true, | ||
| unused_data: Vec::new(), | ||
| input_buffer: Vec::new(), | ||
| } | ||
| } | ||
|
|
||
| #[must_use] | ||
| pub fn eof(&self) -> bool { | ||
| self.eof | ||
| } | ||
|
|
||
| #[must_use] | ||
| pub fn failed(&self) -> bool { | ||
| self.failed | ||
| } | ||
|
|
||
| #[must_use] | ||
| pub fn needs_input(&self) -> bool { | ||
| self.needs_input | ||
| } | ||
|
|
||
| #[must_use] | ||
| pub fn unused_data(&self) -> &[u8] { | ||
| &self.unused_data | ||
| } | ||
|
|
||
| /// Decompress from new input and any bytes buffered by an earlier call. | ||
| /// `max_length` of `None` means unlimited output. | ||
| pub fn decompress( | ||
| &mut self, | ||
| data: &[u8], | ||
| max_length: Option<usize>, | ||
| ) -> Result<Vec<u8>, Bz2Error> { | ||
| let max_length = max_length.unwrap_or(usize::MAX); | ||
| let mut out = Vec::new(); | ||
| let mut block = vec![0u8; INITIAL_BUFFER_SIZE.min(max_length)]; | ||
|
|
||
| let mut failed = None; | ||
| let mut stream_end = false; | ||
| let leftover = { | ||
| let mut chunks = Chunker::chain(&self.input_buffer, data); | ||
| loop { | ||
| let chunk = chunks.chunk(); | ||
| let previous_in = self.decompress.total_in(); | ||
| let previous_out = self.decompress.total_out(); | ||
| let status = self.decompress.decompress(chunk, &mut block); | ||
| let consumed = (self.decompress.total_in() - previous_in) as usize; | ||
| let produced = (self.decompress.total_out() - previous_out) as usize; | ||
| chunks.advance(consumed); | ||
| out.extend_from_slice(&block[..produced]); | ||
| match status { | ||
| Err(error) => { | ||
| failed = Some(error.into()); | ||
| break; | ||
| } | ||
| Ok(Status::MemNeeded) => { | ||
| failed = Some(Bz2Error::Mem); | ||
| break; | ||
| } | ||
| Ok(Status::StreamEnd) => { | ||
| stream_end = true; | ||
| break; | ||
| } | ||
| Ok(_) => {} | ||
| } | ||
| if chunks.is_empty() { | ||
| break; | ||
| } | ||
| if produced == block.len() { | ||
| // The output block is full: grow it unless `max_length` | ||
| // has already been reached. | ||
| if out.len() == max_length { | ||
| break; | ||
| } | ||
| block = vec![0u8; new_buffer_size(block.len()).min(max_length - out.len())]; | ||
| } | ||
| } | ||
| if chunks.is_empty() { | ||
| None | ||
| } else { | ||
| Some(chunks.to_vec()) | ||
| } | ||
| }; | ||
|
|
||
| if let Some(error) = failed { | ||
| return Err(self.fail(error)); | ||
| } | ||
|
|
||
| if stream_end { | ||
| self.eof = true; | ||
| self.needs_input = false; | ||
| self.input_buffer.clear(); | ||
| if let Some(unused) = leftover { | ||
| self.unused_data = unused; | ||
| } | ||
| } else if let Some(remaining) = leftover { | ||
| self.needs_input = false; | ||
| self.input_buffer = remaining; | ||
| } else { | ||
| self.needs_input = true; | ||
| self.input_buffer.clear(); | ||
| } | ||
| out.shrink_to_fit(); | ||
| Ok(out) | ||
| } | ||
|
|
||
| /// Latch the first failure and drop the pending input with it. | ||
| fn fail(&mut self, error: Bz2Error) -> Bz2Error { | ||
| self.failed = true; | ||
| self.needs_input = false; | ||
| self.input_buffer = Vec::new(); | ||
| error | ||
| } | ||
| } | ||
|
|
||
| impl Default for Decompressor { | ||
| fn default() -> Self { | ||
| Self::new() | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| fn roundtrip(data: &[u8], level: i64) -> Vec<u8> { | ||
| let mut compressor = Compressor::new(level).unwrap(); | ||
| let mut encoded = compressor.compress(data).unwrap(); | ||
| encoded.extend(compressor.flush().unwrap()); | ||
| let mut decompressor = Decompressor::new(); | ||
| let out = decompressor.decompress(&encoded, None).unwrap(); | ||
| assert!(decompressor.eof()); | ||
| out | ||
| } | ||
|
|
||
| #[test] | ||
| fn invalid_level_is_rejected() { | ||
| assert!(Compressor::new(0).is_none()); | ||
| assert!(Compressor::new(10).is_none()); | ||
| assert!(Compressor::new(-1).is_none()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn streaming_roundtrip() { | ||
| let data = b"the quick brown fox jumps over the lazy dog".repeat(50); | ||
| assert_eq!(roundtrip(&data, 9), data); | ||
| } | ||
|
|
||
| #[test] | ||
| fn unused_data_after_stream_end() { | ||
| let mut compressor = Compressor::new(9).unwrap(); | ||
| let mut encoded = compressor.compress(b"hello").unwrap(); | ||
| encoded.extend(compressor.flush().unwrap()); | ||
| encoded.extend_from_slice(b"trailing"); | ||
|
|
||
| let mut decompressor = Decompressor::new(); | ||
| let out = decompressor.decompress(&encoded, None).unwrap(); | ||
| assert_eq!(out, b"hello"); | ||
| assert!(decompressor.eof()); | ||
| assert!(!decompressor.needs_input()); | ||
| assert_eq!(decompressor.unused_data(), b"trailing"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn max_length_leaves_unconsumed_input() { | ||
| let data = b"abcdefghij".repeat(20); | ||
| let mut compressor = Compressor::new(9).unwrap(); | ||
| let mut encoded = compressor.compress(&data).unwrap(); | ||
| encoded.extend(compressor.flush().unwrap()); | ||
|
|
||
| let mut decompressor = Decompressor::new(); | ||
| let first = decompressor.decompress(&encoded, Some(5)).unwrap(); | ||
| assert_eq!(first.len(), 5); | ||
| assert!(!decompressor.eof()); | ||
| assert!(!decompressor.needs_input()); | ||
| let rest = decompressor.decompress(&[], None).unwrap(); | ||
| assert_eq!([first, rest].concat(), data); | ||
| assert!(decompressor.eof()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn bad_data_latches_failure() { | ||
| let mut decompressor = Decompressor::new(); | ||
| let err = decompressor.decompress(b"not a bz2 stream", None); | ||
| assert_eq!(err, Err(Bz2Error::Data)); | ||
| assert!(decompressor.failed()); | ||
| assert!(!decompressor.needs_input()); | ||
| assert!(!decompressor.eof()); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
You can’t perform that action at this time.

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 5821
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 21423
🌐 Web query:
bzip2-rs Decompress decompress output buffer full input exhausted BZ2_bzDecompress contract💡 Result:
In the context of the libbzip2 C library, which underpins many Rust wrappers (such as the bzip2 crate), the function BZ2_bzDecompress is designed to decompress data from a user-supplied input buffer to a user-supplied output buffer [1][2]. Regarding the behavior when the output buffer is full or input is exhausted: 1. BZ2_bzDecompress Contract: The function operates by transferring data between the provided input and output buffers. It stops when the input buffer is empty or the output buffer is full [1][3]. 2. Output Buffer Full (BZ_OUTBUFF_FULL): If the output buffer is insufficient to hold the decompressed data produced by the current input segment, the function returns BZ_OUTBUFF_FULL [1][4]. When this happens, you must empty the output buffer (or provide a new one) and call BZ2_bzDecompress again to continue decompression [1]. 3. Input Exhausted: If the input buffer is exhausted but the stream has not reached its end, the function typically returns BZ_OK (or similar) indicating that it requires more input [1][4]. You should replenish the input buffer and call the function again. 4. Handling in Rust: - The bzip2 crate (bindings to libbz2) provides a Decompress struct that manages these buffers [5][2]. Its decompress method returns a Status, which informs you if it needs more input or has filled the output buffer, requiring the caller to handle the state accordingly [5]. - The bzip2-rs crate (a pure Rust implementation) manages this internally through its Decoder interface, where Read and Write implementations abstract away the manual buffer management [6][7][8]. To ensure correct operation, you should repeatedly call the decompression function in a loop, checking the status or return code, until BZ_STREAM_END is returned, which signifies the logical end of the compressed stream and ensures all output has been fully processed [1][3].
Citations:
🏁 Script executed:
Repository: RustPython/RustPython
Length of output: 8772
Reorder the exhausted-input check after the output-full check.
When
chunks.is_empty()andproduced == block.len(), libbz2 can still have decoded output pending. The current order setsneeds_input = true, so_compression.DecompressReadercan raiseEOFErrorat end of file before returning that output.Handle a full output block first, then break when input is exhausted. Add a regression test for this case.
🤖 Prompt for AI Agents