Destructure arrays to avoid unnecessarily fallible conversions

This partly relies on slice::as_chunks(), which was added in Rust v1.88.0.
This commit is contained in:
Oliver Hamlet
2025-07-22 19:56:20 +01:00
parent ea48f5d14e
commit 39a16fc117
4 changed files with 100 additions and 114 deletions
+13 -18
View File
@@ -4,9 +4,7 @@ use std::{
io::{BufRead, Seek},
};
use super::error::{ArchiveParsingError, slice_too_small};
use super::parse::{to_u32, to_u64};
use super::error::ArchiveParsingError;
pub(super) const TYPE_ID: [u8; 4] = *b"BTDX";
const HEADER_SIZE: usize = 24;
@@ -26,12 +24,20 @@ impl TryFrom<[u8; HEADER_SIZE - TYPE_ID.len()]> for Header {
type Error = ArchiveParsingError;
fn try_from(value: [u8; HEADER_SIZE - TYPE_ID.len()]) -> Result<Self, Self::Error> {
// LIMITATION: There's no syntax to infallibly split an array into sub-arrays.
#[rustfmt::skip]
let [
v0, v1, v2, v3, // version
a0, a1, a2, a3, // archive_type
c0, c1, c2, c3, // file_count
file_paths_offset @ ..,
] = value;
let header = Self {
type_id: TYPE_ID,
version: to_u32(&value, 0)?,
archive_type: to_archive_type(&value)?,
file_count: to_u32(&value, 8)?,
file_paths_offset: to_u64(&value, 12)?,
version: u32::from_le_bytes([v0, v1, v2, v3]),
archive_type: [a0, a1, a2, a3],
file_count: u32::from_le_bytes([c0, c1, c2, c3]),
file_paths_offset: u64::from_le_bytes(file_paths_offset),
};
// The header version is 1, 7 or 8 for Fallout 4 and 2 or 3 for Starfield.
@@ -51,17 +57,6 @@ impl TryFrom<[u8; HEADER_SIZE - TYPE_ID.len()]> for Header {
}
}
fn to_archive_type(
array: &[u8; HEADER_SIZE - TYPE_ID.len()],
) -> Result<[u8; 4], ArchiveParsingError> {
let slice = &array[4..8];
slice
.try_into()
// This should be impossible, but it can't be asserted at compile time.
.map_err(|_e| slice_too_small(slice, 4))
}
pub(super) fn read_assets<T: BufRead + Seek>(
mut reader: T,
) -> Result<BTreeMap<u64, BTreeSet<u64>>, ArchiveParsingError> {
+87 -50
View File
@@ -5,8 +5,6 @@ use std::{
use super::error::ArchiveParsingError;
use super::parse::{to_u32, to_u64, to_usize};
pub(super) const TYPE_ID: [u8; 4] = *b"BSA\0";
const HEADER_SIZE: usize = 36;
const FILE_RECORD_SIZE: usize = 16;
@@ -28,16 +26,48 @@ impl TryFrom<[u8; HEADER_SIZE - TYPE_ID.len()]> for Header {
type Error = ArchiveParsingError;
fn try_from(value: [u8; HEADER_SIZE - TYPE_ID.len()]) -> Result<Self, Self::Error> {
// LIMITATION: There's no syntax to infallibly split an array into sub-arrays.
#[rustfmt::skip]
let [
v0, v1, v2, v3, // version
r0, r1, r2, r3, // records_offset
a0, a1, a2, a3, // archive_flags
folder_count0, folder_count1, folder_count2, folder_count3,
file_count0, file_count1, file_count2, file_count3,
folder_names0, folder_names1, folder_names2, folder_names3,
file_names0, file_names1, file_names2, file_names3,
content_type_flags @ ..,
] = value;
let header = Self {
type_id: TYPE_ID,
version: to_u32(&value, 0)?,
records_offset: to_u32(&value, 4)?,
archive_flags: to_u32(&value, 8)?,
folder_count: to_u32(&value, 12)?,
total_file_count: to_u32(&value, 16)?,
total_folder_names_length: to_u32(&value, 20)?,
total_file_names_length: to_u32(&value, 24)?,
content_type_flags: to_u32(&value, 28)?,
version: u32::from_le_bytes([v0, v1, v2, v3]),
records_offset: u32::from_le_bytes([r0, r1, r2, r3]),
archive_flags: u32::from_le_bytes([a0, a1, a2, a3]),
folder_count: u32::from_le_bytes([
folder_count0,
folder_count1,
folder_count2,
folder_count3,
]),
total_file_count: u32::from_le_bytes([
file_count0,
file_count1,
file_count2,
file_count3,
]),
total_folder_names_length: u32::from_le_bytes([
folder_names0,
folder_names1,
folder_names2,
folder_names3,
]),
total_file_names_length: u32::from_le_bytes([
file_names0,
file_names1,
file_names2,
file_names3,
]),
content_type_flags: u32::from_le_bytes(content_type_flags),
};
if to_usize(header.records_offset) != HEADER_SIZE {
@@ -62,54 +92,43 @@ struct FolderRecord {
// Also used for v104 BSAs.
mod v103 {
use crate::archive::{
error::ArchiveParsingError,
parse::{to_u32, to_u64},
};
use super::FolderRecord;
pub(super) const FOLDER_RECORD_SIZE: usize = 16;
pub(super) fn read_folder_record(value: &[u8]) -> Result<FolderRecord, ArchiveParsingError> {
if value.len() < FOLDER_RECORD_SIZE {
return Err(ArchiveParsingError::SliceTooSmall {
expected: FOLDER_RECORD_SIZE,
actual: value.len(),
});
}
pub(super) fn read_folder_record(value: &[u8; FOLDER_RECORD_SIZE]) -> FolderRecord {
// LIMITATION: There's no syntax to infallibly split an array into sub-arrays.
let [name_hash @ .., c0, c1, c2, c3, o0, o1, o2, o3] = *value;
Ok(FolderRecord {
name_hash: to_u64(value, 0)?,
file_count: to_u32(value, 8)?,
file_records_offset: to_u32(value, 12)?,
})
FolderRecord {
name_hash: u64::from_le_bytes(name_hash),
file_count: u32::from_le_bytes([c0, c1, c2, c3]),
file_records_offset: u32::from_le_bytes([o0, o1, o2, o3]),
}
}
}
mod v105 {
use crate::archive::{
error::ArchiveParsingError,
parse::{to_u32, to_u64},
};
use super::FolderRecord;
pub(super) const FOLDER_RECORD_SIZE: usize = 24;
pub(super) fn read_folder_record(value: &[u8]) -> Result<FolderRecord, ArchiveParsingError> {
if value.len() < FOLDER_RECORD_SIZE {
return Err(ArchiveParsingError::SliceTooSmall {
expected: FOLDER_RECORD_SIZE,
actual: value.len(),
});
}
pub(super) fn read_folder_record(value: &[u8; FOLDER_RECORD_SIZE]) -> FolderRecord {
// LIMITATION: There's no syntax to infallibly split an array into sub-arrays.
#[rustfmt::skip]
let [
name_hash @ ..,
c0, c1, c2, c3, // file_count
_, _, _, _,
o0, o1, o2, o3, // file_records_offset
_, _, _, _,
] = *value;
Ok(FolderRecord {
name_hash: to_u64(value, 0)?,
file_count: to_u32(value, 8)?,
file_records_offset: to_u32(value, 16)?,
})
FolderRecord {
name_hash: u64::from_le_bytes(name_hash),
file_count: u32::from_le_bytes([c0, c1, c2, c3]),
file_records_offset: u32::from_le_bytes([o0, o1, o2, o3]),
}
}
}
@@ -142,7 +161,7 @@ pub(super) fn read_assets<T: BufRead>(
fn read_assets_with_header<T: BufRead, const U: usize>(
mut reader: T,
header: &Header,
read_folder_record: impl Fn(&[u8]) -> Result<FolderRecord, ArchiveParsingError>,
read_folder_record: impl Fn(&[u8; U]) -> FolderRecord,
) -> Result<BTreeMap<u64, BTreeSet<u64>>, ArchiveParsingError> {
let mut folders_buffer: Vec<u8> = vec![0; U * to_usize(header.folder_count)];
@@ -160,8 +179,8 @@ fn read_assets_with_header<T: BufRead, const U: usize>(
HEADER_SIZE + folders_buffer.len() + to_usize(header.total_file_names_length);
let mut assets = BTreeMap::new();
for chunk in folders_buffer.chunks_exact(U) {
let folder_record = read_folder_record(chunk)?;
for chunk in folders_buffer.as_chunks::<U>().0 {
let folder_record = read_folder_record(chunk);
let entry = assets.entry(folder_record.name_hash);
if let Entry::Occupied(_) = entry {
@@ -177,7 +196,7 @@ fn read_assets_with_header<T: BufRead, const U: usize>(
to_usize(folder_record.file_records_offset) - folder_record_offset_baseline;
if let Some(folder_name_length) = file_records_buffer.get(folder_name_length_offset) {
folder_name_length_offset + 1 + to_usize(u32::from(*folder_name_length))
folder_name_length_offset + 1 + usize::from(*folder_name_length)
} else {
return Err(ArchiveParsingError::InvalidFolderNameLengthOffset(
folder_name_length_offset,
@@ -194,10 +213,12 @@ fn read_assets_with_header<T: BufRead, const U: usize>(
let file_hashes: &mut BTreeSet<u64> = entry.or_default();
for file_chunk in file_records_buffer
.chunks_exact(FILE_RECORD_SIZE)
.as_chunks::<FILE_RECORD_SIZE>()
.0
.iter()
.take(to_usize(folder_record.file_count))
{
let file_hash = to_u64(file_chunk, 0)?;
let file_hash = file_record_hash(file_chunk);
if !file_hashes.insert(file_hash) {
return Err(ArchiveParsingError::HashCollision {
@@ -210,3 +231,19 @@ fn read_assets_with_header<T: BufRead, const U: usize>(
Ok(assets)
}
fn file_record_hash(file_record: &[u8; FILE_RECORD_SIZE]) -> u64 {
// LIMITATION: There's no syntax to infallibly split an array into sub-arrays.
let [hash_bytes @ .., _, _, _, _, _, _, _, _] = file_record;
u64::from_le_bytes(*hash_bytes)
}
#[expect(
clippy::as_conversions,
reason = "A compile-time assertion ensures that this conversion will be lossless on all relevant target platforms"
)]
const fn to_usize(value: u32) -> usize {
// Error at compile time if this conversion isn't lossless.
const _: () = assert!(u32::BITS <= usize::BITS, "cannot fit a u32 into a usize!");
value as usize
}
-12
View File
@@ -49,7 +49,6 @@ pub(crate) enum ArchiveParsingError {
UsesBigEndianNumbers,
FolderHashCollision(u64),
HashCollision { folder_hash: u64, file_hash: u64 },
SliceTooSmall { expected: usize, actual: usize },
}
impl std::fmt::Display for ArchiveParsingError {
@@ -81,10 +80,6 @@ impl std::fmt::Display for ArchiveParsingError {
f,
"unexpected collision for file name hash {file_hash:x} in set for folder name hash {folder_hash:x}"
),
Self::SliceTooSmall { expected, actual } => write!(
f,
"byte slice was unexpectedly too small: expected {expected} bytes, got {actual} bytes"
),
}
}
}
@@ -103,10 +98,3 @@ impl From<std::io::Error> for ArchiveParsingError {
ArchiveParsingError::IoError(value)
}
}
pub(super) fn slice_too_small(slice: &[u8], expected_size: usize) -> ArchiveParsingError {
ArchiveParsingError::SliceTooSmall {
expected: expected_size,
actual: slice.len(),
}
}
-34
View File
@@ -7,7 +7,6 @@ use std::{
use super::error::{ArchiveParsingError, ArchivePathParsingError};
use crate::{
archive::error::slice_too_small,
escape_ascii,
logging::{self, format_details},
plugin::has_ascii_extension,
@@ -95,39 +94,6 @@ fn get_assets_in_archive(
}
}
pub(super) fn to_u32(bytes: &[u8], start_index: usize) -> Result<u32, ArchiveParsingError> {
const ARRAY_SIZE: usize = to_usize(u32::BITS >> 3);
subarray::<ARRAY_SIZE>(bytes, start_index).map(u32::from_le_bytes)
}
pub(super) fn to_u64(bytes: &[u8], start_index: usize) -> Result<u64, ArchiveParsingError> {
const ARRAY_SIZE: usize = to_usize(u64::BITS >> 3);
subarray::<ARRAY_SIZE>(bytes, start_index).map(u64::from_le_bytes)
}
fn subarray<const SIZE: usize>(
bytes: &[u8],
start_index: usize,
) -> Result<[u8; SIZE], ArchiveParsingError> {
let stop_index = start_index + SIZE;
let bytes = bytes
.get(start_index..stop_index)
.ok_or_else(|| slice_too_small(bytes, stop_index))?;
<[u8; SIZE]>::try_from(bytes).map_err(|_e| slice_too_small(bytes, SIZE))
}
#[expect(
clippy::as_conversions,
reason = "A compile-time assertion ensures that this conversion will be lossless on all relevant target platforms"
)]
pub(super) const fn to_usize(value: u32) -> usize {
// Error at compile time if this conversion isn't lossless.
const _: () = assert!(u32::BITS <= usize::BITS, "cannot fit a u32 into a usize!");
value as usize
}
#[cfg(test)]
mod tests {
use super::*;