ck3save/
file.rs

1use crate::{
2    flavor::{flavor_from_tape, Ck3BinaryFlavor},
3    Ck3Error, Ck3ErrorKind, Ck3Melter, Encoding, SaveHeader,
4};
5use jomini::{
6    binary::{BinaryDeserializerBuilder, FailedResolveStrategy, TokenResolver},
7    text::ObjectReader,
8    BinaryDeserializer, BinaryTape, TextDeserializer, TextTape, Utf8Encoding,
9};
10use serde::Deserialize;
11use std::io::Cursor;
12use zip::result::ZipError;
13
14enum FileKind<'a> {
15    Text(&'a [u8]),
16    Binary(&'a [u8]),
17    Zip {
18        archive: Ck3ZipFiles<'a>,
19        metadata: &'a [u8],
20        gamestate: VerifiedIndex,
21        is_text: bool,
22    },
23}
24
25/// Entrypoint for parsing CK3 saves
26///
27/// Only consumes enough data to determine encoding of the file
28pub struct Ck3File<'a> {
29    header: SaveHeader,
30    kind: FileKind<'a>,
31}
32
33impl<'a> Ck3File<'a> {
34    /// Creates a CK3 file from a slice of data
35    pub fn from_slice(data: &[u8]) -> Result<Ck3File, Ck3Error> {
36        let header = SaveHeader::from_slice(data)?;
37        let data = &data[header.header_len()..];
38
39        let reader = Cursor::new(data);
40        match zip::ZipArchive::new(reader) {
41            Ok(mut zip) => {
42                let metadata = &data[..zip.offset() as usize];
43                let files = Ck3ZipFiles::new(&mut zip, data);
44                let gamestate_idx = files
45                    .gamestate_index()
46                    .ok_or(Ck3ErrorKind::ZipMissingEntry)?;
47
48                let is_text = !header.kind().is_binary();
49                Ok(Ck3File {
50                    header,
51                    kind: FileKind::Zip {
52                        archive: files,
53                        gamestate: gamestate_idx,
54                        metadata,
55                        is_text,
56                    },
57                })
58            }
59            Err(ZipError::InvalidArchive(_)) => {
60                if header.kind().is_binary() {
61                    Ok(Ck3File {
62                        header,
63                        kind: FileKind::Binary(data),
64                    })
65                } else {
66                    Ok(Ck3File {
67                        header,
68                        kind: FileKind::Text(data),
69                    })
70                }
71            }
72            Err(e) => Err(Ck3ErrorKind::ZipArchive(e).into()),
73        }
74    }
75
76    /// Returns the detected decoding of the file
77    pub fn encoding(&self) -> Encoding {
78        match &self.kind {
79            FileKind::Text(_) => Encoding::Text,
80            FileKind::Binary(_) => Encoding::Binary,
81            FileKind::Zip { is_text, .. } if *is_text => Encoding::TextZip,
82            FileKind::Zip { .. } => Encoding::BinaryZip,
83        }
84    }
85
86    /// Returns the size of the file
87    ///
88    /// The size includes the inflated size of the zip
89    pub fn size(&self) -> usize {
90        match &self.kind {
91            FileKind::Text(x) | FileKind::Binary(x) => x.len(),
92            FileKind::Zip { gamestate, .. } => gamestate.size,
93        }
94    }
95
96    pub fn parse_metadata(&self) -> Result<Ck3ParsedFile<'a>, Ck3Error> {
97        match &self.kind {
98            FileKind::Text(x) => {
99                // The metadata section should be way smaller than the total
100                // length so if the total data isn't significantly bigger (2x or
101                // more), assume that the header doesn't accurately represent
102                // the metadata length. Like maybe someone accidentally
103                // converted the line endings from unix to dos.
104                let len = self.header.metadata_len() as usize;
105                let data = if len * 2 > x.len() { x } else { &x[..len] };
106
107                let text = Ck3Text::from_raw(data)?;
108                Ok(Ck3ParsedFile {
109                    kind: Ck3ParsedFileKind::Text(text),
110                })
111            }
112            FileKind::Binary(x) => {
113                let metadata = x.get(..self.header.metadata_len() as usize).unwrap_or(x);
114                let binary = Ck3Binary::from_raw(metadata, self.header.clone())?;
115                Ok(Ck3ParsedFile {
116                    kind: Ck3ParsedFileKind::Binary(binary),
117                })
118            }
119            FileKind::Zip {
120                metadata, is_text, ..
121            } if *is_text => {
122                let text = Ck3Text::from_raw(metadata)?;
123                Ok(Ck3ParsedFile {
124                    kind: Ck3ParsedFileKind::Text(text),
125                })
126            }
127            FileKind::Zip { metadata, .. } => {
128                let binary = Ck3Binary::from_raw(metadata, self.header.clone())?;
129                Ok(Ck3ParsedFile {
130                    kind: Ck3ParsedFileKind::Binary(binary),
131                })
132            }
133        }
134    }
135
136    /// Parses the entire file
137    ///
138    /// If the file is a zip, the zip contents will be inflated into the zip
139    /// sink before being parsed
140    pub fn parse(&self, zip_sink: &'a mut Vec<u8>) -> Result<Ck3ParsedFile<'a>, Ck3Error> {
141        match &self.kind {
142            FileKind::Text(x) => {
143                let text = Ck3Text::from_raw(x)?;
144                Ok(Ck3ParsedFile {
145                    kind: Ck3ParsedFileKind::Text(text),
146                })
147            }
148            FileKind::Binary(x) => {
149                let binary = Ck3Binary::from_raw(x, self.header.clone())?;
150                Ok(Ck3ParsedFile {
151                    kind: Ck3ParsedFileKind::Binary(binary),
152                })
153            }
154            FileKind::Zip {
155                archive,
156                gamestate,
157                is_text,
158                ..
159            } => {
160                let zip = archive.retrieve_file(*gamestate);
161                zip.read_to_end(zip_sink)?;
162
163                if *is_text {
164                    let text = Ck3Text::from_raw(zip_sink)?;
165                    Ok(Ck3ParsedFile {
166                        kind: Ck3ParsedFileKind::Text(text),
167                    })
168                } else {
169                    let binary = Ck3Binary::from_raw(zip_sink, self.header.clone())?;
170                    Ok(Ck3ParsedFile {
171                        kind: Ck3ParsedFileKind::Binary(binary),
172                    })
173                }
174            }
175        }
176    }
177}
178
179/// Contains the parsed Ck3 file
180pub enum Ck3ParsedFileKind<'a> {
181    /// The Ck3 file as text
182    Text(Ck3Text<'a>),
183
184    /// The Ck3 file as binary
185    Binary(Ck3Binary<'a>),
186}
187
188/// An Ck3 file that has been parsed
189pub struct Ck3ParsedFile<'a> {
190    kind: Ck3ParsedFileKind<'a>,
191}
192
193impl<'a> Ck3ParsedFile<'a> {
194    /// Returns the file as text
195    pub fn as_text(&self) -> Option<&Ck3Text> {
196        match &self.kind {
197            Ck3ParsedFileKind::Text(x) => Some(x),
198            _ => None,
199        }
200    }
201
202    /// Returns the file as binary
203    pub fn as_binary(&self) -> Option<&Ck3Binary> {
204        match &self.kind {
205            Ck3ParsedFileKind::Binary(x) => Some(x),
206            _ => None,
207        }
208    }
209
210    /// Returns the kind of file (binary or text)
211    pub fn kind(&self) -> &Ck3ParsedFileKind {
212        &self.kind
213    }
214
215    /// Prepares the file for deserialization into a custom structure
216    pub fn deserializer(&self) -> Ck3Deserializer {
217        match &self.kind {
218            Ck3ParsedFileKind::Text(x) => Ck3Deserializer {
219                kind: Ck3DeserializerKind::Text(x),
220            },
221            Ck3ParsedFileKind::Binary(x) => Ck3Deserializer {
222                kind: Ck3DeserializerKind::Binary(x.deserializer()),
223            },
224        }
225    }
226}
227
228#[derive(Debug, Clone, Copy)]
229struct VerifiedIndex {
230    data_start: usize,
231    data_end: usize,
232    size: usize,
233}
234
235#[derive(Debug, Clone)]
236struct Ck3ZipFiles<'a> {
237    archive: &'a [u8],
238    gamestate_index: Option<VerifiedIndex>,
239}
240
241impl<'a> Ck3ZipFiles<'a> {
242    pub fn new(archive: &mut zip::ZipArchive<Cursor<&'a [u8]>>, data: &'a [u8]) -> Self {
243        let mut gamestate_index = None;
244
245        for index in 0..archive.len() {
246            if let Ok(file) = archive.by_index_raw(index) {
247                let size = file.size() as usize;
248                let data_start = file.data_start() as usize;
249                let data_end = data_start + file.compressed_size() as usize;
250
251                if file.name() == "gamestate" {
252                    gamestate_index = Some(VerifiedIndex {
253                        data_start,
254                        data_end,
255                        size,
256                    })
257                }
258            }
259        }
260
261        Self {
262            archive: data,
263            gamestate_index,
264        }
265    }
266
267    pub fn retrieve_file(&self, index: VerifiedIndex) -> Ck3ZipFile {
268        let raw = &self.archive[index.data_start..index.data_end];
269        Ck3ZipFile {
270            raw,
271            size: index.size,
272        }
273    }
274
275    pub fn gamestate_index(&self) -> Option<VerifiedIndex> {
276        self.gamestate_index
277    }
278}
279
280struct Ck3ZipFile<'a> {
281    raw: &'a [u8],
282    size: usize,
283}
284
285impl<'a> Ck3ZipFile<'a> {
286    pub fn read_to_end(&self, buf: &mut Vec<u8>) -> Result<(), Ck3Error> {
287        let start_len = buf.len();
288        buf.resize(start_len + self.size(), 0);
289        let body = &mut buf[start_len..];
290        crate::deflate::inflate_exact(self.raw, body).map_err(Ck3ErrorKind::from)?;
291        Ok(())
292    }
293
294    pub fn size(&self) -> usize {
295        self.size
296    }
297}
298
299/// A parsed Ck3 text document
300pub struct Ck3Text<'a> {
301    tape: TextTape<'a>,
302}
303
304impl<'a> Ck3Text<'a> {
305    pub fn from_slice(data: &'a [u8]) -> Result<Self, Ck3Error> {
306        let header = SaveHeader::from_slice(data)?;
307        Self::from_raw(&data[..header.header_len()])
308    }
309
310    pub(crate) fn from_raw(data: &'a [u8]) -> Result<Self, Ck3Error> {
311        let tape = TextTape::from_slice(data).map_err(Ck3ErrorKind::Parse)?;
312        Ok(Ck3Text { tape })
313    }
314
315    pub fn reader(&self) -> ObjectReader<Utf8Encoding> {
316        self.tape.utf8_reader()
317    }
318
319    pub fn deserialize<T>(&self) -> Result<T, Ck3Error>
320    where
321        T: Deserialize<'a>,
322    {
323        let result =
324            TextDeserializer::from_utf8_tape(&self.tape).map_err(Ck3ErrorKind::Deserialize)?;
325        Ok(result)
326    }
327}
328
329/// A parsed Ck3 binary document
330pub struct Ck3Binary<'a> {
331    tape: BinaryTape<'a>,
332    header: SaveHeader,
333}
334
335impl<'a> Ck3Binary<'a> {
336    pub fn from_slice(data: &'a [u8]) -> Result<Self, Ck3Error> {
337        let header = SaveHeader::from_slice(data)?;
338        Self::from_raw(&data[..header.header_len()], header)
339    }
340
341    pub(crate) fn from_raw(data: &'a [u8], header: SaveHeader) -> Result<Self, Ck3Error> {
342        let tape = BinaryTape::from_slice(data).map_err(Ck3ErrorKind::Parse)?;
343        Ok(Ck3Binary { tape, header })
344    }
345
346    pub fn deserializer<'b>(&'b self) -> Ck3BinaryDeserializer<'a, 'b> {
347        Ck3BinaryDeserializer {
348            builder: BinaryDeserializer::builder_flavor(flavor_from_tape(&self.tape)),
349            tape: &self.tape,
350        }
351    }
352
353    pub fn melter<'b>(&'b self) -> Ck3Melter<'a, 'b> {
354        Ck3Melter::new(&self.tape, &self.header)
355    }
356}
357
358enum Ck3DeserializerKind<'a, 'b> {
359    Text(&'b Ck3Text<'a>),
360    Binary(Ck3BinaryDeserializer<'a, 'b>),
361}
362
363/// A deserializer for custom structures
364pub struct Ck3Deserializer<'a, 'b> {
365    kind: Ck3DeserializerKind<'a, 'b>,
366}
367
368impl<'a, 'b> Ck3Deserializer<'a, 'b> {
369    pub fn on_failed_resolve(&mut self, strategy: FailedResolveStrategy) -> &mut Self {
370        if let Ck3DeserializerKind::Binary(x) = &mut self.kind {
371            x.on_failed_resolve(strategy);
372        }
373        self
374    }
375
376    pub fn build<T, R>(&self, resolver: &'a R) -> Result<T, Ck3Error>
377    where
378        R: TokenResolver,
379        T: Deserialize<'a>,
380    {
381        match &self.kind {
382            Ck3DeserializerKind::Text(x) => x.deserialize(),
383            Ck3DeserializerKind::Binary(x) => x.build(resolver),
384        }
385    }
386}
387
388/// Deserializes binary data into custom structures
389pub struct Ck3BinaryDeserializer<'a, 'b> {
390    builder: BinaryDeserializerBuilder<Box<dyn Ck3BinaryFlavor>>,
391    tape: &'b BinaryTape<'a>,
392}
393
394impl<'a, 'b> Ck3BinaryDeserializer<'a, 'b> {
395    pub fn on_failed_resolve(&mut self, strategy: FailedResolveStrategy) -> &mut Self {
396        self.builder.on_failed_resolve(strategy);
397        self
398    }
399
400    pub fn build<T, R>(&self, resolver: &'a R) -> Result<T, Ck3Error>
401    where
402        R: TokenResolver,
403        T: Deserialize<'a>,
404    {
405        let result = self
406            .builder
407            .from_tape(self.tape, resolver)
408            .map_err(|e| match e.kind() {
409                jomini::ErrorKind::Deserialize(e2) => match e2.kind() {
410                    &jomini::DeserializeErrorKind::UnknownToken { token_id } => {
411                        Ck3ErrorKind::UnknownToken { token_id }
412                    }
413                    _ => Ck3ErrorKind::Deserialize(e),
414                },
415                _ => Ck3ErrorKind::Deserialize(e),
416            })?;
417        Ok(result)
418    }
419}