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 *******************************************************************************/
019package com.avpkit.ferry;
020
021import java.io.File;
022import java.io.FileOutputStream;
023import java.io.FilenameFilter;
024import java.io.IOException;
025import java.io.InputStream;
026import java.net.URI;
027import java.net.URISyntaxException;
028import java.net.URL;
029import java.util.Enumeration;
030import java.util.HashMap;
031import java.util.LinkedList;
032import java.util.List;
033import java.util.Map;
034import java.util.Properties;
035import java.util.jar.Manifest;
036
037import org.slf4j.Logger;
038import org.slf4j.LoggerFactory;
039
040public class JNILibrary implements Comparable<JNILibrary>
041{
042  private static final Logger log = LoggerFactory
043      .getLogger(JNILibrary.class);
044  
045  private static final String XUGGLE_TEMP_EXTENSION = ".avpkit";
046
047  private static final Map<String, List<JNIManifest>> mManifestLists = new HashMap<String, List<JNIManifest>>();
048  private static final Map<String, JNILibrary> mAttemptedLibraries = new HashMap<String, JNILibrary>();
049  
050  final static private Object mLock = new Object();
051
052  private static List<JNIManifest> getNativeManifests(String appName)
053  {
054    final List<JNIManifest> cached = mManifestLists.get(appName);
055    if (cached != null)
056      return cached;
057
058    // well, it's not cached.  let's do this the hard way.
059    final List<JNIManifest> retval = new LinkedList<JNIManifest>();
060    final ClassLoader loader = JNILibrary.class.getClassLoader();
061    
062    // First properties
063    try {
064      final Enumeration<URL> properties = loader.getResources("com/avpkit/ferry/native-contents.properties");
065      while (properties.hasMoreElements()) {
066        final URL url = properties.nextElement();
067        log.trace("Examining properties: {}", url);
068        final InputStream stream = url.openStream();
069        if (stream != null) {
070          final Properties props = new Properties();
071          props.load(stream);
072          final JNIManifest manifest = JNIManifest.create(url, appName, props);
073          if (manifest != null) {
074            log.trace("found manifest: {}; url: {}", manifest, url);
075            retval.add(manifest);
076          }
077          stream.close();
078        }
079      }
080    } catch (IOException e) {
081      log.debug("could not open properties: {}", e);  
082    }
083
084    // now manifests
085    try {
086      final Enumeration<URL> manifests = loader.getResources("META-INF/MANIFEST.MF");
087      while (manifests.hasMoreElements()) {
088        final URL url = manifests.nextElement();
089        log.trace("Examining manifest: {}", url);
090        final InputStream stream = url.openStream();
091        if (stream != null) {
092          final Manifest jarManifest = new Manifest(stream);
093          if (jarManifest != null) {
094            final JNIManifest manifest = JNIManifest.create(url, appName, jarManifest);
095            if (manifest != null) {
096              log.trace("found manifest: {}; url: {}", manifest, url);
097              retval.add(manifest); 
098            }
099          }
100        }
101      }
102    } catch (IOException e) {
103      log.debug("could not open manifest: {}", e);
104    }
105
106    // add to cache
107    mManifestLists.put(appName, retval);
108    return retval;
109  }
110
111  private final String mName;
112  private final Long mVersion;
113
114  private boolean mLoadAttempted;
115  private boolean mLoadSuccessful;
116
117  // a static initializer
118  static {
119    deleteTemporaryFiles();
120  }
121  
122  public JNILibrary(String name, Long version)
123  {
124    if (name == null || name.length() <= 0)
125      throw new IllegalArgumentException("need a valid name");
126    mName = name;
127    mVersion = version;
128    mLoadAttempted = false;
129    mLoadSuccessful = false;
130  }
131  public String getName() { return mName; }
132  public Long getVersion() { return mVersion; }
133  
134  public boolean isLoadAttempted() { return mLoadAttempted; }
135  public boolean isLoadSuccessful() { return mLoadSuccessful; }
136  
137  @Override
138  public String toString() { return super.toString()+"[ name=" + mName + "; version=" + mVersion + "; ]"; }
139  
140  /**
141   * Load the given library into the given application.  
142   * 
143   * This method first searches in the classpath for native libraries
144   * that are bundled in there, and only if no matches are found,
145   * will it search the run-time paths of each OS.
146   * 
147   * @param appname the name of the application.  This should match what shows
148   *   up in jar manifests or avpkit native property files.
149   * @param library the library object
150   * @throws UnsatisfiedLinkError if library cannot be loaded.
151   */
152  @SuppressWarnings("deprecation")
153  public static void load(String appname, JNILibrary library) {
154    // we force ALL work on all libraries to be synchronized
155    synchronized(mLock) {
156      deleteTemporaryFiles();
157      try {
158        library.load(appname);
159      } catch (UnsatisfiedLinkError e) {
160        // failed; faill back to old way
161        JNILibraryLoader.loadLibrary(library.getName(),
162            library.getVersion());
163      }
164    }
165  }
166  private void load(String appName) throws UnsatisfiedLinkError, SecurityException
167  {
168    if (mLoadAttempted) {
169      if (mLoadSuccessful)
170        return;
171      else
172        throw new UnsatisfiedLinkError("already attempted and failed to load library: " + getName());
173    }
174    mLoadAttempted = true;
175    // finally attempt to load ourselves   
176    loadFromClasspath(appName);
177    mLoadSuccessful = false;
178  }
179  
180  private void loadFromClasspath(String appName)
181  {
182    final JNILibrary priorAttempt = mAttemptedLibraries.get(getName());
183    if (priorAttempt != null) {
184      if (priorAttempt.mLoadSuccessful)
185        return;
186      else
187        throw new UnsatisfiedLinkError("previously attempted to load library and it failed: " + priorAttempt.getName());
188    }
189    
190    // from the manifests build a list of candidate library names to try
191    final List<String> libraryURLs = generateCandidateLibraryURLs(appName, getName());
192    
193    // finally go through each one until we get a load
194    for(String url : libraryURLs) {
195      if (unpackLibrary(url)) {
196        return;
197      }
198    }
199    // if we get all the way hwere, we did NOT succeed
200    throw new UnsatisfiedLinkError("could not load library: " + getName());
201       
202  }
203  private void doJNILoad(String url)
204  {
205    try {
206      log.trace(
207          "Attempt: library load of library: {}; url: {}",
208          new Object[]
209              {
210              getName(), url
211              });
212
213      System.load(url);
214      log.trace(
215          "Success: library load of library: {}; url: {}",
216          new Object[]
217              {
218              getName(), url
219              });
220      
221    } catch (UnsatisfiedLinkError e) {
222      log.warn(
223          "Failure: library load of library: {}; url: {}; error: {}",
224          new Object[]
225              {
226              getName(), url, e
227              });
228      throw e;
229    } catch (SecurityException e) {
230      log.warn(
231          "Failure: library load of library: {}; url: {}; error: {}",
232          new Object[]
233              {
234              getName(), url, e
235              });
236      throw e;
237    }
238  }
239  
240  /** Looks for a URL in a classpath, and if found, unpacks it */
241  private boolean unpackLibrary(String path)
242  {
243    final URL url = JNILibrary.class.getResource(path);
244    log.trace("path: {}; url: {}", path, url);
245    if (url == null)
246      return false;
247    
248    boolean unpacked = false;
249    File lib;
250    if (url.getProtocol().toLowerCase().equals("file")) {
251      // it SHOULD already exist on the disk.  let's look for it.
252      try {
253        lib = new File(new URI(url.toString()));
254      } catch (URISyntaxException e) {
255        lib = new File(url.getPath());
256      }
257      if (!lib.exists()) {
258        log.error("Unpacked library not unpacked correctedly;  url: {}", url);
259        return false;
260      }
261    } else {
262      // sucktastic -- we cannot in a JVM load a shared library
263      // directly from a JAR, so we need to unpack to a temp
264      // directory and load from there.
265      InputStream stream = JNILibrary.class.getResourceAsStream(path);
266      if (stream == null) {
267        log.error("could not get stream for resource: {}", path);
268        return false;
269      }
270      FileOutputStream out=null;
271      try {
272        File dir = getTmpDir();
273        // did you know windows REQUIRES .dll.  Sigh.
274        lib = ITempFileCreator.Builder.getDefault().createTempFile("avpkit", JNIEnv.getEnv().getOSFamily() == JNIEnv.OSFamily.WINDOWS ? ".dll" : null, dir);
275        lib.deleteOnExit();
276        out = new FileOutputStream(lib);
277        int bytesRead = 0;
278        final byte[] buffer = new byte[2048];
279        while ((bytesRead = stream.read(buffer, 0, buffer.length)) > 0) {
280          out.write(buffer, 0, bytesRead);
281        }
282        unpacked = true;
283      } catch (IOException e) {
284        log.error("could not create temp file: {}", e);
285        return false;
286      } finally {
287        try { stream.close(); } catch (IOException e) {}
288        if (out != null)
289          try { out.close() ; } catch (IOException e) {}
290      }
291    }
292    try {
293      doJNILoad(lib.getAbsolutePath());
294    } finally {
295      if (unpacked) {
296        // Well let's try to clean up after ourselves since
297        // we had ot unpack.
298        deleteUnpackedFile(lib.getAbsolutePath());
299      }
300    }
301    return true;
302  }
303  private void deleteUnpackedFile(String absolutePath)
304  {
305    final File file = new File(absolutePath);
306    if (file.delete())
307      return;
308    // sigh -- we could not delete it.  so we put a marker
309    // file along side and delete it the next time the library starts
310    // up.
311    final String markerName = file.getName()+XUGGLE_TEMP_EXTENSION;
312    try {
313      File marker = new File(file.getParentFile(), markerName);
314      marker.createNewFile();
315    } catch (IOException e) {
316      log.error("could not create marker file: {}; error: {}", markerName, e);
317      // and swallow it.
318    }
319  }
320  private static File getTmpDir() {
321    File tmpdir = new File(System.getProperty("java.io.tmpdir"));
322    File avpkitdir = new File(tmpdir, "avpkit");
323    avpkitdir.mkdirs();
324    return avpkitdir.exists() ? avpkitdir : tmpdir;
325  }
326  /**
327   * Finds all ".avpkit" temp files in the temp directory and
328   * nukes them.
329   */
330  private static void deleteTemporaryFiles()
331  {
332    final File dir = getTmpDir();
333    final FilenameFilter filter = new FilenameFilter() {
334      public boolean accept(File dir, String name)
335      {
336        return name.endsWith(XUGGLE_TEMP_EXTENSION);
337      }
338    };
339    final File markers[] = dir.listFiles(filter);
340    for(File marker: markers)
341    {
342      final String markerName = marker.getName();
343      final String libName = markerName.substring(0, markerName.length() - XUGGLE_TEMP_EXTENSION.length());
344      final File lib = new File(marker.getParentFile(), libName);
345      if (!lib.exists() || lib.delete())
346        marker.delete();
347    }
348  }
349  private List<String> generateCandidateLibraryURLs(String appName,
350      String libname)
351  {
352    final List<String> retval = new LinkedList<String>();    
353    final List<JNIManifest> manifests = getNativeManifests(appName);
354    
355    // for each manifest, generate URLs
356    for (final JNIManifest manifest : manifests) {
357      generateLibnames(retval, manifest.getPath(), libname);
358    }
359    // and finally we also test the top of the classpath in the event
360    // that this is an applet or Web-Start app.
361    generateLibnames(retval, "/", libname);
362    
363    return retval;
364  }
365  
366  private static JNIEnv.OSFamily mOSFamily = JNIEnv.getEnv().getOSFamily();
367  // These methods are at package visibility to allow testing only
368  // They are not meant for use
369  void setOSFamily(JNIEnv.OSFamily os) { mOSFamily = os; }
370  JNIEnv.OSFamily getOSFamily() { return mOSFamily; }
371  
372  private void generateLibnames(List<String> list,
373      String path,
374      String libname)
375  {
376    final String[] prefixes;
377    final String[] suffixes;
378    switch (getOSFamily())
379    {
380      case UNKNOWN:
381      case LINUX:
382        prefixes = new String[]
383        {
384            "lib", ""
385        };
386        suffixes = new String[]
387        {
388          ".so",".so."+com.avpkit.core.Version.MAJOR_VERSION
389        };
390        break;
391      case WINDOWS:
392        prefixes = new String[]
393        {
394            "lib", "", "cyg"
395        };
396        suffixes = new String[]
397        {
398          "-"+com.avpkit.core.Version.MAJOR_VERSION+".dll", ".dll"
399        };
400        break;
401      case MAC:
402        prefixes = new String[]
403        {
404            "lib", ""
405        };
406        suffixes = new String[]
407        {
408          ".dylib"
409        };
410        break;
411      default:
412        // really no cases should get here
413        prefixes = null;
414        suffixes = null;
415        break;
416    }
417    // can assume URL separators
418    final String dirSeparator = "/";
419    if (path.length() > 0 && !path.endsWith(dirSeparator))
420      path = path + dirSeparator;
421    for (String suffix : suffixes)
422      for (String prefix : prefixes)
423        list.add(path + prefix + libname + suffix);   
424  }
425
426  public int compareTo(JNILibrary o)
427  {
428    if (o == null)
429      return -1;
430
431    int retval = mName.compareTo(o.mName);
432    if (retval == 0) {
433      if (mVersion == null) {
434        if (o.mVersion != null) {
435          retval = 1;
436        }
437      } else {
438        if (o.mVersion == null)
439          retval = -1;
440        else
441          retval = mVersion.compareTo(o.mVersion);
442      }
443    }
444    return retval;
445  }
446}