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 extraction 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  public void loadLibrary() throws IOException {
084    try {
085      // First, try loading path
086      System.loadLibrary(m_libraryName);
087    } catch (UnsatisfiedLinkError ule) {
088      // Then load the hash from the resources
089      String hashName = RuntimeDetector.getHashLibraryResource(m_libraryName);
090      String resName = RuntimeDetector.getLibraryResource(m_libraryName);
091      try (InputStream hashIs = m_loadClass.getResourceAsStream(hashName)) {
092        if (hashIs == null) {
093          throw new IOException(getLoadErrorMessage(ule));
094        }
095        try (Scanner scanner = new Scanner(hashIs, StandardCharsets.UTF_8)) {
096          String hash = scanner.nextLine();
097          File jniLibrary = new File(m_extractionRoot, resName + "." + hash);
098          try {
099            // Try to load from an already extracted hash
100            System.load(jniLibrary.getAbsolutePath());
101          } catch (UnsatisfiedLinkError ule2) {
102            // If extraction failed, extract
103            try (InputStream resIs = m_loadClass.getResourceAsStream(resName)) {
104              if (resIs == null) {
105                throw new IOException(getLoadErrorMessage(ule));
106              }
107
108              var parentFile = jniLibrary.getParentFile();
109              if (parentFile == null) {
110                throw new IOException("JNI library has no parent file");
111              }
112              parentFile.mkdirs();
113
114              try (OutputStream os = Files.newOutputStream(jniLibrary.toPath())) {
115                byte[] buffer = new byte[0xFFFF]; // 64K copy buffer
116                int readBytes;
117                while ((readBytes = resIs.read(buffer)) != -1) { // NOPMD
118                  os.write(buffer, 0, readBytes);
119                }
120              }
121              System.load(jniLibrary.getAbsolutePath());
122            }
123          }
124        }
125      }
126    }
127  }
128
129  /**
130   * Load a native library by directly hashing the file.
131   *
132   * @throws IOException if the library failed to load
133   */
134  public void loadLibraryHashed() throws IOException {
135    try {
136      // First, try loading path
137      System.loadLibrary(m_libraryName);
138    } catch (UnsatisfiedLinkError ule) {
139      // Then load the hash from the input file
140      String resName = RuntimeDetector.getLibraryResource(m_libraryName);
141      String hash;
142      try (InputStream is = m_loadClass.getResourceAsStream(resName)) {
143        if (is == null) {
144          throw new IOException(getLoadErrorMessage(ule));
145        }
146        MessageDigest md;
147        try {
148          md = MessageDigest.getInstance("MD5");
149        } catch (NoSuchAlgorithmException nsae) {
150          throw new RuntimeException("Weird Hash Algorithm?");
151        }
152        try (DigestInputStream dis = new DigestInputStream(is, md)) {
153          // Read the entire buffer once to hash
154          byte[] buffer = new byte[0xFFFF];
155          while (dis.read(buffer) > -1) {}
156          MessageDigest digest = dis.getMessageDigest();
157          byte[] digestOutput = digest.digest();
158          StringBuilder builder = new StringBuilder();
159          for (byte b : digestOutput) {
160            builder.append(String.format("%02X", b));
161          }
162          hash = builder.toString().toLowerCase(Locale.ENGLISH);
163        }
164      }
165      if (hash == null) {
166        throw new IOException("Weird Hash?");
167      }
168      File jniLibrary = new File(m_extractionRoot, resName + "." + hash);
169      try {
170        // Try to load from an already extracted hash
171        System.load(jniLibrary.getAbsolutePath());
172      } catch (UnsatisfiedLinkError ule2) {
173        // If extraction failed, extract
174        try (InputStream resIs = m_loadClass.getResourceAsStream(resName)) {
175          if (resIs == null) {
176            throw new IOException(getLoadErrorMessage(ule));
177          }
178
179          var parentFile = jniLibrary.getParentFile();
180          if (parentFile == null) {
181            throw new IOException("JNI library has no parent file");
182          }
183          parentFile.mkdirs();
184
185          try (OutputStream os = Files.newOutputStream(jniLibrary.toPath())) {
186            byte[] buffer = new byte[0xFFFF]; // 64K copy buffer
187            int readBytes;
188            while ((readBytes = resIs.read(buffer)) != -1) { // NOPMD
189              os.write(buffer, 0, readBytes);
190            }
191          }
192          System.load(jniLibrary.getAbsolutePath());
193        }
194      }
195    }
196  }
197}