diff --git a/ironworks/src/excel/error.rs b/ironworks/src/excel/error.rs new file mode 100644 index 00000000..509e931c --- /dev/null +++ b/ironworks/src/excel/error.rs @@ -0,0 +1,7 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("filesystem")] + 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/file/exl.rs b/ironworks/src/file/exl.rs index 856876ac..f5b035c9 100644 --- a/ironworks/src/file/exl.rs +++ b/ironworks/src/file/exl.rs @@ -1,13 +1,17 @@ //! 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}, -}; +use crate::file::{FromReader, ReadError}; -use super::File; +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("bad magic: {0:?}")] + BadMagic(String), + + #[error("I/O")] + Io(#[source] io::Error), +} /// List of known Excel sheets. #[derive(Debug)] @@ -28,23 +32,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 @@ -57,37 +56,44 @@ impl File for 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/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/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 new file mode 100644 index 00000000..b73c3f97 --- /dev/null +++ b/ironworks/src/filesystem.rs @@ -0,0 +1,41 @@ +use std::io; + +use crate::file::{FromReader, ReadError}; + +pub trait Filesystem { + type File; + type Error: std::error::Error + 'static; + + 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; + + fn version(&self) -> Result; +} diff --git a/ironworks/src/lib.rs b/ironworks/src/lib.rs index 1d521da3..28495007 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,17 @@ pub mod sqpack; #[cfg(feature = "zipatch")] pub mod zipatch; -pub use { - crate::ironworks::{FileStream, Ironworks, Resource}, - error::{Error, ErrorValue}, -}; +pub use filesystem::{Filesystem, FilesystemRead}; #[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 new file mode 100644 index 00000000..1ecefd0d --- /dev/null +++ b/ironworks/src/sqpack/error.rs @@ -0,0 +1,42 @@ +use std::io; + +use crate::file::ReadError; + +#[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")] + Malformed(#[source] Box), + + #[error("I/O")] + 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()), + } + } +} + +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; 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 61b76744..00000000 --- a/ironworks/src/sqpack/file/file.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::io::{Cursor, Empty, Read, Seek, SeekFrom}; - -use binrw::BinRead; - -use crate::{error::Result, sqpack::block::BlockStream}; - -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 { - 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 }) - } -} - -#[derive(Debug)] -enum FileStreamKind { - Empty(Empty), - Standard(BlockStream), - Model(Cursor>), - 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 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), - } - } -} diff --git a/ironworks/src/sqpack/file/empty.rs b/ironworks/src/sqpack/format/empty.rs similarity index 80% rename from ironworks/src/sqpack/file/empty.rs rename to ironworks/src/sqpack/format/empty.rs index 57fce40c..5e3a3f53 100644 --- a/ironworks/src/sqpack/file/empty.rs +++ b/ironworks/src/sqpack/format/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/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 99% rename from ironworks/src/sqpack/file/model.rs rename to ironworks/src/sqpack/format/model.rs index b72e8a7c..e1e4a7a5 100644 --- a/ironworks/src/sqpack/file/model.rs +++ b/ironworks/src/sqpack/format/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/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 95% rename from ironworks/src/sqpack/file/standard.rs rename to ironworks/src/sqpack/format/standard.rs index 9fc4ce9f..f0187eb2 100644 --- a/ironworks/src/sqpack/file/standard.rs +++ b/ironworks/src/sqpack/format/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/format/texture.rs similarity index 98% rename from ironworks/src/sqpack/file/texture.rs rename to ironworks/src/sqpack/format/texture.rs index 24ed906a..a430a20f 100644 --- a/ironworks/src/sqpack/file/texture.rs +++ b/ironworks/src/sqpack/format/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..5389903c 100644 --- a/ironworks/src/sqpack/index/index.rs +++ b/ironworks/src/sqpack/index/index.rs @@ -3,15 +3,15 @@ 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}; /// 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 { @@ -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..5106ddb2 100644 --- a/ironworks/src/sqpack/mod.rs +++ b/ironworks/src/sqpack/mod.rs @@ -1,7 +1,9 @@ //! Tools for working with the SqPack package format. mod block; +mod error; mod file; +mod format; mod index; mod install; mod resource; @@ -9,6 +11,7 @@ mod sqpack; pub use { block::{BlockMetadata, BlockPayload, BlockStream}, + error::{Error, Result}, file::File, index::Location, install::Install, @@ -23,14 +26,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/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..03a8a03a 100644 --- a/ironworks/src/sqpack/sqpack.rs +++ b/ironworks/src/sqpack/sqpack.rs @@ -1,14 +1,16 @@ use std::{fmt::Debug, sync::Arc}; use crate::{ - Resource, - error::{Error, ErrorValue, Result}, - ironworks::FileStream, - sqpack, + filesystem::{Filesystem, Version}, utility::{HashMapCache, HashMapCacheExt}, }; -use super::{file::File, index::Index}; +use super::{ + error::{Error, Result}, + file::File, + index, + resource::Resource, +}; const CATEGORIES: &[Option<&str>] = &[ /* 0x00 */ Some("common"), @@ -43,10 +45,10 @@ const REPOSITORIES: &[&str] = &[ pub struct SqPack { resource: Arc, - indexes: HashMapCache<(u8, u8), Index>, + indexes: HashMapCache<(u8, u8), index::Index>, } -impl SqPack { +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 { @@ -57,71 +59,64 @@ 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 (repository, category, index) = self.location(path)?; + Ok(File::new( + self.resource.clone(), + repository, + category, + index, + )) } - /// Read the file at `path` from SqPack. - pub fn file(&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(); - // 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. - 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 + 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(path_not_found)?; + .ok_or_else(|| { + Error::PathInvalid(format!("unknown SqPack category \"{category_segment}\"")) + })? + .try_into() + .expect("category index should never exceed u8::MAX"); - Ok((repository.try_into().unwrap(), category.try_into().unwrap())) + let index = self + .indexes + .get_or_insert((repository, category), || { + index::Index::new(repository, category, self.resource.clone()) + }) + .find(&path)?; + + Ok((repository, category, index)) } } -// 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) - } +impl Filesystem for SqPack { + type File = File; + type Error = Error; - fn file(&self, path: &str) -> Result> { - Ok(Box::new(self.file(path)?)) + fn file(&self, path: &str) -> Result { + self.file(path) } } 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, 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);