| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| zpaqfranzxp.exe | < 8 hours ago | 4.4 MB | |
| zpaqfranz-open.exe | < 8 hours ago | 3.5 MB | |
| zpaqfranzhw.exe | < 8 hours ago | 4.3 MB | |
| zpaqfranz-full.exe | < 8 hours ago | 8.7 MB | |
| zpaqfranz32.exe | < 8 hours ago | 4.6 MB | |
| zpaqfranz.exe | < 8 hours ago | 4.5 MB | |
| zpaqfranz.cpp | < 8 hours ago | 4.6 MB | |
| README.md | < 9 hours ago | 33.0 kB | |
| Windows 32_64 bit executables and source code source code.tar.gz | < 9 hours ago | 2.3 MB | |
| Windows 32_64 bit executables and source code source code.zip | < 9 hours ago | 2.3 MB | |
| Totals: 10 Items | 39.3 MB | 0 | |
zpaqfranz 65.1
From 64.8j (2026-06-29) to 65.1a (2026-09-19) one new command with its own filesystem driver, one new extraction mode, a new way of restoring without destroying anything, and a handful of fixes that only show up after months of real backups.
As a new build with a lot of news, a lot of new bug are to be fixed too :)
The Big News
The headline is mount: a zpaq archive can now be browsed as a normal, read-only drive, on Windows and on Linux/BSD.
Not really tested on *nix, next release...
Part one - what is new, for the user
mount: the archive as a drive letterx -backupdir: restore without erasing anything- Franzen-only archives: no cleartext
.zpaqat all xx: extract with sequential writew -ramdisk: files bigger than the chunkvno longer fails after a metadata-only change- Windows: deletions that really delete (or delete as much as possible)
t/pp -ssd: pre-check of the <[inline_block>2](#8s -home: how old is the stuff in this folder?- Downloads that work on dual-stack (IPv6) machines
- Smaller things
1. mount: the archive as a drive letter
zpaqfranz mount backup.zpaq
That is all. The archive appears on the first free drive letter, an Explorer window opens on it, and every file in it is there - openable, copyable, searchable, playable, diff-able - with no extraction, no temporary folder, no waiting for the whole thing to be unpacked. Ctrl+C unmounts.
By default you get the last version of the archive, and nothing else: the mountpoint is that version, its files and folders are right there. That is what you want nine times out of ten, and it is also the fastest thing to mount, because only that version's directory tree has to be built.
zpaqfranz mount backup.zpaq Z: -all :: every version, one folder each
With -all the mountpoint holds one folder per version instead:
Z:\VER00000000\... the archive as it was after the 1st add
Z:\VER00000001\... ... after the 2nd, and so on
Each folder is a snapshot, not a diff: a file that never changed is visible in all of them. Which makes "what did this file look like in June?" a copy, and "what changed between two backups?" a robocopy /L or a diff -r between two folders.
-until N (or a date) moves the target: -until 3 mounts version 3,
-all -until 3 shows the first three folders.
What it can read:
| plain archives | yes |
AES-256 (-key) |
yes |
Franzen / AES+Franzen (-franzen) |
yes, including franzen-only archives |
multipart (part_???.zpaq) |
yes |
| damaged tail | mounts everything up to the damage, then says so |
| streaming (non-journaling) archives | no: no fragment table, nothing to seek to |
Read-only, always. Nothing you do on the mountpoint can touch the archive.
Windows: needs WinFsp. If it is not installed, mount offers to download and install it for you (pinned version, size and SHA-256 checked, admin + captcha required), then asks you to run the command again. The drive can also be presented as a network drive, which is the usual trick to keep an aggressive antivirus/antisomething from scanning the whole thing:
zpaqfranz mount backup.zpaq Z: -fuseopt VolumePrefix=\zpaqfuse\backup
Linux/BSD: needs libfuse3 (apt install libfuse3-dev and friends). The mountpoint is an existing empty directory, the process stays in the foreground, Ctrl+C or fusermount3 -u unmounts.
The binary must be built with -DZPAQMOUNT , otherwise nothing changes, no FUSE headers are needed, and there is no mount command at all.
While the index is being read (minutes, on a 200 GB archive with hundreds of versions) a progress line with an ETA is printed; -noeta turns it off.
zpaqfranz h mount for the whole story.
2. x -backupdir: restore without erasing anything
x -force overwrites. If the restore turns out to be the wrong version, or the wrong folder, what was on the disk is gone. Now it does not have to be:
zpaqfranz x backup.zpaq -to z:\data -force -backupdir z:\archiv
Every file that would be overwritten is moved into z:\archiv\<YYYYMMDD_HHMMSS>\, keeping its relative path, before a single
byte is written. Nothing inside the backupdir is ever overwritten: each run gets a fresh timestamped folder, and a name collision inside it becomes name_00001.ext. If one move fails, the extraction is aborted before writing anything, and what has already been moved is safe where it is.
With -extras, the files on disk but not in the archive are moved aside too, so the restored tree matches the archive exactly - and still, nothing is deleted:
zpaqfranz x backup.zpaq -to z:\data -force -backupdir z:\archiv -extras
Without -to this becomes "restore in place": the folders compared against the archive are the archive's own top-level folders.
zpaqfranz x backup.zpaq c:\data -to c:\data -force -backupdir z:\archiv
zpaqfranz x backup.zpaq -force -backupdir z:\archiv -extras
It is the equivalent of rclone's --backup-dir, and it needs -force (there is nothing to move aside if you are not overwriting). -extras
refuses to run with the switches that rename files on the way out (-flat -utf -nopath -fix255 -fixcase -fixreserved) - with those, every file
on disk would look like an extra - and refuses to scan an entire drive.
3. Franzen-only archives: no cleartext .zpaq at all
-franzen used to write the archive twice: the cleartext .zpaq and the encrypted .zpaq.franzen beside it. Now the cleartext one is simply not written:
zpaqfranz a backup.zpaq c:\data -franzen mypassword
produces only backup.zpaq.franzen. Add -key too and you get a single AES+Franzen (FRENZEN) file. The archive lives in one encrypted file, full stop.
And it keeps working as an archive: adding to it later appends to the .franzen, l, x, t and mount read it, -copy, -checktxt,
-backuptxt and the fasttxt CRC-32 machinery all follow the physical .zpaq.franzen, and an interrupted add rolls it back to its previous size.
If you point a command at an archive whose .zpaq is missing but whose .zpaq.franzen is there, zpaqfranz recognizes it, tells you what it is (Franzen-only, or AES+Franzen) and asks for the password instead of quietly starting a brand new archive beside it. Using -key on a franzen-only archive (or forgetting it on an AES+Franzen one) is refused with a clear message rather than a corrupt result.
(-debug still writes both files, but only when creating: an existing franzen-only archive stays franzen-only, because a .zpaq born from an append would be a fragment of an archive, not an archive.)
4. xx: extract with sequential write
zpaqfranz xx backup.zpaq -to z:\restore
xx is x -recover with its own name and its own help page. It reads the archive in stored order (random reads, so an SSD helps) and writes the output files through a RAM cache, sequentially. Three consequences:
- much faster on spinning drives, which hate the interleaved writes of the normal extraction;
- the restored files come out far less fragmented;
- on a damaged archive it gets out as much as it can, instead of stopping.
-ramsize X sizes the cache (default 1 GB). Everything else (-to -only -not -until -force ...) works exactly as in x.
zpaqfranz xx z:\1.zpaq -to z:\muz7\ -ramsize 4GB
zpaqfranz xx z:\1.zpaq -to z:\muz7\ -force :: salvage a damaged archive
5. w -ramdisk: files bigger than the chunk
Chunked extraction (w) used to refuse the job when a single file was bigger than the chunk:
02453: chunk size (-maxsize) too small ..., at least ... needed (bypass with -space)
Now, in -ramdisk mode, an oversized file is extracted in sequential RAM windows of at most one chunk each, written into the output file at the right offset:
02560: RAM window 3/7 offset=6442450944 size=2147483648
So a 40 GB VM disk can be extracted with a 2 GB RAM budget. Two old bugs in the same area went with it: the chunk counter could spin forever on an oversized file, and an empty "spare" chunk was always counted and extracted at the end.
6. v no longer fails after a metadata-only change
If a file's attributes (or creation/access date) changed but its content did not, add does not re-read the file: it carries the old fragments over. The stored hash, though, was written as an !ERROR! placeholder - and from that moment on, v (verify) reported that file as FAILED, forever, on every run.
On a big backup that is enough to make the whole verify output useless as a monitoring signal.
The archived bytes are exactly the bytes the previous version hashed, so now the previous hash (and CRC-32) are carried over with the fragments. The bogus ERROR expected N getted 0 bytes message for that case is gone too.
Thanks to MarkSchmied for reporting it.
7. Windows: deletions that really delete
When a thin NTFS image is restored onto a volume (image with -image -ntfs), the files on the destination that are not in the image have to go. That deletion has to survive read-only/hidden/system attributes, hostile ACLs, junctions and paths over MAX_PATH - and it was failing silently on every long path, because of a Win32 API that does not accept the \\?\ prefix. Rewritten: ownership and DACL are now forced through a file handle, permissions are forced on a subfolder before descending into it, and symlinks/junctions are removed without ever following them.
When an item really cannot be removed, it is scheduled for deletion at the next reboot - and, unlike before, you are told, item by item and in the final
summary:
Total items : 1.234
Deleted OK : 1.230
Reboot pending : 4
Failed : 0
*** RIAVVIO DI WINDOWS NECESSARIO ***
4 elementi ... IL RIPRISTINO NON E' COMPLETO FINO AL RIAVVIO!
8. t / pp -ssd: pre-check of the .franzen
On a Franzen archive, t (test) and pp (paranoid test) with -ssd (or -paranoid) now decode the .franzen first, to catch a corrupted container before spending time on the archive inside it:
zpaqfranz pp j:\1.zpaq.franzen -ssd
A failed pre-check is reported and makes the command exit with 2, even when everything else looks fine.
9. s -home: how old is the stuff in this folder?
s -home now shows, for every folder, the date of its oldest and newest file:
123.456.789 117.74 MB 00001234 [2019-03-02]-[2026-09-14] c:/users/franco
which makes "cold" folders (nothing touched for years, prime candidates for the archive) visible at a glance. -verbose adds a second listing with the full timestamps and the name of the newest file.
Dates in the future (broken clock, broken filesystem, a VM with a wrong RTC) are excluded from the calculation, the folder is highlighted, and you are told how many there were and what the worst one is: a silently wrong "newest file: 2038" would be much more annoying.
10. Downloads that work on dual-stack (IPv6) machines
Two fixes to every HTTP download zpaqfranz does on Windows (version check, self-update, the WinFsp installer above):
- the URL was being converted to UTF-16 with the same function used for file names, which turns every
/into\; - when urlmon gives up on a dual-stack host whose IPv6 address does not answer (
INET_E_DOWNLOAD_FAILURE, typically a VM on Windows 11), the download is retried with WinINet, which falls back to IPv4.
11. Smaller things
is_file_zpaq()is now Unicode-safe on Windows (it usedfopen(), so an archive whose name contained non-ANSI characters looked like "not a zpaq archive").- New
is_file_frenzen(): tells AES+Franzen apart from plain Franzen, so the-keyrequirement can be checked before doing anything. addno longer tries to truncate or re-date a.zpaqthat does not exist (franzen-only archives).- Build/licensing documentation for FUSE (LGPL) and WinFsp (GPLv3 + FLOSS exception) added to the in-source license list, and
-DZPAQMOUNTdocumented among the build switches. - Thanks list: MarkSchmied (the WORK_NONE hash warning) and dannyboy76705 (the mount idea).
Part two - technical detail
1. mount
~2,330 lines, all inside #ifdef ZPAQMOUNT: a header block right after windows.h (the FUSE headers have to come early), namespace franzmount (1,872 lines) just before Jidac::doCommand(), the Windows WinFsp glue, and Jidac::mount() (190 lines) as the command itself. Command letter 'V', help_mount(), -fuseopt and -backend as new string options.
Layering
Jidac::mount() arguments, backend choice, FUSE loop
franzmount FUSE layer getattr/readdir/open/read/statfs/init
franzmount generic path resolution, name escaping, dates,
block cache + read-ahead, read_range()
franzmount::MountBackend the ONLY thing the layers above know about an archive
backends "core" and "jidac"
MountBackend is a pure interface: versions(), version_date(v), children(v, dir), entry(v, path, out), frag_size(f), block_of(f, &off),
new_reader(), archive_bytes(), parts(), remarks(), prepare(v).
Everything above it is engine-agnostic, which is why the two engines can be mounted side by side on two mountpoints and compared on the same archive.
The two engines
-backend |
what it is |
|---|---|
core |
the standalone zpaqfuse scanner: memory-mapped part files, own journaling parser, one full record snapshot per version. Plain archives only. |
jidac |
the same index read through InputArchive, the native zpaqfranz reader: AES-256, Franzen, AES+Franzen, franzen-only, multipart, the shared password prompt. Version snapshots share their records (a file unchanged across 300 versions is stored once), the SHA-1 of every index block is verified, an incomplete transaction is dropped and a damaged tail truncates the mount instead of failing it. |
auto |
the default: jidac if -key/-franzen is in play or the first bytes on disk are not a zpaq header, core otherwise. |
Neither engine ever decompresses a d (data) block during the scan: only c/h/i journaling blocks are read, and the c block's jump is used to skip over the data.
Version layout
MountState::allversions (from the global -all) decides everything:
- with
-all,mount_split()requires the first path component to be a version folder (mount_version_name(v)=VER%08u, zero padded so a lexicographic sort keepsVER00000009beforeVER00000010; the bare number is still accepted on lookup), and the rootreaddirlists the versions; - without it,
mount_split()maps every path onto versionnversions-1and the root is that version's own root.
-until N (or a 14-digit date) cuts nversions first, so it picks which version is the last one in both modes.
Building the directory tree
Each version needs a map<string, vector<string>> of "directory → sorted immediate children", with every ancestor directory synthesized even when zpaq never stored it. On a 200 GB / 170-version / 700k-file archive that is 119 million mount_tree_add() calls, and it dominated the mount time. Sorry.
It is now deferred: the scan only snapshots the records, and MountBackend::prepare(v) builds the tree, called by Jidac::mount() for
the exposed versions only, before fuse_main(), from one thread. Default mode builds one tree instead of N.
Reading
mount_read_range() walks the file's fragment list, maps each fragment to its block (fragloc_, an O(1) table built at scan time), pulls the block from the cache and memcpy's the overlapping slice. Then it queues up to READAHEAD_DEPTH = 4 distinct following blocks for background decompression.
MountCache is an LRU of decompressed blocks bounded by bytes, split into N independent shards (each with its own MountReader , its own file handle,
its own AES keystream, its own Franzen context, its own mutex and its own LRU), plus a small worker pool for the read-ahead. Blocks are handed out as shared_ptr<const string>, so a 4 KB read of a 16 MB block costs no copy and the data survives an eviction mid-read.
Tuning, environment only (no rebuild, no new switches):
ZPAQFUSE_CACHE_MB (256), ZPAQFUSE_SHARDS (threads, capped at 16),
ZPAQFUSE_PREFETCH_THREADS (threads), and on Windows ZPAQFUSE_WINNAMES,
ZPAQFUSE_CASEFOLD, ZPAQFUSE_CASE_INSENSITIVE. -threads N sets thedefault for the last three (4 if not given).
Names
An archive can hold names Windows cannot represent. mount_escape_name() percent-encodes, deterministically and injectively, exactly what NTFS refuses: \ : * ? " < > |, control characters, a trailing . or space, the reserved device names (CON, LPT1, ...), and bytes that are not valid UTF-8. % itself is escaped only when followed by two hex digits, which is exactly when it would be ambiguous on decode (100%.txt stays as it is, a%3Ab becomes a%253Ab). mount_unescape_name() is its exact inverse, so the escaped name
resolves back to the stored one.
A stored absolute path keeps its drive as c:, which is not a legal name either: at the root of a version it is shown as c, and both spellings resolve.
With ZPAQFUSE_CASEFOLD (default on Windows) a lookup that fails falls back to an ASCII case-insensitive search among the children, so Z:\WINDOWS\... finds windows/....
Windows specifics
The executable does not link against the WinFsp DLL. Every fsp_* entry point declared by the headers is turned into a private function pointer (FSP_FUSE_API_NAME / FSP_FUSE_API_CALL are redefined before including fuse3/fuse.h) resolved on first use by zpaqmount_winfsp_bind(); zpaqmount_winfsp_load() does the LoadLibrary once (std::call_once), picking winfsp-a64/x64/x86.dll from the registry InstallDir.
So the build needs the headers only, and a machine without WinFsp runs every other command normally.
kickstart_mount() runs before anything else: if the DLL does not load, it downloads the pinned MSI (winfsp-2.1.25156.msi) into the system temp folder, verifies size and SHA-256, requires an elevated process and a captcha, runs msiexec /qn /norestart /l*v, decodes the usual exit codes (1602 cancelled, 1618 another install running, 3010 reboot needed) and quits: the DLL lookup is cached by call_once, so the command has to be run again.
With no mountpoint (or *) the first free drive letter scanning from Z down to D is announced before the scan, and a detached thread opens an Explorer window on it as soon as the root becomes visible (polling GetFileAttributesW, up to 30 s). The volume label is the archive's base name, with ?/* turned into #, truncated to 32 characters. uid, gid and the three *InfoTimeout options default to -1 unless -fuseopt overrides them.
Scan progress
MountScanProgress (g_mountscan), one \r line refreshed five times a second: due() is a single clock read, line() a printf-style refresh, so a caller in a hot loop masks its own counter first (& 8191) and the clock is not read a million times a second.
Three phases :
index scan (percentage and ETA from the archive offset), directory tree of the versions being mounted,
totals for statfs , then the line is wiped.
Silent with -noeta, -pakka, -silent, -terse, -stdout; one line per percentage point when the output is redirected (myprintf() turns \r into \n down there); @SPK@EXT@ telemetry with -catpaqmode.
2. x -backupdir / -extras
~560 lines before Jidac::extract(), plus the hooks inside it. New globals g_backupdir (string) and flagextras (bool).
Path handling is the delicate part, and it is all in four helpers:
backupdir_collapse() (forward slashes, collapsed duplicates, // UNC
prefix preserved), backupdir_slashes() (plus \\?\ stripped),
backupdir_comparekey() (ASCII-lowercased on Windows, for comparisons) and
backupdir_sanitize(), which turns any path into something that can live
inside the backup folder: c:/data/a.txt → c_/data/a.txt, .. → __,
every character Windows forbids → _, trailing dots and spaces removed. The
folder layout matters less than not losing the file.
When there is exactly one root (the normal case: -to, or a single archive root) the relative path inside the run folder is kept 1:1, otherwise every file would be stored under its full sanitized path, which on Windows blows past MAX_PATH very quickly and the move would fail.
The run folder is <backupdir>/YYYYMMDD_HHMMSS/, with _00001 ... appended if it already exists (two runs in the same second, from a script), created up-front so a broken -backupdir fails before anything is touched.
backupdir_movefile() never overwrites: on collision the target gets the same _00001 suffix treatment.
On Windows it is MoveFileExW with MOVEFILE_COPY_ALLOWED (so it works across volumes), retried once after resetting the attributes. On POSIX it is rename(), and on EXDEV a copy + size verification + unlink, preserving mode and mtime; if the copy fails the partial target is removed and the source is left untouched.
The hook in extract() is where files were being erased: instead of delete_file(), the file is appended to backupdir_tomove.
The moves all happen afterwards, in one pass, before the first byte of output is written, and if one fails, the extraction returns 2 with a count of what has already been moved (which is safe where it is).
-extras adds a second pass.
The set of files the version would write is needed in full, not just the selected ones, so when files/-only/-not are in play the archive is read a second time with the filters cleared (jidacreset() afterwards) and the compare keys collected into a std::set<string>. Then backupdir_scantree() walks the target roots (regular files only, never following a reparse point, skipping the backupdir itself) and everything not in the set is moved. Without -to,Jidac::backupdir_archiveroots() derives the roots from the archive itself, mapped through rename() exactly like the extraction does, sorted by compare key, so a parent always comes before its children and nested folders are dropped: nothing is scanned twice.
Refused combinations: no -force (71321), -stdout/-test/-repack/-zero (71322), -extras without -backupdir (71320), more than one -to (71323), the renaming switches (71324), a whole filesystem root as the scan target (71336).
3. Franzen-only archives
The mechanism is one global, g_franzen_zpaq_nowrite, set in Jidac::add() as
(g_franzen != "") && (!exists(arcname)) && ((!flagdebug) || is_file_franzen(arcname+".franzen"))
&& (g_chunk_size == 0) && (!g_fakewrite) && (!index)
and consumed by the writer. OutputArchive grows a nowrite flag and a nowriteeof virtual EOF: with no physical file behind it, tell(), seek(), put(), write() and close() work on the virtual offset, and myfwrite() pretends a full write (so the franzen encoder downstream, which is fed from the same call, sees exactly the same byte stream it would have seen). The crypto offset for AES comes from that virtual offset instead of ftello().
Appending to an existing franzen-only archive opens the .franzen for update instead of creating it, checks that get_is_aes_too() matches the presence of -key (43792/43793/43794/43796) and does not rewrite the AES salt, which is already at decoded offset 0. Reading the salt back (Jidac::add(), when the first part cannot be opened) goes through a franzcri read_at_to() on the .franzen.
InputArchive::isopen() is new and returns true when uses_franzen is set, which is what lets every reader work with fp == FPNULL.
isfranzenonly(name) ("-franzen given, name does not exist, a valid name.franzen does") is then used to redirect: -copy copies the
.franzen, gestiscicalcolifinali() hashes it for -checktxt/-backuptxt, aggiornafasttxt() recomputes QUICK and CRC-32 on it (no cleartext CRC math is possible), g_starting_zpaqsize and the final size come from it, the rollback on a failed add truncates it back to g_starting_franzenfile, and the "Updating ..." banner says (franzen-only).
The detection at the top of Jidac::add() ("automagic"): if the .zpaq is missing but a valid .zpaq.franzen is there, is_file_frenzen() says whether it is AES+Franzen, -key is required (43804) or refused (43801), and if -franzen was not given the password is asked for (43800/43805/43802). The point is to never fork a fresh cleartext .zpaq beside an existing encrypted archive.
4. xx
xx is parsed as a command in its own right (help_xx, its own entry in the help map and in the "is this a command?" list) and then lowered to command='x'; flagrecover=true;. Jidac::extract() routes flagrecover to Jidac::extractstdout(0,""), which is the sequential-write extractor with the -ramsize RAM cache. No new code path, just a name for a strategy that was hidden behind a switch, plus a help page that explains when it is the right one.
5. w -ramdisk windows
franzfs (the RAM file buffer) gets windowed, windowoffset, initwindow(size, offset) and mappedsize(offset, size): in windowed mode ramwrite() clips the incoming fragment to the window and silently ignores what falls outside it, and mappedsize() tells the progress accounting how many of those bytes actually landed, so job.total_done stays honest.
ExtractJob grows windowed / window_start / window_size, and Jidac::extractqueue2() takes two new optional parameters
(i_windowoffset, i_windowsize, default "no window"). In windowed mode it requires exactly one file, allocates the window buffer, walks the fragment list keeping a running file offset and skips every fragment that does not overlap the window (a fragment with unknown size is a hard error, 02569).
After the threads join, the buffer is written to the output file at window_start (WB for the first window, RBPLUS afterwards, fseeko then 1 GB-at-a-time myfwrite), the file is closed with its date and attributes only on the last window, and the buffer is released.
The caller (Jidac::chunkedextract()) now has a third branch for "file bigger than the chunk": flush the chunk being assembled, then either ceil(size/chunk) RAM windows (-ramdisk) or one direct streaming pass.
The pre-count loop got the same branch and, incidentally, the fix for an infinite loop: the old while never advanced indice for an oversized file.
quantichunk++ at the end is now conditional on a non-empty chunk, as is the final extractqueue2() call, so an empty spare chunk is no longer counted or extracted. A zero or negative chunk size is refused (02567) instead of producing nonsense.
6. Hash carry-over
Two new functions. ishashusable(hash) is true only for a non-empty, all-hex, not-all-zeros string => the !ERROR! placeholder becomes all zeros for the binary hash types, because hex2binary() cannot encode it, which is why the check is "all zeros" and not "== !ERROR!".
Jidac::carryoverhash(old, io_hash, io_crc32) decodes the previous version's franz block (decode_franz_block()), and takes its hash only if the algorithm is the same one (oldfranzotype == g_franzotype, or the hash would be stored under the wrong name) and its CRC-32 if that is usable. In Jidac::add() it is called when a != dt.end() && data == 0 && size > 0 && hashedsize == 0 - the exact signature of a metadata-only update, and the carried hash also becomes currentcrc32, which is what -verify writes.
preparahashtobewritten() takes the new i_fileaggiunto parameter so it can recognize WORK_NONE and skip the 02115: ERROR expected N getted 0 bytes message, which in that case was describing normal behaviour.
7. Windows aggressive deletion
The path is image -image -ntfs → Jidac::restoreimage() →
Jidac::restore_ntfs_to_disk() → franzimager::deleteExcludedFiles() →
deleteFileAggressive() / deleteDirectoryAggressive().
forceFilePermissions() used SetNamedSecurityInfoW(), which does not accept the \\?\ prefix that fixLongPath() adds, so it failed silently on every path it was given. It now opens a handle (FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) with WRITE_OWNER and calls SetSecurityInfo() to take ownership, then reopens with WRITE_DAC | READ_CONTROL to set the DACL, being owner is what makes the second step legal.
deleteFileAggressive() keeps its five attempts (direct, strip attributes, force permissions, rename-then-delete, schedule at reboot) but now records the Win32 error of each one under -debug3. deleteDirectoryAggressive() forces permissions and resets attributes on a child before recursing (otherwise FindFirstFile on it can fail on ACLs), retries the enumeration once after a second forceFilePermissions() if it failed, removes reparse points without following them, and falls back to MoveFileExW(NULL, MOVEFILE_DELAY_UNTIL_REBOOT) at every level instead of only at the top.
A file-scope counter, s_rebootScheduled, counts what got scheduled; it is reset at the top of deleteExcludedFiles(), printed per item in magenta, and summarized at the end. "All excluded files deleted successfully" is now printed only when that counter is zero too.
8. Franzen pre-check
In Jidac::test() (after read_archive(), so a plain archive is unaffected) and in Jidac::extractstdout(): if is_file_franzen(archive) and
-ssd (or -paranoid, in test() only), do_franzen_operation(archive, "", false, true) decodes the container in test-only mode. The result is kept in franzentestresult and turned into an exit code 2 at every return point of those functions, so a corrupted container cannot be masked by a successful test of whatever could still be read out of it.
9. s -home
In Jidac::homesize(). For every folder, files_edt[i] is walked skipping directories, ADS and zero dates; a date whose year is beyond the current one (nowutc() / 10000000000) is counted as an anomaly instead, with the worst one remembered. The normal listing gains a fixed-width [YYYY-MM-DD]-[YYYY-MM-DD] column ((no valid date), same width, when a folder has entries but no usable date), anomalous folders are printed in yellow with a 03974! line under them, and a global 03973! warning is printed in red at the end. -verbose adds a second pass with the full timestamps and the newest file's name, folder names truncated to 30 characters.
10. Downloads
utow() converts to UTF-16 and replaces / with \ by default, which is right for a file name and wrong for a URL; URLDownloadToFileW() is now called with utow(url, '/').
downloadfile_wininet() (Windows only, ~140 lines) loads wininet.dll at run time by name - no new link-time dependency - and does
InternetOpenW / InternetOpenUrlW (with INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_KEEP_CONNECTION) / HttpQueryInfoW for the status code and content length / InternetReadFile into a 64 KB buffer, with a progress line every second and a final size check; the partial file is deleted on any failure. downloadfile() calls it when urlmon fails, before giving up.
Note
The help line for -franzen in a still reads "EXPERIMENTAL: create.franzen file too with key X", which describes the 64.8 behaviour, not the current one (no cleartext .zpaq is created any more, unless -debug).