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