diff --git a/src/main/java/com/dostalgia/ExecutableDetector.java b/src/main/java/com/dostalgia/ExecutableDetector.java index 9c6512c..34288a3 100644 --- a/src/main/java/com/dostalgia/ExecutableDetector.java +++ b/src/main/java/com/dostalgia/ExecutableDetector.java @@ -25,7 +25,6 @@ public class ExecutableDetector { @Inject PlatformDetector platform; - /** Executable stems to never select as main game executable. */ private static final Set SKIP_EXE_NAMES = Set.of( "dos4gw", "dos32a", "dos4gw2", "pmode", "pmodew", "cwsdpmi", "emm386", "himem", "himemx", "debug", @@ -36,12 +35,11 @@ public class ExecutableDetector { "ace", "unace", "zoo", "arc", "ha", "cab" ); - /** Collect all discoverable executables (relative paths) from the extracted directory. */ public List findAll(Path dir) throws IOException { List result = new ArrayList<>(); Files.walkFileTree(dir, new SimpleFileVisitor<>() { @Override - public FileVisitResult visitFile(@Nonnull Path f, @Nonnull BasicFileAttributes a) { + public @Nonnull FileVisitResult visitFile(@Nonnull Path f, @Nonnull BasicFileAttributes a) { String name = f.getFileName().toString().toLowerCase(); if (name.endsWith(".exe") || name.endsWith(".com") || name.endsWith(".bat")) { result.add(dir.relativize(f).toString().replace('\\', '/')); @@ -53,14 +51,13 @@ public class ExecutableDetector { return result; } - /** Find the best main executable. */ public String findMain(Path dir) throws IOException { record Candidate(int depth, boolean isInstaller, long size, String path, String platform) {} List candidates = new ArrayList<>(); Files.walkFileTree(dir, new SimpleFileVisitor<>() { @Override - public FileVisitResult visitFile(@Nonnull Path f, @Nonnull BasicFileAttributes a) { + public @Nonnull FileVisitResult visitFile(@Nonnull Path f, @Nonnull BasicFileAttributes a) { String name = f.getFileName().toString().toLowerCase(); if (!name.endsWith(".exe") && !name.endsWith(".com") && !name.endsWith(".bat")) return FileVisitResult.CONTINUE; @@ -96,7 +93,6 @@ public class ExecutableDetector { return candidates.getFirst().path(); } - /** Find a setup/install/config executable. */ public String findSetup(Path dir) throws IOException { record Candidate(int depth, long size, String path) {} List candidates = new ArrayList<>(); @@ -104,7 +100,7 @@ public class ExecutableDetector { Files.walkFileTree(dir, new SimpleFileVisitor<>() { @Override - public FileVisitResult visitFile(@Nonnull Path f, @Nonnull BasicFileAttributes a) { + public @Nonnull FileVisitResult visitFile(@Nonnull Path f, @Nonnull BasicFileAttributes a) { String name = f.getFileName().toString().toLowerCase(); String stem = name.contains(".") ? name.substring(0, name.lastIndexOf('.')) : name; if (!name.endsWith(".exe") && !name.endsWith(".com") && !name.endsWith(".bat")) @@ -125,7 +121,6 @@ public class ExecutableDetector { return candidates.getFirst().path(); } - /** Check if an executable is likely a self-extracting archive (PKSFX stub, etc.). */ public static boolean isLikelySelfExtractor(Path exePath) { try (var fis = Files.newInputStream(exePath)) { byte[] buf = new byte[32768]; diff --git a/src/main/java/com/dostalgia/GameService.java b/src/main/java/com/dostalgia/GameService.java index 8216b97..7accb52 100644 --- a/src/main/java/com/dostalgia/GameService.java +++ b/src/main/java/com/dostalgia/GameService.java @@ -126,12 +126,12 @@ public class GameService implements GameStore { if (Files.exists(dir)) { Files.walkFileTree(dir, new SimpleFileVisitor<>() { @Override - public FileVisitResult visitFile(@Nonnull Path f, @Nonnull BasicFileAttributes a) throws IOException { + public @Nonnull FileVisitResult visitFile(@Nonnull Path f, @Nonnull BasicFileAttributes a) throws IOException { Files.delete(f); return FileVisitResult.CONTINUE; } @Override - public FileVisitResult postVisitDirectory(@Nonnull Path d, IOException e) throws IOException { + public @Nonnull FileVisitResult postVisitDirectory(@Nonnull Path d, IOException e) throws IOException { Files.delete(d); return FileVisitResult.CONTINUE; } @@ -226,20 +226,7 @@ public class GameService implements GameStore { java.util.zip.ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (entry.getName().equals(relExe.replace('\\', '/'))) { - byte[] buf = new byte[0x80]; - int read = zis.readNBytes(buf, 0, buf.length); - // Read more if PE/NE signature is beyond the initial buffer - if (read >= 0x40) { - int peOffset = (buf[0x3C] & 0xFF) | ((buf[0x3D] & 0xFF) << 8) - | ((buf[0x3E] & 0xFF) << 16) | ((buf[0x3F] & 0xFF) << 24); - if (peOffset + 2 > read) { - buf = java.util.Arrays.copyOf(buf, Math.max(buf.length, peOffset + 2)); - int remaining = buf.length - read; - zis.readNBytes(buf, read, remaining); - read = buf.length; - } - } - return PlatformDetector.detectFromBytes(buf, read); + return PlatformDetector.detectFromBytes(PlatformDetector.readHeaderBytes(zis)); } zis.closeEntry(); } diff --git a/src/main/java/com/dostalgia/IgdbService.java b/src/main/java/com/dostalgia/IgdbService.java index eb456ff..1e7dc9a 100644 --- a/src/main/java/com/dostalgia/IgdbService.java +++ b/src/main/java/com/dostalgia/IgdbService.java @@ -118,13 +118,10 @@ public class IgdbService { entry.put("igdb_id", game.get("id").asInt()); entry.put("name", game.has("name") ? game.get("name").asText() : query); - epochToYear(game, "first_release_date").ifPresent(y -> entry.put("year", y)); + epochToYear(game).ifPresent(y -> entry.put("year", y)); - if (game.has("genres") && game.get("genres").isArray()) { - List genres = new ArrayList<>(); - for (JsonNode g : game.get("genres")) { - if (g.has("name")) genres.add(g.get("name").asText()); - } + List genres = extractGenres(game); + if (!genres.isEmpty()) { entry.put("genres", genres); } @@ -288,10 +285,10 @@ public class IgdbService { // ─── Utility ────────────────────────────────────────────────── - /** Convert IGDB epoch (seconds) to year, if present in the node. */ - private static Optional epochToYear(JsonNode node, String field) { - if (node.has(field)) { - long epoch = node.get(field).asLong(); + /** Convert IGDB epoch (seconds) to year, from 'first_release_date' field. */ + private static Optional epochToYear(JsonNode node) { + if (node.has("first_release_date")) { + long epoch = node.get("first_release_date").asLong(); Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(epoch * 1000); return Optional.of(cal.get(Calendar.YEAR)); @@ -299,6 +296,17 @@ public class IgdbService { return Optional.empty(); } + /** Extract genre names from a JsonNode that may have a 'genres' array. */ + private static List extractGenres(JsonNode node) { + List genres = new ArrayList<>(); + if (node.has("genres") && node.get("genres").isArray()) { + for (JsonNode g : node.get("genres")) { + if (g.has("name")) genres.add(g.get("name").asText()); + } + } + return genres; + } + private String sanitize(String s) { return s.replace("\\", "\\\\").replace("\"", "\\\""); } @@ -307,12 +315,10 @@ public class IgdbService { Map map = new LinkedHashMap<>(); map.put("igdb_id", node.has("id") ? node.get("id").asInt() : 0); map.put("name", node.has("name") ? node.get("name").asText() : ""); - epochToYear(node, "first_release_date").ifPresent(y -> map.put("year", y)); - if (node.has("genres") && node.get("genres").isArray()) { - List genres = new ArrayList<>(); - for (JsonNode g : node.get("genres")) { - if (g.has("name")) genres.add(g.get("name").asText()); - } + epochToYear(node).ifPresent(y -> map.put("year", y)); + + List genres = extractGenres(node); + if (!genres.isEmpty()) { map.put("genres", genres); } if (node.has("involved_companies") && node.get("involved_companies").isArray()) { diff --git a/src/main/java/com/dostalgia/PlatformDetector.java b/src/main/java/com/dostalgia/PlatformDetector.java index 5f214ec..6b4f7e9 100644 --- a/src/main/java/com/dostalgia/PlatformDetector.java +++ b/src/main/java/com/dostalgia/PlatformDetector.java @@ -3,59 +3,44 @@ package com.dostalgia; import jakarta.enterprise.context.ApplicationScoped; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.logging.Logger; /** * Detects platform type (DOS vs Windows) by reading executable headers. - * Pure logic — no I/O beyond reading the file header bytes. */ @ApplicationScoped public class PlatformDetector { private static final Logger LOG = Logger.getLogger(PlatformDetector.class.getName()); - /** - * Detect whether an executable targets DOS or Windows by reading its PE/NE header. - *
    - *
  • Pure MZ, LE (DOS/4GW extender), LX (OS/2) → "dos"
  • - *
  • NE (Windows 3.x), PE (Win32) → "windows"
  • - *
  • Cannot read → null
  • - *
- */ public String detect(Path exePath) { String name = exePath.getFileName().toString().toLowerCase(); if (name.endsWith(".com") || name.endsWith(".bat")) return "dos"; try (var fis = Files.newInputStream(exePath)) { - byte[] buf = new byte[0x80]; - int read = fis.readNBytes(buf, 0, buf.length); - // If the PE/NE signature is beyond the initial buffer, read more - if (read >= 0x40) { - int peOffset = (buf[0x3C] & 0xFF) - | ((buf[0x3D] & 0xFF) << 8) - | ((buf[0x3E] & 0xFF) << 16) - | ((buf[0x3F] & 0xFF) << 24); - if (peOffset + 2 > read) { - // Need to read beyond the initial buffer - buf = java.util.Arrays.copyOf(buf, Math.max(buf.length, peOffset + 2)); - int remaining = buf.length - read; - fis.readNBytes(buf, read, remaining); - read = buf.length; - } - } - return detectFromBytes(buf, read); + return detectFromBytes(readHeaderBytes(fis)); } catch (IOException e) { LOG.warning("Could not read EXE header for " + exePath + ": " + e.getMessage()); return null; } } - /** - * Detect platform from pre-read MZ/PE header bytes. - * Shared between filesystem and ZIP-stream callers to avoid duplication. - */ + /** Read enough PE header bytes from any InputStream (shared by Path and ZIP callers). */ + static byte[] readHeaderBytes(InputStream is) throws IOException { + byte[] buf = new byte[0x200]; + int read = is.readNBytes(buf, 0, buf.length); + return read < buf.length ? java.util.Arrays.copyOf(buf, read) : buf; + } + + /** Detect platform from pre-read MZ/PE header bytes. */ + static String detectFromBytes(byte[] buf) { + return detectFromBytes(buf, buf.length); + } + + /** Detect platform from pre-read MZ/PE header bytes with explicit read length. */ static String detectFromBytes(byte[] buf, int read) { if (read < 2) return null; if (buf[0] != 0x4D || buf[1] != 0x5A) return null; @@ -77,4 +62,4 @@ public class PlatformDetector { return "dos"; } -} \ No newline at end of file +} diff --git a/src/main/java/com/dostalgia/UploadResource.java b/src/main/java/com/dostalgia/UploadResource.java index cc8fd21..157ede2 100644 --- a/src/main/java/com/dostalgia/UploadResource.java +++ b/src/main/java/com/dostalgia/UploadResource.java @@ -224,12 +224,12 @@ public class UploadResource { if (!Files.exists(dir)) return; Files.walkFileTree(dir, new SimpleFileVisitor<>() { @Override - public FileVisitResult visitFile(@Nonnull Path f, @Nonnull BasicFileAttributes a) throws IOException { + public @Nonnull FileVisitResult visitFile(@Nonnull Path f, @Nonnull BasicFileAttributes a) throws IOException { Files.delete(f); return FileVisitResult.CONTINUE; } @Override - public FileVisitResult postVisitDirectory(@Nonnull Path d, IOException e) throws IOException { + public @Nonnull FileVisitResult postVisitDirectory(@Nonnull Path d, IOException e) throws IOException { Files.delete(d); return FileVisitResult.CONTINUE; }