|
| 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