Skip to main content

bzip3_rs/
frame.rs

1//! One-shot 13-byte framed format plus streaming 9-byte framed format.
2//!
3//! The one-shot header holds the magic, the effective block size, and
4//! the block count. Each block holds its compressed size, its original
5//! size, and the payload. Empty input yields zero blocks. Trailing
6//! bytes after the last block are ignored on decode.
7//!
8//! The streaming header holds only the magic and the configured block
9//! size. Blocks follow until the input ends. The streaming decoder
10//! rejects trailing bytes that do not form a full valid block.
11
12use crate::BlockSize;
13use crate::Jobs;
14use crate::MAGIC_NUMBER;
15use crate::block::BlockDecoder;
16use crate::block::BlockEncoder;
17use crate::compress_bound;
18use crate::error::Error;
19use crate::error::Result;
20use crate::stream::decode_blocks;
21use crate::stream::encode_blocks;
22use crate::stream::run_in_pool;
23
24/// Length of the frame header (magic plus two sizes).
25pub const ONE_SHOT_HEADER_LEN: usize = 13;
26
27/// Sizes of one framed block, shared by both framings.
28///
29/// Each block on the wire holds the compressed size, the original
30/// size, and the payload, in that order.
31///
32/// # Examples
33///
34/// ```
35/// use bzip3_rs::BlockHeader;
36///
37/// let header = BlockHeader::new(10, 100);
38/// let bytes = header.to_bytes();
39/// assert_eq!(bytes.len(), BlockHeader::LEN);
40/// assert_eq!(BlockHeader::from_bytes(&bytes), header);
41/// ```
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43pub struct BlockHeader {
44    /// Compressed payload bytes after this header.
45    pub compressed_size: u32,
46    /// Original bytes the payload decodes to.
47    pub original_size: u32,
48}
49
50impl BlockHeader {
51    /// Length of one block header on the wire.
52    pub const LEN: usize = 8;
53
54    /// Build a block header from both sizes.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use bzip3_rs::BlockHeader;
60    ///
61    /// let header = BlockHeader::new(10, 100);
62    /// assert_eq!(header.compressed_size, 10);
63    /// assert_eq!(header.original_size, 100);
64    /// ```
65    #[must_use]
66    #[inline]
67    pub const fn new(compressed_size: u32, original_size: u32) -> Self {
68        Self {
69            compressed_size,
70            original_size,
71        }
72    }
73
74    /// Build the header for one encoded block.
75    ///
76    /// # Errors
77    ///
78    /// Returns [`Error::BlockTooLarge`] when a size does not fit 32 bits.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use bzip3_rs::BlockHeader;
84    ///
85    /// # fn main() -> Result<(), bzip3_rs::error::Error> {
86    /// let header = BlockHeader::for_block(10, 100)?;
87    /// assert_eq!(header, BlockHeader::new(10, 100));
88    /// # Ok(())
89    /// # }
90    /// ```
91    #[inline]
92    pub fn for_block(payload_len: usize, original_len: usize) -> Result<Self> {
93        let comp = u32::try_from(payload_len).map_err(|_| Error::BlockTooLarge)?;
94        let orig = u32::try_from(original_len).map_err(|_| Error::BlockTooLarge)?;
95        Ok(Self::new(comp, orig))
96    }
97
98    /// Encode the header as 8 little-endian bytes.
99    ///
100    /// # Examples
101    ///
102    /// ```
103    /// use bzip3_rs::BlockHeader;
104    ///
105    /// let bytes = BlockHeader::new(1, 2).to_bytes();
106    /// assert_eq!(bytes, [1, 0, 0, 0, 2, 0, 0, 0]);
107    /// ```
108    #[must_use]
109    #[inline]
110    pub const fn to_bytes(self) -> [u8; 8] {
111        let comp = self.compressed_size.to_le_bytes();
112        let orig = self.original_size.to_le_bytes();
113        [
114            comp[0], comp[1], comp[2], comp[3], orig[0], orig[1], orig[2], orig[3],
115        ]
116    }
117
118    /// Decode a header from 8 wire bytes.
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use bzip3_rs::BlockHeader;
124    ///
125    /// let header = BlockHeader::from_bytes(&[1, 0, 0, 0, 2, 0, 0, 0]);
126    /// assert_eq!(header, BlockHeader::new(1, 2));
127    /// ```
128    #[must_use]
129    #[inline]
130    pub const fn from_bytes(bytes: &[u8; 8]) -> Self {
131        Self {
132            compressed_size: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
133            original_size: u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
134        }
135    }
136}
137
138/// One parsed streaming block borrowing its payload.
139pub struct FramedBlock<'a> {
140    /// Compressed and original sizes from the wire.
141    pub header: BlockHeader,
142    /// Payload bytes (`compressed_size` long).
143    pub payload: &'a [u8],
144}
145
146/// Streaming frame header: the magic bytes and the block size.
147///
148/// The header is 9 bytes long. It records the requested block size
149/// verbatim. No clamp applies: short inputs still encode under the
150/// configured size. Blocks follow it until the input ends.
151///
152/// # Examples
153///
154/// ```
155/// use bzip3_rs::{BlockSize, MAGIC_NUMBER, StreamHeader};
156///
157/// # fn main() -> Result<(), bzip3_rs::error::Error> {
158/// let header = StreamHeader::new(BlockSize::MIN);
159/// let bytes = header.to_bytes();
160/// assert_eq!(bytes.len(), StreamHeader::LEN);
161/// assert!(bytes.starts_with(&MAGIC_NUMBER));
162/// assert_eq!(StreamHeader::from_bytes(&bytes)?, header);
163/// # Ok(())
164/// # }
165/// ```
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
167pub struct StreamHeader {
168    /// Largest original block size in the stream.
169    pub block_size: BlockSize,
170}
171
172impl StreamHeader {
173    /// Length of the header in bytes.
174    pub const LEN: usize = 9;
175
176    /// Build a header for `block_size`.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use bzip3_rs::{BlockSize, StreamHeader};
182    ///
183    /// let header = StreamHeader::new(BlockSize::DEFAULT);
184    /// assert_eq!(header.block_size, BlockSize::DEFAULT);
185    /// ```
186    #[must_use]
187    #[inline]
188    pub const fn new(block_size: BlockSize) -> Self {
189        Self { block_size }
190    }
191
192    /// Encode the header into its 9 bytes.
193    ///
194    /// The first 5 bytes are the magic number. The last 4 bytes are the
195    /// block size as a little-endian integer.
196    ///
197    /// # Examples
198    ///
199    /// ```
200    /// use bzip3_rs::{BlockSize, StreamHeader};
201    ///
202    /// let bytes = StreamHeader::new(BlockSize::MIN).to_bytes();
203    /// assert_eq!(&bytes[..5], b"BZ3v1");
204    /// assert_eq!(bytes[5..], BlockSize::MIN.get().to_le_bytes());
205    /// ```
206    #[must_use]
207    pub const fn to_bytes(self) -> [u8; Self::LEN] {
208        let size = self.block_size.get().to_le_bytes();
209        [
210            MAGIC_NUMBER[0],
211            MAGIC_NUMBER[1],
212            MAGIC_NUMBER[2],
213            MAGIC_NUMBER[3],
214            MAGIC_NUMBER[4],
215            size[0],
216            size[1],
217            size[2],
218            size[3],
219        ]
220    }
221
222    /// Parse the 9 header bytes.
223    ///
224    /// # Errors
225    ///
226    /// Returns [`Error::InvalidMagic`] on bad magic,
227    /// [`Error::InvalidBlockSize`] on a block size outside the valid range.
228    ///
229    /// # Examples
230    ///
231    /// ```
232    /// use bzip3_rs::error::Error;
233    /// use bzip3_rs::{BlockSize, StreamHeader};
234    ///
235    /// # fn main() -> Result<(), Error> {
236    /// let bytes = StreamHeader::new(BlockSize::MIN).to_bytes();
237    /// assert_eq!(StreamHeader::from_bytes(&bytes)?.block_size, BlockSize::MIN);
238    ///
239    /// let bad = [0_u8; StreamHeader::LEN];
240    /// assert!(matches!(StreamHeader::from_bytes(&bad), Err(Error::InvalidMagic)));
241    /// # Ok(())
242    /// # }
243    /// ```
244    #[inline]
245    pub fn from_bytes(bytes: &[u8; Self::LEN]) -> Result<Self> {
246        if bytes[..5] != MAGIC_NUMBER {
247            return Err(Error::InvalidMagic);
248        }
249        Ok(Self::new(block_size_from_field(&bytes[5..9])?))
250    }
251
252    /// Parse the header at the start of `data`.
253    ///
254    /// # Errors
255    ///
256    /// Returns [`Error::TruncatedHeader`] on a short header,
257    /// [`Error::InvalidMagic`] on bad magic,
258    /// [`Error::InvalidBlockSize`] on a block size outside the valid range.
259    #[inline]
260    pub(crate) fn parse(data: &[u8]) -> Result<Self> {
261        let Some(bytes) = data.first_chunk::<{ Self::LEN }>() else {
262            return Err(Error::TruncatedHeader);
263        };
264        Self::from_bytes(bytes)
265    }
266}
267
268/// Read a block size field. Any invalid value fails with [`Error::InvalidBlockSize`].
269#[inline]
270fn block_size_from_field(bytes: &[u8]) -> Result<BlockSize> {
271    let value = u32::try_from(read_i32(bytes)).map_err(|_| Error::InvalidBlockSize)?;
272    BlockSize::new(value).map_err(|_| Error::InvalidBlockSize)
273}
274
275/// Parse and check the streaming block header at the start of `bytes`.
276///
277/// `bytes` must hold at least [`BlockHeader::LEN`] bytes.
278///
279/// # Errors
280///
281/// Returns [`Error::InvalidHeader`] on a payload above `compress_bound` or an
282/// original size above the block size. A negative wire size reads as a
283/// value above both limits, so it fails the same way.
284pub fn parse_block_header(bytes: &[u8], block_size: BlockSize) -> Result<BlockHeader> {
285    let header = BlockHeader::new(read_u32(bytes), read_u32(&bytes[4..]));
286    if header.compressed_size as usize > compress_bound(block_size.as_usize())
287        || header.original_size > block_size.get()
288    {
289        return Err(Error::InvalidHeader);
290    }
291    Ok(header)
292}
293
294/// Walk the blocks of streaming input, one at a time.
295///
296/// Blocks borrow their payloads, so no copy occurs. The walk stops
297/// after its first error.
298pub struct StreamBlocks<'a> {
299    /// Bytes after the last block that the walk returned.
300    rest: &'a [u8],
301    /// Block size from the stream header.
302    block_size: BlockSize,
303}
304
305impl<'a> StreamBlocks<'a> {
306    /// Check the 9-byte stream header and start the walk after it.
307    ///
308    /// # Errors
309    ///
310    /// Returns [`Error::TruncatedHeader`] on a short header.
311    /// Returns [`Error::InvalidMagic`] on bad magic.
312    /// Returns [`Error::InvalidBlockSize`] on an invalid header block size.
313    pub fn new(data: &'a [u8]) -> Result<Self> {
314        let block_size = StreamHeader::parse(data)?.block_size;
315        Ok(Self {
316            rest: &data[StreamHeader::LEN..],
317            block_size,
318        })
319    }
320
321    /// Return the block size from the stream header.
322    #[must_use]
323    #[inline]
324    pub const fn block_size(&self) -> BlockSize {
325        self.block_size
326    }
327
328    /// Split the next block off the input.
329    fn split(&self) -> Result<(FramedBlock<'a>, &'a [u8])> {
330        let Some(block_header) = self.rest.get(..BlockHeader::LEN) else {
331            return Err(Error::TruncatedHeader);
332        };
333        let header = parse_block_header(block_header, self.block_size)?;
334        let end = BlockHeader::LEN + header.compressed_size as usize;
335        let Some(payload) = self.rest.get(BlockHeader::LEN..end) else {
336            return Err(Error::TruncatedData);
337        };
338        Ok((FramedBlock { header, payload }, &self.rest[end..]))
339    }
340}
341
342impl<'a> Iterator for StreamBlocks<'a> {
343    type Item = Result<FramedBlock<'a>>;
344
345    /// Return the next block, or the error that ends the walk.
346    ///
347    /// # Errors
348    ///
349    /// Returns [`Error::InvalidHeader`] on bad sizes.
350    /// Returns [`Error::TruncatedHeader`] on trailing bytes
351    /// below a full block header. Returns [`Error::TruncatedData`] on a
352    /// short payload.
353    fn next(&mut self) -> Option<Self::Item> {
354        if self.rest.is_empty() {
355            return None;
356        }
357        match self.split() {
358            Ok((block, rest)) => {
359                self.rest = rest;
360                Some(Ok(block))
361            }
362            Err(fault) => {
363                self.rest = &[];
364                Some(Err(fault))
365            }
366        }
367    }
368}
369
370/// Split streaming input into its header block size and blocks.
371///
372/// # Errors
373///
374/// Returns [`Error::TruncatedHeader`] on a short header or trailing bytes
375/// below a full block header. Returns [`Error::InvalidMagic`] on bad magic.
376/// Returns [`Error::InvalidHeader`] on bad sizes.
377/// Returns [`Error::TruncatedData`] on a short payload.
378/// Returns [`Error::InvalidBlockSize`] on an invalid header block size.
379#[inline]
380pub fn split_stream(data: &[u8]) -> Result<(BlockSize, Vec<FramedBlock<'_>>)> {
381    let walk = StreamBlocks::new(data)?;
382    let block_size = walk.block_size();
383    Ok((block_size, walk.collect::<Result<_>>()?))
384}
385
386/// Encode `chunk` through `state` and append header plus payload to `out`.
387///
388/// The payload goes straight into `out`. The block header takes its
389/// place after the payload size is known, so no copy occurs.
390///
391/// # Errors
392///
393/// Returns the codec fault of [`BlockEncoder::encode_block_into`], or
394/// [`Error::BlockTooLarge`] when a size does not fit 32 bits. On an error,
395/// `out` keeps its length.
396pub fn append_framed_block(
397    state: &mut BlockEncoder,
398    chunk: &[u8],
399    out: &mut Vec<u8>,
400) -> Result<()> {
401    let at = out.len();
402    out.extend_from_slice(&[0_u8; BlockHeader::LEN]);
403    let outcome = state
404        .encode_block_into(chunk, out)
405        .and_then(|()| BlockHeader::for_block(out.len() - at - BlockHeader::LEN, chunk.len()));
406    match outcome {
407        Ok(header) => {
408            out[at..at + BlockHeader::LEN].copy_from_slice(&header.to_bytes());
409            Ok(())
410        }
411        Err(e) => {
412            out.truncate(at);
413            Err(e)
414        }
415    }
416}
417
418/// Copy the first four bytes of a slice. Panics on a shorter slice.
419#[inline]
420const fn first_four(bytes: &[u8]) -> [u8; 4] {
421    [bytes[0], bytes[1], bytes[2], bytes[3]]
422}
423
424/// Read a little-endian `i32` from a slice prefix.
425///
426/// # Panics
427///
428/// Panics when `bytes` holds fewer than four bytes.
429#[inline]
430pub const fn read_i32(bytes: &[u8]) -> i32 {
431    i32::from_le_bytes(first_four(bytes))
432}
433
434/// Read a little-endian `u32` from a slice prefix.
435///
436/// # Panics
437///
438/// Panics when `bytes` holds fewer than four bytes.
439#[inline]
440pub const fn read_u32(bytes: &[u8]) -> u32 {
441    u32::from_le_bytes(first_four(bytes))
442}
443
444/// Mirror the block-size clamp for one-shot encode.
445///
446/// When the requested size exceeds the input, encode with
447/// `compress_bound(input)` clamped to a minimum of 65 KiB and record that value.
448fn clamp_block_size(requested: BlockSize, input_len: usize) -> Result<BlockSize> {
449    let size = if requested.as_usize() > input_len {
450        compress_bound(input_len).max(BlockSize::MIN.as_usize())
451    } else {
452        requested.as_usize()
453    };
454    BlockSize::new(u32::try_from(size).map_err(|_| Error::InvalidBlockSize)?)
455}
456
457/// Encode one-shot framed output for `data`.
458///
459/// `jobs` follows the rule of [`crate::stream::compress_slice`]:
460/// `Jobs::SERIAL` runs serially, with one state that encodes every block
461/// and reuses its working memory. Other counts encode blocks on a
462/// pool. Output bytes are identical at every count. Blocks at or
463/// above 64 bytes run the full pipeline.
464///
465/// # Errors
466///
467/// Returns [`Error::InvalidBlockSize`] when the clamped size leaves the valid range,
468/// [`Error::OutOfMemory`] when the encoder state allocation fails,
469/// [`Error::BlockTooLarge`] when a block exceeds the effective size,
470/// [`Error::BwtFailed`] when a transform fails,
471/// [`Error::ThreadPoolFailed`] when the pool build fails.
472pub fn encode_frame(data: &[u8], block_size: BlockSize, jobs: Jobs) -> Result<Vec<u8>> {
473    let effective = clamp_block_size(block_size, data.len())?;
474    let width = effective.as_usize();
475    let block_count = data.len().div_ceil(width);
476    let count_field = u32::try_from(block_count).map_err(|_| Error::BlockTooLarge)?;
477
478    let mut out =
479        Vec::with_capacity(ONE_SHOT_HEADER_LEN + block_count * BlockHeader::LEN + data.len() + 8);
480    out.extend_from_slice(&StreamHeader::new(effective).to_bytes());
481    out.extend_from_slice(&count_field.to_le_bytes());
482
483    if jobs.is_serial() || block_count < 2 {
484        let mut state = BlockEncoder::new(effective)?;
485        for chunk in data.chunks(width) {
486            append_framed_block(&mut state, chunk, &mut out)?;
487        }
488    } else {
489        let parts = run_in_pool(jobs, block_count, || encode_blocks(data, effective))?;
490        for part in &parts {
491            out.extend_from_slice(part);
492        }
493    }
494    Ok(out)
495}
496
497/// Decode one-shot framed input, ignoring trailing bytes.
498///
499/// `jobs` follows the rule of [`crate::stream::compress_slice`].
500/// Output bytes and the reported error are identical at every count.
501/// A block fault comes before a header fault of a later block, as in
502/// the serial path.
503///
504/// # Errors
505///
506/// Returns [`Error::TruncatedHeader`] on a short header,
507/// [`Error::InvalidMagic`] on bad magic,
508/// [`Error::InvalidHeader`] on bad block sizes, [`Error::InvalidBlockSize`] on a bad header block size,
509/// [`Error::TruncatedData`] on a short payload,
510/// [`Error::OutOfMemory`] when a decoder state fails to build,
511/// [`Error::ThreadPoolFailed`] when the pool build fails,
512/// and the block error when a payload fails to decode.
513pub fn decode_frame(data: &[u8], jobs: Jobs) -> Result<Vec<u8>> {
514    let Some(header) = data.get(..ONE_SHOT_HEADER_LEN) else {
515        return Err(Error::TruncatedHeader);
516    };
517    let block_size = StreamHeader::parse(header)?.block_size;
518    let block_count = read_u32(&header[StreamHeader::LEN..]);
519    let limit = compress_bound(block_size.as_usize());
520
521    // The serial path decodes each block after its split. A block fault
522    // still comes before a header fault of a later block.
523    if jobs.is_serial() {
524        let mut decoder = BlockDecoder::new(block_size)?;
525        let mut out = Vec::new();
526        let mut rest = &data[ONE_SHOT_HEADER_LEN..];
527        for _ in 0..block_count {
528            let (block, next) = split_block(rest, block_size, limit)?;
529            decoder.decode_block_into(block.payload, block.header.original_size, &mut out)?;
530            rest = next;
531        }
532        return Ok(out);
533    }
534
535    // Walk the headers first. A header fault stops the walk, but the
536    // blocks before it still decode before the fault shows.
537    let mut rest = &data[ONE_SHOT_HEADER_LEN..];
538    let mut blocks = Vec::new();
539    let mut walk_fault = None;
540    for _ in 0..block_count {
541        match split_block(rest, block_size, limit) {
542            Ok((block, next)) => {
543                blocks.push(block);
544                rest = next;
545            }
546            Err(fault) => {
547                walk_fault = Some(fault);
548                break;
549            }
550        }
551    }
552
553    let mut out = Vec::new();
554    if blocks.len() < 2 {
555        let mut decoder = BlockDecoder::new(block_size)?;
556        for block in &blocks {
557            decoder.decode_block_into(block.payload, block.header.original_size, &mut out)?;
558        }
559    } else {
560        out = run_in_pool(jobs, blocks.len(), || decode_blocks(&blocks, block_size))?;
561    }
562    walk_fault.map_or(Ok(out), Err)
563}
564
565/// Split the first one-shot block off `rest`.
566///
567/// Returns the block and the bytes after it. The checks keep the
568/// reference order.
569///
570/// # Errors
571///
572/// Returns [`Error::TruncatedHeader`] on a short header,
573/// [`Error::InvalidHeader`] on bad sizes,
574/// [`Error::TruncatedData`] on a short payload.
575fn split_block(
576    rest: &[u8],
577    block_size: BlockSize,
578    limit: usize,
579) -> Result<(FramedBlock<'_>, &[u8])> {
580    let Some(block_header) = rest.get(..BlockHeader::LEN) else {
581        return Err(Error::TruncatedHeader);
582    };
583    // A negative wire size reads as a value above both limits.
584    let header = BlockHeader::new(read_u32(block_header), read_u32(&block_header[4..]));
585    let comp_len = header.compressed_size as usize;
586    if comp_len > limit {
587        return Err(Error::InvalidHeader);
588    }
589    let Some(payload) = rest.get(BlockHeader::LEN..BlockHeader::LEN + comp_len) else {
590        return Err(Error::TruncatedData);
591    };
592    if header.original_size > block_size.get() {
593        return Err(Error::InvalidHeader);
594    }
595    Ok((
596        FramedBlock { header, payload },
597        &rest[BlockHeader::LEN + comp_len..],
598    ))
599}
600
601#[cfg(test)]
602mod tests {
603    use super::BlockHeader;
604    use super::StreamBlocks;
605    use super::StreamHeader;
606    use super::append_framed_block;
607    use super::decode_frame;
608    use super::encode_frame;
609    use super::parse_block_header;
610    use super::split_stream;
611    use crate::BlockSize;
612    use crate::Jobs;
613    use crate::block::BlockEncoder;
614    use crate::compress_bound;
615    use crate::error::Error;
616    use crate::stream;
617
618    #[test]
619    fn empty_yields_zero_block_header() {
620        let out = encode_frame(b"", BlockSize::DEFAULT, Jobs::SERIAL).unwrap();
621        assert_eq!(out.len(), 13);
622        assert_eq!(&out[..5], b"BZ3v1");
623        assert_eq!(u32::from_le_bytes(out[5..9].try_into().unwrap()), 65 * 1024);
624        assert_eq!(u32::from_le_bytes(out[9..13].try_into().unwrap()), 0);
625        assert_eq!(decode_frame(&out, Jobs::SERIAL).unwrap(), b"");
626    }
627
628    #[test]
629    fn short_inputs_use_one_stored_block() {
630        let out = encode_frame(b"A", BlockSize::DEFAULT, Jobs::SERIAL).unwrap();
631        assert_eq!(u32::from_le_bytes(out[9..13].try_into().unwrap()), 1);
632        assert_eq!(decode_frame(&out, Jobs::SERIAL).unwrap(), b"A");
633    }
634
635    #[test]
636    fn truncated_payload_reports_truncation() {
637        let mut out = encode_frame(b"A", BlockSize::DEFAULT, Jobs::SERIAL).unwrap();
638        out.truncate(out.len() - 2);
639        assert!(matches!(
640            decode_frame(&out, Jobs::SERIAL),
641            Err(Error::TruncatedData)
642        ));
643    }
644
645    #[test]
646    fn negative_block_size_fails_invalid_block_size() {
647        let mut out = encode_frame(b"A", BlockSize::DEFAULT, Jobs::SERIAL).unwrap();
648        out[5..9].copy_from_slice(&(-1_i32).to_le_bytes());
649        assert!(matches!(
650            decode_frame(&out, Jobs::SERIAL),
651            Err(Error::InvalidBlockSize)
652        ));
653    }
654
655    #[test]
656    fn for_block_rejects_sizes_above_u32() {
657        let huge = u32::MAX as usize + 1;
658        assert!(matches!(
659            BlockHeader::for_block(huge, 0),
660            Err(Error::BlockTooLarge)
661        ));
662        assert!(matches!(
663            BlockHeader::for_block(0, huge),
664            Err(Error::BlockTooLarge)
665        ));
666        assert!(BlockHeader::for_block(10, 100).is_ok());
667    }
668
669    #[test]
670    fn parse_block_header_rejects_oversize() {
671        let size = BlockSize::MIN;
672        let limit = compress_bound(size.as_usize());
673        let mut bad_comp = Vec::new();
674        bad_comp.extend_from_slice(&u32::try_from(limit + 1).unwrap().to_le_bytes());
675        bad_comp.extend_from_slice(&0_u32.to_le_bytes());
676        assert!(matches!(
677            parse_block_header(&bad_comp, size),
678            Err(Error::InvalidHeader)
679        ));
680        let mut bad_orig = Vec::new();
681        bad_orig.extend_from_slice(&0_u32.to_le_bytes());
682        bad_orig.extend_from_slice(&(size.get() + 1).to_le_bytes());
683        assert!(matches!(
684            parse_block_header(&bad_orig, size),
685            Err(Error::InvalidHeader)
686        ));
687        let mut ok = Vec::new();
688        ok.extend_from_slice(&0_u32.to_le_bytes());
689        ok.extend_from_slice(&size.get().to_le_bytes());
690        assert!(parse_block_header(&ok, size).is_ok());
691    }
692
693    #[test]
694    fn split_stream_rejects_short_inputs() {
695        assert!(matches!(split_stream(&[]), Err(Error::TruncatedHeader)));
696        assert!(matches!(
697            split_stream(b"BZ3v1"),
698            Err(Error::TruncatedHeader)
699        ));
700        let mut coded = Vec::new();
701        stream::compress_slice(b"hi", &mut coded, BlockSize::MIN, Jobs::SERIAL).unwrap();
702        let (size, blocks) = split_stream(&coded).unwrap();
703        assert_eq!(size, BlockSize::MIN);
704        assert_eq!(blocks.len(), 1);
705        let mut cut = coded.clone();
706        cut.truncate(StreamHeader::LEN + BlockHeader::LEN - 1);
707        assert!(matches!(split_stream(&cut), Err(Error::TruncatedHeader)));
708        let header = BlockHeader::new(10, 2).to_bytes();
709        let mut short = StreamHeader::new(BlockSize::MIN).to_bytes().to_vec();
710        short.extend_from_slice(&header);
711        short.extend_from_slice(&[0_u8; 2]);
712        assert!(matches!(split_stream(&short), Err(Error::TruncatedData)));
713        let mut bad = StreamHeader::new(BlockSize::MIN).to_bytes().to_vec();
714        let limit = compress_bound(BlockSize::MIN.as_usize()) + 1;
715        bad.extend_from_slice(&u32::try_from(limit).unwrap().to_le_bytes());
716        bad.extend_from_slice(&0_u32.to_le_bytes());
717        assert!(matches!(split_stream(&bad), Err(Error::InvalidHeader)));
718    }
719
720    #[test]
721    fn stream_blocks_empty_rest_yields_none() {
722        let mut coded = Vec::new();
723        stream::compress_slice(b"", &mut coded, BlockSize::MIN, Jobs::SERIAL).unwrap();
724        let mut walk = StreamBlocks::new(&coded).unwrap();
725        assert!(walk.next().is_none());
726        assert!(walk.next().is_none());
727    }
728
729    #[test]
730    fn append_framed_block_rolls_back_on_error() {
731        let mut state = BlockEncoder::new(BlockSize::MIN).unwrap();
732        let big = vec![0_u8; BlockSize::MIN.as_usize() + 1];
733        let mut out = vec![9_u8, 9, 9];
734        assert!(matches!(
735            append_framed_block(&mut state, &big, &mut out),
736            Err(Error::BlockTooLarge)
737        ));
738        assert_eq!(out, vec![9_u8, 9, 9]);
739        let mut ok_out = Vec::new();
740        append_framed_block(&mut state, b"hi", &mut ok_out).unwrap();
741        assert!(ok_out.len() > BlockHeader::LEN);
742    }
743}