use std::io::{BufRead, Seek};
use nom::bytes::complete::{tag, take};
use nom::combinator::{all_consuming, map};
use nom::number::complete::le_u32;
use nom::sequence::delimited;
use nom::{IResult, Parser};
use crate::error::Error;
use crate::game_id::GameId;
use crate::record::Record;
use crate::record_id::RecordId;
use crate::ParsingErrorKind;
const GROUP_TYPE: &[u8] = b"GRUP";
#[derive(Clone, PartialEq, Eq, Debug, Hash, Default)]
pub(crate) struct Group;
impl Group {
pub(crate) fn read_form_ids<R: BufRead + Seek>(
reader: &mut R,
game_id: GameId,
form_ids: &mut Vec<u32>,
header_buffer: &mut [u8],
) -> Result<(), Error> {
let group_header_length = group_or_record_header_length(game_id);
let skip_length = get_header_length_to_skip(game_id);
let Some(header_bytes) = header_buffer.get_mut(..usize::from(group_header_length)) else {
return Err(Error::ParsingError(
header_buffer.to_vec().into_boxed_slice(),
ParsingErrorKind::GenericParserError("Group::read_form_ids".into()),
));
};
reader.read_exact(header_bytes)?;
let (_, size_of_records) =
all_consuming(parse_header(group_header_length, skip_length)).parse(header_bytes)?;
read_records(reader, game_id, form_ids, header_buffer, size_of_records)
}
}
fn group_or_record_header_length(game_id: GameId) -> u8 {
match game_id {
GameId::Oblivion => 20,
_ => 24,
}
}
fn get_header_length_to_skip(game_id: GameId) -> u8 {
match game_id {
GameId::Oblivion => 12,
_ => 16,
}
}
fn parse_header(group_header_length: u8, skip_length: u8) -> impl Fn(&[u8]) -> IResult<&[u8], u32> {
move |input| {
map(
delimited(tag(GROUP_TYPE), le_u32, take(skip_length)),
move |group_size| group_size - u32::from(group_header_length),
)
.parse(input)
}
}
fn read_records<R: BufRead + Seek>(
reader: &mut R,
game_id: GameId,
form_ids: &mut Vec<u32>,
header_buffer: &mut [u8],
size_of_records: u32,
) -> Result<(), Error> {
let header_length = group_or_record_header_length(game_id);
let skip_length = get_header_length_to_skip(game_id);
let parse_header = parse_header(header_length, skip_length);
let mut bytes_read = 0;
while bytes_read < size_of_records {
let Some(header_bytes) = header_buffer.get_mut(..usize::from(header_length)) else {
return Err(Error::ParsingError(
header_buffer.to_vec().into_boxed_slice(),
ParsingErrorKind::GenericParserError("read_records".into()),
));
};
reader.read_exact(header_bytes)?;
bytes_read += u32::from(header_length);
if header_bytes.starts_with(GROUP_TYPE) {
let (_, size_of_records) = all_consuming(&parse_header).parse(header_bytes)?;
read_records(reader, game_id, form_ids, header_buffer, size_of_records)?;
bytes_read += size_of_records;
} else {
let (record_bytes_read, record_id) =
Record::read_record_id(reader, game_id, header_bytes, true)?;
bytes_read += record_bytes_read;
if let Some(RecordId::FormId(form_id)) = record_id {
form_ids.push(form_id.get());
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use crate::record::MAX_RECORD_HEADER_LENGTH;
use super::*;
#[test]
fn new_should_store_formids_for_all_records_in_a_group() {
let data =
&include_bytes!("../testing-plugins/Skyrim/Data/Blank - Master Dependent.esm")[0x56..];
let mut form_ids: Vec<u32> = Vec::new();
let mut header_buf = [0; MAX_RECORD_HEADER_LENGTH];
Group::read_form_ids(
&mut Cursor::new(&data),
GameId::Skyrim,
&mut form_ids,
&mut header_buf,
)
.unwrap();
assert_eq!(8, form_ids.len());
assert!(form_ids.contains(&0xCF0));
assert!(form_ids.contains(&0x0100_0CEB));
assert!(form_ids.contains(&0x0100_0CED));
}
#[test]
fn new_should_store_formids_for_all_records_in_subgroups() {
let data = &include_bytes!("../testing-plugins/Skyrim/Data/Blank.esm")[0x1004C..0x10114];
let mut form_ids: Vec<u32> = Vec::new();
let mut header_buf = [0; MAX_RECORD_HEADER_LENGTH];
Group::read_form_ids(
&mut Cursor::new(&data),
GameId::Skyrim,
&mut form_ids,
&mut header_buf,
)
.unwrap();
assert_eq!(1, form_ids.len());
assert!(form_ids.contains(&0xCF9));
}
}