HMCL-dev/HMCL · error · IOException

Duplicate theme-pack entry

Error message

Duplicate theme-pack entry: ${entryName}

What it means

During theme-pack validation, ThemePackManager collects normalized entry names in a set; when a second entry shares an already-seen name, add() returns false and this IOException is thrown. Zip files technically allow duplicate entries, but duplicates make extraction ambiguous, so the archive is rejected.

Solutions

  1. Inspect the zip for duplicates (`unzip -l pack.zip | sort | uniq -d`) and rebuild it without duplicates.
  2. Re-export the theme pack using ThemePackExporter to guarantee a clean archive.
  3. If producing zips programmatically, track entry names in a Set and skip/overwrite instead of writing duplicates.

Example fix

// before
for (Path f : files) zip.addEntry(f.toString(), bytes); // may duplicate
// after
Set<String> seen = new HashSet<>();
for (Path f : files) {
    String name = "/" + root.relativize(f);
    if (seen.add(name)) zip.addEntry(name, bytes);
}
Defensive patterns

Strategy: validation

Validate before calling

Set<String> seen = new HashSet<>();
try (ZipFile z = new ZipFile(pack)) {
    for (var e : Collections.list(z.entries())) {
        if (!seen.add(e.getName().replace('\\', '/'))) throw new IllegalArgumentException("duplicate entry: " + e.getName());
    }
}

Try / catch

try {
    ThemePackManager.install(pack, dir);
} catch (IOException e) {
    if (e.getMessage().contains("Duplicate theme-pack entry")) {
        pack = rebuildWithoutDuplicates(pack);
        ThemePackManager.install(pack, dir);
    } else throw e;
}

Prevention

When it happens

Trigger: Installing a theme-pack zip that contains two entries with the same normalized name (e.g. 'assets/bg.png' twice, or 'assets/bg.png' plus 'assets\bg.png' after normalization).

Common situations: Zips updated by appending a new version of a file without removing the old entry; archives concatenated from two packs; repacking scripts that add an entry twice.

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


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

Appendix: source

Thrown at HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManager.java:1372

    }

    /// Validates all zip entries in a theme-pack file.
    private static void validateThemePackFile(Path themePackFile) throws IOException {
        Set<String> entries = new HashSet<>();
        boolean hasManifest = false;

        try (ZipArchiveReader zipFile = new ZipArchiveReader(themePackFile, StandardCharsets.UTF_8)) {
            for (ZipArchiveEntry entry : zipFile.getEntries()) {
                String rawEntryName = entry.getName();
                String entryName = normalizeThemePackEntryName(rawEntryName);
                String canonicalEntryName = entry.isDirectory() ? entryName + "/" : entryName;
                if (!canonicalEntryName.equals(rawEntryName)) {
                    throw new IOException("Theme-pack entry name is not normalized: " + rawEntryName);
                }
                checkSupportedThemePackEntry(entryName);

                if (!entries.add(entryName)) {
                    throw new IOException("Duplicate theme-pack entry: " + entryName);
                }
                if (ThemePackExporter.MANIFEST_ENTRY.equals(entryName) && !entry.isDirectory()) {
                    hasManifest = true;
                }
            }
        }

        if (!hasManifest) {
            throw new IOException("Theme pack does not contain " + ThemePackExporter.MANIFEST_ENTRY);
        }
    }

    /// Moves a file into place, using an atomic move when the platform supports it.
    private static void moveReplacing(Path source, Path target) throws IOException {
        try {
            Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
        } catch (AtomicMoveNotSupportedException e) {
            Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);

View on GitHub (pinned to 24702dc5a0)