mca/
writer.rs

1use std::{
2    collections::HashMap,
3    io::Write,
4    time::{SystemTime, UNIX_EPOCH},
5};
6
7use crate::{chunk::PendingChunk, CompressionType, McaError, REGION_SIZE, SECTOR_SIZE};
8
9/// A writer used to write chunks to a region (`mca`) file.  
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
11pub struct RegionWriter {
12    chunks: Vec<PendingChunk>,
13}
14
15impl RegionWriter {
16    /// Gets the current time in unix epoch
17    fn get_current_timestamp() -> u32 {
18        let start = SystemTime::now();
19        let since_the_epoch = start.duration_since(UNIX_EPOCH).unwrap().as_secs() as u32;
20        since_the_epoch.to_be()
21    }
22
23    /// Creates a new region writer
24    pub fn new() -> RegionWriter {
25        RegionWriter { chunks: vec![] }
26    }
27
28    /// Pushes a raw chunk into the writer  
29    /// Defaults to `LZ4` compression, use [`push_chunk_with_compression`] for other compression types.  
30    ///
31    /// Timestamp will be current time since [`UNIX_EPOCH`], use [`push_pending_chunk`] to override it.  
32    pub fn push_chunk<B>(&mut self, raw_data: &[u8], coordinate: (B, B)) -> Result<(), McaError>
33    where
34        B: Into<u8>,
35    {
36        let chunk = PendingChunk::new(
37            &raw_data,
38            CompressionType::LZ4,
39            RegionWriter::get_current_timestamp(),
40            coordinate,
41        )?;
42        self.chunks.push(chunk);
43
44        Ok(())
45    }
46
47    /// Pushes a raw chunk into the writer  
48    /// This specifies the compression type used  
49    pub fn push_chunk_with_compression<B>(
50        &mut self,
51        raw_data: &[u8],
52        coordinate: (B, B),
53        compression_type: CompressionType,
54    ) -> Result<(), McaError>
55    where
56        B: Into<u8>,
57    {
58        let chunk = PendingChunk::new(
59            &raw_data,
60            compression_type,
61            RegionWriter::get_current_timestamp(),
62            coordinate,
63        )?;
64        self.chunks.push(chunk);
65
66        Ok(())
67    }
68
69    /// Just pushes a [`PendingChunk`] to the writer
70    pub fn push_pending_chunk(&mut self, chunk: PendingChunk) {
71        self.chunks.push(chunk)
72    }
73
74    /// Writes all chunks into one region file.  
75    ///
76    /// ## Example
77    /// ```ignore
78    /// use mca::{RegionWriter};
79    ///
80    /// let mut writer = RegionWriter::new();
81    ///
82    /// // Push some chunk data
83    /// // ...
84    ///
85    /// let mut buf: Vec<u8> = vec![];
86    /// writer.write(&mut buf).unwrap();
87    ///
88    /// std::fs::File::write("r.0.0.mca", &buf).unwrap();
89    /// ```
90    pub fn write<'a, W>(&self, w: &'a mut W) -> Result<(), McaError>
91    where
92        W: Write,
93    {
94        // payload prepping, needed for location header, hence it first
95        let mut chunk_offsets: HashMap<(u8, u8), usize> = HashMap::new();
96        let mut chunk_map: HashMap<(u8, u8), &PendingChunk> = HashMap::new(); // dont know the perf hit for this but this can for sure be removed
97
98        let mut curr_chunk_offset: usize = SECTOR_SIZE * 2; // init pos for chunks
99        let mut payloads: Vec<u8> = vec![];
100
101        for chunk in self.chunks.iter() {
102            let len_b = (chunk.compressed_data.len() as u32 + 1).to_be_bytes(); // this little +1 accounts for the compression byte
103            let len = [len_b[0], len_b[1], len_b[2], len_b[3]];
104
105            let compression = chunk.compression.to_u8();
106
107            let mut payload_len = 0;
108            payload_len += payloads.write(&len)?;
109            payload_len += payloads.write(&[compression])?;
110            payload_len += payloads.write(&chunk.compressed_data)?;
111
112            // pad the chunk so its always in sector chunks
113            let remaining = SECTOR_SIZE - (payload_len % SECTOR_SIZE);
114            let padding = std::iter::repeat(0u8).take(remaining).collect::<Vec<u8>>();
115            payload_len += payloads.write(&padding)?;
116
117            chunk_offsets.insert(chunk.coordinate, curr_chunk_offset);
118            chunk_map.insert(chunk.coordinate, chunk);
119
120            // offset it by current + how many bytes we just wrote
121            curr_chunk_offset = curr_chunk_offset + payload_len;
122        }
123
124        // location header
125        for x in 0..REGION_SIZE {
126            for z in 0..REGION_SIZE {
127                let offset = match chunk_offsets.get(&(z as u8, x as u8)) {
128                    Some(offset) => offset,
129                    None => {
130                        w.write(&[0, 0, 0, 0])?;
131                        continue;
132                    }
133                };
134
135                let chunk = chunk_map.get(&(z as u8, x as u8)).unwrap(); // handle this unwrap but this shouldnt be possible when we have the above statement
136
137                let offset_bytes = {
138                    let be = ((*offset / SECTOR_SIZE) as u32).to_be_bytes();
139                    [be[1], be[2], be[3]]
140                };
141
142                w.write(&offset_bytes)?;
143
144                let sector_count = ((chunk.compressed_data.len() + 4 + 1) as f32
145                    / SECTOR_SIZE as f32)
146                    .ceil() as u8;
147
148                w.write(&[sector_count])?;
149            }
150        }
151
152        // timestamp header
153        for x in 0..REGION_SIZE {
154            for z in 0..REGION_SIZE {
155                match &self.chunks.get(x * REGION_SIZE + z) {
156                    Some(chunk) => {
157                        let timestamp = {
158                            let b = chunk.timestamp.to_be_bytes();
159                            [b[0], b[1], b[2], b[3]]
160                        };
161                        w.write(&timestamp)?
162                    }
163                    None => w.write(&[0, 0, 0, 0])?,
164                };
165            }
166        }
167
168        w.write(&payloads)?;
169
170        w.flush()?;
171
172        Ok(())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::RegionReader;
180
181    const REGION: &[u8] = include_bytes!("../benches/r.0.0.mca");
182
183    #[test]
184    fn round_trip() {
185        let region = RegionReader::new(REGION).unwrap();
186        let mut writer = RegionWriter::new();
187
188        for (idx, chunk) in region.iter().enumerate() {
189            let chunk = match chunk.unwrap() {
190                Some(data) => data,
191                None => continue,
192            };
193
194            let data = chunk.decompress().unwrap();
195            writer
196                .push_chunk_with_compression(
197                    &data,
198                    ((idx % REGION_SIZE) as u8, (idx / REGION_SIZE) as u8),
199                    CompressionType::Zlib,
200                )
201                .unwrap();
202        }
203
204        let mut buf = vec![];
205        writer.write(&mut buf).unwrap();
206
207        let new_region = RegionReader::new(&buf).unwrap();
208        let chunk = new_region.get_chunk(0, 0).unwrap().unwrap();
209
210        let data = chunk.decompress().unwrap();
211        let _ = sculk::chunk::Chunk::from_bytes(&data).unwrap();
212    }
213}