From 554347ac0ec688b9c66fa363ab341ea2141ff5ec Mon Sep 17 00:00:00 2001 From: David Alvarez Date: Mon, 8 Jun 2026 12:49:33 +0200 Subject: [PATCH] feat: include mount/imgmount in jsdos.json autoexec script + remove fix-paths endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit js-dos v8 uses the jsdos.json autoexec.script instead of the dosbox.conf [autoexec] section. Without mount/imgmount in the jsdos.json script, the CD image was never mounted — games loaded base data from C: (bundle root) but crashed when trying to access CD-ROM content (videos, audio). Also removes the now-unnecessary fix-paths endpoint (both endpoint and GameService implementation) since config path fixing is automatic during upload via the game-agnostic scanner. --- src/main/java/com/dostalgia/GameResource.java | 19 -- src/main/java/com/dostalgia/GameService.java | 225 ++---------------- .../java/com/dostalgia/UploadResource.java | 39 ++- 3 files changed, 49 insertions(+), 234 deletions(-) diff --git a/src/main/java/com/dostalgia/GameResource.java b/src/main/java/com/dostalgia/GameResource.java index 66adc07..136819d 100644 --- a/src/main/java/com/dostalgia/GameResource.java +++ b/src/main/java/com/dostalgia/GameResource.java @@ -164,25 +164,6 @@ 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) { if (v instanceof Number n) return n.intValue(); return null; diff --git a/src/main/java/com/dostalgia/GameService.java b/src/main/java/com/dostalgia/GameService.java index 7e77cbb..201bb08 100644 --- a/src/main/java/com/dostalgia/GameService.java +++ b/src/main/java/com/dostalgia/GameService.java @@ -169,7 +169,7 @@ public class GameService { // Build new config entries — preserve any CD images in the bundle List cdImages = findCdImagesInBundle(bundlePath); byte[] newDosboxConf = buildDosboxConfBytes(executable, cdImages); - byte[] newJsdosJson = buildJsdosJsonBytes(executable); + byte[] newJsdosJson = buildJsdosJsonBytes(executable, cdImages); // Patch the ZIP: read old, write new with modified config entries Path tmpPath = bundlePath.resolveSibling(bundlePath.getFileName() + ".tmp"); @@ -204,210 +204,6 @@ public class GameService { save(game); } - /** - * Patch hardcoded absolute DOS paths (C:\dir\ → .\) in game config files - * within a .jsdos bundle. Game-agnostic — scans all small text entries - * for drive-letter paths and rewrites stale directory prefixes. - */ - 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; - - // Phase 1: build a set of all entry paths for existence checking - java.util.Set allEntries = new java.util.HashSet<>(); - java.util.Set dirs = new java.util.HashSet<>(); - java.util.List 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"); - 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)); - var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(tmpPath))) { - - java.util.zip.ZipEntry entry; - while ((entry = zis.getNextEntry()) != null) { - String name = entry.getName(); - zos.putNextEntry(new java.util.zip.ZipEntry(name)); - - // Check if this entry needs patching - var ed = findEntryData(textEntries, name); - if (ed != null) { - String text = new String(ed.data, java.nio.charset.StandardCharsets.ISO_8859_1); - String patchedText = patchZipPaths(text, existsChecker, drvPattern); - byte[] result = patchedText.getBytes(java.nio.charset.StandardCharsets.ISO_8859_1); - if (!java.util.Arrays.equals(result, ed.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 boolean isSmallTextEntry(java.util.zip.ZipEntry entry, java.util.zip.ZipInputStream zis) { - if (entry.isDirectory()) return false; - long size = entry.getSize(); - if (size > 100 * 1024 || size == 0) return true; // unknown size is OK - return true; - } - - private static boolean hasDrivePath(String text) { - return java.util.regex.Pattern.compile("[A-Za-z]:\\\\").matcher(text).find(); - } - - private static EntryData findEntryData(java.util.List 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 entries; // all entries in original case - final java.util.Set dirs; // all directory prefixes - - ZipExistenceChecker(java.util.Set entries, java.util.Set 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 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 { - 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; - } - } - if (!found) sb.append(fullPath); - lastEnd = m.end(); - } - 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). */ private List findCdImagesInBundle(Path bundlePath) throws IOException { Set cdExt = Set.of(".iso", ".cue", ".img", ".ccd", ".bin"); @@ -474,10 +270,23 @@ public class GameService { return sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8); } - /** Build jsdos.json for a given executable path. */ - private byte[] buildJsdosJsonBytes(String relExe) { + /** Build jsdos.json for a given executable path and CD images. */ + private byte[] buildJsdosJsonBytes(String relExe, List cdImages) { var parts = splitExePath(relExe); StringBuilder script = new StringBuilder(); + // js-dos v8 uses the jsdos.json autoexec.script instead of the + // dosbox.conf [autoexec] section, so mount/imgmount MUST be here. + script.append("mount c .\n"); + char driveLetter = 'D'; + for (String img : cdImages) { + String imgLower = img.toLowerCase(); + String flags = (imgLower.endsWith(".bin") && !imgLower.endsWith(".cue")) + ? " -t cdrom -fs iso" : " -t cdrom"; + script.append("imgmount ").append(driveLetter).append(" \"") + .append(img).append("\"").append(flags).append("\n"); + driveLetter++; + } + script.append("c:\n"); if (parts[0] != null) { script.append("path=c:\\\n"); script.append("cd ").append(parts[0]).append("\n"); @@ -553,7 +362,7 @@ public class GameService { // Build new config entries — preserve any CD images in the bundle List cdImages = findCdImagesInBundle(bundlePath); byte[] newDosboxConf = buildDosboxConfBytes(setupExe, cdImages); - byte[] newJsdosJson = buildJsdosJsonBytes(setupExe); + byte[] newJsdosJson = buildJsdosJsonBytes(setupExe, cdImages); try (var zis = new java.util.zip.ZipInputStream(Files.newInputStream(bundlePath)); var zos = new java.util.zip.ZipOutputStream(out)) { diff --git a/src/main/java/com/dostalgia/UploadResource.java b/src/main/java/com/dostalgia/UploadResource.java index 28cfc87..1459577 100644 --- a/src/main/java/com/dostalgia/UploadResource.java +++ b/src/main/java/com/dostalgia/UploadResource.java @@ -712,11 +712,11 @@ public class UploadResource { if (exePath != null) { String relExe = extractDir.relativize(Path.of(exePath)).toString(); Files.writeString(jsdos.resolve("dosbox.conf"), buildDosboxConf(relExe, cdImages)); - Files.write(jsdos.resolve("jsdos.json"), buildJsdosJson(relExe)); + Files.write(jsdos.resolve("jsdos.json"), buildJsdosJson(relExe, cdImages)); } else { // CD-only game — no executable, mount CD and show a DOS prompt Files.writeString(jsdos.resolve("dosbox.conf"), buildCdOnlyDosboxConf(cdImages)); - Files.write(jsdos.resolve("jsdos.json"), buildCdOnlyJsdosJson()); + Files.write(jsdos.resolve("jsdos.json"), buildCdOnlyJsdosJson(cdImages)); } // ZIP it up directly from extractDir (no temp copy) @@ -804,13 +804,25 @@ public class UploadResource { return new String[]{null, relExe}; } - private byte[] buildJsdosJson(String relExe) { + private byte[] buildJsdosJson(String relExe, List cdImages) { // DOS uses \\ as path separator; / is a switch character. - // If the executable is in a subdirectory, cd into it first. + // js-dos v8 uses the jsdos.json autoexec.script instead of the + // dosbox.conf [autoexec] section, so mount/imgmount MUST be here. var parts = splitDirExe(relExe); StringBuilder script = new StringBuilder(); + script.append("mount c .\n"); + // Mount CD images as D:, E:, … so games can find their CD. + char driveLetter = 'D'; + for (String img : resolveCdImages(cdImages)) { + String imgLower = img.toLowerCase(); + String flags = (imgLower.endsWith(".bin") && !imgLower.endsWith(".cue")) + ? " -t cdrom -fs iso" : " -t cdrom"; + script.append("imgmount ").append(driveLetter).append(" \"") + .append(img).append("\"").append(flags).append("\n"); + driveLetter++; + } + script.append("c:\n"); if (parts[0] != null) { - // Set PATH to include root so DOS extenders (DOS4GW etc.) are findable script.append("path=c:\\\n"); script.append("cd ").append(parts[0]).append("\n"); } @@ -892,10 +904,23 @@ public class UploadResource { return sb.toString(); } - private byte[] buildCdOnlyJsdosJson() { + private byte[] buildCdOnlyJsdosJson(List cdImages) { + StringBuilder script = new StringBuilder(); + script.append("mount c .\n"); + char driveLetter = 'D'; + for (String img : resolveCdImages(cdImages)) { + String imgLower = img.toLowerCase(); + String flags = (imgLower.endsWith(".bin") && !imgLower.endsWith(".cue")) + ? " -t cdrom -fs iso" : " -t cdrom"; + script.append("imgmount ").append(driveLetter).append(" \"") + .append(img).append("\"").append(flags).append("\n"); + driveLetter++; + } + script.append("c:\n"); String json = "{" + "\"autoexec\":{" - + "\"options\":{\"script\":{\"value\":\"c:\\\\\"}}" + + "\"options\":{\"script\":{\"value\":\"" + + escapeJson(script.toString()) + "\"}}" + "}," + "\"output\":{" + "\"options\":{\"autolock\":{\"value\":true}}}"