• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.internal.content;
18 
19 import android.annotation.NonNull;
20 import android.content.ContentResolver;
21 import android.os.Environment;
22 import android.os.incremental.IncrementalManager;
23 import android.provider.Settings.Secure;
24 import android.text.TextUtils;
25 import android.util.Slog;
26 
27 import java.io.File;
28 import java.io.IOException;
29 import java.nio.file.Files;
30 import java.util.ArrayList;
31 import java.util.List;
32 
33 /**
34  * Utility methods to work with the f2fs file system.
35  */
36 public final class F2fsUtils {
37     private static final String TAG = "F2fsUtils";
38     private static final boolean DEBUG_F2FS = false;
39 
40     /** Directory containing kernel features */
41     private static final File sKernelFeatures =
42             new File("/sys/fs/f2fs/features");
43     /** File containing features enabled on "/data" */
44     private static final File sUserDataFeatures =
45             new File("/dev/sys/fs/by-name/userdata/features");
46     private static final File sDataDirectory = Environment.getDataDirectory();
47     /** Name of the compression feature */
48     private static final String COMPRESSION_FEATURE = "compression";
49 
50     private static final boolean sKernelCompressionAvailable;
51     private static final boolean sUserDataCompressionAvailable;
52 
53     static {
54         sKernelCompressionAvailable = isCompressionEnabledInKernel();
55         if (!sKernelCompressionAvailable) {
56             if (DEBUG_F2FS) {
Slog.d(TAG, "f2fs compression DISABLED; feature not part of the kernel")57                 Slog.d(TAG, "f2fs compression DISABLED; feature not part of the kernel");
58             }
59         }
60         sUserDataCompressionAvailable = isCompressionEnabledOnUserData();
61         if (!sUserDataCompressionAvailable) {
62             if (DEBUG_F2FS) {
Slog.d(TAG, "f2fs compression DISABLED; feature not enabled on filesystem")63                 Slog.d(TAG, "f2fs compression DISABLED; feature not enabled on filesystem");
64             }
65         }
66     }
67 
68     /**
69      * Releases compressed blocks from eligible installation artifacts.
70      * <p>
71      * Modern f2fs implementations starting in {@code S} support compression
72      * natively within the file system. The data blocks of specific installation
73      * artifacts [eg. .apk, .so, ...] can be compressed at the file system level,
74      * making them look and act like any other uncompressed file, but consuming
75      * a fraction of the space.
76      * <p>
77      * However, the unused space is not free'd automatically. Instead, we must
78      * manually tell the file system to release the extra blocks [the delta between
79      * the compressed and uncompressed block counts] back to the free pool.
80      * <p>
81      * Because of how compression works within the file system, once the blocks
82      * have been released, the file becomes read-only and cannot be modified until
83      * the free'd blocks have again been reserved from the free pool.
84      */
releaseCompressedBlocks(ContentResolver resolver, File file)85     public static void releaseCompressedBlocks(ContentResolver resolver, File file) {
86         if (!sKernelCompressionAvailable || !sUserDataCompressionAvailable) {
87             return;
88         }
89 
90         // NOTE: Retrieving this setting means we need to delay releasing cblocks
91         // of any APKs installed during the PackageManagerService constructor. Instead
92         // of being able to release them in the constructor, they can only be released
93         // immediately prior to the system being available. When we no longer need to
94         // read this setting, move cblock release back to the package manager constructor.
95         final boolean releaseCompressBlocks =
96                 Secure.getInt(resolver, Secure.RELEASE_COMPRESS_BLOCKS_ON_INSTALL, 1) != 0;
97         if (!releaseCompressBlocks) {
98             if (DEBUG_F2FS) {
99                 Slog.d(TAG, "SKIP; release compress blocks not enabled");
100             }
101             return;
102         }
103         if (!isCompressionAllowed(file)) {
104             if (DEBUG_F2FS) {
105                 Slog.d(TAG, "SKIP; compression not allowed");
106             }
107             return;
108         }
109         final File[] files = getFilesToRelease(file);
110         if (files == null || files.length == 0) {
111             if (DEBUG_F2FS) {
112                 Slog.d(TAG, "SKIP; no files to compress");
113             }
114             return;
115         }
116         for (int i = files.length - 1; i >= 0; --i) {
117             final long releasedBlocks = nativeReleaseCompressedBlocks(files[i].getAbsolutePath());
118             if (DEBUG_F2FS) {
119                 Slog.d(TAG, "RELEASED " + releasedBlocks + " blocks"
120                         + " from \"" + files[i] + "\"");
121             }
122         }
123     }
124 
125     /**
126      * Returns {@code true} if compression is allowed on the file system containing
127      * the given file.
128      * <p>
129      * NOTE: The return value does not mean if the given file, or any other file
130      * on the same file system, is actually compressed. It merely determines whether
131      * not files <em>may</em> be compressed.
132      */
isCompressionAllowed(@onNull File file)133     private static boolean isCompressionAllowed(@NonNull File file) {
134         final String filePath;
135         try {
136             filePath = file.getCanonicalPath();
137         } catch (IOException e) {
138             if (DEBUG_F2FS) {
139                 Slog.d(TAG, "f2fs compression DISABLED; could not determine path");
140             }
141             return false;
142         }
143         if (IncrementalManager.isIncrementalPath(filePath)) {
144             if (DEBUG_F2FS) {
145                 Slog.d(TAG, "f2fs compression DISABLED; file on incremental fs");
146             }
147             return false;
148         }
149         if (!isChild(sDataDirectory, filePath)) {
150             if (DEBUG_F2FS) {
151                 Slog.d(TAG, "f2fs compression DISABLED; file not on /data");
152             }
153             return false;
154         }
155         if (DEBUG_F2FS) {
156             Slog.d(TAG, "f2fs compression ENABLED");
157         }
158         return true;
159     }
160 
161     /**
162      * Returns {@code true} if the given child is a descendant of the base.
163      */
isChild(@onNull File base, @NonNull String childPath)164     private static boolean isChild(@NonNull File base, @NonNull String childPath) {
165         try {
166             base = base.getCanonicalFile();
167 
168             File parentFile = new File(childPath).getCanonicalFile();
169             while (parentFile != null) {
170                 if (base.equals(parentFile)) {
171                     return true;
172                 }
173                 parentFile = parentFile.getParentFile();
174             }
175             return false;
176         } catch (IOException ignore) {
177             return false;
178         }
179     }
180 
181     /**
182      * Returns whether or not the compression feature is enabled in the kernel.
183      * <p>
184      * NOTE: This doesn't mean compression is enabled on a particular file system
185      * or any files have been compressed. Only that the functionality is enabled
186      * on the device.
187      */
isCompressionEnabledInKernel()188     private static boolean isCompressionEnabledInKernel() {
189         final File[] features = sKernelFeatures.listFiles();
190         if (features == null || features.length == 0) {
191             if (DEBUG_F2FS) {
192                 Slog.d(TAG, "ERROR; no kernel features");
193             }
194             return false;
195         }
196         for (int i = features.length - 1; i >= 0; --i) {
197             final File feature = features[i];
198             if (COMPRESSION_FEATURE.equals(features[i].getName())) {
199                 if (DEBUG_F2FS) {
200                     Slog.d(TAG, "FOUND kernel compression feature");
201                 }
202                 return true;
203             }
204         }
205         if (DEBUG_F2FS) {
206             Slog.d(TAG, "ERROR; kernel compression feature not found");
207         }
208         return false;
209     }
210 
211     /**
212      * Returns whether or not the compression feature is enabled on user data [ie. "/data"].
213      * <p>
214      * NOTE: This doesn't mean any files have been compressed. Only that the functionality
215      * is enabled on the file system.
216      */
isCompressionEnabledOnUserData()217     private static boolean isCompressionEnabledOnUserData() {
218         if (!sUserDataFeatures.exists()
219                 || !sUserDataFeatures.isFile()
220                 || !sUserDataFeatures.canRead()) {
221             if (DEBUG_F2FS) {
222                 Slog.d(TAG, "ERROR; filesystem features not available");
223             }
224             return false;
225         }
226         final List<String> configLines;
227         try {
228             configLines = Files.readAllLines(sUserDataFeatures.toPath());
229         } catch (IOException ignore) {
230             if (DEBUG_F2FS) {
231                 Slog.d(TAG, "ERROR; couldn't read filesystem features");
232             }
233             return false;
234         }
235         if (configLines == null
236                 || configLines.size() > 1
237                 || TextUtils.isEmpty(configLines.get(0))) {
238             if (DEBUG_F2FS) {
239                 Slog.d(TAG, "ERROR; no filesystem features");
240             }
241             return false;
242         }
243         final String[] features = configLines.get(0).split(",");
244         for (int i = features.length - 1; i >= 0; --i) {
245             if (COMPRESSION_FEATURE.equals(features[i].trim())) {
246                 if (DEBUG_F2FS) {
247                     Slog.d(TAG, "FOUND filesystem compression feature");
248                 }
249                 return true;
250             }
251         }
252         if (DEBUG_F2FS) {
253             Slog.d(TAG, "ERROR; filesystem compression feature not found");
254         }
255         return false;
256     }
257 
258     /**
259      * Returns all files contained within the directory at any depth from the given path.
260      */
getFilesRecursive(@onNull File path)261     private static List<File> getFilesRecursive(@NonNull File path) {
262         final File[] allFiles = path.listFiles();
263         if (allFiles == null) {
264             return null;
265         }
266         final ArrayList<File> files = new ArrayList<>();
267         for (File f : allFiles) {
268             if (f.isDirectory()) {
269                 files.addAll(getFilesRecursive(f));
270             } else if (f.isFile()) {
271                 files.add(f);
272             }
273         }
274         return files;
275     }
276 
277     /**
278      * Returns all files contained within the directory at any depth from the given path.
279      */
getFilesToRelease(@onNull File codePath)280     private static File[] getFilesToRelease(@NonNull File codePath) {
281         final List<File> files = getFilesRecursive(codePath);
282         if (files == null) {
283             if (codePath.isFile()) {
284                 return new File[] { codePath };
285             }
286             return null;
287         }
288         if (files.size() == 0) {
289             return null;
290         }
291         return files.toArray(new File[files.size()]);
292     }
293 
nativeReleaseCompressedBlocks(String path)294     private static native long nativeReleaseCompressedBlocks(String path);
295 
296 }
297