Skip to content

Commit bef337c

Browse files
authored
Add ability to fix zip corruption (#87)
`@turbowarp/[email protected]` now includes a recovery mode where it tries to find file entries by scanning the zip if the central directory is missing. The way that JSZip is really nice as it puts all the sizes and everything directly in the local header, so we don't need to do a lot of guesswork. If we had to support a bunch of random zip generators, this would be more work. The sb3fix changes are to enable this mode, add logs, and remove files that have mismatched uncompressed data sizes so that regular Scratch will be able to load it. These projects now get automatically fixed instead of waiting for me to run `zip -FF` manually: - All 3 projects in #82 (comment) - #80 - #59 - #32 - #31 - #30 - #28 - #7 It probably would've automatically fixed these but the links no longer work, so can't verify for certain: - #81 - #50 - #22 - #5 - #4 - #3
1 parent 7792865 commit bef337c

File tree

6 files changed

+46
-6
lines changed

6 files changed

+46
-6
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
},
2222
"homepage": "https://github.com/TurboWarp/sb3fix#readme",
2323
"dependencies": {
24-
"@turbowarp/jszip": "^3.11.1"
24+
"@turbowarp/jszip": "^3.12.0"
2525
},
2626
"devDependencies": {
2727
"copy-webpack-plugin": "^13.0.0",

src/sb3fix.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,10 +609,46 @@ const fixJSON = (data, options = {}) => {
609609
* @returns {Promise<Uint8Array>} A promise that resolves to a fixed compressed .sb3 file.
610610
*/
611611
const fixZip = async (data, options = {}) => {
612+
/**
613+
* @param {string} message
614+
*/
615+
const log = (message) => {
616+
if (options.logCallback) {
617+
options.logCallback(message);
618+
}
619+
};
620+
612621
// JSZip is not a small library, so we'll load it somewhat lazily.
613622
const JSZip = require('@turbowarp/jszip');
614623

615-
const zip = await JSZip.loadAsync(data);
624+
let zip = await JSZip.loadAsync(data, {
625+
recoverCorrupted: true,
626+
onCorruptCentralDirectory: (error) => {
627+
log(`zip had corrupt central directory: ${error}`);
628+
},
629+
onUnrecoverableFileEntry: (error) => {
630+
log(`zip had unrecoverable file entry: ${error}`);
631+
}
632+
});
633+
634+
/** @type {Array<[string, import("@turbowarp/jszip").JSZipObject]>} */
635+
const zipFiles = [];
636+
zip.forEach((relativePath, file) => {
637+
zipFiles.push([relativePath, file]);
638+
});
639+
640+
// Remove any unreadable files from the zip. This can notably happen if the compressed data in the zip was
641+
// corrupted, which would make the uncompressed data size field not match. Scratch/JSZip will refuse to
642+
// keep loading the project if that happens. If we remove the asset, at least there's a chance it can now
643+
// be downloaded from the asset server instead.
644+
for (const [relativePath, file] of zipFiles) {
645+
try {
646+
await file.async('uint8array');
647+
} catch (error) {
648+
log(`zip had unreadable file ${relativePath}: ${error}`);
649+
zip.remove(relativePath);
650+
}
651+
}
616652

617653
// json is not guaranteed to be stored in the root.
618654
const jsonFile = zip.file(/(?:project|sprite)\.json/)[0];
957 Bytes
Binary file not shown.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
zip had corrupt central directory: Error: Corrupted zip: can't find end of central directory
2+
zip had unrecoverable file entry: Error: End of data reached (data length = 1000, asked index = 2194). Corrupted zip ?
3+
checking target 0
4+
checking target 1

tests/samples/very-truncated.sb3

1000 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)