6 releases
Uses new Rust 2024
| 0.1.5 | Dec 18, 2025 |
|---|---|
| 0.1.4 | Nov 21, 2025 |
| 0.1.3 | Oct 27, 2025 |
| 0.1.2 | Aug 11, 2025 |
#325 in Cryptography
81KB
1.5K
SLoC
PF8 - Rust Library for PF6/PF8 Archive Files
A comprehensive Rust library for encoding and decoding PF6 and PF8 archive files. This library provides both high-level convenience APIs and low-level control for working with these archive formats.
PF6 and PF8 are proprietary archive formats for the Artemis galgame engine.
Features
- Multiple Format Support:
- PF6: Read-only support, no encryption
- PF8: Full read/write support with XOR encryption
- Streaming Support: Read and write archives without loading everything into memory
- Built-in Encryption: XOR encryption with SHA1-based keys (PF8 only)
- Progress Callbacks: Real-time progress reporting during extraction
- Cancellation Support: Cancel long-running operations at any time
- Flexible API: Both high-level convenience methods and low-level control
- Path Handling: Automatic conversion between system paths and archive internal format
- Comprehensive Error Handling: Detailed error types with helpful messages
- Optional Display Features: Pretty-printed archive listings (requires
displayfeature)
Quick Start
Add this to your Cargo.toml:
[dependencies]
pf8 = "0.1"
# Without display features (pretty-printed tables)
pf8 = { version = "0.1", default-features = false }
Convenience Functions
For simple operations, use the convenience functions:
use pf8::{extract, create_from_dir, Result};
fn main() -> Result<()> {
// Extract an archive
extract("root.pfs", "output_directory")?;
// Create an archive from a directory
create_from_dir("input_directory", "new_archive.pfs")?;
Ok(())
}
Reading PF8 Archives
use pf8::{Pf8Archive, Result};
fn main() -> Result<()> {
// Open an existing PF8 archive
let mut archive = Pf8Archive::open("root.pfs")?;
// List all files in the archive
for entry in archive.entries() {
println!("{}: {} bytes", entry.path().display(), entry.size());
}
// Extract all files to a directory
archive.extract_all("output_dir")?;
// Extract a specific file
if let Some(_entry) = archive.get_entry("system/table/list_windows.tbl") {
let data = archive.read_file("system/table/list_windows.tbl")?;
std::fs::write("extracted_list_windows.tbl", data)?;
}
Ok(())
}
Creating PF8 Archives
use pf8::{Pf8Builder, Result};
fn main() -> Result<()> {
// Create a new archive builder
let mut builder = Pf8Builder::new();
// Configure encryption filters (files matching these patterns won't be encrypted)
// Ignore this will use default unencrypted lists
// builder.unencrypted_extensions(&[".mp4", ".flv"]);
// Add files and directories
builder.add_dir("scripts")?;
builder.add_file("single_file.txt")?;
builder.add_file_as("list_windows.tbl", "system/table/list_windows.tbl")?;
// Write the archive to a file
builder.write_to_file("root.pfs")?;
Ok(())
}
Display Features
With the display feature enabled, you can pretty-print archive contents:
use pf8::display::list_archive;
fn main() -> pf8::Result<()> {
list_archive("root.pfs")?;
Ok(())
}
This will output a formatted table like:
archive.pfs
| File | Size |
|-------------------|-----------|
| config/game.ini | 1.2 KB |
| image/image1.png | 45.6 MB |
| scripts/main.ast | 3.4 KB |
Total: 3 files, Total size: 45.6 MB
Advanced Usage
Low-level Reader API
use pf8::{Pf8Reader, Result};
fn main() -> Result<()> {
let mut reader = Pf8Reader::open("root.pfs")?;
for entry in reader.entries() {
println!("File: {}", entry.path().display());
println!("Size: {} bytes", entry.size());
println!("Encrypted: {}", entry.is_encrypted());
// Read file data
let data = reader.read_file(entry.path())?;
// Process data...
}
Ok(())
}
Custom Encryption Patterns
use pf8::{Pf8Archive, Pf8Builder, Result};
fn main() -> Result<()> {
// When reading, specify which files should be unencrypted
let unencrypted_patterns = &[".mp4", ".flv"];
let archive = Pf8Archive::open_with_patterns("root.pfs", unencrypted_patterns)?;
// When creating, specify patterns for unencrypted files
let mut builder = Pf8Builder::new();
builder.unencrypted_patterns(&[".mp4", ".flv"]);
builder.add_dir("src")?;
builder.write_to_file("root.pfs")?;
Ok(())
}
Streaming Operations
For large archives, you can use streaming operations to avoid loading everything into memory:
use pf8::{Pf8Reader, Result};
fn extract_large_archive(archive_path: &str, output_dir: &str) -> Result<()> {
let mut reader = Pf8Reader::open(archive_path)?;
for entry in reader.entries() {
let output_path = std::path::Path::new(output_dir).join(entry.path());
// Create parent directories
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent)?;
}
// Stream file data directly to disk
use std::fs::File;
use std::io::Write;
let mut output_file = File::create(&output_path)?;
reader.read_file_streaming(entry.path(), |chunk| {
output_file.write_all(chunk)?;
Ok(())
})?;
}
Ok(())
}
Progress Callbacks and Cancellation
For better user experience, especially in GUI applications or JNI scenarios, you can track extraction progress and cancel operations:
use pf8::{Pf8Archive, ProgressCallback, ProgressInfo, CancellationToken, Result};
use std::path::Path;
// Implement a progress callback
struct MyCallback;
impl ProgressCallback for MyCallback {
fn on_progress(&mut self, progress: &ProgressInfo) -> Result<()> {
println!(
"Progress: {:.1}% - Processing: {} ({}/{})",
progress.overall_progress(),
progress.current_file,
progress.current_file_index + 1,
progress.total_files
);
Ok(())
}
fn on_file_start(&mut self, path: &Path, index: usize, total: usize) -> Result<()> {
println!("[{}/{}] Starting: {}", index + 1, total, path.display());
Ok(())
}
}
fn main() -> Result<()> {
let mut callback = MyCallback;
// Extract with progress reporting
extract_with_progress("root.pfs", "output_directory", &mut callback)?;
Ok(())
}
For cancellable operations:
use pf8::{CancellationToken, CancellableCallback};
fn main() -> Result<()> {
let token = CancellationToken::new();
let token_clone = token.clone();
// In another thread, you can call token_clone.cancel()
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_secs(5));
token_clone.cancel();
});
let mut callback = CancellableCallback::new(MyCallback, token);
match extract_all_with_progress("archive.pf8", "output/", &mut callback) {
Ok(_) => println!("Completed"),
Err(pf8::Error::Cancelled) => println!("Cancelled by user"),
Err(e) => eprintln!("Error: {}", e),
}
Ok(())
}
Error Handling
The library provides comprehensive error types:
use pf8::{Error, Result};
fn handle_errors() -> Result<()> {
match pf8::extract("root.pfs", "output") {
Ok(()) => println!("Extraction successful"),
Err(Error::Io(e)) => eprintln!("I/O error: {}", e),
Err(Error::InvalidFormat(msg)) => eprintln!("Invalid format: {}", msg),
Err(Error::FileNotFound(name)) => eprintln!("File not found: {}", name),
Err(Error::Corrupted(msg)) => eprintln!("Archive corrupted: {}", msg),
Err(Error::Cancelled) => eprintln!("Operation was cancelled"),
Err(e) => eprintln!("Other error: {}", e),
}
Ok(())
}
PF8 Format Details
The PF8 format is a custom archive format with the following features:
- Magic Number: Files start with "pf8" (3 bytes)
- Index Structure: Contains file names, offsets, and sizes
- XOR Encryption: File contents are encrypted using XOR with SHA1-derived keys
- Path Format: Uses backslash separators internally
- Little-Endian: All multi-byte integers are stored in little-endian format
Performance Considerations
- Streaming operations for files read/write
- The
displayfeature adds dependencies - disable if not needed - Encryption/decryption is performed in-memory
License
This project is licensed under the MIT license - see the LICENSE file for details.
Dependencies
~0.8–4MB
~78K SLoC