Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions ironworks/src/excel/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("filesystem")]
Filesystem(#[source] Box<dyn std::error::Error>),
}

pub type Result<T> = std::result::Result<T, Error>;
79 changes: 22 additions & 57 deletions ironworks/src/excel/excel.rs
Original file line number Diff line number Diff line change
@@ -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<Ironworks>,
#[derive(Debug)]
pub struct Excel<F> {
filesystem: F,

default_language: Language,

#[derivative(Debug = "ignore")]
list: OnceLock<exl::ExcelList>,
#[derivative(Debug = "ignore")]
sheets: HashMapCache<String, SheetCache>,
}

impl Excel {
/// Build an view into the Excel database for a given ironworks instance.
pub fn new(ironworks: impl Into<Arc<Ironworks>>) -> Self {
impl<F> Excel<F> {
/// 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(),
}
}

Expand All @@ -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<F> Excel<F>
where
F: Filesystem,
F::File: Version,
{
pub fn version(&self) -> Result<String> {
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::<exl::ExcelList>(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<S: SheetMetadata>(&self, metadata: S) -> Result<Sheet<S>> {
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())
}
31 changes: 2 additions & 29 deletions ironworks/src/excel/mod.rs
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -25,26 +14,10 @@ mod test {
#[test]
fn test_send() {
fn assert_send<T: Send>() {}
assert_send::<ColumnSpecifier>();
assert_send::<Excel>();
assert_send::<Field>();
assert_send::<Language>();
assert_send::<Row>();
assert_send::<RowOptions>();
assert_send::<Sheet<()>>();
assert_send::<SheetIterator<()>>();
}

#[test]
fn test_sync() {
fn assert_sync<T: Sync>() {}
assert_sync::<ColumnSpecifier>();
assert_sync::<Excel>();
assert_sync::<Field>();
assert_sync::<Language>();
assert_sync::<Row>();
assert_sync::<RowOptions>();
assert_sync::<Sheet<()>>();
assert_sync::<SheetIterator<()>>();
}
}
54 changes: 30 additions & 24 deletions ironworks/src/file/exl.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand All @@ -28,23 +32,18 @@ impl ExcelList {
}
}

impl File for ExcelList {
fn read(mut stream: impl FileStream) -> Result<Self> {
impl ExcelList {
pub fn read(mut stream: impl io::Read) -> Result<Self, Error> {
// 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
Expand All @@ -57,37 +56,44 @@ impl File for ExcelList {
}
}

impl FromReader for ExcelList {
fn read(reader: impl io::Read + io::Seek) -> Result<Self, ReadError> {
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"));
}
}
15 changes: 0 additions & 15 deletions ironworks/src/file/file.rs

This file was deleted.

4 changes: 2 additions & 2 deletions ironworks/src/file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,4 +25,4 @@ pub mod sklb;
#[cfg(feature = "tex")]
pub mod tex;

pub use file::File;
pub use traits::{FromReader, ReadError};
18 changes: 16 additions & 2 deletions ironworks/src/file/patch/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -67,7 +81,7 @@ pub struct FileHeaderChunk {
/// Version 3 specific fields.
#[br(if(version == 3))]
#[get = "pub"]
v3: Option<FileHeaderV3>,
v3: Option<FileHeaderChunkV3>,
}

#[binread]
Expand All @@ -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,
///
Expand Down
8 changes: 4 additions & 4 deletions ironworks/src/file/patch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
8 changes: 8 additions & 0 deletions ironworks/src/file/patch/patch.rs
Original file line number Diff line number Diff line change
@@ -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 {}
Loading