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