1use crate::error::SaveError;
4
5const BLOCK_MAGIC: u32 = 0xFEEDA1E5;
7
8const BLOCK_HEADER_SIZE: usize = 0x10;
10
11const MAX_CHUNK_SIZE: usize = 0x80000;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SaveFormat {
17 Lz4Compressed,
19 PlaintextJson,
21}
22
23#[derive(Debug, Clone, Copy)]
25struct BlockHeader {
26 compressed_size: u32,
27 decompressed_size: u32,
28}
29
30pub fn detect_format(data: &[u8]) -> SaveFormat {
35 if data.len() >= 2 && data[0] == 0x7B && data[1] == 0x22 {
36 SaveFormat::PlaintextJson
37 } else {
38 SaveFormat::Lz4Compressed
39 }
40}
41
42pub fn decompress_save(data: &[u8]) -> Result<Vec<u8>, SaveError> {
50 match detect_format(data) {
51 SaveFormat::PlaintextJson => Ok(data.to_vec()),
52 SaveFormat::Lz4Compressed => decompress_blocks(data),
53 }
54}
55
56pub fn decompress_save_file(path: &std::path::Path) -> Result<Vec<u8>, SaveError> {
58 let data = std::fs::read(path)?;
59 decompress_save(&data)
60}
61
62fn parse_block_header(data: &[u8], offset: usize) -> Result<BlockHeader, SaveError> {
64 if offset + BLOCK_HEADER_SIZE > data.len() {
65 return Err(SaveError::UnexpectedEof {
66 offset,
67 expected: BLOCK_HEADER_SIZE,
68 });
69 }
70
71 let magic = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
72 if magic != BLOCK_MAGIC {
73 return Err(SaveError::InvalidMagic {
74 offset,
75 found: magic,
76 });
77 }
78
79 let compressed_size = u32::from_le_bytes(data[offset + 4..offset + 8].try_into().unwrap());
80 let decompressed_size = u32::from_le_bytes(data[offset + 8..offset + 12].try_into().unwrap());
81
82 if decompressed_size > MAX_CHUNK_SIZE as u32 {
83 return Err(SaveError::ChunkTooLarge {
84 offset,
85 declared: decompressed_size,
86 });
87 }
88
89 Ok(BlockHeader {
90 compressed_size,
91 decompressed_size,
92 })
93}
94
95fn decompress_blocks(data: &[u8]) -> Result<Vec<u8>, SaveError> {
97 let mut output = Vec::new();
98 let mut offset: usize = 0;
99
100 while offset < data.len() {
101 let header = parse_block_header(data, offset)?;
102 offset += BLOCK_HEADER_SIZE;
103
104 let payload_end = offset + header.compressed_size as usize;
105 if payload_end > data.len() {
106 return Err(SaveError::UnexpectedEof {
107 offset,
108 expected: header.compressed_size as usize,
109 });
110 }
111
112 let compressed = &data[offset..payload_end];
113
114 let decompressed =
115 lz4_flex::block::decompress(compressed, header.decompressed_size as usize).map_err(
116 |e| SaveError::DecompressionFailed {
117 offset: offset - BLOCK_HEADER_SIZE,
118 message: e.to_string(),
119 },
120 )?;
121
122 output.extend_from_slice(&decompressed);
123 offset = payload_end;
124 }
125
126 Ok(output)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn create_test_save(json: &[u8]) -> Vec<u8> {
135 let mut output = Vec::new();
136 for chunk in json.chunks(MAX_CHUNK_SIZE) {
137 let compressed = lz4_flex::block::compress(chunk);
138 let compressed_size = compressed.len() as u32;
139 let decompressed_size = chunk.len() as u32;
140
141 output.extend_from_slice(&BLOCK_MAGIC.to_le_bytes());
142 output.extend_from_slice(&compressed_size.to_le_bytes());
143 output.extend_from_slice(&decompressed_size.to_le_bytes());
144 output.extend_from_slice(&0u32.to_le_bytes());
145 output.extend_from_slice(&compressed);
146 }
147 output
148 }
149
150 #[test]
151 fn detect_plaintext_json() {
152 let data = br#"{"Version": 6726}"#;
153 assert_eq!(detect_format(data), SaveFormat::PlaintextJson);
154 }
155
156 #[test]
157 fn detect_lz4_compressed() {
158 let mut data = vec![0xE5, 0xA1, 0xED, 0xFE];
159 data.extend_from_slice(&[0; 12]);
160 assert_eq!(detect_format(&data), SaveFormat::Lz4Compressed);
161 }
162
163 #[test]
164 fn detect_empty_input() {
165 assert_eq!(detect_format(&[]), SaveFormat::Lz4Compressed);
166 }
167
168 #[test]
169 fn parse_block_header_valid() {
170 let mut header = Vec::new();
171 header.extend_from_slice(&BLOCK_MAGIC.to_le_bytes());
172 header.extend_from_slice(&100u32.to_le_bytes());
173 header.extend_from_slice(&200u32.to_le_bytes());
174 header.extend_from_slice(&0u32.to_le_bytes());
175
176 let bh = parse_block_header(&header, 0).unwrap();
177 assert_eq!(bh.compressed_size, 100);
178 assert_eq!(bh.decompressed_size, 200);
179 }
180
181 #[test]
182 fn parse_block_header_invalid_magic() {
183 let mut header = Vec::new();
184 header.extend_from_slice(&0xDEADBEEFu32.to_le_bytes());
185 header.extend_from_slice(&[0; 12]);
186
187 let err = parse_block_header(&header, 0).unwrap_err();
188 match err {
189 SaveError::InvalidMagic { offset, found } => {
190 assert_eq!(offset, 0);
191 assert_eq!(found, 0xDEADBEEF);
192 }
193 _ => panic!("expected InvalidMagic, got {err:?}"),
194 }
195 }
196
197 #[test]
198 fn parse_block_header_truncated() {
199 let data = [0u8; 8];
200 let err = parse_block_header(&data, 0).unwrap_err();
201 match err {
202 SaveError::UnexpectedEof { offset, expected } => {
203 assert_eq!(offset, 0);
204 assert_eq!(expected, BLOCK_HEADER_SIZE);
205 }
206 _ => panic!("expected UnexpectedEof, got {err:?}"),
207 }
208 }
209
210 #[test]
211 fn parse_block_header_chunk_too_large() {
212 let mut header = Vec::new();
213 header.extend_from_slice(&BLOCK_MAGIC.to_le_bytes());
214 header.extend_from_slice(&100u32.to_le_bytes());
215 header.extend_from_slice(&(MAX_CHUNK_SIZE as u32 + 1).to_le_bytes());
216 header.extend_from_slice(&0u32.to_le_bytes());
217
218 let err = parse_block_header(&header, 0).unwrap_err();
219 match err {
220 SaveError::ChunkTooLarge { offset, declared } => {
221 assert_eq!(offset, 0);
222 assert_eq!(declared, MAX_CHUNK_SIZE as u32 + 1);
223 }
224 _ => panic!("expected ChunkTooLarge, got {err:?}"),
225 }
226 }
227
228 #[test]
229 fn roundtrip_single_block() {
230 let json = br#"{"Version": 6726, "Platform": "PC"}"#;
231 let save = create_test_save(json);
232 let result = decompress_save(&save).unwrap();
233 assert_eq!(&result, json);
234 }
235
236 #[test]
237 fn roundtrip_multiple_blocks() {
238 let big_json = format!(r#"{{"data": "{}"}}"#, "x".repeat(MAX_CHUNK_SIZE + 1000));
239 let save = create_test_save(big_json.as_bytes());
240 let result = decompress_save(&save).unwrap();
241 assert_eq!(result, big_json.as_bytes());
242 }
243
244 #[test]
245 fn plaintext_passthrough() {
246 let json = br#"{"Version": 6726}"#;
247 let result = decompress_save(json).unwrap();
248 assert_eq!(&result, json);
249 }
250
251 #[test]
252 fn truncated_payload() {
253 let mut data = Vec::new();
254 data.extend_from_slice(&BLOCK_MAGIC.to_le_bytes());
255 data.extend_from_slice(&1000u32.to_le_bytes());
256 data.extend_from_slice(&2000u32.to_le_bytes());
257 data.extend_from_slice(&0u32.to_le_bytes());
258 data.extend_from_slice(&[0u8; 10]);
259
260 let err = decompress_save(&data).unwrap_err();
261 assert!(matches!(err, SaveError::UnexpectedEof { .. }));
262 }
263
264 #[test]
265 fn invalid_magic_at_second_block() {
266 let json = b"hello";
267 let compressed = lz4_flex::block::compress(json);
268 let mut data = Vec::new();
269
270 data.extend_from_slice(&BLOCK_MAGIC.to_le_bytes());
272 data.extend_from_slice(&(compressed.len() as u32).to_le_bytes());
273 data.extend_from_slice(&(json.len() as u32).to_le_bytes());
274 data.extend_from_slice(&0u32.to_le_bytes());
275 data.extend_from_slice(&compressed);
276
277 let bad_magic: u32 = 0xBAD;
279 data.extend_from_slice(&bad_magic.to_le_bytes());
280 data.extend_from_slice(&[0u8; 12]);
281
282 let err = decompress_save(&data).unwrap_err();
283 match err {
284 SaveError::InvalidMagic { offset, .. } => {
285 assert_eq!(offset, BLOCK_HEADER_SIZE + compressed.len());
286 }
287 _ => panic!("expected InvalidMagic, got {err:?}"),
288 }
289 }
290
291 #[test]
292 fn decompress_save_file_not_found() {
293 let err = decompress_save_file(std::path::Path::new("/nonexistent/save.hg")).unwrap_err();
294 assert!(matches!(err, SaveError::Io(_)));
295 }
296}