/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.soloader;
import java.io.File;
import java.io.IOException;
import android.content.Context;
import java.util.jar.JarFile;
import java.util.jar.JarEntry;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import android.os.Build;
import android.system.Os;
import android.system.ErrnoException;
import java.util.HashMap;
import java.util.Map;
import java.util.Enumeration;
import java.io.InputStream;
import java.io.FileOutputStream;
import android.util.Log;
/**
* {@link SoSource} that extracts libraries from an APK to the filesystem.
*/
public class ApkSoSource extends DirectorySoSource {
private static final String TAG = SoLoader.TAG;
private static final boolean DEBUG = SoLoader.DEBUG;
/**
* Make a new ApkSoSource that extracts DSOs from our APK instead of relying on the system to do
* the extraction for us.
*
* @param context Application context
*/
public ApkSoSource(Context context) throws IOException {
//
// Initialize a normal DirectorySoSource that will load from our extraction directory. At this
// point, the directory may be empty or contain obsolete libraries, but that's okay.
//
super(SysUtil.createLibsDirectory(context), DirectorySoSource.RESOLVE_DEPENDENCIES);
//
// Synchronize the contents of that directory with the library payload in our APK, deleting and
// extracting as needed.
//
try (JarFile apk = new JarFile(context.getApplicationInfo().publicSourceDir)) {
File libsDir = super.soDirectory;
if (DEBUG) {
Log.v(TAG, "synchronizing log directory: " + libsDir);
}
Map<String, SoInfo> providedLibraries = findProvidedLibraries(apk);
try (FileLocker lock = SysUtil.lockLibsDirectory(context)) {
// Delete files in libsDir that we don't provide or that are out of date. Forget about any
// libraries that are up-to-date already so we don't unpack them below.
File extantFiles[] = libsDir.listFiles();
for (int i = 0; i < extantFiles.length; ++i) {
File extantFile = extantFiles[i];
if (DEBUG) {
Log.v(TAG, "considering libdir file: " + extantFile);
}
String name = extantFile.getName();
SoInfo so = providedLibraries.get(name);
boolean shouldDelete =
(so == null ||
so.entry.getSize() != extantFile.length() ||
so.entry.getTime() != extantFile.lastModified());
boolean upToDate = (so != null && !shouldDelete);
if (shouldDelete) {
if (DEBUG) {
Log.v(TAG, "deleting obsolete or unexpected file: " + extantFile);
}
SysUtil.deleteOrThrow(extantFile);
}
if (upToDate) {
if (DEBUG) {
Log.v(TAG, "found up-to-date library: " + extantFile);
}
providedLibraries.remove(name);
}
}
// Now extract any libraries left in providedLibraries; we removed all the up-to-date ones.
for (SoInfo so : providedLibraries.values()) {
JarEntry entry = so.entry;
try (InputStream is = apk.getInputStream(entry)) {
if (DEBUG) {
Log.v(TAG, "extracting library: " + so.soName);
}
SysUtil.reliablyCopyExecutable(
is,
new File(libsDir, so.soName),
entry.getSize(),
entry.getTime());
}
SysUtil.freeCopyBuffer();
}
}
}
}
/**
* Find the shared libraries provided in this APK and supported on this system. Each returend
* SoInfo points to the most preferred version of that library bundled with the given APK: for
* example, if we're on an armv7-a system and we have both arm and armv7-a versions of libfoo, the
* returned entry for libfoo points to the armv7-a version of libfoo.
*
* The caller owns the returned value and may mutate it.
*
* @param apk Opened application APK file
* @return Map of sonames to SoInfo instances
*/
private static Map<String, SoInfo> findProvidedLibraries(JarFile apk) {
// Subgroup 1: ABI. Subgroup 2: soname.
Pattern libPattern = Pattern.compile("^lib/([^/]+)/([^/]+\\.so)$");
HashMap<String, SoInfo> providedLibraries = new HashMap<>();
String[] supportedAbis = SysUtil.getSupportedAbis();
Enumeration<JarEntry> entries = apk.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
Matcher m = libPattern.matcher(entry.getName());
if (m.matches()) {
String libraryAbi = m.group(1);
String soName = m.group(2);
int abiScore = SysUtil.findAbiScore(supportedAbis, libraryAbi);
if (abiScore >= 0) {
SoInfo so = providedLibraries.get(soName);
if (so == null || abiScore < so.abiScore) {
providedLibraries.put(soName, new SoInfo(soName, entry, abiScore));
}
}
}
}
return providedLibraries;
}
private static final class SoInfo {
public final String soName;
public final JarEntry entry;
public final int abiScore;
SoInfo(String soName, JarEntry entry, int abiScore) {
this.soName = soName;
this.entry = entry;
this.abiScore = abiScore;
}
}
}