diff --git a/pom.xml b/pom.xml
index 8c017f0..e477426 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,6 +51,12 @@
io.quarkus
quarkus-arc
+
+
+ io.quarkus
+ quarkus-junit5
+ test
+
diff --git a/src/main/java/com/dostalgia/StaticResource.java b/src/main/java/com/dostalgia/StaticResource.java
index 7944b47..4b50cba 100644
--- a/src/main/java/com/dostalgia/StaticResource.java
+++ b/src/main/java/com/dostalgia/StaticResource.java
@@ -72,11 +72,7 @@ public class StaticResource {
if (n.endsWith(".png")) return "image/png";
if (n.endsWith(".webp")) return "image/webp";
if (n.endsWith(".gif")) return "image/gif";
- if (n.endsWith(".conf") || n.endsWith(".txt")) return "text/plain";
if (n.endsWith(".json")) return "application/json";
- if (n.endsWith(".html")) return "text/html";
- if (n.endsWith(".js")) return "application/javascript";
- if (n.endsWith(".css")) return "text/css";
return MediaType.APPLICATION_OCTET_STREAM;
}
diff --git a/src/test/java/com/dostalgia/CdImageServiceTest.java b/src/test/java/com/dostalgia/CdImageServiceTest.java
new file mode 100644
index 0000000..bd4be2c
--- /dev/null
+++ b/src/test/java/com/dostalgia/CdImageServiceTest.java
@@ -0,0 +1,103 @@
+package com.dostalgia;
+
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+class CdImageServiceTest {
+
+ private final CdImageService service = new CdImageService();
+
+ @Test
+ void findInDirectory_findsAllCdFormats(@TempDir Path dir) throws Exception {
+ Files.writeString(dir.resolve("game.cue"), "FILE \"game.bin\" BINARY");
+ Files.writeString(dir.resolve("game.bin"), "data");
+ Files.writeString(dir.resolve("game.iso"), "iso");
+ Files.writeString(dir.resolve("readme.txt"), "text");
+
+ List result = service.findInDirectory(dir);
+ assertEquals(3, result.size());
+ assertTrue(result.contains("game.bin"));
+ assertTrue(result.contains("game.cue"));
+ assertTrue(result.contains("game.iso"));
+ assertFalse(result.contains("readme.txt"));
+ }
+
+ @Test
+ void findInDirectory_emptyDir(@TempDir Path dir) throws Exception {
+ assertTrue(service.findInDirectory(dir).isEmpty());
+ }
+
+ @Test
+ void findInDirectory_caseInsensitive(@TempDir Path dir) throws Exception {
+ Files.writeString(dir.resolve("GAME.CUE"), "data");
+ Files.writeString(dir.resolve("GAME.ISO"), "data");
+ List result = service.findInDirectory(dir);
+ assertEquals(2, result.size());
+ }
+
+ @Test
+ void fixCueReferences_rewritesNonexistentReference(@TempDir Path dir) throws Exception {
+ // CUE references game.bin, but only game.img exists
+ Files.writeString(dir.resolve("game.cue"), "FILE \"game.bin\" BINARY\n TRACK 01 MODE1/2352\n INDEX 01 00:00:00");
+ Files.writeString(dir.resolve("game.img"), "data");
+
+ service.fixCueReferences(dir);
+
+ String fixed = Files.readString(dir.resolve("game.cue"));
+ assertTrue(fixed.contains("game.img"), "Should reference existing .img file");
+ assertFalse(fixed.contains("game.bin"), "Should no longer reference missing .bin");
+ }
+
+ @Test
+ void fixCueReferences_skipsWhenExists(@TempDir Path dir) throws Exception {
+ // CUE references game.bin — and it exists, so no change
+ String original = "FILE \"game.bin\" BINARY\n TRACK 01 MODE1/2352\n INDEX 01 00:00:00";
+ Files.writeString(dir.resolve("game.cue"), original);
+ Files.writeString(dir.resolve("game.bin"), "data");
+
+ service.fixCueReferences(dir);
+
+ assertEquals(original, Files.readString(dir.resolve("game.cue")).trim());
+ }
+
+ @Test
+ void fixCueReferences_noFileLine_noChange(@TempDir Path dir) throws Exception {
+ String original = "TITLE \"Test Game\"\nTRACK 01 MODE1/2352";
+ Files.writeString(dir.resolve("game.cue"), original);
+
+ service.fixCueReferences(dir);
+
+ assertEquals(original, Files.readString(dir.resolve("game.cue")).trim());
+ }
+
+ @Test
+ void findInBundle_findsCdImages(@TempDir Path dir) throws Exception {
+ // Create a test bundle with CD images and regular files
+ Path bundle = dir.resolve("test.jsdos");
+ try (var zos = new java.util.zip.ZipOutputStream(Files.newOutputStream(bundle))) {
+ zos.putNextEntry(new java.util.zip.ZipEntry("game.cue"));
+ zos.write("cue data".getBytes());
+ zos.closeEntry();
+ zos.putNextEntry(new java.util.zip.ZipEntry("game.iso"));
+ zos.write("iso data".getBytes());
+ zos.closeEntry();
+ zos.putNextEntry(new java.util.zip.ZipEntry("readme.txt"));
+ zos.write("text".getBytes());
+ zos.closeEntry();
+ zos.putNextEntry(new java.util.zip.ZipEntry("subdir/game.img"));
+ zos.write("img".getBytes());
+ zos.closeEntry();
+ }
+
+ List result = service.findInBundle(bundle);
+ assertEquals(3, result.size());
+ assertTrue(result.contains("game.cue"));
+ assertTrue(result.contains("game.iso"));
+ assertTrue(result.contains("subdir/game.img"));
+ }
+}
diff --git a/src/test/java/com/dostalgia/ConfigPatcherTest.java b/src/test/java/com/dostalgia/ConfigPatcherTest.java
new file mode 100644
index 0000000..2c4bc9a
--- /dev/null
+++ b/src/test/java/com/dostalgia/ConfigPatcherTest.java
@@ -0,0 +1,109 @@
+package com.dostalgia;
+
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+class ConfigPatcherTest {
+
+ private final ConfigPatcher patcher = new ConfigPatcher();
+
+ @Test
+ void fixAbsolutePaths_rewritesStalePrefix(@TempDir Path dir) throws Exception {
+ // Create a file that references C:\FALLOUT1\MASTER.DAT
+ // but after flattening, MASTER.DAT is at the root
+ String content = "master_dat=C:\\FALLOUT1\\MASTER.DAT\ncritter_dat=C:\\FALLOUT1\\CRITTER.DAT\n";
+ Files.writeString(dir.resolve("FALLOUT.CFG"), content, StandardCharsets.ISO_8859_1);
+
+ // The referenced files exist at root
+ Files.writeString(dir.resolve("MASTER.DAT"), "data");
+ Files.writeString(dir.resolve("CRITTER.DAT"), "data");
+
+ patcher.fixAbsolutePaths(dir);
+
+ String fixed = Files.readString(dir.resolve("FALLOUT.CFG"), StandardCharsets.ISO_8859_1);
+ assertTrue(fixed.contains(".\\MASTER.DAT"), "Should rewrite to relative path");
+ assertTrue(fixed.contains(".\\CRITTER.DAT"), "Both files should be rewritten");
+ assertFalse(fixed.contains("FALLOUT1"), "Stale prefix should be removed");
+ }
+
+ @Test
+ void fixAbsolutePaths_leavesNonCDrives(@TempDir Path dir) throws Exception {
+ // D: drive references are CD-ROM — should stay
+ String content = "cd_path=D:\\GAMEDATA\\SOUND.DAT\n";
+ Files.writeString(dir.resolve("config.ini"), content, StandardCharsets.ISO_8859_1);
+
+ patcher.fixAbsolutePaths(dir);
+
+ String fixed = Files.readString(dir.resolve("config.ini"), StandardCharsets.ISO_8859_1);
+ assertEquals(content.trim(), fixed.trim(), "D: references should be preserved");
+ }
+
+ @Test
+ void fixAbsolutePaths_leavesValidPaths(@TempDir Path dir) throws Exception {
+ // Path exists with full structure — should stay as-is
+ Path sub = Files.createDirectory(dir.resolve("VALIDDIR"));
+ Files.writeString(sub.resolve("file.dat"), "data");
+
+ String content = "data=C:\\VALIDDIR\\file.dat\n";
+ Files.writeString(dir.resolve("config.ini"), content, StandardCharsets.ISO_8859_1);
+
+ patcher.fixAbsolutePaths(dir);
+
+ String fixed = Files.readString(dir.resolve("config.ini"), StandardCharsets.ISO_8859_1);
+ assertEquals(content.trim(), fixed.trim(), "Valid full paths should be preserved");
+ }
+
+ @Test
+ void fixAbsolutePaths_skipsNonTextFiles(@TempDir Path dir) throws Exception {
+ // Binary file with null bytes — should not be processed
+ byte[] binary = new byte[200];
+ binary[0] = 'C';
+ binary[1] = ':';
+ binary[2] = '\\';
+ binary[3] = 'D';
+ binary[10] = 0; // null byte makes it "not text"
+ Files.write(dir.resolve("binary.dat"), binary);
+
+ // Should not throw or modify binary files
+ assertDoesNotThrow(() -> patcher.fixAbsolutePaths(dir));
+
+ byte[] result = Files.readAllBytes(dir.resolve("binary.dat"));
+ assertArrayEquals(binary, result, "Binary file should be unchanged");
+ }
+
+ @Test
+ void fixAbsolutePaths_skipsLargeFiles(@TempDir Path dir) throws Exception {
+ // File > 100KB should be skipped
+ byte[] large = new byte[101 * 1024];
+ large[0] = 'C';
+ large[1] = ':';
+ large[2] = '\\';
+ Files.write(dir.resolve("large.cfg"), large);
+
+ // Should not throw
+ assertDoesNotThrow(() -> patcher.fixAbsolutePaths(dir));
+ }
+
+ @Test
+ void fixAbsolutePaths_emptyDir_noError(@TempDir Path dir) {
+ assertDoesNotThrow(() -> patcher.fixAbsolutePaths(dir));
+ }
+
+ @Test
+ void fixAbsolutePaths_unixStylePath(@TempDir Path dir) throws Exception {
+ // Some games use forward slashes: C:/GAME/DATA
+ String content = "path=C:/MYGAME/DATA.DAT\n";
+ Files.writeString(dir.resolve("config.cfg"), content, StandardCharsets.ISO_8859_1);
+ Files.writeString(dir.resolve("DATA.DAT"), "data");
+
+ patcher.fixAbsolutePaths(dir);
+
+ String fixed = Files.readString(dir.resolve("config.cfg"), StandardCharsets.ISO_8859_1);
+ assertTrue(fixed.contains("./DATA.DAT"), "Forward-slash paths should be fixed");
+ }
+}
diff --git a/src/test/java/com/dostalgia/ExecutableDetectorTest.java b/src/test/java/com/dostalgia/ExecutableDetectorTest.java
new file mode 100644
index 0000000..3f37f04
--- /dev/null
+++ b/src/test/java/com/dostalgia/ExecutableDetectorTest.java
@@ -0,0 +1,136 @@
+package com.dostalgia;
+
+import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+class ExecutableDetectorTest {
+
+ private final ExecutableDetector detector = new ExecutableDetector();
+
+ @Test
+ void findAll_findsExeComBat(@TempDir Path dir) throws Exception {
+ Files.writeString(dir.resolve("GAME.EXE"), "MZ");
+ Files.writeString(dir.resolve("helper.com"), "MZ");
+ Files.writeString(dir.resolve("run.bat"), "@echo off");
+ Files.writeString(dir.resolve("readme.txt"), "text"); // should NOT be found
+
+ List result = detector.findAll(dir);
+ assertEquals(3, result.size());
+ assertTrue(result.contains("GAME.EXE"));
+ assertTrue(result.contains("helper.com"));
+ assertTrue(result.contains("run.bat"));
+ assertFalse(result.contains("readme.txt"));
+ }
+
+ @Test
+ void findMain_prefersLargestNonInstaller(@TempDir Path dir) throws Exception {
+ // Small installer
+ Files.write(dir.resolve("INSTALL.EXE"), createMZ(100));
+ // Large real game
+ Files.write(dir.resolve("GAME.EXE"), createMZ(5000));
+
+ String main = detector.findMain(dir);
+ assertEquals(dir.resolve("GAME.EXE").toString(), main);
+ }
+
+ @Test
+ void findMain_skipsExtenders(@TempDir Path dir) throws Exception {
+ Files.write(dir.resolve("DOS4GW.EXE"), createMZ(200));
+ Files.write(dir.resolve("GAME.EXE"), createMZ(1000));
+
+ String main = detector.findMain(dir);
+ assertEquals(dir.resolve("GAME.EXE").toString(), main);
+ }
+
+ @Test
+ void findMain_skipsSelfExtractors(@TempDir Path dir) throws Exception {
+ byte[] sfx = createMZ(500);
+ // Embed PKSFX string
+ String content = new String(sfx);
+ int pos = content.indexOf("MZ") + 2;
+ byte[] withSfx = new byte[sfx.length + 20];
+ System.arraycopy(sfx, 0, withSfx, 0, sfx.length);
+ byte[] pksfx = "PKSFX".getBytes();
+ System.arraycopy(pksfx, 0, withSfx, pos, pksfx.length);
+ Files.write(dir.resolve("GAME.EXE"), withSfx);
+ Files.write(dir.resolve("REAL.EXE"), createMZ(2000));
+
+ String main = detector.findMain(dir);
+ assertEquals(dir.resolve("REAL.EXE").toString(), main);
+ }
+
+ @Test
+ void findMain_emptyDir_returnsNull(@TempDir Path dir) {
+ assertNull(detector.findMain(dir));
+ }
+
+ @Test
+ void findMain_subdirectoryDeprioritized(@TempDir Path dir) throws Exception {
+ // Root-level large exe
+ Files.write(dir.resolve("GAME.EXE"), createMZ(1000));
+ // Subdirectory exe (should be deprioritized even if larger)
+ Path sub = Files.createDirectory(dir.resolve("SUBDIR"));
+ Files.write(sub.resolve("OTHER.EXE"), createMZ(2000));
+
+ String main = detector.findMain(dir);
+ assertEquals(dir.resolve("GAME.EXE").toString(), main);
+ }
+
+ @Test
+ void findSetup_findsSetupExe(@TempDir Path dir) throws Exception {
+ Files.writeString(dir.resolve("SETUP.EXE"), "MZ");
+ Files.writeString(dir.resolve("GAME.EXE"), "MZ");
+
+ String setup = detector.findSetup(dir);
+ assertEquals(dir.resolve("SETUP.EXE").toString(), setup);
+ }
+
+ @Test
+ void findSetup_findsInstallBat(@TempDir Path dir) throws Exception {
+ Files.writeString(dir.resolve("INSTALL.BAT"), "@echo off");
+
+ String setup = detector.findSetup(dir);
+ assertEquals(dir.resolve("INSTALL.BAT").toString(), setup);
+ }
+
+ @Test
+ void findSetup_noSetup_returnsNull(@TempDir Path dir) throws Exception {
+ Files.writeString(dir.resolve("GAME.EXE"), "MZ");
+ assertNull(detector.findSetup(dir));
+ }
+
+ @Test
+ void isLikelySelfExtractor_detectsPKSFX(@TempDir Path dir) throws Exception {
+ byte[] buf = new byte[1000];
+ buf[0] = 0x4D; // M
+ buf[1] = 0x5A; // Z
+ byte[] pksfx = "PKSFX".getBytes();
+ System.arraycopy(pksfx, 0, buf, 10, pksfx.length);
+
+ Path f = dir.resolve("sfx.exe");
+ Files.write(f, buf);
+ assertTrue(ExecutableDetector.isLikelySelfExtractor(f));
+ }
+
+ @Test
+ void isLikelySelfExtractor_plainExe_returnsFalse(@TempDir Path dir) throws Exception {
+ byte[] buf = createMZ(500);
+ Path f = dir.resolve("game.exe");
+ Files.write(f, buf);
+ assertFalse(ExecutableDetector.isLikelySelfExtractor(f));
+ }
+
+ // ─── helpers ─────────────────────────────────────────────────
+
+ private static byte[] createMZ(int size) {
+ byte[] buf = new byte[size];
+ buf[0] = 0x4D; // M
+ buf[1] = 0x5A; // Z
+ return buf;
+ }
+}