From 6b7d0edd7be46a704e39267436c91d93f1eba47f Mon Sep 17 00:00:00 2001 From: ryan-rsm-mckenzie Date: Wed, 24 Jan 2024 15:27:13 -0800 Subject: [PATCH] add documentation --- Cargo.toml | 1 + README.md | 3 +++ src/derive.rs | 20 ++++++++++++++--- src/fo4/archive.rs | 6 ++++- src/fo4/chunk.rs | 1 + src/fo4/file.rs | 1 + src/fo4/hashing.rs | 8 +++++++ src/fo4/mod.rs | 29 ++++++++++++++++++++++++ src/guess.rs | 4 ++++ src/lib.rs | 14 ++++++++++++ src/tes3/archive.rs | 6 ++++- src/tes3/file.rs | 1 + src/tes3/hashing.rs | 5 +++++ src/tes4/archive.rs | 52 ++++++++++++++++++++++++++++++++++++++++++- src/tes4/directory.rs | 6 ++++- src/tes4/file.rs | 1 + src/tes4/hashing.rs | 13 +++++++++++ src/tes4/mod.rs | 10 +++++++++ 18 files changed, 174 insertions(+), 7 deletions(-) create mode 100644 README.md diff --git a/Cargo.toml b/Cargo.toml index f1ba3de..1814c3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ include = [ keywords = ["archive", "ba2", "bethesda", "bgs", "bsa"] license = "0BSD" name = "bsa" +readme = "README.md" repository = "https://github.com/Ryan-rsm-McKenzie/bsa-rs" version = "1.0.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..201f61c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +A complete\* implementation of a bsa parsing library for the Creation Engine games, supporting Morrowing up to Fallout 4, written in Rust. + +\* Some exotic formats such as XMem compression for TES4 and the GNMF format for FO4 are unsupported. diff --git a/src/derive.rs b/src/derive.rs index e7c9c4f..a3fc26d 100644 --- a/src/derive.rs +++ b/src/derive.rs @@ -232,6 +232,7 @@ pub(crate) use compressable_bytes; macro_rules! key { ($this:ident: $hash:ident) => { + /// A key for indexing into the relevant mapping. #[derive(::core::clone::Clone, ::core::fmt::Debug, ::core::default::Default)] pub struct $this<'bytes> { pub(crate) hash: $hash, @@ -307,12 +308,17 @@ macro_rules! key { pub(crate) use key; macro_rules! mapping { - ($this:ident, $mapping:ident: ($key:ident: $hash:ident) => $value:ident) => { + ( + $(#[doc=$doc:literal])* + $this:ident + $mapping:ident: ($key:ident: $hash:ident) => $value:ident + ) => { pub(crate) type $mapping<'bytes> = ::std::collections::BTreeMap<$key<'bytes>, $value<'bytes>>; impl<'bytes> crate::Sealed for $this<'bytes> {} + $(#[doc=$doc])* #[derive(::core::clone::Clone, ::core::fmt::Debug, ::core::default::Default)] pub struct $this<'bytes> { pub(crate) map: $mapping<'bytes>, @@ -463,8 +469,16 @@ macro_rules! mapping { pub(crate) use mapping; macro_rules! archive { - ($this:ident => $result:ident, $mapping:ident: ($key:ident: $hash:ident) => $value:ident) => { - crate::derive::mapping!($this, $mapping: ($key: $hash) => $value); + ( + $(#[doc=$doc:literal])* + $this:ident => $result:ident + $mapping:ident: ($key:ident: $hash:ident) => $value:ident + ) => { + crate::derive::mapping! { + $(#[doc=$doc])* + $this + $mapping: ($key: $hash) => $value + } crate::derive::reader!($this => $result); }; } diff --git a/src/fo4/archive.rs b/src/fo4/archive.rs index 2e41ea5..6749f4c 100644 --- a/src/fo4/archive.rs +++ b/src/fo4/archive.rs @@ -162,7 +162,11 @@ impl<'bytes> Key<'bytes> { } type ReadResult = (T, Options); -derive::archive!(Archive => ReadResult, Map: (Key: FileHash) => File); +derive::archive! { + /// Represents the FO4 revision of the ba2 format. + Archive => ReadResult + Map: (Key: FileHash) => File +} impl<'bytes> Archive<'bytes> { pub fn write(&self, stream: &mut Out, options: &Options) -> Result<()> diff --git a/src/fo4/chunk.rs b/src/fo4/chunk.rs index 2e2ae78..9e90cc0 100644 --- a/src/fo4/chunk.rs +++ b/src/fo4/chunk.rs @@ -88,6 +88,7 @@ impl From for Extra { } } +/// Represents a chunk of a file within the FO4 virtual filesystem. #[derive(Clone, Debug, Default)] pub struct Chunk<'bytes> { pub(crate) bytes: CompressableBytes<'bytes>, diff --git a/src/fo4/file.rs b/src/fo4/file.rs index f8ffa5f..b009258 100644 --- a/src/fo4/file.rs +++ b/src/fo4/file.rs @@ -220,6 +220,7 @@ impl From for Header { type Container<'bytes> = Vec>; +/// Represents a file within the FO4 virtual filesystem. #[derive(Clone, Debug, Default)] pub struct File<'bytes> { pub(crate) chunks: Container<'bytes>, diff --git a/src/fo4/hashing.rs b/src/fo4/hashing.rs index 7d6d80f..e907041 100644 --- a/src/fo4/hashing.rs +++ b/src/fo4/hashing.rs @@ -2,11 +2,15 @@ use crate::{derive, hashing}; use bstr::{BStr, BString, ByteSlice as _}; // archives aren't sorted in any particular order, so we can just default these +/// The underlying hash object used to uniquely identify objects within the archive. #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] #[repr(C)] pub struct Hash { + /// The file's stem crc. pub file: u32, + /// The first 4 bytes of the file's extension. pub extension: u32, + /// The file's parent path crc. pub directory: u32, } @@ -97,12 +101,16 @@ fn split_path(path: &BStr) -> Split<'_> { } } +/// Produces a hash using the given path. #[must_use] pub fn hash_file(path: &BStr) -> (FileHash, BString) { let mut path = path.to_owned(); (hash_file_in_place(&mut path), path) } +/// Produces a hash using the given path. +/// +/// The path is normalized in place. After the function returns, the path contains the string that would be stored on disk. #[must_use] pub fn hash_file_in_place(path: &mut BString) -> FileHash { hashing::normalize_path(path); diff --git a/src/fo4/mod.rs b/src/fo4/mod.rs index e7906a3..849bebe 100644 --- a/src/fo4/mod.rs +++ b/src/fo4/mod.rs @@ -83,37 +83,66 @@ impl From for Error { pub type Result = core::result::Result; +/// A list of all compression methods supported by the ba2 format. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum CompressionFormat { + /// The default compression format, compatible with all games that utilize the ba2 format. #[default] Zip, + + /// A more specialized format leveraging lz4's fast decompression to improve streaming time. + /// + /// Only compatible with Starfield or later. LZ4, } +/// Specifies the compression level to use when compressing data. +/// +/// Only compatible with [`CompressionFormat::Zip`]. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum CompressionLevel { + /// Fallout 4. #[default] FO4, + + /// Fallout 4 on the xbox. + /// + /// Uses a smaller windows size, but higher a compression level to yield a higher compression ratio. FO4Xbox, + + /// Starfield. + /// + /// Uses a custom DEFLATE algorithm with zlib wrapper to obtain a good compression ratio. SF, } impl CompressionLevel { + /// Fallout 76. pub const FO76: Self = Self::FO4; } +/// Represents the file format for an archive. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum Format { + /// A general archive can contain any kind of file. #[default] GNRL, + + /// A directx archive can only contain .dds files. DX10, } +/// Indicates the version of an archive. #[allow(non_camel_case_types)] #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] pub enum Version { + /// Initial format introduced in Fallout 4. #[default] v1 = 1, + + /// Intoduced in Starfield. v2 = 2, + + /// Intoduced in Starfield. v3 = 3, } diff --git a/src/guess.rs b/src/guess.rs index 069b827..a2374de 100644 --- a/src/guess.rs +++ b/src/guess.rs @@ -2,6 +2,7 @@ use crate::cc; use core::mem; use std::io::Read; +/// The file format for a given archive. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum FileFormat { TES3, @@ -12,6 +13,9 @@ pub enum FileFormat { const BSA: u32 = cc::make_four(b"BSA"); const BTDX: u32 = cc::make_four(b"BTDX"); +/// Guesses the archive format for a given source. +/// +/// This function does not guarantee that the given source constitutes a well-formed archive of the deduced format. It merely remarks that if the file were a well-formed archive, it would be of the deduced format. #[allow(clippy::module_name_repetitions)] pub fn guess_format(source: &mut In) -> Option where diff --git a/src/lib.rs b/src/lib.rs index fb77bed..2c45c40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,8 +23,14 @@ pub mod tes4; pub use guess::{guess_format, FileFormat}; +/// Makes a shallow copy of the input. +/// +/// The lifetime of the result is tied to the input buffer. pub struct Borrowed<'borrow>(pub &'borrow [u8]); +/// Makes a deep copy of the input. +/// +/// The lifetime of the result is independent of the input buffer. pub struct Copied<'copy>(pub &'copy [u8]); mod private { @@ -37,20 +43,26 @@ pub trait Reader: Sealed { type Error; type Item; + /// Reads and instance `Self::Item` from the given source. fn read(source: T) -> core::result::Result; } pub trait CompressableFrom: Sealed { + /// Makes a compressed instance of `Self` using the given data. #[must_use] fn from_compressed(value: T, decompressed_len: usize) -> Self; + /// Makes a decompressed instance of `Self` using the given data. #[must_use] fn from_decompressed(value: T) -> Self; } +/// Indicates whether the operation should finish by compressing the data or not. #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum CompressionResult { + /// The data will finish in a compressed state. Compressed, + /// The data will finish in a decompressed state. #[default] Decompressed, } @@ -60,11 +72,13 @@ pub trait ReaderWithOptions: Sealed { type Item; type Options; + /// Reads an instance of `Self::Item` from the given source, using the given options. fn read(source: T, options: &Self::Options) -> core::result::Result; } pub use bstr::{BStr, BString, ByteSlice, ByteVec}; +/// Convenience using statements for traits that are needed to work with the library. pub mod prelude { pub use crate::{CompressableFrom as _, Reader as _, ReaderWithOptions as _}; } diff --git a/src/tes3/archive.rs b/src/tes3/archive.rs index eae66f7..52fc030 100644 --- a/src/tes3/archive.rs +++ b/src/tes3/archive.rs @@ -54,7 +54,11 @@ impl<'bytes> Key<'bytes> { } type ReadResult = T; -derive::archive!(Archive => ReadResult, Map: (Key: FileHash) => File); +derive::archive! { + /// Represents the TES3 revision of the bsa format. + Archive => ReadResult + Map: (Key: FileHash) => File +} impl<'bytes> Archive<'bytes> { pub fn write(&self, stream: &mut Out) -> Result<()> diff --git a/src/tes3/file.rs b/src/tes3/file.rs index c2533be..5163b7a 100644 --- a/src/tes3/file.rs +++ b/src/tes3/file.rs @@ -6,6 +6,7 @@ use crate::{ }; use std::io::Write; +/// Represents a file within the TES3 virtual filesystem. #[derive(Clone, Debug, Default)] pub struct File<'bytes> { pub(crate) bytes: Bytes<'bytes>, diff --git a/src/tes3/hashing.rs b/src/tes3/hashing.rs index f341bc7..18ddf54 100644 --- a/src/tes3/hashing.rs +++ b/src/tes3/hashing.rs @@ -2,6 +2,7 @@ use crate::{derive, hashing}; use bstr::{BStr, BString}; use core::cmp::Ordering; +/// The underlying hash object used to uniquely identify objects within the archive. #[derive(Clone, Copy, Debug, Default)] #[repr(C)] pub struct Hash { @@ -44,12 +45,16 @@ impl Ord for Hash { } } +/// Produces a hash using the given path. #[must_use] pub fn hash_file(path: &BStr) -> (FileHash, BString) { let mut path = path.to_owned(); (hash_file_in_place(&mut path), path) } +/// Produces a hash using the given path. +/// +/// The path is normalized in place. After the function returns, the path contains the string that would be stored on disk. #[must_use] pub fn hash_file_in_place(path: &mut BString) -> FileHash { hashing::normalize_path(path); diff --git a/src/tes4/archive.rs b/src/tes4/archive.rs index e62fa22..f593e86 100644 --- a/src/tes4/archive.rs +++ b/src/tes4/archive.rs @@ -13,19 +13,62 @@ use core::mem; use std::{borrow::Cow, io::Write}; bitflags::bitflags! { + /// Archive flags can impact the layout of an archive, or how it is read. #[repr(transparent)] #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct Flags: u32 { + /// Includes directory paths within the archive. + /// + /// `archive.exe` does not let you write archives without this flag set. + /// + /// This includes only the parent path of all files, and not filenames. const DIRECTORY_STRINGS = 1 << 0; + + /// Includes filenames within the archive. + /// + /// `archive.exe` does not let you write archives without this flag set. + /// + /// This includes only the filename of all files, and not the parent path. const FILE_STRINGS = 1 << 1; + + /// Compresses the data within the archive. + /// + /// * The v103 format uses zlib. + /// * The v104 format uses xmem and zlib. + /// * The v105 format uses lz4. const COMPRESSED = 1 << 2; + + /// Impacts runtime parsing. const RETAIN_DIRECTORY_NAMES = 1 << 3; + + /// Impacts runtime parsing. const RETAIN_FILE_NAMES = 1 << 4; + + /// Impacts runtime parsing. const RETAIN_FILE_NAME_OFFSETS = 1 << 5; + + /// Writes the archive in the xbox (big-endian) format. + /// + /// This flag affects the sort order of files on disk. + /// + /// Only the crc hash is actually written in big-endian format. const XBOX_ARCHIVE = 1 << 6; + + /// Impacts runtime parsing. const RETAIN_STRINGS_DURING_STARTUP = 1 << 7; + + /// Writes the full (virtual) path of a file next to the data blob. + /// + /// This flag has a different meaning in the v103 format. const EMBEDDED_FILE_NAMES = 1 << 8; + + /// Uses the xmem codec from XNA 4.0 to compress the archive. + /// + /// This flag requires [`Self::compressed`] to be set as well. + /// + /// This flag is unused in SSE. const XBOX_COMPRESSED = 1 << 9; + const _ = !0; } } @@ -89,6 +132,9 @@ impl Flags { } bitflags::bitflags! { + /// Specifies file types contained within an archive. + /// + /// It's not apparent if the game actually uses these flags for anything. #[repr(transparent)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub struct Types: u16 { @@ -316,7 +362,11 @@ impl Options { } type ReadResult = (T, Options); -derive::archive!(Archive => ReadResult, Map: (Key: DirectoryHash) => Directory); +derive::archive! { + /// Represents the TES4 revision of the bsa format. + Archive => ReadResult + Map: (Key: DirectoryHash) => Directory +} impl<'bytes> Archive<'bytes> { pub fn write(&self, stream: &mut Out, options: &Options) -> Result<()> diff --git a/src/tes4/directory.rs b/src/tes4/directory.rs index 2718787..f8e879e 100644 --- a/src/tes4/directory.rs +++ b/src/tes4/directory.rs @@ -13,7 +13,11 @@ impl<'bytes> Key<'bytes> { } } -derive::mapping!(Directory, Map: (Key: FileHash) => File); +derive::mapping! { + /// Represents a directory within the TES4 virtual filesystem. + Directory + Map: (Key: FileHash) => File +} #[cfg(test)] mod tests { diff --git a/src/tes4/file.rs b/src/tes4/file.rs index 5231058..f0f04a1 100644 --- a/src/tes4/file.rs +++ b/src/tes4/file.rs @@ -124,6 +124,7 @@ impl ReadOptions { } } +/// Represents a file within the TES4 virtual filesystem. #[derive(Clone, Debug, Default)] pub struct File<'bytes> { pub(crate) bytes: CompressableBytes<'bytes>, diff --git a/src/tes4/hashing.rs b/src/tes4/hashing.rs index 981c156..4dae13b 100644 --- a/src/tes4/hashing.rs +++ b/src/tes4/hashing.rs @@ -2,12 +2,17 @@ use crate::{cc, derive, hashing}; use bstr::{BStr, BString, ByteSlice as _}; use core::cmp::Ordering; +/// The underlying hash object used to uniquely identify objects within the archive. #[derive(Clone, Copy, Debug, Default)] #[repr(C)] pub struct Hash { + /// The last character of the path (directory) or stem (file). pub last: u8, + /// The second to last character of the path (directory) or stem (file). pub last2: u8, + /// The length of the path (directory) or stem (file). pub length: u8, + /// The first character of the path (directory) or stem (file). pub first: u8, pub crc: u32, } @@ -60,12 +65,16 @@ fn crc32(bytes: &[u8]) -> u32 { crc } +/// Produces a hash using the given path. #[must_use] pub fn hash_directory(path: &BStr) -> (DirectoryHash, BString) { let mut path = path.to_owned(); (hash_directory_in_place(&mut path), path) } +/// Produces a hash using the given path. +/// +/// The path is normalized in place. After the function returns, the path contains the string that would be stored on disk. #[must_use] pub fn hash_directory_in_place(path: &mut BString) -> DirectoryHash { hashing::normalize_path(path); @@ -93,12 +102,16 @@ pub fn hash_directory_in_place(path: &mut BString) -> DirectoryHash { h.into() } +/// Produces a hash using the given path. #[must_use] pub fn hash_file(path: &BStr) -> (FileHash, BString) { let mut path = path.to_owned(); (hash_file_in_place(&mut path), path) } +/// Produces a hash using the given path. +/// +/// The path is normalized in place. After the function returns, the path contains the string that would be stored on disk. #[must_use] pub fn hash_file_in_place(path: &mut BString) -> FileHash { const LUT: [u32; 6] = [ diff --git a/src/tes4/mod.rs b/src/tes4/mod.rs index 5d38f6d..3591a3c 100644 --- a/src/tes4/mod.rs +++ b/src/tes4/mod.rs @@ -66,14 +66,19 @@ impl From for Error { pub type Result = core::result::Result; +/// Specifies the codec to use when performing compression/decompression actions on files. #[non_exhaustive] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub enum CompressionCodec { + /// The default compression codec. #[default] Normal, //XMem, } +/// The archive version. +/// +/// Each version has an impact on the abi of the TES4 archive file format. #[allow(non_camel_case_types)] #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] pub enum Version { @@ -84,9 +89,14 @@ pub enum Version { } impl Version { + /// The Elder Scrolls IV: Oblivion. pub const TES4: Self = Self::v103; + /// Fallout 3. pub const FO3: Self = Self::v104; + /// Fallout: New Vegas. pub const FNV: Self = Self::v104; + /// The Elder Scrolls V: Skyrim. pub const TES5: Self = Self::v104; + /// The Elder Scrolls V: Skyrim - Special Edition. pub const SSE: Self = Self::v105; }