Files
tubestation/mobile/android/base/util/GeckoJarReader.java

265 lines
9.6 KiB
Java

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.util;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.mozglue.GeckoLoader;
import org.mozilla.gecko.mozglue.NativeZip;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Stack;
/* Reads out of a multiple level deep jar file such as
* jar:jar:file:///data/app/org.mozilla.fennec.apk!/omni.ja!/chrome/chrome/content/branding/favicon32.png
*/
public final class GeckoJarReader {
private static final String LOGTAG = "GeckoJarReader";
private GeckoJarReader() {}
public static Bitmap getBitmap(Context context, Resources resources, String url) {
BitmapDrawable drawable = getBitmapDrawable(context, resources, url);
return (drawable != null) ? drawable.getBitmap() : null;
}
public static BitmapDrawable getBitmapDrawable(Context context, Resources resources,
String url) {
Stack<String> jarUrls = parseUrl(url);
InputStream inputStream = null;
BitmapDrawable bitmap = null;
NativeZip zip = null;
try {
// Load the initial jar file as a zip
zip = getZipFile(context, jarUrls.pop());
inputStream = getStream(zip, jarUrls, url);
if (inputStream != null) {
bitmap = new BitmapDrawable(resources, inputStream);
}
} catch (IOException | URISyntaxException ex) {
Log.e(LOGTAG, "Exception ", ex);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch(IOException ex) {
Log.e(LOGTAG, "Error closing stream", ex);
}
}
if (zip != null) {
zip.close();
}
}
return bitmap;
}
public static String getText(Context context, String url) {
Stack<String> jarUrls = parseUrl(url);
NativeZip zip = null;
BufferedReader reader = null;
String text = null;
try {
zip = getZipFile(context, jarUrls.pop());
InputStream input = getStream(zip, jarUrls, url);
if (input != null) {
reader = new BufferedReader(new InputStreamReader(input));
text = reader.readLine();
}
} catch (IOException | URISyntaxException ex) {
Log.e(LOGTAG, "Exception ", ex);
} finally {
if (reader != null) {
try {
reader.close();
} catch(IOException ex) {
Log.e(LOGTAG, "Error closing reader", ex);
}
}
if (zip != null) {
zip.close();
}
}
return text;
}
private static NativeZip getZipFile(Context context, String url)
throws IOException, URISyntaxException {
URI fileUrl = new URI(url);
GeckoLoader.loadMozGlue(context);
return new NativeZip(fileUrl.getPath());
}
@RobocopTarget
/**
* Extract a (possibly nested) file from an archive and write it to a temporary file.
*
* @param context Android context.
* @param url to open. Can include jar: to "reach into" nested archives.
* @param dir to write temporary file to.
* @return a <code>File</code>, if one could be written; otherwise null.
* @throws IOException if an error occured.
*/
public static File extractStream(Context context, String url, File dir, String suffix) throws IOException {
InputStream input = null;
try {
try {
final URI fileURI = new URI(url);
// We don't check the scheme because we want to catch bare files, not just file:// URIs.
// If we let bare files through, we'd try to open them as ZIP files later -- and crash in native code.
if (fileURI != null && fileURI.getPath() != null) {
final File inputFile = new File(fileURI.getPath());
if (inputFile != null && inputFile.exists()) {
input = new FileInputStream(inputFile);
}
}
} catch (URISyntaxException e) {
// Not a file:// URI.
}
if (input == null) {
// No luck with file:// URI; maybe some other URI?
input = getStream(context, url);
}
if (input == null) {
// Not found!
return null;
}
// n.b.: createTempFile does not in fact delete the file.
final File file = File.createTempFile("extractStream", suffix, dir);
OutputStream output = null;
try {
output = new FileOutputStream(file);
byte[] buf = new byte[8192];
int len;
while ((len = input.read(buf)) >= 0) {
output.write(buf, 0, len);
}
return file;
} finally {
if (output != null) {
output.close();
}
}
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
Log.w(LOGTAG, "Got exception closing stream; ignoring.", e);
}
}
}
}
@RobocopTarget
public static InputStream getStream(Context context, String url) {
Stack<String> jarUrls = parseUrl(url);
try {
NativeZip zip = getZipFile(context, jarUrls.pop());
return getStream(zip, jarUrls, url);
} catch (Exception ex) {
// Some JNI code throws IllegalArgumentException on a bad file name;
// swallow the error and return null. We could also see legitimate
// IOExceptions here.
Log.e(LOGTAG, "Exception getting input stream from jar URL: " + url, ex);
return null;
}
}
private static InputStream getStream(NativeZip zip, Stack<String> jarUrls, String origUrl) {
InputStream inputStream = null;
// loop through children jar files until we reach the innermost one
while (!jarUrls.empty()) {
String fileName = jarUrls.pop();
if (inputStream != null) {
// intermediate NativeZips and InputStreams will be garbage collected.
try {
zip = new NativeZip(inputStream);
} catch (IllegalArgumentException e) {
String description = "!!! BUG 849589 !!! origUrl=" + origUrl;
Log.e(LOGTAG, description, e);
throw new IllegalArgumentException(description);
}
}
inputStream = zip.getInputStream(fileName);
if (inputStream == null) {
Log.d(LOGTAG, "No Entry for " + fileName);
return null;
}
}
return inputStream;
}
/* Returns a stack of strings breaking the url up into pieces. Each piece
* is assumed to point to a jar file except for the final one. Callers should
* pass in the url to parse, and null for the parent parameter (used for recursion)
* For example, jar:jar:file:///data/app/org.mozilla.fennec.apk!/omni.ja!/chrome/chrome/content/branding/favicon32.png
* will return:
* file:///data/app/org.mozilla.fennec.apk
* omni.ja
* chrome/chrome/content/branding/favicon32.png
*/
private static Stack<String> parseUrl(String url) {
return parseUrl(url, null);
}
private static Stack<String> parseUrl(String url, Stack<String> results) {
if (results == null) {
results = new Stack<String>();
}
if (url.startsWith("jar:")) {
int jarEnd = url.lastIndexOf("!");
String subStr = url.substring(4, jarEnd);
results.push(url.substring(jarEnd+2)); // remove the !/ characters
return parseUrl(subStr, results);
} else {
results.push(url);
return results;
}
}
public static String getJarURL(Context context, String pathInsideJAR) {
// We need to encode the package resource path, because it might contain illegal characters. For example:
// /mnt/asec2/[2]org.mozilla.fennec-1/pkg.apk
// The round-trip through a URI does this for us.
final String resourcePath = context.getPackageResourcePath();
return computeJarURI(resourcePath, pathInsideJAR);
}
/**
* Encodes its resource path correctly.
*/
@RobocopTarget
public static String computeJarURI(String resourcePath, String pathInsideJAR) {
final String resURI = new File(resourcePath).toURI().toString();
// TODO: do we need to encode the file path, too?
return "jar:jar:" + resURI + "!/" + AppConstants.OMNIJAR_NAME + "!/" + pathInsideJAR;
}
}