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
- Inspect the zip for duplicates (`unzip -l pack.zip | sort | uniq -d`) and rebuild it without duplicates.
- Re-export the theme pack using ThemePackExporter to guarantee a clean archive.
- 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
- Never append-update zips; rewrite them from scratch.
- Track written entry names in a Set when generating archives.
- Sanity-check repacked archives with unzip -l | uniq -d.
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
- Duplicate theme-pack zip entry:
- Theme pack does not contain
- Installed theme-pack file is missing:
- Theme-pack entry name is not normalized
- Theme pack does not contain
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)