Skip to main content

dream_ini/plugin/
tes3.rs

1// SPDX-License-Identifier: GPL-3.0-only
2
3use std::fs::File;
4use std::io::{self, Read, Seek, SeekFrom};
5use std::path::Path;
6
7use crate::{ImportError, TextEncoding};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct PluginHeader {
11    pub name: String,
12    pub masters: Vec<String>,
13}
14
15pub(crate) fn read_header(
16    path: &Path,
17    encoding: TextEncoding,
18) -> Result<PluginHeader, ImportError> {
19    let mut file = File::open(path).map_err(|source| ImportError::Io {
20        path: path.to_owned(),
21        source,
22    })?;
23    let mut record_header = [0; 16];
24    read_exact_plugin(
25        &mut file,
26        path,
27        &mut record_header,
28        "unexpected end of file",
29    )?;
30
31    if &record_header[0..4] != b"TES3" {
32        return Err(ImportError::InvalidPluginHeader {
33            path: path.to_owned(),
34            message: "missing TES3 record".to_owned(),
35        });
36    }
37
38    let record_size = u64::from(u32::from_le_bytes(
39        record_header[4..8]
40            .try_into()
41            .expect("slice length checked"),
42    ));
43    let record_end =
44        16u64
45            .checked_add(record_size)
46            .ok_or_else(|| ImportError::InvalidPluginHeader {
47                path: path.to_owned(),
48                message: "TES3 record size overflow".to_owned(),
49            })?;
50
51    let file_len = file
52        .metadata()
53        .map_err(|source| ImportError::Io {
54            path: path.to_owned(),
55            source,
56        })?
57        .len();
58    if file_len < record_end {
59        return Err(ImportError::InvalidPluginHeader {
60            path: path.to_owned(),
61            message: "TES3 record extends past end of file".to_owned(),
62        });
63    }
64
65    let mut offset = 16u64;
66    let mut masters = Vec::new();
67
68    while offset + 8 <= record_end {
69        let (name, size) = read_subrecord_header(&mut file, path)?;
70        offset += 8;
71
72        let subrecord_end =
73            offset
74                .checked_add(size)
75                .ok_or_else(|| ImportError::InvalidPluginHeader {
76                    path: path.to_owned(),
77                    message: "subrecord size overflow".to_owned(),
78                })?;
79        if subrecord_end > record_end {
80            return Err(ImportError::InvalidPluginHeader {
81                path: path.to_owned(),
82                message: "subrecord extends past TES3 record".to_owned(),
83            });
84        }
85
86        if name == *b"MAST" {
87            let mut data = vec![
88                0;
89                usize::try_from(size).map_err(|_| {
90                    ImportError::InvalidPluginHeader {
91                        path: path.to_owned(),
92                        message: "subrecord size does not fit in memory".to_owned(),
93                    }
94                })?
95            ];
96            read_exact_plugin(
97                &mut file,
98                path,
99                &mut data,
100                "TES3 record extends past end of file",
101            )?;
102            masters.push(read_c_string(&data, encoding));
103        } else {
104            skip_subrecord_data(&mut file, path, size)?;
105        }
106
107        offset = subrecord_end;
108    }
109
110    if offset != record_end {
111        return Err(ImportError::InvalidPluginHeader {
112            path: path.to_owned(),
113            message: "trailing partial subrecord header in TES3 record".to_owned(),
114        });
115    }
116
117    Ok(PluginHeader {
118        name: path.file_name().map_or_else(
119            || path.display().to_string(),
120            |name| name.to_string_lossy().into_owned(),
121        ),
122        masters,
123    })
124}
125
126fn read_subrecord_header(file: &mut File, path: &Path) -> Result<([u8; 4], u64), ImportError> {
127    let mut header = [0; 8];
128    read_exact_plugin(
129        file,
130        path,
131        &mut header,
132        "TES3 record extends past end of file",
133    )?;
134    let name = header[0..4].try_into().expect("slice length checked");
135    let size = u64::from(u32::from_le_bytes(
136        header[4..8].try_into().expect("slice length checked"),
137    ));
138    Ok((name, size))
139}
140
141fn skip_subrecord_data(file: &mut File, path: &Path, size: u64) -> Result<(), ImportError> {
142    let offset = i64::try_from(size).map_err(|_| ImportError::InvalidPluginHeader {
143        path: path.to_owned(),
144        message: "subrecord size does not fit in seek offset".to_owned(),
145    })?;
146    file.seek(SeekFrom::Current(offset))
147        .map(|_| ())
148        .map_err(|source| ImportError::Io {
149            path: path.to_owned(),
150            source,
151        })
152}
153
154fn read_exact_plugin(
155    file: &mut File,
156    path: &Path,
157    buffer: &mut [u8],
158    eof_message: &str,
159) -> Result<(), ImportError> {
160    file.read_exact(buffer).map_err(|source| {
161        if source.kind() == io::ErrorKind::UnexpectedEof {
162            ImportError::InvalidPluginHeader {
163                path: path.to_owned(),
164                message: eof_message.to_owned(),
165            }
166        } else {
167            ImportError::Io {
168                path: path.to_owned(),
169                source,
170            }
171        }
172    })
173}
174
175fn read_c_string(bytes: &[u8], encoding: TextEncoding) -> String {
176    let end = bytes
177        .iter()
178        .position(|byte| *byte == 0)
179        .unwrap_or(bytes.len());
180    let (decoded, _, _) = encoding.encoding_rs().decode(&bytes[..end]);
181    decoded.into_owned()
182}