use flate2::Compression;
use flate2::bufread::GzDecoder;
use flate2::write::GzEncoder;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Write};
use std::mem::MaybeUninit;
use std::path::{Path, PathBuf};
use std::{fs, io};
use zerocopy::IntoBytes;
const INITIAL_FILENAME_BUFFER_SIZE: usize = 256;
macro_rules! debug_println {
($($arg:tt)*) => (#[cfg(debug_assertions)] println!($($arg)*));
}
pub fn decompress<P: AsRef<Path>>(file_path: P) -> Result<(), String> {
let file = File::open(&file_path).map_err(|e| format!("Could not open save file: {}", e))?;
let gzip_input = BufReader::new(file);
debug_println!("fs read buffer size = {}", gzip_input.capacity());
let mut input = GzDecoder::new(gzip_input);
let directory_path: PathBuf = file_path
.as_ref()
.parent()
.ok_or("Could not get parent directory of save file")?
.join(
file_path
.as_ref()
.file_stem()
.ok_or("Could not remove extension from save file")?,
);
debug_println!("directory_path = {}", directory_path.display());
fs::create_dir(&directory_path).map_err(|e| {
format!(
"Could not create target directory \"{}\": {}",
directory_path.display(),
e
)
})?;
let mut length_buffer: [u8; 4] = unsafe {
#[allow(clippy::uninit_assumed_init, invalid_value)]
MaybeUninit::uninit().assume_init()
};
let mut filename_buffer = Vec::<u16>::with_capacity(INITIAL_FILENAME_BUFFER_SIZE);
loop {
if input.read_exact(&mut length_buffer).is_err() {
break;
}
let filename_length: usize = u32::from_le_bytes(length_buffer) as usize;
debug_println!("filename_size = {}", filename_length * 2);
if filename_buffer.capacity() < filename_length {
filename_buffer.reserve(filename_length - filename_buffer.capacity());
}
unsafe {
filename_buffer.set_len(filename_length);
}
input
.read_exact(filename_buffer.as_mut_bytes())
.map_err(|e| format!("Reached end of stream unexpectedly when reading filename: {}", e))?;
let filename =
String::from_utf16(&filename_buffer).map_err(|e| format!("Filename was not valid UTF-16: {}", e))?;
debug_println!("Decoded filename: {}", filename);
input
.read_exact(&mut length_buffer)
.map_err(|e| format!("Reached end of stream unexpectedly when reading file length: {}", e))?;
let file_length: u64 = u32::from_le_bytes(length_buffer) as u64;
debug_println!("file_length = {}", file_length);
let output_file_path = directory_path.join(&filename);
let mut output_file =
File::create(&output_file_path).map_err(|e| format!("Unable to create output file: {}", e))?;
let mut output_file_reader = input.take(file_length);
let bytes_written = io::copy(&mut output_file_reader, &mut output_file)
.map_err(|e| format!("Error writing decompressed file: {}", e))?;
assert_eq!(bytes_written, file_length, "unexpected number of bytes written to file");
input = output_file_reader.into_inner();
debug_println!("wrote {}", output_file_path.display());
}
Ok(())
}
pub fn compress<P: AsRef<Path>>(directory_path: P) -> Result<(), String> {
let file_path: PathBuf = directory_path.as_ref().with_extension("save");
let mut input_file_paths = Vec::with_capacity(2);
for entry in fs::read_dir(directory_path).map_err(|e| format!("Unable to enumerate input directory: {}", e))? {
let entry =
entry.map_err(|e| format!("Unable to read an entry while enumerating the input directory: {}", e))?;
let path = entry.path();
if !path.is_file() {
return Err(format!("Unable to compress nested directories: \"{}\"", path.display()));
}
input_file_paths.push(path);
}
if file_path.exists() {
return Err(format!("Target file \"{}\" already exists", file_path.display()));
}
let output_file = File::create(file_path).map_err(|e| format!("Unable to create output file: {}", e))?;
let gzip_output = BufWriter::new(output_file);
debug_println!("fs write buffer size = {}", gzip_output.capacity());
let mut output = GzEncoder::new(gzip_output, Compression::default());
let mut filename_buffer = Vec::<u16>::with_capacity(INITIAL_FILENAME_BUFFER_SIZE);
for input_file_path in input_file_paths {
debug_println!("processing: {}", input_file_path.display());
let mut input_file = File::open(&input_file_path).map_err(|e| format!("Unable to open input file: {}", e))?;
let input_filename = input_file_path
.file_name()
.ok_or("Unable to extract filename of input file")?
.to_str()
.ok_or("Unable to convert input filename to unicode")?;
let input_filename_length = input_filename.len() as u32;
filename_buffer.clear();
filename_buffer.extend(input_filename.encode_utf16());
let input_filename_length_prefix = input_filename_length.to_le_bytes();
output
.write_all(&input_filename_length_prefix)
.map_err(|e| format!("Unable to write filename length prefix to save: {}", e))?;
output
.write_all(filename_buffer.as_bytes())
.map_err(|e| format!("Unable to write filename to save: {}", e))?;
let file_size = input_file
.metadata()
.map_err(|e| format!("Unable to read metadata for input file: {}", e))?
.len();
let file_size_prefix: u32 = file_size.try_into().map_err(|e| {
format!(
"Input file too long (blame the Baro devs for their 4GB filesize limit): {}",
e
)
})?;
let file_size_prefix = file_size_prefix.to_le_bytes();
output
.write_all(&file_size_prefix)
.map_err(|e| format!("Unable to write filesize prefix to save: {}", e))?;
io::copy(&mut input_file, &mut output).map_err(|e| format!("Error writing input file to save: {}", e))?;
}
output
.flush()
.map_err(|e| format!("Error flushing save to disk: {}", e))?;
Ok(())
}