Skip to content

fix(mod-management): fail a truncated .7z as damaged, not as in use - #25

Draft
doodlum wants to merge 1 commit into
masterfrom
fix/laz-1287-truncated-archive
Draft

doodlum wants to merge 1 commit into
masterfrom
fix/laz-1287-truncated-archive

Conversation

@doodlum

@doodlum doodlum commented Oct 5, 2026 •

Copy link
Copy Markdown
Owner

Paused, not ready for review. Work stopped at the user's request on 2026-10-05. The fix, regression tests, a real-archive A/B and one round of independent QA are done. Still owed: the round-2 fix for QA finding 1 (the install callback, below; in progress locally, not pushed), round-2 QA, pnpm run verify and the E2E baseline.

Problem

A truncated or corrupt .7z (for example an interrupted download of a large collection member) makes Vortex retry the extraction three times as if the file were locked, then show "Archive damaged" with a Continue button that installs nothing. Every way out ends as a user cancel: finish mod install {outcome: "canceled"}, and in a collection dependency install failed, requeued for retry {error: "canceled by user"} plus "Installation canceled". Seen in a user's 2.7.2 log (Skyland AIO 4.32.0) and reproduced on master 34f3def. LAZ-1287.

7-Zip reports such a file with one error string that holds both Open ERROR: Cannot open the file as [7z] archive and ERRORS: Unexpected end of archive. In InstallManager:

  • isFileInUse matches its "cannot open", so extractWithRetry retries.
  • isCritical returns false as soon as isFileInUse matches, so "unexpected end of archive" never yields an ArchiveBrokenError.
  • queryContinue's terminal check looks for the literal Can not open the file as archive, which current 7z never prints, so Continue is always offered.

Change

src/renderer/src/extensions/mod_management/InstallManager.ts (+10/−2):

  • 7-Zip 26.00 names the archive type ("as [7z] archive") only after it has read the file and recognised its format. That wording means the content is unreadable, never locked.
  • isFileInUse now ignores that typed wording. The untyped "Cannot open the file as archive", which is what an archive held open by another process produces (followed by the system's sharing-violation message), still counts as in use and is still retried. That untyped wording is also what 7z prints for a file it can't identify at all (e.g. a zero-filled .rar), so such a file is still retried three times; only its Continue goes away.
  • With that, isCritical sees "Unexpected end of archive" and the install fails with ArchiveBrokenError, through the existing damaged-archive handling.
  • queryContinue's terminal check matches "Cannot" or "Can not open the file as [type] archive", with or without a type and in any case, so Continue is no longer offered when 7z could not open the archive at all.

InstallManager.archiveErrors.test.ts (new): regression tests on the real InstallManager, using 7-Zip 26.00 output captured from real runs. The non-English lock message in one test is a stand-in, not captured output.

Behaviour changes

  • Truncated .7z: no retries and no "Archive damaged" dialog. The install fails (outcome: "failed") instead of being canceled, and the download is marked failed. The user gets "Installation failed, archive is damaged" (Delete, Delete & Redownload) and a " failed to install" error. Truncated .zip and .rar already failed this way on master.
  • A file 7z recognises by type but can't read (e.g. a zero-filled .zip, a text file renamed .7z): no retries; the dialog offers Cancel and Delete, no longer Continue.
  • An archive still locked after the three retries, or a file 7z can't identify: still retried; the dialog no longer offers Continue.
  • Analytics: a truncated .7z now sends "installation failed" instead of "installation cancelled"; its error_message carries the archive's file name and error_code is "unknown".
  • In a collection: such a member fails as an error instead of a user cancel. As on master, it is requeued once and not driven again; it sits until the stall watchdog (about 10 minutes) settles it. Re-downloading it is LAZ-1286.
  • simulate classifies the same way.
  • A locked archive is unchanged: retried, and it installs once released. A CRC error in a .zip still offers Continue.

Evidence

  • In the app (A/B), author: source development build, Windows 11 on ARM under x64 emulation; base renderer from master 34f3def, head 2625473. Disposable Stardew Valley folder, Premium login. Real archive nxm://stardewvalley/mods/4399/files/121125 ("Xtardew Valley-4399-3-2-0-1735946829.7z", 1,718,827 B), downloaded by Vortex, truncated in place to 859,413 B, installed with start-install-download. One run per side.

    master this branch
    "archive file in use, retrying extraction" 3× 0
    "Archive damaged" dialog shown, with Continue not shown
    Continue ENOENT walk → fallback installer → canceled (the user's log) n/a
    Cancel outcome: "canceled" n/a
    Outcome canceled, download stays finished failed, download failed, "archive is damaged" notification
    Same archive intact, exclusively locked for 6 s during the install 3 retries, extraction after release, outcome: "success"
  • In the app (A/B), independent QA with its own real archives (7z mod 1083 file 56424, 15,973,244 B; zip mod 5969 file 99115; rar mod 4542 file 21774), one run each:

    Scenario master this branch
    .7z truncated to 50% 3 retries, dialog with Continue, canceled 1 extraction, no dialog, failed
    .7z truncated to 95% — same as 50%
    Text file renamed .7z 3 retries, Continue no retries, Cancel/Delete only
    Zero-filled .rar 3 retries, Continue 3 retries, no Continue
    .zip with a CRC error Continue Continue
    Lock held 40 s 3 retries, Continue 3 retries, Cancel/Delete only
    Lock released after 11 s — 1 retry, installs

    "Delete & Redownload" on the new notification re-fetched the file and the install succeeded.

  • Regression test: 6 tests. With the InstallManager.ts change reverted, 3 fail (truncated .7z not an ArchiveBrokenError; not-an-archive extracted 4 times instead of once; Continue offered after a lasting lock). The lock-retry and CRC-Continue tests pass on both sides. Confirmed by pr-preflight's revert check, by the author and by QA.

  • Scoped suites: mod_management, 31 files, 354 tests pass. Renderer typecheck and eslint clean. Trial merge with the LAZ-1286 branch: clean, 32 files and 363 tests pass.

  • pr-preflight: 0 fail, 1 warn (callers outside the diff: isCritical, extractWithRetry, simulate, installInner, each reviewed; util.ts's queryContinue is an unrelated option), revert check pass.

  • pnpm run verify: not started; it was due after round-2 QA, and work was paused first.

  • E2E: not started, for the same reason.

  • CI: the fork's checks started when this draft was opened; see the Checks tab.

Review

Round 1 QA on 2625473 by a separate agent, which reproduced the problem independently with its own archives. Nothing blocking. Findings:

  1. Medium (judgment), open: the ArchiveBrokenError branch of install()'s catch (InstallManager.ts ~1932–1987) never calls promiseCallback when the install isn't unattended. Pre-existing for truncated zip/rar, but this change routes a truncated .7z into it: start-install-download … __CALLBACK__ never returns ("Installation completed but callback was not called"). Callers left waiting: Mods page Enable on not-yet-installed downloads, collections' did-download-collection, the import notification's "Install All". Collection and dependency installs run unattended and are not affected. A fix with an install()-level test was in progress when work paused; it is not on this branch.
  2. Medium (author): the first draft claimed a damaged collection member is requeued up to three times and then settled failed. In the app it is requeued once and waits for the stall watchdog, as on master. Corrected in "Behaviour changes" above.
  3. Low (author): the analytics change was undisclosed. Now listed.
  4. Low (judgment): unidentifiable files (zero-filled .rar) still count as in use. Now stated in "Change".
  5. Low (new): a byte-range lock in the middle of the file gives only "…another process has locked a portion of the file", which matches no pattern, on master too. Under "Not covered".
  6. Nit: the title overstated the change to all archive types; narrowed to .7z.
  7. Nit: the German lock string in the test is a stand-in; now noted in "Change". It couldn't be verified on this machine (no German language pack).

Classification: 0 preflight, 2 author, 1 new, 2 judgment (plus 2 nits).

Not covered

  • QA finding 1, the missing install callback (above).
  • A mid-file byte-range lock matches no in-use pattern and is offered Continue (pre-existing).
  • Collections: a damaged member is requeued against the same broken file. Replacing a wrong or damaged member archive is LAZ-1286, the sibling draft.

🤖 Generated with Claude Code


Written with doodlebot.

7z reports a truncated or unreadable archive as "Cannot open the file as
[7z] archive" with "Unexpected end of archive". isFileInUse matched the
"cannot open", so extraction was retried three times, isCritical never got
to "unexpected end of archive", and queryContinue's terminal check looked
for the old "Can not open the file as archive" wording, so the "Archive
damaged" dialog offered Continue, which installed nothing. Every way out of
it ended as a user cancel.

7z names the archive type only once it has read the file, so that wording
is now excluded from the file-in-use match, while the untyped "Cannot open
the file as archive" that a locked file produces is still retried. The
terminal check matches both wordings, case-insensitively.

Fixes LAZ-1287

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@doodlum
doodlum requested a balanced review from Copilot October 5, 2026 22:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Interactive damaged-archive installs can leave start-install-download callers waiting because the callback is not completed.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Updates archive-error classification so truncated .7z files fail as damaged instead of being retried as locked.

Changes:

  • Distinguishes typed 7-Zip open failures from file-lock errors.
  • Removes Continue for archives that could not be opened.
  • Adds regression coverage for corruption, locks, and CRC errors.
File Description
InstallManager.ts Refines extraction-error classification and dialog actions.
InstallManager.archiveErrors.test.ts Adds archive-error regression tests.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

return true;
}
const lowered = errorMessage.toLowerCase();
const lowered = errorMessage.replace(ARCHIVE_UNREADABLE_AS_TYPE, "").toLowerCase();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants