Skip to content

Commit

Permalink
add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Ryan-rsm-McKenzie committed Jan 24, 2024
1 parent e3a41ac commit 6b7d0ed
Show file tree
Hide file tree
Showing 18 changed files with 174 additions and 7 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 17 additions & 3 deletions src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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);
};
}
Expand Down
6 changes: 5 additions & 1 deletion src/fo4/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ impl<'bytes> Key<'bytes> {
}

type ReadResult<T> = (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<Out>(&self, stream: &mut Out, options: &Options) -> Result<()>
Expand Down
1 change: 1 addition & 0 deletions src/fo4/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ impl From<DX10> 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>,
Expand Down
1 change: 1 addition & 0 deletions src/fo4/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ impl From<DX10> for Header {

type Container<'bytes> = Vec<Chunk<'bytes>>;

/// Represents a file within the FO4 virtual filesystem.
#[derive(Clone, Debug, Default)]
pub struct File<'bytes> {
pub(crate) chunks: Container<'bytes>,
Expand Down
8 changes: 8 additions & 0 deletions src/fo4/hashing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -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);
Expand Down
29 changes: 29 additions & 0 deletions src/fo4/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,37 +83,66 @@ impl From<TryFromIntError> for Error {

pub type Result<T> = core::result::Result<T, Error>;

/// 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,
}
4 changes: 4 additions & 0 deletions src/guess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<In>(source: &mut In) -> Option<FileFormat>
where
Expand Down
14 changes: 14 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -37,20 +43,26 @@ pub trait Reader<T>: Sealed {
type Error;
type Item;

/// Reads and instance `Self::Item` from the given source.
fn read(source: T) -> core::result::Result<Self::Item, Self::Error>;
}

pub trait CompressableFrom<T>: 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,
}
Expand All @@ -60,11 +72,13 @@ pub trait ReaderWithOptions<T>: 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<Self::Item, Self::Error>;
}

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 _};
}
6 changes: 5 additions & 1 deletion src/tes3/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ impl<'bytes> Key<'bytes> {
}

type ReadResult<T> = 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<Out>(&self, stream: &mut Out) -> Result<()>
Expand Down
1 change: 1 addition & 0 deletions src/tes3/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down
5 changes: 5 additions & 0 deletions src/tes3/hashing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
52 changes: 51 additions & 1 deletion src/tes4/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -316,7 +362,11 @@ impl Options {
}

type ReadResult<T> = (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<Out>(&self, stream: &mut Out, options: &Options) -> Result<()>
Expand Down
Loading

0 comments on commit 6b7d0ed

Please sign in to comment.