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:
@@ -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];
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user