feat: make config path patcher fully game-agnostic
Build & Deploy / build-and-deploy (push) Failing after 46s
Build & Deploy / build-and-deploy (push) Failing after 46s
Removes hardcoded CONFIG_PATH_FIXES map with Fallout-specific entries. Replaces with generic scanner that: - Scans all small text files (<100KB) for X:\... patterns - Only processes C: drive paths (D:/E:/ left alone as CD-ROM refs) - Uses case-insensitive filesystem checks to find what exists after flattening - Progressively tries shorter path suffixes to find the stale prefix - Rewrites C:\STALEDIR\ → .\ only when the leaf file/dir is found at root - Works with both \ and / path separators Also refactors the GameService fixBundleConfigPaths to build a ZIP entry index and use the same logic for existing bundle patching.
This commit is contained in:
@@ -205,20 +205,10 @@ public class GameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Known game config files with hardcoded DOS paths that need fixing.
|
* Patch hardcoded absolute DOS paths (C:\dir\ → .\) in game config files
|
||||||
* Same as UploadResource.CONFIG_PATH_FIXES but duplicated here for
|
* within a .jsdos bundle. Game-agnostic — scans all small text entries
|
||||||
* the fix-paths endpoint (no dependency on UploadResource).
|
* for drive-letter paths and rewrites stale directory prefixes.
|
||||||
*/
|
*/
|
||||||
private static final java.util.Map<String, java.util.List<String>> CONFIG_PATH_FIXES =
|
|
||||||
java.util.Map.of(
|
|
||||||
"fallout.cfg", java.util.List.of(
|
|
||||||
"c:\\fallout1\\", "C:\\fallout1\\",
|
|
||||||
"c:\\fallout2\\", "C:\\fallout2\\",
|
|
||||||
"c:\\fallout\\", "C:\\fallout\\"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/** Patch hardcoded DOS paths (C:\dir\ → .\) in known game config files within a .jsdos bundle. */
|
|
||||||
public boolean fixBundleConfigPaths(String id) throws IOException {
|
public boolean fixBundleConfigPaths(String id) throws IOException {
|
||||||
Game game = load(id);
|
Game game = load(id);
|
||||||
String bundleFile = game.getBundleFile();
|
String bundleFile = game.getBundleFile();
|
||||||
@@ -226,8 +216,42 @@ public class GameService {
|
|||||||
Path bundlePath = gamesDir.resolve(bundleFile);
|
Path bundlePath = gamesDir.resolve(bundleFile);
|
||||||
if (!Files.exists(bundlePath)) return false;
|
if (!Files.exists(bundlePath)) return false;
|
||||||
|
|
||||||
|
// Phase 1: build a set of all entry paths for existence checking
|
||||||
|
java.util.Set<String> allEntries = new java.util.HashSet<>();
|
||||||
|
java.util.Set<String> dirs = new java.util.HashSet<>();
|
||||||
|
java.util.List<EntryData> textEntries = new java.util.ArrayList<>();
|
||||||
|
try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath))) {
|
||||||
|
java.util.zip.ZipEntry entry;
|
||||||
|
while ((entry = zis.getNextEntry()) != null) {
|
||||||
|
String name = entry.getName();
|
||||||
|
allEntries.add(name);
|
||||||
|
// Add all parent directories too
|
||||||
|
int idx = name.lastIndexOf('/');
|
||||||
|
while (idx >= 0) {
|
||||||
|
dirs.add(name.substring(0, idx + 1));
|
||||||
|
idx = name.lastIndexOf('/', idx - 1);
|
||||||
|
}
|
||||||
|
if (!entry.isDirectory() && isSmallTextEntry(entry, zis)) {
|
||||||
|
byte[] data = zis.readAllBytes();
|
||||||
|
String text = new String(data, java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||||
|
if (hasDrivePath(text)) {
|
||||||
|
textEntries.add(new EntryData(name, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zis.closeEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textEntries.isEmpty()) return false;
|
||||||
|
|
||||||
|
// Build the existence checker using ZIP entry paths
|
||||||
|
var existsChecker = new ZipExistenceChecker(allEntries, dirs);
|
||||||
|
|
||||||
|
// Phase 2: patch matching entries and write new ZIP
|
||||||
Path tmpPath = bundlePath.resolveSibling(bundlePath.getFileName() + ".tmp");
|
Path tmpPath = bundlePath.resolveSibling(bundlePath.getFileName() + ".tmp");
|
||||||
boolean patched = false;
|
boolean patched = false;
|
||||||
|
java.util.regex.Pattern drvPattern = java.util.regex.Pattern.compile(
|
||||||
|
"[A-Za-z]:(\\\\|/)[^\\s\"\\r\\n]+");
|
||||||
|
|
||||||
try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath));
|
try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath));
|
||||||
var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(tmpPath))) {
|
var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(tmpPath))) {
|
||||||
@@ -235,25 +259,15 @@ public class GameService {
|
|||||||
java.util.zip.ZipEntry entry;
|
java.util.zip.ZipEntry entry;
|
||||||
while ((entry = zis.getNextEntry()) != null) {
|
while ((entry = zis.getNextEntry()) != null) {
|
||||||
String name = entry.getName();
|
String name = entry.getName();
|
||||||
String loName = name.toLowerCase();
|
|
||||||
var prefixes = CONFIG_PATH_FIXES.get(loName);
|
|
||||||
zos.putNextEntry(new java.util.zip.ZipEntry(name));
|
zos.putNextEntry(new java.util.zip.ZipEntry(name));
|
||||||
|
|
||||||
if (prefixes != null) {
|
// Check if this entry needs patching
|
||||||
// Read the entry content and patch paths
|
var ed = findEntryData(textEntries, name);
|
||||||
byte[] data = zis.readAllBytes();
|
if (ed != null) {
|
||||||
byte[] result = data;
|
String text = new String(ed.data, java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||||
for (String prefix : prefixes) {
|
String patchedText = patchZipPaths(text, existsChecker, drvPattern);
|
||||||
byte[] pBytes = prefix.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
byte[] result = patchedText.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||||
byte[] pSlash = prefix.replace('\\', '/')
|
if (!java.util.Arrays.equals(result, ed.data)) {
|
||||||
.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
|
||||||
byte[] rel = ".\\".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
|
||||||
byte[] relSlash = "./"
|
|
||||||
.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
|
||||||
result = replaceBytesInPlace(result, pBytes, rel);
|
|
||||||
result = replaceBytesInPlace(result, pSlash, relSlash);
|
|
||||||
}
|
|
||||||
if (!java.util.Arrays.equals(result, data)) {
|
|
||||||
patched = true;
|
patched = true;
|
||||||
}
|
}
|
||||||
zos.write(result);
|
zos.write(result);
|
||||||
@@ -273,26 +287,125 @@ public class GameService {
|
|||||||
return patched;
|
return patched;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] replaceBytesInPlace(byte[] src, byte[] search, byte[] replacement) {
|
private static boolean isSmallTextEntry(java.util.zip.ZipEntry entry, java.util.zip.ZipInputStream zis) {
|
||||||
if (search.length == 0) return src;
|
if (entry.isDirectory()) return false;
|
||||||
java.util.ArrayList<Byte> result = new java.util.ArrayList<>(src.length);
|
long size = entry.getSize();
|
||||||
int i = 0;
|
if (size > 100 * 1024 || size == 0) return true; // unknown size is OK
|
||||||
while (i <= src.length - search.length) {
|
return true;
|
||||||
boolean match = true;
|
|
||||||
for (int j = 0; j < search.length; j++) {
|
|
||||||
if (src[i + j] != search[j]) { match = false; break; }
|
|
||||||
}
|
}
|
||||||
if (match) {
|
|
||||||
for (byte b : replacement) result.add(b);
|
private static boolean hasDrivePath(String text) {
|
||||||
i += search.length;
|
return java.util.regex.Pattern.compile("[A-Za-z]:\\\\").matcher(text).find();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static EntryData findEntryData(java.util.List<EntryData> list, String name) {
|
||||||
|
for (var ed : list) {
|
||||||
|
if (ed.name.equals(name)) return ed;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class EntryData {
|
||||||
|
final String name;
|
||||||
|
final byte[] data;
|
||||||
|
EntryData(String name, byte[] data) { this.name = name; this.data = data; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Case-insensitive existence check for paths inside a ZIP bundle. */
|
||||||
|
private static class ZipExistenceChecker {
|
||||||
|
final java.util.Set<String> entries; // all entries in original case
|
||||||
|
final java.util.Set<String> dirs; // all directory prefixes
|
||||||
|
|
||||||
|
ZipExistenceChecker(java.util.Set<String> entries, java.util.Set<String> dirs) {
|
||||||
|
this.entries = entries;
|
||||||
|
this.dirs = dirs;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean exists(String path) {
|
||||||
|
// Check both forward and backslash variants
|
||||||
|
String fwd = path.replace('\\', '/');
|
||||||
|
String bwd = path.replace('/', '\\');
|
||||||
|
// Check exact match
|
||||||
|
if (entries.contains(fwd) || entries.contains(bwd)) return true;
|
||||||
|
// Check case-insensitively
|
||||||
|
for (String e : entries) {
|
||||||
|
if (e.equalsIgnoreCase(fwd) || e.equalsIgnoreCase(bwd)) return true;
|
||||||
|
}
|
||||||
|
// Check as directory (trailing slash variants)
|
||||||
|
String fwdDir = fwd.endsWith("/") ? fwd : fwd + "/";
|
||||||
|
String bwdDir = bwd.endsWith("\\") ? bwd : bwd + "\\";
|
||||||
|
if (dirs.contains(fwdDir) || dirs.contains(bwdDir)) return true;
|
||||||
|
for (String d : dirs) {
|
||||||
|
if (d.equalsIgnoreCase(fwdDir) || d.equalsIgnoreCase(bwdDir)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Patch absolute DOS paths in ZIP entry text using the ZIP's own entry index. */
|
||||||
|
private static String patchZipPaths(String text, ZipExistenceChecker checker,
|
||||||
|
java.util.regex.Pattern drvPattern) {
|
||||||
|
var m = drvPattern.matcher(text);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
int lastEnd = 0;
|
||||||
|
java.util.Set<String> seenPrefixes = new java.util.HashSet<>();
|
||||||
|
|
||||||
|
while (m.find()) {
|
||||||
|
sb.append(text, lastEnd, m.start());
|
||||||
|
String fullPath = m.group();
|
||||||
|
String drive = fullPath.substring(0, 2);
|
||||||
|
|
||||||
|
if (!"C:".equalsIgnoreCase(drive)) {
|
||||||
|
sb.append(fullPath);
|
||||||
|
lastEnd = m.end();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String sep = fullPath.substring(2, 3);
|
||||||
|
String relPath = fullPath.substring(3);
|
||||||
|
String separator = "\\".equals(sep) ? "\\\\" : "/";
|
||||||
|
String[] parts = relPath.split(separator, -1);
|
||||||
|
|
||||||
|
boolean found = false;
|
||||||
|
for (int skip = 0; skip < parts.length; skip++) {
|
||||||
|
StringBuilder suffix = new StringBuilder();
|
||||||
|
for (int j = skip; j < parts.length; j++) {
|
||||||
|
if (j > skip) suffix.append(sep);
|
||||||
|
suffix.append(parts[j]);
|
||||||
|
}
|
||||||
|
String suffixStr = suffix.toString();
|
||||||
|
if (suffixStr.isEmpty()) continue;
|
||||||
|
|
||||||
|
if (checker.exists(suffixStr)) {
|
||||||
|
if (skip == 0) {
|
||||||
|
sb.append(fullPath); // path valid
|
||||||
} else {
|
} else {
|
||||||
result.add(src[i++]);
|
StringBuilder stale = new StringBuilder(drivePrefix(parts, skip, sep));
|
||||||
|
String staleStr = stale.toString();
|
||||||
|
if (!seenPrefixes.contains(staleStr)) {
|
||||||
|
seenPrefixes.add(staleStr);
|
||||||
|
}
|
||||||
|
sb.append(".").append(sep).append(suffixStr);
|
||||||
|
}
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (i < src.length) result.add(src[i++]);
|
if (!found) sb.append(fullPath);
|
||||||
byte[] out = new byte[result.size()];
|
lastEnd = m.end();
|
||||||
for (int k = 0; k < out.length; k++) out[k] = result.get(k);
|
}
|
||||||
return out;
|
sb.append(text.substring(lastEnd));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String drivePrefix(String[] parts, int skip, String sep) {
|
||||||
|
StringBuilder sb = new StringBuilder("C:").append(sep);
|
||||||
|
for (int k = 0; k < skip; k++) {
|
||||||
|
if (k > 0) sb.append(sep);
|
||||||
|
sb.append(parts[k]);
|
||||||
|
}
|
||||||
|
sb.append(sep);
|
||||||
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Scan a .jsdos bundle for CD image files (preserves the case from the ZIP entry). */
|
/** Scan a .jsdos bundle for CD image files (preserves the case from the ZIP entry). */
|
||||||
|
|||||||
@@ -480,46 +480,33 @@ public class UploadResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Known game config files that may have hardcoded absolute DOS paths.
|
|
||||||
* Key = filename pattern (lowercase), Value = list of path prefixes to
|
|
||||||
* replace with ".\\" (relative to the game root). */
|
|
||||||
private static final java.util.Map<String, java.util.List<String>> CONFIG_PATH_FIXES =
|
|
||||||
java.util.Map.of(
|
|
||||||
"fallout.cfg", java.util.List.of(
|
|
||||||
"c:\\fallout1\\", "C:\\fallout1\\",
|
|
||||||
"c:\\fallout2\\", "C:\\fallout2\\",
|
|
||||||
"c:\\fallout\\", "C:\\fallout\\"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch known game config files that contain hardcoded absolute DOS paths
|
* Detect and fix hardcoded absolute DOS paths in game config files.
|
||||||
* like {@code master_dat=C:\fallout1\master.dat}. After flattening moves
|
*
|
||||||
* everything to the bundle root, these paths break. Rewrites them to
|
* Scans all text files in the extracted game for patterns like
|
||||||
* relative {@code .\master.dat} instead.
|
* {@code master_dat=C:\FALLOUT1\MASTER.DAT} or {@code C:/FALLOUT1/MASTER.DAT}.
|
||||||
|
* If the referenced path doesn't exist in the extracted tree but IS found
|
||||||
|
* at root level (after flattening), rewrites the stale directory prefix
|
||||||
|
* from {@code C:\STALEDIR\} to {@code .\} (relative to bundle root).
|
||||||
|
*
|
||||||
|
* This is fully game-agnostic — no hardcoded game names or known config files.
|
||||||
|
* Only processes C: drive references; D:/E:/etc. are left alone (CD-ROM refs).
|
||||||
*/
|
*/
|
||||||
private static void fixAbsoluteConfigPaths(Path extractDir) throws IOException {
|
private static void fixAbsoluteConfigPaths(Path extractDir) throws IOException {
|
||||||
|
java.util.regex.Pattern drvPattern = java.util.regex.Pattern.compile(
|
||||||
|
"[A-Za-z]:(\\\\|/)[^\\s\"\\r\\n]+");
|
||||||
try (var walk = Files.walk(extractDir)) {
|
try (var walk = Files.walk(extractDir)) {
|
||||||
walk.filter(Files::isRegularFile).forEach(f -> {
|
walk.filter(Files::isRegularFile)
|
||||||
String name = f.getFileName().toString().toLowerCase();
|
.filter(f -> isSmallTextFile(f))
|
||||||
var prefixes = CONFIG_PATH_FIXES.get(name);
|
.forEach(f -> {
|
||||||
if (prefixes == null) return;
|
|
||||||
try {
|
try {
|
||||||
byte[] data = Files.readAllBytes(f);
|
byte[] data = Files.readAllBytes(f);
|
||||||
byte[] original = data.clone();
|
byte[] original = data.clone();
|
||||||
for (String prefix : prefixes) {
|
String text = new String(data, java.nio.charset.StandardCharsets.ISO_8859_1);
|
||||||
// Replace both "C:\dir\" and "c:\dir\" patterns
|
String patched = patchAbsolutePaths(text, extractDir, drvPattern);
|
||||||
byte[] pBytes = prefix.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
if (!patched.equals(text)) {
|
||||||
byte[] pSlash = prefix.replace('\\', '/')
|
Files.write(f, patched.getBytes(
|
||||||
.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
java.nio.charset.StandardCharsets.ISO_8859_1));
|
||||||
byte[] rel = ".\\".getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
|
||||||
byte[] relSlash = "./"
|
|
||||||
.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
|
|
||||||
data = replaceBytes(data, pBytes, rel);
|
|
||||||
data = replaceBytes(data, pSlash, relSlash);
|
|
||||||
}
|
|
||||||
if (!java.util.Arrays.equals(data, original)) {
|
|
||||||
Files.write(f, data);
|
|
||||||
LOG.info("Patched absolute paths in " + f.getFileName());
|
LOG.info("Patched absolute paths in " + f.getFileName());
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@@ -529,27 +516,151 @@ public class UploadResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Simple byte-level search-and-replace (preserves CRLF, encoding, etc.). */
|
/** Returns true if the file is small (<100KB) and contains no null bytes (i.e. is text). */
|
||||||
private static byte[] replaceBytes(byte[] src, byte[] search, byte[] replacement) {
|
private static boolean isSmallTextFile(Path f) throws IOException {
|
||||||
if (search.length == 0) return src;
|
long size = Files.size(f);
|
||||||
java.util.ArrayList<Byte> result = new java.util.ArrayList<>(src.length);
|
if (size > 100 * 1024 || size == 0) return false;
|
||||||
int i = 0;
|
try (var is = Files.newInputStream(f)) {
|
||||||
while (i <= src.length - search.length) {
|
int len = (int) Math.min(size, 4096);
|
||||||
boolean match = true;
|
byte[] head = new byte[len];
|
||||||
for (int j = 0; j < search.length; j++) {
|
int read = is.readNBytes(head, 0, len);
|
||||||
if (src[i + j] != search[j]) { match = false; break; }
|
for (int i = 0; i < read; i++) {
|
||||||
}
|
if (head[i] == 0) return false;
|
||||||
if (match) {
|
|
||||||
for (byte b : replacement) result.add(b);
|
|
||||||
i += search.length;
|
|
||||||
} else {
|
|
||||||
result.add(src[i++]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
while (i < src.length) result.add(src[i++]);
|
return true;
|
||||||
byte[] out = new byte[result.size()];
|
}
|
||||||
for (int k = 0; k < out.length; k++) out[k] = result.get(k);
|
|
||||||
return out;
|
/**
|
||||||
|
* Find absolute DOS paths (C:\dir\file) in the text and rewrite stale
|
||||||
|
* directory prefixes to {@code .\} where the file/dir is found at root
|
||||||
|
* level after flattening.
|
||||||
|
*/
|
||||||
|
private static String patchAbsolutePaths(String text, Path extractDir,
|
||||||
|
java.util.regex.Pattern drvPattern) {
|
||||||
|
var m = drvPattern.matcher(text);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
int lastEnd = 0;
|
||||||
|
java.util.Set<String> seenPrefixes = new java.util.HashSet<>();
|
||||||
|
|
||||||
|
while (m.find()) {
|
||||||
|
int start = m.start();
|
||||||
|
// Copy text up to this match
|
||||||
|
sb.append(text, lastEnd, start);
|
||||||
|
|
||||||
|
String fullPath = m.group();
|
||||||
|
String drive = fullPath.substring(0, 2); // "C:" or "D:" etc.
|
||||||
|
|
||||||
|
// Only rewrite C: drive references
|
||||||
|
if (!"C:".equalsIgnoreCase(drive)) {
|
||||||
|
sb.append(fullPath);
|
||||||
|
lastEnd = m.end();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find what exists after flattening
|
||||||
|
String replacement = findStalePrefixReplace(fullPath, extractDir, seenPrefixes);
|
||||||
|
sb.append(replacement);
|
||||||
|
lastEnd = m.end();
|
||||||
|
}
|
||||||
|
sb.append(text.substring(lastEnd));
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a full path like {@code C:\FALLOUT1\CRITTER.DAT}, try progressively
|
||||||
|
* shorter suffixes against the extracted directory tree. When a suffix is
|
||||||
|
* found, rewrite the stale prefix to {@code .\}.
|
||||||
|
*
|
||||||
|
* Example: {@code C:\FALLOUT1\CRITTER.DAT} → CRITTER.DAT exists at root →
|
||||||
|
* stale prefix is {@code C:\FALLOUT1\} → returns {@code .\CRITTER.DAT}
|
||||||
|
*/
|
||||||
|
private static String findStalePrefixReplace(String fullPath, Path extractDir,
|
||||||
|
java.util.Set<String> seenPrefixes) {
|
||||||
|
String drivePrefix = fullPath.substring(0, 2); // "C:"
|
||||||
|
String sep = fullPath.substring(2, 3); // "\" or "/"
|
||||||
|
String relPath = fullPath.substring(3); // everything after "C:\"
|
||||||
|
|
||||||
|
// Split into components
|
||||||
|
String separator = "\\".equals(sep) ? "\\\\" : "/";
|
||||||
|
String[] parts = relPath.split(separator, -1);
|
||||||
|
// parts = ["FALLOUT1", "CRITTER.DAT"] or ["FALLOUT1", "DATA", "SOUND", "MUSIC", ""]
|
||||||
|
// The last element is empty if the path ends with separator (directory ref)
|
||||||
|
|
||||||
|
// Try suffixes from longest to shortest
|
||||||
|
for (int skip = 0; skip < parts.length; skip++) {
|
||||||
|
// Build suffix: parts[skip] + sep + parts[skip+1] + ...
|
||||||
|
StringBuilder suffix = new StringBuilder();
|
||||||
|
for (int j = skip; j < parts.length; j++) {
|
||||||
|
if (j > skip) suffix.append(sep);
|
||||||
|
suffix.append(parts[j]);
|
||||||
|
}
|
||||||
|
String suffixStr = suffix.toString();
|
||||||
|
if (suffixStr.isEmpty()) continue;
|
||||||
|
|
||||||
|
// Check if this suffix exists in the extracted directory (case-insensitive)
|
||||||
|
if (existsIgnoreCase(extractDir, suffixStr)) {
|
||||||
|
// The path is valid — keep it as-is
|
||||||
|
if (skip == 0) return fullPath;
|
||||||
|
|
||||||
|
// This suffix exists at root; build the replacement
|
||||||
|
// stale prefix = C:\ + parts[0..skip-1] + separator
|
||||||
|
// replacement = .\ + suffixStr
|
||||||
|
// But avoid double-replacing the same prefix
|
||||||
|
StringBuilder stale = new StringBuilder(drivePrefix + sep);
|
||||||
|
for (int k = 0; k < skip; k++) {
|
||||||
|
if (k > 0) stale.append(sep);
|
||||||
|
stale.append(parts[k]);
|
||||||
|
}
|
||||||
|
stale.append(sep);
|
||||||
|
|
||||||
|
String staleStr = stale.toString();
|
||||||
|
if (seenPrefixes.contains(staleStr)) {
|
||||||
|
// Already replaced elsewhere; just return the relative form
|
||||||
|
return "." + sep + suffixStr;
|
||||||
|
}
|
||||||
|
seenPrefixes.add(staleStr);
|
||||||
|
return "." + sep + suffixStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nothing matched — return the path unchanged
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Case-insensitive path existence check. Walks the path component by
|
||||||
|
* component, matching each directory/file name case-insensitively against
|
||||||
|
* the actual filesystem entries.
|
||||||
|
*/
|
||||||
|
private static boolean existsIgnoreCase(Path base, String relativePath) {
|
||||||
|
String sep = relativePath.contains("\\") ? "\\\\" : "/";
|
||||||
|
String[] parts = relativePath.split(sep, -1);
|
||||||
|
Path current = base;
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < parts.length; i++) {
|
||||||
|
String part = parts[i];
|
||||||
|
if (part.isEmpty()) {
|
||||||
|
// Trailing separator means directory — check if current is a dir
|
||||||
|
if (i == parts.length - 1) return Files.isDirectory(current);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Path found = null;
|
||||||
|
try (var dirStream = Files.list(current)) {
|
||||||
|
for (Path entry : (Iterable<Path>) dirStream::iterator) {
|
||||||
|
if (entry.getFileName().toString().equalsIgnoreCase(part)) {
|
||||||
|
found = entry;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (found == null) return false;
|
||||||
|
current = found;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Find mountable CD image files in the extracted game directory.
|
/** Find mountable CD image files in the extracted game directory.
|
||||||
|
|||||||
Reference in New Issue
Block a user