# Quick Start Guide - Python

Get from SDK download to working forensic analysis in 10 minutes.

---

## Prerequisites

- **Python 3.14+** (standard library only - no pip install needed)
- **FEX Core SDK** extracted to a local directory
- **Windows x64** or **Linux x64** (or Docker)

---

## Step 1: Set Up Your Project

Create a working directory and copy (or symlink) the Python SDK:

```
my-project/
├── core/              # Copy from sdk/python/core/
│   ├── __init__.py
│   ├── constants.py
│   ├── fexcore.py
│   └── loader.py
├── my_script.py       # Your code
```

On Windows, ensure `FEX.Core.dll` and its dependencies are in `bin/Win64/` relative to the SDK root (already included in the SDK package). The loader searches standard locations automatically.

On Linux, place `libfexcore.so` in `/usr/local/lib/` or set `LD_LIBRARY_PATH`.

---

## Step 2: Read Image Metadata

Create `my_script.py`:

```python
#!/usr/bin/env python3
"""Example: Read forensic image metadata."""

import json
import os
import sys
from core import FexCore
from core.loader import load_library

def main():
    if len(sys.argv) < 2:
        print("Usage: python my_script.py <image_path>")
        sys.exit(1)

    image_path = sys.argv[1]

    # Load the FEX Core library (auto-detects Windows DLL or Linux .so)
    lib, lib_path = load_library()
    fex = FexCore(lib)
    print(f"Loaded: {lib_path}")

    # FEX2 license key from GetData — set FEX_LICENSE_KEY in your environment
    # (or pull it from your secret store). See ../license-keys.md.
    key = os.environ["FEX_LICENSE_KEY"]

    # Known issue (Windows): InitLibrary uses GetCurrentDirectory rather than
    # GetModuleFileName when locating its license context, so it returns -20
    # (RESULT_INVALIDKEY) when the calling process's CWD is not the same
    # directory as FEX.Core.dll — even with a valid key. Workaround: chdir
    # to the DLL directory across the init_library call. The fix will move
    # to the DLL itself in a future release.
    prev_cwd = os.getcwd()
    os.chdir(os.path.dirname(lib_path))
    try:
        fex.init_library(key)
    finally:
        os.chdir(prev_cwd)
    print("Library initialized")

    # Open the forensic image
    image_id = fex.open_image(image_path)
    if image_id < 0:
        print(f"Error opening image: {image_id}")
        sys.exit(1)
    print(f"Image opened (ID: {image_id})")

    # Get image metadata as JSON
    info_str = fex.image_info_json(image_id)
    info = json.loads(info_str)

    print("\nImage Metadata:")
    print(f"  Description:  {info.get('description', 'N/A')}")
    print(f"  Image Type:   {info.get('imageType', 'N/A')}")
    print(f"  Image Kind:   {info.get('imageKind', 'N/A')}")
    print(f"  Device Size:  {info.get('deviceSize', 'N/A')}")

    # Clean up
    fex.close_image(image_id)
    print("\nDone.")

if __name__ == "__main__":
    main()
```

Run it:

```bash
python my_script.py path/to/evidence.E01
```

**Expected output:**

```
Loaded: C:\FEX-SDK\bin\Win64\FEX.Core.dll
Library initialized
Image opened (ID: 0)

Image Metadata:
  Description:  My Evidence
  Image Type:   E01
  Image Kind:   Physical
  Device Size:  536870912000

Done.
```

---

## Step 3: Create a CSV File Listing

This example reads the filesystem and writes a CSV with full paths, sizes, and timestamps.

```python
#!/usr/bin/env python3
"""Example: Export file listing to CSV."""

import csv
import json
import os
import sys
from datetime import datetime, timedelta
from core import FexCore
from core.loader import load_library
from core.constants import FILESTATUS_FOLDER, FILESTATUS_DELETED

# TFileRecord date fields are Windows FILETIME: 100-nanosecond intervals
# since 1601-01-01 UTC. Zero means "not set".
FILETIME_EPOCH = datetime(1601, 1, 1)

def filetime_to_datetime(raw_int64):
    """Convert Windows FILETIME int64 to Python datetime (UTC)."""
    if raw_int64 == 0:
        return None
    try:
        return FILETIME_EPOCH + timedelta(microseconds=raw_int64 / 10)
    except (ValueError, OverflowError):
        return None

def format_datetime(dt):
    """Format datetime as ISO string, or empty string if None."""
    if dt is None:
        return ""
    return dt.strftime("%Y-%m-%d %H:%M:%S")

def main():
    if len(sys.argv) < 3:
        print("Usage: python csv_listing.py <image_path> <output.csv>")
        sys.exit(1)

    image_path = sys.argv[1]
    csv_path = sys.argv[2]

    # Load and initialize (FEX2 key from environment — see ../license-keys.md).
    # Windows: chdir to DLL directory across init_library — see the first
    # example for the workaround rationale.
    lib, lib_path = load_library()
    fex = FexCore(lib)
    prev_cwd = os.getcwd()
    os.chdir(os.path.dirname(lib_path))
    try:
        fex.init_library(os.environ["FEX_LICENSE_KEY"])
    finally:
        os.chdir(prev_cwd)

    # Open image
    image_id = fex.open_image(image_path)
    if image_id < 0:
        print(f"Error opening image: {image_id}")
        sys.exit(1)

    # Read filesystem (returns (result_code, file_count); records are fetched separately below)
    result, file_count = fex.read_file_system(image_id)
    if result != 0:
        print(f"Error reading filesystem: {result}")
        sys.exit(1)
    print(f"Filesystem read: {file_count} entries")

    # Get file records (includes full paths via V2 API)
    result, records = fex.get_file_system_records(image_id)

    # Write CSV
    with open(csv_path, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow([
            "Index", "Path", "Filename", "LogicalSize", "PhysicalSize",
            "Created", "Modified", "Accessed", "IsFolder", "IsDeleted"
        ])

        for rec in records:
            is_folder = bool(rec["status"] & FILESTATUS_FOLDER)
            is_deleted = bool(rec["status"] & FILESTATUS_DELETED)

            writer.writerow([
                rec["_index"],
                rec.get("path", ""),
                rec["filename"],
                rec["logicalSize"],
                rec["physicalSize"],
                format_datetime(filetime_to_datetime(rec["createdDate"])),
                format_datetime(filetime_to_datetime(rec["modifiedDate"])),
                format_datetime(filetime_to_datetime(rec["accessedDate"])),
                is_folder,
                is_deleted,
            ])

    print(f"Written {len(records)} records to {csv_path}")

    fex.close_image(image_id)

if __name__ == "__main__":
    main()
```

Run it:

```bash
python csv_listing.py path/to/evidence.E01 file_listing.csv
```

**Output CSV:**

```csv
Index,Path,Filename,LogicalSize,PhysicalSize,Created,Modified,Accessed,IsFolder,IsDeleted
0,,Root,0,0,,,,True,False
1,Documents,Documents,0,0,2024-01-15 10:30:00,2024-01-15 10:30:00,,True,False
2,Documents\report.pdf,report.pdf,1572864,1576960,2024-01-15 11:00:00,2024-01-15 11:05:00,2024-01-15 11:05:00,False,False
...
```

---

## Step 4: Extract All Files

Use the included `fex_extract.py` tool for bulk extraction:

```bash
# Extract all files to a folder
python fex_extract.py path/to/evidence.E01 ./extracted_files

# Skip deleted files
python fex_extract.py path/to/evidence.E01 ./extracted_files --skip-deleted

# Dry run (show what would be extracted without writing)
python fex_extract.py path/to/evidence.E01 ./extracted_files --dry-run

# Verbose output
python fex_extract.py path/to/evidence.E01 ./extracted_files --verbose
```

**Expected output:**

```
FEX Extract - Bulk File Extraction
============================================================
Opening image: evidence.E01
Reading filesystem... 1,247 files found
Extracting to: ./extracted_files

  [  1/1247] Documents\report.pdf (1.5 MB)
  [  2/1247] Documents\notes.txt (2.0 KB)
  [  3/1247] Pictures\photo.jpg (100.0 KB)
  ...

============================================================
Extraction complete: 1,200 files extracted, 47 folders created
Total size: 2.5 GB
Errors: 0
```

Files are extracted preserving the original directory structure. The tool automatically:
- Preserves folder hierarchy from the forensic image
- Truncates files to logical size (strips cluster slack)
- Sanitizes Unicode filenames for cross-platform compatibility
- Handles path length limits

---

## Step 5: Write Your Own Extraction Logic

For custom extraction (e.g., filtering by file type or size):

> ⚠️ **Note on bulk extraction.** The example below uses `fex.read_file_data()`,
> which wraps the DLL's `ReadFileData` — fine for the small demo case. For
> extracting hundreds or thousands of files, switch to the handle-based stream
> API (`RequestFileStream` / `ReadFileStream` / `ReleaseFileStream`) to avoid
> unbounded memory growth. See the Python example in
> [Common Workflows — Workflow 3](../docs/tutorials/common-workflows.md#workflow-3-extract-file-content).

```python
#!/usr/bin/env python3
"""Example: Extract only PDF files larger than 1 KB."""

import os
import sys
from core import FexCore
from core.loader import load_library
from core.constants import FILESTATUS_FOLDER, RESULT_OK

READ_CHUNK = 1024 * 1024  # 1 MB chunks

def extract_file(fex, image_id, file_index, logical_size, output_path):
    """Extract a single file by reading in chunks."""
    os.makedirs(os.path.dirname(output_path), exist_ok=True)
    written = 0
    with open(output_path, "wb") as f:
        offset = 0
        while offset < logical_size:
            chunk_size = min(READ_CHUNK, logical_size - offset)
            result, bytes_read, buf = fex.read_file_data(
                image_id, file_index, offset, chunk_size
            )
            if result != RESULT_OK or bytes_read == 0:
                break
            f.write(bytes(buf[:bytes_read]))
            written += bytes_read
            offset += bytes_read
    # Truncate to exact logical size
    if written > logical_size:
        with open(output_path, "r+b") as f:
            f.truncate(logical_size)

def main():
    image_path = sys.argv[1]
    output_dir = sys.argv[2]

    # Windows: chdir to DLL directory across init_library — see the first
    # example for the workaround rationale.
    lib, lib_path = load_library()
    fex = FexCore(lib)
    prev_cwd = os.getcwd()
    os.chdir(os.path.dirname(lib_path))
    try:
        fex.init_library(os.environ["FEX_LICENSE_KEY"])
    finally:
        os.chdir(prev_cwd)

    image_id = fex.open_image(image_path)
    result, _ = fex.read_file_system(image_id)
    result, records = fex.get_file_system_records(image_id)

    extracted = 0
    for rec in records:
        # Skip folders
        if rec["status"] & FILESTATUS_FOLDER:
            continue
        # Filter: only PDFs larger than 1 KB
        filename = rec["filename"].lower()
        if not filename.endswith(".pdf") or rec["logicalSize"] < 1024:
            continue

        path = rec.get("path", rec["filename"])
        output_path = os.path.join(output_dir, path)
        print(f"Extracting: {path} ({rec['logicalSize']} bytes)")
        extract_file(fex, image_id, rec["_index"], rec["logicalSize"], output_path)
        extracted += 1

    fex.close_image(image_id)
    print(f"\nExtracted {extracted} PDF files")

if __name__ == "__main__":
    main()
```

---

## API Reference

The Python SDK wraps the FEX Core C API. Key methods on the `FexCore` class:

| Method | Description |
|--------|-------------|
| `init_library(key)` | Initialize with license key |
| `open_image(path)` | Open forensic image, returns `image_id` |
| `close_image(image_id)` | Close an opened image |
| `image_info_json(image_id)` | Get image metadata as JSON string |
| `read_file_system(image_id)` | Read filesystem, returns `(result, file_count)` |
| `get_file_system_records(image_id)` | Get all file records as list of dicts |
| `read_file_data(image_id, index, offset, size)` | Read file content bytes |
| `size_of_file(image_id, index)` | Get logical file size |
| `get_file_path(image_id, index)` | Get full path for a file |
| `call_v2_json(func, *args)` | Call any V2 function returning JSON |

For the full C API reference, see [V2 API Documentation](api/v2-api.md).

---

## Next Steps

- [Docker Guide](../docker/README.md) - Deploy on Linux with Docker
- [V2 API Reference](api/v2-api.md) - Complete API documentation
- [V3 API (Custom Fields)](api/v3-api.md) - Flexible property queries
- [Error Codes](api/error-codes.md) - All error codes with descriptions
- [Architecture Overview](architecture-overview.md) - API design and memory patterns
