use crc::crc32;
use io_partition::PartitionMutex;
use pmd_sir0::{Sir0, Sir0Error};
use std::collections::HashMap;
use std::io;
use std::io::{Read, Seek, SeekFrom};
use std::sync::{Arc, Mutex};
use std::string::FromUtf16Error;
use std::fmt::Display;
use std::fmt;
use std::error::Error;
#[derive(Debug)]
pub enum FarcError {
IOerror(io::Error),
InvalidType(u32),
PartitionCreationError(io::Error),
CreateSir0Error(Sir0Error),
UnsuportedFat5Type(u32),
Poisoned,
NamedFileNotFound(String),
HashedFileNotFound(u32),
FromUtf16Error(FromUtf16Error),
}
impl Error for FarcError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::IOerror(err) | Self::PartitionCreationError(err) => Some(err),
Self::CreateSir0Error(err) => Some(err),
Self::FromUtf16Error(err) => Some(err),
Self::InvalidType(_) | Self::UnsuportedFat5Type(_) | Self::Poisoned | Self::NamedFileNotFound(_) | Self::HashedFileNotFound(_) => None,
}
}
}
impl Display for FarcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IOerror(_) => write!(f, "An error occured while performing an IO operation"),
Self::InvalidType(id) => write!(f, "The type of the file is not reconized: found type {}", id),
Self::PartitionCreationError(_) => write!(f, "An error happened while creating a partition of the file"),
Self::CreateSir0Error(_) => write!(f, "An error happened while creating a Sir0 file"),
Self::UnsuportedFat5Type(id) => write!(f, "The fat5 type is not supported: found {}", id),
Self::Poisoned => write!(f, "The mutex guarding the access to the file is poisoned"),
Self::NamedFileNotFound(name) => write!(f, "The file with name \"{}\" does not exist", name),
Self::HashedFileNotFound(hash) => write!(f, "The file with the hash \"{}\" does not exist", hash),
Self::FromUtf16Error(_) => write!(f, "An error happened while parsing an utf-16 string"),
}
}
}
impl From<FromUtf16Error> for FarcError {
fn from(err: FromUtf16Error) -> Self {
Self::FromUtf16Error(err)
}
}
impl From<io::Error> for FarcError {
fn from(err: io::Error) -> Self {
Self::IOerror(err)
}
}
fn read_u32_le<T: Read>(file: &mut T) -> Result<u32, FarcError> {
let mut buffer = [0; 4];
file.read_exact(&mut buffer)?;
Ok(u32::from_le_bytes(buffer))
}
fn read_u16_le<T: Read>(file: &mut T) -> Result<u16, FarcError> {
let mut buffer = [0; 2];
file.read_exact(&mut buffer)?;
Ok(u16::from_le_bytes(buffer))
}
fn read_null_terminated_utf16_string<T: Read>(file: &mut T) -> Result<String, FarcError> {
let mut buffer: Vec<u16> = Vec::new();
loop {
let chara = read_u16_le(file)?;
if chara == 0 {
break;
};
buffer.push(chara);
}
Ok(String::from_utf16(&buffer)?)
}
#[derive(Debug, Clone)]
struct FarcFile {
start: u32,
length: u32,
}
impl FarcFile {
fn new(start: u32, length: u32) -> Self {
Self { start, length }
}
}
fn string_to_utf16(to_transform: &str) -> Vec<u8> {
to_transform
.encode_utf16()
.map(|chara| chara.to_le_bytes().to_vec())
.flatten()
.collect()
}
#[derive(Debug, Default)]
struct FileNameIndex {
file_data: Vec<FarcFile>,
name_crc32: HashMap<u32, usize>,
name_string: HashMap<String, usize>,
}
impl FileNameIndex {
fn add_file_with_hash(&mut self, hash: u32, offset: u32, length: u32) {
let file_id = self.file_data.len();
self.file_data.push(FarcFile::new(offset, length));
self.name_crc32.insert(hash, file_id);
}
fn add_file_with_name(&mut self, name: String, offset: u32, lenght: u32) {
let file_id = self.file_data.len();
self.file_data.push(FarcFile::new(offset, lenght));
self.name_string.insert(name, file_id);
}
fn check_file_name(&mut self, name: &str) -> bool {
let name_encoded_utf16 = string_to_utf16(name);
let hash = crc32::checksum_ieee(&name_encoded_utf16);
if self.name_crc32.contains_key(&hash) {
trace!("found a corresponding hash: {} <-> {}", hash, name);
let file_id = self.name_crc32.remove(&hash).unwrap(); if self.name_string.insert(name.to_string(), file_id).is_some() {
panic!("hash mismach !!! Prove you shouldn't use crc32...");
};
true
} else {
if log_enabled!(log::Level::Trace) && !self.name_string.contains_key(name) {
trace!("hash not found in the file name: {} for {}", hash, name);
}
false
}
}
fn get_named_file_data(&self, name: &str) -> Option<FarcFile> {
self.file_data
.get(match self.name_string.get(name) {
Some(value) => *value,
None => match self.name_crc32.get(&crc32::checksum_ieee(&string_to_utf16(name))) {
Some(value) => *value,
None => return None,
}
}).cloned()
}
fn get_unnamed_file_data(&self, hash: u32) -> Option<FarcFile> {
self.file_data
.get(match self.name_crc32.get(&hash) {
Some(value) => *value,
None => return None,
}).cloned()
}
}
#[derive(Debug)]
pub struct Farc<F: Read + Seek> {
file: Arc<Mutex<F>>,
index: FileNameIndex,
}
impl<F: Read + Seek> Farc<F> {
pub fn new(file: F) -> Result<Self, FarcError> {
let file = Arc::new(Mutex::new(file));
let sir0_type;
let sir0_offset;
let sir0_lenght;
let all_data_offset;
{
let mut file = file.lock().unwrap(); file.seek(SeekFrom::Start(0x20))?;
sir0_type = read_u32_le(&mut *file)?;
if sir0_type != 4 && sir0_type != 5 {
return Err(FarcError::InvalidType(sir0_type));
}
sir0_offset = read_u32_le(&mut *file)?;
sir0_lenght = read_u32_le(&mut *file)?;
all_data_offset = read_u32_le(&mut *file)?;
}
let sir0_partition = io_partition::PartitionMutex::new(
file.clone(),
sir0_offset as u64,
sir0_lenght as u64,
)
.map_err(FarcError::PartitionCreationError)?;
let mut sir0 = Sir0::new(sir0_partition).map_err(FarcError::CreateSir0Error)?;
let h = sir0.get_header();
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 - 1) {
sir0_file.seek(
SeekFrom::Start(sir0_data_offset as u64 + (file_index * entry_lenght) as u64),
)?;
let filename_offset_or_hash = read_u32_le(&mut sir0_file)?;
let data_offset = read_u32_le(&mut sir0_file)?;
let data_length = read_u32_le(&mut sir0_file)?;
match sir0_fat5_type {
0 => {
sir0_file.seek(
SeekFrom::Start(filename_offset_or_hash as u64),
)?;
let name = read_null_terminated_utf16_string(&mut sir0_file)?;
index.add_file_with_name(
name,
all_data_offset + data_offset,
data_length
);
}
1 => index.add_file_with_hash(
filename_offset_or_hash,
all_data_offset + data_offset,
data_length,
),
x => return Err(FarcError::UnsuportedFat5Type(x)),
};
}
Ok(Self {
file,
index,
})
}
pub fn file_count(&self) -> usize {
self.file_count_hashed() + self.file_count_named()
}
pub fn file_count_hashed(&self) -> usize {
self.index.name_crc32.len()
}
pub fn file_count_named(&self) -> usize {
self.index.name_string.len()
}
pub fn iter_name(&self) -> std::vec::IntoIter<&String> {
self.index
.name_string
.iter()
.map(|x| x.0)
.collect::<Vec<_>>()
.into_iter()
}
pub fn iter_hash(&self) -> std::vec::IntoIter<u32> {
self.index
.name_crc32
.iter()
.map(|x| *x.0)
.collect::<Vec<_>>()
.into_iter()
}
pub fn get_named_file(&self, name: &str) -> Result<PartitionMutex<F>, FarcError> {
let file_data = match self.index.get_named_file_data(name) {
Some(value) => value,
None => return Err(FarcError::NamedFileNotFound(name.to_string())),
};
self.create_partition_from_data(file_data)
}
pub fn get_unnamed_file(&self, hash: u32) -> Result<PartitionMutex<F>, FarcError> {
let file_data = match self.index.get_unnamed_file_data(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(),
file_data.start as u64,
file_data.length as u64,
)
.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);
}
}
}