diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java index 54167b4b30420..6aa50dbd078eb 100644 --- a/graphics/java/android/graphics/fonts/SystemFonts.java +++ b/graphics/java/android/graphics/fonts/SystemFonts.java @@ -50,9 +50,11 @@ public final class SystemFonts { private static final String DEFAULT_FAMILY = "sans-serif"; private static final String FONTS_XML = "/system/etc/fonts.xml"; - private static final String SYSTEM_FONT_DIR = "/system/fonts/"; + /** @hide */ + public static final String SYSTEM_FONT_DIR = "/system/fonts/"; private static final String OEM_XML = "/product/etc/fonts_customization.xml"; - private static final String OEM_FONT_DIR = "/product/fonts/"; + /** @hide */ + public static final String OEM_FONT_DIR = "/product/fonts/"; private SystemFonts() {} // Do not instansiate. diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java index a0d9e8e0a6fb1..425e3ab0d4711 100644 --- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java +++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java @@ -31,6 +31,8 @@ import android.util.Slog; import com.android.internal.annotations.GuardedBy; import com.android.server.LocalServices; import com.android.server.SystemService; +import com.android.server.security.FileIntegrityService; +import com.android.server.security.VerityUtils; import java.io.File; import java.io.FileDescriptor; @@ -39,6 +41,7 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.NioUtils; import java.nio.channels.FileChannel; +import java.util.Arrays; import java.util.Map; /** A service for managing system fonts. */ @@ -78,7 +81,17 @@ public final class FontManagerService { private static class OtfFontFileParser implements UpdatableFontDir.FontFileParser { @Override - public long getVersion(File file) throws IOException { + public String getPostScriptName(File file) throws IOException { + ByteBuffer buffer = mmap(file); + try { + return FontFileUtil.getPostScriptName(buffer, 0); + } finally { + NioUtils.freeDirectBuffer(buffer); + } + } + + @Override + public long getRevision(File file) throws IOException { ByteBuffer buffer = mmap(file); try { return FontFileUtil.getRevision(buffer, 0); @@ -95,18 +108,48 @@ public final class FontManagerService { } } + private static class FsverityUtilImpl implements UpdatableFontDir.FsverityUtil { + @Override + public boolean hasFsverity(String filePath) { + return VerityUtils.hasFsverity(filePath); + } + + @Override + public void setUpFsverity(String filePath, byte[] pkcs7Signature) throws IOException { + VerityUtils.setUpFsverity(filePath, pkcs7Signature); + } + + @Override + public boolean rename(File src, File dest) { + // rename system call preserves fs-verity bit. + return src.renameTo(dest); + } + } + @Nullable private final UpdatableFontDir mUpdatableFontDir; @GuardedBy("FontManagerService.this") - @Nullable SystemFontSettings mCurrentFontSettings = null; + @Nullable + private SystemFontSettings mCurrentFontSettings = null; private FontManagerService() { - mUpdatableFontDir = ENABLE_FONT_UPDATES - ? new UpdatableFontDir(new File(FONT_FILES_DIR), new OtfFontFileParser()) : null; + mUpdatableFontDir = createUpdatableFontDir(); } - @NonNull private SystemFontSettings getCurrentFontSettings() { + @Nullable + private static UpdatableFontDir createUpdatableFontDir() { + if (!ENABLE_FONT_UPDATES) return null; + // If apk verity is supported, fs-verity should be available. + if (!FileIntegrityService.isApkVeritySupported()) return null; + return new UpdatableFontDir(new File(FONT_FILES_DIR), + Arrays.asList(new File(SystemFonts.SYSTEM_FONT_DIR), + new File(SystemFonts.OEM_FONT_DIR)), + new OtfFontFileParser(), new FsverityUtilImpl()); + } + + @NonNull + private SystemFontSettings getCurrentFontSettings() { synchronized (FontManagerService.this) { if (mCurrentFontSettings == null) { mCurrentFontSettings = SystemFontSettings.create(mUpdatableFontDir); @@ -115,13 +158,14 @@ public final class FontManagerService { } } - private boolean installFontFile(String name, FileDescriptor fd) { + // TODO(b/173619554): Expose as API. + private boolean installFontFile(FileDescriptor fd, byte[] pkcs7Signature) { if (mUpdatableFontDir == null) return false; synchronized (FontManagerService.this) { try { - mUpdatableFontDir.installFontFile(name, fd); + mUpdatableFontDir.installFontFile(fd, pkcs7Signature); } catch (IOException e) { - Slog.w(TAG, "Failed to install font file: " + name, e); + Slog.w(TAG, "Failed to install font file"); return false; } // Create updated font map in the next getSerializedSystemFontMap() call. @@ -194,5 +238,5 @@ public final class FontManagerService { } return null; } - }; + } } diff --git a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java index 7306471e68c2b..8da579fd4fa37 100644 --- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java +++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java @@ -16,6 +16,7 @@ package com.android.server.graphics.fonts; +import android.annotation.Nullable; import android.os.FileUtils; import android.util.Base64; import android.util.Slog; @@ -28,71 +29,158 @@ import java.io.FileOutputStream; import java.io.IOException; import java.security.SecureRandom; import java.util.HashMap; +import java.util.List; import java.util.Map; final class UpdatableFontDir { private static final String TAG = "UpdatableFontDir"; private static final String RANDOM_DIR_PREFIX = "~~"; + // TODO: Support .otf + private static final String ALLOWED_EXTENSION = ".ttf"; /** Interface to mock font file access in tests. */ interface FontFileParser { - long getVersion(File file) throws IOException; + String getPostScriptName(File file) throws IOException; + + long getRevision(File file) throws IOException; } - /** Data class to hold font file path and version. */ - static final class FontFileInfo { - final File mFile; - final long mVersion; + /** Interface to mock fs-verity in tests. */ + interface FsverityUtil { + boolean hasFsverity(String path); - FontFileInfo(File file, long version) { + void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException; + + boolean rename(File src, File dest); + } + + /** Data class to hold font file path and revision. */ + private static final class FontFileInfo { + private final File mFile; + private final long mRevision; + + FontFileInfo(File file, long revision) { mFile = file; - mVersion = version; + mRevision = revision; + } + + public File getFile() { + return mFile; + } + + /** Returns the unique randomized font dir containing this font file. */ + public File getRandomizedFontDir() { + return mFile.getParentFile(); + } + + public long getRevision() { + return mRevision; } } /** - * Root directory for storing updated font files. Each font file is stored in a unique random - * dir. The font file path would be {@code mFilesDir/~~{randomStr}/{fontFileName}}. + * Root directory for storing updated font files. Each font file is stored in a unique + * randomized dir. The font file path would be {@code mFilesDir/~~{randomStr}/{fontFileName}}. */ private final File mFilesDir; + private final List mPreinstalledFontDirs; private final FontFileParser mParser; + private final FsverityUtil mFsverityUtil; + /** + * A mutable map containing mapping from font file name (e.g. "NotoColorEmoji.ttf") to {@link + * FontFileInfo}. All files in this map are validated, and have higher revision numbers than + * corresponding font files in {@link #mPreinstalledFontDirs}. + */ @GuardedBy("UpdatableFontDir.this") private final Map mFontFileInfoMap = new HashMap<>(); - UpdatableFontDir(File filesDir, FontFileParser parser) { + UpdatableFontDir(File filesDir, List preinstalledFontDirs, FontFileParser parser, + FsverityUtil fsverityUtil) { mFilesDir = filesDir; + mPreinstalledFontDirs = preinstalledFontDirs; mParser = parser; + mFsverityUtil = fsverityUtil; loadFontFileMap(); } private void loadFontFileMap() { + // TODO: SIGBUS crash protection synchronized (UpdatableFontDir.this) { + boolean success = false; mFontFileInfoMap.clear(); - File[] dirs = mFilesDir.listFiles(); - if (dirs == null) return; - for (File dir : dirs) { - if (!dir.getName().startsWith(RANDOM_DIR_PREFIX)) continue; - File[] files = dir.listFiles(); - if (files == null || files.length != 1) continue; - addFileToMapLocked(files[0], true); + try { + File[] dirs = mFilesDir.listFiles(); + if (dirs == null) return; + for (File dir : dirs) { + if (!dir.getName().startsWith(RANDOM_DIR_PREFIX)) return; + File[] files = dir.listFiles(); + if (files == null || files.length != 1) return; + FontFileInfo fontFileInfo = validateFontFile(files[0]); + if (fontFileInfo == null) { + Slog.w(TAG, "Broken file is found. Clearing files."); + return; + } + addFileToMapLocked(fontFileInfo, true /* deleteOldFile */); + } + success = true; + } finally { + // Delete all files just in case if we find a problematic file. + if (!success) { + mFontFileInfoMap.clear(); + FileUtils.deleteContents(mFilesDir); + } } } } - void installFontFile(String name, FileDescriptor fd) throws IOException { - // TODO: Validate name. + /** + * Installs a new font file, or updates an existing font file. + * + *

The new font will be immediately available for new Zygote-forked processes through + * {@link #getFontFileMap()}. Old font files will be kept until next system server reboot, + * because existing Zygote-forked processes have paths to old font files. + * + * @param fd A file descriptor to the font file. + * @param pkcs7Signature A PKCS#7 detached signature to enable fs-verity for the font file. + */ + void installFontFile(FileDescriptor fd, byte[] pkcs7Signature) throws IOException { synchronized (UpdatableFontDir.this) { - // TODO: proper error handling File newDir = getRandomDir(mFilesDir); if (!newDir.mkdir()) { + // TODO: Define and return an error code for API throw new IOException("Failed to create a new dir"); } - File newFontFile = new File(newDir, name); - try (FileOutputStream out = new FileOutputStream(newFontFile)) { - FileUtils.copy(fd, out.getFD()); + boolean success = false; + try { + File tempNewFontFile = new File(newDir, "font.ttf"); + try (FileOutputStream out = new FileOutputStream(tempNewFontFile)) { + FileUtils.copy(fd, out.getFD()); + } + // Do not parse font file before setting up fs-verity. + // setUpFsverity throws IOException if failed. + mFsverityUtil.setUpFsverity(tempNewFontFile.getAbsolutePath(), pkcs7Signature); + String postScriptName = mParser.getPostScriptName(tempNewFontFile); + File newFontFile = new File(newDir, postScriptName + ALLOWED_EXTENSION); + if (!mFsverityUtil.rename(tempNewFontFile, newFontFile)) { + // TODO: Define and return an error code for API + throw new IOException("Failed to rename"); + } + FontFileInfo fontFileInfo = validateFontFile(newFontFile); + if (fontFileInfo == null) { + // TODO: Define and return an error code for API + throw new IllegalArgumentException("Invalid file"); + } + if (!addFileToMapLocked(fontFileInfo, false)) { + // TODO: Define and return an error code for API + throw new IllegalArgumentException("Version downgrade"); + } + success = true; + } finally { + if (!success) { + FileUtils.deleteContentsAndDir(newDir); + } } - addFileToMapLocked(newFontFile, false); } } @@ -114,27 +202,110 @@ final class UpdatableFontDir { return dir; } - private void addFileToMapLocked(File file, boolean deleteOldFile) { - final long version; + /** + * Add the given {@link FontFileInfo} to {@link #mFontFileInfoMap} if its font revision is + * higher than the currently used font file (either in {@link #mFontFileInfoMap} or {@link + * #mPreinstalledFontDirs}). + */ + private boolean addFileToMapLocked(FontFileInfo fontFileInfo, boolean deleteOldFile) { + String name = fontFileInfo.getFile().getName(); + FontFileInfo existingInfo = mFontFileInfoMap.get(name); + final boolean shouldAddToMap; + if (existingInfo == null) { + // We got a new updatable font. We need to check if it's newer than preinstalled fonts. + // Note that getPreinstalledFontRevision() returns -1 if there is no preinstalled font + // with 'name'. + shouldAddToMap = getPreinstalledFontRevision(name) < fontFileInfo.getRevision(); + } else { + shouldAddToMap = existingInfo.getRevision() < fontFileInfo.getRevision(); + } + if (shouldAddToMap) { + if (deleteOldFile && existingInfo != null) { + FileUtils.deleteContentsAndDir(existingInfo.getRandomizedFontDir()); + } + mFontFileInfoMap.put(name, fontFileInfo); + return true; + } else { + if (deleteOldFile) { + FileUtils.deleteContentsAndDir(fontFileInfo.getRandomizedFontDir()); + } + return false; + } + } + + private long getPreinstalledFontRevision(String name) { + long maxRevision = -1; + for (File dir : mPreinstalledFontDirs) { + File preinstalledFontFile = new File(dir, name); + if (!preinstalledFontFile.exists()) continue; + long revision = getFontRevision(preinstalledFontFile); + if (revision == -1) { + Slog.w(TAG, "Invalid preinstalled font file"); + continue; + } + if (revision > maxRevision) { + maxRevision = revision; + } + } + return maxRevision; + } + + /** + * Checks the fs-verity protection status of the given font file, validates the file name, and + * returns a {@link FontFileInfo} on success. This method does not check if the font revision + * is higher than the currently used font. + */ + @Nullable + private FontFileInfo validateFontFile(File file) { + if (!mFsverityUtil.hasFsverity(file.getAbsolutePath())) { + Slog.w(TAG, "Font validation failed. Fs-verity is not enabled: " + file); + return null; + } + if (!validateFontFileName(file)) { + Slog.w(TAG, "Font validation failed. Could not validate font file name: " + file); + return null; + } + long revision = getFontRevision(file); + if (revision == -1) { + Slog.w(TAG, "Font validation failed. Could not read font revision: " + file); + return null; + } + return new FontFileInfo(file, revision); + } + + /** + * Returns true if the font file's file name matches with the PostScript name metadata in the + * font file. + * + *

We check the font file names because apps use file name to look up fonts. + *

Because PostScript name does not include extension, the extension is appended for + * comparison. For example, if the file name is "NotoColorEmoji.ttf", the PostScript name should + * be "NotoColorEmoji". + */ + private boolean validateFontFileName(File file) { + String fileName = file.getName(); + String postScriptName = getPostScriptName(file); + return (postScriptName + ALLOWED_EXTENSION).equals(fileName); + } + + /** Returns the PostScript name of the given font file, or null. */ + @Nullable + private String getPostScriptName(File file) { try { - version = mParser.getVersion(file); + return mParser.getPostScriptName(file); } catch (IOException e) { Slog.e(TAG, "Failed to read font file", e); - return; + return null; } - if (version == -1) { - Slog.e(TAG, "Invalid font file"); - return; - } - FontFileInfo info = mFontFileInfoMap.get(file.getName()); - if (info == null) { - // TODO: check version of font in /system/fonts and /product/fonts - mFontFileInfoMap.put(file.getName(), new FontFileInfo(file, version)); - } else if (info.mVersion < version) { - if (deleteOldFile) { - FileUtils.deleteContentsAndDir(info.mFile.getParentFile()); - } - mFontFileInfoMap.put(file.getName(), new FontFileInfo(file, version)); + } + + /** Returns the non-negative font revision of the given font file, or -1. */ + private long getFontRevision(File file) { + try { + return mParser.getRevision(file); + } catch (IOException e) { + Slog.e(TAG, "Failed to read font file", e); + return -1; } } @@ -142,7 +313,7 @@ final class UpdatableFontDir { Map map = new HashMap<>(); synchronized (UpdatableFontDir.this) { for (Map.Entry entry : mFontFileInfoMap.entrySet()) { - map.put(entry.getKey(), entry.getValue().mFile); + map.put(entry.getKey(), entry.getValue().getFile()); } } return map; diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java index 225bd82e9a21e..6ec71b717ec64 100644 --- a/services/core/java/com/android/server/security/FileIntegrityService.java +++ b/services/core/java/com/android/server/security/FileIntegrityService.java @@ -60,8 +60,7 @@ public class FileIntegrityService extends SystemService { private final IBinder mService = new IFileIntegrityService.Stub() { @Override public boolean isApkVeritySupported() { - return Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.R - || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2; + return FileIntegrityService.isApkVeritySupported(); } @Override @@ -111,6 +110,11 @@ public class FileIntegrityService extends SystemService { } }; + public static boolean isApkVeritySupported() { + return Build.VERSION.FIRST_SDK_INT >= Build.VERSION_CODES.R + || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2; + } + public FileIntegrityService(final Context context) { super(context); try { diff --git a/services/core/java/com/android/server/security/VerityUtils.java b/services/core/java/com/android/server/security/VerityUtils.java index f204aa2cc1ca0..09ee001a5544d 100644 --- a/services/core/java/com/android/server/security/VerityUtils.java +++ b/services/core/java/com/android/server/security/VerityUtils.java @@ -73,7 +73,12 @@ abstract public class VerityUtils { if (Files.size(Paths.get(signaturePath)) > MAX_SIGNATURE_FILE_SIZE_BYTES) { throw new SecurityException("Signature file is unexpectedly large: " + signaturePath); } - byte[] pkcs7Signature = Files.readAllBytes(Paths.get(signaturePath)); + setUpFsverity(filePath, Files.readAllBytes(Paths.get(signaturePath))); + } + + /** Enables fs-verity for the file with a PKCS#7 detached signature bytes. */ + public static void setUpFsverity(@NonNull String filePath, @NonNull byte[] pkcs7Signature) + throws IOException { // This will fail if the public key is not already in .fs-verity kernel keyring. int errno = enableFsverityNative(filePath, pkcs7Signature); if (errno != 0) { diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java index 0cf0af39dc1e5..75bf1e6bd485b 100644 --- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java +++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java @@ -19,6 +19,8 @@ package com.android.server.graphics.fonts; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.fail; + import android.content.Context; import android.os.FileUtils; import android.platform.test.annotations.Presubmit; @@ -35,7 +37,12 @@ import org.junit.runner.RunWith; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; @Presubmit @SmallTest @@ -44,17 +51,63 @@ public final class UpdatableFontDirTest { /** * A {@link UpdatableFontDir.FontFileParser} for testing. Instead of using real font files, - * this test uses fake font files. A fake font file has its version as its file content. + * this test uses fake font files. A fake font file has its PostScript naem and revision as the + * file content. */ private static class FakeFontFileParser implements UpdatableFontDir.FontFileParser { @Override - public long getVersion(File file) throws IOException { - return Long.parseLong(FileUtils.readTextFile(file, 100, "")); + public String getPostScriptName(File file) throws IOException { + String content = FileUtils.readTextFile(file, 100, ""); + return content.split(",")[0]; + } + + @Override + public long getRevision(File file) throws IOException { + String content = FileUtils.readTextFile(file, 100, ""); + return Long.parseLong(content.split(",")[1]); + } + } + + // FakeFsverityUtil will successfully set up fake fs-verity if the signature is GOOD_SIGNATURE. + private static final String GOOD_SIGNATURE = "Good signature"; + + /** A fake FsverityUtil to keep fake verity bit in memory. */ + private static class FakeFsverityUtil implements UpdatableFontDir.FsverityUtil { + private final Set mHasFsverityPaths = new HashSet<>(); + + public void remove(String name) { + mHasFsverityPaths.remove(name); + } + + @Override + public boolean hasFsverity(String path) { + return mHasFsverityPaths.contains(path); + } + + @Override + public void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException { + String fakeSignature = new String(pkcs7Signature, StandardCharsets.UTF_8); + if (GOOD_SIGNATURE.equals(fakeSignature)) { + mHasFsverityPaths.add(path); + } else { + throw new IOException("Failed to set up fake fs-verity"); + } + } + + @Override + public boolean rename(File src, File dest) { + if (src.renameTo(dest)) { + mHasFsverityPaths.remove(src.getAbsolutePath()); + mHasFsverityPaths.add(dest.getAbsolutePath()); + return true; + } + return false; } } private File mCacheDir; private File mUpdatableFontFilesDir; + private List mPreinstalledFontDirs; @SuppressWarnings("ResultOfMethodCallIgnored") @Before @@ -65,6 +118,12 @@ public final class UpdatableFontDirTest { mCacheDir.mkdirs(); mUpdatableFontFilesDir = new File(mCacheDir, "updatable_fonts"); mUpdatableFontFilesDir.mkdir(); + mPreinstalledFontDirs = new ArrayList<>(); + mPreinstalledFontDirs.add(new File(mCacheDir, "system_fonts")); + mPreinstalledFontDirs.add(new File(mCacheDir, "product_fonts")); + for (File dir : mPreinstalledFontDirs) { + dir.mkdir(); + } } @After @@ -75,19 +134,22 @@ public final class UpdatableFontDirTest { @Test public void construct() throws Exception { FakeFontFileParser parser = new FakeFontFileParser(); - UpdatableFontDir dirForPreparation = new UpdatableFontDir(mUpdatableFontFilesDir, parser); - installFontFile(dirForPreparation, "foo.ttf", "1"); - installFontFile(dirForPreparation, "bar.ttf", "2"); - installFontFile(dirForPreparation, "foo.ttf", "3"); - installFontFile(dirForPreparation, "bar.ttf", "4"); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dirForPreparation = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "bar,4", GOOD_SIGNATURE); // Four font dirs are created. assertThat(mUpdatableFontFilesDir.list()).hasLength(4); - UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); assertThat(dir.getFontFileMap()).containsKey("foo.ttf"); - assertThat(parser.getVersion(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(3); + assertThat(parser.getRevision(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(3); assertThat(dir.getFontFileMap()).containsKey("bar.ttf"); - assertThat(parser.getVersion(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(4); + assertThat(parser.getRevision(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(4); // Outdated font dir should be deleted. assertThat(mUpdatableFontFilesDir.list()).hasLength(2); } @@ -95,66 +157,189 @@ public final class UpdatableFontDirTest { @Test public void construct_empty() { FakeFontFileParser parser = new FakeFontFileParser(); - UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); assertThat(dir.getFontFileMap()).isEmpty(); } + @Test + public void construct_missingFsverity() throws Exception { + FakeFontFileParser parser = new FakeFontFileParser(); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dirForPreparation = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "bar,4", GOOD_SIGNATURE); + // Four font dirs are created. + assertThat(mUpdatableFontFilesDir.list()).hasLength(4); + + fakeFsverityUtil.remove( + dirForPreparation.getFontFileMap().get("foo.ttf").getAbsolutePath()); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + assertThat(dir.getFontFileMap()).isEmpty(); + // All font dirs (including dir for "bar.ttf") should be deleted. + assertThat(mUpdatableFontFilesDir.list()).hasLength(0); + } + + @Test + public void construct_fontNameMismatch() throws Exception { + FakeFontFileParser parser = new FakeFontFileParser(); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dirForPreparation = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "bar,4", GOOD_SIGNATURE); + // Four font dirs are created. + assertThat(mUpdatableFontFilesDir.list()).hasLength(4); + + // Overwrite "foo.ttf" with wrong contents. + FileUtils.stringToFile(dirForPreparation.getFontFileMap().get("foo.ttf"), "bar,4"); + + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + assertThat(dir.getFontFileMap()).isEmpty(); + // All font dirs (including dir for "bar.ttf") should be deleted. + assertThat(mUpdatableFontFilesDir.list()).hasLength(0); + } + + @Test + public void construct_olderThanPreinstalledFont() throws Exception { + FakeFontFileParser parser = new FakeFontFileParser(); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dirForPreparation = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE); + installFontFile(dirForPreparation, "bar,4", GOOD_SIGNATURE); + // Four font dirs are created. + assertThat(mUpdatableFontFilesDir.list()).hasLength(4); + + // Add preinstalled fonts. + FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "foo.ttf"), "foo,5"); + FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "bar.ttf"), "bar,1"); + FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(1), "bar.ttf"), "bar,2"); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + // For foo.ttf, preinstalled font (revision 5) should be used. + assertThat(dir.getFontFileMap()).doesNotContainKey("foo.ttf"); + // For bar.ttf, updated font (revision 4) should be used. + assertThat(dir.getFontFileMap()).containsKey("bar.ttf"); + assertThat(parser.getRevision(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(4); + // Outdated font dir should be deleted. + // We don't delete bar.ttf in this case, because it's normal that OTA updates preinstalled + // fonts. + assertThat(mUpdatableFontFilesDir.list()).hasLength(1); + } + @Test public void installFontFile() throws Exception { FakeFontFileParser parser = new FakeFontFileParser(); - UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); - installFontFile(dir, "test.ttf", "1"); + installFontFile(dir, "test,1", GOOD_SIGNATURE); assertThat(dir.getFontFileMap()).containsKey("test.ttf"); - assertThat(parser.getVersion(dir.getFontFileMap().get("test.ttf"))).isEqualTo(1); + assertThat(parser.getRevision(dir.getFontFileMap().get("test.ttf"))).isEqualTo(1); } @Test public void installFontFile_upgrade() throws Exception { FakeFontFileParser parser = new FakeFontFileParser(); - UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); - installFontFile(dir, "test.ttf", "1"); + installFontFile(dir, "test,1", GOOD_SIGNATURE); Map mapBeforeUpgrade = dir.getFontFileMap(); - installFontFile(dir, "test.ttf", "2"); + installFontFile(dir, "test,2", GOOD_SIGNATURE); assertThat(dir.getFontFileMap()).containsKey("test.ttf"); - assertThat(parser.getVersion(dir.getFontFileMap().get("test.ttf"))).isEqualTo(2); + assertThat(parser.getRevision(dir.getFontFileMap().get("test.ttf"))).isEqualTo(2); assertThat(mapBeforeUpgrade).containsKey("test.ttf"); assertWithMessage("Older fonts should not be deleted until next loadFontFileMap") - .that(parser.getVersion(mapBeforeUpgrade.get("test.ttf"))).isEqualTo(1); + .that(parser.getRevision(mapBeforeUpgrade.get("test.ttf"))).isEqualTo(1); } @Test public void installFontFile_downgrade() throws Exception { FakeFontFileParser parser = new FakeFontFileParser(); - UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); - installFontFile(dir, "test.ttf", "2"); - installFontFile(dir, "test.ttf", "1"); + installFontFile(dir, "test,2", GOOD_SIGNATURE); + try { + installFontFile(dir, "test,1", GOOD_SIGNATURE); + fail("Expect IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Expect + } assertThat(dir.getFontFileMap()).containsKey("test.ttf"); - assertWithMessage("Font should not be downgraded to an older version") - .that(parser.getVersion(dir.getFontFileMap().get("test.ttf"))).isEqualTo(2); + assertWithMessage("Font should not be downgraded to an older revision") + .that(parser.getRevision(dir.getFontFileMap().get("test.ttf"))).isEqualTo(2); } @Test public void installFontFile_multiple() throws Exception { FakeFontFileParser parser = new FakeFontFileParser(); - UpdatableFontDir dir = new UpdatableFontDir(mUpdatableFontFilesDir, parser); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); - installFontFile(dir, "foo.ttf", "1"); - installFontFile(dir, "bar.ttf", "2"); + installFontFile(dir, "foo,1", GOOD_SIGNATURE); + installFontFile(dir, "bar,2", GOOD_SIGNATURE); assertThat(dir.getFontFileMap()).containsKey("foo.ttf"); - assertThat(parser.getVersion(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(1); + assertThat(parser.getRevision(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(1); assertThat(dir.getFontFileMap()).containsKey("bar.ttf"); - assertThat(parser.getVersion(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(2); + assertThat(parser.getRevision(dir.getFontFileMap().get("bar.ttf"))).isEqualTo(2); } - private void installFontFile(UpdatableFontDir dir, String name, String content) + @Test + public void installFontFile_invalidSignature() { + FakeFontFileParser parser = new FakeFontFileParser(); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + + try { + installFontFile(dir, "test,1", "Invalid signature"); + fail("Expect IOException"); + } catch (IOException e) { + // Expect + } + assertThat(dir.getFontFileMap()).isEmpty(); + } + + @Test + public void installFontFile_olderThanPreinstalledFont() throws Exception { + FakeFontFileParser parser = new FakeFontFileParser(); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"), "test,1"); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + + try { + installFontFile(dir, "test,1", GOOD_SIGNATURE); + fail("Expect IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Expect + } + assertThat(dir.getFontFileMap()).isEmpty(); + } + + private void installFontFile(UpdatableFontDir dir, String content, String signature) throws IOException { - File file = File.createTempFile(name, "", mCacheDir); + File file = File.createTempFile("font", "ttf", mCacheDir); FileUtils.stringToFile(file, content); try (FileInputStream in = new FileInputStream(file)) { - dir.installFontFile(name, in.getFD()); + dir.installFontFile(in.getFD(), signature.getBytes()); } } }