5package com.microsoft.z3;
7import java.io.FileNotFoundException;
8import java.io.IOException;
9import java.io.InputStream;
10import java.nio.file.Files;
11import java.nio.file.Path;
12import java.nio.file.Paths;
13import java.nio.file.LinkOption;
14import java.nio.file.DirectoryStream;
15import java.nio.file.StandardCopyOption;
16import java.nio.file.StandardOpenOption;
17import java.nio.file.attribute.PosixFilePermissions;
18import java.nio.file.attribute.PosixFilePermission;
19import java.nio.channels.FileChannel;
20import java.nio.channels.FileLock;
21import java.nio.channels.OverlappingFileLockException;
22import java.security.MessageDigest;
23import java.security.NoSuchAlgorithmException;
24import java.util.Locale;
26import java.util.TreeMap;
29final class NativeLibraryLoader {
30 private NativeLibraryLoader() { }
34 if (Boolean.parseBoolean(System.getProperty(
"z3.skipLibraryLoad"))) {
37 UnsatisfiedLinkError first;
39 System.loadLibrary(
"z3java");
41 }
catch (UnsatisfiedLinkError ex) {
44 UnsatisfiedLinkError second;
46 System.loadLibrary(
"libz3java");
48 }
catch (UnsatisfiedLinkError ex) {
52 String platform = platform();
53 String stage =
"finding bundled resources for " + platform;
54 Throwable cacheFailure =
null;
56 String core = libraryName(platform,
"z3");
57 String bridge = libraryName(platform,
"z3java");
59 if (!
"false".equalsIgnoreCase(System.getProperty(
"z3.nativeCache"))) {
60 loadCached(platform, core, bridge);
63 }
catch (IOException | UnsatisfiedLinkError | SecurityException
64 | OverlappingFileLockException | java.nio.file.InvalidPathException
65 | UnsupportedOperationException ex) {
70 try (InputStream coreInput = resource(platform, core);
71 InputStream bridgeInput = resource(platform, bridge)) {
72 Path directory = Files.createTempDirectory(
"z3-native-");
73 directory.toFile().deleteOnExit();
74 stage =
"extracting bundled resources for " + platform +
" to " + directory;
75 Path corePath = extract(directory, core, coreInput);
76 Path bridgePath = extract(directory, bridge, bridgeInput);
77 stage =
"loading " + corePath;
78 System.load(corePath.toAbsolutePath().toString());
79 stage =
"loading " + bridgePath;
80 System.load(bridgePath.toAbsolutePath().toString());
82 }
catch (IOException | UnsatisfiedLinkError | SecurityException ex) {
83 UnsatisfiedLinkError error =
new UnsatisfiedLinkError(
84 "Failed to load Z3 native libraries for " + platform +
" while " + stage
85 +
". System attempts: z3java: " + first.getMessage()
86 +
"; libz3java: " + second.getMessage()
87 + (cacheFailure ==
null ?
"" :
". Cache attempt: " + cacheFailure)
88 +
". Bundled attempt: " + ex.getMessage()
89 +
". Check the platform classifier, native runtime dependencies and writable java.io.tmpdir.");
91 error.addSuppressed(first);
92 error.addSuppressed(second);
93 if (cacheFailure !=
null) {
94 ex.addSuppressed(cacheFailure);
100 private static InputStream resource(String platform, String name)
throws IOException {
101 String path =
"/com/microsoft/z3/native/" + platform +
"/" + name;
102 InputStream input = NativeLibraryLoader.class.getResourceAsStream(path);
104 throw new FileNotFoundException(
"Missing bundled native resource " + path);
109 private static Path extract(Path directory, String name, InputStream input)
throws IOException {
110 Path target = directory.resolve(name);
112 target.toFile().deleteOnExit();
113 Files.copy(input, target);
117 private static MessageDigest digest() {
119 return MessageDigest.getInstance(
"SHA-256");
120 }
catch (NoSuchAlgorithmException ex) {
121 throw new AssertionError(
"SHA-256 is required by Java", ex);
125 private static String hash(InputStream input)
throws IOException {
126 MessageDigest digest = digest();
127 byte[] buffer =
new byte[65536];
129 while ((count = input.read(buffer)) != -1) {
130 digest.update(buffer, 0, count);
132 StringBuilder text =
new StringBuilder(64);
133 for (
byte value : digest.digest()) {
134 text.append(Character.forDigit((value & 255) >>> 4, 16));
135 text.append(Character.forDigit(value & 15, 16));
137 return text.toString();
140 private static boolean matches(Path path, String expected)
throws IOException {
141 if (!Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS)) {
144 try (InputStream input = Files.newInputStream(path, LinkOption.NOFOLLOW_LINKS)) {
145 return expected.equals(hash(input));
149 private static void checkPath(Path path)
throws IOException {
150 for (Path part = path; part !=
null; part = part.getParent()) {
151 if (Files.isSymbolicLink(part)) {
152 throw new IOException(
"Symbolic link in native cache path: " + part);
157 private static Path cacheRoot() throws IOException {
158 String configured = System.getProperty(
"z3.nativeCacheDir");
159 String profile = System.getProperty(
"user.home");
160 if (profile ==
null || profile.isEmpty()) {
161 throw new IOException(
"No user.home for the native cache");
163 Path root = (configured ==
null
164 ? Paths.get(profile,
".z3",
"native")
165 : Paths.get(configured)).toAbsolutePath().normalize();
167 if (root.getFileSystem().supportedFileAttributeViews().contains(
"posix")) {
168 Files.createDirectories(root, PosixFilePermissions.asFileAttribute(
169 PosixFilePermissions.fromString(
"rwx------")));
170 Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(root);
171 if (permissions.contains(PosixFilePermission.GROUP_WRITE)
172 || permissions.contains(PosixFilePermission.OTHERS_WRITE)) {
173 throw new IOException(
"Native cache is writable by other users: " + root);
177 Files.createDirectories(root);
180 if (!Files.getOwner(root).equals(Files.getOwner(Paths.get(System.getProperty(
"user.home"))))) {
181 throw new IOException(
"Native cache is not owned by the profile owner: " + root);
186 private static void loadCached(String platform, String core, String bridge)
throws IOException {
189 synchronized (
"com.microsoft.z3.NativeLibraryLoader.cache") {
190 loadCachedLocked(platform, core, bridge);
194 private static void loadCachedLocked(String platform, String core, String bridge)
throws IOException {
198 try (InputStream input = resource(platform, core)) {
199 coreHash = hash(input);
201 try (InputStream input = resource(platform, bridge)) {
202 bridgeHash = hash(input);
205 try (InputStream input =
new java.io.ByteArrayInputStream(
206 (coreHash + bridgeHash).getBytes(java.nio.charset.StandardCharsets.US_ASCII))) {
207 identity = hash(input);
209 Path root = cacheRoot();
212 try (FileChannel channel = FileChannel.open(root.resolve(
".lock"),
213 StandardOpenOption.CREATE, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS);
214 FileLock lock = channel.lock()) {
215 Path directory = root.resolve(platform +
"-" + identity);
216 checkPath(directory);
217 if (!matches(directory.resolve(core), coreHash) || !matches(directory.resolve(bridge), bridgeHash)) {
218 if (Files.exists(directory, LinkOption.NOFOLLOW_LINKS)) {
219 removeEntry(directory, core, bridge);
221 Path staging = Files.createTempDirectory(root,
".extract-");
223 try (InputStream input = resource(platform, core)) {
224 Files.copy(input, staging.resolve(core));
226 try (InputStream input = resource(platform, bridge)) {
227 Files.copy(input, staging.resolve(bridge));
229 if (!matches(staging.resolve(core), coreHash) || !matches(staging.resolve(bridge), bridgeHash)) {
230 throw new IOException(
"Bundled native resources changed during extraction");
234 Files.move(staging, directory, StandardCopyOption.ATOMIC_MOVE);
236 if (Files.exists(staging, LinkOption.NOFOLLOW_LINKS)) {
237 removeEntry(staging, core, bridge);
241 Files.setLastModifiedTime(directory, java.nio.file.attribute.FileTime.fromMillis(System.currentTimeMillis()));
242 System.load(directory.resolve(core).toString());
243 System.load(directory.resolve(bridge).toString());
244 prune(root, directory, platform, core, bridge);
249 private static void checkEntry(Path directory, String core, String bridge)
throws IOException {
250 if (!Files.isDirectory(directory, LinkOption.NOFOLLOW_LINKS)) {
251 throw new IOException(
"Not a native cache directory: " + directory);
253 try (DirectoryStream<Path> files = Files.newDirectoryStream(directory)) {
254 for (Path file : files) {
255 String name = file.getFileName().toString();
256 if ((!name.equals(core) && !name.equals(bridge))
257 || !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
258 throw new IOException(
"Unexpected native cache entry: " + file);
264 private static void removeEntry(Path directory, String core, String bridge)
throws IOException {
265 checkEntry(directory, core, bridge);
266 Files.deleteIfExists(directory.resolve(core));
267 Files.deleteIfExists(directory.resolve(bridge));
268 Files.delete(directory);
271 private static void prune(Path root, Path current, String platform, String core, String bridge) {
273 TreeMap<String, Path> previous =
new TreeMap<String, Path>();
274 long stale = System.currentTimeMillis() - 30L * 24 * 60 * 60 * 1000;
275 try (DirectoryStream<Path> entries = Files.newDirectoryStream(root)) {
276 for (Path entry : entries) {
277 String name = entry.getFileName().toString();
278 if (entry.equals(current) || !Files.isDirectory(entry, LinkOption.NOFOLLOW_LINKS)) {
281 long modified = Files.getLastModifiedTime(entry, LinkOption.NOFOLLOW_LINKS).toMillis();
282 if (name.matches(platform +
"-[0-9a-f]{64}")) {
283 try { checkEntry(entry, core, bridge); }
catch (IOException ignored) {
continue; }
284 previous.put(
String.format(Locale.ROOT,
"%020d-%s", modified, name), entry);
285 }
else if (name.startsWith(
".extract-") && modified < stale) {
286 try { removeEntry(entry, core, bridge); }
catch (IOException ignored) { }
291 while (previous.size() > 2) {
292 Path entry = previous.pollFirstEntry().getValue();
293 try { removeEntry(entry, core, bridge); }
catch (IOException ignored) { }
295 }
catch (IOException | SecurityException ignored) {
300 private static String libraryName(String platform, String name) {
301 if (platform.startsWith(
"win-")) {
302 return name.equals(
"z3") ?
"libz3.dll" :
"z3java.dll";
304 return "lib" + name + (platform.startsWith(
"osx-") ?
".dylib" :
".so");
307 static String platform() {
308 String os = System.getProperty(
"os.name",
"").toLowerCase(Locale.ROOT);
309 String arch = System.getProperty(
"os.arch",
"").toLowerCase(Locale.ROOT);
311 if (os.startsWith(
"windows")) {
313 }
else if (os.startsWith(
"mac") || os.equals(
"darwin")) {
315 }
else if (os.equals(
"linux")) {
318 osPart =
"unsupported-os";
321 if (arch.equals(
"amd64") || arch.equals(
"x86_64")) {
323 }
else if (arch.equals(
"aarch64") || arch.equals(
"arm64")) {
325 }
else if (arch.equals(
"x86") || arch.matches(
"i[3-6]86")) {
328 archPart =
"unsupported-arch";
330 return osPart +
"-" + archPart;