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