Summary
When repacking a bundle with env.file.save(packer="lz4"), a data chunk whose LZ4-compressed size
is exactly equal to its uncompressed size is still flagged as LZ4-compressed instead of
being stored uncompressed. This produces a UnityFS data block where compressedSize == uncompressedSize but the block flag = LZ4 (2/3).
Standard Unity tolerates such a block, but stricter runtimes reject it — e.g. Unity WebGL on the
Tuanjie engine (团结引擎) fails to mount the bundle with:
The file 'archive:/CAB-xxx/CAB-xxx' is corrupted! Remove it and launch unity again! [Position out of
bounds!]
Could not allocate memory: System out of memory! Trying to allocate <huge>B
Root cause
UnityPy/helpers/CompressionHelper.py → chunk_based_compress() (used by BundleFile.save):
if len(compressed_data) > chunk_size: # <-- should be >=
# store this chunk uncompressed, clear the compression flag
compressed_file_data.extend(data[p : p + chunk_size])
block_info.append((chunk_size, chunk_size, block_info_flag ^ switch))
else:
# keep LZ4 data + LZ4 flag
...
The fallback-to-stored only triggers when the compressed output is strictly larger than the
input. When LZ4 lands exactly at break-even (len(compressed_data) == chunk_size), the >
check is false, so the chunk keeps the LZ4 flag even though there is no actual compression —
yielding compressedSize == uncompressedSize on an LZ4 block.
Unity's own packer (and the safe behavior) is to store the chunk raw whenever the compressed result
is not smaller, i.e. >=.
When it triggers
Rare but real: it needs a 128KB chunk that LZ4 compresses to exactly its original size. This
happens on high-entropy data — e.g. chunks dominated by already-compressed texture payloads
(crunched DXT/DXT5Crunched). The crunch payload is already maximally compressed, so the outer LZ4
can't shrink the chunk and occasionally lands exactly on break-even.
In our case (ASTC→DXT(Crunch) repack, ~10k bundles), 3 bundles hit this, and they crash on Tuanjie
WebGL while every other bundle loads fine.
Suggested fix
Change the comparison to >= so a chunk that doesn't actually shrink is stored uncompressed:
if len(compressed_data) >= chunk_size:
compressed_file_data.extend(data[p : p + chunk_size])
block_info.append((chunk_size, chunk_size, block_info_flag ^ switch))
else:
...
This is a harmless robustness improvement (a break-even chunk gains nothing from compression anyway)
and matches Unity's native packing behavior.
Environment
- UnityPy version:
- Bundle: UnityFS, LZ4 (
packer="lz4"), Unity
- Failing runtime: Unity WebGL / Tuanjie engine (团结引擎)
Summary
When repacking a bundle with
env.file.save(packer="lz4"), a data chunk whose LZ4-compressed sizeis exactly equal to its uncompressed size is still flagged as LZ4-compressed instead of
being stored uncompressed. This produces a UnityFS data block where
compressedSize == uncompressedSizebut the block flag = LZ4 (2/3).Standard Unity tolerates such a block, but stricter runtimes reject it — e.g. Unity WebGL on the
Tuanjie engine (团结引擎) fails to mount the bundle with:
Root cause
UnityPy/helpers/CompressionHelper.py→chunk_based_compress()(used byBundleFile.save):The fallback-to-stored only triggers when the compressed output is strictly larger than the
input. When LZ4 lands exactly at break-even (
len(compressed_data) == chunk_size), the>check is false, so the chunk keeps the LZ4 flag even though there is no actual compression —
yielding
compressedSize == uncompressedSizeon an LZ4 block.Unity's own packer (and the safe behavior) is to store the chunk raw whenever the compressed result
is not smaller, i.e.
>=.When it triggers
Rare but real: it needs a 128KB chunk that LZ4 compresses to exactly its original size. This
happens on high-entropy data — e.g. chunks dominated by already-compressed texture payloads
(crunched DXT/DXT5Crunched). The crunch payload is already maximally compressed, so the outer LZ4
can't shrink the chunk and occasionally lands exactly on break-even.
In our case (ASTC→DXT(Crunch) repack, ~10k bundles), 3 bundles hit this, and they crash on Tuanjie
WebGL while every other bundle loads fine.
Suggested fix
Change the comparison to
>=so a chunk that doesn't actually shrink is stored uncompressed:This is a harmless robustness improvement (a break-even chunk gains nothing from compression anyway)
and matches Unity's native packing behavior.
Environment
packer="lz4"), Unity