aboutsummaryrefslogtreecommitdiff
path: root/libs/bsa_ffi/src/archive/ba2_reader.rs
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/bsa_ffi/src/archive/ba2_reader.rs
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/bsa_ffi/src/archive/ba2_reader.rs')
-rw-r--r--libs/bsa_ffi/src/archive/ba2_reader.rs125
1 files changed, 125 insertions, 0 deletions
diff --git a/libs/bsa_ffi/src/archive/ba2_reader.rs b/libs/bsa_ffi/src/archive/ba2_reader.rs
new file mode 100644
index 0000000..e6dd984
--- /dev/null
+++ b/libs/bsa_ffi/src/archive/ba2_reader.rs
@@ -0,0 +1,125 @@
+//! BA2 (Fallout 4/Starfield) archive reading
+//!
+//! Provides read support for FO4 format BA2 files (Fallout 4, Fallout 76, Starfield).
+
+use anyhow::{bail, Context, Result};
+use ba2::fo4::{Archive, File as Ba2File, FileWriteOptions};
+use ba2::prelude::*;
+use ba2::ByteSlice;
+use rayon::prelude::*;
+use std::collections::HashSet;
+use std::io::Cursor;
+use std::path::Path;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use tracing::debug;
+
+/// Entry for a file in a BA2 archive
+#[derive(Debug, Clone)]
+pub struct Ba2FileEntry {
+ pub path: String,
+}
+
+/// List all files in a BA2 archive
+pub fn list_files(ba2_path: &Path) -> Result<Vec<Ba2FileEntry>> {
+ let (archive, _options): (Archive, _) = Archive::read(ba2_path)
+ .with_context(|| format!("Failed to open BA2: {}", ba2_path.display()))?;
+
+ let mut files = Vec::new();
+
+ for (key, _file) in archive.iter() {
+ let path = String::from_utf8_lossy(key.name().as_bytes()).to_string();
+
+ files.push(Ba2FileEntry { path });
+ }
+
+ debug!("Listed {} files in BA2 {}", files.len(), ba2_path.display());
+ Ok(files)
+}
+
+/// Extract a single file from a BA2 archive
+#[allow(dead_code)]
+pub fn extract_file(ba2_path: &Path, file_path: &str) -> Result<Vec<u8>> {
+ let (archive, options): (Archive, _) = Archive::read(ba2_path)
+ .with_context(|| format!("Failed to open BA2: {}", ba2_path.display()))?;
+
+ let write_options: FileWriteOptions = options.into();
+
+ // Normalize path for comparison (BA2 uses forward slashes typically)
+ let normalized = file_path.replace('\\', "/").to_lowercase();
+ let normalized_backslash = file_path.replace('/', "\\").to_lowercase();
+
+ for (key, file) in archive.iter() {
+ let current_path = String::from_utf8_lossy(key.name().as_bytes()).to_lowercase();
+
+ // Try both slash conventions
+ if current_path == normalized
+ || current_path == normalized_backslash
+ || current_path.replace('\\', "/") == normalized
+ || current_path.replace('/', "\\") == normalized_backslash
+ {
+ // Write to memory buffer
+ let mut buffer = Cursor::new(Vec::new());
+ file.write(&mut buffer, &write_options)
+ .with_context(|| format!("Failed to extract file: {}", file_path))?;
+
+ return Ok(buffer.into_inner());
+ }
+ }
+
+ bail!(
+ "File not found in BA2: {} (searched for '{}')",
+ file_path,
+ normalized
+ )
+}
+
+/// Extract multiple files from a BA2 archive in parallel.
+/// Opens the archive once, collects matching entries, then decompresses
+/// and writes them in parallel using rayon.
+/// `wanted` should contain lowercase forward-slash-separated paths.
+pub fn extract_files_batch<F>(
+ ba2_path: &Path,
+ wanted: &HashSet<String>,
+ callback: F,
+) -> Result<usize>
+where
+ F: Fn(&str, Vec<u8>) -> Result<()> + Send + Sync,
+{
+ let (archive, options): (Archive, _) = Archive::read(ba2_path)
+ .with_context(|| format!("Failed to open BA2: {}", ba2_path.display()))?;
+
+ let write_options: FileWriteOptions = options.into();
+
+ // Collect matching entries with references
+ let mut entries: Vec<(String, &Ba2File)> = Vec::new();
+ for (key, file) in archive.iter() {
+ let path = String::from_utf8_lossy(key.name().as_bytes()).to_string();
+ let lookup = path.replace('\\', "/").to_lowercase();
+ if wanted.contains(&lookup) {
+ entries.push((path, file));
+ }
+ }
+
+ // Decompress + write in parallel
+ let extracted = AtomicUsize::new(0);
+ entries
+ .par_iter()
+ .try_for_each(|(path, file)| -> Result<()> {
+ let mut buffer = Cursor::new(Vec::new());
+ file.write(&mut buffer, &write_options)
+ .with_context(|| format!("Failed to extract file: {}", path))?;
+
+ callback(path, buffer.into_inner())?;
+ extracted.fetch_add(1, Ordering::Relaxed);
+ Ok(())
+ })?;
+
+ let count = extracted.load(Ordering::Relaxed);
+ debug!(
+ "Batch extracted {} of {} wanted files from BA2 {}",
+ count,
+ wanted.len(),
+ ba2_path.display()
+ );
+ Ok(count)
+}