Skip to content

Commit 2bdfb53

Browse files
CopilotedburnsCopilot
committed
[Java] Add PlatformDetector for runtime classifier selection (#2173)
* Initial plan * [Java] Add PlatformDetector utility and tests Co-authored-by: edburns <75821+edburns@users.noreply.github.com> * Address PlatformDetector review findings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: edburns <75821+edburns@users.noreply.github.com> Co-authored-by: Ed Burns <edburns@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a7651ff commit 2bdfb53

2 files changed

Lines changed: 520 additions & 0 deletions

File tree

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.ffi;
6+
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
import java.nio.charset.StandardCharsets;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.util.Locale;
13+
import java.util.Map;
14+
import java.util.Set;
15+
16+
/**
17+
* Detects the current platform and resolves the runtime classifier.
18+
*/
19+
public final class PlatformDetector {
20+
private static final int ELF_HEADER_PROBE_BYTES = 2048;
21+
private static final int ELF_MAGIC_0 = 0x7F;
22+
private static final int ELF_MAGIC_1 = 'E';
23+
private static final int ELF_MAGIC_2 = 'L';
24+
private static final int ELF_MAGIC_3 = 'F';
25+
private static final int ELF_CLASS_32 = 1;
26+
private static final int ELF_CLASS_64 = 2;
27+
private static final int ELF_DATA_LITTLE_ENDIAN = 1;
28+
private static final int ELF_DATA_BIG_ENDIAN = 2;
29+
private static final int ELF32_PROGRAM_HEADER_SIZE = 32;
30+
private static final int ELF64_PROGRAM_HEADER_SIZE = 56;
31+
private static final int PT_INTERP = 3;
32+
33+
private static final Set<String> SUPPORTED_CLASSIFIERS = Set.of("linux-x64", "linux-arm64", "linuxmusl-x64",
34+
"linuxmusl-arm64", "darwin-x64", "darwin-arm64", "win32-x64", "win32-arm64");
35+
36+
private static final Map<ClassifierKey, String> CLASSIFIER_BY_KEY = Map.ofEntries(
37+
Map.entry(new ClassifierKey("linux", "x64", LinuxLibc.GLIBC), "linux-x64"),
38+
Map.entry(new ClassifierKey("linux", "arm64", LinuxLibc.GLIBC), "linux-arm64"),
39+
Map.entry(new ClassifierKey("linux", "x64", LinuxLibc.MUSL), "linuxmusl-x64"),
40+
Map.entry(new ClassifierKey("linux", "arm64", LinuxLibc.MUSL), "linuxmusl-arm64"),
41+
Map.entry(new ClassifierKey("linux", "x64", LinuxLibc.UNKNOWN), "linux-x64"),
42+
Map.entry(new ClassifierKey("linux", "arm64", LinuxLibc.UNKNOWN), "linux-arm64"),
43+
Map.entry(new ClassifierKey("darwin", "x64", LinuxLibc.UNKNOWN), "darwin-x64"),
44+
Map.entry(new ClassifierKey("darwin", "arm64", LinuxLibc.UNKNOWN), "darwin-arm64"),
45+
Map.entry(new ClassifierKey("win32", "x64", LinuxLibc.UNKNOWN), "win32-x64"),
46+
Map.entry(new ClassifierKey("win32", "arm64", LinuxLibc.UNKNOWN), "win32-arm64"));
47+
48+
private PlatformDetector() {
49+
}
50+
51+
/**
52+
* Linux C runtime classification.
53+
*/
54+
public enum LinuxLibc {
55+
/** GNU libc runtime. */
56+
GLIBC,
57+
58+
/** musl libc runtime. */
59+
MUSL,
60+
61+
/** Unknown or undetectable runtime. */
62+
UNKNOWN
63+
}
64+
65+
/**
66+
* Detects the normalized operating system identifier.
67+
*
68+
* @return {@code darwin}, {@code linux}, or {@code win32}
69+
*/
70+
public static String detectOs() {
71+
return detectOs(System.getProperty("os.name", ""));
72+
}
73+
74+
/**
75+
* Detects the normalized architecture identifier.
76+
*
77+
* @return {@code x64} or {@code arm64}
78+
*/
79+
public static String detectArch() {
80+
return detectArch(System.getProperty("os.arch", ""));
81+
}
82+
83+
/**
84+
* Detects the Linux libc variant using {@code /proc/self/exe} PT_INTERP.
85+
*
86+
* @return Linux libc classification; {@code UNKNOWN} on non-Linux or parse
87+
* failures
88+
*/
89+
public static LinuxLibc detectLinuxLibc() {
90+
if (!"linux".equals(detectOs())) {
91+
return LinuxLibc.UNKNOWN;
92+
}
93+
return detectLinuxLibc(Path.of("/proc/self/exe"));
94+
}
95+
96+
/**
97+
* Detects the runtime classifier for the current platform.
98+
*
99+
* @return platform classifier string
100+
*/
101+
public static String detectClassifier() {
102+
return detectClassifier(detectOs(), detectArch(), detectLinuxLibc());
103+
}
104+
105+
static String detectOs(String osName) {
106+
String normalized = osName.toLowerCase(Locale.ROOT);
107+
if (normalized.contains("mac") || normalized.contains("darwin")) {
108+
return "darwin";
109+
}
110+
if (normalized.contains("win")) {
111+
return "win32";
112+
}
113+
if (normalized.contains("linux")) {
114+
return "linux";
115+
}
116+
throw new IllegalStateException("Unsupported os.name: " + osName);
117+
}
118+
119+
static String detectArch(String osArch) {
120+
String normalized = osArch.toLowerCase(Locale.ROOT).replace('-', '_');
121+
if (normalized.equals("amd64") || normalized.equals("x86_64") || normalized.equals("x64")) {
122+
return "x64";
123+
}
124+
if (normalized.equals("aarch64") || normalized.equals("arm64")) {
125+
return "arm64";
126+
}
127+
throw new IllegalStateException("Unsupported os.arch: " + osArch);
128+
}
129+
130+
static LinuxLibc detectLinuxLibc(Path executablePath) {
131+
try {
132+
return detectLinuxLibc(readPrefix(executablePath, ELF_HEADER_PROBE_BYTES));
133+
} catch (IOException ex) {
134+
return LinuxLibc.UNKNOWN;
135+
}
136+
}
137+
138+
static LinuxLibc detectLinuxLibc(byte[] elfPrefix) throws IOException {
139+
String interpreter = readElfPtInterp(elfPrefix);
140+
if (interpreter.contains("/ld-musl-")) {
141+
return LinuxLibc.MUSL;
142+
}
143+
if (interpreter.contains("/ld-linux-")) {
144+
return LinuxLibc.GLIBC;
145+
}
146+
return LinuxLibc.UNKNOWN;
147+
}
148+
149+
static String detectClassifier(String os, String arch, LinuxLibc linuxLibc) {
150+
LinuxLibc classifierLibc = "linux".equals(os) ? linuxLibc : LinuxLibc.UNKNOWN;
151+
String classifier = CLASSIFIER_BY_KEY.get(new ClassifierKey(os, arch, classifierLibc));
152+
if (classifier == null || !SUPPORTED_CLASSIFIERS.contains(classifier)) {
153+
throw new IllegalStateException(
154+
"Unsupported platform tuple: os=" + os + ", arch=" + arch + ", libc=" + classifierLibc);
155+
}
156+
return classifier;
157+
}
158+
159+
static Set<String> supportedClassifiers() {
160+
return SUPPORTED_CLASSIFIERS;
161+
}
162+
163+
private static String readElfPtInterp(byte[] probe) throws IOException {
164+
int size = probe.length;
165+
if (size < 64) {
166+
throw new IOException("ELF probe too small: " + size + " bytes");
167+
}
168+
if ((probe[0] & 0xFF) != ELF_MAGIC_0 || (probe[1] & 0xFF) != ELF_MAGIC_1 || (probe[2] & 0xFF) != ELF_MAGIC_2
169+
|| (probe[3] & 0xFF) != ELF_MAGIC_3) {
170+
throw new IOException("Not an ELF executable");
171+
}
172+
173+
int elfClass = probe[4] & 0xFF;
174+
int elfData = probe[5] & 0xFF;
175+
if (elfData != ELF_DATA_LITTLE_ENDIAN && elfData != ELF_DATA_BIG_ENDIAN) {
176+
throw new IOException("Unsupported ELF data encoding: " + elfData);
177+
}
178+
boolean littleEndian = elfData == ELF_DATA_LITTLE_ENDIAN;
179+
180+
long phoff;
181+
int phentsize;
182+
int phnum;
183+
int minimumPhentsize;
184+
if (elfClass == ELF_CLASS_64) {
185+
phoff = readUInt64(probe, 32, littleEndian);
186+
phentsize = readUInt16(probe, 54, littleEndian);
187+
phnum = readUInt16(probe, 56, littleEndian);
188+
minimumPhentsize = ELF64_PROGRAM_HEADER_SIZE;
189+
} else if (elfClass == ELF_CLASS_32) {
190+
phoff = readUInt32(probe, 28, littleEndian);
191+
phentsize = readUInt16(probe, 42, littleEndian);
192+
phnum = readUInt16(probe, 44, littleEndian);
193+
minimumPhentsize = ELF32_PROGRAM_HEADER_SIZE;
194+
} else {
195+
throw new IOException("Unsupported ELF class: " + elfClass);
196+
}
197+
198+
if (phoff < 0 || phoff >= size) {
199+
throw new IOException("Program header table offset outside probe window: " + phoff);
200+
}
201+
if (phentsize < minimumPhentsize || phnum <= 0) {
202+
throw new IOException("Invalid ELF program header metadata: phentsize=" + phentsize + ", phnum=" + phnum);
203+
}
204+
205+
for (int i = 0; i < phnum; i++) {
206+
long baseLong = phoff + ((long) i * phentsize);
207+
if (baseLong < 0 || baseLong > Integer.MAX_VALUE) {
208+
break;
209+
}
210+
int base = (int) baseLong;
211+
if (base + phentsize > size) {
212+
break;
213+
}
214+
215+
long pType = readUInt32(probe, base, littleEndian);
216+
if (pType != PT_INTERP) {
217+
continue;
218+
}
219+
220+
long pOffset;
221+
long pFileSize;
222+
if (elfClass == ELF_CLASS_64) {
223+
pOffset = readUInt64(probe, base + 8, littleEndian);
224+
pFileSize = readUInt64(probe, base + 32, littleEndian);
225+
} else {
226+
pOffset = readUInt32(probe, base + 4, littleEndian);
227+
pFileSize = readUInt32(probe, base + 16, littleEndian);
228+
}
229+
230+
if (pOffset < 0 || pFileSize <= 0 || pOffset > Integer.MAX_VALUE || pFileSize > Integer.MAX_VALUE) {
231+
throw new IOException("Invalid PT_INTERP bounds");
232+
}
233+
234+
int start = (int) pOffset;
235+
int end = start + (int) pFileSize;
236+
if (end > size) {
237+
throw new IOException("PT_INTERP extends past probe window; increase probe size");
238+
}
239+
240+
int nulIndex = start;
241+
while (nulIndex < end && probe[nulIndex] != 0) {
242+
nulIndex++;
243+
}
244+
if (nulIndex == start) {
245+
throw new IOException("Empty PT_INTERP segment");
246+
}
247+
return new String(probe, start, nulIndex - start, StandardCharsets.UTF_8);
248+
}
249+
250+
throw new IOException("ELF PT_INTERP segment not found");
251+
}
252+
253+
private static byte[] readPrefix(Path path, int maxBytes) throws IOException {
254+
byte[] buffer = new byte[maxBytes];
255+
int total = 0;
256+
try (InputStream in = Files.newInputStream(path)) {
257+
while (total < maxBytes) {
258+
int read = in.read(buffer, total, maxBytes - total);
259+
if (read < 0) {
260+
break;
261+
}
262+
total += read;
263+
}
264+
}
265+
byte[] resized = new byte[total];
266+
System.arraycopy(buffer, 0, resized, 0, total);
267+
return resized;
268+
}
269+
270+
private static int readUInt16(byte[] data, int offset, boolean littleEndian) {
271+
int b0 = data[offset] & 0xFF;
272+
int b1 = data[offset + 1] & 0xFF;
273+
return littleEndian ? (b0 | (b1 << 8)) : ((b0 << 8) | b1);
274+
}
275+
276+
private static long readUInt32(byte[] data, int offset, boolean littleEndian) {
277+
long b0 = data[offset] & 0xFFL;
278+
long b1 = data[offset + 1] & 0xFFL;
279+
long b2 = data[offset + 2] & 0xFFL;
280+
long b3 = data[offset + 3] & 0xFFL;
281+
if (littleEndian) {
282+
return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
283+
}
284+
return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3;
285+
}
286+
287+
private static long readUInt64(byte[] data, int offset, boolean littleEndian) {
288+
long result = 0L;
289+
if (littleEndian) {
290+
for (int i = 7; i >= 0; i--) {
291+
result = (result << 8) | (data[offset + i] & 0xFFL);
292+
}
293+
return result;
294+
}
295+
for (int i = 0; i < 8; i++) {
296+
result = (result << 8) | (data[offset + i] & 0xFFL);
297+
}
298+
return result;
299+
}
300+
301+
private record ClassifierKey(String os, String arch, LinuxLibc libc) {
302+
}
303+
}

0 commit comments

Comments
 (0)