1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.pm.parsing;
18 
19 import android.annotation.NonNull;
20 import android.content.pm.PackageParserCacheHelper;
21 import android.os.FileUtils;
22 import android.os.Parcel;
23 import android.system.ErrnoException;
24 import android.system.Os;
25 import android.system.OsConstants;
26 import android.system.StructStat;
27 import android.util.Slog;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.server.pm.parsing.pkg.PackageImpl;
31 import com.android.server.pm.parsing.pkg.ParsedPackage;
32 
33 import libcore.io.IoUtils;
34 
35 import java.io.File;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.util.concurrent.atomic.AtomicInteger;
39 
40 public class PackageCacher {
41 
42     private static final String TAG = "PackageCacher";
43 
44     /**
45      * Total number of packages that were read from the cache.  We use it only for logging.
46      */
47     public static final AtomicInteger sCachedPackageReadCount = new AtomicInteger();
48 
49     @NonNull
50     private File mCacheDir;
51 
PackageCacher(@onNull File cacheDir)52     public PackageCacher(@NonNull File cacheDir) {
53         this.mCacheDir = cacheDir;
54     }
55 
56     /**
57      * Returns the cache key for a specified {@code packageFile} and {@code flags}.
58      */
getCacheKey(File packageFile, int flags)59     private String getCacheKey(File packageFile, int flags) {
60         StringBuilder sb = new StringBuilder(packageFile.getName());
61         sb.append('-');
62         sb.append(flags);
63 
64         return sb.toString();
65     }
66 
67     @VisibleForTesting
fromCacheEntry(byte[] bytes)68     protected ParsedPackage fromCacheEntry(byte[] bytes) {
69         return fromCacheEntryStatic(bytes);
70     }
71 
72     /** static version of {@link #fromCacheEntry} for unit tests. */
73     @VisibleForTesting
fromCacheEntryStatic(byte[] bytes)74     public static ParsedPackage fromCacheEntryStatic(byte[] bytes) {
75         final Parcel p = Parcel.obtain();
76         p.unmarshall(bytes, 0, bytes.length);
77         p.setDataPosition(0);
78 
79         final PackageParserCacheHelper.ReadHelper helper = new PackageParserCacheHelper.ReadHelper(p);
80         helper.startAndInstall();
81 
82         ParsedPackage pkg = new PackageImpl(p);
83 
84         p.recycle();
85 
86         sCachedPackageReadCount.incrementAndGet();
87 
88         return pkg;
89     }
90 
91     @VisibleForTesting
toCacheEntry(ParsedPackage pkg)92     protected byte[] toCacheEntry(ParsedPackage pkg) {
93         return toCacheEntryStatic(pkg);
94 
95     }
96 
97     /** static version of {@link #toCacheEntry} for unit tests. */
98     @VisibleForTesting
toCacheEntryStatic(ParsedPackage pkg)99     public static byte[] toCacheEntryStatic(ParsedPackage pkg) {
100         final Parcel p = Parcel.obtain();
101         final PackageParserCacheHelper.WriteHelper helper = new PackageParserCacheHelper.WriteHelper(p);
102 
103         pkg.writeToParcel(p, 0 /* flags */);
104 
105         helper.finishAndUninstall();
106 
107         byte[] serialized = p.marshall();
108         p.recycle();
109 
110         return serialized;
111     }
112 
113     /**
114      * Given a {@code packageFile} and a {@code cacheFile} returns whether the
115      * cache file is up to date based on the mod-time of both files.
116      */
isCacheUpToDate(File packageFile, File cacheFile)117     private static boolean isCacheUpToDate(File packageFile, File cacheFile) {
118         try {
119             // NOTE: We don't use the File.lastModified API because it has the very
120             // non-ideal failure mode of returning 0 with no excepions thrown.
121             // The nio2 Files API is a little better but is considerably more expensive.
122             final StructStat pkg = Os.stat(packageFile.getAbsolutePath());
123             final StructStat cache = Os.stat(cacheFile.getAbsolutePath());
124             return pkg.st_mtime < cache.st_mtime;
125         } catch (ErrnoException ee) {
126             // The most common reason why stat fails is that a given cache file doesn't
127             // exist. We ignore that here. It's easy to reason that it's safe to say the
128             // cache isn't up to date if we see any sort of exception here.
129             //
130             // (1) Exception while stating the package file : This should never happen,
131             // and if it does, we do a full package parse (which is likely to throw the
132             // same exception).
133             // (2) Exception while stating the cache file : If the file doesn't exist, the
134             // cache is obviously out of date. If the file *does* exist, we can't read it.
135             // We will attempt to delete and recreate it after parsing the package.
136             if (ee.errno != OsConstants.ENOENT) {
137                 Slog.w("Error while stating package cache : ", ee);
138             }
139 
140             return false;
141         }
142     }
143 
144     /**
145      * Returns the cached parse result for {@code packageFile} for parse flags {@code flags},
146      * or {@code null} if no cached result exists.
147      */
getCachedResult(File packageFile, int flags)148     public ParsedPackage getCachedResult(File packageFile, int flags) {
149         final String cacheKey = getCacheKey(packageFile, flags);
150         final File cacheFile = new File(mCacheDir, cacheKey);
151 
152         try {
153             // If the cache is not up to date, return null.
154             if (!isCacheUpToDate(packageFile, cacheFile)) {
155                 return null;
156             }
157 
158             final byte[] bytes = IoUtils.readFileAsByteArray(cacheFile.getAbsolutePath());
159             return fromCacheEntry(bytes);
160         } catch (Throwable e) {
161             Slog.w(TAG, "Error reading package cache: ", e);
162 
163             // If something went wrong while reading the cache entry, delete the cache file
164             // so that we regenerate it the next time.
165             cacheFile.delete();
166             return null;
167         }
168     }
169 
170     /**
171      * Caches the parse result for {@code packageFile} with flags {@code flags}.
172      */
cacheResult(File packageFile, int flags, ParsedPackage parsed)173     public void cacheResult(File packageFile, int flags, ParsedPackage parsed) {
174         try {
175             final String cacheKey = getCacheKey(packageFile, flags);
176             final File cacheFile = new File(mCacheDir, cacheKey);
177 
178             if (cacheFile.exists()) {
179                 if (!cacheFile.delete()) {
180                     Slog.e(TAG, "Unable to delete cache file: " + cacheFile);
181                 }
182             }
183 
184             final byte[] cacheEntry = toCacheEntry(parsed);
185 
186             if (cacheEntry == null) {
187                 return;
188             }
189 
190             try (FileOutputStream fos = new FileOutputStream(cacheFile)) {
191                 fos.write(cacheEntry);
192             } catch (IOException ioe) {
193                 Slog.w(TAG, "Error writing cache entry.", ioe);
194                 cacheFile.delete();
195             }
196         } catch (Throwable e) {
197             Slog.w(TAG, "Error saving package cache.", e);
198         }
199     }
200 
201     /**
202      * Delete the cache files for the given {@code packageFile}.
203      */
cleanCachedResult(@onNull File packageFile)204     public void cleanCachedResult(@NonNull File packageFile) {
205         final String packageName = packageFile.getName();
206         final File[] files = FileUtils.listFilesOrEmpty(mCacheDir,
207                 (dir, name) -> name.startsWith(packageName));
208         for (File file : files) {
209             if (!file.delete()) {
210                 Slog.e(TAG, "Unable to clean cache file: " + file);
211             }
212         }
213     }
214 }
215