feat: add RuntimeType enum and RuntimeDetector for JAR probing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-12 12:59:00 +02:00
parent 51cc2c1d3c
commit cbf29a5d87
3 changed files with 212 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
package com.cameleer3.server.core.runtime;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
public final class RuntimeDetector {
private RuntimeDetector() {}
public record DetectionResult(RuntimeType runtimeType, String mainClass) {}
/**
* Detect the runtime type of a file (JAR or native binary).
* Returns a result with null runtimeType if detection fails.
*/
public static DetectionResult detect(Path file) {
if (!Files.exists(file) || Files.isDirectory(file)) {
return new DetectionResult(null, null);
}
// Check if it's a ZIP/JAR (starts with PK magic bytes)
if (!isZipFile(file)) {
// Non-ZIP file with content = native binary
try {
if (Files.size(file) > 0) {
return new DetectionResult(RuntimeType.NATIVE, null);
}
} catch (IOException e) {
// fall through
}
return new DetectionResult(null, null);
}
// It's a JAR — read the manifest
try (JarFile jar = new JarFile(file.toFile())) {
Manifest manifest = jar.getManifest();
if (manifest == null) {
return new DetectionResult(null, null);
}
String mainClass = manifest.getMainAttributes().getValue("Main-Class");
if (mainClass == null || mainClass.isBlank()) {
return new DetectionResult(null, null);
}
// Spring Boot: any launcher in org.springframework.boot.loader
if (mainClass.startsWith("org.springframework.boot.loader")) {
return new DetectionResult(RuntimeType.SPRING_BOOT, null);
}
// Quarkus
if (mainClass.equals("io.quarkus.bootstrap.runner.QuarkusEntryPoint")) {
return new DetectionResult(RuntimeType.QUARKUS, null);
}
// Plain Java: has a Main-Class, not a known framework
return new DetectionResult(RuntimeType.PLAIN_JAVA, mainClass);
} catch (IOException e) {
return new DetectionResult(null, null);
}
}
private static boolean isZipFile(Path file) {
try (InputStream is = Files.newInputStream(file)) {
byte[] magic = new byte[2];
int read = is.read(magic);
return read == 2 && magic[0] == 'P' && magic[1] == 'K';
} catch (IOException e) {
return false;
}
}
}

View File

@@ -0,0 +1,27 @@
package com.cameleer3.server.core.runtime;
public enum RuntimeType {
AUTO,
SPRING_BOOT,
QUARKUS,
PLAIN_JAVA,
NATIVE;
/** Parse from containerConfig string value, case-insensitive. Returns null if unrecognized. */
public static RuntimeType fromString(String value) {
if (value == null || value.isBlank()) return AUTO;
return switch (value.toLowerCase().replace("-", "_")) {
case "auto" -> AUTO;
case "spring_boot" -> SPRING_BOOT;
case "quarkus" -> QUARKUS;
case "plain_java" -> PLAIN_JAVA;
case "native" -> NATIVE;
default -> null;
};
}
/** Lowercase kebab-case for JSON serialization. */
public String toConfigValue() {
return name().toLowerCase().replace("_", "-");
}
}

View File

@@ -0,0 +1,108 @@
package com.cameleer3.server.core.runtime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.Attributes;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import static org.junit.jupiter.api.Assertions.*;
class RuntimeDetectorTest {
@TempDir
Path tempDir;
private Path createJarWithMainClass(String mainClass) throws IOException {
Path jar = tempDir.resolve("app.jar");
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, mainClass);
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar.toFile()), manifest)) {
// empty JAR with manifest
}
return jar;
}
private Path createJarWithoutMainClass() throws IOException {
Path jar = tempDir.resolve("no-main.jar");
Manifest manifest = new Manifest();
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jar.toFile()), manifest)) {
// empty JAR, no Main-Class
}
return jar;
}
@Test
void detectsSpringBootJarLauncher() throws IOException {
Path jar = createJarWithMainClass("org.springframework.boot.loader.launch.JarLauncher");
RuntimeDetector.DetectionResult result = RuntimeDetector.detect(jar);
assertEquals(RuntimeType.SPRING_BOOT, result.runtimeType());
assertNull(result.mainClass());
}
@Test
void detectsSpringBootPropertiesLauncher() throws IOException {
Path jar = createJarWithMainClass("org.springframework.boot.loader.launch.PropertiesLauncher");
RuntimeDetector.DetectionResult result = RuntimeDetector.detect(jar);
assertEquals(RuntimeType.SPRING_BOOT, result.runtimeType());
assertNull(result.mainClass());
}
@Test
void detectsSpringBootLegacyLauncher() throws IOException {
Path jar = createJarWithMainClass("org.springframework.boot.loader.JarLauncher");
RuntimeDetector.DetectionResult result = RuntimeDetector.detect(jar);
assertEquals(RuntimeType.SPRING_BOOT, result.runtimeType());
assertNull(result.mainClass());
}
@Test
void detectsQuarkus() throws IOException {
Path jar = createJarWithMainClass("io.quarkus.bootstrap.runner.QuarkusEntryPoint");
RuntimeDetector.DetectionResult result = RuntimeDetector.detect(jar);
assertEquals(RuntimeType.QUARKUS, result.runtimeType());
assertNull(result.mainClass());
}
@Test
void detectsPlainJava() throws IOException {
Path jar = createJarWithMainClass("com.example.MyApp");
RuntimeDetector.DetectionResult result = RuntimeDetector.detect(jar);
assertEquals(RuntimeType.PLAIN_JAVA, result.runtimeType());
assertEquals("com.example.MyApp", result.mainClass());
}
@Test
void detectsNativeBinary() throws IOException {
Path binary = tempDir.resolve("app");
// ELF header (not a ZIP/JAR)
Files.write(binary, new byte[]{0x7f, 'E', 'L', 'F', 0, 0, 0, 0});
RuntimeDetector.DetectionResult result = RuntimeDetector.detect(binary);
assertEquals(RuntimeType.NATIVE, result.runtimeType());
assertNull(result.mainClass());
}
@Test
void returnsNullForJarWithoutMainClass() throws IOException {
Path jar = createJarWithoutMainClass();
RuntimeDetector.DetectionResult result = RuntimeDetector.detect(jar);
assertNull(result.runtimeType());
assertNull(result.mainClass());
}
@Test
void returnsNullForEmptyFile() throws IOException {
Path empty = tempDir.resolve("empty");
Files.write(empty, new byte[0]);
RuntimeDetector.DetectionResult result = RuntimeDetector.detect(empty);
assertNull(result.runtimeType());
assertNull(result.mainClass());
}
}