1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
use crate::cc;
use core::mem;
use std::io::Read;

/// The file format for a given archive.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FileFormat {
    TES3,
    TES4,
    FO4,
}

const BSA: u32 = cc::make_four(b"BSA");
const BTDX: u32 = cc::make_four(b"BTDX");

/// Guesses the archive format for a given source.
///
/// This function does not guarantee that the given source constitutes a well-formed archive of the deduced format. It merely remarks that if the file were a well-formed archive, it would be of the deduced format.
#[allow(clippy::module_name_repetitions)]
pub fn guess_format<In>(source: &mut In) -> Option<FileFormat>
where
    In: ?Sized + Read,
{
    let mut buf = [0u8; mem::size_of::<u32>()];
    source.read_exact(&mut buf).ok()?;
    let magic = u32::from_le_bytes(buf);
    match magic {
        0x100 => Some(FileFormat::TES3),
        BSA => Some(FileFormat::TES4),
        BTDX => Some(FileFormat::FO4),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use crate::FileFormat;
    use anyhow::Context as _;
    use std::{fs::File, path::Path};

    #[test]
    fn guess() -> anyhow::Result<()> {
        let root = Path::new("data/common_guess_test");
        let tests = [
            (FileFormat::TES3, "tes3.bsa"),
            (FileFormat::TES4, "tes4.bsa"),
            (FileFormat::FO4, "fo4.ba2"),
        ];

        for (format, file_name) in tests {
            let mut file = File::open(root.join(file_name))
                .with_context(|| format!("failed to open file: {file_name}"))?;
            let guess = crate::guess_format(&mut file);
            assert_eq!(guess, Some(format));
        }

        Ok(())
    }
}