Architecture
Solution layout
Five projects (solution CSharp_SimpleZipDrive.sln, all net10.0-windows, SDK pinned via global.json):
| Project | Kind | Role |
|---|---|---|
SimpleZipDrive | WPF exe | Dokan variant: UI + DokanNet mount service + Dokan IDokanOperations implementation (ZipFs.cs) |
SimpleZipDrive_WinFsp | WPF exe | WinFsp variant: same UI + FileSystemHost-based mount service + WinFsp IFileSystem implementation (ZipFs.cs) |
SimpleZipDrive.Core | class library | Shared engine: archive parsing, caches, streams, services, dialogs, logging, error reporting |
SimpleZipDrive.Tests | xUnit | ~919 [Fact] + 54 [Theory] methods; a WinFsp\ mirror of the service tests; Fakes\ for driver/report doubles |
FileBenchmark | console exe | Cold-I/O benchmark tool (standby-list purge, XXH3 hashing) |
The WinFsp app project contains a custom MSBuild target, KeepWinFspInteropOutOfBundle, that is essential for packaged builds — see Building & Packaging.
Component overview
flowchart TB
subgraph Apps
UI[MainWindow / dialogs] --> MS[MountService]
end
subgraph Core
MS --> ZFS[ZipFileSystemCore]
ZFS --> CD[Central directory parse<br/>EntryNode tree]
ZFS --> SES[StoredEntryStream<br/>zero-copy + read-ahead]
ZFS --> MEM[SharedMemoryStream<br/>+ MemoryEntryCacheEntry]
ZFS --> DISK[Disk cache<br/>secure temp files]
ZFS --> SZ[SevenZipFallback<br/>SharpSevenZip]
ZFS --> SC[SharpCompress]
UI --> LOG[LoggingService / DiagnosticLogger / AppLogger]
LOG --> BUG[BugReportSink → bug API]
UI --> SET[SettingsService → settings.dat]
UI --> UPD[UpdateService → GitHub API]
end
MS --> DRV{Driver}
DRV -- DokanNet --> D[Dokan driver]
DRV -- FileSystemHost --> W[WinFsp driver]
Mount flow (both variants)
MainWindow.ProcessCommandLineArgsAsyncclassifies: 1 arg = drag-and-drop, ≥2 args = archive + mount point.MountService.MountAsyncguards (one mount per instance, file exists, extension supported).- Mount-point resolution (Mounting) →
MountWithAutoDriveLetterAsync/MountWithSpecifiedPointAsync/MountWithCrossIntegrityFolderAsync. - Pre-mount checks (driver presence/version/service, mount-point availability, elevation).
OpenArchiveFileStreamAsyncopens the archive withFileShare.ReadWrite(3 attempts, awaited backoff).ZipFileSystemCoreopens via SharpCompress, parses the central directory, builds theEntryNodetree (including implicit directories for every ancestor path), detects encryption, prompts/verifies the password if needed.- The driver object (
ZipFs— DokanIDokanOperationsor WinFspIFileSystem) is constructed and mounted in-process; the lifecycle task parks until unmount. - Unmount: cancel → driver unmount → 500 ms grace → dispose engine (caches, temp dir).
The archive engine (ZipFileSystemCore)
- Entry model:
EntryNodewithNormalizedPath(/dir/file.txt, forward slashes, Unicode Form C),CanonicalPath,IsDir, optionalIArchiveEntry, sizes and timestamps. Lookup dictionaries are ordinal-ignore-case;./..are resolved; corrupt directories abort the mount with a corruption error. - Stream selection (
OpenEntryStream), in order:- Failed entry →
null(read error). - Stored fast path — ZIP-only, non-encrypted, non-solid,
CompressionType.NoneorCompressedSize == Size→StoredEntryStreamwindow over the raw archive withRandomAccesspositional reads and 4 MB read-ahead. - Large entry (
size ≥ MaxMemoryPerFileMb) → disk cache. - Small entry → shared memory cache (
AcquireSharedMemoryStream+DecompressEntryToBuffer). - Decompression failure → SevenZip fallback → failed entry.
- Failed entry →
- Memory cache:
MemoryEntryCacheEntry { byte[] Buffer, RefCount, LastUsed }; per-entrySemaphoreSlimserializes first decompression;EvictColdMemoryEntriesevicts refcount-0 buffers LRU; total budget = 90 % ofTotalAvailableMemoryBytes; over-budget/OOM → disk cache fallback.SharedMemoryStream.Disposedecrements the refcount; refcount-0 buffers stay warm. - Disk cache:
TempDirectoryPath = %LOCALAPPDATA%\SimpleZipDrive\Temp\<pid>_<guid>; secure temp files (current-user-only ACL); free-space check; reuse registry per session; recursive delete on dispose; orphan sweep (CleanupOrphanedTempDirectories) with PID + process-name guard. - Reads: positional (
RandomAccess) where possible, strictly sequential fallback for non-seekable sources;ReadOnDemand(WinFsp) services paging I/O without handle context usingArrayPool<byte>.Shared.
Driver interop details
- Dokan:
DokanInstanceBuilder+DokanOptions.RemovableDrive; version probe viaDokanVersion()P/Invoke; driver output piped throughDokanPrefixedLogger([DokanNet]prefix); 2-retry loop onDokanExceptionexcept “Can’t install”. - WinFsp:
winfsp.net 2.1.25156pinned deliberately — newer 2.2.x interops reject the stable 2.1 native driver (“incorrect dll version (need 2.2, have 2.1)”);RequiredWinFspVersion = 2.1; native DLL preloaded;WinFsp.Launcherservice verified;host.Mount(mountPoint, securityDescriptor, false, DebugLog=-1)with a per-attempt native debug log; NTSTATUS→message mapping (Mounting). - Packaging constraint: winfsp-msil’s static initializer calls
FileVersionInfo.GetVersionInfo(Assembly.Location), which is empty inside single-file bundles — hencewinfsp-msil.dllmust ship beside the exe (Building & Packaging).
Services and cross-cutting concerns
- ServiceProvider: static registry populated in
App.OnStartup(Logging → Settings → Mount → Notifications → Screenshots → Update → Stats); disposed in reverse at exit. - Settings:
AppSettingsJSON at%LOCALAPPDATA%\SimpleZipDrive\settings.dat; corrupt file → reported + reset. - Logging: single Serilog pipeline (
AppLogger): verbose → session file; Information+ → debugger; Warning+ →BugReportSink→ bug API (filtered byErrorLogger.IsUserError); UI pane viaLoggingService(5000-entry cap, 100 ms dedupe);DiagnosticLoggerfacade with sections/headers. - Global exception handling: WPF dispatcher / AppDomain / unobserved tasks →
ErrorLoggerStatic; fatal reports posted synchronously (30 s) before exit; pending reports drained at shutdown (5 s). - Update check:
releases/latestGitHub API,tag_nameregex compare, silent on failure. - Stats: startup POST
{ applicationId, version }; HTTP 429 ignored.
Threading and shutdown
- Driver callbacks arrive on driver thread pools; the engine guards shared state with a global archive lock, a memory-cache lock (
System.Threading.Lock), and per-entry semaphores; disposal isInterlocked-guarded and idempotent. - The mount lifecycle parks on an infinite cancellable delay; unmount cancels it.
- Window-close shutdown races unmount against 5 s, then a 3 s watchdog calls
TerminateProcess(GetCurrentProcess(), 0)(exit code 0) if teardown hangs;App.OnExitflushes logs and drains pending bug reports.
Conventions
- Modern C# on .NET 10: file-scoped namespaces, collection expressions,
requiredmembers,System.Threading.Lock,System.IO.RandomAccess. - Clean-up enforced by Meziantou.Analyzer + Roslynator (all warnings resolved; dev-only packages).
InternalsVisibleTogrants test access;References\holds third-party reference sources excluded from compilation.