barotrauma_compress/
lib.rs1use flate2::Compression;
8use flate2::bufread::GzDecoder;
9use flate2::write::GzEncoder;
10use std::fs::File;
11use std::io::{BufReader, BufWriter, Read, Write};
12use std::mem::MaybeUninit;
13use std::path::{Path, PathBuf};
14use std::{fs, io};
15use zerocopy::IntoBytes;
16
17const INITIAL_FILENAME_BUFFER_SIZE: usize = 256;
20
21macro_rules! debug_println {
24 ($($arg:tt)*) => (#[cfg(debug_assertions)] println!($($arg)*));
25}
26
27pub fn decompress<P: AsRef<Path>>(file_path: P) -> Result<(), String> {
30 let file = File::open(&file_path).map_err(|e| format!("Could not open save file: {}", e))?;
32 let gzip_input = BufReader::new(file);
33 debug_println!("fs read buffer size = {}", gzip_input.capacity());
35 let mut input = GzDecoder::new(gzip_input);
36
37 let directory_path: PathBuf = file_path
39 .as_ref()
40 .parent()
41 .ok_or("Could not get parent directory of save file")?
42 .join(
43 file_path
44 .as_ref()
45 .file_stem()
46 .ok_or("Could not remove extension from save file")?,
47 );
48 debug_println!("directory_path = {}", directory_path.display());
49 fs::create_dir(&directory_path).map_err(|e| {
50 format!(
51 "Could not create target directory \"{}\": {}",
52 directory_path.display(),
53 e
54 )
55 })?;
56
57 let mut length_buffer: [u8; 4] = unsafe {
60 #[allow(clippy::uninit_assumed_init, invalid_value)]
61 MaybeUninit::uninit().assume_init()
62 };
63
64 let mut filename_buffer = Vec::<u16>::with_capacity(INITIAL_FILENAME_BUFFER_SIZE);
66
67 loop {
68 if input.read_exact(&mut length_buffer).is_err() {
70 break;
73 }
74
75 let filename_length: usize = u32::from_le_bytes(length_buffer) as usize;
76 debug_println!("filename_size = {}", filename_length * 2);
77
78 if filename_buffer.capacity() < filename_length {
80 filename_buffer.reserve(filename_length - filename_buffer.capacity());
81 }
82
83 unsafe {
86 filename_buffer.set_len(filename_length);
87 }
88
89 input
91 .read_exact(filename_buffer.as_mut_bytes())
92 .map_err(|e| format!("Reached end of stream unexpectedly when reading filename: {}", e))?;
93 let filename =
94 String::from_utf16(&filename_buffer).map_err(|e| format!("Filename was not valid UTF-16: {}", e))?;
95 debug_println!("Decoded filename: {}", filename);
96
97 input
99 .read_exact(&mut length_buffer)
100 .map_err(|e| format!("Reached end of stream unexpectedly when reading file length: {}", e))?;
101 let file_length: u64 = u32::from_le_bytes(length_buffer) as u64;
102 debug_println!("file_length = {}", file_length);
103
104 let output_file_path = directory_path.join(&filename);
106 let mut output_file =
107 File::create(&output_file_path).map_err(|e| format!("Unable to create output file: {}", e))?;
108
109 let mut output_file_reader = input.take(file_length);
110
111 let bytes_written = io::copy(&mut output_file_reader, &mut output_file)
112 .map_err(|e| format!("Error writing decompressed file: {}", e))?;
113 assert_eq!(bytes_written, file_length, "unexpected number of bytes written to file");
114 input = output_file_reader.into_inner();
115
116 debug_println!("wrote {}", output_file_path.display());
117 }
118
119 Ok(())
120}
121
122pub fn compress<P: AsRef<Path>>(directory_path: P) -> Result<(), String> {
125 let file_path: PathBuf = directory_path.as_ref().with_extension("save");
126
127 let mut input_file_paths = Vec::with_capacity(2);
129 for entry in fs::read_dir(directory_path).map_err(|e| format!("Unable to enumerate input directory: {}", e))? {
130 let entry =
131 entry.map_err(|e| format!("Unable to read an entry while enumerating the input directory: {}", e))?;
132 let path = entry.path();
133 if !path.is_file() {
134 return Err(format!("Unable to compress nested directories: \"{}\"", path.display()));
136 }
137 input_file_paths.push(path);
138 }
139
140 if file_path.exists() {
142 return Err(format!("Target file \"{}\" already exists", file_path.display()));
143 }
144
145 let output_file = File::create(file_path).map_err(|e| format!("Unable to create output file: {}", e))?;
147 let gzip_output = BufWriter::new(output_file);
149 debug_println!("fs write buffer size = {}", gzip_output.capacity());
150 let mut output = GzEncoder::new(gzip_output, Compression::default());
152
153 let mut filename_buffer = Vec::<u16>::with_capacity(INITIAL_FILENAME_BUFFER_SIZE);
155
156 for input_file_path in input_file_paths {
158 debug_println!("processing: {}", input_file_path.display());
159 let mut input_file = File::open(&input_file_path).map_err(|e| format!("Unable to open input file: {}", e))?;
160
161 let input_filename = input_file_path
163 .file_name()
164 .ok_or("Unable to extract filename of input file")?
165 .to_str()
166 .ok_or("Unable to convert input filename to unicode")?;
167 let input_filename_length = input_filename.len() as u32;
168 filename_buffer.clear();
169 filename_buffer.extend(input_filename.encode_utf16());
170
171 let input_filename_length_prefix = input_filename_length.to_le_bytes();
173 output
174 .write_all(&input_filename_length_prefix)
175 .map_err(|e| format!("Unable to write filename length prefix to save: {}", e))?;
176
177 output
179 .write_all(filename_buffer.as_bytes())
180 .map_err(|e| format!("Unable to write filename to save: {}", e))?;
181
182 let file_size = input_file
184 .metadata()
185 .map_err(|e| format!("Unable to read metadata for input file: {}", e))?
186 .len();
187 let file_size_prefix: u32 = file_size.try_into().map_err(|e| {
188 format!(
189 "Input file too long (blame the Baro devs for their 4GB filesize limit): {}",
190 e
191 )
192 })?;
193 let file_size_prefix = file_size_prefix.to_le_bytes();
194 output
195 .write_all(&file_size_prefix)
196 .map_err(|e| format!("Unable to write filesize prefix to save: {}", e))?;
197
198 io::copy(&mut input_file, &mut output).map_err(|e| format!("Error writing input file to save: {}", e))?;
200 }
201
202 output
204 .flush()
205 .map_err(|e| format!("Error flushing save to disk: {}", e))?;
206 Ok(())
207}