diff options
Diffstat (limited to 'mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java')
-rw-r--r-- | mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java | 261 |
1 files changed, 261 insertions, 0 deletions
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java new file mode 100644 index 000000000..4e11592a4 --- /dev/null +++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/util/GeckoJarReader.java @@ -0,0 +1,261 @@ +/* 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); + // BitmapDrawable created from a stream does not set the correct target density from resources. + // In fact it discards the resources https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/graphics/java/android/graphics/drawable/BitmapDrawable.java#191 + bitmap.setTargetDensity(resources.getDisplayMetrics()); + } + } 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); + } + } + } + + 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); + } + } + } + + 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; + } +} |