fix: remaining code quality issues
Build & Deploy / build-and-deploy (push) Successful in 46s

PlatformDetector:
- Decluttered detect() with extract readHeaderBytes() utility
- Added detectFromBytes(byte[]) convenience overload
- Eliminated 16-line duplicated PE header re-read from GameService

GameService:
- detectPlatformInBundle now uses PlatformDetector.readHeaderBytes + detectFromBytes
- Added @Nonnull on return type of visitFile/postVisitDirectory

ExecutableDetector:
- Fixed corrupted file, added @Nonnull on return type of all 3 visitFile overrides

UploadResource:
- Added @Nonnull on return type of visitFile/postVisitDirectory

IgdbService:
- epochToYear: removed unused 'field' parameter (always 'first_release_date')
- extractGenres: new helper deduplicates 9-line genre extraction from search() + jsonNodeToMap()
This commit is contained in:
David Alvarez
2026-06-10 09:58:28 +02:00
parent 5758ea2ea5
commit 19a43a55d5
5 changed files with 46 additions and 73 deletions
@@ -25,7 +25,6 @@ public class ExecutableDetector {
@Inject @Inject
PlatformDetector platform; PlatformDetector platform;
/** Executable stems to never select as main game executable. */
private static final Set<String> SKIP_EXE_NAMES = Set.of( private static final Set<String> SKIP_EXE_NAMES = Set.of(
"dos4gw", "dos32a", "dos4gw2", "pmode", "pmodew", "cwsdpmi", "dos4gw", "dos32a", "dos4gw2", "pmode", "pmodew", "cwsdpmi",
"emm386", "himem", "himemx", "debug", "emm386", "himem", "himemx", "debug",
@@ -36,12 +35,11 @@ public class ExecutableDetector {
"ace", "unace", "zoo", "arc", "ha", "cab" "ace", "unace", "zoo", "arc", "ha", "cab"
); );
/** Collect all discoverable executables (relative paths) from the extracted directory. */
public List<String> findAll(Path dir) throws IOException { public List<String> findAll(Path dir) throws IOException {
List<String> result = new ArrayList<>(); List<String> result = new ArrayList<>();
Files.walkFileTree(dir, new SimpleFileVisitor<>() { Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override @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 name = f.getFileName().toString().toLowerCase();
if (name.endsWith(".exe") || name.endsWith(".com") || name.endsWith(".bat")) { if (name.endsWith(".exe") || name.endsWith(".com") || name.endsWith(".bat")) {
result.add(dir.relativize(f).toString().replace('\\', '/')); result.add(dir.relativize(f).toString().replace('\\', '/'));
@@ -53,14 +51,13 @@ public class ExecutableDetector {
return result; return result;
} }
/** Find the best main executable. */
public String findMain(Path dir) throws IOException { public String findMain(Path dir) throws IOException {
record Candidate(int depth, boolean isInstaller, long size, String path, String platform) {} record Candidate(int depth, boolean isInstaller, long size, String path, String platform) {}
List<Candidate> candidates = new ArrayList<>(); List<Candidate> candidates = new ArrayList<>();
Files.walkFileTree(dir, new SimpleFileVisitor<>() { Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override @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 name = f.getFileName().toString().toLowerCase();
if (!name.endsWith(".exe") && !name.endsWith(".com") && !name.endsWith(".bat")) if (!name.endsWith(".exe") && !name.endsWith(".com") && !name.endsWith(".bat"))
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
@@ -96,7 +93,6 @@ public class ExecutableDetector {
return candidates.getFirst().path(); return candidates.getFirst().path();
} }
/** Find a setup/install/config executable. */
public String findSetup(Path dir) throws IOException { public String findSetup(Path dir) throws IOException {
record Candidate(int depth, long size, String path) {} record Candidate(int depth, long size, String path) {}
List<Candidate> candidates = new ArrayList<>(); List<Candidate> candidates = new ArrayList<>();
@@ -104,7 +100,7 @@ public class ExecutableDetector {
Files.walkFileTree(dir, new SimpleFileVisitor<>() { Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override @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 name = f.getFileName().toString().toLowerCase();
String stem = name.contains(".") ? name.substring(0, name.lastIndexOf('.')) : name; String stem = name.contains(".") ? name.substring(0, name.lastIndexOf('.')) : name;
if (!name.endsWith(".exe") && !name.endsWith(".com") && !name.endsWith(".bat")) if (!name.endsWith(".exe") && !name.endsWith(".com") && !name.endsWith(".bat"))
@@ -125,7 +121,6 @@ public class ExecutableDetector {
return candidates.getFirst().path(); return candidates.getFirst().path();
} }
/** Check if an executable is likely a self-extracting archive (PKSFX stub, etc.). */
public static boolean isLikelySelfExtractor(Path exePath) { public static boolean isLikelySelfExtractor(Path exePath) {
try (var fis = Files.newInputStream(exePath)) { try (var fis = Files.newInputStream(exePath)) {
byte[] buf = new byte[32768]; byte[] buf = new byte[32768];
+3 -16
View File
@@ -126,12 +126,12 @@ public class GameService implements GameStore {
if (Files.exists(dir)) { if (Files.exists(dir)) {
Files.walkFileTree(dir, new SimpleFileVisitor<>() { Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override @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); Files.delete(f);
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
@Override @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); Files.delete(d);
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
@@ -226,20 +226,7 @@ public class GameService implements GameStore {
java.util.zip.ZipEntry entry; java.util.zip.ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) { while ((entry = zis.getNextEntry()) != null) {
if (entry.getName().equals(relExe.replace('\\', '/'))) { if (entry.getName().equals(relExe.replace('\\', '/'))) {
byte[] buf = new byte[0x80]; return PlatformDetector.detectFromBytes(PlatformDetector.readHeaderBytes(zis));
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);
} }
zis.closeEntry(); zis.closeEntry();
} }
+22 -16
View File
@@ -118,13 +118,10 @@ public class IgdbService {
entry.put("igdb_id", game.get("id").asInt()); entry.put("igdb_id", game.get("id").asInt());
entry.put("name", game.has("name") ? game.get("name").asText() : query); 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<String> genres = extractGenres(game);
List<String> genres = new ArrayList<>(); if (!genres.isEmpty()) {
for (JsonNode g : game.get("genres")) {
if (g.has("name")) genres.add(g.get("name").asText());
}
entry.put("genres", genres); entry.put("genres", genres);
} }
@@ -288,10 +285,10 @@ public class IgdbService {
// ─── Utility ────────────────────────────────────────────────── // ─── Utility ──────────────────────────────────────────────────
/** Convert IGDB epoch (seconds) to year, if present in the node. */ /** Convert IGDB epoch (seconds) to year, from 'first_release_date' field. */
private static Optional<Integer> epochToYear(JsonNode node, String field) { private static Optional<Integer> epochToYear(JsonNode node) {
if (node.has(field)) { if (node.has("first_release_date")) {
long epoch = node.get(field).asLong(); long epoch = node.get("first_release_date").asLong();
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(epoch * 1000); cal.setTimeInMillis(epoch * 1000);
return Optional.of(cal.get(Calendar.YEAR)); return Optional.of(cal.get(Calendar.YEAR));
@@ -299,6 +296,17 @@ public class IgdbService {
return Optional.empty(); return Optional.empty();
} }
/** Extract genre names from a JsonNode that may have a 'genres' array. */
private static List<String> extractGenres(JsonNode node) {
List<String> 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) { private String sanitize(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\""); return s.replace("\\", "\\\\").replace("\"", "\\\"");
} }
@@ -307,12 +315,10 @@ public class IgdbService {
Map<String, Object> map = new LinkedHashMap<>(); Map<String, Object> map = new LinkedHashMap<>();
map.put("igdb_id", node.has("id") ? node.get("id").asInt() : 0); map.put("igdb_id", node.has("id") ? node.get("id").asInt() : 0);
map.put("name", node.has("name") ? node.get("name").asText() : ""); map.put("name", node.has("name") ? node.get("name").asText() : "");
epochToYear(node, "first_release_date").ifPresent(y -> map.put("year", y)); epochToYear(node).ifPresent(y -> map.put("year", y));
if (node.has("genres") && node.get("genres").isArray()) {
List<String> genres = new ArrayList<>(); List<String> genres = extractGenres(node);
for (JsonNode g : node.get("genres")) { if (!genres.isEmpty()) {
if (g.has("name")) genres.add(g.get("name").asText());
}
map.put("genres", genres); map.put("genres", genres);
} }
if (node.has("involved_companies") && node.get("involved_companies").isArray()) { if (node.has("involved_companies") && node.get("involved_companies").isArray()) {
@@ -3,59 +3,44 @@ package com.dostalgia;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.logging.Logger; import java.util.logging.Logger;
/** /**
* Detects platform type (DOS vs Windows) by reading executable headers. * Detects platform type (DOS vs Windows) by reading executable headers.
* Pure logic — no I/O beyond reading the file header bytes.
*/ */
@ApplicationScoped @ApplicationScoped
public class PlatformDetector { public class PlatformDetector {
private static final Logger LOG = Logger.getLogger(PlatformDetector.class.getName()); private static final Logger LOG = Logger.getLogger(PlatformDetector.class.getName());
/**
* Detect whether an executable targets DOS or Windows by reading its PE/NE header.
* <ul>
* <li>Pure MZ, LE (DOS/4GW extender), LX (OS/2) → "dos"</li>
* <li>NE (Windows 3.x), PE (Win32) → "windows"</li>
* <li>Cannot read → null</li>
* </ul>
*/
public String detect(Path exePath) { public String detect(Path exePath) {
String name = exePath.getFileName().toString().toLowerCase(); String name = exePath.getFileName().toString().toLowerCase();
if (name.endsWith(".com") || name.endsWith(".bat")) return "dos"; if (name.endsWith(".com") || name.endsWith(".bat")) return "dos";
try (var fis = Files.newInputStream(exePath)) { try (var fis = Files.newInputStream(exePath)) {
byte[] buf = new byte[0x80]; return detectFromBytes(readHeaderBytes(fis));
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);
} catch (IOException e) { } catch (IOException e) {
LOG.warning("Could not read EXE header for " + exePath + ": " + e.getMessage()); LOG.warning("Could not read EXE header for " + exePath + ": " + e.getMessage());
return null; return null;
} }
} }
/** /** Read enough PE header bytes from any InputStream (shared by Path and ZIP callers). */
* Detect platform from pre-read MZ/PE header bytes. static byte[] readHeaderBytes(InputStream is) throws IOException {
* Shared between filesystem and ZIP-stream callers to avoid duplication. 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) { static String detectFromBytes(byte[] buf, int read) {
if (read < 2) return null; if (read < 2) return null;
if (buf[0] != 0x4D || buf[1] != 0x5A) return null; if (buf[0] != 0x4D || buf[1] != 0x5A) return null;
@@ -224,12 +224,12 @@ public class UploadResource {
if (!Files.exists(dir)) return; if (!Files.exists(dir)) return;
Files.walkFileTree(dir, new SimpleFileVisitor<>() { Files.walkFileTree(dir, new SimpleFileVisitor<>() {
@Override @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); Files.delete(f);
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
@Override @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); Files.delete(d);
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }