001/*******************************************************************************
002 * Copyright (c) 2024, 2026, Olivier Ayache.  All rights reserved.
003 *
004 * This file is part of AVPKit.
005 *
006 * AVPKit is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU Lesser General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * AVPKit is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with AVPKit.  If not, see <http://www.gnu.org/licenses/>.
018 *******************************************************************************/
019
020package com.avpkit.ferry;
021
022import java.io.File;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Map;
028import java.util.Set;
029
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033/**
034 * Internal Only.  Finds and loads native libraries that we depend on.
035 * <p>
036 * It supplements the Java builtin {@link System#loadLibrary(String)} by looking
037 * in more places.
038 * </p>
039 * <p>
040 * Many methods in this class are marked as "package level". In reality, they
041 * are private, but are at this protection level so our test suite can test
042 * internals.
043 * </p>
044 * 
045 */
046public final class JNILibraryLoader
047{
048  private static final Logger log = LoggerFactory
049      .getLogger(JNILibraryLoader.class);
050
051  /**
052   * An enumeration of the types of OS's we will do special handling for.
053   */
054  enum OSName
055  {
056    Unknown, Windows, MacOSX, Linux
057  };
058
059  /**
060   * @deprecated Use {@link JNILibrary} instead.
061   * 
062   * Attempts to find and load the given library, with the given version of the
063   * library if version is asked for.
064   * <p>
065   * First, if we detect that we've already loaded the given library, we'll just
066   * return rather than attempt a second load (which will fail on some OSes).
067   * </p>
068   * <p>
069   * If we haven't already loaded this library, we will search in the path
070   * defined by the property java.library.path for the library, creating
071   * OS-dependent names, and using version strings. If we can't find it in that
072   * property, we'll search through the OS-dependent shared-library
073   * specification environment variable.
074   * </p>
075   * <p>
076   * If we can't find a versioned library (and one was requested) but can find
077   * an unversioned library, we'll use the unversioned library. But we will
078   * first search all directories for a versioned library under the assumption
079   * that if you asked for a version, you care more about meeting that
080   * requirement than finding it in the first directory we run across.
081   * </p>
082   * <p>
083   * If all that still fails, we'll fall back to the
084   * {@link System#loadLibrary(String)} method (for example, if we cannot guess
085   * a libtool-like convention for the OS we're on).
086   * </p>
087   * <p>
088   * We assume a libtool-like library name for the shared library, but will
089   * check for common variants on that name.
090   * </p>
091   * <p>
092   * Hopefully an illustration will make this all clearer. Assume we're looking
093   * for a library named "foo" with version 1, this method will search as
094   * follows:
095   * </p>
096   * <p>
097   * <table>
098   * <tr>
099   * <th>OS</th>
100   * <th>Filenames searched for (in order)</th>
101   * <th>Directories looked in (in order)</th>
102   * </tr>
103   * <tr>
104   * <td>
105   * On Windows:</td>
106   * <td>
107   * <ol>
108   * <li>foo-1.dll</li>
109   * <li>libfoo-1.dll</li>
110   * <li>cygfoo-1.dll</li>
111   * <li>foo.dll</li>
112   * <li>libfoo.dll</li>
113   * <li>cygfoo.dll</li>
114   * </ol>
115   * </td>
116   * <td>
117   * <ol>
118   * <li>Every directory in the java property <b>java.library.path</b></li>
119   * <li>Every directory in the environment variable <b>PATH</b></li>
120   * </ol>
121   * </td>
122   * </tr>
123   * <tr>
124   * <td>
125   * On Linux:</td>
126   * <td>
127   * <ol>
128   * <li>libfoo.so.1</li>
129   * <li>libfoo.so</li>
130   * </ol>
131   * </td>
132   * <td>
133   * <ol>
134   * <li>Every directory in the java property <b>java.library.path</b></li>
135   * <li>Every directory in the environment variable <b>LD_LIBRARY_PATH</b></li>
136   * </ol>
137   * </td>
138   * </tr>
139   * <tr>
140   * <td>
141   * On Apple OS-X:</td>
142   * <td>
143   * <ol>
144   * <li>libfoo.1.dylib</li>
145   * <li>libfoo.dylib</li>
146   * </ol>
147   * </td>
148   * <td>
149   * <ol>
150   * <li>Every directory in the java property <b>java.library.path</b></li>
151   * <li>Every directory in the environment variable <b>DYLD_LIBRARY_PATH</b></li>
152   * </ol>
153   * </td>
154   * </tr>
155   * </table>
156   * 
157   * @param aLibraryName
158   *          The name of the library, without any OS-dependent adornments like
159   *          "lib", ".so" or ".dll".
160   * @param aMajorVersion
161   *          The major version of this library, or null if you want to take any
162   *          version.
163   * 
164   * @throws UnsatisfiedLinkError
165   *           If we cannot find the library after searching in all the
166   *           aforementioned locations.
167   */
168  @Deprecated
169  public static void loadLibrary(String aLibraryName, Long aMajorVersion)
170  {
171    getInstance().loadLibrary0(aLibraryName, aMajorVersion);
172  }
173
174  /**
175   * Redirects to {@link #loadLibrary(String, Long)}, but leaves the version as
176   * null (not requested).
177   * 
178   * @param aLibraryName
179   *          The name of the library, without any OS-dependent adornments like
180   *          "lib", ".so" or ".dll".
181   */
182  public static void loadLibrary(String aLibraryName)
183  {
184    loadLibrary(aLibraryName, null);
185  }
186
187  /**
188   * The singleton instance of the {@link JNILibraryLoader}. We only allow one
189   * per class-loader instance. Technically it would be nice to ensure only one
190   * per process, but that's currently beyond my Java ken.
191   */
192  private static JNILibraryLoader mGlobalLoader = new JNILibraryLoader();
193
194  /**
195   * The set of directories in the java.library.path property; this is queried
196   * once per object and then cached, as the property is read-only per Sun's
197   * documentation and will not change during the lifetime of this process.
198   */
199  private String[] mJavaPropPaths;
200
201  /**
202   * The set of directories in the environment variable this OS uses to denote
203   * places where shared objects may live (e.g. LD_LIBRARY_PATH on Linux); this
204   * is queried once per object and then cached, as the property is read-only
205   * per Sun's documentation and will not change during the lifetime of this
206   * process.
207   */
208  private String[] mJavaEnvPaths;
209
210  /**
211   * What we think the Operating System we're running on is. This is used to
212   * guess which environment variable to use for shared objects, and how to turn
213   * a library name into a fully qualified file name.
214   */
215  private OSName mOS = null;
216
217  /**
218   * A cache that maps a library name to a set of versions we've loaded.
219   */
220  private Map<String, Set<Long>> mLoadedLibraries = new HashMap<String, Set<Long>>();
221
222  /**
223   * Get the singleton instance of the {@link JNILibraryLoader}.
224   * 
225   * @return The singleton instance to use for all queries.
226   */
227  static JNILibraryLoader getInstance()
228  {
229    return mGlobalLoader;
230  }
231
232  /**
233   * Constructor for {@link JNILibraryLoader}. Should not be callable from
234   * outside this class so as to ensure only the singleton object can exist.
235   */
236  private JNILibraryLoader()
237  {
238    log.trace("<init>");
239  }
240
241  /**
242   * This is the method that actually loads the library. It maintains an object
243   * level lock, and since this class only allows a singleton object, that is a
244   * class-level lock. That means if you're loading a library on one thread,
245   * other threads will block until it finishes.
246   * 
247   * This should be OK in general.
248   * 
249   * @param aLibraryName
250   *          The library name.
251   * @param aMajorVersion
252   *          The version, or null if you don't care.
253   */
254  synchronized void loadLibrary0(String aLibraryName, Long aMajorVersion)
255  {
256    if (alreadyLoadedLibrary(aLibraryName, aMajorVersion))
257      // our work is done.
258      return;
259
260    List<String> libCandidates = getLibraryCandidates(aLibraryName,
261        aMajorVersion);
262    if (libCandidates != null && libCandidates.size() > 0
263        && !loadCandidateLibrary(aLibraryName, aMajorVersion, libCandidates))
264    {
265      // finally, try the System.loadLibrary call
266      try
267      {
268        System.loadLibrary(aLibraryName);
269      }
270      catch (UnsatisfiedLinkError e)
271      {
272        log
273            .error(
274                "Could not load library: {}; version: {}; Visit http://www.avpkit.com/core/faq/ to find common solutions to this problem",
275                aLibraryName, aMajorVersion == null ? "" : aMajorVersion);
276        throw e;
277
278      }
279      // and if we get here it means we successfully loaded since no
280      // exception was thrown. Add our library to the cache.
281      setLoadedLibrary(aLibraryName, aMajorVersion);
282    }
283    log.trace("Successfully Loaded library: {}; Version: {}", aLibraryName, aMajorVersion);
284  }
285 
286
287  /**
288   * Tell the cache that we've loaded this version.
289   * 
290   * @param aLibraryName
291   * @param aMajorVersion
292   */
293  void setLoadedLibrary(String aLibraryName, Long aMajorVersion)
294  {
295    Set<Long> foundVersions = mLoadedLibraries.get(aLibraryName);
296    if (foundVersions == null)
297    {
298      foundVersions = new HashSet<Long>();
299      mLoadedLibraries.put(aLibraryName, foundVersions);
300    }
301    foundVersions.add(aMajorVersion);
302  }
303
304  /**
305   * Iterates through the set of aLibCandidates until it succeeds in loading a
306   * library. If it succeeds, it lets the cache know.
307   * 
308   * @param aLibraryName
309   *          The library name.
310   * @param aMajorVersion
311   *          The version we want, or null if we don't care.
312   * @param aLibCandidates
313   *          The set of candidates generated by
314   *          {@link #getLibraryCandidates(String, Long)}
315   * @return true if we succeeded in loading a library; false otherwise
316   */
317  boolean loadCandidateLibrary(String aLibraryName, Long aMajorVersion,
318      List<String> aLibCandidates)
319  {
320    boolean retval = false;
321    for (String candidate : aLibCandidates)
322    {
323      log
324          .trace(
325              "Attempt: library load of library: {}; version: {}: relative path: {}",
326              new Object[]
327              {
328                  aLibraryName,
329                  aMajorVersion == null ? "<unspecified>" : aMajorVersion
330                      .longValue(), candidate
331              });
332      File candidateFile = new File(candidate);
333      if (candidateFile.exists())
334      {
335        String absPath = candidateFile.getAbsolutePath();
336        try
337        {
338          log
339              .trace(
340                  "Attempt: library load of library: {}; version: {}: absolute path: {}",
341                  new Object[]
342                  {
343                      aLibraryName,
344                      aMajorVersion == null ? "<unspecified>" : aMajorVersion
345                          .longValue(), absPath
346                  });
347          // Here's where we attempt the actual load.
348          System.load(absPath);
349          log
350              .trace(
351                  "Success: library load of library: {}; version: {}: absolute path: {}",
352                  new Object[]
353                  {
354                      aLibraryName,
355                      aMajorVersion == null ? "<unspecified>" : aMajorVersion
356                          .longValue(), absPath
357                  });
358          // if we got here, we loaded successfully
359          setLoadedLibrary(aLibraryName, aMajorVersion);
360          retval = true;
361          break;
362        }
363        catch (UnsatisfiedLinkError e)
364        {
365          log
366              .warn(
367                  "Failure: library load of library: {}; version: {}: absolute path: {}; error: {}",
368                  new Object[]
369                  {
370                      aLibraryName,
371                      aMajorVersion == null ? "<unspecified>" : aMajorVersion
372                          .longValue(), absPath, e
373                  });
374        }
375        catch (SecurityException e)
376        {
377          log
378              .warn(
379                  "Failure: library load of library: {}; version: {}: absolute path: {}; error: {}",
380                  new Object[]
381                  {
382                      aLibraryName,
383                      aMajorVersion == null ? "<unspecified>" : aMajorVersion
384                          .longValue(), absPath, e
385                  });
386        }
387      }
388    }
389    return retval;
390  }
391
392  /**
393   * For a given library, and the OS we're running on, this method generates a
394   * list of potential absolute file paths that
395   * {@link #loadCandidateLibrary(String, Long, String[])} should attempt (in
396   * order) to load. This method will not check for existence and readability of
397   * the file we're attempting to load.
398   * 
399   * @param aLibraryName
400   *          The library name
401   * @param aMajorVersion
402   *          The version, or null if we don't care.
403   * @return The set of absolute file paths to try.
404   */
405  List<String> getLibraryCandidates(String aLibraryName, Long aMajorVersion)
406  {
407    final List<String> retval = new LinkedList<String>();
408    // Note: when done each of these variables must be set to a non-null, non
409    // empty string array
410    final String[] prefixes;
411    final String[] suffixes;
412    final String[] preSuffixVersions;
413    final String[] postSuffixVersions;
414
415    switch (getOS())
416    {
417      case Unknown:
418      case Linux:
419        prefixes = new String[]
420        {
421            "lib", ""
422        };
423        suffixes = new String[]
424        {
425          ".so"
426        };
427        preSuffixVersions = new String[]
428        {
429          ""
430        };
431        postSuffixVersions = (aMajorVersion == null ? new String[]
432        {
433          ""
434        } : new String[]
435        {
436          "." + aMajorVersion.longValue()
437        });
438        break;
439      case Windows:
440        prefixes = new String[]
441        {
442            "lib", "", "cyg"
443        };
444        suffixes = new String[]
445        {
446          ".dll"
447        };
448        preSuffixVersions = (aMajorVersion == null ? new String[]
449        {
450          ""
451        } : new String[]
452        {
453          "-" + aMajorVersion.longValue()
454        });
455        postSuffixVersions = new String[]
456        {
457          ""
458        };
459        break;
460      case MacOSX:
461        prefixes = new String[]
462        {
463            "lib", ""
464        };
465        suffixes = new String[]
466        {
467          ".dylib"
468        };
469        preSuffixVersions = (aMajorVersion == null ? new String[]
470        {
471          ""
472        } : new String[]
473        {
474          "." + aMajorVersion.longValue()
475        });
476        postSuffixVersions = new String[]
477        {
478          ""
479        };
480        break;
481      default:
482        // really no cases should get here
483        prefixes = null;
484        suffixes = null;
485        preSuffixVersions = null;
486        postSuffixVersions = null;
487        break;
488    }
489    initializeSearchPaths();
490
491    // First check the versioned paths
492    if (aMajorVersion != null)
493    {
494      for (String directory : mJavaPropPaths)
495      {
496        generateFileNames(retval, directory, aLibraryName, prefixes, suffixes,
497            preSuffixVersions, postSuffixVersions, true);
498      }
499      for (String directory : mJavaEnvPaths)
500      {
501        generateFileNames(retval, directory, aLibraryName, prefixes, suffixes,
502            preSuffixVersions, postSuffixVersions, true);
503      }
504    }
505    for (String directory : mJavaPropPaths)
506    {
507      generateFileNames(retval, directory, aLibraryName, prefixes, suffixes,
508          preSuffixVersions, postSuffixVersions, false);
509    }
510    for (String directory : mJavaEnvPaths)
511    {
512      generateFileNames(retval, directory, aLibraryName, prefixes, suffixes,
513          preSuffixVersions, postSuffixVersions, false);
514    }
515    return retval;
516  }
517
518  void generateFileNames(List<String> aResults, String aDirectory,
519      String aLibraryName, String[] aPrefixes, String[] aSuffixes,
520      String[] aPreSuffixVersions, String[] aPostSuffixVersions,
521      boolean aIncludeVersion)
522  {
523    // make sure aDirectory ends with correct terminator
524    String dirSeparator = File.separator;
525    if (!aDirectory.endsWith(dirSeparator))
526      aDirectory = aDirectory + dirSeparator;
527    for (String suffix : aSuffixes)
528    {
529      for (String prefix : aPrefixes)
530      {
531        if (aIncludeVersion)
532        {
533          for (String preSuffixVersion : aPreSuffixVersions)
534          {
535            for (String postSuffixVersion : aPostSuffixVersions)
536            {
537              String result = aDirectory + prefix + aLibraryName
538                  + preSuffixVersion + suffix + postSuffixVersion;
539              aResults.add(result);
540            }
541          }
542        }
543        else
544        {
545          String result = aDirectory + prefix + aLibraryName + suffix;
546          aResults.add(result);
547        }
548      }
549    }
550  }
551
552  /**
553   * Initialize the paths we'll search for libraries in.
554   */
555  private void initializeSearchPaths()
556  {
557    String pathVar = null;
558    if (mJavaPropPaths == null)
559    {
560      pathVar = System.getProperty("java.library.path", "");
561      log.trace("property java.library.path: {}", pathVar);
562      mJavaPropPaths = getEntitiesFromPath(pathVar);
563    }
564    if (mJavaEnvPaths == null)
565    {
566      String envVar = getSystemRuntimeLibraryPathVar();
567      pathVar = System.getenv(envVar);
568      log.trace("OS environment runtime shared library path ({}): {}", envVar,
569          pathVar);
570      mJavaEnvPaths = getEntitiesFromPath(pathVar);
571    }
572
573  }
574
575  OSName getOS()
576  {
577    if (mOS != null)
578      return mOS;
579    OSName retval = OSName.Linux;
580    String osName = System.getProperty("os.name", "Linux");
581    if (osName.length() > 0)
582    {
583      if (osName.startsWith("Windows"))
584        retval = OSName.Windows;
585      else if (osName.startsWith("Mac"))
586        retval = OSName.MacOSX;
587      else if (osName.startsWith("Linux"))
588        retval = OSName.Linux;
589      else
590        // default everything to Linux
591        retval = OSName.Linux;
592    }
593    mOS = retval;
594    log.trace("Detected OS: {}", mOS);
595    return retval;
596  }
597
598
599  /**
600   * For internal use only. This method allows tests to override the guessed OS
601   * name.
602   * 
603   * @param os
604   *          The OS to set
605   */
606  void setOS(OSName os)
607  {
608    mOS = os;
609  }
610
611  String getSystemRuntimeLibraryPathVar()
612  {
613    String retval = "LD_LIBRARY_PATH";
614    switch (getOS())
615    {
616      case Windows:
617        retval = "PATH";
618        break;
619      case MacOSX:
620        retval = "DYLD_LIBRARY_PATH";
621        break;
622      case Linux:
623      case Unknown:
624        break;
625    }
626    return retval;
627  }
628
629  String[] getEntitiesFromPath(String aPathVar)
630  {
631    String[] retval = null;
632    String sep = File.pathSeparator;
633    if (aPathVar == null || aPathVar.length() == 0)
634    {
635      retval = new String[1];
636      retval[0] = ".";
637      log
638          .trace("Have empty path var; assuming current directory to find native libraries");
639    }
640    else
641    {
642      log.trace("Parsing path var: {}", aPathVar);
643      int len = aPathVar.length();
644
645      // find out how many paths there are
646      int i = 0;
647      int n = 0;
648      int j = 0;
649
650      n = 1;
651      i = aPathVar.indexOf(sep);
652      while (i >= 0)
653      {
654        n++;
655        i = aPathVar.indexOf(sep, i + 1);
656      }
657
658      log.trace("Found {} paths in path var: {}", n, aPathVar);
659      // Create the return array
660      retval = new String[n];
661
662      // now fill in the actual strings
663      i = 0;
664      n = 0;
665      j = aPathVar.indexOf(sep);
666      while (j >= 0)
667      {
668        if (j - i > 0)
669        {
670          // a non zero entry was found
671          retval[n] = aPathVar.substring(i, j);
672          log.trace("Added path {} for path var: {}", retval[n], aPathVar);
673          ++n;
674        }
675        else if (j - i == 0)
676        {
677          // someone put two path separators, with nothing in
678          // between. just assume they meant the current directory
679          retval[n] = ".";
680          log.trace("Added path {} for path var: {}", retval[n], aPathVar);
681          ++n;
682        }
683        i = j + 1;
684        j = aPathVar.indexOf(sep, i);
685      }
686      // and get the last entry which should have no separator
687      retval[n] = aPathVar.substring(i, len);
688      log.trace("Adding last path {} for path var: {}", retval[n], aPathVar);
689      if (retval[n] == null || retval[n].length() == 0)
690      {
691        // A malformed path with a separator at the end; we just add
692        // the current directory again.
693        retval[n] = ".";
694        log.trace("Faking last path {} for malformed path var: {}", retval[n],
695            aPathVar);
696      }
697
698    }
699    return retval;
700  }
701
702  /**
703   * Checks our cache to see if we've already loaded this library.
704   * 
705   * We will also detect if we've already loaded another version of this
706   * library, and log a warning, but otherwise will return false in that case.
707   * 
708   * @param aLibraryName
709   *          The library name.
710   * @param aMajorVersion
711   *          The version, or null if we don't care.
712   * @return true if in cache; false otherwise
713   */
714  boolean alreadyLoadedLibrary(String aLibraryName, Long aMajorVersion)
715  {
716    boolean retval = false;
717    Set<Long> foundVersions = mLoadedLibraries.get(aLibraryName);
718    if (foundVersions != null)
719    {
720      // we found at least some versions
721      if (aMajorVersion == null || foundVersions.contains(aMajorVersion))
722      {
723        retval = true;
724      }
725      else
726      {
727        log
728            .warn(
729                "Attempting load of {}, version {}, but already loaded verions: {}."
730                    + "  We will attempt to load the specified version but behavior is undefined",
731                new Object[]
732                {
733                    aLibraryName, aMajorVersion, foundVersions.toArray()
734                });
735      }
736    }
737    return retval;
738  }
739}