diff --git a/src/main/java/com/dostalgia/UploadResource.java b/src/main/java/com/dostalgia/UploadResource.java index 73523d4..ef7a8b1 100644 --- a/src/main/java/com/dostalgia/UploadResource.java +++ b/src/main/java/com/dostalgia/UploadResource.java @@ -296,47 +296,46 @@ public class UploadResource { // ZIP it up directly from extractDir (no temp copy) try (var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(bundlePath))) { - var addedDirs = new java.util.HashSet(); - // Helper: write a directory entry and all its ancestors if not yet added - // (Emscripten FS requires every directory in the chain as explicit entries) - java.util.function.Consumer ensureDir = entryName -> { - int idx = entryName.lastIndexOf('/'); - while (idx >= 0) { - String parent = entryName.substring(0, idx + 1); - if (addedDirs.add(parent)) { + // ── Phase 1: Collect all directory paths from the file tree ── + // js-dos C code (jsdos-libzip.c) iterates ZIP entries linearly and + // calls mkdir() on each directory entry. All parents must come + // before their children or mkdir() fails with exit(1). + var allDirs = new java.util.TreeSet(); + try (var walk = Files.walk(extractDir)) { + walk.filter(Files::isRegularFile).forEach(f -> { + String entryName = extractDir.relativize(f).toString().replace('\\', '/'); + int idx = entryName.lastIndexOf('/'); + while (idx >= 0) { + allDirs.add(entryName.substring(0, idx + 1)); + idx = entryName.lastIndexOf('/', idx - 1); + } + }); + } + + // ── Phase 2: Write ALL directory entries first (sorted → shallowest first) ── + for (String dir : allDirs) { + zos.putNextEntry(new java.util.zip.ZipEntry(dir)); + zos.closeEntry(); + } + + // ── Phase 3: .jsdos config files (must come before game files) ── + try (var walk = Files.walk(jsdos)) { + walk.filter(Files::isRegularFile) + .sorted() + .forEach(f -> { try { - zos.putNextEntry(new java.util.zip.ZipEntry(parent)); + String entryName = extractDir.relativize(f).toString().replace('\\', '/'); + zos.putNextEntry(new java.util.zip.ZipEntry(entryName)); + Files.copy(f, zos); zos.closeEntry(); } catch (IOException e) { throw new UncheckedIOException(e); } - } - idx = entryName.lastIndexOf('/', idx - 1); - } - }; - - // Helper: write a single file entry with its directory chain - java.util.function.Consumer writeFile = f -> { - try { - String entryName = extractDir.relativize(f).toString().replace('\\', '/'); - ensureDir.accept(entryName); - zos.putNextEntry(new java.util.zip.ZipEntry(entryName)); - Files.copy(f, zos); - zos.closeEntry(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }; - - // ── First pass: .jsdos config files (must come first in ZIP) ── - try (var walk = Files.walk(jsdos)) { - walk.filter(Files::isRegularFile) - .sorted() // deterministic order - .forEach(writeFile); + }); } - // ── Second pass: all other game files ── + // ── Phase 4: All other game files ── try (var walk = Files.walk(extractDir)) { walk.filter(Files::isRegularFile) .filter(f -> !f.startsWith(jsdos)) @@ -345,8 +344,17 @@ public class UploadResource { int dot = n.lastIndexOf('.'); return dot < 0 || !SKIP_EXT.contains(n.substring(dot)); }) - .sorted() // deterministic order - .forEach(writeFile); + .sorted() + .forEach(f -> { + try { + String entryName = extractDir.relativize(f).toString().replace('\\', '/'); + zos.putNextEntry(new java.util.zip.ZipEntry(entryName)); + Files.copy(f, zos); + zos.closeEntry(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } }