Quick Start — C#
This walkthrough takes about 15 minutes and gets you from a fresh checkout to extracting files out of a forensic image with the C# example application FexViewer.
FexViewer is a console app at sdk/examples/csharp/. It uses direct P/Invoke
into FEX.Core.dll — there is no separate managed wrapper assembly, so the
patterns you see in NativeMethods.cs and FexApi.cs are exactly what
your own integration will look like.
- Prerequisites
- 1. Build FexViewer
- 2. Inspect an image —
info - 3. List the filesystem —
list - 4. Extract files —
extract - 5. Multi-segment and encrypted images
- Where to go next
Prerequisites
| Requirement | Notes |
|---|---|
| .NET 8 SDK | dotnet --version should print 8.x. Get it from https://dotnet.microsoft.com/download/dotnet/8.0. |
| Windows x64 | The 1.4.x SDK ships Win64 binaries. Linux and macOS are deferred to a follow-up patch. |
| FEX2 license key | Set the environment variable FEX_LICENSE_KEY=FEX2-…. Contact sales@getdata.com for an evaluation key. |
| A forensic image | The SDK ships sdk/sample-data/sample-image.dd — a 50 MB FAT32 image. Substitute your own E01 / Ex01 / L01 / DD / DMG / AFF4 if you have one. |
The runtime DLLs (FEX.Core.dll, libcrypto-1_1-x64.dll, bzip2.dll) live in
sdk/bin/Win64/. The FexViewer csproj copies them next to the executable on
every build, so you don't manage that yourself.
1. Build FexViewer
From a PowerShell prompt:
cd "sdk\examples\csharp"
dotnet build -c Release
Output lives at sdk\examples\csharp\bin\Release\net8.0\FexViewer.exe. The
build is configured with TreatWarningsAsErrors=true and ships clean —
any warnings on your environment indicate something to investigate before
trusting the output.
To verify the binary loads FEX.Core.dll and the license is recognised:
$env:FEX_LICENSE_KEY = "FEX2-...your-key..."
.\bin\Release\net8.0\FexViewer.exe info --image "..\..\sample-data\sample-image.dd"
If you see JSON metadata, you're set.
2. Inspect an image — info
FexViewer info calls GetImageInfoByFilenameAsJSON and prints the parsed
metadata: image type, device size, segment count, hashes, and any
format-specific blocks (e.g. EnCase case name / examiner).
.\FexViewer.exe info --image "C:\evidence\disk.E01"
Sample output:
Image type: E01
Filename: disk.E01
Device size: 250.06 GB
Segments: 12
MD5: 5d41402abc4b2a76b9719d911017c592
SHA1: aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
Case name: OPS-2026-001
Evidence number: 2
Examiner: L. Investigator
The two C# calls behind that output (from FexApi.GetImageMetadata):
int rc = NativeMethods.GetImageInfoByFilenameAsJSON(
absolutePath, out IntPtr buffer, out uint size);
// ... check rc, marshal UTF-8 JSON, deserialize into ImageMetadata ...
NativeMethods.FreeAllocatedBuffer(buffer); // pair every V2 call
The buffer-ownership rule applies to every V2 export: the DLL allocates,
your code frees with FreeAllocatedBuffer in a finally block. See
memory-management.md for the full pattern.
3. List the filesystem — list
FexViewer list enumerates every record using GetFileSystemRecords_V2 and
prints them as a table, JSON, or CSV.
# Default table format
.\FexViewer.exe list --image "C:\evidence\disk.E01" --limit 20
# JSON to a file (one object per record, no truncation)
.\FexViewer.exe list --image "C:\evidence\disk.E01" --format json --out files.json
# CSV
.\FexViewer.exe list --image "C:\evidence\disk.E01" --format csv --out files.csv
The C# pattern looks like this:
NativeMethods.ReadFileSystem(imageId, out _);
IntPtr records = IntPtr.Zero;
try
{
int rc = NativeMethods.GetFileSystemRecords_V2(
imageId, out uint count, out records, out _);
if (rc != NativeMethods.RESULT_OK)
throw new FexCoreException("GetFileSystemRecords_V2", rc);
int stride = Marshal.SizeOf<FileRecordNative>();
IntPtr cur = records;
for (int i = 0; i < count; i++)
{
var native = Marshal.PtrToStructure<FileRecordNative>(cur);
string name = Marshal.PtrToStringUTF8(native.filename) ?? "";
// ... use whatever fields you need ...
cur = IntPtr.Add(cur, stride);
}
}
finally
{
if (records != IntPtr.Zero)
NativeMethods.FreeAllocatedBuffer(records);
}
Two important details about this pattern:
- Resolve every UTF-8 string before the
FreeAllocatedBuffercall. Thefilenamepointer inside each record lives inside the same allocation; freeing the buffer invalidates them all. - Use
[StructLayout(LayoutKind.Sequential, Pack = 1)]on the C# struct that mirrorsTFileRecord— Pascal records are packed, and any alignment mismatch produces silently corrupted reads. The canonical layout isFileRecordNativeinsdk/examples/csharp/FexApi.cs.
The status flags on each record (deleted, folder, system, partition, free-space, …) are documented in types-and-constants.md.
When to switch to V3
GetFileSystemRecords_V2 materialises every record and every filename in a
single allocation. For datasets with multiple millions of records, that
allocation can hit memory limits and return RESULT_BUFFER_OVERFLOW.
At that point switch to V3 windowed extraction —
v3-api.md walks through the workflow with a complete C#
example.
4. Extract files — extract
FexViewer extract writes selected files from the image to a directory on
your host filesystem.
# Extract one folder (recursive)
.\FexViewer.exe extract `
--image "C:\evidence\disk.E01" `
--path "/Users/Alice/Documents" `
--out "C:\out\Alice-Documents"
# Extract everything; skip deleted records
.\FexViewer.exe extract `
--image "C:\evidence\disk.E01" `
--path "/" `
--out "C:\out\full" `
--skip-deleted
# Dry run — print what would be written without touching disk
.\FexViewer.exe extract `
--image "C:\evidence\disk.E01" `
--path "/Users" `
--out "C:\out\dryrun" `
--dry-run
The output directory is relativised to --path: with --path /Users/Alice
and --out C:\out, the children of Alice land directly in C:\out\….
Per-file logic mirrors what your own extractor will do:
- Open the image, read filesystem, get records (as above).
- For each record you want:
- Skip system / partition / free-space / device entries (status flags).
- Use
GetFileSizeor thelogicalSizefield from the record. - Read in chunks with
ReadFileData, truncating the final write tologicalSizeto strip cluster-slack bytes. - Restore timestamps from
createdDate/modifiedDate/accessedDate(FILETIMEint64s; convert withDateTime.FromFileTimeUtc). - Sanitise filenames for Windows (the FexViewer
extractcommand replaces\ / : * ? " < > |with_).
The chunked-read pattern in C#:
const int CHUNK = 1024 * 1024;
IntPtr scratch = Marshal.AllocHGlobal(CHUNK);
try
{
long offset = 0;
while (offset < logicalSize)
{
int want = (int)Math.Min(CHUNK, logicalSize - offset);
int rc = NativeMethods.ReadFileData(
imageId, fileIndex, offset, scratch, want, out int got);
if (rc != NativeMethods.RESULT_OK || got <= 0) break;
var slice = new byte[got];
Marshal.Copy(scratch, slice, 0, got);
outputStream.Write(slice, 0, got);
offset += got;
}
}
finally
{
Marshal.FreeHGlobal(scratch);
}
ReadFileData is a caller-allocated function (Pattern 3 in
memory-management.md) — Marshal.AllocHGlobal
pairs with Marshal.FreeHGlobal, and there's no FreeAllocatedBuffer
involved.
5. Multi-segment and encrypted images
FexViewer accepts a JSON manifest in place of --image <path>:
{
"manifest_version": 1,
"segments": [
"C:\\evidence\\case123\\disk.E01",
"C:\\evidence\\case123\\disk.E02",
"C:\\evidence\\case123\\disk.E03"
],
"passwords": [
"MyBitLockerPassword",
"C:\\evidence\\case123\\recovery.bek"
]
}
.\FexViewer.exe info --image "case123.json"
Behind the scenes:
- Multi-segment images — only the first segment is passed to
OpenImage. The DLL auto-detects companion segments by naming convention (E01,E02, …). The manifest'ssegmentslist is used to validate that companion files exist; missing files are warned about rather than silently ignored. - Encrypted volumes — each entry in
passwordsis tried in order. An entry that resolves to an existing file path is treated as a key file (e.g. a BitLocker.bek); everything else is treated as a literal password. This lets one manifest carry credentials from multiple vendors.
The DLL-level encoding for credentials is the optionsString parameter to
OpenImage:
{ "Password": "MyVolumePassword" }
The Password field is overloaded — the DLL recognises the BitLocker key
file format if the value resolves to an existing file. See
OpenImage in v2-api.md for the full
specification.
If you'd rather keep credentials out of the case manifest, FexViewer
accepts a separate --passwords <file> argument pointing at a JSON file
with the same shape (just the passwords array). That overrides anything
in the case manifest.
Where to go next
| You want to… | Read |
|---|---|
| Understand every V2 export in detail | api/v2-api.md |
| Page through a million-record image | api/v3-api.md |
| Look up a result code | api/error-codes.md |
See TFileRecord / TPropertySpec layouts |
api/types-and-constants.md |
| Get the buffer ownership rules right | api/memory-management.md |
| Find a function alphabetically | api/function-index.md |
| Read the FexViewer source | sdk/examples/csharp/ — NativeMethods.cs, FexApi.cs, Commands/, Models/ |