001// Copyright (c) FIRST and other WPILib contributors.
002// Open Source Software; you can modify and/or share it under the terms of
003// the WPILib BSD license file in the root directory of this project.
004
005package edu.wpi.first.util;
006
007import java.io.File;
008import java.io.IOException;
009import java.io.InputStream;
010import java.io.OutputStream;
011import java.nio.charset.StandardCharsets;
012import java.nio.file.Files;
013import java.nio.file.Paths;
014import java.security.DigestInputStream;
015import java.security.MessageDigest;
016import java.security.NoSuchAlgorithmException;
017import java.util.Locale;
018import java.util.Scanner;
019
020/**
021 * Loads a native library at runtime.
022 *
023 * @param <T> The class to load.
024 */
025public final class RuntimeLoader<T> {
026  private static String defaultExtractionRoot;
027
028  /**
029   * Gets the default extraction root location (~/.wpilib/nativecache).
030   *
031   * @return The default extraction root location.
032   */
033  public static synchronized String getDefaultExtractionRoot() {
034    if (defaultExtractionRoot != null) {
035      return defaultExtractionRoot;
036    }
037    String home = System.getProperty("user.home");
038    defaultExtractionRoot = Paths.get(home, ".wpilib", "nativecache").toString();
039    return defaultExtractionRoot;
040  }
041
042  private final String m_libraryName;
043  private final Class<T> m_loadClass;
044  private final String m_extractionRoot;
045
046  /**
047   * Creates a new library loader.
048   *
049   * @param libraryName Name of library to load.
050   * @param extractionRoot Location from which to load the library.
051   * @param cls Class whose classpath the given library belongs.
052   */
053  public RuntimeLoader(String libraryName, String extractionRoot, Class<T> cls) {
054    m_libraryName = libraryName;
055    m_loadClass = cls;
056    m_extractionRoot = extractionRoot;
057  }
058
059  /**
060   * Returns a load error message given the information in the provided UnsatisfiedLinkError.
061   *
062   * @param ule UnsatisfiedLinkError object.
063   * @return A load error message.
064   */
065  private String getLoadErrorMessage(UnsatisfiedLinkError ule) {
066    StringBuilder msg = new StringBuilder(512);
067    msg.append(m_libraryName)
068        .append(
069            " could not be loaded from path or an embedded resource.\n"
070                + "\tattempted to load for platform ")
071        .append(RuntimeDetector.getPlatformPath())
072        .append("\nLast Load Error: \n")
073        .append(ule.getMessage())
074        .append('\n');
075    if (RuntimeDetector.isWindows()) {
076      msg.append(
077          "A common cause of this error is missing the C++ runtime.\n"
078              + "Download the latest at https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads\n");
079    }
080    return msg.toString();
081  }
082
083  /**
084   * Loads a native library.
085   *
086   * @throws IOException if the library fails to load
087   */
088  public void loadLibrary() throws IOException {
089    try {
090      // First, try loading path
091      System.loadLibrary(m_libraryName);
092    } catch (UnsatisfiedLinkError ule) {
093      // Then load the hash from the resources
094      String hashName = RuntimeDetector.getHashLibraryResource(m_libraryName);
095      String resName = RuntimeDetector.getLibraryResource(m_libraryName);
096      try (InputStream hashIs = m_loadClass.getResourceAsStream(hashName)) {
097        if (hashIs == null) {
098          throw new IOException(getLoadErrorMessage(ule));
099        }
100        try (Scanner scanner = new Scanner(hashIs, StandardCharsets.UTF_8)) {
101          String hash = scanner.nextLine();
102          File jniLibrary = new File(m_extractionRoot, resName + "." + hash);
103          try {
104            // Try to load from an already extracted hash
105            System.load(jniLibrary.getAbsolutePath());
106          } catch (UnsatisfiedLinkError ule2) {
107            // If extraction failed, extract
108            try (InputStream resIs = m_loadClass.getResourceAsStream(resName)) {
109              if (resIs == null) {
110                throw new IOException(getLoadErrorMessage(ule));
111              }
112
113              var parentFile = jniLibrary.getParentFile();
114              if (parentFile == null) {
115                throw new IOException("JNI library has no parent file");
116              }
117              parentFile.mkdirs();
118
119              try (OutputStream os = Files.newOutputStream(jniLibrary.toPath())) {
120                byte[] buffer = new byte[0xFFFF]; // 64K copy buffer
121                int readBytes;
122                while ((readBytes = resIs.read(buffer)) != -1) { // NOPMD
123                  os.write(buffer, 0, readBytes);
124                }
125              }
126              System.load(jniLibrary.getAbsolutePath());
127            }
128          }
129        }
130      }
131    }
132  }
133
134  /**
135   * Load a native library by directly hashing the file.
136   *
137   * @throws IOException if the library failed to load
138   */
139  public void loadLibraryHashed() throws IOException {
140    try {
141      // First, try loading path
142      System.loadLibrary(m_libraryName);
143    } catch (UnsatisfiedLinkError ule) {
144      // Then load the hash from the input file
145      String resName = RuntimeDetector.getLibraryResource(m_libraryName);
146      String hash;
147      try (InputStream is = m_loadClass.getResourceAsStream(resName)) {
148        if (is == null) {
149          throw new IOException(getLoadErrorMessage(ule));
150        }
151        MessageDigest md;
152        try {
153          md = MessageDigest.getInstance("MD5");
154        } catch (NoSuchAlgorithmException nsae) {
155          throw new RuntimeException("Weird Hash Algorithm?");
156        }
157        try (DigestInputStream dis = new DigestInputStream(is, md)) {
158          // Read the entire buffer once to hash
159          byte[] buffer = new byte[0xFFFF];
160          while (dis.read(buffer) > -1) {}
161          MessageDigest digest = dis.getMessageDigest();
162          byte[] digestOutput = digest.digest();
163          StringBuilder builder = new StringBuilder();
164          for (byte b : digestOutput) {
165            builder.append(String.format("%02X", b));
166          }
167          hash = builder.toString().toLowerCase(Locale.ENGLISH);
168        }
169      }
170      if (hash == null) {
171        throw new IOException("Weird Hash?");
172      }
173      File jniLibrary = new File(m_extractionRoot, resName + "." + hash);
174      try {
175        // Try to load from an already extracted hash
176        System.load(jniLibrary.getAbsolutePath());
177      } catch (UnsatisfiedLinkError ule2) {
178        // If extraction failed, extract
179        try (InputStream resIs = m_loadClass.getResourceAsStream(resName)) {
180          if (resIs == null) {
181            throw new IOException(getLoadErrorMessage(ule));
182          }
183
184          var parentFile = jniLibrary.getParentFile();
185          if (parentFile == null) {
186            throw new IOException("JNI library has no parent file");
187          }
188          parentFile.mkdirs();
189
190          try (OutputStream os = Files.newOutputStream(jniLibrary.toPath())) {
191            byte[] buffer = new byte[0xFFFF]; // 64K copy buffer
192            int readBytes;
193            while ((readBytes = resIs.read(buffer)) != -1) { // NOPMD
194              os.write(buffer, 0, readBytes);
195            }
196          }
197          System.load(jniLibrary.getAbsolutePath());
198        }
199      }
200    }
201  }
202}