| 1 | use crate::{Error, Object, ObjectData, Reference, Store, bytes::Cursor}; |
| 2 | |
| 3 | #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] |
| 4 | pub enum FileDataReference { |
| 5 | Internal([u8; 16]), |
| 6 | External(String), |
| 7 | Invalid, |
| 8 | } |
| 9 | |
| 10 | fn 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 | |
| 31 | impl 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 | |
| 56 | impl 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 | |
| 81 | impl<'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`. |
| 142 | pub(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 | } |