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#[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
43pub type RegionResult<T> = Result<T, RegionError>;
45
46
47pub struct RegionFile {
49 dir: PathBuf,
51 file: File,
53 metadata: [ChunkMetadata; 1024],
55 sectors: BitVec
58}
59
60impl RegionFile {
61
62 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 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 let sectors_count = file_len / SECTOR_SIZE;
107 sectors = BitVec::from_elem(sectors_count as usize - 2, true);
108
109 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 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 #[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 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 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 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 } else {
254 (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 if let Some(free_sector) = first_free_sector {
290 external = true;
291 offset = free_sector as u64;
292 length = 1;
295 } else {
296 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 fill_sectors(&mut self.sectors, offset as usize - 2, length as usize, false);
310
311 metadata.set_location(offset, length);
313
314 }
315
316 metadata.set_timestamp_now();
318 self.write_metadata(metadata_index, metadata)?;
319
320 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
441pub 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 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}