Merge "Use fs-verity for protecting updatable font files."

This commit is contained in:
Kohsuke Yatoh
2021-01-22 16:05:12 +00:00
committed by Android (Google) Code Review
6 changed files with 500 additions and 89 deletions

View File

@@ -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.

View File

@@ -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;
}
};
}
}

View File

@@ -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<File> 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<String, FontFileInfo> mFontFileInfoMap = new HashMap<>();
UpdatableFontDir(File filesDir, FontFileParser parser) {
UpdatableFontDir(File filesDir, List<File> 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.
*
* <p>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.
*
* <p>We check the font file names because apps use file name to look up fonts.
* <p>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<String, File> map = new HashMap<>();
synchronized (UpdatableFontDir.this) {
for (Map.Entry<String, FontFileInfo> entry : mFontFileInfoMap.entrySet()) {
map.put(entry.getKey(), entry.getValue().mFile);
map.put(entry.getKey(), entry.getValue().getFile());
}
}
return map;

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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<String> 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<File> 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<String, File> 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());
}
}
}