diff options
| author | 2021-01-28 01:49:59 +0000 | |
|---|---|---|
| committer | 2021-01-28 01:49:59 +0000 | |
| commit | 9b9486ebf298438c3144bbc9dd27102780f73b52 (patch) | |
| tree | 375bd4932948f2eae35ab33f010bcaef282cc315 | |
| parent | 299897e9baf78b9c0dc1d1007fea74efcbaa6137 (diff) | |
| parent | 9387e7f4cda0f24eb88231254c1929b400df8935 (diff) | |
Merge "Add more shell command for font" into sc-dev
12 files changed, 949 insertions, 128 deletions
diff --git a/core/java/android/graphics/fonts/FontManager.java b/core/java/android/graphics/fonts/FontManager.java index ea6cf2f44be9..eca56b375dd4 100644 --- a/core/java/android/graphics/fonts/FontManager.java +++ b/core/java/android/graphics/fonts/FontManager.java @@ -16,6 +16,7 @@ package android.graphics.fonts; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -28,6 +29,8 @@ import android.util.Log; import com.android.internal.graphics.fonts.IFontManager; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Objects; /** @@ -41,6 +44,116 @@ public class FontManager { private static final String TAG = "FontManager"; private final @NonNull IFontManager mIFontManager; + /** @hide */ + @IntDef(prefix = "ERROR_CODE_", + value = { ERROR_CODE_OK, ERROR_CODE_FAILED_TO_WRITE_FONT_FILE, + ERROR_CODE_VERIFICATION_FAILURE, ERROR_CODE_FONT_NAME_MISMATCH, + ERROR_CODE_INVALID_FONT_FILE, ERROR_CODE_MISSING_POST_SCRIPT_NAME, + ERROR_CODE_DOWNGRADING, ERROR_CODE_FAILED_TO_CREATE_CONFIG_FILE, + ERROR_CODE_FONT_UPDATER_DISABLED }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCode {} + + /** + * Indicates an operation has processed successfully. + * @hide + */ + public static final int ERROR_CODE_OK = 0; + + /** + * Indicates a failure of writing font files. + * @hide + */ + public static final int ERROR_CODE_FAILED_TO_WRITE_FONT_FILE = -1; + + /** + * Indicates a failure of fs-verity setup. + * @hide + */ + public static final int ERROR_CODE_VERIFICATION_FAILURE = -2; + + /** + * Indicates a failure of verifying the font name with PostScript name. + * @hide + */ + public static final int ERROR_CODE_FONT_NAME_MISMATCH = -3; + + /** + * Indicates a failure of placing fonts due to unexpected font contents. + * @hide + */ + public static final int ERROR_CODE_INVALID_FONT_FILE = -4; + + /** + * Indicates a failure due to missing PostScript name in name table. + * @hide + */ + public static final int ERROR_CODE_MISSING_POST_SCRIPT_NAME = -5; + + /** + * Indicates a failure of placing fonts due to downgrading. + * @hide + */ + public static final int ERROR_CODE_DOWNGRADING = -6; + + /** + * Indicates a failure of writing system font configuration XML file. + * @hide + */ + public static final int ERROR_CODE_FAILED_TO_CREATE_CONFIG_FILE = -7; + + /** + * Indicates a failure due to disabled font updater. + * @hide + */ + public static final int ERROR_CODE_FONT_UPDATER_DISABLED = -8; + + /** + * Indicates a failure of opening font file. + * + * This error code is only used with the shell command interaction. + * + * @hide + */ + public static final int ERROR_CODE_FAILED_TO_OPEN_FONT_FILE = -10001; + + /** + * Indicates a failure of opening signature file. + * + * This error code is only used with the shell command interaction. + * + * @hide + */ + public static final int ERROR_CODE_FAILED_TO_OPEN_SIGNATURE_FILE = -10002; + + /** + * Indicates a failure of invalid shell command arguments. + * + * This error code is only used with the shell command interaction. + * + * @hide + */ + public static final int ERROR_CODE_INVALID_SHELL_ARGUMENT = -10003; + + /** + * Indicates a failure of reading signature file. + * + * This error code is only used with the shell command interaction. + * + * @hide + */ + public static final int ERROR_CODE_INVALID_SIGNATURE_FILE = -10004; + + /** + * Indicates a failure due to exceeding allowed signature file size (8kb). + * + * This error code is only used with the shell command interaction. + * + * @hide + */ + public static final int ERROR_CODE_SIGNATURE_TOO_LARGE = -10005; + + private FontManager(@NonNull IFontManager iFontManager) { mIFontManager = iFontManager; } diff --git a/core/java/android/text/FontConfig.java b/core/java/android/text/FontConfig.java index 82d7399c86e0..53fe1ba9c4b4 100644 --- a/core/java/android/text/FontConfig.java +++ b/core/java/android/text/FontConfig.java @@ -53,6 +53,8 @@ import java.util.List; public final class FontConfig implements Parcelable { private final @NonNull List<FontFamily> mFamilies; private final @NonNull List<Alias> mAliases; + private final long mLastModifiedDate; + private final int mConfigVersion; /** * Construct a FontConfig instance. @@ -62,9 +64,12 @@ public final class FontConfig implements Parcelable { * * @hide Only system server can create this instance and passed via IPC. */ - public FontConfig(@NonNull List<FontFamily> families, @NonNull List<Alias> aliases) { + public FontConfig(@NonNull List<FontFamily> families, @NonNull List<Alias> aliases, + long lastModifiedDate, @IntRange(from = 0) int configVersion) { mFamilies = families; mAliases = aliases; + mLastModifiedDate = lastModifiedDate; + mConfigVersion = configVersion; } /** @@ -88,6 +93,26 @@ public final class FontConfig implements Parcelable { } /** + * Returns the last modified date as Java epoch seconds. + * + * If there is no update, this return 0. + * @hide + */ + public long getLastModifiedDate() { + return mLastModifiedDate; + } + + /** + * Returns the monotonically increasing config version value. + * + * The config version is reset to 0 when the system is restarted. + * @hide + */ + public @IntRange(from = 0) int getConfigVersion() { + return mConfigVersion; + } + + /** * Returns the ordered list of families included in the system fonts. * @deprecated Use getFontFamilies instead. * @hide @@ -107,6 +132,8 @@ public final class FontConfig implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeParcelableList(mFamilies, flags); dest.writeParcelableList(mAliases, flags); + dest.writeLong(mLastModifiedDate); + dest.writeInt(mConfigVersion); } public static final @NonNull Creator<FontConfig> CREATOR = new Creator<FontConfig>() { @@ -116,7 +143,9 @@ public final class FontConfig implements Parcelable { FontFamily.class.getClassLoader()); List<Alias> aliases = source.readParcelableList(new ArrayList<>(), Alias.class.getClassLoader()); - return new FontConfig(families, aliases); + long lastModifiedDate = source.readLong(); + int configVersion = source.readInt(); + return new FontConfig(families, aliases, lastModifiedDate, configVersion); } @Override diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java index 65ea2a8373aa..3df2e90241ff 100644 --- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java +++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java @@ -171,7 +171,8 @@ public class TypefaceSystemFallbackTest { FontConfig fontConfig; try { fontConfig = FontListParser.parse( - TEST_FONTS_XML, TEST_FONT_DIR, oemXmlPath, TEST_OEM_DIR, updatableFontMap); + TEST_FONTS_XML, TEST_FONT_DIR, oemXmlPath, TEST_OEM_DIR, updatableFontMap, 0, + 0); } catch (IOException | XmlPullParserException e) { throw new RuntimeException(e); } @@ -199,7 +200,7 @@ public class TypefaceSystemFallbackTest { FontConfig fontConfig; try { fontConfig = FontListParser.parse( - SYSTEM_FONTS_XML, SYSTEM_FONT_DIR, null, TEST_OEM_DIR, null); + SYSTEM_FONTS_XML, SYSTEM_FONT_DIR, null, TEST_OEM_DIR, null, 0, 0); } catch (IOException | XmlPullParserException e) { throw new RuntimeException(e); } diff --git a/core/tests/coretests/src/android/text/FontFallbackSetup.java b/core/tests/coretests/src/android/text/FontFallbackSetup.java index e301037d01f1..90a6ca3cd1d1 100644 --- a/core/tests/coretests/src/android/text/FontFallbackSetup.java +++ b/core/tests/coretests/src/android/text/FontFallbackSetup.java @@ -79,7 +79,7 @@ public class FontFallbackSetup implements AutoCloseable { FontConfig fontConfig; try { - fontConfig = FontListParser.parse(testFontsXml, mTestFontsDir, null, null, null); + fontConfig = FontListParser.parse(testFontsXml, mTestFontsDir, null, null, null, 0, 0); } catch (IOException | XmlPullParserException e) { throw new RuntimeException(e); } diff --git a/graphics/java/android/graphics/FontListParser.java b/graphics/java/android/graphics/FontListParser.java index c8ff95d15626..bb795cd99e6e 100644 --- a/graphics/java/android/graphics/FontListParser.java +++ b/graphics/java/android/graphics/FontListParser.java @@ -51,7 +51,8 @@ public class FontListParser { XmlPullParser parser = Xml.newPullParser(); parser.setInput(in, null); parser.nextTag(); - return readFamilies(parser, "/system/fonts/", new FontCustomizationParser.Result(), null); + return readFamilies(parser, "/system/fonts/", new FontCustomizationParser.Result(), null, + 0, 0); } /** @@ -71,7 +72,9 @@ public class FontListParser { @NonNull String systemFontDir, @Nullable String oemCustomizationXmlPath, @Nullable String productFontDir, - @Nullable Map<String, File> updatableFontMap + @Nullable Map<String, File> updatableFontMap, + long lastModifiedDate, + int configVersion ) throws IOException, XmlPullParserException { FontCustomizationParser.Result oemCustomization; if (oemCustomizationXmlPath != null) { @@ -90,7 +93,8 @@ public class FontListParser { XmlPullParser parser = Xml.newPullParser(); parser.setInput(is, null); parser.nextTag(); - return readFamilies(parser, systemFontDir, oemCustomization, updatableFontMap); + return readFamilies(parser, systemFontDir, oemCustomization, updatableFontMap, + lastModifiedDate, configVersion); } } @@ -98,7 +102,9 @@ public class FontListParser { @NonNull XmlPullParser parser, @NonNull String fontDir, @NonNull FontCustomizationParser.Result customization, - @Nullable Map<String, File> updatableFontMap) + @Nullable Map<String, File> updatableFontMap, + long lastModifiedDate, + int configVersion) throws XmlPullParserException, IOException { List<FontConfig.FontFamily> families = new ArrayList<>(); List<FontConfig.Alias> aliases = new ArrayList<>(customization.getAdditionalAliases()); @@ -126,7 +132,7 @@ public class FontListParser { } families.addAll(oemNamedFamilies.values()); - return new FontConfig(families, aliases); + return new FontConfig(families, aliases, lastModifiedDate, configVersion); } /** diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java index a41215f9cae1..c166e12fc6bf 100644 --- a/graphics/java/android/graphics/fonts/SystemFonts.java +++ b/graphics/java/android/graphics/fonts/SystemFonts.java @@ -240,10 +240,12 @@ public final class SystemFonts { * @hide */ public static @NonNull FontConfig getSystemFontConfig( - @Nullable Map<String, File> updatableFontMap + @Nullable Map<String, File> updatableFontMap, + long lastModifiedDate, + int configVersion ) { return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, - updatableFontMap); + updatableFontMap, lastModifiedDate, configVersion); } /** @@ -251,7 +253,8 @@ public final class SystemFonts { * @hide */ public static @NonNull FontConfig getSystemPreinstalledFontConfig() { - return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null); + return getSystemFontConfigInternal(FONTS_XML, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR, null, + 0, 0); } /* package */ static @NonNull FontConfig getSystemFontConfigInternal( @@ -259,17 +262,19 @@ public final class SystemFonts { @NonNull String systemFontDir, @Nullable String oemXml, @Nullable String productFontDir, - @Nullable Map<String, File> updatableFontMap + @Nullable Map<String, File> updatableFontMap, + long lastModifiedDate, + int configVersion ) { try { return FontListParser.parse(fontsXml, systemFontDir, oemXml, productFontDir, - updatableFontMap); + updatableFontMap, lastModifiedDate, configVersion); } catch (IOException e) { Log.e(TAG, "Failed to open/read system font configurations.", e); - return new FontConfig(Collections.emptyList(), Collections.emptyList()); + return new FontConfig(Collections.emptyList(), Collections.emptyList(), 0, 0); } catch (XmlPullParserException e) { Log.e(TAG, "Failed to parse the system font configuration.", e); - return new FontConfig(Collections.emptyList(), Collections.emptyList()); + return new FontConfig(Collections.emptyList(), Collections.emptyList(), 0, 0); } } 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 bda4240a5858..5b3db011b427 100644 --- a/services/core/java/com/android/server/graphics/fonts/FontManagerService.java +++ b/services/core/java/com/android/server/graphics/fonts/FontManagerService.java @@ -22,12 +22,15 @@ import android.content.Context; import android.graphics.Typeface; import android.graphics.fonts.FontFamily; import android.graphics.fonts.FontFileUtil; +import android.graphics.fonts.FontManager; import android.graphics.fonts.SystemFonts; -import android.os.ParcelFileDescriptor; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.SharedMemory; +import android.os.ShellCallback; import android.system.ErrnoException; import android.text.FontConfig; +import android.util.AndroidException; import android.util.IndentingPrintWriter; import android.util.Slog; @@ -48,6 +51,7 @@ import java.nio.ByteBuffer; import java.nio.NioUtils; import java.nio.channels.FileChannel; import java.util.Arrays; +import java.util.Collections; import java.util.Map; /** A service for managing system fonts. */ @@ -55,8 +59,6 @@ import java.util.Map; public final class FontManagerService extends IFontManager.Stub { private static final String TAG = "FontManagerService"; - // TODO: make this a DeviceConfig flag. - private static final boolean ENABLE_FONT_UPDATES = false; private static final String FONT_FILES_DIR = "/data/fonts/files"; @Override @@ -64,6 +66,24 @@ public final class FontManagerService extends IFontManager.Stub { return getCurrentFontSettings().getSystemFontConfig(); } + /* package */ static class SystemFontException extends AndroidException { + private final int mErrorCode; + + SystemFontException(@FontManager.ErrorCode int errorCode, String msg, Throwable cause) { + super(msg, cause); + mErrorCode = errorCode; + } + + SystemFontException(int errorCode, String msg) { + super(msg); + mErrorCode = errorCode; + } + + @FontManager.ErrorCode int getErrorCode() { + return mErrorCode; + } + } + /** Class to manage FontManagerService's lifecycle. */ public static final class Lifecycle extends SystemService { private final FontManagerService mService; @@ -151,7 +171,6 @@ public final class FontManagerService extends IFontManager.Stub { @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), @@ -178,19 +197,34 @@ public final class FontManagerService extends IFontManager.Stub { } } - // TODO(b/173619554): Expose as API. - private boolean installFontFile(FileDescriptor fd, byte[] pkcs7Signature) { - if (mUpdatableFontDir == null) return false; + /* package */ void installFontFile(FileDescriptor fd, byte[] pkcs7Signature) + throws SystemFontException { + if (mUpdatableFontDir == null) { + throw new SystemFontException( + FontManager.ERROR_CODE_FONT_UPDATER_DISABLED, + "The font updater is disabled."); + } synchronized (FontManagerService.this) { - try { - mUpdatableFontDir.installFontFile(fd, pkcs7Signature); - } catch (IOException e) { - Slog.w(TAG, "Failed to install font file"); - return false; - } + mUpdatableFontDir.installFontFile(fd, pkcs7Signature); // Create updated font map in the next getSerializedSystemFontMap() call. mCurrentFontSettings = null; - return true; + } + } + + /* package */ void clearUpdates() throws SystemFontException { + if (mUpdatableFontDir == null) { + throw new SystemFontException( + FontManager.ERROR_CODE_FONT_UPDATER_DISABLED, + "The font updater is disabled."); + } + mUpdatableFontDir.clearUpdates(); + } + + /* package */ Map<String, File> getFontFileMap() { + if (mUpdatableFontDir == null) { + return Collections.emptyMap(); + } else { + return mUpdatableFontDir.getFontFileMap(); } } @@ -202,11 +236,13 @@ public final class FontManagerService extends IFontManager.Stub { } @Override - public int handleShellCommand(@NonNull ParcelFileDescriptor in, - @NonNull ParcelFileDescriptor out, @NonNull ParcelFileDescriptor err, - @NonNull String[] args) { - return new FontManagerShellCommand(this).exec(this, - in.getFileDescriptor(), out.getFileDescriptor(), err.getFileDescriptor(), args); + public void onShellCommand(@Nullable FileDescriptor in, + @Nullable FileDescriptor out, + @Nullable FileDescriptor err, + @NonNull String[] args, + @Nullable ShellCallback callback, + @NonNull ResultReceiver result) throws RemoteException { + new FontManagerShellCommand(this).exec(this, in, out, err, args, callback, result); } /* package */ static class SystemFontSettings { @@ -245,8 +281,7 @@ public final class FontManagerService extends IFontManager.Stub { public static @Nullable SystemFontSettings create( @Nullable UpdatableFontDir updatableFontDir) { if (updatableFontDir != null) { - final FontConfig fontConfig = SystemFonts.getSystemFontConfig( - updatableFontDir.getFontFileMap()); + final FontConfig fontConfig = updatableFontDir.getSystemFontConfig(); final Map<String, FontFamily[]> fallback = SystemFonts.buildSystemFallback(fontConfig); final Map<String, Typeface> typefaceMap = @@ -274,4 +309,5 @@ public final class FontManagerService extends IFontManager.Stub { return null; } } + } diff --git a/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java b/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java index acb582637e55..fd5c020b1a15 100644 --- a/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java +++ b/services/core/java/com/android/server/graphics/fonts/FontManagerShellCommand.java @@ -16,19 +16,33 @@ package com.android.server.graphics.fonts; +import static com.android.server.graphics.fonts.FontManagerService.SystemFontException; + import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.graphics.fonts.Font; import android.graphics.fonts.FontFamily; +import android.graphics.fonts.FontManager; import android.graphics.fonts.FontVariationAxis; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.os.Process; import android.os.ShellCommand; import android.text.FontConfig; import android.util.IndentingPrintWriter; +import android.util.Slog; import com.android.internal.util.DumpUtils; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; import java.io.PrintWriter; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -38,6 +52,13 @@ import java.util.Map; public class FontManagerShellCommand extends ShellCommand { private static final String TAG = "FontManagerShellCommand"; + /** + * The maximum size of signature file. This is just to avoid potential abuse. + * + * This is copied from VerityUtils.java. + */ + private static final int MAX_SIGNATURE_FILE_SIZE_BYTES = 8192; + @NonNull private final FontManagerService mService; FontManagerShellCommand(@NonNull FontManagerService service) { @@ -46,12 +67,31 @@ public class FontManagerShellCommand extends ShellCommand { @Override public int onCommand(String cmd) { + final int callingUid = Binder.getCallingUid(); + if (callingUid != Process.ROOT_UID && callingUid != Process.SHELL_UID) { + // Do not change this string since this string is expected in the CTS. + getErrPrintWriter().println("Only shell or root user can execute font command."); + return 1; + } return execCommand(this, cmd); } @Override public void onHelp() { - dumpHelp(getOutPrintWriter()); + PrintWriter w = getOutPrintWriter(); + w.println("Font service (font) commands"); + w.println("help"); + w.println(" Print this help text."); + w.println(); + w.println("dump [family name]"); + w.println(" Dump all font files in the specified family name."); + w.println(" Dump current system font configuration if no family name was specified."); + w.println(); + w.println("update [font file path] [signature file path]"); + w.println(" Update installed font files with new font file."); + w.println(); + w.println("clear"); + w.println(" Remove all installed font files and reset to the initial state."); } /* package */ void dumpAll(@NonNull IndentingPrintWriter w) { @@ -165,16 +205,6 @@ public class FontManagerShellCommand extends ShellCommand { w.decreaseIndent(); } - private static void dumpHelp(@NonNull PrintWriter w) { - w.println("Font service (font) commands"); - w.println("help"); - w.println(" Print this help text."); - w.println(); - w.println("dump [family name]"); - w.println(" Dump all font files in the specified family name."); - w.println(" Dump current system font configuration if no family name was specified."); - } - private void dumpFallback(@NonNull IndentingPrintWriter writer, @NonNull FontFamily[] families) { for (FontFamily family : families) { @@ -233,37 +263,143 @@ public class FontManagerShellCommand extends ShellCommand { writer.println(sb.toString()); } - private int execCommand(@NonNull ShellCommand shell, @NonNull String cmd) { + private void writeCommandResult(ShellCommand shell, SystemFontException e) { + // Print short summary to the stderr. + PrintWriter pw = shell.getErrPrintWriter(); + pw.println(e.getErrorCode()); + pw.println(e.getMessage()); + + // Dump full stack trace to logcat. + + Slog.e(TAG, "Command failed: " + Arrays.toString(shell.getAllArgs()), e); + } + + private int dump(ShellCommand shell) { final Context ctx = mService.getContext(); - if (cmd == null) { - return shell.handleDefaultCommands(null); + final FontManagerService.SystemFontSettings settings = + mService.getCurrentFontSettings(); + if (!DumpUtils.checkDumpPermission(ctx, TAG, shell.getErrPrintWriter())) { + return 1; + } + final IndentingPrintWriter writer = + new IndentingPrintWriter(shell.getOutPrintWriter(), " "); + String nextArg = shell.getNextArg(); + if (nextArg == null) { + dumpFontConfig(writer, settings.getSystemFontConfig()); + } else { + final Map<String, FontFamily[]> fallbackMap = + settings.getSystemFallbackMap(); + FontFamily[] families = fallbackMap.get(nextArg); + if (families == null) { + writer.println("Font Family \"" + nextArg + "\" not found"); + } else { + dumpFallback(writer, families); + } } + return 0; + } - final FontManagerService.SystemFontSettings settings = mService.getCurrentFontSettings(); + private int update(ShellCommand shell) throws SystemFontException { + String fontPath = shell.getNextArg(); + if (fontPath == null) { + throw new SystemFontException( + FontManager.ERROR_CODE_INVALID_SHELL_ARGUMENT, + "Font file path argument is required."); + } + String signaturePath = shell.getNextArg(); + if (signaturePath == null) { + throw new SystemFontException( + FontManager.ERROR_CODE_INVALID_SHELL_ARGUMENT, + "Signature file argument is required."); + } + + ParcelFileDescriptor fontFd = shell.openFileForSystem(fontPath, "r"); + if (fontFd == null) { + throw new SystemFontException( + FontManager.ERROR_CODE_FAILED_TO_OPEN_FONT_FILE, + "Failed to open font file"); + } - switch (cmd) { - case "dump": - if (!DumpUtils.checkDumpPermission(ctx, TAG, shell.getErrPrintWriter())) { - return 1; + ParcelFileDescriptor sigFd = shell.openFileForSystem(signaturePath, "r"); + if (sigFd == null) { + throw new SystemFontException( + FontManager.ERROR_CODE_FAILED_TO_OPEN_SIGNATURE_FILE, + "Failed to open signature file"); + } + + try (FileInputStream sigFis = new FileInputStream(sigFd.getFileDescriptor())) { + try (FileInputStream fontFis = new FileInputStream(fontFd.getFileDescriptor())) { + int len = sigFis.available(); + if (len > MAX_SIGNATURE_FILE_SIZE_BYTES) { + throw new SystemFontException( + FontManager.ERROR_CODE_SIGNATURE_TOO_LARGE, + "Signature file is too large"); } - final IndentingPrintWriter writer = - new IndentingPrintWriter(shell.getOutPrintWriter(), " "); - String nextArg = shell.getNextArg(); - if (nextArg == null) { - dumpFontConfig(writer, settings.getSystemFontConfig()); - } else { - final Map<String, FontFamily[]> fallbackMap = settings.getSystemFallbackMap(); - FontFamily[] families = fallbackMap.get(nextArg); - if (families == null) { - writer.println("Font Family \"" + nextArg + "\" not found"); - } else { - dumpFallback(writer, families); - } + byte[] signature = new byte[len]; + if (sigFis.read(signature, 0, len) != len) { + throw new SystemFontException( + FontManager.ERROR_CODE_INVALID_SIGNATURE_FILE, + "Invalid read length"); } - return 0; - default: - shell.handleDefaultCommands(cmd); + mService.installFontFile(fontFis.getFD(), signature); + } catch (IOException e) { + throw new SystemFontException( + FontManager.ERROR_CODE_INVALID_SIGNATURE_FILE, + "Failed to read signature file.", e); + } + } catch (IOException e) { + throw new SystemFontException( + FontManager.ERROR_CODE_INVALID_FONT_FILE, + "Failed to read font files.", e); } + + shell.getOutPrintWriter().println("Success"); // TODO: Output more details. return 0; } + + private int clear(ShellCommand shell) throws SystemFontException { + mService.clearUpdates(); + shell.getOutPrintWriter().println("Success"); + return 0; + } + + private int status(ShellCommand shell) throws SystemFontException { + final FontManagerService.SystemFontSettings settings = mService.getCurrentFontSettings(); + final IndentingPrintWriter writer = + new IndentingPrintWriter(shell.getOutPrintWriter(), " "); + FontConfig config = settings.getSystemFontConfig(); + + writer.println("Current Version: " + config.getConfigVersion()); + LocalDateTime dt = LocalDateTime.ofEpochSecond(config.getLastModifiedDate(), 0, + ZoneOffset.UTC); + writer.println("Last Modified Date: " + dt.format(DateTimeFormatter.ISO_DATE_TIME)); + + Map<String, File> fontFileMap = mService.getFontFileMap(); + writer.println("Number of updated font files: " + fontFileMap.size()); + return 0; + } + + private int execCommand(@NonNull ShellCommand shell, @Nullable String cmd) { + if (cmd == null) { + return shell.handleDefaultCommands(null); + } + + try { + switch (cmd) { + case "dump": + return dump(shell); + case "update": + return update(shell); + case "clear": + return clear(shell); + case "status": + return status(shell); + default: + return shell.handleDefaultCommands(cmd); + } + } catch (SystemFontException e) { + writeCommandResult(shell, e); + return 1; + } + } } diff --git a/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java b/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java new file mode 100644 index 000000000000..f0d14ba80383 --- /dev/null +++ b/services/core/java/com/android/server/graphics/fonts/PersistentSystemFontConfig.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.graphics.fonts; + +import android.annotation.NonNull; +import android.text.TextUtils; +import android.util.Slog; +import android.util.TypedXmlPullParser; +import android.util.TypedXmlSerializer; +import android.util.Xml; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/* package */ class PersistentSystemFontConfig { + private static final String TAG = "PersistentSystemFontConfig"; + + private static final String TAG_ROOT = "fontConfig"; + private static final String TAG_LAST_MODIFIED_DATE = "lastModifiedDate"; + private static final String TAG_VALUE = "value"; + + /* package */ static class Config { + public long lastModifiedDate; + + public void reset() { + lastModifiedDate = 0; + } + + public void copyTo(@NonNull Config out) { + out.lastModifiedDate = lastModifiedDate; + } + } + + /** + * Read config XML and write to out argument. + */ + public static void loadFromXml(@NonNull InputStream is, @NonNull Config out) + throws XmlPullParserException, IOException { + out.reset(); + TypedXmlPullParser parser = Xml.resolvePullParser(is); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (type != XmlPullParser.START_TAG) { + continue; + } + final int depth = parser.getDepth(); + final String tag = parser.getName(); + if (depth == 1) { + if (!TAG_ROOT.equals(tag)) { + Slog.e(TAG, "Invalid root tag: " + tag); + return; + } + } else if (depth == 2) { + switch (tag) { + case TAG_LAST_MODIFIED_DATE: + out.lastModifiedDate = parseLongAttribute(parser, TAG_VALUE, 0); + break; + default: + Slog.w(TAG, "Skipping unknown tag: " + tag); + } + } + } + + } + + /** + * Write config to OutputStream as XML file. + */ + public static void writeToXml(@NonNull OutputStream os, @NonNull Config config) + throws IOException { + TypedXmlSerializer out = Xml.resolveSerializer(os); + out.startDocument(null /* encoding */, true /* standalone */); + + out.startTag(null, TAG_ROOT); + out.startTag(null, TAG_LAST_MODIFIED_DATE); + out.attribute(null, TAG_VALUE, Long.toString(config.lastModifiedDate)); + out.endTag(null, TAG_LAST_MODIFIED_DATE); + out.endTag(null, TAG_ROOT); + + out.endDocument(); + } + + private static long parseLongAttribute(TypedXmlPullParser parser, String attr, long defValue) { + final String value = parser.getAttributeValue(null /* namespace */, attr); + if (TextUtils.isEmpty(value)) { + return defValue; + } + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return defValue; + } + } + +} 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 8da579fd4fa3..8ec72770d782 100644 --- a/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java +++ b/services/core/java/com/android/server/graphics/fonts/UpdatableFontDir.java @@ -16,18 +16,28 @@ package com.android.server.graphics.fonts; +import static com.android.server.graphics.fonts.FontManagerService.SystemFontException; + +import android.annotation.NonNull; import android.annotation.Nullable; +import android.graphics.fonts.FontManager; +import android.graphics.fonts.SystemFonts; import android.os.FileUtils; +import android.text.FontConfig; import android.util.Base64; import android.util.Slog; import com.android.internal.annotations.GuardedBy; +import org.xmlpull.v1.XmlPullParserException; + import java.io.File; import java.io.FileDescriptor; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.security.SecureRandom; +import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -39,6 +49,8 @@ final class UpdatableFontDir { // TODO: Support .otf private static final String ALLOWED_EXTENSION = ".ttf"; + private static final String CONFIG_XML_FILE = "/data/fonts/config/config.xml"; + /** Interface to mock font file access in tests. */ interface FontFileParser { String getPostScriptName(File file) throws IOException; @@ -55,6 +67,15 @@ final class UpdatableFontDir { boolean rename(File src, File dest); } + /** Interface to mock persistent configuration */ + interface PersistentConfig { + void loadFromXml(PersistentSystemFontConfig.Config out) + throws XmlPullParserException, IOException; + void writeToXml(PersistentSystemFontConfig.Config config) + 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; @@ -87,6 +108,16 @@ final class UpdatableFontDir { private final List<File> mPreinstalledFontDirs; private final FontFileParser mParser; private final FsverityUtil mFsverityUtil; + private final File mConfigFile; + private final File mTmpConfigFile; + + @GuardedBy("UpdatableFontDir.this") + private final PersistentSystemFontConfig.Config mConfig = + new PersistentSystemFontConfig.Config(); + + @GuardedBy("UpdatableFontDir.this") + private int mConfigVersion = 1; + /** * 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 @@ -101,6 +132,20 @@ final class UpdatableFontDir { mPreinstalledFontDirs = preinstalledFontDirs; mParser = parser; mFsverityUtil = fsverityUtil; + mConfigFile = new File(CONFIG_XML_FILE); + mTmpConfigFile = new File(CONFIG_XML_FILE + ".tmp"); + loadFontFileMap(); + } + + // For unit testing + UpdatableFontDir(File filesDir, List<File> preinstalledFontDirs, FontFileParser parser, + FsverityUtil fsverityUtil, File configFile) { + mFilesDir = filesDir; + mPreinstalledFontDirs = preinstalledFontDirs; + mParser = parser; + mFsverityUtil = fsverityUtil; + mConfigFile = configFile; + mTmpConfigFile = new File(configFile.getAbsoluteFile() + ".tmp"); loadFontFileMap(); } @@ -108,6 +153,13 @@ final class UpdatableFontDir { // TODO: SIGBUS crash protection synchronized (UpdatableFontDir.this) { boolean success = false; + + try (FileInputStream fis = new FileInputStream(mConfigFile)) { + PersistentSystemFontConfig.loadFromXml(fis, mConfig); + } catch (IOException | XmlPullParserException e) { + mConfig.reset(); + } + mFontFileInfoMap.clear(); try { File[] dirs = mFilesDir.listFiles(); @@ -117,13 +169,13 @@ final class UpdatableFontDir { 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 */); + addFileToMapIfNewerLocked(fontFileInfo, true /* deleteOldFile */); } success = true; + } catch (Throwable t) { + // If something happened during loading system fonts, clear all contents in finally + // block. Here, just dumping errors. + Slog.e(TAG, "Failed to load font mappings.", t); } finally { // Delete all files just in case if we find a problematic file. if (!success) { @@ -134,6 +186,24 @@ final class UpdatableFontDir { } } + /* package */ void clearUpdates() throws SystemFontException { + synchronized (UpdatableFontDir.this) { + mFontFileInfoMap.clear(); + FileUtils.deleteContents(mFilesDir); + + mConfig.reset(); + mConfig.lastModifiedDate = Instant.now().getEpochSecond(); + try (FileOutputStream fos = new FileOutputStream(mConfigFile)) { + PersistentSystemFontConfig.writeToXml(fos, mConfig); + } catch (Exception e) { + throw new SystemFontException( + FontManager.ERROR_CODE_FAILED_TO_CREATE_CONFIG_FILE, + "Failed to write config XML.", e); + } + mConfigVersion++; + } + } + /** * Installs a new font file, or updates an existing font file. * @@ -143,38 +213,92 @@ final class UpdatableFontDir { * * @param fd A file descriptor to the font file. * @param pkcs7Signature A PKCS#7 detached signature to enable fs-verity for the font file. + * @throws SystemFontException if error occurs. */ - void installFontFile(FileDescriptor fd, byte[] pkcs7Signature) throws IOException { + void installFontFile(FileDescriptor fd, byte[] pkcs7Signature) throws SystemFontException { synchronized (UpdatableFontDir.this) { 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"); + throw new SystemFontException( + FontManager.ERROR_CODE_FAILED_TO_WRITE_FONT_FILE, + "Failed to create font directory."); } boolean success = false; try { File tempNewFontFile = new File(newDir, "font.ttf"); try (FileOutputStream out = new FileOutputStream(tempNewFontFile)) { FileUtils.copy(fd, out.getFD()); + } catch (IOException e) { + throw new SystemFontException( + FontManager.ERROR_CODE_FAILED_TO_WRITE_FONT_FILE, + "Failed to write font file to storage.", e); + } + try { + // Do not parse font file before setting up fs-verity. + // setUpFsverity throws IOException if failed. + mFsverityUtil.setUpFsverity(tempNewFontFile.getAbsolutePath(), + pkcs7Signature); + } catch (IOException e) { + throw new SystemFontException( + FontManager.ERROR_CODE_VERIFICATION_FAILURE, + "Failed to setup fs-verity.", e); + } + String postScriptName; + try { + postScriptName = mParser.getPostScriptName(tempNewFontFile); + } catch (IOException e) { + throw new SystemFontException( + FontManager.ERROR_CODE_INVALID_FONT_FILE, + "Failed to read PostScript name from font file", e); + } + if (postScriptName == null) { + throw new SystemFontException( + FontManager.ERROR_CODE_MISSING_POST_SCRIPT_NAME, + "Failed to read PostScript name from font file"); } - // 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"); + throw new SystemFontException( + FontManager.ERROR_CODE_FAILED_TO_WRITE_FONT_FILE, + "Failed to move verified font file."); } FontFileInfo fontFileInfo = validateFontFile(newFontFile); - if (fontFileInfo == null) { - // TODO: Define and return an error code for API - throw new IllegalArgumentException("Invalid file"); + + // Write config file. + PersistentSystemFontConfig.Config copied = new PersistentSystemFontConfig.Config(); + mConfig.copyTo(copied); + + copied.lastModifiedDate = Instant.now().getEpochSecond(); + try (FileOutputStream fos = new FileOutputStream(mTmpConfigFile)) { + PersistentSystemFontConfig.writeToXml(fos, copied); + } catch (Exception e) { + throw new SystemFontException( + FontManager.ERROR_CODE_FAILED_TO_CREATE_CONFIG_FILE, + "Failed to write config XML.", e); } - if (!addFileToMapLocked(fontFileInfo, false)) { - // TODO: Define and return an error code for API - throw new IllegalArgumentException("Version downgrade"); + + // Backup the mapping for rollback. + HashMap<String, FontFileInfo> backup = new HashMap<>(mFontFileInfoMap); + if (!addFileToMapIfNewerLocked(fontFileInfo, false)) { + throw new SystemFontException( + FontManager.ERROR_CODE_DOWNGRADING, + "Downgrading font file is forbidden."); + } + + if (!mFsverityUtil.rename(mTmpConfigFile, mConfigFile)) { + // If we fail to stage the config file, need to rollback the config. + mFontFileInfoMap.clear(); + mFontFileInfoMap.putAll(backup); + throw new SystemFontException( + FontManager.ERROR_CODE_FAILED_TO_CREATE_CONFIG_FILE, + "Failed to stage the config file."); } + + + // Now font update is succeeded. Update config version. + copied.copyTo(mConfig); + mConfigVersion++; + success = true; } finally { if (!success) { @@ -207,7 +331,7 @@ final class UpdatableFontDir { * higher than the currently used font file (either in {@link #mFontFileInfoMap} or {@link * #mPreinstalledFontDirs}). */ - private boolean addFileToMapLocked(FontFileInfo fontFileInfo, boolean deleteOldFile) { + private boolean addFileToMapIfNewerLocked(FontFileInfo fontFileInfo, boolean deleteOldFile) { String name = fontFileInfo.getFile().getName(); FontFileInfo existingInfo = mFontFileInfoMap.get(name); final boolean shouldAddToMap; @@ -224,13 +348,12 @@ final class UpdatableFontDir { FileUtils.deleteContentsAndDir(existingInfo.getRandomizedFontDir()); } mFontFileInfoMap.put(name, fontFileInfo); - return true; } else { if (deleteOldFile) { FileUtils.deleteContentsAndDir(fontFileInfo.getRandomizedFontDir()); } - return false; } + return shouldAddToMap; } private long getPreinstalledFontRevision(String name) { @@ -255,20 +378,23 @@ final class UpdatableFontDir { * 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) { + @NonNull + private FontFileInfo validateFontFile(File file) throws SystemFontException { if (!mFsverityUtil.hasFsverity(file.getAbsolutePath())) { - Slog.w(TAG, "Font validation failed. Fs-verity is not enabled: " + file); - return null; + throw new SystemFontException( + FontManager.ERROR_CODE_VERIFICATION_FAILURE, + "Font validation failed. Fs-verity is not enabled: " + file); } if (!validateFontFileName(file)) { - Slog.w(TAG, "Font validation failed. Could not validate font file name: " + file); - return null; + throw new SystemFontException( + FontManager.ERROR_CODE_FONT_NAME_MISMATCH, + "Font validation failed. Could not validate font file name: " + file); } long revision = getFontRevision(file); if (revision == -1) { - Slog.w(TAG, "Font validation failed. Could not read font revision: " + file); - return null; + throw new SystemFontException( + FontManager.ERROR_CODE_INVALID_FONT_FILE, + "Font validation failed. Could not read font revision: " + file); } return new FontFileInfo(file, revision); } @@ -318,4 +444,14 @@ final class UpdatableFontDir { } return map; } + + /* package */ FontConfig getSystemFontConfig() { + synchronized (UpdatableFontDir.this) { + return SystemFonts.getSystemFontConfig( + getFontFileMap(), + mConfig.lastModifiedDate, + mConfigVersion + ); + } + } } diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/PersistentSystemFontConfigTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/PersistentSystemFontConfigTest.java new file mode 100644 index 000000000000..c10cee9b4c3d --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/PersistentSystemFontConfigTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.graphics.fonts; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +@Presubmit +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class PersistentSystemFontConfigTest { + + @Test + public void testWriteRead() throws IOException, XmlPullParserException { + long expectedModifiedDate = 1234567890; + PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); + config.lastModifiedDate = expectedModifiedDate; + + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + PersistentSystemFontConfig.writeToXml(baos, config); + + byte[] written = baos.toByteArray(); + assertThat(written).isNotEmpty(); + + try (ByteArrayInputStream bais = new ByteArrayInputStream(written)) { + PersistentSystemFontConfig.Config another = new PersistentSystemFontConfig.Config(); + PersistentSystemFontConfig.loadFromXml(bais, another); + + assertThat(another.lastModifiedDate).isEqualTo(expectedModifiedDate); + } + } + } + + @Test + public void testWrongType() throws IOException, XmlPullParserException { + String xml = "<fontConfig>" + + " <lastModifiedDate value=\"string\" />" + + "</fontConfig>"; + + try (ByteArrayInputStream bais = + new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) { + PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); + PersistentSystemFontConfig.loadFromXml(bais, config); + assertThat(config.lastModifiedDate).isEqualTo(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 75bf1e6bd485..0e872d96bd85 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 @@ -22,6 +22,7 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.fail; import android.content.Context; +import android.graphics.fonts.FontManager; import android.os.FileUtils; import android.platform.test.annotations.Presubmit; @@ -36,6 +37,7 @@ import org.junit.runner.RunWith; import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -107,6 +109,7 @@ public final class UpdatableFontDirTest { private File mCacheDir; private File mUpdatableFontFilesDir; + private File mConfigFile; private List<File> mPreinstalledFontDirs; @SuppressWarnings("ResultOfMethodCallIgnored") @@ -124,6 +127,7 @@ public final class UpdatableFontDirTest { for (File dir : mPreinstalledFontDirs) { dir.mkdir(); } + mConfigFile = new File(mCacheDir, "config.xml"); } @After @@ -133,19 +137,30 @@ public final class UpdatableFontDirTest { @Test public void construct() throws Exception { + long expectedModifiedDate = 1234567890; FakeFontFileParser parser = new FakeFontFileParser(); FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); + config.lastModifiedDate = expectedModifiedDate; + writeConfig(config, mConfigFile); UpdatableFontDir dirForPreparation = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); + assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedDate()) + .isEqualTo(expectedModifiedDate); 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); + // + assertThat(dirForPreparation.getSystemFontConfig().getLastModifiedDate()) + .isNotEqualTo(expectedModifiedDate); UpdatableFontDir dir = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); assertThat(dir.getFontFileMap()).containsKey("foo.ttf"); assertThat(parser.getRevision(dir.getFontFileMap().get("foo.ttf"))).isEqualTo(3); assertThat(dir.getFontFileMap()).containsKey("bar.ttf"); @@ -159,7 +174,8 @@ public final class UpdatableFontDirTest { FakeFontFileParser parser = new FakeFontFileParser(); FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); UpdatableFontDir dir = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); assertThat(dir.getFontFileMap()).isEmpty(); } @@ -168,7 +184,8 @@ public final class UpdatableFontDirTest { FakeFontFileParser parser = new FakeFontFileParser(); FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); UpdatableFontDir dirForPreparation = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE); installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE); installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE); @@ -179,7 +196,8 @@ public final class UpdatableFontDirTest { fakeFsverityUtil.remove( dirForPreparation.getFontFileMap().get("foo.ttf").getAbsolutePath()); UpdatableFontDir dir = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); assertThat(dir.getFontFileMap()).isEmpty(); // All font dirs (including dir for "bar.ttf") should be deleted. assertThat(mUpdatableFontFilesDir.list()).hasLength(0); @@ -190,7 +208,8 @@ public final class UpdatableFontDirTest { FakeFontFileParser parser = new FakeFontFileParser(); FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); UpdatableFontDir dirForPreparation = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE); installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE); installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE); @@ -202,7 +221,8 @@ public final class UpdatableFontDirTest { FileUtils.stringToFile(dirForPreparation.getFontFileMap().get("foo.ttf"), "bar,4"); UpdatableFontDir dir = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); assertThat(dir.getFontFileMap()).isEmpty(); // All font dirs (including dir for "bar.ttf") should be deleted. assertThat(mUpdatableFontFilesDir.list()).hasLength(0); @@ -213,7 +233,8 @@ public final class UpdatableFontDirTest { FakeFontFileParser parser = new FakeFontFileParser(); FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); UpdatableFontDir dirForPreparation = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); installFontFile(dirForPreparation, "foo,1", GOOD_SIGNATURE); installFontFile(dirForPreparation, "bar,2", GOOD_SIGNATURE); installFontFile(dirForPreparation, "foo,3", GOOD_SIGNATURE); @@ -226,7 +247,8 @@ public final class UpdatableFontDirTest { 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); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); // 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. @@ -239,11 +261,22 @@ public final class UpdatableFontDirTest { } @Test + public void construct_failedToLoadConfig() throws Exception { + FakeFontFileParser parser = new FakeFontFileParser(); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + new File("/dev/null")); + assertThat(dir.getFontFileMap()).isEmpty(); + } + + @Test public void installFontFile() throws Exception { FakeFontFileParser parser = new FakeFontFileParser(); FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); UpdatableFontDir dir = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); installFontFile(dir, "test,1", GOOD_SIGNATURE); assertThat(dir.getFontFileMap()).containsKey("test.ttf"); @@ -255,7 +288,8 @@ public final class UpdatableFontDirTest { FakeFontFileParser parser = new FakeFontFileParser(); FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); UpdatableFontDir dir = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); installFontFile(dir, "test,1", GOOD_SIGNATURE); Map<String, File> mapBeforeUpgrade = dir.getFontFileMap(); @@ -272,14 +306,15 @@ public final class UpdatableFontDirTest { FakeFontFileParser parser = new FakeFontFileParser(); FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); UpdatableFontDir dir = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); installFontFile(dir, "test,2", GOOD_SIGNATURE); try { installFontFile(dir, "test,1", GOOD_SIGNATURE); fail("Expect IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // Expect + } catch (FontManagerService.SystemFontException e) { + assertThat(e.getErrorCode()).isEqualTo(FontManager.ERROR_CODE_DOWNGRADING); } assertThat(dir.getFontFileMap()).containsKey("test.ttf"); assertWithMessage("Font should not be downgraded to an older revision") @@ -291,7 +326,8 @@ public final class UpdatableFontDirTest { FakeFontFileParser parser = new FakeFontFileParser(); FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); UpdatableFontDir dir = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); installFontFile(dir, "foo,1", GOOD_SIGNATURE); installFontFile(dir, "bar,2", GOOD_SIGNATURE); @@ -302,17 +338,19 @@ public final class UpdatableFontDirTest { } @Test - public void installFontFile_invalidSignature() { + public void installFontFile_invalidSignature() throws Exception { FakeFontFileParser parser = new FakeFontFileParser(); FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); UpdatableFontDir dir = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); try { installFontFile(dir, "test,1", "Invalid signature"); - fail("Expect IOException"); - } catch (IOException e) { - // Expect + fail("Expect SystemFontException"); + } catch (FontManagerService.SystemFontException e) { + assertThat(e.getErrorCode()) + .isEqualTo(FontManager.ERROR_CODE_VERIFICATION_FAILURE); } assertThat(dir.getFontFileMap()).isEmpty(); } @@ -323,23 +361,155 @@ public final class UpdatableFontDirTest { FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"), "test,1"); UpdatableFontDir dir = new UpdatableFontDir( - mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil); + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); try { installFontFile(dir, "test,1", GOOD_SIGNATURE); fail("Expect IllegalArgumentException"); - } catch (IllegalArgumentException e) { - // Expect + } catch (FontManagerService.SystemFontException e) { + assertThat(e.getErrorCode()).isEqualTo(FontManager.ERROR_CODE_DOWNGRADING); + } + assertThat(dir.getFontFileMap()).isEmpty(); + } + + @Test + public void installFontFile_failedToWriteConfigXml() throws Exception { + long expectedModifiedDate = 1234567890; + FakeFontFileParser parser = new FakeFontFileParser(); + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + FileUtils.stringToFile(new File(mPreinstalledFontDirs.get(0), "test.ttf"), "test,1"); + + File readonlyDir = new File(mCacheDir, "readonly"); + assertThat(readonlyDir.mkdir()).isTrue(); + File readonlyFile = new File(readonlyDir, "readonly_config.xml"); + + PersistentSystemFontConfig.Config config = new PersistentSystemFontConfig.Config(); + config.lastModifiedDate = expectedModifiedDate; + writeConfig(config, readonlyFile); + + assertThat(readonlyDir.setWritable(false, false)).isTrue(); + try { + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + readonlyFile); + + try { + installFontFile(dir, "test,2", GOOD_SIGNATURE); + } catch (FontManagerService.SystemFontException e) { + assertThat(e.getErrorCode()) + .isEqualTo(FontManager.ERROR_CODE_FAILED_TO_CREATE_CONFIG_FILE); + } + assertThat(dir.getSystemFontConfig().getLastModifiedDate()) + .isEqualTo(expectedModifiedDate); + assertThat(dir.getFontFileMap()).isEmpty(); + } finally { + assertThat(readonlyDir.setWritable(true, true)).isTrue(); + } + } + + @Test + public void installFontFile_failedToParsePostScript() throws Exception { + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, + new UpdatableFontDir.FontFileParser() { + @Override + public String getPostScriptName(File file) throws IOException { + return null; + } + + @Override + public long getRevision(File file) throws IOException { + return 0; + } + }, fakeFsverityUtil, mConfigFile); + + try { + installFontFile(dir, "foo,1", GOOD_SIGNATURE); + fail("Expect SystemFontException"); + } catch (FontManagerService.SystemFontException e) { + assertThat(e.getErrorCode()) + .isEqualTo(FontManager.ERROR_CODE_MISSING_POST_SCRIPT_NAME); + } + assertThat(dir.getFontFileMap()).isEmpty(); + } + + @Test + public void installFontFile_failedToParsePostScriptName_invalidFont() throws Exception { + FakeFsverityUtil fakeFsverityUtil = new FakeFsverityUtil(); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, + new UpdatableFontDir.FontFileParser() { + @Override + public String getPostScriptName(File file) throws IOException { + throw new IOException(); + } + + @Override + public long getRevision(File file) throws IOException { + return 0; + } + }, fakeFsverityUtil, mConfigFile); + + try { + installFontFile(dir, "foo,1", GOOD_SIGNATURE); + fail("Expect SystemFontException"); + } catch (FontManagerService.SystemFontException e) { + assertThat(e.getErrorCode()) + .isEqualTo(FontManager.ERROR_CODE_INVALID_FONT_FILE); + } + assertThat(dir.getFontFileMap()).isEmpty(); + } + + @Test + public void installFontFile_renameToPsNameFailure() throws Exception { + UpdatableFontDir.FsverityUtil fakeFsverityUtil = new UpdatableFontDir.FsverityUtil() { + private final FakeFsverityUtil mFake = new FakeFsverityUtil(); + + @Override + public boolean hasFsverity(String path) { + return mFake.hasFsverity(path); + } + + @Override + public void setUpFsverity(String path, byte[] pkcs7Signature) throws IOException { + mFake.setUpFsverity(path, pkcs7Signature); + } + + @Override + public boolean rename(File src, File dest) { + return false; + } + }; + FakeFontFileParser parser = new FakeFontFileParser(); + UpdatableFontDir dir = new UpdatableFontDir( + mUpdatableFontFilesDir, mPreinstalledFontDirs, parser, fakeFsverityUtil, + mConfigFile); + + try { + installFontFile(dir, "foo,1", GOOD_SIGNATURE); + fail("Expect SystemFontException"); + } catch (FontManagerService.SystemFontException e) { + assertThat(e.getErrorCode()) + .isEqualTo(FontManager.ERROR_CODE_FAILED_TO_WRITE_FONT_FILE); } assertThat(dir.getFontFileMap()).isEmpty(); } private void installFontFile(UpdatableFontDir dir, String content, String signature) - throws IOException { + throws Exception { File file = File.createTempFile("font", "ttf", mCacheDir); FileUtils.stringToFile(file, content); try (FileInputStream in = new FileInputStream(file)) { dir.installFontFile(in.getFD(), signature.getBytes()); } } + + private void writeConfig(PersistentSystemFontConfig.Config config, + File file) throws IOException { + try (FileOutputStream fos = new FileOutputStream(file)) { + PersistentSystemFontConfig.writeToXml(fos, config); + } + } } |