Fix reading multi-sector files whose sectors are stored verbatim - #39
Open
dmang-dev wants to merge 1 commit into
Open
Fix reading multi-sector files whose sectors are stored verbatim#39dmang-dev wants to merge 1 commit into
dmang-dev wants to merge 1 commit into
Conversation
MPQ_FILE_COMPRESS on a block entry means the file has a sector offset table, not that every sector is compressed. Storm deflates each sector independently and keeps the compressed form only when it is smaller, so an incompressible file carries MPQ_FILE_COMPRESS while every one of its sectors is stored as-is. Whether a sector is compressed therefore has to be decided by comparing its stored size against the plain size of that sector, which is a full sector for every sector but the last. read_file compared it against sector_bytes_left, the total the file still owes, which is larger than the sector for every sector except the last one. Verbatim sectors in a multi-sector file were handed to decompress(), which read the first byte as a compression mask and raised "Unsupported compression type". This generalises eagleflogh-26. That change fixed the same problem for the final sector and its description already cited the right rule from StormLib ("they cut down the expected sector size if the last sector isn't big enough"), but the comparison it shipped only reduces to that rule when the sector is the last one. Using min(sector_size, sector_bytes_left) covers every sector and is identical to eagleflogh-26 for the last, so test/last_sector_compression.s2ma still reads unchanged. See StormLib src/SFileReadFile.cpp: dwBytesInThisSector is clamped to the bytes remaining (lines 108-117) and a sector is only decompressed when dwRawBytesInThisSector < dwBytesInThisSector (line 165). Also fixes the sector count, which used size // sector_size + 1 and so over-counted by one when the file size was an exact multiple of the sector size. StormLib uses ((size - 1) / sector_size) + 1. Tests build small archives in a temp file rather than adding a binary fixture, so the layouts under test are visible in the diff. They cover verbatim, deflated and mixed multi-sector files, an exact multiple of the sector size, a single short sector and an empty file. Without the fix, test_sectors_roundtrip and test_exact_multiple_of_sector_size fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
MPQ_FILE_COMPRESSon a block entry means the file has a sector offset table, not that every sector is compressed. Storm deflates each sector independently and keeps the compressed form only when it is actually smaller, so an incompressible file carriesMPQ_FILE_COMPRESSwhile every one of its sectors is stored as-is.Deciding whether a sector is compressed therefore has to compare its stored size against the plain size of that sector, which is a full sector for every sector but the last.
read_filecompared it againstsector_bytes_left, the total the file still owes:That is larger than the sector for every sector except the last one, so verbatim sectors in any multi-sector file get handed to
decompress(), which reads the first data byte as a compression mask and raisesUnsupported compression type: <whatever that byte was>.Relationship to #26
This generalises #26 rather than reverting it. That change fixed the same class of problem for the final sector, and its description already cited the correct rule from StormLib — "they cut down the expected sector size if the last sector isn't big enough" — but the comparison it shipped only reduces to that rule when the sector happens to be the last one.
min(sector_size, sector_bytes_left)is that description implemented for every sector. For the last sector the two expressions are identical, sotest/last_sector_compression.s2mareads back unchanged.For reference, in StormLib's
src/SFileReadFile.cpp,dwBytesInThisSectoris clamped to the bytes remaining (lines 108–117) and a sector is decompressed only whendwRawBytesInThisSector < dwBytesInThisSector(line 165).Also fixed
The sector count used
size // sector_size + 1, which over-counts by one when the file size is an exact multiple of the sector size. That makespositionsone entry too long, reading 4 bytes of sector data as a table entry. StormLib uses((size - 1) / sector_size) + 1.Tests
test/test_multi_sector.pybuilds small archives in a temp file rather than adding another binary fixture, so the layouts under test are visible in the diff and reviewable.test/mpqbuilder.pyis a minimal writer for that purpose (~150 lines: crypt table, hash/block tables, sector offset table; contributed under the project's BSD licence).Covered: verbatim, deflated and mixed multi-sector files, a file that is an exact multiple of the sector size, a single short sector, and an empty file. One test also asserts the incompressible fixture really did end up stored verbatim, so the test cannot silently stop exercising the path it targets.
Without the fix,
test_sectors_roundtripandtest_exact_multiple_of_sector_sizefail withUnsupported compression type: 229. With it, the full suite passes (8 passed), including the two pre-existing fixtures.How this turned up
Extracting the scenarios from a StarCraft 64 cartridge and repackaging them as PC StarCraft maps. Their scenario data is incompressible enough that every sector ends up stored verbatim, so all 96 maps were rejected by mpyq while StormLib read them fine. The same would happen for any archive holding a multi-sector file that does not compress.
🤖 Generated with Claude Code