1use 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}