Loading core/java/android/content/pm/PackageParser.java +158 −3 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package android.content.pm; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; Loading @@ -39,6 +40,9 @@ import android.os.FileUtils; import android.os.PatternMatcher; import android.os.Trace; import android.os.UserHandle; import android.system.ErrnoException; import android.system.OsConstants; import android.system.StructStat; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; Loading @@ -54,6 +58,8 @@ import android.util.jar.StrictJarFile; import android.view.Gravity; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; Loading Loading @@ -259,6 +265,7 @@ public class PackageParser { private String[] mSeparateProcesses; private boolean mOnlyCoreApps; private DisplayMetrics mMetrics; private File mCacheDir; private static final int SDK_VERSION = Build.VERSION.SDK_INT; private static final String[] SDK_CODENAMES = Build.VERSION.ACTIVE_CODENAMES; Loading Loading @@ -453,6 +460,13 @@ public class PackageParser { mMetrics = metrics; } /** * Sets the cache directory for this package parser. */ public void setCacheDir(File cacheDir) { mCacheDir = cacheDir; } public static final boolean isApkFile(File file) { return isApkPath(file.getName()); } Loading Loading @@ -806,13 +820,154 @@ public class PackageParser { * Note that this <em>does not</em> perform signature verification; that * must be done separately in {@link #collectCertificates(Package, int)}. * * If {@code useCaches} is true, the package parser might return a cached * result from a previous parse of the same {@code packageFile} with the same * {@code flags}. Note that this method does not check whether {@code packageFile} * has changed since the last parse, it's up to callers to do so. * * @see #parsePackageLite(File, int) */ public Package parsePackage(File packageFile, int flags) throws PackageParserException { public Package parsePackage(File packageFile, int flags, boolean useCaches) throws PackageParserException { Package parsed = useCaches ? getCachedResult(packageFile, flags) : null; if (parsed != null) { return parsed; } if (packageFile.isDirectory()) { return parseClusterPackage(packageFile, flags); parsed = parseClusterPackage(packageFile, flags); } else { return parseMonolithicPackage(packageFile, flags); parsed = parseMonolithicPackage(packageFile, flags); } cacheResult(packageFile, flags, parsed); return parsed; } /** * Equivalent to {@link #parsePackage(File, int, boolean)} with {@code useCaches == false}. */ public Package parsePackage(File packageFile, int flags) throws PackageParserException { return parsePackage(packageFile, flags, false /* useCaches */); } /** * Returns the cache key for a specificied {@code packageFile} and {@code flags}. */ private String getCacheKey(File packageFile, int flags) { StringBuilder sb = new StringBuilder(packageFile.getName()); sb.append('-'); sb.append(flags); return sb.toString(); } @VisibleForTesting protected Package fromCacheEntry(byte[] bytes) throws IOException { return null; } @VisibleForTesting protected byte[] toCacheEntry(Package pkg) throws IOException { return null; } /** * Given a {@code packageFile} and a {@code cacheFile} returns whether the * cache file is up to date based on the mod-time of both files. */ private static boolean isCacheUpToDate(File packageFile, File cacheFile) { try { // NOTE: We don't use the File.lastModified API because it has the very // non-ideal failure mode of returning 0 with no excepions thrown. // The nio2 Files API is a little better but is considerably more expensive. final StructStat pkg = android.system.Os.stat(packageFile.getAbsolutePath()); final StructStat cache = android.system.Os.stat(cacheFile.getAbsolutePath()); return pkg.st_mtime < cache.st_mtime; } catch (ErrnoException ee) { // The most common reason why stat fails is that a given cache file doesn't // exist. We ignore that here. It's easy to reason that it's safe to say the // cache isn't up to date if we see any sort of exception here. // // (1) Exception while stating the package file : This should never happen, // and if it does, we do a full package parse (which is likely to throw the // same exception). // (2) Exception while stating the cache file : If the file doesn't exist, the // cache is obviously out of date. If the file *does* exist, we can't read it. // We will attempt to delete and recreate it after parsing the package. if (ee.errno != OsConstants.ENOENT) { Slog.w("Error while stating package cache : ", ee); } return false; } } /** * Returns the cached parse result for {@code packageFile} for parse flags {@code flags}, * or {@code null} if no cached result exists. */ private Package getCachedResult(File packageFile, int flags) { if (mCacheDir == null) { return null; } final String cacheKey = getCacheKey(packageFile, flags); final File cacheFile = new File(mCacheDir, cacheKey); // If the cache is not up to date, return null. if (!isCacheUpToDate(packageFile, cacheFile)) { return null; } try { final byte[] bytes = IoUtils.readFileAsByteArray(cacheFile.getAbsolutePath()); return fromCacheEntry(bytes); } catch (IOException ioe) { Slog.w(TAG, "Error reading package cache: ", ioe); // If something went wrong while reading the cache entry, delete the cache file // so that we regenerate it the next time. cacheFile.delete(); return null; } } /** * Caches the parse result for {@code packageFile} with flags {@code flags}. */ private void cacheResult(File packageFile, int flags, Package parsed) { if (mCacheDir == null) { return; } final String cacheKey = getCacheKey(packageFile, flags); final File cacheFile = new File(mCacheDir, cacheKey); if (cacheFile.exists()) { if (!cacheFile.delete()) { Slog.e(TAG, "Unable to delete cache file: " + cacheFile); } } final byte[] cacheEntry; try { cacheEntry = toCacheEntry(parsed); } catch (IOException ioe) { Slog.e(TAG, "Unable to serialize parsed package for: " + packageFile); return; } if (cacheEntry == null) { return; } try (FileOutputStream fos = new FileOutputStream(cacheFile)) { fos.write(cacheEntry); } catch (IOException ioe) { Slog.w(TAG, "Error writing cache entry.", ioe); cacheFile.delete(); } } Loading services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java 0 → 100644 +105 −0 Original line number Diff line number Diff line /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.pm; import android.content.pm.PackageParser; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.MediumTest; import java.io.File; import java.nio.charset.StandardCharsets; import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import libcore.io.IoUtils; @RunWith(AndroidJUnit4.class) @MediumTest public class PackageParserTest { private File mTmpDir; private static final File FRAMEWORK = new File("/system/framework/framework-res.apk"); @Before public void setUp() { // Create a new temporary directory for each of our tests. mTmpDir = IoUtils.createTemporaryDirectory("PackageParserTest"); } @Test public void testParse_noCache() throws Exception { PackageParser pp = new CachePackageNameParser(); PackageParser.Package pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, false /* useCaches */); assertNotNull(pkg); pp.setCacheDir(mTmpDir); pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, false /* useCaches */); assertNotNull(pkg); // Make sure that we always write out a cache entry for future reference, // whether or not we're asked to use caches. assertEquals(1, mTmpDir.list().length); } @Test public void testParse_withCache() throws Exception { PackageParser pp = new CachePackageNameParser(); pp.setCacheDir(mTmpDir); // The first parse will write this package to the cache. pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, true /* useCaches */); // Now attempt to parse the package again, should return the // cached result. PackageParser.Package pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, true /* useCaches */); assertEquals("cache_android", pkg.packageName); // Try again, with useCaches == false, shouldn't return the parsed // result. pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, false /* useCaches */); assertEquals("android", pkg.packageName); // We haven't set a cache directory here : the parse should still succeed, // just not using the cached results. pp = new CachePackageNameParser(); pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, true /* useCaches */); assertEquals("android", pkg.packageName); pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, false /* useCaches */); assertEquals("android", pkg.packageName); } /** * A trivial subclass of package parser that only caches the package name, and throws away * all other information. */ public static class CachePackageNameParser extends PackageParser { @Override public byte[] toCacheEntry(Package pkg) { return ("cache_" + pkg.packageName).getBytes(StandardCharsets.UTF_8); } @Override public Package fromCacheEntry(byte[] cacheEntry) { return new Package(new String(cacheEntry, StandardCharsets.UTF_8)); } } } Loading
core/java/android/content/pm/PackageParser.java +158 −3 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package android.content.pm; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; Loading @@ -39,6 +40,9 @@ import android.os.FileUtils; import android.os.PatternMatcher; import android.os.Trace; import android.os.UserHandle; import android.system.ErrnoException; import android.system.OsConstants; import android.system.StructStat; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; Loading @@ -54,6 +58,8 @@ import android.util.jar.StrictJarFile; import android.view.Gravity; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; Loading Loading @@ -259,6 +265,7 @@ public class PackageParser { private String[] mSeparateProcesses; private boolean mOnlyCoreApps; private DisplayMetrics mMetrics; private File mCacheDir; private static final int SDK_VERSION = Build.VERSION.SDK_INT; private static final String[] SDK_CODENAMES = Build.VERSION.ACTIVE_CODENAMES; Loading Loading @@ -453,6 +460,13 @@ public class PackageParser { mMetrics = metrics; } /** * Sets the cache directory for this package parser. */ public void setCacheDir(File cacheDir) { mCacheDir = cacheDir; } public static final boolean isApkFile(File file) { return isApkPath(file.getName()); } Loading Loading @@ -806,13 +820,154 @@ public class PackageParser { * Note that this <em>does not</em> perform signature verification; that * must be done separately in {@link #collectCertificates(Package, int)}. * * If {@code useCaches} is true, the package parser might return a cached * result from a previous parse of the same {@code packageFile} with the same * {@code flags}. Note that this method does not check whether {@code packageFile} * has changed since the last parse, it's up to callers to do so. * * @see #parsePackageLite(File, int) */ public Package parsePackage(File packageFile, int flags) throws PackageParserException { public Package parsePackage(File packageFile, int flags, boolean useCaches) throws PackageParserException { Package parsed = useCaches ? getCachedResult(packageFile, flags) : null; if (parsed != null) { return parsed; } if (packageFile.isDirectory()) { return parseClusterPackage(packageFile, flags); parsed = parseClusterPackage(packageFile, flags); } else { return parseMonolithicPackage(packageFile, flags); parsed = parseMonolithicPackage(packageFile, flags); } cacheResult(packageFile, flags, parsed); return parsed; } /** * Equivalent to {@link #parsePackage(File, int, boolean)} with {@code useCaches == false}. */ public Package parsePackage(File packageFile, int flags) throws PackageParserException { return parsePackage(packageFile, flags, false /* useCaches */); } /** * Returns the cache key for a specificied {@code packageFile} and {@code flags}. */ private String getCacheKey(File packageFile, int flags) { StringBuilder sb = new StringBuilder(packageFile.getName()); sb.append('-'); sb.append(flags); return sb.toString(); } @VisibleForTesting protected Package fromCacheEntry(byte[] bytes) throws IOException { return null; } @VisibleForTesting protected byte[] toCacheEntry(Package pkg) throws IOException { return null; } /** * Given a {@code packageFile} and a {@code cacheFile} returns whether the * cache file is up to date based on the mod-time of both files. */ private static boolean isCacheUpToDate(File packageFile, File cacheFile) { try { // NOTE: We don't use the File.lastModified API because it has the very // non-ideal failure mode of returning 0 with no excepions thrown. // The nio2 Files API is a little better but is considerably more expensive. final StructStat pkg = android.system.Os.stat(packageFile.getAbsolutePath()); final StructStat cache = android.system.Os.stat(cacheFile.getAbsolutePath()); return pkg.st_mtime < cache.st_mtime; } catch (ErrnoException ee) { // The most common reason why stat fails is that a given cache file doesn't // exist. We ignore that here. It's easy to reason that it's safe to say the // cache isn't up to date if we see any sort of exception here. // // (1) Exception while stating the package file : This should never happen, // and if it does, we do a full package parse (which is likely to throw the // same exception). // (2) Exception while stating the cache file : If the file doesn't exist, the // cache is obviously out of date. If the file *does* exist, we can't read it. // We will attempt to delete and recreate it after parsing the package. if (ee.errno != OsConstants.ENOENT) { Slog.w("Error while stating package cache : ", ee); } return false; } } /** * Returns the cached parse result for {@code packageFile} for parse flags {@code flags}, * or {@code null} if no cached result exists. */ private Package getCachedResult(File packageFile, int flags) { if (mCacheDir == null) { return null; } final String cacheKey = getCacheKey(packageFile, flags); final File cacheFile = new File(mCacheDir, cacheKey); // If the cache is not up to date, return null. if (!isCacheUpToDate(packageFile, cacheFile)) { return null; } try { final byte[] bytes = IoUtils.readFileAsByteArray(cacheFile.getAbsolutePath()); return fromCacheEntry(bytes); } catch (IOException ioe) { Slog.w(TAG, "Error reading package cache: ", ioe); // If something went wrong while reading the cache entry, delete the cache file // so that we regenerate it the next time. cacheFile.delete(); return null; } } /** * Caches the parse result for {@code packageFile} with flags {@code flags}. */ private void cacheResult(File packageFile, int flags, Package parsed) { if (mCacheDir == null) { return; } final String cacheKey = getCacheKey(packageFile, flags); final File cacheFile = new File(mCacheDir, cacheKey); if (cacheFile.exists()) { if (!cacheFile.delete()) { Slog.e(TAG, "Unable to delete cache file: " + cacheFile); } } final byte[] cacheEntry; try { cacheEntry = toCacheEntry(parsed); } catch (IOException ioe) { Slog.e(TAG, "Unable to serialize parsed package for: " + packageFile); return; } if (cacheEntry == null) { return; } try (FileOutputStream fos = new FileOutputStream(cacheFile)) { fos.write(cacheEntry); } catch (IOException ioe) { Slog.w(TAG, "Error writing cache entry.", ioe); cacheFile.delete(); } } Loading
services/tests/servicestests/src/com/android/server/pm/PackageParserTest.java 0 → 100644 +105 −0 Original line number Diff line number Diff line /* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.pm; import android.content.pm.PackageParser; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.MediumTest; import java.io.File; import java.nio.charset.StandardCharsets; import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import libcore.io.IoUtils; @RunWith(AndroidJUnit4.class) @MediumTest public class PackageParserTest { private File mTmpDir; private static final File FRAMEWORK = new File("/system/framework/framework-res.apk"); @Before public void setUp() { // Create a new temporary directory for each of our tests. mTmpDir = IoUtils.createTemporaryDirectory("PackageParserTest"); } @Test public void testParse_noCache() throws Exception { PackageParser pp = new CachePackageNameParser(); PackageParser.Package pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, false /* useCaches */); assertNotNull(pkg); pp.setCacheDir(mTmpDir); pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, false /* useCaches */); assertNotNull(pkg); // Make sure that we always write out a cache entry for future reference, // whether or not we're asked to use caches. assertEquals(1, mTmpDir.list().length); } @Test public void testParse_withCache() throws Exception { PackageParser pp = new CachePackageNameParser(); pp.setCacheDir(mTmpDir); // The first parse will write this package to the cache. pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, true /* useCaches */); // Now attempt to parse the package again, should return the // cached result. PackageParser.Package pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, true /* useCaches */); assertEquals("cache_android", pkg.packageName); // Try again, with useCaches == false, shouldn't return the parsed // result. pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, false /* useCaches */); assertEquals("android", pkg.packageName); // We haven't set a cache directory here : the parse should still succeed, // just not using the cached results. pp = new CachePackageNameParser(); pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, true /* useCaches */); assertEquals("android", pkg.packageName); pkg = pp.parsePackage(FRAMEWORK, 0 /* parseFlags */, false /* useCaches */); assertEquals("android", pkg.packageName); } /** * A trivial subclass of package parser that only caches the package name, and throws away * all other information. */ public static class CachePackageNameParser extends PackageParser { @Override public byte[] toCacheEntry(Package pkg) { return ("cache_" + pkg.packageName).getBytes(StandardCharsets.UTF_8); } @Override public Package fromCacheEntry(byte[] cacheEntry) { return new Package(new String(cacheEntry, StandardCharsets.UTF_8)); } } }