pf8

Crates.iopf8
lib.rspf8
version0.1.5
created_at2025-08-10 11:08:08.873812+00
updated_at2025-12-18 07:16:17.186475+00
descriptionA Rust library for encoding and decoding artemis PF8 archive files
homepage
repositoryhttps://github.com/sakarie9/pfs-rs
max_upload_size
id1788793
size118,224
Sakari (sakarie9)

documentation

README

PF8 - Rust Library for PF6/PF8 Archive Files

Crates.io Documentation License

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 display feature)

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 display feature 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.

Commit count: 0

cargo fmt