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

  1. Repack so entry names are relative to the archive root (e.g. 'assets/bg.png'), not absolute paths.
  2. Use ThemePackExporter to produce the archive with guaranteed-relative entry names.
  3. 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

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


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)