HMCL-dev/HMCL · error · IOException

World zip malformed

Error message

World zip malformed

What it means

World.install throws IOException("World zip malformed") when the zip has no root-level level.dat and its top level does not contain exactly one subdirectory. HMCL needs an unambiguous world root (either level.dat at '/' or a single top-level folder) to know what to extract.

Solutions

  1. Repackage the zip so level.dat is at the root, or so exactly one top-level directory contains it
  2. Remove extraneous root entries (readme files, .DS_Store, extra folders) from the archive
  3. Flatten double-nested archives (zip/world/level.dat -> zip/level.dat)
  4. If several worlds are bundled, split them into one zip per world

Example fix

// before: zip layout: [WorldA/, WorldB/, readme.txt]
new World(Path.of("bundle.zip")).install(savesDir, "x"); // malformed
// after: one zip per world with level.dat at root
// bundle.zip layout: [level.dat, region/, ...]
new World(Path.of("worldA.zip")).install(savesDir, "WorldA");
Defensive patterns

Strategy: validation

Validate before calling

static boolean installableZipLayout(Path zip) throws IOException {
    try (FileSystem fs = CompressingUtils.readonly(zip).setAutoDetectEncoding(true).build()) {
        if (Files.isRegularFile(fs.getPath("/level.dat"))) return true;
        try (Stream<Path> s = Files.list(fs.getPath("/"))) {
            List<Path> top = s.filter(p -> !p.getFileName().toString().startsWith(".")).toList();
            return top.size() == 1 && Files.isDirectory(top.get(0));
        }
    }
}

Try / catch

try {
    world.install(savesDir, name);
} catch (IOException e) {
    if (String.valueOf(e.getMessage()).equals("World zip malformed")) {
        // repackage the zip with level.dat at root or a single top-level folder
    }
}

Prevention

When it happens

Trigger: Calling world.install on a zip that lacks /level.dat and whose '/' listing yields 0 or 2+ entries (multiple top-level folders, or loose files plus folders, or an empty zip).

Common situations: Zips created by selecting several world folders at once, archives with a README or .DS_Store sitting next to the world folder in the root, archives where the world is nested two levels deep (world/level.dat under an extra folder), or empty zips.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10). Data as JSON: /api/errors/35f1779e3cd40ff0. Report an issue: GitHub.

Appendix: source

Thrown at HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java:341

            throw new IOException(e);
        }

        if (Files.isDirectory(worldDir)) {
            throw new FileAlreadyExistsException("World already exists");
        }

        if (Files.isRegularFile(file)) {
            try (FileSystem fs = CompressingUtils.readonly(file).setAutoDetectEncoding(true).build()) {
                Path levelDatPath = fs.getPath("/level.dat");
                if (Files.isRegularFile(levelDatPath)) {
                    fileName = FileUtils.getName(file);

                    new Unzipper(file, worldDir).unzip();
                } else {
                    try (Stream<Path> stream = Files.list(fs.getPath("/"))) {
                        List<Path> subDirs = stream.toList();
                        if (subDirs.size() != 1) {
                            throw new IOException("World zip malformed");
                        }
                        String subDirectoryName = FileUtils.getName(subDirs.get(0));
                        new Unzipper(file, worldDir)
                                .setSubDirectory("/" + subDirectoryName + "/")
                                .unzip();
                    }
                }

            }
            new World(worldDir).rename(name);
        } else if (Files.isDirectory(file)) {
            FileUtils.copyDirectory(file, worldDir);
        }
    }

    public void export(Path zip, String worldName) throws IOException {
        if (!Files.isDirectory(file))
            throw new IOException();

View on GitHub (pinned to 24702dc5a0)