# Common Forensic Workflows

This guide provides step-by-step tutorials for common forensic operations using FEX Core SDK.

## Prerequisites

Before starting these tutorials:

1. FEX Core SDK extracted to a local directory
2. Python 3.14+ installed (64-bit)
3. Valid license key (or trial key)
4. Sample forensic image (`sample-data/sample-image.dd`)

## Workflow 1: Open and Verify an Image

**Goal:** Open a forensic image and verify it loaded correctly.

### Use Case

Before processing files, you need to open the forensic image and confirm FEX Core can read it. This validates the image format is supported and the file is accessible.

### Python Implementation

```python
import ctypes
import json
import os

# Load the DLL
dll_path = os.path.join("bin", "Win64", "FEX.Core.dll")
dll = ctypes.CDLL(dll_path)

# Define function signatures
dll.ImageOpen.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
dll.ImageOpen.restype = ctypes.c_int

dll.GetImageInfoByIdAsJSON.argtypes = [
    ctypes.c_int,
    ctypes.POINTER(ctypes.c_void_p),
    ctypes.POINTER(ctypes.c_uint32)
]
dll.GetImageInfoByIdAsJSON.restype = ctypes.c_int

dll.FreeAllocatedBuffer.argtypes = [ctypes.c_void_p]
dll.FreeAllocatedBuffer.restype = None

dll.ImageClose.argtypes = [ctypes.c_int]
dll.ImageClose.restype = ctypes.c_int

# Open the image
image_path = b"sample-data/sample-image.dd"
license_key = b"YOUR_LICENSE_KEY"

image_id = dll.ImageOpen(image_path, license_key)
if image_id < 0:
    print(f"Failed to open image: error code {image_id}")
    exit(1)

print(f"Image opened successfully! ID: {image_id}")

# Get image info
buffer = ctypes.c_void_p()
size = ctypes.c_uint32()

result = dll.GetImageInfoByIdAsJSON(image_id, ctypes.byref(buffer), ctypes.byref(size))
if result == 0:
    try:
        json_data = ctypes.string_at(buffer, size.value).decode('utf-8')
        info = json.loads(json_data)
        print(f"Image Type: {info.get('ImageType', 'Unknown')}")
        print(f"Total Size: {info.get('TotalSize', 0):,} bytes")
    finally:
        dll.FreeAllocatedBuffer(buffer)

# Close image when done
dll.ImageClose(image_id)
print("Image closed.")
```

### Expected Output

```
Image opened successfully! ID: 1
Image Type: DD
Total Size: 52,428,800 bytes
Image closed.
```

### Error Handling

| Error Code | Meaning | Solution |
|------------|---------|----------|
| -4 | Image not supported | Check file format, use supported type |
| -6 | Could not open | Verify path, check permissions |
| -20 | Invalid license | Check license key |

---

## Workflow 2: List All Files

**Goal:** Enumerate all files and folders in a forensic image.

### Use Case

File listing is the foundation for most forensic operations - finding specific files, building timelines, or exporting file inventories.

### Python Implementation

```python
import ctypes
import json
import os

# Assume DLL is loaded and image is open (image_id from Workflow 1)

# Define GetFileSystemRecords_V2
dll.GetFileSystemRecords_V2.argtypes = [
    ctypes.c_int,  # image_id
    ctypes.c_int,  # partition_index (0 for first)
    ctypes.POINTER(ctypes.c_void_p),  # buffer
    ctypes.POINTER(ctypes.c_uint32)   # size
]
dll.GetFileSystemRecords_V2.restype = ctypes.c_int

def list_files(image_id, partition_index=0):
    """List all files in a partition."""
    buffer = ctypes.c_void_p()
    size = ctypes.c_uint32()

    result = dll.GetFileSystemRecords_V2(
        image_id,
        partition_index,
        ctypes.byref(buffer),
        ctypes.byref(size)
    )

    if result != 0:
        print(f"Error listing files: {result}")
        return []

    try:
        json_data = ctypes.string_at(buffer, size.value).decode('utf-8')
        records = json.loads(json_data)
        return records
    finally:
        dll.FreeAllocatedBuffer(buffer)

# Get and display files
files = list_files(image_id)
print(f"Found {len(files)} files/folders\n")

# Display first 10 files
print(f"{'Name':<30} {'Size':>12} {'Type':<10}")
print("-" * 55)

for record in files[:10]:
    name = record.get('Name', 'Unknown')
    size = record.get('Size', 0)
    is_folder = record.get('Status', 0) & 0x04  # FILESTATUS_FOLDER
    file_type = "Folder" if is_folder else "File"
    print(f"{name:<30} {size:>12,} {file_type:<10}")
```

### Expected Output

```
Found 47 files/folders

Name                                  Size Type
-------------------------------------------------------
.                                        0 Folder
..                                       0 Folder
Documents                                0 Folder
Pictures                                 0 Folder
report.txt                           1,234 File
invoice.pdf                         45,678 File
photo.jpg                          234,567 File
...
```

### Filtering Files

To filter results, process the returned list:

```python
# Find only deleted files
deleted_files = [f for f in files if f.get('Status', 0) & 0x02]

# Find files larger than 1MB
large_files = [f for f in files if f.get('Size', 0) > 1_000_000]

# Find files by extension
pdf_files = [f for f in files if f.get('Name', '').lower().endswith('.pdf')]

# Find files modified in date range
from datetime import datetime
def parse_date(timestamp):
    # FEX Core returns Windows FILETIME or Unix timestamp
    return datetime.fromtimestamp(timestamp) if timestamp else None

recent_files = [
    f for f in files
    if parse_date(f.get('Modified', 0)) and
       parse_date(f.get('Modified', 0)).year >= 2024
]
```

---

## Workflow 3: Extract File Content

**Goal:** Read the contents of a specific file from the forensic image.

### Use Case

Extract files for analysis, export evidence, or preview file contents during investigation.

### API choice

FEX Core exposes two ways to read file data:

| Function | When to use |
|---|---|
| `ReadFileData` | One-off reads of a few files. Simple, but each first-use of a `FileIndex` allocates an internal reader that is **not freed until `CloseImage()`** — do not use in a loop over many files. |
| `RequestFileStream` → `ReadFileStream` → `ReleaseFileStream` | **Bulk extraction or anything multi-threaded.** Backed by an LRU pool (256 slots per image); `ReleaseFileStream` returns the slot immediately. |

The extraction example below uses the handle-based stream API. See
[V2 API Reference](../api/v2-api.md) for full signatures.

### Python implementation (handle-based stream)

```python
import ctypes

# Wire up the handle-based stream API
dll.RequestFileStream.argtypes = [
    ctypes.c_int32,                     # image_id
    ctypes.c_int32,                     # file_index
    ctypes.POINTER(ctypes.c_int32),     # out: stream_handle
]
dll.RequestFileStream.restype = ctypes.c_int32

dll.ReadFileStream.argtypes = [
    ctypes.c_int32,                     # image_id
    ctypes.c_int32,                     # stream_handle
    ctypes.c_int64,                     # offset
    ctypes.POINTER(ctypes.c_ubyte),     # buffer (caller-allocated)
    ctypes.c_int32,                     # buffer_size
    ctypes.POINTER(ctypes.c_int32),     # out: bytes_read
]
dll.ReadFileStream.restype = ctypes.c_int32

dll.ReleaseFileStream.argtypes = [ctypes.c_int32, ctypes.c_int32]
dll.ReleaseFileStream.restype = ctypes.c_int32

dll.GetFileSize.argtypes = [ctypes.c_int32, ctypes.c_int32, ctypes.POINTER(ctypes.c_uint64)]
dll.GetFileSize.restype = ctypes.c_int32

CHUNK = 1024 * 1024  # 1 MB

def extract_file(image_id, file_index, output_path):
    """Extract a file from the forensic image to disk by its file index."""

    # Logical size — how many bytes of content to write. GetFileSize returns
    # logicalSize, not the allocated-on-disk physicalSize.
    logical_size = ctypes.c_uint64(0)
    rc = dll.GetFileSize(image_id, file_index, ctypes.byref(logical_size))
    if rc != 0 or logical_size.value == 0:
        return False

    handle = ctypes.c_int32(0)
    rc = dll.RequestFileStream(image_id, file_index, ctypes.byref(handle))
    if rc != 0:
        print(f"RequestFileStream failed: {rc}")
        return False

    try:
        buf = (ctypes.c_ubyte * CHUNK)()
        bytes_read = ctypes.c_int32(0)
        offset = 0
        remaining = logical_size.value

        with open(output_path, "wb") as out_file:
            while remaining > 0:
                to_read = min(CHUNK, remaining)
                rc = dll.ReadFileStream(
                    image_id, handle, offset, buf, to_read, ctypes.byref(bytes_read)
                )
                if rc != 0 or bytes_read.value <= 0:
                    break
                out_file.write(bytes(buf[: bytes_read.value]))
                offset += bytes_read.value
                remaining -= bytes_read.value
                print(f"\rExtracted {offset:,} bytes...", end="")

        print(f"\nFile extracted to: {output_path}")
        return True
    finally:
        # ALWAYS release — the pool slot is held until you do.
        dll.ReleaseFileStream(image_id, handle)

# file_index comes from GetFileSystemRecords_V2() (see Workflow 2).
extract_file(image_id, 42, "extracted_report.txt")
```

### Expected output

```
Extracted 1,234 bytes...
File extracted to: extracted_report.txt
```

### Hex-dump preview

For a quick preview without writing to disk — same handle pattern, single read:

```python
def hex_dump(image_id, file_index, num_bytes=256):
    handle = ctypes.c_int32(0)
    if dll.RequestFileStream(image_id, file_index, ctypes.byref(handle)) != 0:
        return
    try:
        buf = (ctypes.c_ubyte * num_bytes)()
        bytes_read = ctypes.c_int32(0)
        rc = dll.ReadFileStream(image_id, handle, 0, buf, num_bytes, ctypes.byref(bytes_read))
        if rc == 0 and bytes_read.value > 0:
            data = bytes(buf[: bytes_read.value])
            for i in range(0, len(data), 16):
                hex_part = " ".join(f"{b:02x}" for b in data[i:i+16])
                ascii_part = "".join(chr(b) if 32 <= b < 127 else "." for b in data[i:i+16])
                print(f"{i:08x}  {hex_part:<48}  {ascii_part}")
    finally:
        dll.ReleaseFileStream(image_id, handle)

hex_dump(image_id, 42)
```

---

## Workflow 4: Batch Processing Multiple Images

**Goal:** Process multiple forensic images efficiently.

### Use Case

Forensic labs often need to process dozens of images. This workflow shows how to handle multiple images with proper error handling and progress tracking.

### Python Implementation

```python
import ctypes
import os
from concurrent.futures import ThreadPoolExecutor, as_completed

def process_image(image_path, license_key):
    """Process a single image and return summary."""

    result = {
        'path': image_path,
        'status': 'success',
        'file_count': 0,
        'total_size': 0,
        'error': None
    }

    # Open image
    image_id = dll.ImageOpen(
        image_path.encode('utf-8'),
        license_key.encode('utf-8')
    )

    if image_id < 0:
        result['status'] = 'error'
        result['error'] = f"Failed to open: error code {image_id}"
        return result

    try:
        # Get file list
        files = list_files(image_id)  # From Workflow 2

        result['file_count'] = len(files)
        result['total_size'] = sum(f.get('Size', 0) for f in files)

    except Exception as e:
        result['status'] = 'error'
        result['error'] = str(e)

    finally:
        dll.ImageClose(image_id)

    return result

def batch_process(image_paths, license_key, max_workers=4):
    """Process multiple images in parallel."""

    results = []

    print(f"Processing {len(image_paths)} images...")
    print("-" * 60)

    # Sequential processing (safer for DLL)
    for i, path in enumerate(image_paths, 1):
        print(f"[{i}/{len(image_paths)}] Processing: {os.path.basename(path)}")

        result = process_image(path, license_key)
        results.append(result)

        if result['status'] == 'success':
            print(f"    Files: {result['file_count']:,}, "
                  f"Size: {result['total_size']:,} bytes")
        else:
            print(f"    ERROR: {result['error']}")

    # Summary
    print("-" * 60)
    successful = sum(1 for r in results if r['status'] == 'success')
    print(f"Completed: {successful}/{len(image_paths)} images processed successfully")

    return results

# Process multiple images
image_list = [
    "evidence/case001.E01",
    "evidence/case002.dd",
    "evidence/case003.L01"
]

results = batch_process(image_list, "YOUR_LICENSE_KEY")
```

### Expected Output

```
Processing 3 images...
------------------------------------------------------------
[1/3] Processing: case001.E01
    Files: 1,234, Size: 45,678,901 bytes
[2/3] Processing: case002.dd
    Files: 567, Size: 12,345,678 bytes
[3/3] Processing: case003.L01
    ERROR: Failed to open: error code -4
------------------------------------------------------------
Completed: 2/3 images processed successfully
```

### Error Handling Patterns

```python
def robust_process(image_path, license_key, retries=3):
    """Process with retry logic for transient failures."""

    for attempt in range(retries):
        try:
            result = process_image(image_path, license_key)
            if result['status'] == 'success':
                return result

            # Don't retry certain errors
            if result['error'] and 'not supported' in result['error']:
                return result  # Format issue, won't help to retry

        except Exception as e:
            if attempt < retries - 1:
                print(f"Attempt {attempt + 1} failed, retrying...")
                continue
            result = {
                'path': image_path,
                'status': 'error',
                'error': str(e)
            }

    return result
```

---

## Cross-Platform Notes

### Windows vs Linux Path Handling

```python
import os

# Use os.path for cross-platform compatibility
dll_name = "FEX.Core.dll" if os.name == 'nt' else "libfexcore.so"
dll_path = os.path.join("bin", "Win64" if os.name == 'nt' else "Linux64", dll_name)

# Normalize paths
image_path = os.path.normpath(image_path)
```

### DLL Loading Differences

```python
import ctypes
import os

if os.name == 'nt':
    # Windows: CDLL or WinDLL
    dll = ctypes.CDLL(dll_path)
else:
    # Linux: Use CDLL with full path
    dll = ctypes.CDLL(os.path.abspath(dll_path))
```

## Next Steps

- **[Best Practices](best-practices.md)** - Optimization and patterns
- **[V2 API Reference](../api/v2-api.md)** - Complete function documentation
- **[Troubleshooting](../troubleshooting.md)** - Common problems and solutions

---

**[Back to Documentation Index](../index.md)**
