-
Notifications
You must be signed in to change notification settings - Fork 7
Developer Guide
For developers who want to embed NKit or NKDS in their own projects, understand the codebase, or contribute.
The source code is available on GitHub. Contributions and bug reports are welcome.
Note: This documentation is a first draft and will be improved over time. If something is unclear or missing, please open an issue or ask on Discord.
| Library | Assembly | Namespace | Purpose |
|---|---|---|---|
NKit |
NKitLib |
Nanook.NKit |
Disc image processing engine — convert, extract, scan, verify, fix, dedupe |
NKitDataStore |
NKitDataStore |
NKitDataStore |
Block storage engine — content-addressed shards, index, compaction |
NKDS |
NKDSLib |
NKDS |
DataStore product — set management, VFS mount orchestration |
NKitCore |
(dependency) | NKitCore |
Generic async section pipeline (StreamBlockCore) |
NKitStream |
(dependency) | NKitStream |
BufferStream and streaming primitives |
Planned NuGet packages:
NKit·NKitDataStore·NKDS— not yet published. See the Roadmap for details.
NuGet packages are planned for a future release. See the Roadmap for details.
| Library | Frameworks |
|---|---|
| NKit, NKitDataStore, NKitCore, NKitStream |
net10.0, net8.0, netstandard2.1
|
| NKDS, NKDS.UI, NKit.UI | net10.0 |
| CLI apps (nkit, nkds) |
net10.0 (AOT-published) |
All libraries are designed for .NET Native AOT. This means:
- No reflection in any read/write/parse/process path
-
No runtime code generation (no
Emit, no expression trees in hot paths) -
No reflection-based serialisers (no
System.Text.JsonorYamlDotNetin format I/O paths) — all parsing is hand-written - Custom YAML reading/writing uses
AotYamlSerializerinNKit.UI/Helpers/Yaml/
If you extend NKit with a new format reader, step, or container decoder, it must follow these constraints.
All disc image formats and NKDS's own binary format use big-endian byte order on disk. Use the explicit endian helpers from NKit/Common/Utils.cs:
// Reading
uint value = buffer.ReadUInt32B(offset); // big-endian
uint value = buffer.ReadUInt32L(offset); // little-endian
// Writing
buffer.WriteUInt32B(offset, value);Never use BitConverter.ToUInt32(bytes, offset) directly — it is host-endian and will produce wrong results on any non-x64 host, and inconsistent results across platforms.
Source images are read as forward-only streams. Decoders and format readers must not seek backwards in the raw source. If you need to re-read a region, the BufferStream cache handles it transparently — but only for regions within the current cache window. Design format readers to be forward-sequential.
The simplest entry point is NKitProcessor. It accepts an AppSettings (loaded from a YAML config or built programmatically) and a source file path:
// Load settings from a config file
AppSettings settings = new AppSettings("path/to/nkit.yaml", overrides: null);
// Process a file
NKitProcessor processor = new NKitProcessor(settings, sourceFilePath, logSink);
NKitTaskResults results = processor.Process(cancellationToken);
// Check results
foreach (NKitTaskResult result in results.Results)
{
Console.WriteLine($"{result.InFile}: {result.VerifyResult}");
}AppSettings can also be constructed programmatically by passing a dictionary of override key-value pairs as the second argument, without a config file:
Dictionary<string, string> overrides = new()
{
["task"] = "convert",
["convert"] = "rvz:zstd:19:128k:16",
["out"] = "/output/path",
};
AppSettings settings = new AppSettings(configPath: null, overrides);// Open (or create) a set
using DataStore store = DataStore.Open("/data/nkit", setName: "wii");
// List images
IReadOnlyList<ImageRecord> images = store.ListImages();
// Export an image
OperationResult result = store.ExportImage(imageId, outputPath, format: "rvz:zstd:19");The DataStore class in NKitDataStore is the main entry point for storage operations. It is thread-safe for concurrent reads and enforces single-writer per set.
New container decoders implement IAsIso — the interface that presents any source as a raw ISO stream to the readers above:
public interface IAsIso : IDisposable
{
Stream Stream { get; } // the ISO-like stream
long Size { get; } // logical size
SystemType SystemType { get; }
// ...
}Implement IAsIso.Create(byte[] header) as a static factory that returns null if the header bytes don't match your format. Register the factory in ContainerFactory alongside the existing decoders.
The container decoder wraps the source stream and translates reads. It must be forward-reading (no seeks into source), endian-explicit, and AOT-safe.
Steps implement INKitStep and are instantiated per task:
public interface INKitStep
{
string Name { get; }
bool CanProcess(SystemType system, TaskType task);
void Process(NKitStepContext context, ISection section);
void Complete(NKitStepContext context);
}Steps receive sections in output order (serial, ordered) on the Output stage thread. They must not assume anything about threading within Process — multiple sections may be processing concurrently in the stages above, but by the time a section reaches INKitStep.Process it is always serial.
Both CLI apps use a shared IPrompter abstraction from NKit/Common/Interactive/IPrompter.cs. This lets interactive builders be unit-tested with a scripted prompter:
public class MyInteractiveFlow
{
private readonly IPrompter _p;
public MyInteractiveFlow(IPrompter prompter) => _p = prompter;
public string[] Build()
{
string choice = _p.Select("Pick a task", new[] { "convert", "scan", "verify" });
// ...
}
}
// In tests:
var prompter = new ScriptedPrompter(new[] { "convert", "yes" });
var flow = new MyInteractiveFlow(prompter);
string[] result = flow.Build();The Spectre.Console-backed implementation (Shared/Cli/SpectrePrompter.cs) is compiled into each CLI app via a linked source file, keeping the core libraries free of any Spectre dependency.
Tests use xUnit with FsCheck for property-based tests. The test projects are:
| Project | Scope |
|---|---|
NKit.Tests |
CLI parser, configuration, logging, settings |
NKitDataStore.Tests |
DataStore operations, compaction, crash recovery, VFS |
NKDS.Tests |
NKDS operations, dat verification |
NKitCore.Tests |
Pipeline section engine |
NKitStream.Tests |
BufferStream, file scanning |
Wiped image tests (NKit.Tests/WipedImageTests/) are integration tests that run the full pipeline against reference disc images. They require the test fixture files and are not run in CI by default — they take ~40 minutes.
Run a filtered subset:
dotnet test NKit.Tests --filter "FullyQualifiedName~CliParserTests" --no-restore
dotnet test NKit.Tests --filter "Trait=Area~Configuration" --no-restore- C# latest language version
- Nullable reference types enabled in newer projects
- No
#regionblocks - Prefer explicit types over
varin non-obvious cases - All binary I/O through explicit endian helpers (see above)
- No reflection in format/processing hot paths
- Architecture — Design principles and how the pieces fit together
- NKDS Storage Model — Block store design and crash safety
- NKit CLI — Full parameter reference (same parameters available programmatically)
- Roadmap — Planned features including NuGet library releases