diff --git a/crates/ltk_meta/src/error.rs b/crates/ltk_meta/src/error.rs index b6207419..0578d08d 100644 --- a/crates/ltk_meta/src/error.rs +++ b/crates/ltk_meta/src/error.rs @@ -1,5 +1,7 @@ use miette::Diagnostic; +use crate::header::ReadHeaderError; + use super::property::Kind; #[derive(Debug, thiserror::Error, Diagnostic)] @@ -15,6 +17,9 @@ pub enum Error { #[error("Invalid size - expected {0}, got {1} bytes")] InvalidSize(u64, u64), + #[error("Error reading header - {0}")] + ReadHeaderError(#[from] ReadHeaderError), + #[error("Container type {0:?} cannot be nested!")] InvalidNesting(Kind), #[error("Invalid map key type {0:?}, only primitive types can be used as keys.")] diff --git a/crates/ltk_meta/src/header.rs b/crates/ltk_meta/src/header.rs new file mode 100644 index 00000000..ee4316d4 --- /dev/null +++ b/crates/ltk_meta/src/header.rs @@ -0,0 +1,196 @@ +use std::io::{Read, Write}; + +use byteorder::{ReadBytesExt, WriteBytesExt, LE}; +use ltk_primitives::PrefixString; + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PropHeader { + pub version: u32, + /// List of other property bins this file depends on. + /// + /// Property bins can depend on other property bins in a similar fashion + /// to importing code libraries. + pub dependencies: Option>>, + pub object_count: u32, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PatchHeader { + pub version: u32, + pub override_count: u32, +} + +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Header { + Prop(PropHeader), + Patch(PatchHeader, PropHeader), +} + +impl From for Header { + fn from(value: PropHeader) -> Self { + Self::Prop(value) + } +} +impl From<(PatchHeader, PropHeader)> for Header { + fn from(value: (PatchHeader, PropHeader)) -> Self { + Self::Patch(value.0, value.1) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum ReadHeaderError { + #[error("Invalid file signature")] + InvalidFileSignature, + #[error("Invalid file version '{0}'")] + InvalidFileVersion(u32), + + #[error(transparent)] + ReaderError(#[from] ltk_io_ext::ReaderError), + #[error(transparent)] + StringReadError(#[from] ltk_primitives::StringReadError), + + #[error("IO Error - {0}")] + IOError(#[from] std::io::Error), +} +#[derive(Debug, thiserror::Error)] +pub enum WriteHeaderError { + #[error(transparent)] + StringWriteError(#[from] ltk_primitives::StringWriteError), + + #[error("IO Error - {0}")] + IOError(#[from] std::io::Error), +} +impl Header { + pub fn from_reader(reader: &mut (impl Read + ?Sized)) -> Result { + let magic = reader.read_u32::()?; + match magic { + PropHeader::MAGIC => PropHeader::from_reader(reader).map(Self::Prop), + PatchHeader::MAGIC => Ok(Self::Patch( + PatchHeader::from_reader(reader)?, + PropHeader::from_reader(reader)?, + )), + _ => Err(ReadHeaderError::InvalidFileSignature), + } + } + + pub fn to_writer(&self, writer: &mut (impl Write + ?Sized)) -> Result<(), WriteHeaderError> { + match self { + Header::Prop(prop_header) => { + writer.write_u32::(PropHeader::MAGIC)?; + prop_header.to_writer(writer)?; + } + Header::Patch(patch_header, prop_header) => { + writer.write_u32::(PatchHeader::MAGIC)?; + patch_header.to_writer(writer)?; + writer.write_u32::(PropHeader::MAGIC)?; + prop_header.to_writer(writer)?; + } + } + Ok(()) + } + + pub fn prop_header(&self) -> &PropHeader { + match &self { + Header::Prop(prop) => prop, + Header::Patch(_, prop) => prop, + } + } + pub fn into_parts(self) -> (PropHeader, Option) { + match self { + Header::Prop(prop) => (prop, None), + Header::Patch(patch, prop) => (prop, Some(patch)), + } + } +} + +impl PropHeader { + pub const LATEST_VERSION: u32 = 3; + pub const MAGIC: u32 = u32::from_le_bytes(*b"PROP"); + pub fn from_reader(reader: &mut (impl Read + ?Sized)) -> Result { + use ReadHeaderError::*; + + let version = reader.read_u32::()?; + if !matches!(version, 1..=3) { + // TODO (alan): distinguish override/non-override version in err? + return Err(InvalidFileVersion(version)); + } + + let dependencies = match version { + 2.. => { + let dep_count = reader.read_u32::()?; + let mut dependencies = Vec::with_capacity(dep_count as _); + for _ in 0..dep_count { + dependencies.push(PrefixString::from_reader(reader)?); + } + Some(dependencies) + } + _ => None, + }; + + let object_count = reader.read_u32::()?; + + Ok(Self { + version, + dependencies, + object_count, + }) + } + + pub fn to_writer(&self, writer: &mut (impl Write + ?Sized)) -> Result<(), WriteHeaderError> { + writer.write_u32::(self.version)?; + writer.write_u32::( + self.dependencies + .as_ref() + .map(|d| d.len().try_into().unwrap()) + .unwrap_or_default(), + )?; + + if let Some(deps) = self.dependencies.as_ref() { + for dep in deps { + dep.to_writer(writer)?; + } + } + + writer.write_u32::(self.object_count)?; + + Ok(()) + } +} + +impl PatchHeader { + pub const MAGIC: u32 = u32::from_le_bytes(*b"PTCH"); + pub fn from_reader(reader: &mut (impl Read + ?Sized)) -> Result { + use ReadHeaderError::*; + + let version = reader.read_u32::()?; + if version != 1 { + return Err(InvalidFileVersion(version)); + } + + let override_count = reader.read_u32::()?; + + let magic = reader.read_u32::()?; + if magic != PropHeader::MAGIC { + // TODO (alan): repr this in the error + log::error!( + "Expected PROP ({:#x}) section after PTCH ({:#x}), got '{:#x}'", + PropHeader::MAGIC, + PatchHeader::MAGIC, + magic + ); + return Err(InvalidFileSignature); + } + + Ok(Self { + version, + override_count, + }) + } + + pub fn to_writer(&self, _writer: &mut (impl Write + ?Sized)) -> Result<(), WriteHeaderError> { + todo!("TODO: implement PTCH header write"); + } +} diff --git a/crates/ltk_meta/src/lazy.rs b/crates/ltk_meta/src/lazy.rs new file mode 100644 index 00000000..c4fa1d0d --- /dev/null +++ b/crates/ltk_meta/src/lazy.rs @@ -0,0 +1,93 @@ +use std::io::{self, Read, Seek}; + +use byteorder::{ReadBytesExt as _, LE}; +use ltk_hash::{BinHash, ReadBytesExt as _}; + +use crate::{ + header::{Header, PatchHeader, PropHeader, ReadHeaderError}, + traits::ReaderExt, + PropertyValueEnum, +}; + +#[derive(Debug)] +pub struct LazyBin<'r, R: Read + Seek + ?Sized> { + pub header: PropHeader, + pub patch: Option, + reader: &'r mut R, +} + +#[derive(Debug)] +pub struct LazyObject<'r, R: Read + Seek + ?Sized> { + pub header: PropHeader, + reader: &'r mut R, + + pub size: u32, + pub prop_count: u16, +} + +#[derive(Debug, thiserror::Error)] +pub enum ReadErr { + #[error(transparent)] + Error(#[from] crate::Error), + #[error(transparent)] + ReadHeader(#[from] ReadHeaderError), + #[error(transparent)] + Io(#[from] io::Error), +} + +impl<'r, R: Read + Seek + ?Sized> LazyBin<'r, R> { + pub fn from_reader(reader: &'r mut R) -> Result { + let (header, patch) = Header::from_reader(reader)?.into_parts(); + Ok(Self { + header, + patch, + reader, + }) + } + + pub fn object(self, path: impl Into) -> Result>, ReadErr> { + let path = path.into(); + + self.reader.seek_relative( + i64::from(self.header.object_count) * i64::try_from(size_of::()).unwrap(), + )?; + + for _ in 0..self.header.object_count { + let size = self.reader.read_u32::()?; + let cur_path = self.reader.read_bin_hash::()?; + if cur_path == path { + return Ok(Some(LazyObject { + prop_count: self.reader.read_u16::()?, + header: self.header, + reader: self.reader, + size, + })); + } + self.reader.seek_relative(i64::from(size) - 4)?; + } + + Ok(None) + } +} +impl<'r, R: Read + Seek + ?Sized> LazyObject<'r, R> { + pub fn properties<'a>( + &'a mut self, + legacy: bool, + ) -> impl Iterator> + use<'r, 'a, R> { + (0..self.prop_count).map(move |_| { + let name = self.reader.read_bin_hash::()?; + let kind = self.reader.read_property_kind(legacy)?; + let value = kind.read(self.reader, legacy)?; + Ok((name, value)) + }) + } +} + +#[derive(Debug)] +pub struct LazyField<'r, R: Read + Seek + ?Sized> { + pub header: PropHeader, + pub reader: &'r mut R, + + pub size: u32, + pub prop_count: u16, +} diff --git a/crates/ltk_meta/src/lib.rs b/crates/ltk_meta/src/lib.rs index b189cfd9..2d28126b 100644 --- a/crates/ltk_meta/src/lib.rs +++ b/crates/ltk_meta/src/lib.rs @@ -78,4 +78,6 @@ pub use tree::*; mod error; pub use error::*; +pub mod header; +pub mod lazy; pub mod traits; diff --git a/crates/ltk_meta/src/tree.rs b/crates/ltk_meta/src/tree.rs index 24136992..a4b1a653 100644 --- a/crates/ltk_meta/src/tree.rs +++ b/crates/ltk_meta/src/tree.rs @@ -3,17 +3,20 @@ pub use builder::Builder; mod object; use ltk_hash::BinHash; +use ltk_primitives::PrefixString; pub use object::{BinObject, Builder as ObjectBuilder}; mod read; mod write; +pub use write::*; + #[cfg(test)] mod tests; use indexmap::IndexMap; -use crate::property::NoMeta; +use crate::{header::PropHeader, property::NoMeta}; /// The complete contents of a League of Legends property bin file. /// @@ -56,7 +59,7 @@ pub struct Bin { /// /// Property bins can depend on other property bins in a similar fashion /// to importing code libraries. - pub dependencies: Vec, + pub dependencies: Vec>, /// Data overrides (currently not fully implemented). data_overrides: Vec<()>, @@ -91,10 +94,10 @@ impl Bin { /// ``` pub fn new( objects: impl IntoIterator>, - dependencies: impl IntoIterator>, + dependencies: impl IntoIterator>>, ) -> Self { Self { - version: 3, + version: PropHeader::LATEST_VERSION, is_override: false, objects: objects .into_iter() @@ -168,7 +171,7 @@ impl Bin { } /// Adds a dependency to the tree. - pub fn add_dependency(&mut self, dependency: impl Into) { + pub fn add_dependency(&mut self, dependency: impl Into>) { self.dependencies.push(dependency.into()); } diff --git a/crates/ltk_meta/src/tree/builder.rs b/crates/ltk_meta/src/tree/builder.rs index a72ef205..7229e94b 100644 --- a/crates/ltk_meta/src/tree/builder.rs +++ b/crates/ltk_meta/src/tree/builder.rs @@ -1,3 +1,5 @@ +use ltk_primitives::PrefixString; + use super::BinObject; use crate::Bin; @@ -19,7 +21,7 @@ use crate::Bin; pub struct Builder { is_override: bool, objects: Vec>, - dependencies: Vec, + dependencies: Vec>, } impl Builder { @@ -40,13 +42,16 @@ impl Builder { } /// Adds a single dependency. - pub fn dependency(mut self, dep: impl Into) -> Self { + pub fn dependency(mut self, dep: impl Into>) -> Self { self.dependencies.push(dep.into()); self } /// Adds multiple dependencies. - pub fn dependencies(mut self, deps: impl IntoIterator>) -> Self { + pub fn dependencies( + mut self, + deps: impl IntoIterator>>, + ) -> Self { self.dependencies.extend(deps.into_iter().map(Into::into)); self } diff --git a/crates/ltk_meta/src/tree/read.rs b/crates/ltk_meta/src/tree/read.rs index 94481d5e..2fa28517 100644 --- a/crates/ltk_meta/src/tree/read.rs +++ b/crates/ltk_meta/src/tree/read.rs @@ -1,12 +1,11 @@ use std::io; -use crate::Error; +use crate::{header::Header, Error}; use super::{Bin, BinObject}; use byteorder::{ReadBytesExt as _, LE}; use indexmap::IndexMap; use ltk_hash::{BinHash, ReadBytesExt as _}; -use ltk_io_ext::ReaderExt; impl Bin { pub const PROP: u32 = u32::from_le_bytes(*b"PROP"); @@ -20,55 +19,9 @@ impl Bin { pub fn from_reader( reader: &mut R, ) -> Result { - let magic = reader.read_u32::()?; - let is_override = match magic { - Self::PROP => false, - Self::PTCH => { - let override_version = reader.read_u32::()?; - if override_version != 1 { - return Err(Error::InvalidFileVersion(override_version)); - } - - // It might be possible to create an override property bin - // and set the original file as a dependency. - // This seems to be the object count of the override section - let _maybe_override_object_count = reader.read_u32::()?; - - let magic = reader.read_u32::()?; - if magic != Self::PROP { - // TODO (alan): repr this in the error - log::error!( - "Expected PROP ({:#x}) section after PTCH ({:#x}), got '{:#x}'", - Self::PROP, - Self::PTCH, - magic - ); - return Err(Error::InvalidFileSignature); - } - true - } - _ => return Err(Error::InvalidFileSignature), - }; - - let version = reader.read_u32::()?; - if !matches!(version, 1..=3) { - // TODO (alan): distinguish override/non-override version - return Err(Error::InvalidFileVersion(version)); - } - - let dependencies = match version { - 2.. => { - let dep_count = reader.read_u32::()?; - let mut dependencies = Vec::with_capacity(dep_count as _); - for _ in 0..dep_count { - dependencies.push(reader.read_sized_string_u16::()?); - } - dependencies - } - _ => Vec::new(), - }; + let (prop, patch) = Header::from_reader(reader)?.into_parts(); - let obj_count = reader.read_u32::()? as usize; + let obj_count = prop.object_count.try_into().unwrap(); let mut obj_classes = Vec::with_capacity(obj_count); for _ in 0..obj_count { obj_classes.push(reader.read_bin_hash::()?); @@ -84,7 +37,7 @@ impl Bin { e => e?, } - let data_overrides = match (is_override, version) { + let data_overrides = match (patch.is_some(), prop.version) { (true, 3..) => { let count = reader.read_u32::()?; let mut v = Vec::with_capacity(count as _); @@ -97,10 +50,15 @@ impl Bin { }; Ok(Self { - version, - is_override, + version: prop.version, + is_override: patch.is_some(), objects, - dependencies, + dependencies: prop + .dependencies + .unwrap_or_default() + .into_iter() + .map(|d| d.into()) + .collect(), data_overrides, }) } diff --git a/crates/ltk_meta/src/tree/write.rs b/crates/ltk_meta/src/tree/write.rs index 25eaae01..478048aa 100644 --- a/crates/ltk_meta/src/tree/write.rs +++ b/crates/ltk_meta/src/tree/write.rs @@ -1,20 +1,24 @@ use std::io; +use crate::header::PropHeader; + use super::Bin; use byteorder::{WriteBytesExt as _, LE}; use ltk_hash::WriteBytesExt as _; -use ltk_io_ext::WriterExt as _; +use ltk_primitives::StringWriteError; -/// The version used when writing Bin files. -/// -/// This is always version 3, which supports all features including -/// dependencies and data overrides. -pub const WRITE_VERSION: u32 = 3; +#[derive(Debug, thiserror::Error)] +pub enum BinWriteError { + #[error(transparent)] + StringWrite(#[from] StringWriteError), + #[error(transparent)] + Io(#[from] io::Error), +} impl Bin { /// Write this bin to a writer. /// - /// The output will always use version 3 format, regardless of the + /// The output will always use the latest format we support ([`PropHeader::LATEST_VERSION`]), regardless of the /// `version` field on this struct. This ensures maximum compatibility /// and feature support. /// @@ -31,24 +35,21 @@ impl Bin { /// let tree = Bin::default(); /// let mut buffer = Cursor::new(Vec::new()); /// tree.to_writer(&mut buffer)?; - /// # Ok::<(), std::io::Error>(()) + /// # Ok::<(), ltk_meta::BinWriteError>(()) /// ``` - pub fn to_writer(&self, writer: &mut W) -> io::Result<()> { - match self.is_override { - true => todo!("implement is_override Bin write"), - false => { - writer.write_u32::(Self::PROP)?; - } - } + pub fn to_writer( + &self, + writer: &mut W, + ) -> Result<(), BinWriteError> { + writer.write_u32::(PropHeader::MAGIC)?; - // Always write version 3 - writer.write_u32::(WRITE_VERSION)?; - writer.write_u32::(self.dependencies.len() as _)?; + writer.write_u32::(self.version)?; + writer.write_u32::(self.dependencies.len().try_into().unwrap())?; for dep in &self.dependencies { - writer.write_len_prefixed_string::(dep)?; + dep.to_writer(writer)?; } - writer.write_u32::(self.objects.len() as _)?; + writer.write_u32::(self.objects.len().try_into().unwrap())?; for obj in self.objects.values() { writer.write_bin_hash::(obj.class_hash)?; }