{"record":{"id":"7bf03e9ae99d170a","repo":"HMCL-dev/HMCL","slug":"theme-pack-entry-must-be-relative-entryname","errorCode":null,"errorMessage":"Theme-pack entry must be relative: ${entryName}","messagePattern":"Theme-pack entry must be relative: (.+?)","errorType":"validation","errorClass":"IOException","httpStatus":null,"severity":"error","filePath":"HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManager.java","lineNumber":1406,"sourceCode":"            Files.move(source, target, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);\n        } catch (AtomicMoveNotSupportedException e) {\n            Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);\n        }\n    }\n\n    /// Returns a normalized and safe theme-pack zip entry name.\n    private static String normalizeThemePackEntryName(String entryName) throws IOException {\n        Objects.requireNonNull(entryName);\n\n        String normalized = entryName.trim().replace('\\\\', '/');\n        if (normalized.endsWith(\"/\")) {\n            normalized = normalized.substring(0, normalized.length() - 1);\n        }\n        if (normalized.isEmpty()) {\n            throw new IOException(\"Theme-pack entry is empty\");\n        }\n        if (normalized.startsWith(\"/\") || normalized.matches(\"^[A-Za-z]:.*\")) {\n            throw new IOException(\"Theme-pack entry must be relative: \" + entryName);\n        }\n\n        for (String segment : normalized.split(\"/\")) {\n            if (segment.isEmpty() || \".\".equals(segment) || \"..\".equals(segment)) {\n                throw new IOException(\"Theme-pack entry contains an unsafe segment: \" + entryName);\n            }\n            for (int i = 0; i < segment.length(); i++) {\n                char ch = segment.charAt(i);\n                if (Character.isISOControl(ch) || ch == '\\0') {\n                    throw new IOException(\"Theme-pack entry contains a control character: \" + entryName);\n                }\n            }\n        }\n        return normalized;\n    }\n\n    /// Checks that a theme-pack zip entry belongs to the current file layout.\n    private static void checkSupportedThemePackEntry(String entryName) throws IOException {","sourceCodeStart":1388,"sourceCodeEnd":1424,"githubUrl":"https://github.com/HMCL-dev/HMCL/blob/24702dc5a0214034f4c27166d5fd30cad08cec19/HMCL/src/main/java/org/jackhuang/hmcl/theme/ThemePackManager.java#L1388-L1424","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nzip.putNextEntry(new ZipArchiveEntry(sourceFile.getAbsolutePath()));\n// after\nzip.putNextEntry(new ZipArchiveEntry(packRoot.relativize(sourceFile).toString().replace('\\\\', '/')));","handlingStrategy":"validation","validationCode":"for (var e : Collections.list(new ZipFile(pack).entries())) {\n    String n = e.getName().replace('\\\\', '/');\n    if (n.startsWith(\"/\") || n.matches(\"^[A-Za-z]:.*\")) throw new IllegalArgumentException(\"absolute entry path: \" + n);\n}","typeGuard":null,"tryCatchPattern":"try {\n    ThemePackManager.install(pack, dir);\n} catch (IOException e) {\n    if (e.getMessage().contains(\"must be relative\")) {\n      pack = relativizeEntries(pack);\n      ThemePackManager.install(pack, dir);\n    } else throw e;\n}","preventionTips":["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."],"tags":["zip","theme-pack","path-validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"24702dc5a0214034f4c27166d5fd30cad08cec19","analyzedAt":"2026-09-10T12:36:46.680Z","contentChangedAt":"2026-09-10T12:36:46.680Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}