use crate::Error; /// A cursor over little-endian data. #[derive(Debug, Clone)] pub(crate) struct Reader<'a> { bytes: &'a [u8], position: usize, } impl<'a> Reader<'a> { pub fn new(bytes: &'a [u8]) -> Self { Self { bytes, position: 0 } } pub fn at(bytes: &'a [u8], position: usize) -> Self { Self { bytes, position } } pub fn position(&self) -> usize { self.position } pub fn bytes(&mut self, len: usize) -> Result<&'a [u8], Error> { let bytes = self .bytes .get(self.position..) .and_then(|rest| rest.get(..len)) .ok_or(Error::Invalid("ended early"))?; self.position += len; Ok(bytes) } pub fn skip(&mut self, len: usize) -> Result<(), Error> { self.bytes(len).map(|_| ()) } pub fn array(&mut self) -> Result<[u8; N], Error> { Ok(self.bytes(N)?.try_into().expect("read N bytes")) } pub fn u8(&mut self) -> Result { Ok(self.bytes(1)?[0]) } pub fn u32(&mut self) -> Result { self.array().map(u32::from_le_bytes) } pub fn i32(&mut self) -> Result { self.array().map(i32::from_le_bytes) } pub fn u64(&mut self) -> Result { self.array().map(u64::from_le_bytes) } /// An Unreal `FString`: a length, then that many UTF-8 bytes, or UTF-16 /// units if it's negative, with a terminator either way. pub fn fstring(&mut self) -> Result { self.fstring_with(|_| {}) } /// An `FString` whose UTF-16 units, but not its terminator, are first /// passed through `decode`. pub fn fstring_with(&mut self, decode: impl FnOnce(&mut [u8])) -> Result { let len = self.i32()?; if len == 0 { return Ok(String::new()); } if len > 0 { let bytes = self.bytes(len as usize)?; let (text, terminator) = bytes.split_at(bytes.len() - 1); if terminator != [0] { return Err(Error::Invalid("string isn't terminated")); } return String::from_utf8(text.to_vec()) .map_err(|_| Error::Invalid("string isn't UTF-8")); } let bytes = self.bytes(len.unsigned_abs() as usize * 2)?; let (text, terminator) = bytes.split_at(bytes.len() - 2); if terminator != [0, 0] { return Err(Error::Invalid("string isn't terminated")); } let mut text = text.to_vec(); decode(&mut text); let units: Vec = text .as_chunks::<2>() .0 .iter() .map(|&unit| u16::from_le_bytes(unit)) .collect(); String::from_utf16(&units).map_err(|_| Error::Invalid("string isn't UTF-16")) } } #[cfg(test)] mod tests { use pretty_assertions::assert_eq; use super::*; #[test] fn test_fstring() { let utf8 = [4, 0, 0, 0, b'a', b'b', b'c', 0]; assert_eq!(Reader::new(&utf8).fstring().unwrap(), "abc"); let utf16 = [0xfd, 0xff, 0xff, 0xff, b'h', 0, b'i', 0, 0, 0]; assert_eq!(Reader::new(&utf16).fstring().unwrap(), "hi"); assert_eq!(Reader::new(&[0, 0, 0, 0]).fstring().unwrap(), ""); let unterminated = [3, 0, 0, 0, b'a', b'b', b'c']; assert!(Reader::new(&unterminated).fstring().is_err()); } }