#[cfg(feature = "compressed-fields")]
use std::io;
#[cfg(feature = "compressed-fields")]
use std::io::Read;
#[cfg(feature = "compressed-fields")]
use flate2::read::DeflateDecoder;
use nom::bytes::complete::take;
use nom::combinator::map_res;
use nom::multi::length_data;
use nom::number::complete::{le_u16, le_u32};
use nom::sequence::{pair, preceded, separated_pair};
use nom::{IResult, Parser};
use crate::game_id::GameId;
const SUBRECORD_TYPE_LENGTH: usize = 4;
pub(crate) type SubrecordType = [u8; 4];
#[derive(Clone, PartialEq, Eq, Debug, Hash, Default)]
pub(crate) struct Subrecord {
#[expect(clippy::struct_field_names, reason = "type is a keyword")]
subrecord_type: SubrecordType,
data: Vec<u8>,
is_compressed: bool,
}
impl Subrecord {
pub(crate) fn new(
input: &[u8],
game_id: GameId,
data_length_override: u32,
is_compressed: bool,
) -> IResult<&[u8], Subrecord> {
let (remaining_input, (subrecord_type, data)) =
parse(input, game_id, data_length_override)?;
Ok((
remaining_input,
Subrecord {
subrecord_type,
data: data.to_vec(),
is_compressed,
},
))
}
#[cfg(feature = "compressed-fields")]
pub(crate) fn decompress_data(&self) -> Result<Vec<u8>, io::Error> {
if !self.is_compressed {
return Ok(self.data.clone());
}
let Some(data) = self.data.get(4..) else {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Compressed subrecord is too small",
));
};
let mut deflater = DeflateDecoder::new(data);
let mut decompressed_data: Vec<u8> = Vec::new();
deflater.read_to_end(&mut decompressed_data)?;
Ok(decompressed_data)
}
pub(crate) fn subrecord_type(&self) -> &SubrecordType {
&self.subrecord_type
}
pub(crate) fn data(&self) -> &[u8] {
&self.data
}
}
pub(crate) struct SubrecordRef<'a> {
subrecord_type: SubrecordType,
data: &'a [u8],
}
impl<'a> SubrecordRef<'a> {
pub(crate) fn new(
input: &'a [u8],
game_id: GameId,
data_length_override: u32,
) -> IResult<&'a [u8], SubrecordRef<'a>> {
let (remaining_input, (subrecord_type, data)) =
parse(input, game_id, data_length_override)?;
Ok((
remaining_input,
SubrecordRef {
subrecord_type,
data,
},
))
}
pub(crate) fn subrecord_type(&'a self) -> &'a SubrecordType {
&self.subrecord_type
}
pub(crate) fn data(&self) -> &'a [u8] {
self.data
}
}
fn parse(
input: &[u8],
game_id: GameId,
data_length_override: u32,
) -> IResult<&[u8], (SubrecordType, &[u8])> {
if game_id == GameId::Morrowind {
morrowind_subrecord(input)
} else if data_length_override != 0 {
presized_subrecord(input, data_length_override)
} else {
simple_subrecord(input)
}
}
fn subrecord_type(input: &[u8]) -> IResult<&[u8], SubrecordType> {
map_res(take(SUBRECORD_TYPE_LENGTH), |s: &[u8]| s.try_into()).parse(input)
}
fn morrowind_subrecord(input: &[u8]) -> IResult<&[u8], (SubrecordType, &[u8])> {
(subrecord_type, length_data(le_u32)).parse(input)
}
fn simple_subrecord(input: &[u8]) -> IResult<&[u8], (SubrecordType, &[u8])> {
(subrecord_type, length_data(le_u16)).parse(input)
}
fn presized_subrecord(input: &[u8], data_length: u32) -> IResult<&[u8], (SubrecordType, &[u8])> {
separated_pair(subrecord_type, le_u16, take(data_length)).parse(input)
}
pub(crate) fn parse_subrecord_data_as_u32(input: &[u8]) -> IResult<&[u8], u32> {
preceded(pair(subrecord_type, le_u16), le_u32).parse(input)
}
#[cfg(test)]
mod tests {
mod morrowind {
use super::super::*;
const TES3_DATA_SUBRECORD: &[u8] = &[
0x44, 0x41, 0x54, 0x41, 0x08, 0x00, 0x00, 0x00, 0x6D, 0x63, 0x61, 0x72, 0x6F, 0x66,
0x61, 0x6E,
];
#[test]
fn parse_should_parse_a_subrecord_correctly() {
let subrecord = Subrecord::new(TES3_DATA_SUBRECORD, GameId::Morrowind, 0, false)
.unwrap()
.1;
assert_eq!(b"DATA", &subrecord.subrecord_type);
assert_eq!(&TES3_DATA_SUBRECORD[8..], subrecord.data.as_slice());
}
#[test]
fn parse_should_ignore_data_length_override_subrecords() {
let subrecord = Subrecord::new(TES3_DATA_SUBRECORD, GameId::Morrowind, 5, false)
.unwrap()
.1;
assert_eq!(b"DATA", &subrecord.subrecord_type);
assert_eq!(&TES3_DATA_SUBRECORD[8..], subrecord.data.as_slice());
}
#[test]
fn subrecord_ref_new_should_parse_a_subrecord_correctly() {
let subrecord = SubrecordRef::new(TES3_DATA_SUBRECORD, GameId::Morrowind, 0)
.unwrap()
.1;
assert_eq!(b"DATA", subrecord.subrecord_type());
assert_eq!(&TES3_DATA_SUBRECORD[8..], subrecord.data());
}
}
mod nonmorrowind {
use super::super::*;
const TES4_CNAM_SUBRECORD: &[u8] = &[
0x43, 0x4E, 0x41, 0x4D, 0x0A, 0x00, 0x6D, 0x63, 0x61, 0x72, 0x6F, 0x66, 0x61, 0x6E,
0x6F, 0x00,
];
#[test]
fn parse_should_parse_a_subrecord_with_no_data_length_override_correctly() {
let subrecord = Subrecord::new(TES4_CNAM_SUBRECORD, GameId::Skyrim, 0, false)
.unwrap()
.1;
assert_eq!(b"CNAM", &subrecord.subrecord_type);
assert_eq!(&TES4_CNAM_SUBRECORD[6..], subrecord.data.as_slice());
}
#[test]
fn parse_should_use_data_length_override_if_non_zero() {
let subrecord = Subrecord::new(TES4_CNAM_SUBRECORD, GameId::Oblivion, 4, false)
.unwrap()
.1;
assert_eq!(b"CNAM", &subrecord.subrecord_type);
assert_eq!(&TES4_CNAM_SUBRECORD[6..10], subrecord.data.as_slice());
let subrecord = Subrecord::new(TES4_CNAM_SUBRECORD, GameId::Skyrim, 4, false)
.unwrap()
.1;
assert_eq!(b"CNAM", &subrecord.subrecord_type);
assert_eq!(&TES4_CNAM_SUBRECORD[6..10], subrecord.data.as_slice());
let subrecord = Subrecord::new(TES4_CNAM_SUBRECORD, GameId::Fallout3, 4, false)
.unwrap()
.1;
assert_eq!(b"CNAM", &subrecord.subrecord_type);
assert_eq!(&TES4_CNAM_SUBRECORD[6..10], subrecord.data.as_slice());
let subrecord = Subrecord::new(TES4_CNAM_SUBRECORD, GameId::FalloutNV, 4, false)
.unwrap()
.1;
assert_eq!(b"CNAM", &subrecord.subrecord_type);
assert_eq!(&TES4_CNAM_SUBRECORD[6..10], subrecord.data.as_slice());
let subrecord = Subrecord::new(TES4_CNAM_SUBRECORD, GameId::Fallout4, 4, false)
.unwrap()
.1;
assert_eq!(b"CNAM", &subrecord.subrecord_type);
assert_eq!(&TES4_CNAM_SUBRECORD[6..10], subrecord.data.as_slice());
}
#[test]
fn subrecord_ref_new_should_parse_a_subrecord_correctly() {
let subrecord = SubrecordRef::new(TES4_CNAM_SUBRECORD, GameId::Skyrim, 0)
.unwrap()
.1;
assert_eq!(b"CNAM", subrecord.subrecord_type());
assert_eq!(&TES4_CNAM_SUBRECORD[6..], subrecord.data());
}
#[test]
#[cfg(feature = "compressed-fields")]
fn decompress_data_should_read_a_compressed_subrecord_correctly() {
const DATA: &[u8] = &[
0x42, 0x50, 0x54, 0x4E, 0x1D, 0x00, 0x19, 0x00, 0x00, 0x00, 0x75, 0xc5, 0x21, 0x0d, 0x00, 0x00, 0x08, 0x05, 0xd1,
0x6c, 0x6c, 0xdc, 0x57, 0x48, 0x3c, 0xfd, 0x5b, 0x5c, 0x02,
0xd4, 0x6b, 0x32, 0xb5, 0xdc, 0xa3, ];
let subrecord = Subrecord::new(DATA, GameId::Skyrim, 0, true).unwrap().1;
let decompressed_data = subrecord.decompress_data().unwrap();
assert_eq!(b"BPTN", &subrecord.subrecord_type);
assert_eq!(
"DEFLATE_DEFLATE_DEFLATE_DEFLATE".as_bytes(),
decompressed_data.as_slice()
);
}
#[test]
#[cfg(feature = "compressed-fields")]
fn decompress_data_should_error_if_the_compressed_data_is_invalid() {
const DATA: &[u8] = &[
0x42, 0x50, 0x54, 0x4E, 0x1D, 0x00, 0x19, 0x00, 0x00, 0x00, 0x75, 0xc5, 0x21, 0x0d, 0x00, 0x00, 0xA8, 0x05, 0xd1,
0x6c, 0x6c, 0xdc, 0x57, 0x48, 0x3c, 0xfd, 0x5b, 0x5c, 0x02,
0xd4, 0x6b, 0x32, 0xb5, 0xdc, 0xa3, ];
let subrecord = Subrecord::new(DATA, GameId::Skyrim, 0, true).unwrap().1;
assert!(subrecord.decompress_data().is_err());
}
#[test]
#[cfg(feature = "compressed-fields")]
fn decompress_data_should_error_if_the_compressed_data_is_too_small() {
const DATA: &[u8] = &[
0x42, 0x50, 0x54, 0x4E, 0x02, 0x00, 0x19, 0x00, ];
let subrecord = Subrecord::new(DATA, GameId::Skyrim, 0, true).unwrap().1;
assert!(subrecord.decompress_data().is_err());
}
}
}