feat: fix hardcoded DOS paths in configs during upload + fix-paths endpoint
Build & Deploy / build-and-deploy (push) Successful in 43s

- Add both uppercase C: and lowercase c: variants to CONFIG_PATH_FIXES
- Add fixBundleConfigPaths() to GameService to patch existing bundles
- Add POST /api/games/{id}/fix-paths endpoint for existing games
This commit is contained in:
David Alvarez
2026-06-08 10:37:04 +02:00
parent 22a9a55af8
commit 145f11eaa0
3 changed files with 115 additions and 1 deletions
@@ -164,6 +164,25 @@ public class GameResource {
} }
} }
@POST
@Path("/{id}/fix-paths")
@Produces(MediaType.APPLICATION_JSON)
public Response fixPaths(@PathParam("id") String id) {
try {
boolean patched = svc.fixBundleConfigPaths(id);
if (patched) {
return Response.ok(Map.of("status", "patched",
"message", "Fixed hardcoded DOS paths in game config")).build();
}
return Response.ok(Map.of("status", "noop",
"message", "No known config files needed patching")).build();
} catch (NoSuchFileException e) {
return Response.status(404).entity(Map.of("error", "Game not found")).build();
} catch (Exception e) {
return Response.serverError().entity(Map.of("error", e.getMessage())).build();
}
}
private Integer asInt(Object v) { private Integer asInt(Object v) {
if (v instanceof Number n) return n.intValue(); if (v instanceof Number n) return n.intValue();
return null; return null;
@@ -204,6 +204,97 @@ public class GameService {
save(game); save(game);
} }
/**
* Known game config files with hardcoded DOS paths that need fixing.
* Same as UploadResource.CONFIG_PATH_FIXES but duplicated here for
* the fix-paths endpoint (no dependency on UploadResource).
*/
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 {
Game game = load(id);
String bundleFile = game.getBundleFile();
if (bundleFile == null) return false;
Path bundlePath = gamesDir.resolve(bundleFile);
if (!Files.exists(bundlePath)) return false;
Path tmpPath = bundlePath.resolveSibling(bundlePath.getFileName() + ".tmp");
boolean patched = false;
try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath));
var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(tmpPath))) {
java.util.zip.ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
String name = entry.getName();
String loName = name.toLowerCase();
var prefixes = CONFIG_PATH_FIXES.get(loName);
zos.putNextEntry(new java.util.zip.ZipEntry(name));
if (prefixes != null) {
// Read the entry content and patch paths
byte[] data = zis.readAllBytes();
byte[] result = data;
for (String prefix : prefixes) {
byte[] pBytes = prefix.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1);
byte[] pSlash = prefix.replace('\\', '/')
.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;
}
zos.write(result);
} else {
zis.transferTo(zos);
}
zos.closeEntry();
zis.closeEntry();
}
}
if (patched) {
Files.move(tmpPath, bundlePath, StandardCopyOption.REPLACE_EXISTING);
} else {
Files.deleteIfExists(tmpPath);
}
return patched;
}
private static byte[] replaceBytesInPlace(byte[] src, byte[] search, byte[] replacement) {
if (search.length == 0) return src;
java.util.ArrayList<Byte> result = new java.util.ArrayList<>(src.length);
int i = 0;
while (i <= src.length - search.length) {
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);
i += search.length;
} else {
result.add(src[i++]);
}
}
while (i < src.length) result.add(src[i++]);
byte[] out = new byte[result.size()];
for (int k = 0; k < out.length; k++) out[k] = result.get(k);
return out;
}
/** 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). */
private List<String> findCdImagesInBundle(Path bundlePath) throws IOException { private List<String> findCdImagesInBundle(Path bundlePath) throws IOException {
Set<String> cdExt = Set.of(".iso", ".cue", ".img", ".ccd", ".bin"); Set<String> cdExt = Set.of(".iso", ".cue", ".img", ".ccd", ".bin");
@@ -485,7 +485,11 @@ public class UploadResource {
* replace with ".\\" (relative to the game root). */ * replace with ".\\" (relative to the game root). */
private static final java.util.Map<String, java.util.List<String>> CONFIG_PATH_FIXES = private static final java.util.Map<String, java.util.List<String>> CONFIG_PATH_FIXES =
java.util.Map.of( java.util.Map.of(
"fallout.cfg", java.util.List.of("c:\\fallout1\\", "c:\\fallout2\\", "c:\\fallout\\") "fallout.cfg", java.util.List.of(
"c:\\fallout1\\", "C:\\fallout1\\",
"c:\\fallout2\\", "C:\\fallout2\\",
"c:\\fallout\\", "C:\\fallout\\"
)
); );
/** /**