fix: close Windows extraction namespace gaps

Reject alternate data streams, reserved Win32 device names, and trailing-dot or trailing-space aliases before internal ZIP, JVM, or native extraction writes. Preflight native archive entry lists, reconcile opened JVM outputs to partial or removed state on abnormal results, and replace hardlink-based owner markers with portable exclusive reservation plus atomic replacement while allowing direct scoped extraction to continue when marker persistence is unavailable.
This commit is contained in:
Sucukdeluxe
2026-08-22 15:40:59 +02:00
parent 11d63dd2be
commit 6724b3605c
19 changed files with 566 additions and 54 deletions
@@ -668,20 +668,25 @@ public final class JBindExtractorMain {
return output;
}
private static String normalizeEntryName(String value, String fallback) {
String entry = value == null ? "" : value.trim();
if (entry.length() == 0) {
return fallback;
}
entry = entry.replace('\\', '/');
private static String normalizeEntryName(String value, String fallback) {
String entry = value == null ? "" : value;
if (entry.trim().length() == 0) {
return fallback;
}
entry = entry.replace('\\', '/');
while (entry.startsWith("./")) {
entry = entry.substring(2);
}
if (entry.length() == 0) {
return fallback;
}
String[] segments = entry.split("/", -1);
if (entry.length() == 0) {
return fallback;
}
while (entry.endsWith("/")) {
entry = entry.substring(0, entry.length() - 1);
}
validateWindowsEntryName(entry);
String[] segments = entry.split("/", -1);
StringBuilder sanitized = new StringBuilder();
for (int i = 0; i < segments.length; i++) {
if (i > 0) {
@@ -693,8 +698,33 @@ public final class JBindExtractorMain {
if (entry.length() == 0) {
return fallback;
}
return entry;
}
return entry;
}
private static void validateWindowsEntryName(String entry) {
if (entry.startsWith("/") || entry.matches("^[a-zA-Z]:.*")) {
throw new IllegalArgumentException("Ungueltiger Windows-Archivpfad: " + entry);
}
String[] segments = entry.split("/", -1);
for (String segment : segments) {
if (segment.length() == 0 || ".".equals(segment) || "..".equals(segment)
|| segment.endsWith(".") || segment.endsWith(" ")
|| WINDOWS_SPECIAL_CHARS_RE.matcher(segment).find()) {
throw new IllegalArgumentException("Ungueltiger Windows-Archivpfad: " + entry);
}
for (int i = 0; i < segment.length(); i++) {
if (segment.charAt(i) < 32) {
throw new IllegalArgumentException("Ungueltiger Windows-Archivpfad: " + entry);
}
}
int dot = segment.indexOf('.');
String base = (dot >= 0 ? segment.substring(0, dot) : segment).toUpperCase(Locale.ROOT);
if ("CON".equals(base) || "PRN".equals(base) || "AUX".equals(base) || "NUL".equals(base)
|| base.matches("COM[1-9¹²³]") || base.matches("LPT[1-9¹²³]")) {
throw new IllegalArgumentException("Reservierter Windows-Archivpfad: " + entry);
}
}
}
private static long safeSize(Long value) {
if (value == null) {