Reference
Rust port status
RustyGet replaces NZBGet's C++ with Rust one module at a time, while the program stays in daily use. Each step keeps the output identical, so the rewrite never trades correctness for progress.
How the port works
The Rust code lives in rust/ as a library crate that builds into the same binary. C++ calls it through a C interface declared in rust/include/nzbget_rs.h. Over time, more of the program moves behind that interface, until the C++ is only glue and then gone.
- Pick a module. Hot paths first, measured with real downloads. Whole modules are preferred over single functions.
- Port it byte for byte. The Rust code reproduces the C++ output exactly, quirks included.
- Prove it. A differential test links the old C++ and the new Rust side by side and feeds both the same inputs. Every run includes random and mutated inputs. The tests run under AddressSanitizer and UndefinedBehaviorSanitizer.
- Review it. Each port is a pull request with an automated review, fixed until it's clean.
- Ship it. The port is squash-merged as one signed commit and goes into daily use.
When a port finds a bug in the C++ code, the port keeps the bug, and the fix follows as a separate commit. Bugs found this way include an error message that glibc left empty, and a RAR header parser that kept going on a block that doesn't advance.
The C++ version of each ported routine stays behind NZBGET_USE_RUST, so builds without Rust still work during migration. Removing those fallbacks is a separate final phase below. New features, such as twin detection, exist only in Rust.
Zero C++ checklist
This is the remaining-work inventory, checked against source revision 29fdbe88. The app source list, headers, Rust call boundaries, dependency definitions, and platform packaging define its scope. Keep this inventory current when adding code or dependencies. The per-file migration inventory, caller boundary map, and dependency retirement audit retain detailed evidence and existing holds; this checklist doesn't change their qualification or ownership gates.
The module table below lists Rust code that exists. A listed module can replace only selected routines: C++ can still own its objects, locks, transport, callbacks, and callers. A module's presence or a benchmark result doesn't complete a checklist item.
Track three milestones separately:
- App: no project-owned C++ in the daemon, clients, or retained companion programs.
- Linked dependencies: no C++ in the executable or libraries it loads, including transitive dependencies, static archives, and optional libraries.
- Distribution: no bundled C++ helper executables or required C++ runtime components in supported release artifacts and container images.
All items start open. An open item can contain Rust code already; it means the whole remaining boundary hasn't been retired. Update the checkboxes in this page's source, with the reviewed commit or pull request and qualification evidence beside each completed item. The rendered checkboxes are read-only. Complete an item only after its C++ paths and fallbacks are removed from the retained scope. A proposed port, open pull request, feature that is turned off, or unused Rust replacement doesn't qualify. Explicitly record any approved feature or platform retirement.
Startup, configuration, and runtime ownership
- Replace the entry point and app startup, reload, shutdown, signal handling, and global object ownership (
main/nzbget.cpp). - Finish options loading, validation, defaults, categories, server settings, configuration writes, and reload behavior (
main/Options.*); the Rust value parsers cover only part of this area. - Replace remaining command-line execution and remote-client dispatch around the Rust argument parser (
main/CommandLineParser.*). - Replace runtime work state, disk-space monitoring, maintenance and update operations, and stack traces (
main/WorkState.*,DiskService.*,Maintenance.*,StackTrace.*). - Replace scheduler ownership, task execution, cancellation, and shutdown (
main/Scheduler.*); Rust timing decisions alone don't complete the scheduler.
Connections and article downloads
- Replace socket ownership, DNS, connection lifecycle, TLS integration, HTTP transport, and network-speed tests (
connect/Connection.*,TlsSocket.*,HttpClient.*,NetworkSpeedTest.*). - Finish HTTP downloads, redirects, body handling, retries, cancellation, and file output (
connect/WebDownloader.*); header and status decisions are already partly in Rust. - Replace NNTP socket I/O, connection state, server selection, connection pools, limits, and retry/failover ownership (
nntp/NntpConnection.*,NewsServer.*,ServerPool.*). - Replace article download/fetch workers and their queue interactions, cancellation, and lifetimes (
nntp/ArticleDownloader.*,ArticleFetcher.*). - Replace article writing, buffering, allocation, partial-file handling, assembly, and durable completion (
nntp/ArticleWriter.*). - Remove C++ decoder wrappers and callbacks; qualify the retained yEnc/CRC backend separately (
nntp/Decoder.*). - Finish statistics accumulation, persistence, synchronization, and speed/volume accounting (
nntp/StatMeter.*); Rust slot arithmetic is only part of it.
NZB intake, queue, history, and persistence
- Replace NZB XML parsing and its callbacks, object construction, filenames, and error handling (
queue/NzbFile.*). The parked Rust XML parser isn't an adopted replacement. - Replace incoming-directory scanning, archive intake, NZB backup, and URL-to-queue admission (
queue/Scanner.*,ArchiveProcessor.*,UrlCoordinator.*). - Transfer download/file/article models, IDs, ownership, inline header logic, and shared queue locks into Rust (
queue/DownloadInfo.*). - Replace queue coordination, scheduling, pause/resume, completion transitions, and editing (
queue/QueueCoordinator.*,QueueEditor.*). - Replace queue/history state serialization, loading, migration compatibility, crash recovery, and ID persistence (
queue/DiskState.*). - Replace history coordination, retention, redownload, and return-to-queue operations (
queue/HistoryCoordinator.*). - Finish direct-renaming orchestration, file access, queue updates, and deobfuscation ownership (
queue/DirectRenamer.*,Deobfuscation.*).
Duplicate discovery, fleets, and recovery
- Replace duplicate coordination, ranking/admission callers, backup promotion, and twin-check integration (
queue/DupeCoordinator.*,TwinCheck.*). - Replace article fallback, probing, background rechecks, worker cancellation, and retained recovery state (
queue/DupeArticleFallback.*,DupeProbe.*). - Finish stream-repair queue integration, range/part ownership, and PAR damage tracking (
queue/DupeStreamRepair.*,ParDamage.*). - Replace duplicate-search and fleet controllers, locks, deadlines, caches, persisted metadata, and edits (
dupesearch/DupeSearch.*,Fleet.*,DupeIdentity.*). - Replace Newznab queries, HTTP/NZB fetching, XML/NZB readers, donor-health checks, and NNTP probing (
dupesearch/Newznab.*,HttpGet.*,NzbFetcher.*,XmlReader.*,NzbReader.*,DonorHealth.*,NntpHealthServer.*). - Finish dead-posting storage/locking, score callers, posting file/cache access, and release-name wrappers (
dupesearch/DeadPostings.*,DonorScore.*,Posting.*,ReleaseName.*). Existing Rust policy and parsing routines don't replace these controllers.
Repair, unpacking, and post-processing
- Replace post-processing orchestration, status transitions, concurrency, pause/cancel, and shutdown (
postprocess/PrePostProcessor.*). - Replace PAR checking and repair orchestration, packet loading, callbacks, file verification, and repairer ownership (
postprocess/ParChecker.*,Repair.*,ParParser.*). Track the PAR library replacement separately below. - Finish PAR/RAR parsing and renaming callers, metadata, regular expression use, directory walks, and actual file moves (
postprocess/ParRenamer.*,RarReader.*,RarRenamer.*). - Replace unpacking and direct-unpack controllers, subprocess supervision, password handling, output parsing, and cancellation (
postprocess/UnpackController.*,DirectUnpack.*). - Replace post-download/post-unpack renaming, cleanup, moving, and collection-analysis integration (
postprocess/PostDownloadRenamer.*,PostUnpackRenamer.*,Rename.*,Cleanup.*,CollectionAnalyzer.*). - Replace content maps, duplicate archive matching, stream crypto, and stream-repair orchestration (
postprocess/ContentMap.*,DupeMatcher.*,StreamCrypto.*,StreamRepair.*), including compressed/encrypted donor paths.
Web server, APIs, and clients
- Replace listener/socket ownership, HTTP request/body handling, static asset serving, authentication integration, compression, and TLS control (
remote/WebServer.*,RemoteServer.*). - Replace all JSON/XML-RPC command implementations, response collection, serialization, and queue/configuration mutations (
remote/XmlRpc.*). Rust routing and parameter parsing don't complete the API. - Finish binary RPC commands, transport, response snapshots, locks, and remote-client operations (
remote/BinRpc.*,RemoteClient.*,MessageBase.h). - Replace terminal client/frontends, rendering, input, and ncurses integration (
frontend/Frontend.*,ColoredFrontend.*,LoggableFrontend.*,NCursesFrontend.*). - Verify the existing web UI, automation clients, Kodi streaming, NZBHydra2, and other supported integrations work against the Rust-owned APIs. JavaScript/HTML/CSS assets don't need a Rust rewrite to meet the C++ target.
Feeds, extensions, and system health
- Replace feed fetching, XML parsing, feed/history models, polling, and filter callers (
feed/FeedCoordinator.*,FeedFile.*,FeedInfo.*,FeedFilter.*). - Replace extension discovery, manifest/header loading integration, installation/removal, configuration, and lifecycle (
extension/Extension.*,ExtensionLoader.*,ExtensionManager.*,ManifestFile.*,ScriptConfig.*). - Replace command, feed, NZB, post, queue, scan, and scheduler script controllers, environment contracts, exit statuses, and process cleanup (
extension/*Script.*). Extension scripts can remain in their supported languages. - Replace system-health service/status and every validator in
systemhealth/, including configuration, paths, servers/connections, security, feeds, scheduling, queue, repair, unpacking, logging, display, and extensions.
Shared utilities and platform interfaces
- Finish filesystem/path operations, deletes, links, permissions, errors, backup renames, metadata, and file-type callers (
util/FileSystem.*,FileTypes.*). - Replace remaining string, encoding, date, formatting, regular expression, JSON/XML, and checksum callers and header implementations (
util/Util.*,NString.*,Utf8.*,Json.*,Xml.*,Crc32.h,Container.h). - Replace threads, locks, observers, service control, script/process control, logging, analytics, and benchmark support (
util/Thread.*,Observer.*,Service.*,ScriptController.*,Log.*,DataAnalytics.*,Benchmark.*). - Replace app-owned crypto, hashing, archive-command, and unpack wrappers (
util/OpenSSL.*,Unrar.*,SevenZip.*,Unpack.*); a Rust wrapper around C++ doesn't complete dependency removal. - Replace OS, CPU, network, and system-information abstractions (
system/OS.*,CPU.*,Network.*,SystemInfo.*). - Audit all remaining headers, templates, inline functions, conditional branches, and generated sources; the
.cpplist alone isn't a complete inventory.
Companion programs and supported platforms
- Replace the bundled test/news server, its frontend, NZB generator, and yEnc encoder (
nserv/NntpServer.*,NServMain.*,NServFrontend.*,NzbGenerator.*,YEncoder.*). - Replace Windows console, service, encoding, and resource/build integration if Windows remains supported (
daemon/windows/,util/Utf8.*,platforms/windows/,cmake/windows.cmake). Windows currently retains the C++ implementation. - Verify the macOS app launcher, daemon controller, RPC bridge, packaging, signing, and universal builds (
platforms/macos/). Its Objective-C sources aren't C++, but still require a compatibility and native-dependency audit. - Qualify every retained OS, architecture, and build combination. Include Linux, macOS, Windows, FreeBSD, Android, static builds, and cross builds. Record explicit support retirements before removing a platform's fallback.
- Update Docker images, Linux packages, installers, Flatpak, Snap, QNAP packaging, and platform build scripts to use the Rust executable and approved helper set.
Linked and native dependencies
Inventory POSIX dependencies, Windows dependencies, cmake/*.cmake, lib/sources.cmake, rust/Cargo.toml, and rust/Cargo.lock. Record versions, transitive dependencies, feature flags, static/dynamic linkage, and shipped artifacts for each retained target. Rust crate names and C interfaces don't establish implementation language.
- Replace PAR2-Turbo and its
par2-turbo,gf16, andhasherbuild targets; qualify full verification/repair and renaming. The existingrust-par2dependency doesn't prove the C++ repair engine is gone. - Remove Boost headers and compiled components from app builds, including JSON and any remaining algorithm/container use; track
Boost.Testseparately below. - Audit rapidyenc and its CRC/SIMD backend, callbacks, build languages, and transitive native code; remove any C++ requirement or replace the backend with a qualified alternative.
- Decide and record the retained OpenSSL TLS/crypto, libxml2 XML, zlib compression, ncurses terminal, and bundled POSIX regular expression dependencies. These C libraries don't by themselves block zero C++, but are separate work if the target becomes Rust-only.
- Audit Rust dependencies, their feature-selected transitive dependencies,
build.rsscripts, native bindings, generated code, and downloaded/prebuilt archives for hidden C++. - Audit system/runtime linkage per target, including the C++ standard library, compiler/unwind runtimes, platform frameworks, and optional OpenMP or other dependency runtimes. Remove C++ runtime requirements that remain after app ports.
- Verify optional feature combinations, system-library builds, and pinned source builds independently; removing one default dependency path doesn't retire another.
External tools and distribution contents
- Replace required or bundled
unrarexecutables, includingunrarandunrar7, with qualified alternatives covering listing, extraction, encryption, passwords, multipart archives, and failure/cancellation behavior. - Replace required or bundled 7-Zip executables, including
7za,7z, and7zzwhere used, with qualified alternatives covering every retained archive format and extension-installation path. - Audit all installers, packages, app bundles, container layers, and runtime images for unpackers, native libraries, C++ runtimes, and other C++ executables pulled in through base images or package dependencies.
- Define the external extension/user-tool boundary. A user-supplied C++ script helper or unpacker prevents an unrestricted whole-environment claim; document any exclusions and provide a C++-free supported path.
- Keep licenses, notices, settings/defaults, helper discovery, upgrade behavior, and release dependency manifests consistent with each replacement.
Qualification and development tooling
- For each replacement, retain byte-for-byte differential coverage, fuzz/mutated inputs, ownership/lifetime checks, and applicable sanitizer runs while the C++ reference is available.
- Meet the current replacement performance gates with matched complete callers, real I/O/FFI/allocation costs, reproducible inputs, and retained slower/inconclusive results. A module benchmark alone isn't acceptance.
- Verify queue/state upgrade and restart compatibility, IDs, partial downloads, failure recovery, signals, concurrency, and process cleanup across supported platforms.
- Replace app C++ unit/integration tests and
Boost.Testwith coverage of the Rust-owned behavior; retain end-to-end API, download, repair, unpack, and duplicate-recovery scenarios. - Decide the historical reference-tooling boundary. C++ differential/benchmark harnesses can remain in an archived migration reference, but must leave the required build/test/release path before claiming a C++-free development toolchain.
- Audit CI actions, build containers, package builders, signing tools, and developer prerequisites separately from shipped runtime dependencies. If a claim includes those tools, identify and remove their C++ requirements too.
Final C++ removal and acceptance
- Make Rust own the executable entry point, state, object lifetimes, synchronization, I/O, and error propagation; prove there are no remaining calls into app C++.
- Replace C++ callbacks and ownership bridges with Rust calls, then remove obsolete exports and
rust/include/nzbget_rs.hinterfaces. Retained C-library interfaces can remain for a zero-C++ target. - Retire builds without Rust and every
NZBGET_USE_RUSTC++ fallback, following the platform/support decisions in this checklist; remove obsolete configuration switches. - Delete retired app C++ sources/headers, generated C++ bindings, targets, precompiled headers, and compiler/link settings. Update CMake or its replacement and all release/CI entry points.
- Build, test, and package every retained target in a clean environment without a C++ compiler or C++ development headers. Audit transitive and prebuilt inputs as well; this test alone can't detect precompiled C++.
- Inspect compile commands, link maps, static archive members, dynamic/import dependencies, symbols, and packaged files for C++ code/runtime linkage. Static linkage or stripped symbols alone aren't proof of absence.
- Publish per-target dependency and artifact evidence with immutable source, lockfile, toolchain, binary, and image identities; independently review the evidence for each of the three milestones.
- Verify an authorized rollout of the exact qualified artifacts, including fresh downloads, restart/resume, duplicate recovery, repair/unpack, and supported integrations. Record deployment evidence separately from source/build completion.
- Mark zero app C++, zero linked C++ dependencies, and zero distributed C++ helpers/runtime components complete separately, and state the supported targets and exclusions. Claim zero C++ only for the scope all three evidence sets actually cover.
Modules in Rust
This table is generated from the crate's source on every build. Each module's first doc comment says what it does. The ffi module is the C interface that the C++ code calls.
| Module | What it does | Lines |
|---|---|---|
allocation_probe |
Test-only thread-local allocation observation, matching the existing renameanalyzer allocation fixture. | 24 |
binrpc |
The binary remote protocol's frame (BinRpc.cpp): the checks BinRpcProcessor makes on a request's base - credentials compared as the C++ strcmp compared them, the request type, its size - the request… | 1,350 |
bytes |
Byte searches for the ports: the C library's memmem and memchr (SIMD in glibc, the speed of the strstr/strchr the C++ used) where available, a plain scan elsewhere. | 64 |
cmdline |
CommandLineParser (CommandLineParser.cpp): the command line as the C++ read it - getopt_long (or getopt) over the same argv array, which it permutes, with the switches that read their own extra… | 820 |
collection |
CollectionAnalyzer: what a download's files are (main video or book, sample, subtitles, NFOs, others; an ambiguous collection or a disc structure) and how to rename them to the release name. | 342 |
crc |
CRC-32 combination (Crc32::Combine): the CRC of A followed by B from the CRCs of A and B and the length of B. | 93 |
deadpostings |
Pure persisted dead-posting line parsing; C++ owns storage and locking. | 122 |
decode |
Decoders for RPC requests (WebUtil::DecodeBase64, ...). | 289 |
decoder |
The article decoder (Decoder): finds yEnc or UU-encoded data in an article as it arrives in pieces, decodes it into the caller's buffer and checks it (sizes, CRC). | 867 |
deobfuscation |
Deobfuscation: the file name in a usenet subject (Deobfuscate) and whether a file name looks like an obfuscated hash (IsExcessivelyObfuscated). | 421 |
directrenamer |
DirectRenamer's new names (DirectRenamer.cpp): the name a downloaded file gets from the par2 file list (by the MD5 of its first 16 KiB), the name a par2 file gets from its set id, and whether a… | 140 |
donorscore |
DonorScore: the DupeScore offsets of duplicates (donors and backups) under a pick, as daemon/dupesearch/DonorScore.cpp gave them. | 93 |
dupeidentity |
Identity and conservative admission policy for a bounded, decoded first article. | 464 |
dupepurge |
Owned preflight planning only. | 1,121 |
dupesearch_nzb |
Terminal unquoted filename parsing and PAR classification for duplicate search. | 182 |
dupewatch |
Pure queue/history discovery policy. | 635 |
escape |
JSON and XML string escaping for RPC responses (WebUtil::JsonEncode, WebUtil::XmlEncode). | 160 |
extload |
ExtensionLoader::V1 (ExtensionLoader.cpp): the header of a pre-manifest extension script - its kind, about text, queue events, task time, description, requirements, and the options and commands of… | 418 |
feedfilter |
FeedFilter: the RSS feed filter language (rules separated by '%', each an optional command such as "A(options):" or "R:", then terms joined by spaces, braces and '|'). | 743 |
ffi |
C ABI. | 8,378 |
filetypes |
FileTypes: file and directory names and contents classified (archives, parity, video, audio, subtitles, books, images, disc structures, samples, clutter) and a file's type sniffed from its first… | 323 |
fsdelete |
FileSystem's deletes and backup renames (FileSystem.cpp, POSIX): DeleteDirectory (a folder that is empty or holds only hidden files, its empty subfolders removed), DeleteDirectoryWithContent… | 391 |
fsops |
FileSystem's file operations (FileSystem.cpp, POSIX): FileExists, DirectoryExists, ForceDirectories, MakeUniqueFilename and ExpandHomePath, and the error text of GetLastErrorMessage. | 202 |
json |
boost::json::parse with the default parse_options (Json::Deserialize in daemon/util/Json.cpp): strict RFC 8259 (no comments, trailing commas, NaN/Infinity or invalid UTF-8/UTF-16), at most 32 nested… | 598 |
manifest |
ManifestFile::Load (daemon/extension/ManifestFile.cpp): an extension's manifest.json read into the manifest, as the C++ did it - the same required keys in the same order, strings cut at their first… | 249 |
nntp |
NntpConnection's protocol (NntpConnection.cpp): a request with the re-login a 480 answer asks for, the AUTHINFO USER/PASS exchange, the greeting on connect and changing the group, as the C++ did them. | 359 |
nzbfile |
NzbFile's own logic (NzbFile.cpp): a file's segments put in part-number order, the parts its subject declares ("(1/N)") and the tail they count when the nzb plainly stops short, the file's sizes, and… | 264 |
options |
Options' value parsers (Options.cpp): ParseTime and ParseWeekDays (the scheduler's task times), ValidateOptionName's classification of option names, ConvertOldOption's renames, HasScript and… | 403 |
parparser |
ParParser's name checks (ParParser.cpp): whether a file name is a par2 file of a set - its base name and, for a volume, its block count - and whether two names belong to the same set, as the C++ did… | 114 |
parrenamer |
ParRenamer::Execute (daemon/postprocess/ParRenamer.cpp): par-rename - the destination directory and its subdirectories walked, par2 files found by their packet header and renamed to… | 482 |
paths |
FileSystem's path text functions: NormalizePathSeparators, BaseFileName, SplitPathAndFilename, ReservedChar, MakeValidFilename, SanitizePathSegment, SanitizeRelativePath, ExtractFilePathFromCmd and… | 205 |
posting |
Posting (daemon/dupesearch/Posting.cpp): which search results are one posting, the order to fetch postings in, and sketches of postings' articles. | 190 |
purgejournal |
Version-one durable purge registry. | 2,824 |
rarreader |
RarVolume's header parsing (RarReader.cpp): the RAR3 and RAR5 block structure of one volume - its version, naming and volume flags, and the files it holds with their sizes, times, split flags… | 942 |
rarrenamer |
RarRenamer's renaming (RarRenamer.cpp, RenameFiles and what it uses): the rar volumes of a directory gathered into complete sets (MakeSets), the sets not properly named (IsSetProperlyNamed) renamed… | 306 |
releasename |
ReleaseName (daemon/dupesearch/ReleaseName.cpp): release names as indexers list them, parsed into the attributes the duplicate search compares. | 718 |
renameanalyzer |
DirectRenamer's RenameContentAnalyzer (DirectRenamer.cpp): the first bytes of a downloaded file - whether it starts with a PAR2 packet (and its set id) and the MD5 of its first 16 KiB - fed article… | 214 |
rpcparams |
XmlCommand's request parameter parsing (XmlRpc.cpp): PrepareParams and NextParamAsInt/Bool/Str over the request buffer, as the C++ did it: GET query strings, JSON-RPC and XML-RPC, parsing in place… | 562 |
rpcroute |
XmlRpcProcessor's request routing and response envelope (XmlRpc.cpp): the protocol from the URL (Execute), the method name, where the parameters start and the JSON request id (Dispatch), and the text… | 265 |
scheduler |
Scheduler::CheckTasks' timing: which scheduled tasks are due between the last check and now (local time), day by day over the check interval, with the startup tasks and the reset after a clock jump. | 187 |
statmeter |
ServerVolume's slot arithmetic (StatMeter.cpp): which second, minute, hour and day slot a local time falls in (CalcSlots), and AddStats' clearing of the slots the clock moved past (both ways) before… | 149 |
streamcompare |
Stream-repair identity comparison floor; borrowed C++ ranges keep their ownership. | 185 |
streampatchparts |
Batched stream-repair donor selection, preserving the endpoint predicate. | 250 |
text |
WebUtil's in-place text helpers: XmlDecode, XmlStripTags, XmlRemoveEntities, HttpUnquote, UrlDecode, plus UrlEncode and Latin1ToUtf8. | 380 |
twin |
Twins and alts: duplicates of a download told apart by the files their par2 sets describe. | 1,335 |
url |
URL::ParseUrl: splits "scheme://user:password@host:port/resource". | 117 |
urlcoord |
UrlCoordinator's own logic (UrlCoordinator.cpp): what a fetched NZB's X-DNZB headers set and the statuses a finished fetch leaves. | 111 |
util |
Util's string helpers: FormatSize, FormatSpeed, AlphaNum, HashBJ96, ReduceStr, the Tokenizer's tokens and MatchFileExt. | 525 |
webdownload |
WebDownloader's HTTP decisions (WebDownloader.cpp): the status line (CheckResponse), the response headers it acts on (ProcessHeader) and the URL a redirect leads to (ParseRedirect), as the C++ did… | 169 |
webserver |
The web server's request handling decisions (WebProcessor): what a request header line means, the URL's prefix and credentials, the credential check and the authorized-IP list. | 283 |
webutil |
WebUtil's tag and field finders and ParseContentDispositionFilename, matching the C++ code byte for byte, quirks included. | 203 |
wildmask |
Wildcard matching (WildMask): * any run, ? any byte, # one digit; letters compare case-insensitively through a table supplied by the C++ caller for its current locale. |
228 |
Porting notes
The notes below come from rust/PORTING.md in the repository. Its table of finished ports lists the measured speed-ups; the module table on this page lists all current Rust modules. Use the preceding checklist to track the remaining app and dependency work.
nzbget moves to Rust one routine at a time. The Rust code lives in rust/ and
builds as a static library (cmake/rust.cmake). C++ calls it through the C ABI
in rust/include/nzbget_rs.h.
Rules
- Each port matches the C++ output byte for byte, quirks included. A behavior change is a separate commit.
- Before each swap, a differential fuzz runs the old C++ against the Rust code. The C++ version remains as a fallback for platforms without Rust integration.
- Measure before and after; port the hot paths first.
Done
| C++ | Rust | Speed-up |
|---|---|---|
WebUtil::JsonEncode |
escape::json_encode |
1.9x |
WebUtil::XmlEncode |
escape::xml_encode |
1.6x |
WildMask::Match |
wildmask::wild_match |
1.4x |
WebUtil::DecodeBase64 |
decode::base64_in_place |
3.2x |
WebUtil::JsonNextValue |
decode::json_next_value |
4.5x |
WebUtil::JsonDecode |
decode::json_decode |
2.3x |
Crc32::Combine |
crc::combine |
135x |
WebUtil::XmlDecode, XmlStripTags, XmlRemoveEntities, HttpUnquote, UrlDecode, UrlEncode, Latin1ToUtf8 |
text |
port only |
WebProcessor header, URL, credential and IP checks |
webserver |
port only |
Decoder (yEnc, UU, raw; rapidyenc kept) |
decoder |
parity |
Scheduler::CheckTasks timing (tasks run in C++) |
scheduler |
port only |
XmlCommand request parameters (PrepareParams, NextParamAsInt/Bool/Str) |
rpcparams |
port only |
XmlRpcProcessor routing (Execute protocol, Dispatch method/id/params) and BuildResponse envelope |
rpcroute |
port only |
Util::SplitCommandLine, Trim*, SanitizeLine, EndsWith, FormatBuffer, WebUtil::ParseRfc822DateTime |
util |
port only |
ServerVolume::CalcSlots and AddStats slot clearing (StatMeter) |
statmeter |
port only |
Options::ParseTime, ParseWeekDays, ValidateOptionName, ConvertOldOption, HasScript, ParseCategorySource |
options |
port only |
ExtensionLoader::V1 (pre-manifest script headers: kind, about, options, commands, select values) |
extload |
port only |
WebDownloader::CheckResponse, ProcessHeader, ParseRedirect |
webdownload |
port only |
UrlDownloader::ProcessHeader (X-DNZB headers), UrlCoordinator::UrlCompleted statuses |
urlcoord |
port only |
NntpConnection protocol: Request (480 re-login), AUTHINFO USER/PASS, the greeting, GROUP (C++ does the I/O) |
nntp |
port only |
CommandLineParser (getopt over the same argv, the file argument, ID and name lists) |
cmdline |
port only |
FileSystem::FileExists, DirectoryExists, ForceDirectories, MakeUniqueFilename, ExpandHomePath, GetLastErrorMessage (fixed: XSI strerror_r) |
fsops |
port only |
ParParser::ParseParFilename, SameParCollection |
parparser |
port only |
RarVolume header parsing (RAR3/RAR5 blocks, files, encryption records; the source and OpenSSL stay C++) |
rarreader |
port only |
RarRenamer::RenameFiles (sets, proper names, main file, new names; the regex, file checks and moves stay C++) |
rarrenamer |
port only |
DirectRenamer's RenameContentAnalyzer (PAR2 header and set id, MD5 of the first 16 KiB) |
renameanalyzer |
port only |
DirectRenamer's new names (BuildNewRegularName, BuildNewParName, NeedRenamePars) |
directrenamer |
port only |
ManifestFile::Load (an extension's manifest.json; with a Boost.JSON-compatible parser: strict grammar, depth 32, duplicate keys, Boost's imprecise numbers) |
manifest, json |
port only |
TestServerXmlCommand's JSON body (Json::Deserialize, ParseRequestParams: value_tostd::string/int/bool) |
rpcparams, json |
port only |
ParRenamer::Execute (par-rename: par2 files by header and set id, regular files by the MD5 of their first 16 KiB, .bad par2 files, missing files; par2 loading, file IO and moves stay C++) |
parrenamer |
port only |
FileSystem::DeleteDirectory, DeleteDirectoryWithContent, RenameBak, GetFileExtension (links never followed, first error kept, errno as before; the file system calls stay C++) |
fsdelete |
port only |
BinRpc's frame (BinRpcProcessor::Execute credential check, request names, Dispatch size check, SendBoolResponse, ReceiveTrailing length check), list responses (List, Log, PostQueue, History: the C++ collects a response's entries under its locks into plain structs, one call encodes them into caller scratch or one owned allocation), EditQueue's trailing data (size check, ids, names), Download's URL test, and the remote client's reading of list responses (RemoteClient: the existing single-entry queue reader, the log, post-queue and history listings printed from one call each) |
binrpc |
port; the client stops at an entry that doesn't fit the data, unknown stages and statuses print no text; entry fields a kind doesn't set are 0, a negative Log line count asks for no entries |
NzbFile::AddFileInfo (segments in part order, the declared parts and tail, sizes), DeclaredParts, HasDuplicateFilenames (one pass instead of every pair); the XML handlers, the queue objects and the filenames stay C++ |
nzbfile |
17x at 20,000 files |
DonorScore::Base, Target, Rerank (duplicate scores under a pick; Ranks stays C++ and calls Target) |
donorscore |
1.2x |
ReleaseName::Clean, Normalize, ShortQuery, Parse, Readable, SameRelease (the std::regex patterns matched by hand in the engine's order; C ctype as nzbget sets it) |
releasename |
2.6x |
Posting::MakeSketch, SameSketch, SamePosting, GroupListings, ListingMismatch, OrderPostings (CRC-32, stable ordering and first matching listing's rank; file reading and its cache stay C++) |
posting |
2.3x ordering |
Each port has a differential test in rust/tests/ that compares it with the
pre-port C++ under ASan and UBSan.
Each port adds representative Rust kernel coverage to rust/benches/ports.rs or the selective development-only Criterion pilot. Kernel results support attribution and regression history; they do not qualify a runtime replacement.
Before each replacement, the PR shows matched complete C++/current-Rust/candidate caller timings and percentage changes for every representative fixture, with raw samples, verified observable output, immutable source/compiler/binary/input hashes and reproduction instructions. Include the real external C++/FFI call boundaries, allocation, mutable input clone/reset and ownership/destruction costs. Retain original and stronger optimized C++ controls, slower rows, unchanged-control drift and layout/host uncertainty. The replacement must be independently faster than the C++ it replaces and preserve existing Rust caller performance where already ported. Missing or inconclusive complete-caller evidence is HOLD; rework or hold/split slower functions instead of advancing a broad patch.
Bencher's inherited 10% historical regression alert is monitoring only, never a migration waiver. Passing that alert is not proof that a replacement is faster. The historical figures above describe their recorded fixtures, not standing approval for later changes. New shared qualification defaults apply prospectively; partial legacy archives retain unknown fields and raw rows rather than inventing native v1 evidence.
See rust/benches/README.md for the shared remote runner, resource-lock order, schema, whole-process paired statistics, Criterion limits, data-only trusted publication and verified xbmc4lyfe identity. Current Bencher configuration is absent and publication ready=false; workflow definitions or a signed evidence commit alone do not establish activation or attest a measurement.
Native duplicate discovery
dupeidentity reads bounded RAR4/RAR5 first-article headers or a bare media
file's yEnc size. It rejects samples, unknown sizes, encrypted headers and
conflicting release names. The daemon retains article transport and queue
ownership. DupeSameSize hints compare data-file counts and bytes independently
of parity overhead; existing and newly discovered backups use the same rule.
The hints select candidates for the existing checksum validation, rather than
establishing byte identity.
dupewatch owns queue/history eligibility, repeat-search suppression, broader
queries, a shared atomic fetch budget, partial-NZB health normalization and PAR2
repairability estimates. The daemon retains queue locks, HTTP/NNTP transport and
the existing failover controller. A search shares one deadline and a budget of
three HTTP attempts per configured donor across strict and broader passes; zero
keeps the existing unlimited mode. Broader candidates need conservative release
compatibility and matching decoded packed-media identity. Generated duplicate
keys follow renamed picks only when the persisted per-NZB ownership marker
matches; client key edits clear that marker. Explicit client keys remain intact. Empty searches
can retry without losing the title needed to recognize generated keys, while a
usable existing backup suppresses those retries.
The offline differential test compares the pure Python proxy reference with Rust. It deliberately never imports proxy configuration or performs indexer requests. The watcher differential suite likewise runs only local pure-function fixtures. Isolated fake NNTP/Newznab functional scenarios exercise broader identity proof, data-only hints and rename tracking. The Bencher suite includes identity, queue decisions, broader queries and partial health normalization.
The source-extracted pre-port Prepare prefix matches 184,320 C++ ASan/UBSan
fixtures. A separate same-feature C++ control performs the required identity,
cache, ownership, fleet and history work with the faithful pre-port C++
normalizer and an independent allocation-free policy. This control is a
counterfactual implementation, not the old production controller. Parity checks
cover normalization, signed-time extremes, ownership and active/retired fleets.
Decision-prefix measurements include normalization, runtime name-cache work, peer/history snapshots, allocation and the real Rust C ABI. Both callers stop before accepted-search stamp writes, edits, saving, packed-media proof and transport. Cold/restart and warm eligible/cooldown fixtures are measured under the exclusive shared-worker lock. Prebuilt snapshots separately compare the best direct C++ policy with Rust; a second opaque C++ translation-unit experiment makes the ABI boundary comparable. Generic lower-score peer snapshots are valid API stress cases, while the production caller normally filters those peers before the call. These are synthetic fixtures, not deployment measurements.
Earlier comparisons against the old production controller perform different work and are retained only as historical experiments. The context-bearing old C++ policy prefix and the scalar Rust function also have different boundaries; their timings do not establish a language speedup. Wrapper experiments are kept separate from the best direct C++ control. Final gates use paired changes for each row and retain slower and noisy results.
Later complete PrepareNative measurements use the actual native library,
metadata, collection, logging and isolated state/progress writes. The alternative
C++ controller changes only watch/normalization boundaries; existing SameRelease
remains shared production behavior. Tiny (2 live/0 history), normal (4/2) and large
(128/1024) fixtures pass 28 parity cases, including generated-key cold/warm writes.
Ordinary optimized C++ policy remains machine-outlined; no control is handicapped.
Five alternating process pairs preserve every sample. A second fixed experiment
removes backend-specific normalization from untimed fixture resets/assertions and
uses a new composed archive; differences cannot be attributed to that repair alone.
Its paired medians still show tiny warm IO +4.97%, generated warm IO +4.44% and
large warm IO +1.94%. The candidate remains held; no overall faster claim follows.
Matched IO bytes do not independently prove every atomic write or fsync succeeded.
Full raw process pairs and compiler/source provenance are retained in PR #80.
Restarted searches bind donor proof to the original title, key and posting fingerprint; changed/unbound saved state is searched again. Legacy three-field stamps refresh once and preserve ambiguous client keys; only an independently persisted per-NZB marker proves generated ownership. Manual name edits notify only the watcher, while DirectRename retains its queue-script event. Inferred absent parts mark unusable donors dead, including fast additions and existing history members. Generated keys and managed score floors persist without client edits.
Parked
- NZB parsing (
NzbFile::Parse, libxml2 SAX1): a Rust parser that matches libxml2's callbacks exactly. All 5,929 NZBs on the test server plus 9,000 mutated documents match or are left to libxml2. It is still about 25% slower than libxml2 on a 17.6 MB NZB, so it is not merged.
Next
Track remaining application areas, native dependencies, external helpers,
platforms, qualification, and final C++ removal in the
zero C++ checklist.
Its source is site/content/rust-port.md. The generated module table lists
existing Rust modules, not a complete application migration.
The C++ fallbacks remain required during migration. Removing them requires qualified Rust paths for every retained target and the final removal checks; porting the routines in the Done table doesn't retire the C++ application.
Build and verification
Native POSIX builds use Rust when Cargo, rustc, and the target standard library are available; otherwise they retain the C++ encoders. Debug selects Cargo's dev profile; other CMake configurations select release. Cargo is invoked on each build to track its own sources, lockfile, build scripts, and configuration. Both the daemon and test executable inherit the archive and Rust's native link dependencies. Static executable builds select Rust's static CRT dependencies as well.
Windows keeps the original C++ encoders, so its existing build does not acquire a
Rust dependency. Non-macOS cross builds also keep C++ unless a matching target is
provided with -DNZBGET_RUST_TARGET=<triple>. macOS selects the target from the
CMake architecture and forwards CMAKE_OSX_DEPLOYMENT_TARGET. Install the Rust
standard library for the chosen target to enable Rust for that architecture.
An explicit NZBGET_RUST_TARGET requires a working Rust toolchain and standard
library; a missing dependency is an error rather than an automatic fallback.
Run cargo test --release in rust/ for encoder and FFI tests. On POSIX with a
C++ compiler supporting ASan/UBSan, run python3 rust/tests/differential.py from
the repository root for a deterministic comparison against the exact encoder
bodies in commit 78dcb938. This covers arbitrary bytes, embedded NULs, invalid
continuations, truncations, and out-of-range code points.