HMCL-dev/HMCL · error · IOException
Theme-pack entry must be relative
Error message
Theme-pack entry must be relative: ${entryName} What it means
After normalization, an entry name starting with '/' or matching a Windows drive prefix (e.g. 'C:') is absolute, and ThemePackManager rejects it with this IOException. Theme-pack entries must be relative paths so extraction can be safely rooted inside the pack directory.
Solutions
- Repack so entry names are relative to the archive root (e.g. 'assets/bg.png'), not absolute paths.
- Use ThemePackExporter to produce the archive with guaranteed-relative entry names.
- When zipping from a CLI, cd into the pack root and zip relative paths.
Example fix
// before
zip.putNextEntry(new ZipArchiveEntry(sourceFile.getAbsolutePath()));
// after
zip.putNextEntry(new ZipArchiveEntry(packRoot.relativize(sourceFile).toString().replace('\\', '/'))); Defensive patterns
Strategy: validation
Validate before calling
for (var e : Collections.list(new ZipFile(pack).entries())) {
String n = e.getName().replace('\\', '/');
if (n.startsWith("/") || n.matches("^[A-Za-z]:.*")) throw new IllegalArgumentException("absolute entry path: " + n);
} Try / catch
try {
ThemePackManager.install(pack, dir);
} catch (IOException e) {
if (e.getMessage().contains("must be relative")) {
pack = relativizeEntries(pack);
ThemePackManager.install(pack, dir);
} else throw e;
} Prevention
- Relativize file paths against the pack root before zipping.
- Never pass absolute OS paths to zip entry APIs.
- Build archives from a fixed working directory with relative paths.
When it happens
Trigger: Installing a zip whose entries were added from absolute paths (e.g. 'C:/Users/me/assets/bg.png' or '/home/me/assets/bg.png') without relativizing.
Common situations: Packing scripts on Windows that pass full drive-letter paths to the zip tool; Unix zips created with absolute paths via `zip /abs/path` semantics; repacked archives from other applications.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Duplicate theme-pack entry
- Duplicate theme-pack zip entry:
- Installed theme-pack file is missing:
- Theme-pack asset is missing
- Theme pack does not contain
AI-assisted analysis of HMCL-dev/HMCL@24702dc5a0 (2026-09-10).
Data as JSON: /api/errors/7bf03e9ae99d170a.
Report an issue: GitHub.
Appendix: source
Thrown at HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManager.java:1406
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
}
}
/// Returns a normalized and safe theme-pack zip entry name.
private static String normalizeThemePackEntryName(String entryName) throws IOException {
Objects.requireNonNull(entryName);
String normalized = entryName.trim().replace('\\', '/');
if (normalized.endsWith("/")) {
normalized = normalized.substring(0, normalized.length() - 1);
}
if (normalized.isEmpty()) {
throw new IOException("Theme-pack entry is empty");
}
if (normalized.startsWith("/") || normalized.matches("^[A-Za-z]:.*")) {
throw new IOException("Theme-pack entry must be relative: " + entryName);
}
for (String segment : normalized.split("/")) {
if (segment.isEmpty() || ".".equals(segment) || "..".equals(segment)) {
throw new IOException("Theme-pack entry contains an unsafe segment: " + entryName);
}
for (int i = 0; i < segment.length(); i++) {
char ch = segment.charAt(i);
if (Character.isISOControl(ch) || ch == '\0') {
throw new IOException("Theme-pack entry contains a control character: " + entryName);
}
}
}
return normalized;
}
/// Checks that a theme-pack zip entry belongs to the current file layout.
private static void checkSupportedThemePackEntry(String entryName) throws IOException {View on GitHub (pinned to 24702dc5a0)