Skip to main content

mc_core/world/anvil/
region.rs

1use std::io::{Error as IoError, Result as IoResult, Seek, SeekFrom, Read, Write, Cursor, ErrorKind};
2use std::fs::{File, OpenOptions};
3use std::time::SystemTime;
4use std::fmt::Arguments;
5use std::path::PathBuf;
6
7use flate2::read::{GzDecoder, ZlibDecoder};
8use flate2::write::{GzEncoder, ZlibEncoder};
9use flate2::Compression;
10use thiserror::Error;
11use bit_vec::BitVec;
12
13
14const SECTOR_SIZE: u64 = 4096;
15const MAX_SECTOR_OFFSET: u64 = 0xFFFFFF;
16const MAX_SECTOR_LENGTH: u64 = 0xFF;
17const MAX_CHUNK_SIZE: u64 = MAX_SECTOR_LENGTH * SECTOR_SIZE;
18
19
20/// Error type used together with `RegionResult` for every call on region file methods.
21#[derive(Error, Debug)]
22pub enum RegionError {
23    #[error("The region file was not found in the level directory at {0}.")]
24    FileNotFound(PathBuf),
25    #[error("The region file size ({0}) is shorter than 8192 bytes.")]
26    FileTooSmall(u64),
27    #[error("The region file size ({0}) is not a multiple of 4096 (4096 = 1 sector).")]
28    FileNotPadded(u64),
29    #[error("The region file has an invalid chunk (#{0}) metadata that leads to sectors out of the range.")]
30    IllegalMetadata(u16),
31    #[error("The required chunk is empty, it has no sector allocated in the region file.")]
32    EmptyChunk,
33    #[error("The compression method {0} in the chunk header is unknown.")]
34    UnknownCompression(u8),
35    #[error("The external chunk file was not found. This is used if the chunk is too large.")]
36    ExternalChunkNotFound,
37    #[error("No more sectors are available in the region file, really unlikely to happen.")]
38    OutOfSectors,
39    #[error("{0}")]
40    Io(#[from] IoError)
41}
42
43/// A result type with an error of type `RegionError`, it is used in region file methods.
44pub type RegionResult<T> = Result<T, RegionError>;
45
46
47/// This structure holds a region file and all its metadata, it is used
48pub struct RegionFile {
49    /// The base directory path that contains all regions and chunks files.
50    dir: PathBuf,
51    /// The file object of the region.
52    file: File,
53    /// Chunk metadata for each chunk in the 32x32 region.
54    metadata: [ChunkMetadata; 1024],
55    /// A vector of bits for each section, this does not include the 2x headers sectors.
56    /// True if the section is free.
57    sectors: BitVec
58}
59
60impl RegionFile {
61
62    /// Build a new region file, this method will open the region file from the given directory
63    /// and the region coordinates. The directory should be the one of region, not the level dir.
64    /// This method can return the following errors:
65    /// - `RegionError::FileTooSmall` The given file is too small, smaller than 2 sectors.
66    /// - `RegionError::FileNotPadded` The file size is not a multiple of sector size, 4096 bytes.
67    /// - `RegionError::IllegalMetadata` Failed to parse one of the chunk's metadata.
68    /// - `RegionError::Io` An IO error happened.
69    pub fn new(dir: PathBuf, rx: i32, rz: i32, create: bool) -> RegionResult<Self> {
70
71        if create {
72            std::fs::create_dir_all(&dir)?;
73        }
74
75        let file_path = get_region_file_path(&dir, rx, rz);
76        let mut file = OpenOptions::new()
77            .read(true)
78            .write(true)
79            .create(create)
80            .open(&file_path)
81            .map_err(|err| match err.kind() {
82                ErrorKind::NotFound => RegionError::FileNotFound(file_path),
83                _ => RegionError::Io(err)
84            })?;
85
86        let file_len = file.seek(SeekFrom::End(0))?;
87
88        let mut metadata = [ChunkMetadata { location: 0, timestamp: 0 }; 1024];
89        let mut sectors;
90
91        if file_len == 0 && create {
92            file.write_all(&[0; 8192])?;
93            sectors = BitVec::new();
94        } else {
95
96            // The following conditions are used to fix the file
97            if file_len < 8192 {
98                return Err(RegionError::FileTooSmall(file_len));
99            } else if (file_len & 0xFFF) != 0 {
100                return Err(RegionError::FileNotPadded(file_len));
101            }
102
103            file.seek(SeekFrom::Start(0))?;
104
105            // The sectors_count take the two headers sectors into account.
106            let sectors_count = file_len / SECTOR_SIZE;
107            sectors = BitVec::from_elem(sectors_count as usize - 2, true);
108
109            // let mut metadata = [ChunkMetadata { location: 0, timestamp: 0 }; 1024];
110
111            // Reading the first sector containing location information of each chunk.
112            for (idx, meta) in metadata.iter_mut().enumerate() {
113                let mut data = [0u8; 4];
114                file.read_exact(&mut data)?;
115                meta.location = u32::from_be_bytes(data);
116
117                let offset = meta.offset();
118                let length = meta.length();
119
120                if length != 0 {
121                    if (offset + length) <= sectors_count {
122                        fill_sectors(&mut sectors, offset as usize - 2, length as usize, false);
123                    } else {
124                        return Err(RegionError::IllegalMetadata(idx as u16));
125                    }
126                }
127            }
128
129            // Reading the second sector containing last modification times for each chunk.
130            for meta in &mut metadata {
131                let mut data = [0u8; 4];
132                file.read_exact(&mut data)?;
133                meta.timestamp = u32::from_be_bytes(data);
134            }
135
136        }
137
138        Ok(Self {
139            dir,
140            file,
141            metadata,
142            sectors
143        })
144
145    }
146
147    // Metadata //
148
149    #[inline]
150    pub fn get_metadata(&self, cx: i32, cz: i32) -> ChunkMetadata {
151        self.metadata[calc_chunk_index_from_pos(cx, cz)]
152    }
153
154    #[inline]
155    pub fn has_chunk(&self, cx: i32, cz: i32) -> bool {
156        self.get_metadata(cx, cz).length() != 0
157    }
158
159    // Reading //
160
161    /// Get a reader for a specific chunk, the position is not checked and you must ensure that
162    /// the chunk belong to this region. Some errors can be returned by this method:
163    /// - `RegionError::EmptyChunk` The chunk is not yet saved in the region.
164    /// - `RegionError::UnknownCompression` The compression method can't be decoded.
165    /// - `RegionError::ExternalChunkNotFound` The chunk should be saved externally but its file
166    /// is not found.
167    /// - `RegionError::Io` An IO error happened.
168    pub fn get_chunk_reader(&mut self, cx: i32, cz: i32) -> RegionResult<Box<dyn Read>> {
169
170        let metadata = self.metadata[calc_chunk_index_from_pos(cx, cz)];
171        if metadata.length() == 0 {
172            return Err(RegionError::EmptyChunk);
173        }
174
175        self.file.seek(SeekFrom::Start(metadata.offset() * SECTOR_SIZE))?;
176
177        let mut length_data = [0u8; 4];
178        self.file.read_exact(&mut length_data)?;
179        let data_length = u32::from_be_bytes(length_data) - 1;
180
181        let mut compression_id = [0u8; 1];
182        self.file.read_exact(&mut compression_id)?;
183        let compression_id = compression_id[0];
184
185        let compression = CompressionMethod::from_id(compression_id)
186            .ok_or_else(|| RegionError::UnknownCompression(compression_id))?;
187
188        let (compression_method, external) = compression;
189
190        let data = if external {
191
192            let mut external_file = match File::open(get_chunk_file_path(&self.dir, cx, cz)) {
193                Ok(file) => file,
194                Err(e) => return match e.kind() {
195                    ErrorKind::NotFound => Err(RegionError::ExternalChunkNotFound),
196                    _ => Err(RegionError::Io(e))
197                }
198            };
199
200            let mut data = Vec::new();
201            external_file.read_to_end(&mut data)?;
202            data
203
204        } else {
205            let mut data = vec![0u8; data_length as usize];
206            self.file.read_exact(&mut data[..])?;
207            data
208        };
209
210        let cursor = Cursor::new(data);
211
212        Ok(match compression_method {
213            CompressionMethod::Gzip => Box::new(GzDecoder::new(cursor)),
214            CompressionMethod::Zlib => Box::new(ZlibDecoder::new(cursor)),
215            CompressionMethod::None => Box::new(cursor)
216        })
217
218    }
219
220    // Writing //
221
222    pub fn get_chunk_writer(&mut self, cx: i32, cz: i32, method: CompressionMethod) -> ChunkWriter {
223
224        let vec: Vec<u8> = Vec::new();
225        let inner = match method {
226            CompressionMethod::Gzip => ChunkWriterInner::Gzip(GzEncoder::new(vec, Compression::best())),
227            CompressionMethod::Zlib => ChunkWriterInner::Zlib(ZlibEncoder::new(vec, Compression::best())),
228            CompressionMethod::None => ChunkWriterInner::None(vec)
229        };
230
231        ChunkWriter {
232            cx,
233            cz,
234            region: self,
235            inner
236        }
237
238    }
239
240    fn write_chunk(&mut self, cx: i32, cz: i32, data: &[u8], method: CompressionMethod) -> RegionResult<()> {
241
242        let metadata_index = calc_chunk_index_from_pos(cx, cz);
243        let mut metadata = self.metadata[metadata_index];
244        let mut offset = metadata.offset();
245        let mut length = metadata.length();
246
247        // Here, adding 1 to count the compression method byte ID.
248        let needed_byte_length = data.len() as u64 + 1;
249        let mut external = needed_byte_length > MAX_CHUNK_SIZE;
250
251        let needed_length = if external {
252            1 // If external, only one sector is needed to store chunk header.
253        } else {
254            // Adding 4 to the byte length to count the 32 bits length of (data.len() + 1).
255            (needed_byte_length + 4 - 1) / SECTOR_SIZE + 1
256        };
257
258        if needed_length != length {
259
260            if length != 0 {
261                fill_sectors(&mut self.sectors, offset as usize - 2, length as usize, true);
262            }
263
264            offset = 2;
265            length = 0;
266
267            let mut first_free_sector: Option<usize> = None;
268
269            for (sector, free) in self.sectors.iter().enumerate() {
270                if free {
271                    if first_free_sector.is_none() {
272                        first_free_sector = Some(sector + 2);
273                    }
274                    length += 1;
275                    if length == needed_length {
276                        break;
277                    }
278                } else {
279                    length = 0;
280                    offset = sector as u64 + 2 + 1;
281                }
282            }
283
284            if offset > MAX_SECTOR_OFFSET {
285
286                // Here we switch to external chunk storage, then we can only use 1 sector and
287                // store the chunk header. This is why we keep track of the first free sector
288                // even if no free sector were found.
289                if let Some(free_sector) = first_free_sector {
290                    external = true;
291                    offset = free_sector as u64;
292                    // No longer needed but this is virtually the case:
293                    // needed_length = 1;
294                    length = 1;
295                } else {
296                    // Revert the change to sectors "free state".
297                    fill_sectors(&mut self.sectors, metadata.offset() as usize - 2, length as usize, false);
298                    return Err(RegionError::OutOfSectors);
299                }
300
301            } else if length < needed_length {
302                let missing_length = needed_length - length;
303                self.file.set_len((missing_length + self.sectors.len() as u64 + 2) * SECTOR_SIZE)?;
304                self.sectors.extend((0..missing_length).map(|_| true));
305                length = needed_length;
306            }
307
308            // Mark all new sectors to "not free".
309            fill_sectors(&mut self.sectors, offset as usize - 2, length as usize, false);
310
311            // Update metadata for new offset and length.
312            metadata.set_location(offset, length);
313
314        }
315
316        // Update metadata for new timestamp.
317        metadata.set_timestamp_now();
318        self.write_metadata(metadata_index, metadata)?;
319
320        // Actually write the data
321        if external {
322            self.write_chunk_at(offset, 1, &[], method, true)?;
323            File::create(get_chunk_file_path(&self.dir, cx, cz))?.write_all(data)?;
324        } else {
325            self.write_chunk_at(offset, needed_byte_length as u32, data, method, false)?;
326        }
327
328        Ok(())
329
330    }
331
332    fn write_chunk_at(&mut self, sector_offset: u64, length: u32, data: &[u8], method: CompressionMethod, external: bool) -> IoResult<()> {
333        self.file.seek(SeekFrom::Start(sector_offset * SECTOR_SIZE))?;
334        self.file.write_all(&u32::to_be_bytes(length))?;
335        self.file.write_all(&[method.get_id(external)])?;
336        self.file.write_all(data)?;
337        self.file.flush()?;
338        Ok(())
339    }
340
341    fn write_metadata(&mut self, index: usize, metadata: ChunkMetadata) -> IoResult<()> {
342        self.file.seek(SeekFrom::Start(index as u64 * 4))?;
343        self.file.write_all(&u32::to_be_bytes(metadata.location))?;
344        self.file.seek(SeekFrom::Start(SECTOR_SIZE + index as u64 * 4))?;
345        self.file.write_all(&u32::to_be_bytes(metadata.timestamp))?;
346        self.metadata[index] = metadata;
347        Ok(())
348    }
349
350}
351
352
353#[derive(Copy, Clone, Debug)]
354pub struct ChunkMetadata {
355    location: u32,
356    timestamp: u32
357}
358
359impl ChunkMetadata {
360
361    #[inline]
362    pub fn offset(&self) -> u64 {
363        ((self.location >> 8) & 0xFFFFFF) as u64
364    }
365
366    #[inline]
367    pub fn length(&self) -> u64 {
368        (self.location & 0xFF) as u64
369    }
370
371    #[inline]
372    pub fn timestamp(&self) -> u32 {
373        self.timestamp
374    }
375
376    #[inline]
377    fn set_location(&mut self, offset: u64, length: u64) {
378        self.location = (((offset & 0xFFFFFF) as u32) << 8) | ((length & 0xFF) as u32);
379    }
380
381    #[inline]
382    fn set_timestamp(&mut self, timestamp: u32) {
383        self.timestamp = timestamp;
384    }
385
386    fn set_timestamp_now(&mut self) {
387        self.set_timestamp(SystemTime::now()
388            .duration_since(SystemTime::UNIX_EPOCH)
389            .map(|dur| dur.as_secs() as u32)
390            .unwrap_or(0));
391    }
392
393}
394
395
396#[derive(Copy, Clone, Debug)]
397pub enum CompressionMethod {
398    Gzip,
399    Zlib,
400    None
401}
402
403impl CompressionMethod {
404
405    pub fn get_id(self, external: bool) -> u8 {
406        (match self {
407            CompressionMethod::Gzip => 1,
408            CompressionMethod::Zlib => 2,
409            CompressionMethod::None => 3
410        }) + if external { 128 } else { 0 }
411    }
412
413    pub fn from_id(mut id: u8) -> Option<(Self, bool)> {
414
415        let external = id > 128;
416        if external {
417            id -= 128;
418        }
419
420        Some((
421            match id {
422                1 => CompressionMethod::Gzip,
423                2 => CompressionMethod::Zlib,
424                3 => CompressionMethod::None,
425                _ => return None
426            },
427            external
428        ))
429
430    }
431
432}
433
434impl Default for CompressionMethod {
435    fn default() -> Self {
436        Self::Zlib
437    }
438}
439
440
441/// A generic chunk writer for every compression method,
442/// might be slower than `LatestChunkWriter`.
443pub struct ChunkWriter<'region> {
444    cx: i32,
445    cz: i32,
446    region: &'region mut RegionFile,
447    inner: ChunkWriterInner
448}
449
450pub enum ChunkWriterInner {
451    Gzip(GzEncoder<Vec<u8>>),
452    Zlib(ZlibEncoder<Vec<u8>>),
453    None(Vec<u8>)
454}
455
456impl ChunkWriter<'_> {
457
458    #[inline]
459    fn inner_write(&mut self) -> &mut dyn Write {
460        match &mut self.inner {
461            ChunkWriterInner::Gzip(encoder) => encoder,
462            ChunkWriterInner::Zlib(encoder) => encoder,
463            ChunkWriterInner::None(vec) => vec
464        }
465    }
466
467    /// Finalize and write internal buffered data to the region.
468    pub fn write_chunk(&mut self) -> RegionResult<()> {
469
470        self.inner_write().flush()?;
471
472        let (data, method) = match &self.inner {
473            ChunkWriterInner::Gzip(encoder) => (encoder.get_ref(), CompressionMethod::Gzip),
474            ChunkWriterInner::Zlib(encoder) => (encoder.get_ref(), CompressionMethod::Zlib),
475            ChunkWriterInner::None(vec) => (vec, CompressionMethod::None)
476        };
477
478        self.region.write_chunk(self.cx, self.cz, &data[..], method)
479
480    }
481
482}
483
484impl Write for ChunkWriter<'_> {
485
486    fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
487        self.inner_write().write(buf)
488    }
489
490    fn flush(&mut self) -> IoResult<()> {
491        self.inner_write().flush()
492    }
493
494    fn write_all(&mut self, buf: &[u8]) -> IoResult<()> {
495        self.inner_write().write_all(buf)
496    }
497
498    fn write_fmt(&mut self, fmt: Arguments<'_>) -> IoResult<()> {
499        self.inner_write().write_fmt(fmt)
500    }
501
502}
503
504
505#[inline]
506fn calc_chunk_index_from_pos(cx: i32, cz: i32) -> usize {
507    (cx & 31) as usize | (((cz & 31) as usize) << 5)
508}
509
510#[inline]
511fn get_region_file_path(dir: &PathBuf, rx: i32, rz: i32) -> PathBuf {
512    dir.join(format!("r.{}.{}.mca", rx, rz))
513}
514
515#[inline]
516fn get_chunk_file_path(dir: &PathBuf, cx: i32, cz: i32) -> PathBuf {
517    dir.join(format!("c.{}.{}.mcc", cx, cz))
518}
519
520#[inline]
521fn fill_sectors(sectors: &mut BitVec, from: usize, length: usize, value: bool) {
522    for sector in from..(from + length) {
523        sectors.set(sector, value);
524    }
525}
526
527#[inline]
528pub fn calc_region_pos(cx: i32, cz: i32) -> (i32, i32) {
529    (cx >> 5, cz >> 5)
530}