use crate::{FarcFile, FileNameError, FileNameIndex};
use binread::{BinRead, BinReaderExt};
use byteorder::{ReadBytesExt, LE};
use io_partition::PartitionMutex;
use pmd_sir0::{Sir0, Sir0Error};
use std::io::{self, Read, Seek, SeekFrom};
use std::string::FromUtf16Error;
use std::sync::{Arc, Mutex};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FarcError {
#[error("An error occured while performing an IO operation")]
IOerror(#[from] io::Error),
#[error("An error happened while creating a partition of the file")]
PartitionCreationError(io::Error),
#[error("An error happened while creating a Sir0 file")]
CreateSir0Error(#[from] Sir0Error),
#[error("The fat5 type is not supported: found {0}")]
UnsuportedFat5Type(u32),
#[error("The mutex guarding the access to the file is poisoned")]
Poisoned,
#[error("The file with name \"{0}\" does not exist")]
NamedFileNotFound(String),
#[error("The file with the hash \"{0}\" does not exist")]
HashedFileNotFound(u32),
#[error("An error happened while parsing an utf-16 string")]
FromUtf16Error(#[from] FromUtf16Error),
#[error("An error happened while parsing the header of the file")]
ReadHeaderError(#[source] binread::Error),
#[error("The sir0 header isn't long enought. It should be (at least) 12 bytes, but it only have {0} bytes")]
Sir0HeaderNotLongEnought(usize),
#[error("A contained file is position overflow a u32 integer ({0}+{1})")]
DataStartOverflow(u32, u32),
#[error("A conflict between two file happened")]
FileNameError(#[from] FileNameError),
#[error("A sub-file doesn't seem to start at an offset that is a multiple of 16. FARC seem to require this.")]
FileStartBadAlignement,
}
fn read_null_terminated_utf16_string<T: Read>(file: &mut T) -> Result<String, FarcError> {
let mut buffer: Vec<u16> = Vec::new();
loop {
let chara = file.read_u16::<LE>()?;
if chara == 0 {
break;
};
buffer.push(chara);
}
Ok(String::from_utf16(&buffer)?)
}
#[derive(BinRead)]
#[br(little)]
enum Sir0Type {
#[br(magic = 4u32)]
Type4,
#[br(magic = 5u32)]
Type5,
}
#[derive(BinRead)]
#[br(magic = b"FARC", little)]
struct FarcHeader {
_unk_1: [u8; 0x1C],
_sir0_type: Sir0Type,
sir0_offset: u32,
sir0_lenght: u32,
all_data_offset: u32,
_lenght_of_all_data: u32,
}
#[derive(Debug)]
pub struct Farc<F: Read + Seek> {
file: Arc<Mutex<F>>,
index: FileNameIndex,
}
impl<F: Read + Seek> Farc<F> {
pub fn new(mut file: F) -> Result<Self, FarcError> {
let farc_header: FarcHeader = file.read_le().map_err(FarcError::ReadHeaderError)?;
let file = Arc::new(Mutex::new(file));
let sir0_partition = PartitionMutex::new(
file.clone(),
u64::from(farc_header.sir0_offset),
u64::from(farc_header.sir0_lenght),
)
.map_err(FarcError::PartitionCreationError)?;
let mut sir0 = Sir0::new(sir0_partition).map_err(FarcError::CreateSir0Error)?;
let h = sir0.get_header();
if h.len() < 12 {
return Err(FarcError::Sir0HeaderNotLongEnought(h.len()));
};
let sir0_data_offset = u32::from_le_bytes([h[0], h[1], h[2], h[3]]);
let file_count = u32::from_le_bytes([h[4], h[5], h[6], h[7]]);
let sir0_fat5_type = u32::from_le_bytes([h[8], h[9], h[10], h[11]]);
let entry_lenght = match sir0_fat5_type {
0 => 12, 1 => 12,
x => return Err(FarcError::UnsuportedFat5Type(x)),
};
let mut index = FileNameIndex::default();
let mut sir0_file = sir0.get_file();
for file_index in 0..(file_count) {
sir0_file.seek(SeekFrom::Start(
u64::from(sir0_data_offset) + u64::from(file_index * entry_lenght),
))?;
let filename_offset_or_hash = sir0_file.read_u32::<LE>()?;
let data_offset = sir0_file.read_u32::<LE>()?;
let data_length = sir0_file.read_u32::<LE>()?;
let data_start = farc_header
.all_data_offset
.checked_add(data_offset)
.map_or_else(
|| {
Err(FarcError::DataStartOverflow(
farc_header.all_data_offset,
data_offset,
))
},
Ok,
)?;
if data_start % 16 != 0 {
return Err(FarcError::FileStartBadAlignement);
};
match sir0_fat5_type {
0 => {
sir0_file.seek(SeekFrom::Start(u64::from(filename_offset_or_hash)))?;
let name = read_null_terminated_utf16_string(&mut sir0_file)?;
index.add_file_with_name(name, data_start, data_length)?;
}
1 => index.add_file_with_hash(filename_offset_or_hash, data_start, data_length)?,
x => return Err(FarcError::UnsuportedFat5Type(x)),
};
}
Ok(Self { file, index })
}
#[must_use]
pub fn file_count(&self) -> usize {
self.index.len()
}
#[must_use]
pub fn file_unknown_name(&self) -> usize {
self.index.iter().filter(|f| f.name.is_none()).count()
}
#[must_use]
pub fn file_known_name(&self) -> usize {
self.index.iter().filter(|f| f.name.is_some()).count()
}
pub fn iter_name(&self) -> impl Iterator<Item = &String> {
self.index.iter().filter_map(|e| e.name.as_ref())
}
pub fn iter_hash_unknown_name(&self) -> impl Iterator<Item = &u32> {
self.index.iter().filter_map(|e| {
if e.name.is_some() {
None
} else {
Some(&e.name_hash)
}
})
}
pub fn iter(&self) -> impl Iterator<Item = (u32, Option<&String>)> {
self.index.iter().map(|f| (f.name_hash, f.name.as_ref()))
}
pub fn iter_all_hash(&self) -> impl Iterator<Item = &u32> {
self.index.iter().map(|e| &e.name_hash)
}
pub fn get_named_file(&self, name: &str) -> Result<PartitionMutex<F>, FarcError> {
let file_data = match self.index.get_file_by_name(name) {
Some(value) => value,
None => return Err(FarcError::NamedFileNotFound(name.to_string())),
};
self.create_partition_from_data(file_data)
}
pub fn get_hashed_file(&self, hash: u32) -> Result<PartitionMutex<F>, FarcError> {
let file_data = match self.index.get_file_by_hash(hash) {
Some(value) => value,
None => return Err(FarcError::HashedFileNotFound(hash)),
};
self.create_partition_from_data(file_data)
}
fn create_partition_from_data(
&self,
file_data: &FarcFile,
) -> Result<PartitionMutex<F>, FarcError> {
PartitionMutex::new(
self.file.clone(),
u64::from(file_data.start),
u64::from(file_data.length),
)
.map_err(FarcError::PartitionCreationError)
}
pub fn check_file_name(&mut self, name: &str) -> bool {
self.index.check_file_name(name)
}
pub fn check_file_name_iter<T: Iterator<Item = String>>(&mut self, iter: T) {
for value in iter {
self.check_file_name(&value);
}
}
}