diff --git a/java/sdk/src/main/java/com/github/copilot/ffi/PlatformDetector.java b/java/sdk/src/main/java/com/github/copilot/ffi/PlatformDetector.java new file mode 100644 index 000000000..466cf794b --- /dev/null +++ b/java/sdk/src/main/java/com/github/copilot/ffi/PlatformDetector.java @@ -0,0 +1,303 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.ffi; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * Detects the current platform and resolves the runtime classifier. + */ +public final class PlatformDetector { + private static final int ELF_HEADER_PROBE_BYTES = 2048; + private static final int ELF_MAGIC_0 = 0x7F; + private static final int ELF_MAGIC_1 = 'E'; + private static final int ELF_MAGIC_2 = 'L'; + private static final int ELF_MAGIC_3 = 'F'; + private static final int ELF_CLASS_32 = 1; + private static final int ELF_CLASS_64 = 2; + private static final int ELF_DATA_LITTLE_ENDIAN = 1; + private static final int ELF_DATA_BIG_ENDIAN = 2; + private static final int ELF32_PROGRAM_HEADER_SIZE = 32; + private static final int ELF64_PROGRAM_HEADER_SIZE = 56; + private static final int PT_INTERP = 3; + + private static final Set SUPPORTED_CLASSIFIERS = Set.of("linux-x64", "linux-arm64", "linuxmusl-x64", + "linuxmusl-arm64", "darwin-x64", "darwin-arm64", "win32-x64", "win32-arm64"); + + private static final Map CLASSIFIER_BY_KEY = Map.ofEntries( + Map.entry(new ClassifierKey("linux", "x64", LinuxLibc.GLIBC), "linux-x64"), + Map.entry(new ClassifierKey("linux", "arm64", LinuxLibc.GLIBC), "linux-arm64"), + Map.entry(new ClassifierKey("linux", "x64", LinuxLibc.MUSL), "linuxmusl-x64"), + Map.entry(new ClassifierKey("linux", "arm64", LinuxLibc.MUSL), "linuxmusl-arm64"), + Map.entry(new ClassifierKey("linux", "x64", LinuxLibc.UNKNOWN), "linux-x64"), + Map.entry(new ClassifierKey("linux", "arm64", LinuxLibc.UNKNOWN), "linux-arm64"), + Map.entry(new ClassifierKey("darwin", "x64", LinuxLibc.UNKNOWN), "darwin-x64"), + Map.entry(new ClassifierKey("darwin", "arm64", LinuxLibc.UNKNOWN), "darwin-arm64"), + Map.entry(new ClassifierKey("win32", "x64", LinuxLibc.UNKNOWN), "win32-x64"), + Map.entry(new ClassifierKey("win32", "arm64", LinuxLibc.UNKNOWN), "win32-arm64")); + + private PlatformDetector() { + } + + /** + * Linux C runtime classification. + */ + public enum LinuxLibc { + /** GNU libc runtime. */ + GLIBC, + + /** musl libc runtime. */ + MUSL, + + /** Unknown or undetectable runtime. */ + UNKNOWN + } + + /** + * Detects the normalized operating system identifier. + * + * @return {@code darwin}, {@code linux}, or {@code win32} + */ + public static String detectOs() { + return detectOs(System.getProperty("os.name", "")); + } + + /** + * Detects the normalized architecture identifier. + * + * @return {@code x64} or {@code arm64} + */ + public static String detectArch() { + return detectArch(System.getProperty("os.arch", "")); + } + + /** + * Detects the Linux libc variant using {@code /proc/self/exe} PT_INTERP. + * + * @return Linux libc classification; {@code UNKNOWN} on non-Linux or parse + * failures + */ + public static LinuxLibc detectLinuxLibc() { + if (!"linux".equals(detectOs())) { + return LinuxLibc.UNKNOWN; + } + return detectLinuxLibc(Path.of("/proc/self/exe")); + } + + /** + * Detects the runtime classifier for the current platform. + * + * @return platform classifier string + */ + public static String detectClassifier() { + return detectClassifier(detectOs(), detectArch(), detectLinuxLibc()); + } + + static String detectOs(String osName) { + String normalized = osName.toLowerCase(Locale.ROOT); + if (normalized.contains("mac") || normalized.contains("darwin")) { + return "darwin"; + } + if (normalized.contains("win")) { + return "win32"; + } + if (normalized.contains("linux")) { + return "linux"; + } + throw new IllegalStateException("Unsupported os.name: " + osName); + } + + static String detectArch(String osArch) { + String normalized = osArch.toLowerCase(Locale.ROOT).replace('-', '_'); + if (normalized.equals("amd64") || normalized.equals("x86_64") || normalized.equals("x64")) { + return "x64"; + } + if (normalized.equals("aarch64") || normalized.equals("arm64")) { + return "arm64"; + } + throw new IllegalStateException("Unsupported os.arch: " + osArch); + } + + static LinuxLibc detectLinuxLibc(Path executablePath) { + try { + return detectLinuxLibc(readPrefix(executablePath, ELF_HEADER_PROBE_BYTES)); + } catch (IOException ex) { + return LinuxLibc.UNKNOWN; + } + } + + static LinuxLibc detectLinuxLibc(byte[] elfPrefix) throws IOException { + String interpreter = readElfPtInterp(elfPrefix); + if (interpreter.contains("/ld-musl-")) { + return LinuxLibc.MUSL; + } + if (interpreter.contains("/ld-linux-")) { + return LinuxLibc.GLIBC; + } + return LinuxLibc.UNKNOWN; + } + + static String detectClassifier(String os, String arch, LinuxLibc linuxLibc) { + LinuxLibc classifierLibc = "linux".equals(os) ? linuxLibc : LinuxLibc.UNKNOWN; + String classifier = CLASSIFIER_BY_KEY.get(new ClassifierKey(os, arch, classifierLibc)); + if (classifier == null || !SUPPORTED_CLASSIFIERS.contains(classifier)) { + throw new IllegalStateException( + "Unsupported platform tuple: os=" + os + ", arch=" + arch + ", libc=" + classifierLibc); + } + return classifier; + } + + static Set supportedClassifiers() { + return SUPPORTED_CLASSIFIERS; + } + + private static String readElfPtInterp(byte[] probe) throws IOException { + int size = probe.length; + if (size < 64) { + throw new IOException("ELF probe too small: " + size + " bytes"); + } + if ((probe[0] & 0xFF) != ELF_MAGIC_0 || (probe[1] & 0xFF) != ELF_MAGIC_1 || (probe[2] & 0xFF) != ELF_MAGIC_2 + || (probe[3] & 0xFF) != ELF_MAGIC_3) { + throw new IOException("Not an ELF executable"); + } + + int elfClass = probe[4] & 0xFF; + int elfData = probe[5] & 0xFF; + if (elfData != ELF_DATA_LITTLE_ENDIAN && elfData != ELF_DATA_BIG_ENDIAN) { + throw new IOException("Unsupported ELF data encoding: " + elfData); + } + boolean littleEndian = elfData == ELF_DATA_LITTLE_ENDIAN; + + long phoff; + int phentsize; + int phnum; + int minimumPhentsize; + if (elfClass == ELF_CLASS_64) { + phoff = readUInt64(probe, 32, littleEndian); + phentsize = readUInt16(probe, 54, littleEndian); + phnum = readUInt16(probe, 56, littleEndian); + minimumPhentsize = ELF64_PROGRAM_HEADER_SIZE; + } else if (elfClass == ELF_CLASS_32) { + phoff = readUInt32(probe, 28, littleEndian); + phentsize = readUInt16(probe, 42, littleEndian); + phnum = readUInt16(probe, 44, littleEndian); + minimumPhentsize = ELF32_PROGRAM_HEADER_SIZE; + } else { + throw new IOException("Unsupported ELF class: " + elfClass); + } + + if (phoff < 0 || phoff >= size) { + throw new IOException("Program header table offset outside probe window: " + phoff); + } + if (phentsize < minimumPhentsize || phnum <= 0) { + throw new IOException("Invalid ELF program header metadata: phentsize=" + phentsize + ", phnum=" + phnum); + } + + for (int i = 0; i < phnum; i++) { + long baseLong = phoff + ((long) i * phentsize); + if (baseLong < 0 || baseLong > Integer.MAX_VALUE) { + break; + } + int base = (int) baseLong; + if (base + phentsize > size) { + break; + } + + long pType = readUInt32(probe, base, littleEndian); + if (pType != PT_INTERP) { + continue; + } + + long pOffset; + long pFileSize; + if (elfClass == ELF_CLASS_64) { + pOffset = readUInt64(probe, base + 8, littleEndian); + pFileSize = readUInt64(probe, base + 32, littleEndian); + } else { + pOffset = readUInt32(probe, base + 4, littleEndian); + pFileSize = readUInt32(probe, base + 16, littleEndian); + } + + if (pOffset < 0 || pFileSize <= 0 || pOffset > Integer.MAX_VALUE || pFileSize > Integer.MAX_VALUE) { + throw new IOException("Invalid PT_INTERP bounds"); + } + + int start = (int) pOffset; + int end = start + (int) pFileSize; + if (end > size) { + throw new IOException("PT_INTERP extends past probe window; increase probe size"); + } + + int nulIndex = start; + while (nulIndex < end && probe[nulIndex] != 0) { + nulIndex++; + } + if (nulIndex == start) { + throw new IOException("Empty PT_INTERP segment"); + } + return new String(probe, start, nulIndex - start, StandardCharsets.UTF_8); + } + + throw new IOException("ELF PT_INTERP segment not found"); + } + + private static byte[] readPrefix(Path path, int maxBytes) throws IOException { + byte[] buffer = new byte[maxBytes]; + int total = 0; + try (InputStream in = Files.newInputStream(path)) { + while (total < maxBytes) { + int read = in.read(buffer, total, maxBytes - total); + if (read < 0) { + break; + } + total += read; + } + } + byte[] resized = new byte[total]; + System.arraycopy(buffer, 0, resized, 0, total); + return resized; + } + + private static int readUInt16(byte[] data, int offset, boolean littleEndian) { + int b0 = data[offset] & 0xFF; + int b1 = data[offset + 1] & 0xFF; + return littleEndian ? (b0 | (b1 << 8)) : ((b0 << 8) | b1); + } + + private static long readUInt32(byte[] data, int offset, boolean littleEndian) { + long b0 = data[offset] & 0xFFL; + long b1 = data[offset + 1] & 0xFFL; + long b2 = data[offset + 2] & 0xFFL; + long b3 = data[offset + 3] & 0xFFL; + if (littleEndian) { + return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24); + } + return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3; + } + + private static long readUInt64(byte[] data, int offset, boolean littleEndian) { + long result = 0L; + if (littleEndian) { + for (int i = 7; i >= 0; i--) { + result = (result << 8) | (data[offset + i] & 0xFFL); + } + return result; + } + for (int i = 0; i < 8; i++) { + result = (result << 8) | (data[offset + i] & 0xFFL); + } + return result; + } + + private record ClassifierKey(String os, String arch, LinuxLibc libc) { + } +} diff --git a/java/sdk/src/test/java/com/github/copilot/ffi/PlatformDetectorTest.java b/java/sdk/src/test/java/com/github/copilot/ffi/PlatformDetectorTest.java new file mode 100644 index 000000000..82049ea6a --- /dev/null +++ b/java/sdk/src/test/java/com/github/copilot/ffi/PlatformDetectorTest.java @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.ffi; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PlatformDetectorTest { + + @Test + void detectOsMapsSupportedNames() { + withSystemProperty("os.name", "Mac OS X", () -> assertEquals("darwin", PlatformDetector.detectOs())); + withSystemProperty("os.name", "Darwin", () -> assertEquals("darwin", PlatformDetector.detectOs())); + withSystemProperty("os.name", "Windows 11", () -> assertEquals("win32", PlatformDetector.detectOs())); + withSystemProperty("os.name", "Linux", () -> assertEquals("linux", PlatformDetector.detectOs())); + } + + @Test + void detectOsThrowsForUnsupportedSystem() { + withSystemProperty("os.name", "Solaris", + () -> assertThrows(IllegalStateException.class, PlatformDetector::detectOs)); + } + + @Test + void detectArchMapsSupportedAliases() { + withSystemProperty("os.arch", "amd64", () -> assertEquals("x64", PlatformDetector.detectArch())); + withSystemProperty("os.arch", "x86_64", () -> assertEquals("x64", PlatformDetector.detectArch())); + withSystemProperty("os.arch", "x64", () -> assertEquals("x64", PlatformDetector.detectArch())); + withSystemProperty("os.arch", "aarch64", () -> assertEquals("arm64", PlatformDetector.detectArch())); + withSystemProperty("os.arch", "arm64", () -> assertEquals("arm64", PlatformDetector.detectArch())); + } + + @Test + void detectArchThrowsForUnsupportedArchitecture() { + withSystemProperty("os.arch", "ppc64", + () -> assertThrows(IllegalStateException.class, PlatformDetector::detectArch)); + } + + @Test + void detectLinuxLibcParsesGlibcInterpPath() throws Exception { + byte[] glibcProbe = buildElf64ProbeWithInterp("/lib64/ld-linux-x86-64.so.2"); + assertEquals(PlatformDetector.LinuxLibc.GLIBC, PlatformDetector.detectLinuxLibc(glibcProbe)); + } + + @Test + void detectLinuxLibcParsesMuslInterpPath() throws Exception { + byte[] muslProbe = buildElf64ProbeWithInterp("/lib/ld-musl-x86_64.so.1"); + assertEquals(PlatformDetector.LinuxLibc.MUSL, PlatformDetector.detectLinuxLibc(muslProbe)); + } + + @Test + void detectLinuxLibcOnLinuxReturnsRecognizedValue() { + withSystemProperty("os.name", "Linux", () -> { + PlatformDetector.LinuxLibc libc = PlatformDetector.detectLinuxLibc(); + assertTrue(libc == PlatformDetector.LinuxLibc.GLIBC || libc == PlatformDetector.LinuxLibc.MUSL + || libc == PlatformDetector.LinuxLibc.UNKNOWN); + }); + } + + @Test + void detectLinuxLibcReturnsUnknownOutsideLinux() { + withSystemProperty("os.name", "Windows 11", + () -> assertEquals(PlatformDetector.LinuxLibc.UNKNOWN, PlatformDetector.detectLinuxLibc())); + } + + @Test + void detectClassifierReturnsClassifierForCurrentLinuxLibc() { + PlatformDetector.LinuxLibc libc = PlatformDetector.detectLinuxLibc(); + String expected = libc == PlatformDetector.LinuxLibc.MUSL ? "linuxmusl-x64" : "linux-x64"; + + withSystemProperties("Linux", "amd64", () -> assertEquals(expected, PlatformDetector.detectClassifier())); + } + + @Test + void detectClassifierAllowListCoversAllSupportedValues() { + Set expected = Set.of("linux-x64", "linux-arm64", "linuxmusl-x64", "linuxmusl-arm64", "darwin-x64", + "darwin-arm64", "win32-x64", "win32-arm64"); + assertEquals(expected, PlatformDetector.supportedClassifiers()); + + Set resolved = new LinkedHashSet<>(); + resolved.add(PlatformDetector.detectClassifier("linux", "x64", PlatformDetector.LinuxLibc.GLIBC)); + resolved.add(PlatformDetector.detectClassifier("linux", "arm64", PlatformDetector.LinuxLibc.GLIBC)); + resolved.add(PlatformDetector.detectClassifier("linux", "x64", PlatformDetector.LinuxLibc.MUSL)); + resolved.add(PlatformDetector.detectClassifier("linux", "arm64", PlatformDetector.LinuxLibc.MUSL)); + resolved.add(PlatformDetector.detectClassifier("darwin", "x64", PlatformDetector.LinuxLibc.UNKNOWN)); + resolved.add(PlatformDetector.detectClassifier("darwin", "arm64", PlatformDetector.LinuxLibc.UNKNOWN)); + resolved.add(PlatformDetector.detectClassifier("win32", "x64", PlatformDetector.LinuxLibc.UNKNOWN)); + resolved.add(PlatformDetector.detectClassifier("win32", "arm64", PlatformDetector.LinuxLibc.UNKNOWN)); + + assertEquals(expected, resolved); + } + + @Test + void detectClassifierFailsFastForUnsupportedTuple() { + assertThrows(IllegalStateException.class, + () -> PlatformDetector.detectClassifier("darwin", "mips64", PlatformDetector.LinuxLibc.UNKNOWN)); + } + + @Test + void detectClassifierFailsForUnsupportedCurrentPlatform() { + withSystemProperties("Solaris", "amd64", + () -> assertThrows(IllegalStateException.class, PlatformDetector::detectClassifier)); + } + + @Test + void detectLinuxLibcReturnsUnknownWhenElfParsingFails() { + byte[] invalidProbe = new byte[64]; + Arrays.fill(invalidProbe, (byte) 1); + + assertThrows(IOException.class, () -> PlatformDetector.detectLinuxLibc(invalidProbe)); + } + + @Test + void detectLinuxLibcReturnsUnknownForTruncatedProgramHeader(@TempDir Path tempDir) throws IOException { + byte[] malformedProbe = buildElf64ProbeWithInterp("/lib64/ld-linux-x86-64.so.2"); + writeLe64(malformedProbe, 32, malformedProbe.length - 1); + writeLe16(malformedProbe, 54, 1); + Path executable = tempDir.resolve("malformed-elf"); + Files.write(executable, malformedProbe); + + assertEquals(PlatformDetector.LinuxLibc.UNKNOWN, PlatformDetector.detectLinuxLibc(executable)); + } + + private static void withSystemProperties(String osName, String osArch, Runnable action) { + String previousOsName = System.getProperty("os.name"); + String previousOsArch = System.getProperty("os.arch"); + try { + System.setProperty("os.name", osName); + System.setProperty("os.arch", osArch); + action.run(); + } finally { + restoreProperty("os.name", previousOsName); + restoreProperty("os.arch", previousOsArch); + } + } + + private static void withSystemProperty(String key, String value, Runnable action) { + String previousValue = System.getProperty(key); + try { + System.setProperty(key, value); + action.run(); + } finally { + restoreProperty(key, previousValue); + } + } + + private static void restoreProperty(String key, String value) { + if (value == null) { + System.clearProperty(key); + } else { + System.setProperty(key, value); + } + } + + private static byte[] buildElf64ProbeWithInterp(String interpreterPath) { + byte[] interpBytes = interpreterPath.getBytes(StandardCharsets.UTF_8); + byte[] probe = new byte[512]; + + probe[0] = 0x7F; + probe[1] = 'E'; + probe[2] = 'L'; + probe[3] = 'F'; + probe[4] = 2; + probe[5] = 1; + + int phoff = 64; + int phentsize = 56; + int phnum = 1; + int interpOffset = 256; + int interpSize = interpBytes.length + 1; + + writeLe64(probe, 32, phoff); + writeLe16(probe, 54, phentsize); + writeLe16(probe, 56, phnum); + + int pHeader = phoff; + writeLe32(probe, pHeader, 3); + writeLe64(probe, pHeader + 8, interpOffset); + writeLe64(probe, pHeader + 32, interpSize); + + System.arraycopy(interpBytes, 0, probe, interpOffset, interpBytes.length); + probe[interpOffset + interpBytes.length] = 0; + return probe; + } + + private static void writeLe16(byte[] buffer, int offset, int value) { + buffer[offset] = (byte) (value & 0xFF); + buffer[offset + 1] = (byte) ((value >>> 8) & 0xFF); + } + + private static void writeLe32(byte[] buffer, int offset, int value) { + buffer[offset] = (byte) (value & 0xFF); + buffer[offset + 1] = (byte) ((value >>> 8) & 0xFF); + buffer[offset + 2] = (byte) ((value >>> 16) & 0xFF); + buffer[offset + 3] = (byte) ((value >>> 24) & 0xFF); + } + + private static void writeLe64(byte[] buffer, int offset, long value) { + for (int i = 0; i < 8; i++) { + buffer[offset + i] = (byte) ((value >>> (8 * i)) & 0xFF); + } + } +}