1use crate::{Error, Object, ObjectData, Reference, Store, bytes::Cursor};
2
3#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
4pub enum FileDataReference {
5 Internal([u8; 16]),
6 External(String),
7 Invalid,
8}
9
10fn guid(text: &str) -> Option<[u8; 16]> {
11 let bytes = text.as_bytes();
12 if bytes.len() != 36 || [8, 13, 18, 23].iter().any(|index| bytes[*index] != b'-') {
13 return None;
14 }
15 let mut digits = bytes
16 .iter()
17 .enumerate()
18 .filter_map(|(index, byte)| (![8, 13, 18, 23].contains(&index)).then_some(*byte));
19 let mut value = [0; 16];
20 for byte in &mut value {
21 let high = char::from(digits.next()?).to_digit(16)?;
22 let low = char::from(digits.next()?).to_digit(16)?;
23 *byte = u8::try_from((high << 4) | low).ok()?;
24 }
25 value[..4].reverse();
26 value[4..6].reverse();
27 value[6..8].reverse();
28 Some(value)
29}
30
31impl Object<'_> {
32 pub fn file_reference(&self) -> Result<Option<FileDataReference>, Error> {
33 let ObjectData::File { reference, .. } = self.data else {
34 return Ok(None);
35 };
36 if !reference.len().is_multiple_of(2) {
37 return Err(Error {
38 offset: 0,
39 message: "Odd UTF-16 file-data reference length",
40 });
41 }
42 let text = String::from_utf16(
43 &reference
44 .chunks_exact(2)
45 .map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
46 .collect::<Vec<_>>(),
47 )
48 .map_err(|_| Error {
49 offset: 0,
50 message: "Invalid UTF-16 file-data reference",
51 })?;
52 text.parse().map(Some)
53 }
54}
55
56impl std::str::FromStr for FileDataReference {
57 type Err = Error;
58
59 fn from_str(text: &str) -> Result<Self, Error> {
60 let parsed = if let Some(name) = text.strip_prefix("<file>") {
61 name.strip_suffix(".onebin")
62 .and_then(guid)
63 .map(|_| FileDataReference::External(name.to_owned()))
64 } else if let Some(id) = text
65 .strip_prefix("<ifndf>{")
66 .and_then(|text| text.strip_suffix('}'))
67 {
68 guid(id).map(FileDataReference::Internal)
69 } else if text == "<invfdo>" {
70 Some(FileDataReference::Invalid)
71 } else {
72 None
73 };
74 parsed.ok_or(Error {
75 offset: 0,
76 message: "Invalid file-data reference syntax",
77 })
78 }
79}
80
81impl<'a> Store<'a> {
82 pub(crate) fn encryption_key(&self, chunk: crate::Chunk) -> Result<&'a [u8], Error> {
83 let bytes = self.chunk_data(chunk)?;
84 let footer = 0x2649294f8e198b3c_u64.to_le_bytes();
85 if bytes.len() >= 16 && bytes[..8] == 0xfb6ba385dad1a067_u64.to_le_bytes() {
86 for padding in 0..=7 {
87 if bytes.len() < 16 + padding {
88 break;
89 }
90 let end = bytes.len() - padding;
91 if bytes[end - 8..end] == footer
92 && bytes[end..].iter().all(|byte| *byte == 0)
93 && (padding == 0 || bytes.len().is_multiple_of(8))
94 {
95 return Ok(&bytes[8..end - 8]);
96 }
97 }
98 }
99 Err(Error {
100 offset: usize::try_from(chunk.offset).unwrap(),
101 message: "Invalid encryption key container",
102 })
103 }
104
105 pub fn file_data(&self, guid: [u8; 16]) -> Result<&'a [u8], Error> {
106 let mut found = None;
107 for node in self
108 .lists
109 .values()
110 .flat_map(|list| &list.nodes)
111 .filter(|node| node.id == 0x94 && !node.freed())
112 {
113 let mut c = node.fields(self);
114 if c.read::<16>()? != guid {
115 continue;
116 }
117 let Some(Reference::Data(chunk)) = node.reference else {
118 return Err(Error {
119 offset: node.offset,
120 message: "File-data object lacks a data reference",
121 });
122 };
123 if found.replace(chunk).is_some() {
124 return Err(Error {
125 offset: node.offset,
126 message: "Duplicate file-data identity",
127 });
128 }
129 }
130 let chunk = found.ok_or(Error {
131 offset: 0,
132 message: "File-data object is not declared",
133 })?;
134 payload(
135 self.chunk_data(chunk)?,
136 usize::try_from(chunk.offset).unwrap(),
137 )
138 }
139}
140
141/// The payload of a file-data store object, whose bytes start at file offset `offset`.
142pub(crate) fn payload(bytes: &[u8], offset: usize) -> Result<&[u8], Error> {
143 let mut c = Cursor { bytes, offset };
144 if c.read::<16>()?
145 != [
146 0xe7, 0x16, 0xe3, 0xbd, 0x65, 0x26, 0x11, 0x45, 0xa4, 0xc4, 0x8d, 0x4d, 0x0b, 0x7a,
147 0x9e, 0xac,
148 ]
149 {
150 return Err(Error {
151 offset: c.offset - 16,
152 message: "Invalid file-data object header",
153 });
154 }
155 let length = usize::try_from(u64::from_le_bytes(c.read()?)).map_err(|_| Error {
156 offset: c.offset - 8,
157 message: "File-data length exceeds address space",
158 })?;
159 c.take(12)?;
160 let bytes = c.take(length)?;
161 // The object length is aligned; its position in the file need not be.
162 let padding = (8 - (36 + length) % 8) % 8;
163 c.take(padding)?;
164 if c.read::<16>()?
165 != [
166 0x22, 0xa7, 0xfb, 0x71, 0x79, 0x0f, 0x0b, 0x4a, 0xbb, 0x13, 0x89, 0x92, 0x56, 0x42,
167 0x6b, 0x24,
168 ]
169 || !c.bytes.is_empty()
170 {
171 return Err(Error {
172 offset: c.offset - 16,
173 message: "Invalid file-data object footer or length",
174 });
175 }
176 Ok(bytes)
177}