/* 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 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 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 File, 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 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 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 parseUrl(String url) { return parseUrl(url, null); } private static Stack parseUrl(String url, Stack results) { if (results == null) { results = new Stack(); } 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; } }