Skip to main content

ryg_rans_rs_cli/container/
writer.rs

1//! # Container writer — serialize RYGRANS containers
2
3use crate::container::MAGIC;
4use crate::container::block::Block;
5use crate::container::footer::FileFooter;
6use crate::container::header::FileHeader;
7use crate::error::AppError;
8use sha2::{Digest, Sha256};
9use std::io::Write;
10
11/// Container writer that produces canonical RYGRANS containers.
12pub struct ContainerWriter<W: Write> {
13    writer: W,
14    header_written: bool,
15    footer_written: bool,
16    block_count: u64,
17    total_uncompressed: u64,
18    total_payload: u64,
19    hasher: Sha256,
20    decoded_hasher: Sha256,
21}
22
23impl<W: Write> ContainerWriter<W> {
24    /// Create a new container writer.
25    pub fn new(writer: W) -> Self {
26        Self {
27            writer,
28            header_written: false,
29            footer_written: false,
30            block_count: 0,
31            total_uncompressed: 0,
32            total_payload: 0,
33            hasher: Sha256::new(),
34            decoded_hasher: Sha256::new(),
35        }
36    }
37
38    /// Write the file header.
39    pub fn write_header(&mut self, header: &FileHeader) -> Result<(), AppError> {
40        let bytes = header.to_bytes();
41        self.writer.write_all(&bytes).map_err(|e| {
42            AppError::Io(crate::error::IoError {
43                path: None,
44                detail: format!("write header: {}", e),
45            })
46        })?;
47        self.hasher.update(&bytes);
48        self.header_written = true;
49        Ok(())
50    }
51
52    /// Write a block record.
53    pub fn write_block(&mut self, block: &Block, decoded_data: &[u8]) -> Result<(), AppError> {
54        let bytes = block.to_bytes();
55        self.writer.write_all(&bytes).map_err(|e| {
56            AppError::Io(crate::error::IoError {
57                path: None,
58                detail: format!("write block {}: {}", block.block_index, e),
59            })
60        })?;
61
62        self.hasher.update(&bytes);
63        self.decoded_hasher.update(decoded_data);
64
65        self.block_count += 1;
66        self.total_uncompressed = self
67            .total_uncompressed
68            .checked_add(block.uncompressed_length as u64)
69            .unwrap();
70        self.total_payload = self
71            .total_payload
72            .checked_add(block.payload.len() as u64)
73            .unwrap();
74        Ok(())
75    }
76
77    /// Write the file footer.  Must be called after all blocks.
78    pub fn write_footer(&mut self) -> Result<FileFooter, AppError> {
79        use sha2::Digest;
80        let hasher_clone = std::mem::replace(&mut self.hasher, Sha256::new());
81        let container_hash: [u8; 32] = hasher_clone.finalize().into();
82        let dec_hasher_clone = std::mem::replace(&mut self.decoded_hasher, Sha256::new());
83        let decoded_hash: [u8; 32] = dec_hasher_clone.finalize().into();
84
85        let footer = FileFooter::compute(
86            self.block_count,
87            self.total_uncompressed,
88            self.total_payload,
89            container_hash,
90            decoded_hash,
91        );
92
93        let bytes = footer.to_bytes();
94        self.writer.write_all(&bytes).map_err(|e| {
95            AppError::Io(crate::error::IoError {
96                path: None,
97                detail: format!("write footer: {}", e),
98            })
99        })?;
100
101        self.footer_written = true;
102        Ok(footer)
103    }
104
105    /// Flush the underlying writer.
106    pub fn flush(&mut self) -> Result<(), AppError> {
107        self.writer.flush().map_err(|e| {
108            AppError::Io(crate::error::IoError {
109                path: None,
110                detail: format!("flush: {}", e),
111            })
112        })
113    }
114
115    /// Return the inner writer.
116    pub fn into_inner(self) -> W {
117        self.writer
118    }
119}