From 04a6a3d2c831724beeaf021bdc62226852b2dd84 Mon Sep 17 00:00:00 2001 From: ackwell Date: Fri, 18 Apr 2025 12:07:25 +1000 Subject: [PATCH 1/6] First pass sqpack error update --- ironworks/src/sqpack/error.rs | 31 ++++++++++++++++ ironworks/src/sqpack/file/empty.rs | 7 ++-- ironworks/src/sqpack/file/file.rs | 2 +- ironworks/src/sqpack/file/model.rs | 2 +- ironworks/src/sqpack/file/standard.rs | 4 +-- ironworks/src/sqpack/file/texture.rs | 2 +- ironworks/src/sqpack/index/index.rs | 51 +++++++++++++-------------- ironworks/src/sqpack/index/index1.rs | 9 +++-- ironworks/src/sqpack/index/index2.rs | 4 +-- ironworks/src/sqpack/install.rs | 32 ++++++++--------- ironworks/src/sqpack/mod.rs | 1 + ironworks/src/sqpack/resource.rs | 12 +++---- ironworks/src/sqpack/sqpack.rs | 33 ++++++----------- 13 files changed, 102 insertions(+), 88 deletions(-) create mode 100644 ironworks/src/sqpack/error.rs diff --git a/ironworks/src/sqpack/error.rs b/ironworks/src/sqpack/error.rs new file mode 100644 index 00000000..c143ae61 --- /dev/null +++ b/ironworks/src/sqpack/error.rs @@ -0,0 +1,31 @@ +use std::io; + +#[non_exhaustive] +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("requested file could not be found")] + FileNotFound, + + #[error("provided file path is invalid: {0}")] + PathInvalid(String), + + #[error("file is empty or missing header")] + FileIncomplete(Vec), + + #[error("malformed data encountered")] + Malformed(#[source] Box), + + #[error("I/O error encountered")] + Io(#[from] io::Error), +} + +impl From for Error { + fn from(error: binrw::Error) -> Self { + match error { + binrw::Error::Io(inner) => Self::Io(inner), + error => Self::Malformed(error.into()), + } + } +} + +pub type Result = std::result::Result; diff --git a/ironworks/src/sqpack/file/empty.rs b/ironworks/src/sqpack/file/empty.rs index 57fce40c..5e3a3f53 100644 --- a/ironworks/src/sqpack/file/empty.rs +++ b/ironworks/src/sqpack/file/empty.rs @@ -1,6 +1,6 @@ use std::io::{Empty, Read, Seek}; -use crate::error::{Error, ErrorValue, Result}; +use crate::sqpack::error::{Error, Result}; use super::shared::Header; @@ -15,8 +15,5 @@ pub fn read(reader: impl Read + Seek, header: Header) -> Result { // Empty files can't be read as-is - they're either entirely invalid, or need // further processing that doesn't belong in sqpack specifically. - Err(Error::Invalid( - ErrorValue::File(buf), - String::from("Empty file"), - )) + Err(Error::FileIncomplete(buf)) } diff --git a/ironworks/src/sqpack/file/file.rs b/ironworks/src/sqpack/file/file.rs index 61b76744..ce154d5f 100644 --- a/ironworks/src/sqpack/file/file.rs +++ b/ironworks/src/sqpack/file/file.rs @@ -2,7 +2,7 @@ use std::io::{Cursor, Empty, Read, Seek, SeekFrom}; use binrw::BinRead; -use crate::{error::Result, sqpack::block::BlockStream}; +use crate::sqpack::{block::BlockStream, error::Result}; use super::{ empty, model, diff --git a/ironworks/src/sqpack/file/model.rs b/ironworks/src/sqpack/file/model.rs index b72e8a7c..e1e4a7a5 100644 --- a/ironworks/src/sqpack/file/model.rs +++ b/ironworks/src/sqpack/file/model.rs @@ -2,7 +2,7 @@ use std::io::{self, Cursor, Read, Seek, SeekFrom, Write}; use binrw::{BinRead, BinWriterExt, VecArgs, binread}; -use crate::{error::Result, sqpack::block::read_block}; +use crate::sqpack::{block::read_block, error::Result}; use super::shared::Header; diff --git a/ironworks/src/sqpack/file/standard.rs b/ironworks/src/sqpack/file/standard.rs index 9fc4ce9f..f0187eb2 100644 --- a/ironworks/src/sqpack/file/standard.rs +++ b/ironworks/src/sqpack/file/standard.rs @@ -2,9 +2,9 @@ use std::io::{Read, Seek, SeekFrom}; use binrw::{BinRead, VecArgs, binread}; -use crate::{ +use crate::sqpack::{ + block::{BlockHeader, BlockMetadata, BlockStream}, error::Result, - sqpack::block::{BlockHeader, BlockMetadata, BlockStream}, }; use super::shared::Header; diff --git a/ironworks/src/sqpack/file/texture.rs b/ironworks/src/sqpack/file/texture.rs index 24ed906a..a430a20f 100644 --- a/ironworks/src/sqpack/file/texture.rs +++ b/ironworks/src/sqpack/file/texture.rs @@ -2,7 +2,7 @@ use std::io::{self, Cursor, Read, Seek, SeekFrom}; use binrw::{BinRead, VecArgs, binread}; -use crate::{error::Result, sqpack::block::read_block}; +use crate::sqpack::{block::read_block, error::Result}; use super::shared::Header; diff --git a/ironworks/src/sqpack/index/index.rs b/ironworks/src/sqpack/index/index.rs index 8958643b..c8aa44ef 100644 --- a/ironworks/src/sqpack/index/index.rs +++ b/ironworks/src/sqpack/index/index.rs @@ -3,9 +3,9 @@ use std::sync::{Arc, Mutex}; use binrw::BinRead; use getset::CopyGetters; -use crate::{ - error::{Error, ErrorValue, Result}, - sqpack::Resource, +use crate::sqpack::{ + Resource, + error::{Error, Result}, }; use super::{index1::Index1, index2::Index2, shared::FileMetadata}; @@ -54,7 +54,7 @@ impl Index { }; match chunk.find(path) { - Err(Error::NotFound(_)) => None, + Err(Error::FileNotFound) => None, Err(error) => Some(Err(error)), Ok((meta, size)) => Some(Ok(Location { chunk: index, @@ -65,10 +65,7 @@ impl Index { } }); - match location { - None => Err(Error::NotFound(ErrorValue::Path(path.into()))), - Some(result) => result, - } + location.unwrap_or(Err(Error::FileNotFound)) } fn chunks(&self) -> impl Iterator)>> + '_ { @@ -98,19 +95,19 @@ impl Index { match chunk { // Found an index - save it out to the cache. - Ok(chunk) => { + Ok(Some(chunk)) => { let mut guard = self.chunks.lock().unwrap(); guard.insert(index_usize, chunk.into()); Some(Ok((index_u8, guard[index_usize].clone()))) } // No index was found for this chunk - mark index as the max chunk point so we don't do that again. - Err(Error::NotFound(_)) => { + Ok(None) => { *self.max_chunk.lock().unwrap() = Some(index); None } - // Some other error occured, surface it. + // Surface errors Err(error) => Some(Err(error)), } }) @@ -124,21 +121,23 @@ enum IndexChunk { } impl IndexChunk { - fn new(repository: u8, category: u8, chunk: u8, resource: &R) -> Result { - resource - .index(repository, category, chunk) - .and_then(|mut reader| { - let file = Index1::read(&mut reader)?; - Ok(IndexChunk::Index1(file)) - }) - .or_else(|_| { - resource - .index2(repository, category, chunk) - .and_then(|mut reader| { - let file = Index2::read(&mut reader)?; - Ok(IndexChunk::Index2(file)) - }) - }) + fn new( + repository: u8, + category: u8, + chunk: u8, + resource: &R, + ) -> Result> { + if let Some(mut reader) = resource.index(repository, category, chunk)? { + let index = Self::Index1(Index1::read(&mut reader)?); + return Ok(Some(index)); + } + + if let Some(mut reader) = resource.index2(repository, category, chunk)? { + let index = Self::Index2(Index2::read(&mut reader)?); + return Ok(Some(index)); + } + + Ok(None) } fn find(&self, path: &str) -> Result<(FileMetadata, Option)> { diff --git a/ironworks/src/sqpack/index/index1.rs b/ironworks/src/sqpack/index/index1.rs index 6207efd2..256d4505 100644 --- a/ironworks/src/sqpack/index/index1.rs +++ b/ironworks/src/sqpack/index/index1.rs @@ -2,7 +2,7 @@ use std::{collections::BTreeSet, io::SeekFrom}; use binrw::binread; -use crate::error::{Error, ErrorValue, Result}; +use crate::sqpack::error::{Error, Result}; use super::{ crc::crc32, @@ -57,9 +57,8 @@ impl Index1 { let hash = match hashed_segments[..] { [file, directory] => (directory as u64) << 32 | file as u64, _ => { - return Err(Error::Invalid( - ErrorValue::Path(path.into()), - "Paths must contain at least two segments.".into(), + return Err(Error::PathInvalid( + "SqPack paths must contain at least two segments".into(), )); } }; @@ -88,6 +87,6 @@ impl Index1 { (metadata, size) }) - .ok_or_else(|| Error::NotFound(ErrorValue::Path(path.into()))) + .ok_or(Error::FileNotFound) } } diff --git a/ironworks/src/sqpack/index/index2.rs b/ironworks/src/sqpack/index/index2.rs index 9ffb451d..f9c59f1a 100644 --- a/ironworks/src/sqpack/index/index2.rs +++ b/ironworks/src/sqpack/index/index2.rs @@ -2,7 +2,7 @@ use std::{collections::BTreeSet, io::SeekFrom}; use binrw::binread; -use crate::error::{Error, ErrorValue, Result}; +use crate::sqpack::error::{Error, Result}; use super::{ crc::crc32, @@ -66,6 +66,6 @@ impl Index2 { (metadata, size) }) - .ok_or_else(|| Error::NotFound(ErrorValue::Path(path.into()))) + .ok_or(Error::FileNotFound) } } diff --git a/ironworks/src/sqpack/install.rs b/ironworks/src/sqpack/install.rs index 9a14a236..8b76e49e 100644 --- a/ironworks/src/sqpack/install.rs +++ b/ironworks/src/sqpack/install.rs @@ -5,12 +5,12 @@ use std::{ path::{Path, PathBuf}, }; -use crate::{ - error::{Error, ErrorValue, Result}, - utility::{TakeSeekable, TakeSeekableExt}, -}; +use crate::utility::{TakeSeekable, TakeSeekableExt}; -use super::{Location, Resource}; +use super::{ + Location, Resource, + error::{Error, Result}, +}; const TRY_PATHS: &[&str] = &[ r"C:\SquareEnix\FINAL FANTASY XIV - A Realm Reborn", @@ -91,7 +91,7 @@ impl Install { self.repositories .get(usize::from(repository)) .and_then(|option| option.as_ref()) - .ok_or_else(|| Error::NotFound(ErrorValue::Other(format!("repository {repository}")))) + .ok_or(Error::FileNotFound) } } @@ -111,12 +111,12 @@ impl Resource for Install { } type Index = io::Cursor>; - fn index(&self, repository: u8, category: u8, chunk: u8) -> Result { + fn index(&self, repository: u8, category: u8, chunk: u8) -> Result> { read_index(self.build_file_path(repository, category, chunk, "index")?) } type Index2 = io::Cursor>; - fn index2(&self, repository: u8, category: u8, chunk: u8) -> Result { + fn index2(&self, repository: u8, category: u8, chunk: u8) -> Result> { read_index(self.build_file_path(repository, category, chunk, "index2")?) } @@ -173,15 +173,15 @@ fn find_repositories(path: &Path) -> Vec> { .collect() } -fn read_index(path: PathBuf) -> Result>> { +fn read_index(path: PathBuf) -> Result>>> { // Read the entire index into memory before returning - we typically need // the full dataset anyway, and working directly on a File causes significant // slowdowns due to IO syscalls. - let buffer = fs::read(&path).map_err(|error| match error.kind() { - io::ErrorKind::NotFound => { - Error::NotFound(ErrorValue::Other(format!("file path {path:?}"))) - } - _ => Error::Resource(error.into()), - })?; - Ok(io::Cursor::new(buffer)) + match fs::read(&path) { + Ok(buffer) => Ok(Some(io::Cursor::new(buffer))), + Err(error) => match error.kind() { + io::ErrorKind::NotFound => Ok(None), + _ => Err(Error::Io(error)), + }, + } } diff --git a/ironworks/src/sqpack/mod.rs b/ironworks/src/sqpack/mod.rs index defee4c7..bf1da6cb 100644 --- a/ironworks/src/sqpack/mod.rs +++ b/ironworks/src/sqpack/mod.rs @@ -1,6 +1,7 @@ //! Tools for working with the SqPack package format. mod block; +mod error; mod file; mod index; mod install; diff --git a/ironworks/src/sqpack/resource.rs b/ironworks/src/sqpack/resource.rs index 175ee177..45dc94b5 100644 --- a/ironworks/src/sqpack/resource.rs +++ b/ironworks/src/sqpack/resource.rs @@ -1,8 +1,6 @@ use std::io::{Read, Seek}; -use crate::error::Result; - -use super::index::Location; +use super::{error::Result, index::Location}; /// Resource adapter to fetch information and data on request for a SqPack instance. pub trait Resource { @@ -11,13 +9,13 @@ pub trait Resource { /// The type of an index resource. type Index: Read + Seek; - /// Fetches the specified index resource. - fn index(&self, repository: u8, category: u8, chunk: u8) -> Result; + /// Fetch the specified index resource, if it exists. + fn index(&self, repository: u8, category: u8, chunk: u8) -> Result>; /// The type of an index2 resource. type Index2: Read + Seek; - /// Fetches the specified index2 resource. - fn index2(&self, repository: u8, category: u8, chunk: u8) -> Result; + /// Fetch the specified index2 resource, if it exists. + fn index2(&self, repository: u8, category: u8, chunk: u8) -> Result>; /// The type of a file reader resource. type File: Read + Seek; diff --git a/ironworks/src/sqpack/sqpack.rs b/ironworks/src/sqpack/sqpack.rs index 110ac238..d2db7105 100644 --- a/ironworks/src/sqpack/sqpack.rs +++ b/ironworks/src/sqpack/sqpack.rs @@ -1,14 +1,15 @@ use std::{fmt::Debug, sync::Arc}; use crate::{ - Resource, - error::{Error, ErrorValue, Result}, - ironworks::FileStream, sqpack, utility::{HashMapCache, HashMapCacheExt}, }; -use super::{file::File, index::Index}; +use super::{ + error::{Error, Result}, + file::File, + index::Index, +}; const CATEGORIES: &[Option<&str>] = &[ /* 0x00 */ Some("common"), @@ -90,12 +91,12 @@ impl SqPack { // game does, checking the first 3 characters for category and such - but I // think this is cleaner; especially to read. - let path_not_found = || Error::NotFound(ErrorValue::Path(path.to_string())); - let mut split = path.split('/'); let (Some(category_segment), Some(repository_segment)) = (split.next(), split.next()) else { - return Err(path_not_found()); + return Err(Error::PathInvalid( + "SqPack paths must contain at least two segments".into(), + )); }; let repository = REPOSITORIES @@ -106,22 +107,10 @@ impl SqPack { let category = CATEGORIES .iter() .position(|&category| category == Some(category_segment)) - .ok_or_else(path_not_found)?; + .ok_or_else(|| { + Error::PathInvalid(format!("unknown SqPack category \"{category_segment}\"")) + })?; Ok((repository.try_into().unwrap(), category.try_into().unwrap())) } } - -// TODO: work out the resource story for this because it's gonna get cluttery if im not careful -impl Resource for SqPack -where - R: sqpack::Resource + Send + Sync + 'static, -{ - fn version(&self, path: &str) -> Result { - self.version(path) - } - - fn file(&self, path: &str) -> Result> { - Ok(Box::new(self.file(path)?)) - } -} From 5d55e7314ac90522617db493cf2ae6f80ff7ad62 Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 22 Apr 2025 18:03:54 +1000 Subject: [PATCH 2/6] First pass on zipatch --- ironworks/src/file/patch/chunk.rs | 18 ++++- ironworks/src/file/patch/mod.rs | 8 +- ironworks/src/file/patch/patch.rs | 8 ++ ironworks/src/file/patch/zipatch.rs | 112 ---------------------------- ironworks/src/sqpack/error.rs | 2 +- ironworks/src/sqpack/mod.rs | 1 + ironworks/src/zipatch/chunks.rs | 62 +++++++++++++++ ironworks/src/zipatch/lookup.rs | 88 ++++++++++++++-------- ironworks/src/zipatch/mod.rs | 1 + ironworks/src/zipatch/repository.rs | 6 +- ironworks/src/zipatch/view.rs | 98 ++++++++++++------------ ironworks/src/zipatch/zipatch.rs | 6 +- 12 files changed, 205 insertions(+), 205 deletions(-) create mode 100644 ironworks/src/file/patch/patch.rs delete mode 100644 ironworks/src/file/patch/zipatch.rs create mode 100644 ironworks/src/zipatch/chunks.rs diff --git a/ironworks/src/file/patch/chunk.rs b/ironworks/src/file/patch/chunk.rs index 2ffede67..a65d5e97 100644 --- a/ironworks/src/file/patch/chunk.rs +++ b/ironworks/src/file/patch/chunk.rs @@ -8,6 +8,20 @@ use super::command::{ IndexUpdateCommand, PatchInfoCommand, TargetInfoCommand, }; +#[binread] +#[br(big)] +#[derive(Debug)] +pub struct ChunkContainer { + size: u32, + + // NOTE: We're not reading buffers in chunks, so we need to ensure the full + // chunk size is skipped or we drift alignment for the checksum. + #[br(pad_size_to = size, args(size))] + pub chunk: Chunk, + + checksum: u32, +} + /// A chunk of a patch file, encapsulating metadata about the containing file, /// or a single task that should be performed. #[binread] @@ -67,7 +81,7 @@ pub struct FileHeaderChunk { /// Version 3 specific fields. #[br(if(version == 3))] #[get = "pub"] - v3: Option, + v3: Option, } #[binread] @@ -86,7 +100,7 @@ pub enum PatchKind { #[br(big)] #[derive(Debug, CopyGetters)] #[getset(get_copy = "pub")] -pub struct FileHeaderV3 { +pub struct FileHeaderChunkV3 { /// add_directories: u32, /// diff --git a/ironworks/src/file/patch/mod.rs b/ironworks/src/file/patch/mod.rs index 0ea26a2b..ff537a12 100644 --- a/ironworks/src/file/patch/mod.rs +++ b/ironworks/src/file/patch/mod.rs @@ -2,17 +2,17 @@ mod chunk; mod command; -mod zipatch; +mod patch; pub use { chunk::{ - AddDirectoryChunk, ApplyChunk, Chunk, DeleteDirectoryChunk, FileHeaderChunk, FileHeaderV3, - OptionKind, SqPackChunk, + AddDirectoryChunk, ApplyChunk, Chunk, ChunkContainer, DeleteDirectoryChunk, + FileHeaderChunk, FileHeaderChunkV3, OptionKind, SqPackChunk, }, command::{ AddCommand, BlockHeader, DeleteCommand, ExpandCommand, FileOperation, FileOperationCommand, HeaderFileKind, HeaderKind, HeaderUpdateCommand, IndexUpdateCommand, IndexUpdateKind, PatchInfoCommand, SqPackFile, TargetInfoCommand, TargetPlatform, TargetRegion, }, - zipatch::{ChunkIterator, ZiPatch}, + patch::Header, }; diff --git a/ironworks/src/file/patch/patch.rs b/ironworks/src/file/patch/patch.rs new file mode 100644 index 00000000..dc40f87a --- /dev/null +++ b/ironworks/src/file/patch/patch.rs @@ -0,0 +1,8 @@ +use binrw::binread; + +// ZiPatch have practically no header, it's chunks all the way down - this is +// for consistency more than anything else. +#[binread] +#[br(big, magic = b"\x91ZIPATCH\x0D\x0A\x1A\x0A")] +#[derive(Debug)] +pub struct Header {} diff --git a/ironworks/src/file/patch/zipatch.rs b/ironworks/src/file/patch/zipatch.rs deleted file mode 100644 index 6f7ef9cd..00000000 --- a/ironworks/src/file/patch/zipatch.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::{ - io::SeekFrom, - sync::{Arc, Mutex}, -}; - -use binrw::BinRead; -use derivative::Derivative; - -use crate::{FileStream, error::Result, file::File}; - -use super::chunk::Chunk; - -const ZIPATCH_MAGIC: &[u8; 12] = b"\x91ZIPATCH\x0D\x0A\x1A\x0A"; - -/// ZiPatch incremental patch file format. -/// -/// This file format contains a significant number of small headers interspersed -/// between payloads, and will _heavily_ benefit from buffered reading or -/// memory-mapped files. -#[derive(Derivative)] -#[derivative(Debug)] -pub struct ZiPatch { - #[derivative(Debug = "ignore")] - stream: Arc>>, -} - -impl ZiPatch { - /// Get an iterator over the chunks within this patch file. - pub fn chunks(&self) -> ChunkIterator { - ChunkIterator::new(self.stream.clone()) - } -} - -impl File for ZiPatch { - fn read(mut stream: impl FileStream) -> Result { - // Check the magic in the header - let mut magic = [0u8; ZIPATCH_MAGIC.len()]; - stream.read_exact(&mut magic)?; - - if &magic != ZIPATCH_MAGIC { - todo!("error message") - } - - // Rest of the file is chunks that we'll read lazily. - Ok(Self { - // TODO: I'm really not happy with this incantation - stream: Arc::new(Mutex::new(Box::new(stream))), - }) - } -} - -/// Iterator over the chunks within a patch file. -/// -/// Chunks are read lazily from the source stream over the course of iteration. -#[derive(Derivative)] -#[derivative(Debug)] -pub struct ChunkIterator { - #[derivative(Debug = "ignore")] - stream: Arc>>, - offset: u64, - complete: bool, -} - -impl ChunkIterator { - fn new(stream: Arc>>) -> Self { - ChunkIterator { - stream, - offset: ZIPATCH_MAGIC.len().try_into().unwrap(), - complete: false, - } - } - - fn read_chunk(&mut self) -> Result { - let mut handle = self.stream.lock().unwrap(); - - // Seek to last known offset - in a tight loop this is effectively a noop, - // but need to make sure if there's stuff jumping around. - // TODO: lots of jumping around would be catastrophic for read performance - it'd be nice to be able to request something cloneable, so i.e. file handles could be cloned between chunk iterators, rather than trying to share access to a single one - but i'm not sure how to mode that without major refactors. - handle.seek(SeekFrom::Start(self.offset))?; - - let size = u32::read_be(&mut *handle)?; - - let chunk = Chunk::read_args(&mut *handle, (size,))?; - - // Update iterator offset to the start of the next chunk. `size` only represents - // the size of the chunk data itself, so the +12 is to account for the other - // fields in the container. - self.offset += u64::from(size) + 12; - - // TODO: check the hash? is it worth it? I'd need to relock the stream for that... - - Ok(chunk) - } -} - -impl Iterator for ChunkIterator { - type Item = Result; - - fn next(&mut self) -> Option { - if self.complete { - return None; - } - - let chunk = self.read_chunk(); - - if let Ok(Chunk::EndOfFile) = chunk { - self.complete = true; - } - - Some(chunk) - } -} diff --git a/ironworks/src/sqpack/error.rs b/ironworks/src/sqpack/error.rs index c143ae61..2d84921f 100644 --- a/ironworks/src/sqpack/error.rs +++ b/ironworks/src/sqpack/error.rs @@ -28,4 +28,4 @@ impl From for Error { } } -pub type Result = std::result::Result; +pub type Result = std::result::Result; diff --git a/ironworks/src/sqpack/mod.rs b/ironworks/src/sqpack/mod.rs index bf1da6cb..2181b7f4 100644 --- a/ironworks/src/sqpack/mod.rs +++ b/ironworks/src/sqpack/mod.rs @@ -10,6 +10,7 @@ mod sqpack; pub use { block::{BlockMetadata, BlockPayload, BlockStream}, + error::{Error, Result}, file::File, index::Location, install::Install, diff --git a/ironworks/src/zipatch/chunks.rs b/ironworks/src/zipatch/chunks.rs new file mode 100644 index 00000000..7efc114f --- /dev/null +++ b/ironworks/src/zipatch/chunks.rs @@ -0,0 +1,62 @@ +use std::{fs, io}; + +use binrw::BinRead; + +use crate::{file::patch, sqpack}; + +#[derive(Debug)] +enum IteratorState { + Pending, + Active, + Complete, +} + +/// Iterator over the chunks within a patch file. +/// +/// Chunks are read lazily from the source stream over the course of iteration. +#[derive(Debug)] +pub struct ChunkIterator { + stream: io::BufReader, + state: IteratorState, +} + +impl ChunkIterator { + pub fn new(stream: io::BufReader) -> Self { + ChunkIterator { + stream, + state: IteratorState::Pending, + } + } +} + +impl Iterator for ChunkIterator { + type Item = sqpack::Result; + + fn next(&mut self) -> Option { + match self.state { + // We've already hit EOF, fuse. + IteratorState::Complete => return None, + + // Read past the header on first iteration. + IteratorState::Pending => { + if let Err(error) = patch::Header::read(&mut self.stream) { + return Some(Err(error.into())); + } + self.state = IteratorState::Active; + } + + IteratorState::Active => {} + } + + let chunk = match patch::ChunkContainer::read(&mut self.stream) { + Err(error) => return Some(Err(error.into())), + Ok(container) => container.chunk, + }; + + if matches!(chunk, patch::Chunk::EndOfFile) { + self.state = IteratorState::Complete; + } + + Some(Ok(chunk)) + } +} diff --git a/ironworks/src/zipatch/lookup.rs b/ironworks/src/zipatch/lookup.rs index 774ed52a..9208c5b9 100644 --- a/ironworks/src/zipatch/lookup.rs +++ b/ironworks/src/zipatch/lookup.rs @@ -2,20 +2,21 @@ use std::{ fs, hash::Hash, io, + num::ParseIntError, path::{Path, PathBuf}, }; use binrw::{BinRead, BinWrite, binrw}; use crate::{ - error::{Error, ErrorValue, Result}, - file::{ - File, - patch::{Chunk, FileOperation, FileOperationCommand, SqPackChunk, ZiPatch as ZiPatchFile}, - }, + file::patch::{Chunk, FileOperation, FileOperationCommand, SqPackChunk}, + sqpack, }; -use super::utility::{BrwMap, BrwVec}; +use super::{ + chunks::ChunkIterator, + utility::{BrwMap, BrwVec}, +}; #[derive(Debug)] pub struct PatchLookup { @@ -24,11 +25,11 @@ pub struct PatchLookup { } impl PatchLookup { - pub fn build(path: &Path) -> Result { + pub fn build(path: &Path) -> sqpack::Result { read_lookup(path) } - pub fn from_cache(path: &Path, cache: &Path) -> Result { + pub fn from_cache(path: &Path, cache: &Path) -> sqpack::Result { // Try to read data from an existing cache. let data = match fs::File::open(cache) { // File exists. Try to read, but bail if it's an old version or corrupt. @@ -130,14 +131,14 @@ pub struct ResourceChunk { pub size: u64, } -fn read_lookup(path: &Path) -> Result { +fn read_lookup(path: &Path) -> sqpack::Result { let file = io::BufReader::new(fs::File::open(path)?); - let zipatch = ZiPatchFile::read(file)?; + let mut chunks = ChunkIterator::new(file); // TODO: Retry on failure? - zipatch - .chunks() - .try_fold(PatchLookupData::default(), |mut data, chunk| -> Result<_> { + let data = chunks.try_fold( + PatchLookupData::default(), + |mut data, chunk| -> sqpack::Result<_> { match chunk? { Chunk::SqPack(SqPackChunk::FileOperation(command)) => { process_file_operation(&mut data, command)? @@ -172,14 +173,19 @@ fn read_lookup(path: &Path) -> Result { }; Ok(data) - }) - .map(|data| PatchLookup { - path: path.to_owned(), - data, - }) + }, + )?; + + Ok(PatchLookup { + path: path.to_owned(), + data, + }) } -fn process_file_operation(data: &mut PatchLookupData, command: FileOperationCommand) -> Result<()> { +fn process_file_operation( + data: &mut PatchLookupData, + command: FileOperationCommand, +) -> sqpack::Result<()> { let path = command.path().to_string(); if !path.starts_with("sqpack/") { return Ok(()); @@ -210,27 +216,20 @@ fn process_file_operation(data: &mut PatchLookupData, command: FileOperationComm Ok(()) } -fn path_to_specifier(path: &str) -> Result { +fn path_to_specifier(path: &str) -> sqpack::Result { let path = PathBuf::from(path); - fn path_error(path: &Path, reason: &str) -> Error { - Error::Invalid( - ErrorValue::Other(format!("patch path {path:?}")), - reason.into(), - ) - } - let file_name = path .file_stem() .and_then(|osstr| osstr.to_str()) - .ok_or_else(|| path_error(&path, "malformed file name"))?; + .ok_or_else(|| path_error(&path, "invalid unicode", None))?; let category = u8::from_str_radix(&file_name[0..2], 16) - .map_err(|err| path_error(&path, &format!("{err}")))?; + .map_err(|err| path_error(&path, "invalid category", err))?; let repository = u8::from_str_radix(&file_name[2..4], 16) - .map_err(|err| path_error(&path, &format!("{err}")))?; + .map_err(|err| path_error(&path, "invalid repository", err))?; let chunk = u8::from_str_radix(&file_name[4..6], 16) - .map_err(|err| path_error(&path, &format!("{err}")))?; + .map_err(|err| path_error(&path, "invalid chunk", err))?; let extension = match path.extension().and_then(|osstr| osstr.to_str()) { Some("index") => SqPackFileExtension::Index(1), @@ -238,10 +237,10 @@ fn path_to_specifier(path: &str) -> Result { Some(dat) if dat.starts_with("dat") => { let dat_number = dat[3..] .parse::() - .map_err(|_err| path_error(&path, "unhandled file extension"))?; + .map_err(|_err| path_error(&path, "unhandled file extension", None))?; SqPackFileExtension::Dat(dat_number) } - _ => return Err(path_error(&path, "unhandled file extension")), + _ => return Err(path_error(&path, "unhandled file extension", None)), }; Ok(SqPackSpecifier { @@ -251,3 +250,26 @@ fn path_to_specifier(path: &str) -> Result { extension, }) } + +#[derive(Debug, thiserror::Error)] +#[error("zipatch command path {path} is malformed: {reason}")] +struct MalformedPathError { + path: PathBuf, + reason: &'static str, + + #[source] + source: Option, +} + +fn path_error( + path: &Path, + reason: &'static str, + source: impl Into>, +) -> sqpack::Error { + let error = MalformedPathError { + path: path.to_owned(), + reason, + source: source.into(), + }; + sqpack::Error::Malformed(error.into()) +} diff --git a/ironworks/src/zipatch/mod.rs b/ironworks/src/zipatch/mod.rs index b8ffa14d..4197cae7 100644 --- a/ironworks/src/zipatch/mod.rs +++ b/ironworks/src/zipatch/mod.rs @@ -1,5 +1,6 @@ //! Adapters to allow working with game data directly out of ZiPatch files. +mod chunks; mod lookup; mod repository; mod utility; diff --git a/ironworks/src/zipatch/repository.rs b/ironworks/src/zipatch/repository.rs index 9c4529eb..9ae27b15 100644 --- a/ironworks/src/zipatch/repository.rs +++ b/ironworks/src/zipatch/repository.rs @@ -1,11 +1,9 @@ use std::{ cmp::Ordering, - fs, + fs, io, path::{Path, PathBuf}, }; -use crate::error::Result; - /// Representation of a single patch file. #[derive(Debug)] pub struct Patch { @@ -26,7 +24,7 @@ pub struct PatchRepository { impl PatchRepository { /// Read a patch repository from the specified path. Patches will be sorted /// following the FFXIV patch ordering. - pub fn at(repository_path: &Path) -> Result { + pub fn at(repository_path: &Path) -> io::Result { let mut patches = fs::read_dir(repository_path)? .filter_map(|entry| { let patch_path = match entry { diff --git a/ironworks/src/zipatch/view.rs b/ironworks/src/zipatch/view.rs index bb2379e1..7aa6cf9f 100644 --- a/ironworks/src/zipatch/view.rs +++ b/ironworks/src/zipatch/view.rs @@ -1,14 +1,13 @@ use std::{ collections::HashMap, fs, - io::{self, BufReader, Cursor, Seek, SeekFrom}, + io::{self, Seek}, sync::Arc, }; use either::Either; use crate::{ - error::{Error, ErrorValue, Result}, sqpack, utility::{TakeSeekable, TakeSeekableExt}, }; @@ -19,8 +18,8 @@ use super::{ zipatch::LookupCache, }; -type FileReader = - Either>, sqpack::BlockStream>>; +type BufFile = io::BufReader; +type FileReader = Either, sqpack::BlockStream>; #[derive(Debug)] pub struct ViewBuilder { @@ -73,10 +72,11 @@ impl View { fn lookups( &self, repository_id: u8, - ) -> Result>> + '_> { - let repository = self.repositories.get(&repository_id).ok_or_else(|| { - Error::NotFound(ErrorValue::Other(format!("repository {repository_id}"))) - })?; + ) -> sqpack::Result>> + '_> { + let repository = self + .repositories + .get(&repository_id) + .ok_or(sqpack::Error::FileNotFound)?; // We're operating at a patch-by-patch granularity here, with the (very safe) // assumption that a game version is at minimum one patch. @@ -95,7 +95,7 @@ impl View { category: u8, chunk: u8, index_version: u8, - ) -> Result>> { + ) -> sqpack::Result>>> { let target_specifier = SqPackSpecifier { repository, category, @@ -104,7 +104,7 @@ impl View { }; let mut empty = true; - let mut cursor = Cursor::new(Vec::::new()); + let mut cursor = io::Cursor::new(Vec::::new()); for maybe_lookup in self.lookups(repository)? { // Grab the commands for the requested target, if any exist in this patch. @@ -115,13 +115,13 @@ impl View { }; // Read the commands for this patch. - let mut file = BufReader::new(fs::File::open(&lookup.path)?); + let mut file = io::BufReader::new(fs::File::open(&lookup.path)?); for chunk in chunks.iter() { empty = false; cursor.set_position(chunk.target_offset); for block in chunk.blocks.iter() { - file.seek(SeekFrom::Start(block.source_offset))?; + file.seek(io::SeekFrom::Start(block.source_offset))?; let mut reader = sqpack::BlockPayload::new( &mut file, block.compressed_size, @@ -144,49 +144,57 @@ impl View { // If nothing was read, we mark this index as not found. if empty { - // TODO: Improve the error value. - return Err(Error::NotFound(ErrorValue::Other(format!( - "zipatch target {target_specifier:?}" - )))); + return Ok(None); } // Done - reset the cursor's position and return it as a view of the index. cursor.set_position(0); - Ok(cursor) + Ok(Some(cursor)) } } impl sqpack::Resource for View { - fn version(&self, repository_id: u8) -> Result { - let repository = self.repositories.get(&repository_id).ok_or_else(|| { - Error::NotFound(ErrorValue::Other(format!("repository {repository_id}"))) - })?; + fn version(&self, repository_id: u8) -> sqpack::Result { + let repository = self + .repositories + .get(&repository_id) + .ok_or(sqpack::Error::FileNotFound)?; repository .patches .last() - .map(|x| x.name.clone()) - .ok_or_else(|| { - Error::Invalid( - ErrorValue::Other(format!("repository {repository_id}")), - "unspecified repository version".to_string(), - ) - }) + .map(|patch| patch.name.clone()) + .ok_or(sqpack::Error::FileNotFound) } // ASSUMPTION: IndexUpdate chunks are unused, new indexes will always be distributed via FileOperation::AddFile. - type Index = Cursor>; - fn index(&self, repository: u8, category: u8, chunk: u8) -> Result { + type Index = io::Cursor>; + fn index( + &self, + repository: u8, + category: u8, + chunk: u8, + ) -> sqpack::Result> { self.read_index(repository, category, chunk, 1) } - type Index2 = Cursor>; - fn index2(&self, repository: u8, category: u8, chunk: u8) -> Result { + type Index2 = io::Cursor>; + fn index2( + &self, + repository: u8, + category: u8, + chunk: u8, + ) -> sqpack::Result> { self.read_index(repository, category, chunk, 2) } type File = FileReader; - fn file(&self, repository: u8, category: u8, location: sqpack::Location) -> Result { + fn file( + &self, + repository: u8, + category: u8, + location: sqpack::Location, + ) -> sqpack::Result { let target = ( SqPackSpecifier { repository, @@ -219,22 +227,22 @@ impl sqpack::Resource for View { // patch files - if the target couldn't be found in this lookup, continue // to the next. match read_file_chunks(&lookup, &location, chunks) { - Err(Error::NotFound(_)) => {} + Err(sqpack::Error::FileNotFound) => {} other => return other, }; }; } - Err(Error::NotFound(ErrorValue::Other(format!( - "zipatch target {:?}", - target - )))) + Err(sqpack::Error::FileNotFound) } } -fn read_resource_chunk(lookup: &PatchLookup, command: &ResourceChunk) -> Result { - let mut file = BufReader::new(fs::File::open(&lookup.path)?); - file.seek(SeekFrom::Start(command.offset))?; +fn read_resource_chunk( + lookup: &PatchLookup, + command: &ResourceChunk, +) -> sqpack::Result { + let mut file = io::BufReader::new(fs::File::open(&lookup.path)?); + file.seek(io::SeekFrom::Start(command.offset))?; let out = file.take_seekable(command.size)?; Ok(Either::Left(out)) } @@ -243,7 +251,7 @@ fn read_file_chunks( lookup: &PatchLookup, location: &sqpack::Location, chunks: &[FileChunk], -) -> Result { +) -> sqpack::Result { let offset = location.offset(); let outside_target = |offset: u64, size: u64| { @@ -296,13 +304,11 @@ fn read_file_chunks( // If there are 0 blocks that match the target, the provided lookup does not // contain the requested target. if metadata.is_empty() { - return Err(Error::NotFound(ErrorValue::Other(format!( - "sqpack location {location:?}" - )))); + return Err(sqpack::Error::FileNotFound); } // Build the readers & complete - let file_reader = BufReader::new(fs::File::open(&lookup.path)?); + let file_reader = io::BufReader::new(fs::File::open(&lookup.path)?); let block_stream = sqpack::BlockStream::new(file_reader, offset.try_into().unwrap(), metadata); Ok(Either::Right(block_stream)) diff --git a/ironworks/src/zipatch/zipatch.rs b/ironworks/src/zipatch/zipatch.rs index 7515b4d2..17c45ef8 100644 --- a/ironworks/src/zipatch/zipatch.rs +++ b/ironworks/src/zipatch/zipatch.rs @@ -7,7 +7,7 @@ use std::{ }, }; -use crate::error::Result; +use crate::sqpack; use super::{lookup::PatchLookup, repository::Patch, view::ViewBuilder}; @@ -77,7 +77,7 @@ impl LookupCache { self.persist_lookups.store(true, Ordering::SeqCst) } - pub fn lookup(&self, patch: &Patch) -> Result> { + pub fn lookup(&self, patch: &Patch) -> sqpack::Result> { // TODO: honestly this might make sense as an alternate impl of the hashmapcache // Get a lock on the main cache and fetch the internal sync primative. We're // also recording if it existed prior to this call. @@ -135,7 +135,7 @@ impl LookupCache { Ok(lookup) } - fn read_lookup(&self, patch: &Patch) -> Result { + fn read_lookup(&self, patch: &Patch) -> sqpack::Result { let persist_lookups = self.persist_lookups.load(Ordering::SeqCst); if !persist_lookups { return PatchLookup::build(&patch.path); From daa8332fc2b3c84db6969daf85a887ece368b572 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 15 Jun 2025 16:40:45 +1000 Subject: [PATCH 3/6] Spike version trait and wiring --- ironworks/src/excel/error.rs | 7 +++ ironworks/src/excel/excel.rs | 79 +++++++++---------------------- ironworks/src/excel/mod.rs | 31 +----------- ironworks/src/filesystem.rs | 14 ++++++ ironworks/src/lib.rs | 16 +------ ironworks/src/sqpack/error.rs | 2 +- ironworks/src/sqpack/file/file.rs | 62 +++++++++--------------- ironworks/src/sqpack/sqpack.rs | 78 +++++++++++++++++------------- 8 files changed, 115 insertions(+), 174 deletions(-) create mode 100644 ironworks/src/excel/error.rs create mode 100644 ironworks/src/filesystem.rs diff --git a/ironworks/src/excel/error.rs b/ironworks/src/excel/error.rs new file mode 100644 index 00000000..5f3b4c1d --- /dev/null +++ b/ironworks/src/excel/error.rs @@ -0,0 +1,7 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("filesystem error encountered")] + Filesystem(#[source] Box), +} + +pub type Result = std::result::Result; diff --git a/ironworks/src/excel/excel.rs b/ironworks/src/excel/excel.rs index 8474d260..46f83d6f 100644 --- a/ironworks/src/excel/excel.rs +++ b/ironworks/src/excel/excel.rs @@ -1,49 +1,38 @@ use std::{ convert::Infallible, + fmt::Debug, sync::{Arc, OnceLock}, }; use derivative::Derivative; use crate::{ - error::{Error, ErrorValue, Result}, file::exl, - ironworks::Ironworks, + filesystem::{Filesystem, Version}, utility::{HashMapCache, HashMapCacheExt}, }; use super::{ + error::{Error, Result}, language::Language, - metadata::SheetMetadata, path, - sheet::{Sheet, SheetCache}, }; /// An Excel database. -#[derive(Derivative)] -#[derivative(Debug)] -pub struct Excel { - #[derivative(Debug = "ignore")] - ironworks: Arc, +#[derive(Debug)] +pub struct Excel { + filesystem: F, default_language: Language, - - #[derivative(Debug = "ignore")] - list: OnceLock, - #[derivative(Debug = "ignore")] - sheets: HashMapCache, } -impl Excel { - /// Build an view into the Excel database for a given ironworks instance. - pub fn new(ironworks: impl Into>) -> Self { +impl Excel { + /// Build an view into the Excel database for a given filesystem. + pub fn new(filesystem: F) -> Self { Self { - ironworks: ironworks.into(), + filesystem, default_language: Language::None, - - list: Default::default(), - sheets: Default::default(), } } @@ -57,45 +46,21 @@ impl Excel { pub fn set_default_language(&mut self, language: Language) { self.default_language = language; } +} - /// Get the version string of the database. +impl Excel +where + F: Filesystem, + F::File: Version, +{ pub fn version(&self) -> Result { - self.ironworks.version(path::exl()) - } - - /// Fetch the authoritative list of sheets in the database. - pub fn list(&self) -> Result<&exl::ExcelList> { - // Handle hot path before trying anything fancy. - // We're doing this rather than executing .file inside .get_or_init to avoid caching error states. - // TODO: get_or_try_init once (if?) that gets stabilised. - if let Some(list) = self.list.get() { - return Ok(list); - } - - let list = self.ironworks.file::(path::exl())?; + let file = self.filesystem.file(path::exl()).map_err(fs_err)?; + let version = file.version().map_err(fs_err)?; - Ok(self.list.get_or_init(|| list)) + Ok(version) } +} - /// Fetch a sheet from the database. - pub fn sheet(&self, metadata: S) -> Result> { - let name = metadata.name(); - - let list = self.list()?; - if !list.has(&name) { - return Err(Error::NotFound(ErrorValue::Sheet(name))); - } - - let cache = self - .sheets - .try_get_or_insert(name, || -> Result<_, Infallible> { Ok(Default::default()) }) - .unwrap(); - - Ok(Sheet::new( - self.ironworks.clone(), - metadata, - self.default_language, - cache, - )) - } +fn fs_err(error: impl std::error::Error + 'static) -> Error { + Error::Filesystem(error.into()) } diff --git a/ironworks/src/excel/mod.rs b/ironworks/src/excel/mod.rs index 0f333de6..7d37b240 100644 --- a/ironworks/src/excel/mod.rs +++ b/ironworks/src/excel/mod.rs @@ -1,22 +1,11 @@ //! Tools for working with the Excel database format. +mod error; mod excel; -mod field; mod language; -mod metadata; -mod page; mod path; -mod row; -mod sheet; -pub use { - excel::Excel, - field::Field, - language::Language, - metadata::SheetMetadata, - row::{ColumnSpecifier, Row}, - sheet::{RowOptions, Sheet, SheetIterator}, -}; +pub use {excel::Excel, language::Language}; #[cfg(test)] mod test { @@ -25,26 +14,10 @@ mod test { #[test] fn test_send() { fn assert_send() {} - assert_send::(); - assert_send::(); - assert_send::(); - assert_send::(); - assert_send::(); - assert_send::(); - assert_send::>(); - assert_send::>(); } #[test] fn test_sync() { fn assert_sync() {} - assert_sync::(); - assert_sync::(); - assert_sync::(); - assert_sync::(); - assert_sync::(); - assert_sync::(); - assert_sync::>(); - assert_sync::>(); } } diff --git a/ironworks/src/filesystem.rs b/ironworks/src/filesystem.rs new file mode 100644 index 00000000..d96b7bea --- /dev/null +++ b/ironworks/src/filesystem.rs @@ -0,0 +1,14 @@ +use std::io; + +pub trait Filesystem { + type File; + type Error: std::error::Error + 'static; + + fn file(&self, path: &str) -> Result; +} + +pub trait Version { + type Error: std::error::Error + 'static; + + fn version(&self) -> Result; +} diff --git a/ironworks/src/lib.rs b/ironworks/src/lib.rs index 1d521da3..c545a7da 100644 --- a/ironworks/src/lib.rs +++ b/ironworks/src/lib.rs @@ -6,8 +6,7 @@ // Doc config #![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))] -mod error; -mod ironworks; +mod filesystem; mod utility; #[cfg(feature = "excel")] @@ -20,28 +19,15 @@ pub mod sqpack; #[cfg(feature = "zipatch")] pub mod zipatch; -pub use { - crate::ironworks::{FileStream, Ironworks, Resource}, - error::{Error, ErrorValue}, -}; - #[cfg(test)] mod test { - use super::*; - #[test] fn test_send() { fn assert_send() {} - assert_send::(); - assert_send::(); - assert_send::(); } #[test] fn test_sync() { fn assert_sync() {} - assert_sync::(); - assert_sync::(); - assert_sync::(); } } diff --git a/ironworks/src/sqpack/error.rs b/ironworks/src/sqpack/error.rs index 2d84921f..c143ae61 100644 --- a/ironworks/src/sqpack/error.rs +++ b/ironworks/src/sqpack/error.rs @@ -28,4 +28,4 @@ impl From for Error { } } -pub type Result = std::result::Result; +pub type Result = std::result::Result; diff --git a/ironworks/src/sqpack/file/file.rs b/ironworks/src/sqpack/file/file.rs index ce154d5f..a86d01a4 100644 --- a/ironworks/src/sqpack/file/file.rs +++ b/ironworks/src/sqpack/file/file.rs @@ -1,8 +1,19 @@ -use std::io::{Cursor, Empty, Read, Seek, SeekFrom}; +use std::{ + io::{Cursor, Empty, Read, Seek, SeekFrom}, + sync::Arc, +}; use binrw::BinRead; -use crate::sqpack::{block::BlockStream, error::Result}; +use crate::{ + filesystem::Version, + sqpack::{ + Resource, + block::BlockStream, + error::{Error, Result}, + sqpack::Location, + }, +}; use super::{ empty, model, @@ -14,25 +25,8 @@ use super::{ /// A stream of data for a file read from a sqpack dat archive. #[derive(Debug)] pub struct File { - inner: FileStreamKind, -} - -impl File { - /// Create a new File which which will translate SqPack stored data in the given stream. - pub fn new(mut reader: R) -> Result { - // Read in the header. - let header = Header::read(&mut reader)?; - - use FileStreamKind as FSK; - let file_stream = match &header.kind { - FileKind::Empty => FSK::Empty(empty::read(reader, header)?), - FileKind::Standard => FSK::Standard(standard::read(reader, header.size, header)?), - FileKind::Model => FSK::Model(model::read(reader, header.size, header)?), - FileKind::Texture => FSK::Texture(texture::read(reader, header.size, header)?), - }; - - Ok(File { inner: file_stream }) - } + resource: Arc, + location: Location, } #[derive(Debug)] @@ -43,26 +37,16 @@ enum FileStreamKind { Texture(Cursor>), } -impl Read for File { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - use FileStreamKind as FSK; - match &mut self.inner { - FSK::Empty(stream) => stream.read(buf), - FSK::Standard(stream) => stream.read(buf), - FSK::Model(stream) => stream.read(buf), - FSK::Texture(stream) => stream.read(buf), - } +impl File { + pub(crate) fn new(resource: Arc, location: Location) -> Self { + Self { resource, location } } } -impl Seek for File { - fn seek(&mut self, pos: SeekFrom) -> std::io::Result { - use FileStreamKind as FSK; - match &mut self.inner { - FSK::Empty(stream) => stream.seek(pos), - FSK::Standard(stream) => stream.seek(pos), - FSK::Model(stream) => stream.seek(pos), - FSK::Texture(stream) => stream.seek(pos), - } +impl Version for File { + type Error = Error; + + fn version(&self) -> std::result::Result { + self.resource.version(self.location.repository) } } diff --git a/ironworks/src/sqpack/sqpack.rs b/ironworks/src/sqpack/sqpack.rs index d2db7105..9971b617 100644 --- a/ironworks/src/sqpack/sqpack.rs +++ b/ironworks/src/sqpack/sqpack.rs @@ -1,14 +1,15 @@ use std::{fmt::Debug, sync::Arc}; use crate::{ - sqpack, + filesystem::{Filesystem, Version}, utility::{HashMapCache, HashMapCacheExt}, }; use super::{ error::{Error, Result}, file::File, - index::Index, + index, + resource::Resource, }; const CATEGORIES: &[Option<&str>] = &[ @@ -44,10 +45,17 @@ const REPOSITORIES: &[&str] = &[ pub struct SqPack { resource: Arc, - indexes: HashMapCache<(u8, u8), Index>, + indexes: HashMapCache<(u8, u8), index::Index>, } -impl SqPack { +#[derive(Debug)] +pub(super) struct Location { + pub repository: u8, + pub category: u8, + pub index: index::Location, +} + +impl SqPack { /// Build a representation of SqPack packages. The provided resource will be /// queried for lookups as required to fulfil SqPack requests. pub fn new(resource: R) -> Self { @@ -58,35 +66,15 @@ impl SqPack { } } - /// Get the version string for the file at `path`. - pub fn version(&self, path: &str) -> Result { - let (repository, _) = self.path_metadata(&path.to_lowercase())?; - self.resource.version(repository) + pub fn file(&self, path: &str) -> Result> { + let location = self.location(path)?; + Ok(File::new(self.resource.clone(), location)) } - /// Read the file at `path` from SqPack. - pub fn file(&self, path: &str) -> Result> { + fn location(&self, path: &str) -> Result { // SqPack paths are always lower case. let path = path.to_lowercase(); - // Look up the location of the requested path. - let (repository, category) = self.path_metadata(&path)?; - - let location = self - .indexes - .try_get_or_insert((repository, category), || { - Index::new(repository, category, self.resource.clone()) - })? - .find(&path)?; - - // Build a File representation. - let dat = self.resource.file(repository, category, location)?; - - // TODO: Cache files? Tempted to say it's the IW struct's responsibility. Is it even possible here with streams? - File::new(dat) - } - - fn path_metadata(&self, path: &str) -> Result<(u8, u8)> { // NOTE: This could be technically-faster by doing that cursed logic the // game does, checking the first 3 characters for category and such - but I // think this is cleaner; especially to read. @@ -99,18 +87,42 @@ impl SqPack { )); }; - let repository = REPOSITORIES + let repository: u8 = REPOSITORIES .iter() .position(|&repository| repository == repository_segment) - .unwrap_or(0); + .unwrap_or(0) + .try_into() + .expect("repository index should never exceed u8::MAX"); - let category = CATEGORIES + let category: u8 = CATEGORIES .iter() .position(|&category| category == Some(category_segment)) .ok_or_else(|| { Error::PathInvalid(format!("unknown SqPack category \"{category_segment}\"")) - })?; + })? + .try_into() + .expect("category index should never exceed u8::MAX"); + + let index = self + .indexes + .try_get_or_insert((repository, category), || { + index::Index::new(repository, category, self.resource.clone()) + })? + .find(&path)?; + + Ok(Location { + repository, + category, + index, + }) + } +} + +impl Filesystem for SqPack { + type File = File; + type Error = Error; - Ok((repository.try_into().unwrap(), category.try_into().unwrap())) + fn file(&self, path: &str) -> Result { + self.file(path) } } From 347c5cb665c226f57b459dcc55820b808f8e02c6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 17 Jun 2025 00:09:26 +1000 Subject: [PATCH 4/6] Spike file read --- ironworks/src/file/exl.rs | 27 +++++------ ironworks/src/sqpack/error.rs | 2 +- ironworks/src/sqpack/file/file.rs | 70 ++++++++++++++++++++++++----- ironworks/src/sqpack/index/index.rs | 8 ++-- ironworks/src/sqpack/mod.rs | 4 -- ironworks/src/sqpack/sqpack.rs | 4 +- 6 files changed, 78 insertions(+), 37 deletions(-) diff --git a/ironworks/src/file/exl.rs b/ironworks/src/file/exl.rs index 856876ac..d267da6e 100644 --- a/ironworks/src/file/exl.rs +++ b/ironworks/src/file/exl.rs @@ -1,13 +1,15 @@ //! Structs and utilities for parsing .exl files. -use std::{borrow::Cow, collections::HashSet}; +use std::{borrow::Cow, collections::HashSet, io}; -use crate::{ - FileStream, - error::{Error, Result}, -}; +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("bad magic: {0:?}")] + BadMagic(String), -use super::File; + #[error("I/O")] + Io(#[source] io::Error), +} /// List of known Excel sheets. #[derive(Debug)] @@ -28,23 +30,18 @@ impl ExcelList { } } -impl File for ExcelList { - fn read(mut stream: impl FileStream) -> Result { +impl ExcelList { + pub fn read(mut stream: impl io::Read) -> Result { // The excel list is actually just plaintext, read it in as a string. let mut list = String::new(); - stream - .read_to_string(&mut list) - .map_err(|error| Error::Resource(error.into()))?; + stream.read_to_string(&mut list).map_err(Error::Io)?; let mut lines = list.split("\r\n"); // Ensure the first line contains the expected magic let magic = lines.next().and_then(|line| line.get(0..4)); if !matches!(magic, Some("EXLT")) { - return Err(Error::Resource( - format!("Incorrect magic in excel list file: expected \"EXLT\", got {magic:?}") - .into(), - )); + return Err(Error::BadMagic(magic.unwrap_or("").to_string())); } // Build the set of sheets. We're ignoring the sheet ID (second field), as diff --git a/ironworks/src/sqpack/error.rs b/ironworks/src/sqpack/error.rs index c143ae61..c9137902 100644 --- a/ironworks/src/sqpack/error.rs +++ b/ironworks/src/sqpack/error.rs @@ -13,7 +13,7 @@ pub enum Error { FileIncomplete(Vec), #[error("malformed data encountered")] - Malformed(#[source] Box), + Malformed(#[source] Box), #[error("I/O error encountered")] Io(#[from] io::Error), diff --git a/ironworks/src/sqpack/file/file.rs b/ironworks/src/sqpack/file/file.rs index a86d01a4..81890284 100644 --- a/ironworks/src/sqpack/file/file.rs +++ b/ironworks/src/sqpack/file/file.rs @@ -1,7 +1,4 @@ -use std::{ - io::{Cursor, Empty, Read, Seek, SeekFrom}, - sync::Arc, -}; +use std::{io, sync::Arc}; use binrw::BinRead; @@ -24,22 +21,61 @@ use super::{ // Wrapper struct to prevent the innards of the file streams from being public API surface. /// A stream of data for a file read from a sqpack dat archive. #[derive(Debug)] -pub struct File { +pub struct File { resource: Arc, location: Location, + + reader: Option>, } #[derive(Debug)] -enum FileStreamKind { - Empty(Empty), +enum FileReader { + Empty(io::Empty), Standard(BlockStream), - Model(Cursor>), - Texture(Cursor>), + Model(io::Cursor>), + Texture(io::Cursor>), } -impl File { +impl File { pub(crate) fn new(resource: Arc, location: Location) -> Self { - Self { resource, location } + Self { + resource, + location, + reader: None, + } + } + + fn reader(&mut self) -> &mut FileReader { + if self.reader.is_none() { + self.reader = Some(self.build_reader()) + } + + self.reader.as_mut().expect("reader should not be None") + } + + fn build_reader(&self) -> FileReader { + let mut reader = self + .resource + .file( + self.location.repository, + self.location.category, + self.location.index.clone(), + ) + .expect("TODO"); + + let header = Header::read(&mut reader).expect("TODO"); + + use FileReader as FSK; + match &header.kind { + FileKind::Empty => FSK::Empty(empty::read(reader, header).expect("TODO")), + FileKind::Standard => { + FSK::Standard(standard::read(reader, header.size, header).expect("TODO")) + } + FileKind::Model => FSK::Model(model::read(reader, header.size, header).expect("TODO")), + FileKind::Texture => { + FSK::Texture(texture::read(reader, header.size, header).expect("TODO")) + } + } } } @@ -50,3 +86,15 @@ impl Version for File { self.resource.version(self.location.repository) } } + +impl io::Read for File { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + use FileReader as FR; + match self.reader() { + FR::Empty(reader) => reader.read(buf), + FR::Standard(reader) => reader.read(buf), + FR::Model(reader) => reader.read(buf), + FR::Texture(reader) => reader.read(buf), + } + } +} diff --git a/ironworks/src/sqpack/index/index.rs b/ironworks/src/sqpack/index/index.rs index c8aa44ef..5389903c 100644 --- a/ironworks/src/sqpack/index/index.rs +++ b/ironworks/src/sqpack/index/index.rs @@ -11,7 +11,7 @@ use crate::sqpack::{ use super::{index1::Index1, index2::Index2, shared::FileMetadata}; /// Specifier of a file location within a SqPack category. -#[derive(Debug, CopyGetters)] +#[derive(Debug, Clone, CopyGetters)] #[get_copy = "pub"] pub struct Location { /// SqPack chunk the file is in, i.e. `0000XX.win32.dat1`. @@ -36,14 +36,14 @@ pub struct Index { } impl Index { - pub fn new(repository: u8, category: u8, resource: Arc) -> Result { - Ok(Self { + pub fn new(repository: u8, category: u8, resource: Arc) -> Self { + Self { repository, category, resource, max_chunk: None.into(), chunks: Vec::new().into(), - }) + } } pub fn find(&self, path: &str) -> Result { diff --git a/ironworks/src/sqpack/mod.rs b/ironworks/src/sqpack/mod.rs index 2181b7f4..38bdfd91 100644 --- a/ironworks/src/sqpack/mod.rs +++ b/ironworks/src/sqpack/mod.rs @@ -25,14 +25,10 @@ mod test { #[test] fn test_send() { fn assert_send() {} - assert_send::>(); - assert_send::>(); } #[test] fn test_sync() { fn assert_sync() {} - assert_sync::>(); - assert_sync::>(); } } diff --git a/ironworks/src/sqpack/sqpack.rs b/ironworks/src/sqpack/sqpack.rs index 9971b617..16da6d23 100644 --- a/ironworks/src/sqpack/sqpack.rs +++ b/ironworks/src/sqpack/sqpack.rs @@ -105,9 +105,9 @@ impl SqPack { let index = self .indexes - .try_get_or_insert((repository, category), || { + .get_or_insert((repository, category), || { index::Index::new(repository, category, self.resource.clone()) - })? + }) .find(&path)?; Ok(Location { From dce5a1a3617160089463c315de03b441a09c1dd0 Mon Sep 17 00:00:00 2001 From: ackwell Date: Tue, 24 Jun 2025 22:10:24 +1000 Subject: [PATCH 5/6] Split file out of format --- ironworks/src/sqpack/file.rs | 88 +++++++++++++++ ironworks/src/sqpack/file/file.rs | 100 ------------------ .../src/sqpack/{file => format}/empty.rs | 0 ironworks/src/sqpack/format/format.rs | 73 +++++++++++++ ironworks/src/sqpack/{file => format}/mod.rs | 4 +- .../src/sqpack/{file => format}/model.rs | 0 .../src/sqpack/{file => format}/shared.rs | 0 .../src/sqpack/{file => format}/standard.rs | 0 .../src/sqpack/{file => format}/texture.rs | 0 ironworks/src/sqpack/mod.rs | 1 + ironworks/src/sqpack/sqpack.rs | 24 ++--- ironworks/src/utility/hash_map_cache.rs | 9 ++ 12 files changed, 182 insertions(+), 117 deletions(-) create mode 100644 ironworks/src/sqpack/file.rs delete mode 100644 ironworks/src/sqpack/file/file.rs rename ironworks/src/sqpack/{file => format}/empty.rs (100%) create mode 100644 ironworks/src/sqpack/format/format.rs rename ironworks/src/sqpack/{file => format}/mod.rs (63%) rename ironworks/src/sqpack/{file => format}/model.rs (100%) rename ironworks/src/sqpack/{file => format}/shared.rs (100%) rename ironworks/src/sqpack/{file => format}/standard.rs (100%) rename ironworks/src/sqpack/{file => format}/texture.rs (100%) diff --git a/ironworks/src/sqpack/file.rs b/ironworks/src/sqpack/file.rs new file mode 100644 index 00000000..17f23f40 --- /dev/null +++ b/ironworks/src/sqpack/file.rs @@ -0,0 +1,88 @@ +use std::{io, sync::Arc}; + +use crate::{filesystem::Version, sqpack::Resource}; + +use super::{ + error::{Error, Result}, + format::Format, + index::Location, +}; + +#[derive(Debug)] +pub struct File { + resource: Arc, + + repository: u8, + category: u8, + location: Location, + + format: Option>, +} + +impl File +where + R: Resource, +{ + pub(super) fn new(resource: Arc, repository: u8, category: u8, location: Location) -> Self { + Self { + resource, + repository, + category, + location, + format: None, + } + } + + fn format(&mut self) -> Result<&mut Format> { + if self.format.is_none() { + let format = Format::new(self.resource.file( + self.repository, + self.category, + self.location.clone(), + )?)?; + self.format = Some(format); + } + + Ok(self.format.as_mut().expect("format should not be None")) + } +} + +impl Version for File +where + R: Resource, +{ + type Error = Error; + + fn version(&self) -> std::result::Result { + self.resource.version(self.repository) + } +} + +impl io::Read for File +where + R: Resource, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + self.format().map_err(into_io_error)?.read(buf) + } +} + +impl io::Seek for File +where + R: Resource, +{ + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + self.format().map_err(into_io_error)?.seek(pos) + } +} + +fn into_io_error(error: Error) -> io::Error { + let kind = match error { + Error::Io(error) => return error, + Error::FileNotFound => io::ErrorKind::NotFound, + Error::PathInvalid(_) => io::ErrorKind::Other, + Error::FileIncomplete(_) => io::ErrorKind::Other, + Error::Malformed(_) => io::ErrorKind::InvalidData, + }; + io::Error::new(kind, error) +} diff --git a/ironworks/src/sqpack/file/file.rs b/ironworks/src/sqpack/file/file.rs deleted file mode 100644 index 81890284..00000000 --- a/ironworks/src/sqpack/file/file.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::{io, sync::Arc}; - -use binrw::BinRead; - -use crate::{ - filesystem::Version, - sqpack::{ - Resource, - block::BlockStream, - error::{Error, Result}, - sqpack::Location, - }, -}; - -use super::{ - empty, model, - shared::{FileKind, Header}, - standard, texture, -}; - -// Wrapper struct to prevent the innards of the file streams from being public API surface. -/// A stream of data for a file read from a sqpack dat archive. -#[derive(Debug)] -pub struct File { - resource: Arc, - location: Location, - - reader: Option>, -} - -#[derive(Debug)] -enum FileReader { - Empty(io::Empty), - Standard(BlockStream), - Model(io::Cursor>), - Texture(io::Cursor>), -} - -impl File { - pub(crate) fn new(resource: Arc, location: Location) -> Self { - Self { - resource, - location, - reader: None, - } - } - - fn reader(&mut self) -> &mut FileReader { - if self.reader.is_none() { - self.reader = Some(self.build_reader()) - } - - self.reader.as_mut().expect("reader should not be None") - } - - fn build_reader(&self) -> FileReader { - let mut reader = self - .resource - .file( - self.location.repository, - self.location.category, - self.location.index.clone(), - ) - .expect("TODO"); - - let header = Header::read(&mut reader).expect("TODO"); - - use FileReader as FSK; - match &header.kind { - FileKind::Empty => FSK::Empty(empty::read(reader, header).expect("TODO")), - FileKind::Standard => { - FSK::Standard(standard::read(reader, header.size, header).expect("TODO")) - } - FileKind::Model => FSK::Model(model::read(reader, header.size, header).expect("TODO")), - FileKind::Texture => { - FSK::Texture(texture::read(reader, header.size, header).expect("TODO")) - } - } - } -} - -impl Version for File { - type Error = Error; - - fn version(&self) -> std::result::Result { - self.resource.version(self.location.repository) - } -} - -impl io::Read for File { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - use FileReader as FR; - match self.reader() { - FR::Empty(reader) => reader.read(buf), - FR::Standard(reader) => reader.read(buf), - FR::Model(reader) => reader.read(buf), - FR::Texture(reader) => reader.read(buf), - } - } -} diff --git a/ironworks/src/sqpack/file/empty.rs b/ironworks/src/sqpack/format/empty.rs similarity index 100% rename from ironworks/src/sqpack/file/empty.rs rename to ironworks/src/sqpack/format/empty.rs diff --git a/ironworks/src/sqpack/format/format.rs b/ironworks/src/sqpack/format/format.rs new file mode 100644 index 00000000..d85335fb --- /dev/null +++ b/ironworks/src/sqpack/format/format.rs @@ -0,0 +1,73 @@ +use std::{io, sync::Arc}; + +use binrw::BinRead; + +use crate::{ + filesystem::Version, + sqpack::{ + Resource, + block::BlockStream, + error::{Error, Result}, + }, +}; + +use super::{ + empty, model, + shared::{FileKind, Header}, + standard, texture, +}; + +#[derive(Debug)] +pub enum Format { + Empty(io::Empty), + Standard(BlockStream), + Model(io::Cursor>), + Texture(io::Cursor>), +} + +impl Format +where + R: io::Read + io::Seek, +{ + pub fn new(mut reader: R) -> Result { + let header = Header::read(&mut reader)?; + + use Format as F; + let format = match &header.kind { + FileKind::Empty => F::Empty(empty::read(reader, header)?), + FileKind::Standard => F::Standard(standard::read(reader, header.size, header)?), + FileKind::Model => F::Model(model::read(reader, header.size, header)?), + FileKind::Texture => F::Texture(texture::read(reader, header.size, header)?), + }; + + Ok(format) + } +} + +impl io::Read for Format +where + R: io::Read + io::Seek, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match self { + Self::Empty(reader) => reader.read(buf), + Self::Standard(reader) => reader.read(buf), + Self::Model(reader) => reader.read(buf), + Self::Texture(reader) => reader.read(buf), + } + } +} + +impl io::Seek for Format +where + R: io::Read + io::Seek, +{ + fn seek(&mut self, pos: io::SeekFrom) -> io::Result { + match self { + Self::Empty(reader) => reader.seek(pos), + Self::Standard(reader) => reader.seek(pos), + Self::Model(reader) => reader.seek(pos), + Self::Texture(reader) => reader.seek(pos), + } + } +} diff --git a/ironworks/src/sqpack/file/mod.rs b/ironworks/src/sqpack/format/mod.rs similarity index 63% rename from ironworks/src/sqpack/file/mod.rs rename to ironworks/src/sqpack/format/mod.rs index 2eec9ae5..8a344298 100644 --- a/ironworks/src/sqpack/file/mod.rs +++ b/ironworks/src/sqpack/format/mod.rs @@ -1,8 +1,8 @@ mod empty; -mod file; +mod format; mod model; mod shared; mod standard; mod texture; -pub use file::File; +pub use format::Format; diff --git a/ironworks/src/sqpack/file/model.rs b/ironworks/src/sqpack/format/model.rs similarity index 100% rename from ironworks/src/sqpack/file/model.rs rename to ironworks/src/sqpack/format/model.rs diff --git a/ironworks/src/sqpack/file/shared.rs b/ironworks/src/sqpack/format/shared.rs similarity index 100% rename from ironworks/src/sqpack/file/shared.rs rename to ironworks/src/sqpack/format/shared.rs diff --git a/ironworks/src/sqpack/file/standard.rs b/ironworks/src/sqpack/format/standard.rs similarity index 100% rename from ironworks/src/sqpack/file/standard.rs rename to ironworks/src/sqpack/format/standard.rs diff --git a/ironworks/src/sqpack/file/texture.rs b/ironworks/src/sqpack/format/texture.rs similarity index 100% rename from ironworks/src/sqpack/file/texture.rs rename to ironworks/src/sqpack/format/texture.rs diff --git a/ironworks/src/sqpack/mod.rs b/ironworks/src/sqpack/mod.rs index 38bdfd91..5106ddb2 100644 --- a/ironworks/src/sqpack/mod.rs +++ b/ironworks/src/sqpack/mod.rs @@ -3,6 +3,7 @@ mod block; mod error; mod file; +mod format; mod index; mod install; mod resource; diff --git a/ironworks/src/sqpack/sqpack.rs b/ironworks/src/sqpack/sqpack.rs index 16da6d23..03a8a03a 100644 --- a/ironworks/src/sqpack/sqpack.rs +++ b/ironworks/src/sqpack/sqpack.rs @@ -48,13 +48,6 @@ pub struct SqPack { indexes: HashMapCache<(u8, u8), index::Index>, } -#[derive(Debug)] -pub(super) struct Location { - pub repository: u8, - pub category: u8, - pub index: index::Location, -} - impl SqPack { /// Build a representation of SqPack packages. The provided resource will be /// queried for lookups as required to fulfil SqPack requests. @@ -67,11 +60,16 @@ impl SqPack { } pub fn file(&self, path: &str) -> Result> { - let location = self.location(path)?; - Ok(File::new(self.resource.clone(), location)) + let (repository, category, index) = self.location(path)?; + Ok(File::new( + self.resource.clone(), + repository, + category, + index, + )) } - fn location(&self, path: &str) -> Result { + fn location(&self, path: &str) -> Result<(u8, u8, index::Location)> { // SqPack paths are always lower case. let path = path.to_lowercase(); @@ -110,11 +108,7 @@ impl SqPack { }) .find(&path)?; - Ok(Location { - repository, - category, - index, - }) + Ok((repository, category, index)) } } diff --git a/ironworks/src/utility/hash_map_cache.rs b/ironworks/src/utility/hash_map_cache.rs index 52a3be56..f67949b1 100644 --- a/ironworks/src/utility/hash_map_cache.rs +++ b/ironworks/src/utility/hash_map_cache.rs @@ -7,6 +7,8 @@ use std::{ pub type HashMapCache = Mutex>>; pub trait HashMapCacheExt { + fn get_or_insert(&self, key: K, build: impl FnOnce() -> V) -> Arc; + fn try_get_or_insert( &self, key: K, @@ -18,6 +20,13 @@ impl HashMapCacheExt for HashMapCache where K: Eq + Hash, { + fn get_or_insert(&self, key: K, build: impl FnOnce() -> V) -> Arc { + match self.lock().unwrap().entry(key) { + Entry::Occupied(entry) => entry.get().clone(), + Entry::Vacant(entry) => entry.insert(build().into()).clone(), + } + } + fn try_get_or_insert( &self, key: K, From be124fc1903a9306d58d470022ed6fedebc2c74f Mon Sep 17 00:00:00 2001 From: ackwell Date: Wed, 25 Jun 2025 21:25:01 +1000 Subject: [PATCH 6/6] Spike FromReader + read extension method --- ironworks/src/excel/error.rs | 2 +- ironworks/src/file/exl.rs | 27 ++++++++++++++++++--------- ironworks/src/file/file.rs | 15 --------------- ironworks/src/file/mod.rs | 4 ++-- ironworks/src/file/traits.rs | 22 ++++++++++++++++++++++ ironworks/src/filesystem.rs | 27 +++++++++++++++++++++++++++ ironworks/src/lib.rs | 2 ++ ironworks/src/sqpack/error.rs | 15 +++++++++++++-- 8 files changed, 85 insertions(+), 29 deletions(-) delete mode 100644 ironworks/src/file/file.rs create mode 100644 ironworks/src/file/traits.rs diff --git a/ironworks/src/excel/error.rs b/ironworks/src/excel/error.rs index 5f3b4c1d..509e931c 100644 --- a/ironworks/src/excel/error.rs +++ b/ironworks/src/excel/error.rs @@ -1,6 +1,6 @@ #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("filesystem error encountered")] + #[error("filesystem")] Filesystem(#[source] Box), } diff --git a/ironworks/src/file/exl.rs b/ironworks/src/file/exl.rs index d267da6e..f5b035c9 100644 --- a/ironworks/src/file/exl.rs +++ b/ironworks/src/file/exl.rs @@ -2,6 +2,8 @@ use std::{borrow::Cow, collections::HashSet, io}; +use crate::file::{FromReader, ReadError}; + #[derive(Debug, thiserror::Error)] pub enum Error { #[error("bad magic: {0:?}")] @@ -54,37 +56,44 @@ impl ExcelList { } } +impl FromReader for ExcelList { + fn read(reader: impl io::Read + io::Seek) -> Result { + Self::read(reader).map_err(|error| match error { + Error::Io(error) => ReadError::Io(error), + error => ReadError::Malformed(error.into()), + }) + } +} + #[cfg(test)] mod test { - use std::io::{self, Cursor}; - - use crate::{error::Error, file::File}; + use std::io; - use super::ExcelList; + use super::*; const TEST_LIST: &[u8] = b"EXLT\r\nsheet1,0\r\nsheet2,0\r\nsheet3,0\r\n"; #[test] fn empty() { let list = ExcelList::read(io::empty()); - assert!(matches!(list, Err(Error::Resource(_)))); + assert!(matches!(list, Err(Error::BadMagic(_)))); } #[test] fn missing_magic() { - let list = ExcelList::read(Cursor::new(b"hello\r\nworld".to_vec())); - assert!(matches!(list, Err(Error::Resource(_)))); + let list = ExcelList::read(io::Cursor::new(b"hello\r\nworld".to_vec())); + assert!(matches!(list, Err(Error::BadMagic(_)))); } #[test] fn has_sheet() { - let list = ExcelList::read(Cursor::new(TEST_LIST)).unwrap(); + let list = ExcelList::read(io::Cursor::new(TEST_LIST)).unwrap(); assert!(list.has("sheet2")); } #[test] fn missing_sheet() { - let list = ExcelList::read(Cursor::new(TEST_LIST)).unwrap(); + let list = ExcelList::read(io::Cursor::new(TEST_LIST)).unwrap(); assert!(!list.has("sheet4")); } } diff --git a/ironworks/src/file/file.rs b/ironworks/src/file/file.rs deleted file mode 100644 index ad2b8a70..00000000 --- a/ironworks/src/file/file.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::{FileStream, error::Result}; - -/// A file that can be read from ironworks. -pub trait File: Sized { - /// Build an instance of this file from the raw byte representation. - fn read(stream: impl FileStream) -> Result; -} - -impl File for Vec { - fn read(mut stream: impl FileStream) -> Result { - let mut buffer = Vec::new(); - stream.read_to_end(&mut buffer)?; - Ok(buffer) - } -} diff --git a/ironworks/src/file/mod.rs b/ironworks/src/file/mod.rs index 5f27190d..e86551c9 100644 --- a/ironworks/src/file/mod.rs +++ b/ironworks/src/file/mod.rs @@ -2,7 +2,7 @@ //! //! Each file type may contain a number of related supporting items, and as such are namespaced seperately. -mod file; +mod traits; #[cfg(feature = "eqdp")] pub mod eqdp; @@ -25,4 +25,4 @@ pub mod sklb; #[cfg(feature = "tex")] pub mod tex; -pub use file::File; +pub use traits::{FromReader, ReadError}; diff --git a/ironworks/src/file/traits.rs b/ironworks/src/file/traits.rs new file mode 100644 index 00000000..0dacf1c4 --- /dev/null +++ b/ironworks/src/file/traits.rs @@ -0,0 +1,22 @@ +use std::{error::Error, io}; + +#[derive(Debug, thiserror::Error)] +pub enum ReadError { + #[error("malformed data")] + Malformed(#[source] Box), + + #[error("I/O")] + Io(#[source] io::Error), +} + +pub trait FromReader: Sized { + fn read(reader: impl io::Read + io::Seek) -> Result; +} + +impl FromReader for Vec { + fn read(mut reader: impl io::Read + io::Seek) -> Result { + let mut buffer = Vec::new(); + reader.read_to_end(&mut buffer).map_err(ReadError::Io)?; + Ok(buffer) + } +} diff --git a/ironworks/src/filesystem.rs b/ironworks/src/filesystem.rs index d96b7bea..b73c3f97 100644 --- a/ironworks/src/filesystem.rs +++ b/ironworks/src/filesystem.rs @@ -1,5 +1,7 @@ use std::io; +use crate::file::{FromReader, ReadError}; + pub trait Filesystem { type File; type Error: std::error::Error + 'static; @@ -7,6 +9,31 @@ pub trait Filesystem { fn file(&self, path: &str) -> Result; } +pub trait FilesystemRead { + type Error; + + fn read(&self, path: &str) -> Result + where + T: FromReader, + Self::Error: From; +} + +impl FilesystemRead for F +where + F: Filesystem, + F::File: io::Read + io::Seek, +{ + type Error = F::Error; + + fn read(&self, path: &str) -> Result + where + T: FromReader, + Self::Error: From, + { + Ok(T::read(self.file(path)?)?) + } +} + pub trait Version { type Error: std::error::Error + 'static; diff --git a/ironworks/src/lib.rs b/ironworks/src/lib.rs index c545a7da..28495007 100644 --- a/ironworks/src/lib.rs +++ b/ironworks/src/lib.rs @@ -19,6 +19,8 @@ pub mod sqpack; #[cfg(feature = "zipatch")] pub mod zipatch; +pub use filesystem::{Filesystem, FilesystemRead}; + #[cfg(test)] mod test { #[test] diff --git a/ironworks/src/sqpack/error.rs b/ironworks/src/sqpack/error.rs index c9137902..1ecefd0d 100644 --- a/ironworks/src/sqpack/error.rs +++ b/ironworks/src/sqpack/error.rs @@ -1,5 +1,7 @@ use std::io; +use crate::file::ReadError; + #[non_exhaustive] #[derive(Debug, thiserror::Error)] pub enum Error { @@ -12,10 +14,10 @@ pub enum Error { #[error("file is empty or missing header")] FileIncomplete(Vec), - #[error("malformed data encountered")] + #[error("malformed data")] Malformed(#[source] Box), - #[error("I/O error encountered")] + #[error("I/O")] Io(#[from] io::Error), } @@ -28,4 +30,13 @@ impl From for Error { } } +impl From for Error { + fn from(error: ReadError) -> Self { + match error { + ReadError::Malformed(error) => Error::Malformed(error), + ReadError::Io(error) => Error::Io(error), + } + } +} + pub type Result = std::result::Result;