Skip to content

Hash mods' flattened folder content instead of a rebuilt zip - #50

Open
12problems wants to merge 1 commit into
mqttfrom
feat/mod-folder-hash-verification
Open

Hash mods' flattened folder content instead of a rebuilt zip#50
12problems wants to merge 1 commit into
mqttfrom
feat/mod-folder-hash-verification

Conversation

@12problems

Copy link
Copy Markdown

Summary

Companion PR to Balatro-Multiplayer/new-launcher's folder-based mod
deployment change (targets main there). The launcher no longer
deploys mods as zips - NFS.mount()-mounting a .zip didn't behave
correctly for every mod in practice - so it now deploys real extracted
folders instead. This server's "approved hash" has to match what the
launcher actually verifies against, so it switches too: hash a mod's
flattened content directly instead of assembling and hashing a
deterministic zip of it.

  • New mod-folder-hash.ts: computeModFolderHash(), a byte-for-byte
    Node port of the launcher's ModFileHash::hashDirectory()
  • mods-sync.service.ts calls it directly on the flattened extracted
    directory instead of shelling out to modzip
  • Deleted native/modzip/ and its Dockerfile build step entirely - no
    native C binary/libzip dependency left in this repo at all
  • New mod-folder-hash.test.ts covering the algorithm's correctness

Verified with a real cross-language parity check, not just each
side's own unit tests: built a standalone copy of the launcher's
actual modfilehash.cpp and ran it side-by-side with this repo's
mod-folder-hash.ts against the same sample mod tree - both produced
the identical SHA-256 digest, both before and after renaming the
tree's own root folder (deliberately irrelevant to the hash on either
side now, unlike the old zip-based scheme).

No DB schema change - sha256 columns stay hex-sha256, just fed
different input bytes. Every hash stored under the old algorithm is
wrong under this one - pnpm backfill-mod-hashes needs to run once
after this deploys (already idempotent, unchanged).

Test plan

  • pnpm test (mod-folder-hash.test.ts + mod-archive-flatten.test.ts) - all passing
  • tsc --noEmit - no new errors introduced
  • Cross-language hash parity confirmed via a standalone side-by-side run against the launcher's real hash code
  • Run pnpm backfill-mod-hashes against a real deploy after merging
  • End-to-end with the companion launcher PR: fresh Ranked install, confirm the launcher's live hash matches what this server now stores

🤖 Generated with Claude Code

The launcher no longer deploys mods as zips - Steamodded mounting a
.zip via NFS.mount() didn't behave correctly for every mod in
practice, so it now deploys real extracted folders instead (see the
companion launcher-repo PR). The "approved hash" this server computes
has to match what the launcher actually verifies against, so it
switches too: hash a mod's flattened content directly instead of
assembling and hashing a deterministic zip of it.

- New mod-folder-hash.ts: computeModFolderHash() is a byte-for-byte
  Node port of the launcher's ModFileHash::hashDirectory() - sorted
  relative POSIX paths + file bytes, fed into one running SHA-256.
  Verified against the launcher side with a real cross-language
  parity check (not just each side's own unit tests): a standalone
  build of the actual modfilehash.cpp and a Node script calling this
  file produced identical digests for the same sample mod tree, both
  before and after renaming the tree's own root folder (which
  deliberately doesn't factor into the hash on either side, unlike the
  old zip-based scheme where the two had to agree on folder naming for
  the resulting hash to match at all).
- mods-sync.service.ts: computePreparedZipHash() -> renamed
  computeModFolderHashForRelease(), now calls computeModFolderHash()
  on the flattened extracted directory directly instead of shelling
  out to modzip and hashing the assembled archive's bytes.
- Deleted native/modzip/ and its Dockerfile build step entirely - no
  native C binary/libzip to compile or ship anymore.
- mod-archive-flatten.ts/backfill-mod-hashes.ts doc comments updated
  to match; relocateModRoot() itself is unchanged (still needed to
  produce the same flattened layout the launcher's own extraction
  step would).
- New mod-folder-hash.test.ts: hash-vs-hand-computed-SHA-256, root-name
  independence, enumeration-order independence, content/rename
  sensitivity. All passing.

No DB schema change - modRegistry.latestSha256/modRegistryVersions.sha256
stay hex-sha256 columns, just fed different input bytes now. Every
hash stored under the old algorithm is wrong under this one -
`pnpm backfill-mod-hashes` needs to run once after this deploys (see
backfill-mod-hashes.ts, unchanged, already idempotent).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant