Architecture Overview
A deep dive into Batin's internal architecture, design decisions, and why each component exists.
High-Level Architecture
Core Design Principles
1. Zero Unsafe Code
#![forbid(unsafe_code)]
Why? Security tools must be secure themselves. Memory corruption bugs in a file analyzer could be exploited by crafted malicious files.
How achieved:
- All operations use safe Rust abstractions
LazyLockfor thread-safe static initializationRwLockfor concurrent database access- No raw pointer manipulation
2. Defense in Depth
Batin never trusts a single detection method:
3. Bounded Resource Usage
Every operation is resource-limited:
| Resource | Limit | Configuration |
|---|---|---|
| Memory | max_read_bytes | Default 3KB |
| Time | timeout_ms | Default 5s |
| Archive entries | MAX_ARCHIVE_ENTRIES | 10,000 |
| Archive size | MAX_TOTAL_EXTRACTED_SIZE | 100MB |
| Compression ratio | SUSPICIOUS_COMPRESSION_RATIO | 100:1 |
4. Zero Panics Guarantee
Fuzz-tested to never panic on any input:
// All public APIs return Result<T, DetectionError>
pub fn from_bytes(data: &[u8], config: &DetectionConfig) -> Result<Self>
Module Organization
src/
├── lib.rs # Core types, main API
├── main.rs # CLI entry point
├── utils.rs # Byte utilities
│
├── detection/ # File detection
│ ├── mod.rs
│ ├── signatures.rs # Magic byte database
│ ├── entropy.rs # Shannon entropy
│ ├── polyglot.rs # Multi-format detection
│ └── embedded.rs # Embedded threats
│
├── analysis/ # Deep analysis
│ ├── mod.rs
│ ├── validation.rs # Structure validation
│ ├── forensics.rs # Fragment classification
│ └── binary.rs # PE/ELF parsing
│
├── io/ # I/O operations
│ ├── mod.rs
│ ├── batch.rs # Parallel processing
│ ├── archive.rs # Archive scanning
│ └── hasher.rs # File hashing
│
└── cli/ # CLI interface
├── mod.rs
├── scanner.rs # Scan command
├── watcher.rs # Watch command
└── console.rs # UI theming
Why This Structure?
- Separation of Concerns: Detection, analysis, I/O, and CLI are independent
- Feature Flags: Each dir maps to a cargo feature
- Testability: Each module can be tested in isolation
- Maintainability: Related code is co-located
Detection Pipeline
Stage 1: Magic Byte Matching
Why arrays instead of HashMap for frequencies?
// Fast: Fixed size, no hashing, cache-friendly
let mut frequency: [usize; 256] = [0; 256];
// Slow: Heap allocation, hashing overhead
let mut frequency: HashMap<u8, usize> = HashMap::new();
Stage 2: Entropy Analysis
Why single-pass calculation?
Previous implementation made two passes:
// OLD: Two iterations
let entropy = calculate_shannon_entropy(&data);
let chi = chi_square_test(&data);
Now optimized to single pass:
// NEW: One iteration builds frequency, calculates both
let stats = calculate_entropy_stats(&data);
Stage 3: Polyglot Detection
Why check multiple offsets?
Polyglot files hide secondary formats at various locations:
- Offset 0: Primary format header
- Offset 512+: Secondary headers (common for PDF+EXE)
Stage 4: Embedded Threat Scan
Thread Safety
Signature Database
pub static SIGNATURE_DB: LazyLock<RwLock<SignatureDatabase>> =
LazyLock::new(|| RwLock::new(SignatureDatabase::default()));
Why LazyLock<RwLock<...>>?
LazyLock: Initialize once, on first accessRwLock: Multiple readers, single writer- Safe: No manual synchronization needed
Batch Processing
// Rayon for CPU-bound work
files.par_iter().map(|f| process(f)).collect()
// Tokio for I/O-bound work
futures::future::join_all(tasks).await
Why both Rayon and Tokio?
| Workload | Best Tool | Reason |
|---|---|---|
| File I/O | Tokio | Non-blocking, high concurrency |
| Entropy calculation | Rayon | CPU-bound, work-stealing |
Error Handling Strategy
#[derive(Error, Debug)]
pub enum DetectionError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("File too large: {0} bytes (max: {1})")]
FileTooLarge(u64, u64),
#[error("Corrupted file structure: {0}")]
CorruptedStructure(String),
#[error("Detection timeout after {0}ms")]
Timeout(u64),
#[error("Unsupported file type")]
Unsupported,
}
Why custom error types?
- Granular handling: Caller can match specific errors
- Context preservation: Error messages include relevant data
- No panics: All failures are explicit
Result::Err
Performance Characteristics
Time Complexity
| Operation | Complexity | Notes |
|---|---|---|
| Magic byte match | O(n×m) | n=signatures, m=magic length |
| Entropy calculation | O(n) | Single pass over data |
| Polyglot detection | O(n×k) | k=check offsets (4) |
| Embedded scan | O(n) | Pattern search |
Space Complexity
| Component | Space | Notes |
|---|---|---|
| Signature DB | O(1) | Static, ~10KB |
| Entropy arrays | O(1) | Fixed 256 bytes |
| Detection result | O(k) | k=detected threats |
Extension Points
Adding New File Formats
- Add signature to
signatures.rs:
FileSignature {
magic: &[0x00, 0x00, 0x01, 0x00], // ICO magic
offset: 0,
additional_magic: None,
extensions: vec!["ico"],
mime_type: "image/x-icon",
category: FileCategory::Image,
}
- Add validation (optional) in
validation.rs:
pub fn validate_ico(data: &[u8]) -> ValidationResult { ... }
Adding New Threat Detectors
- Add detector function in
embedded.rs:
fn detect_flash_exploits(data: &[u8]) -> Vec<EmbeddedThreat> { ... }
- Call from
scan_embedded_contentbased on file category
Understanding the architecture helps you:
- Know where to add new features
- Maintain consistent design patterns
- Write efficient, safe code
- Understand why decisions were made
See Contributing Guide to start contributing!