diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java index fd73b29276..16f32c4898 100644 --- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java +++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java @@ -73,7 +73,9 @@ class CertificateTransparencyDownloader extends BroadcastReceiver { new CertificateTransparencyInstaller()); } - void registerReceiver() { + void initialize() { + mInstaller.addCompatibilityVersion(Config.COMPATIBILITY_VERSION); + IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE); mContext.registerReceiver(this, intentFilter, Context.RECEIVER_EXPORTED); @@ -185,7 +187,7 @@ class CertificateTransparencyDownloader extends BroadcastReceiver { String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING); String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING); try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) { - success = mInstaller.install(inputStream, version); + success = mInstaller.install(Config.COMPATIBILITY_VERSION, inputStream, version); } catch (IOException e) { Log.e(TAG, "Could not install new content", e); return; diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java index 914af06400..0ae982d3bf 100644 --- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java +++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java @@ -43,7 +43,7 @@ class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesC void initialize() { mDataStore.load(); - mCertificateTransparencyDownloader.registerReceiver(); + mCertificateTransparencyDownloader.initialize(); DeviceConfig.addOnPropertiesChangedListener( Config.NAMESPACE_NETWORK_SECURITY, Executors.newSingleThreadExecutor(), this); if (Config.DEBUG) { diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java index 82dcadfdef..4ca97eb308 100644 --- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java +++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java @@ -15,148 +15,78 @@ */ package com.android.server.net.ct; -import android.annotation.SuppressLint; -import android.system.ErrnoException; -import android.system.Os; import android.util.Log; import java.io.File; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; /** Installer of CT log lists. */ public class CertificateTransparencyInstaller { private static final String TAG = "CertificateTransparencyInstaller"; - private static final String CT_DIR_NAME = "/data/misc/keychain/ct/"; - static final String LOGS_DIR_PREFIX = "logs-"; - static final String LOGS_LIST_FILE_NAME = "log_list.json"; - static final String CURRENT_DIR_SYMLINK_NAME = "current"; + private final Map mCompatVersions = new HashMap<>(); - private final File mCertificateTransparencyDir; - private final File mCurrentDirSymlink; + // The CT root directory. + private final File mRootDirectory; - CertificateTransparencyInstaller(File certificateTransparencyDir) { - mCertificateTransparencyDir = certificateTransparencyDir; - mCurrentDirSymlink = new File(certificateTransparencyDir, CURRENT_DIR_SYMLINK_NAME); + public CertificateTransparencyInstaller(File rootDirectory) { + mRootDirectory = rootDirectory; } - CertificateTransparencyInstaller() { - this(new File(CT_DIR_NAME)); + public CertificateTransparencyInstaller(String rootDirectoryPath) { + this(new File(rootDirectoryPath)); + } + + public CertificateTransparencyInstaller() { + this(Config.CT_ROOT_DIRECTORY_PATH); + } + + void addCompatibilityVersion(String versionName) { + removeCompatibilityVersion(versionName); + CompatibilityVersion newCompatVersion = + new CompatibilityVersion(new File(mRootDirectory, versionName)); + mCompatVersions.put(versionName, newCompatVersion); + } + + void removeCompatibilityVersion(String versionName) { + CompatibilityVersion compatVersion = mCompatVersions.remove(versionName); + if (compatVersion != null && !compatVersion.delete()) { + Log.w(TAG, "Could not delete compatibility version directory."); + } + } + + CompatibilityVersion getCompatibilityVersion(String versionName) { + return mCompatVersions.get(versionName); } /** * Install a new log list to use during SCT verification. * + * @param compatibilityVersion the compatibility version of the new log list * @param newContent an input stream providing the log list - * @param version the version of the new log list + * @param version the minor version of the new log list * @return true if the log list was installed successfully, false otherwise. * @throws IOException if the list cannot be saved in the CT directory. */ - public boolean install(InputStream newContent, String version) throws IOException { - // To support atomically replacing the old configuration directory with the new there's a - // bunch of steps. We create a new directory with the logs and then do an atomic update of - // the current symlink to point to the new directory. - // 1. Ensure that the update dir exists and is readable. - makeDir(mCertificateTransparencyDir); - - File newLogsDir = new File(mCertificateTransparencyDir, LOGS_DIR_PREFIX + version); - // 2. Handle the corner case where the new directory already exists. - if (newLogsDir.exists()) { - // If the symlink has already been updated then the update died between steps 6 and 7 - // and so we cannot delete the directory since it is in use. - if (newLogsDir.getCanonicalPath().equals(mCurrentDirSymlink.getCanonicalPath())) { - deleteOldLogDirectories(); - return false; - } - // If the symlink has not been updated then the previous installation failed and this is - // a re-attempt. Clean-up leftover files and try again. - deleteContentsAndDir(newLogsDir); - } - try { - // 3. Create /data/misc/keychain/ct/logs-/ . - makeDir(newLogsDir); - - // 4. Move the log list json file in logs-/ . - File logListFile = new File(newLogsDir, LOGS_LIST_FILE_NAME); - if (Files.copy(newContent, logListFile.toPath()) == 0) { - throw new IOException("The log list appears empty"); - } - setWorldReadable(logListFile); - - // 5. Create temp symlink. We rename this to the target symlink to get an atomic update. - File tempSymlink = new File(mCertificateTransparencyDir, "new_symlink"); - try { - Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath()); - } catch (ErrnoException e) { - throw new IOException("Failed to create symlink", e); - } - - // 6. Update the symlink target, this is the actual update step. - tempSymlink.renameTo(mCurrentDirSymlink.getAbsoluteFile()); - } catch (IOException | RuntimeException e) { - deleteContentsAndDir(newLogsDir); - throw e; - } - Log.i(TAG, "CT log directory updated to " + newLogsDir.getAbsolutePath()); - // 7. Cleanup - deleteOldLogDirectories(); - return true; - } - - private void makeDir(File dir) throws IOException { - dir.mkdir(); - if (!dir.isDirectory()) { - throw new IOException("Unable to make directory " + dir.getCanonicalPath()); - } - setWorldReadable(dir); - } - - // CT files and directories are readable by all apps. - @SuppressLint("SetWorldReadable") - private void setWorldReadable(File file) throws IOException { - if (!file.setReadable(true, false)) { - throw new IOException("Failed to set " + file.getCanonicalPath() + " readable"); - } - } - - private void deleteOldLogDirectories() throws IOException { - if (!mCertificateTransparencyDir.exists()) { - return; - } - File currentTarget = mCurrentDirSymlink.getCanonicalFile(); - for (File file : mCertificateTransparencyDir.listFiles()) { - if (!currentTarget.equals(file.getCanonicalFile()) - && file.getName().startsWith(LOGS_DIR_PREFIX)) { - deleteContentsAndDir(file); - } - } - } - - static boolean deleteContentsAndDir(File dir) { - if (deleteContents(dir)) { - return dir.delete(); - } else { + public boolean install(String compatibilityVersion, InputStream newContent, String version) + throws IOException { + CompatibilityVersion compatVersion = mCompatVersions.get(compatibilityVersion); + if (compatVersion == null) { + Log.e(TAG, "No compatibility version for " + compatibilityVersion); return false; } - } + // Ensure root directory exists and is readable. + DirectoryUtils.makeDir(mRootDirectory); - private static boolean deleteContents(File dir) { - File[] files = dir.listFiles(); - boolean success = true; - if (files != null) { - for (File file : files) { - if (file.isDirectory()) { - success &= deleteContents(file); - } - if (!file.delete()) { - Log.w(TAG, "Failed to delete " + file); - success = false; - } - } + if (!compatVersion.install(newContent, version)) { + Log.e(TAG, "Failed to install logs for compatibility version " + compatibilityVersion); + return false; } - return success; + Log.i(TAG, "New logs installed at " + compatVersion.getLogsDir()); + return true; } } diff --git a/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java new file mode 100644 index 0000000000..27488b59b6 --- /dev/null +++ b/networksecurity/service/src/com/android/server/net/ct/CompatibilityVersion.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 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.net.ct; + +import android.system.ErrnoException; +import android.system.Os; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; + +/** Represents a compatibility version directory. */ +class CompatibilityVersion { + + static final String LOGS_DIR_PREFIX = "logs-"; + static final String LOGS_LIST_FILE_NAME = "log_list.json"; + + private static final String CURRENT_LOGS_DIR_SYMLINK_NAME = "current"; + + private final File mRootDirectory; + private final File mCurrentLogsDirSymlink; + + private File mCurrentLogsDir = null; + + CompatibilityVersion(File rootDirectory) { + mRootDirectory = rootDirectory; + mCurrentLogsDirSymlink = new File(mRootDirectory, CURRENT_LOGS_DIR_SYMLINK_NAME); + } + + /** + * Installs a log list within this compatibility version directory. + * + * @param newContent an input stream providing the log list + * @param version the version number of the log list + * @return true if the log list was installed successfully, false otherwise. + * @throws IOException if the list cannot be saved in the CT directory. + */ + boolean install(InputStream newContent, String version) throws IOException { + // To support atomically replacing the old configuration directory with the new there's a + // bunch of steps. We create a new directory with the logs and then do an atomic update of + // the current symlink to point to the new directory. + // 1. Ensure that the root directory exists and is readable. + DirectoryUtils.makeDir(mRootDirectory); + + File newLogsDir = new File(mRootDirectory, LOGS_DIR_PREFIX + version); + // 2. Handle the corner case where the new directory already exists. + if (newLogsDir.exists()) { + // If the symlink has already been updated then the update died between steps 6 and 7 + // and so we cannot delete the directory since it is in use. + if (newLogsDir.getCanonicalPath().equals(mCurrentLogsDirSymlink.getCanonicalPath())) { + deleteOldLogDirectories(); + return false; + } + // If the symlink has not been updated then the previous installation failed and this is + // a re-attempt. Clean-up leftover files and try again. + DirectoryUtils.removeDir(newLogsDir); + } + try { + // 3. Create a new logs-/ directory to store the new list. + DirectoryUtils.makeDir(newLogsDir); + + // 4. Move the log list json file in logs-/ . + File logListFile = new File(newLogsDir, LOGS_LIST_FILE_NAME); + if (Files.copy(newContent, logListFile.toPath()) == 0) { + throw new IOException("The log list appears empty"); + } + DirectoryUtils.setWorldReadable(logListFile); + + // 5. Create temp symlink. We rename this to the target symlink to get an atomic update. + File tempSymlink = new File(mRootDirectory, "new_symlink"); + try { + Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath()); + } catch (ErrnoException e) { + throw new IOException("Failed to create symlink", e); + } + + // 6. Update the symlink target, this is the actual update step. + tempSymlink.renameTo(mCurrentLogsDirSymlink.getAbsoluteFile()); + } catch (IOException | RuntimeException e) { + DirectoryUtils.removeDir(newLogsDir); + throw e; + } + // 7. Cleanup + mCurrentLogsDir = newLogsDir; + deleteOldLogDirectories(); + return true; + } + + File getRootDir() { + return mRootDirectory; + } + + File getLogsDir() { + return mCurrentLogsDir; + } + + File getLogsDirSymlink() { + return mCurrentLogsDirSymlink; + } + + File getLogsFile() { + return new File(mCurrentLogsDir, LOGS_LIST_FILE_NAME); + } + + boolean delete() { + return DirectoryUtils.removeDir(mRootDirectory); + } + + private void deleteOldLogDirectories() throws IOException { + if (!mRootDirectory.exists()) { + return; + } + File currentTarget = mCurrentLogsDirSymlink.getCanonicalFile(); + for (File file : mRootDirectory.listFiles()) { + if (!currentTarget.equals(file.getCanonicalFile()) + && file.getName().startsWith(LOGS_DIR_PREFIX)) { + DirectoryUtils.removeDir(file); + } + } + } +} diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java index 611a5c791f..242f13af09 100644 --- a/networksecurity/service/src/com/android/server/net/ct/Config.java +++ b/networksecurity/service/src/com/android/server/net/ct/Config.java @@ -33,6 +33,10 @@ final class Config { private static final String PREFERENCES_FILE_NAME = "ct.preferences"; static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME); + // CT directory + static final String CT_ROOT_DIRECTORY_PATH = "/data/misc/keychain/ct/"; + static final String COMPATIBILITY_VERSION = "v1"; + // Phenotype flags static final String NAMESPACE_NETWORK_SECURITY = "network_security"; private static final String FLAGS_PREFIX = "CertificateTransparencyLogList__"; diff --git a/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java new file mode 100644 index 0000000000..e3b4124ed0 --- /dev/null +++ b/networksecurity/service/src/com/android/server/net/ct/DirectoryUtils.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2024 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.net.ct; + +import android.annotation.SuppressLint; + +import java.io.File; +import java.io.IOException; + +/** Utility class to manipulate CT directories. */ +class DirectoryUtils { + + static void makeDir(File dir) throws IOException { + dir.mkdir(); + if (!dir.isDirectory()) { + throw new IOException("Unable to make directory " + dir.getCanonicalPath()); + } + setWorldReadable(dir); + } + + // CT files and directories are readable by all apps. + @SuppressLint("SetWorldReadable") + static void setWorldReadable(File file) throws IOException { + if (!file.setReadable(true, false)) { + throw new IOException("Failed to set " + file.getCanonicalPath() + " readable"); + } + } + + static boolean removeDir(File dir) { + return deleteContentsAndDir(dir); + } + + private static boolean deleteContentsAndDir(File dir) { + if (deleteContents(dir)) { + return dir.delete(); + } else { + return false; + } + } + + private static boolean deleteContents(File dir) { + File[] files = dir.listFiles(); + boolean success = true; + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + success &= deleteContents(file); + } + if (!file.delete()) { + success = false; + } + } + } + return success; + } +} diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java index 1aad028018..df02446796 100644 --- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java +++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java @@ -159,7 +159,9 @@ public class CertificateTransparencyDownloaderTest { Base64.getEncoder().encodeToString(mPublicKey.getEncoded())); setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri); - when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(true); + when(mCertificateTransparencyInstaller.install( + eq(Config.COMPATIBILITY_VERSION), any(), eq(version))) + .thenReturn(true); assertThat(mDataStore.getProperty(Config.VERSION)).isNull(); assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull(); @@ -168,7 +170,8 @@ public class CertificateTransparencyDownloaderTest { mCertificateTransparencyDownloader.onReceive( mContext, makeDownloadCompleteIntent(contentId)); - verify(mCertificateTransparencyInstaller, times(1)).install(any(), eq(version)); + verify(mCertificateTransparencyInstaller, times(1)) + .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version)); assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version); assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isEqualTo(contentUri.toString()); assertThat(mDataStore.getProperty(Config.METADATA_URL)).isEqualTo(metadataUri.toString()); @@ -185,7 +188,9 @@ public class CertificateTransparencyDownloaderTest { Uri metadataUri = Uri.fromFile(metadataFile); setUpDownloadComplete(version, metadataId, metadataUri, contentId, contentUri); - when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(false); + when(mCertificateTransparencyInstaller.install( + eq(Config.COMPATIBILITY_VERSION), any(), eq(version))) + .thenReturn(false); mCertificateTransparencyDownloader.onReceive( mContext, makeDownloadCompleteIntent(contentId)); @@ -208,7 +213,8 @@ public class CertificateTransparencyDownloaderTest { mCertificateTransparencyDownloader.onReceive( mContext, makeDownloadCompleteIntent(contentId)); - verify(mCertificateTransparencyInstaller, never()).install(any(), eq(version)); + verify(mCertificateTransparencyInstaller, never()) + .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version)); assertThat(mDataStore.getProperty(Config.VERSION)).isNull(); assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull(); assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull(); @@ -230,7 +236,8 @@ public class CertificateTransparencyDownloaderTest { mCertificateTransparencyDownloader.onReceive( mContext, makeDownloadCompleteIntent(contentId)); - verify(mCertificateTransparencyInstaller, never()).install(any(), eq(version)); + verify(mCertificateTransparencyInstaller, never()) + .install(eq(Config.COMPATIBILITY_VERSION), any(), eq(version)); assertThat(mDataStore.getProperty(Config.VERSION)).isNull(); assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull(); assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull(); diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java index bfb8bdfa3a..50d3f23743 100644 --- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java +++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java @@ -17,11 +17,9 @@ package com.android.server.net.ct; import static com.google.common.truth.Truth.assertThat; -import android.system.ErrnoException; -import android.system.Os; - import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,98 +37,134 @@ import java.io.OutputStream; @RunWith(JUnit4.class) public class CertificateTransparencyInstallerTest { + private static final String TEST_VERSION = "test-v1"; + private File mTestDir = new File( InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(), "test-dir"); - private File mTestSymlink = - new File(mTestDir, CertificateTransparencyInstaller.CURRENT_DIR_SYMLINK_NAME); private CertificateTransparencyInstaller mCertificateTransparencyInstaller = new CertificateTransparencyInstaller(mTestDir); @Before public void setUp() { - CertificateTransparencyInstaller.deleteContentsAndDir(mTestDir); + mCertificateTransparencyInstaller.addCompatibilityVersion(TEST_VERSION); + } + + @After + public void tearDown() { + mCertificateTransparencyInstaller.removeCompatibilityVersion(TEST_VERSION); + DirectoryUtils.removeDir(mTestDir); + } + + @Test + public void testCompatibilityVersion_installSuccessful() throws IOException { + assertThat(mTestDir.mkdir()).isTrue(); + String content = "i_am_compatible"; + String version = "i_am_version"; + CompatibilityVersion compatVersion = + mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION); + + try (InputStream inputStream = asStream(content)) { + assertThat(compatVersion.install(inputStream, version)).isTrue(); + } + File logsDir = compatVersion.getLogsDir(); + assertThat(logsDir.exists()).isTrue(); + assertThat(logsDir.isDirectory()).isTrue(); + assertThat(logsDir.getAbsolutePath()) + .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION); + File logsListFile = compatVersion.getLogsFile(); + assertThat(logsListFile.exists()).isTrue(); + assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath()); + assertThat(readAsString(logsListFile)).isEqualTo(content); + File logsSymlink = compatVersion.getLogsDirSymlink(); + assertThat(logsSymlink.exists()).isTrue(); + assertThat(logsSymlink.isDirectory()).isTrue(); + assertThat(logsSymlink.getAbsolutePath()) + .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION + "/current"); + assertThat(logsSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath()); + + assertThat(compatVersion.delete()).isTrue(); + assertThat(logsDir.exists()).isFalse(); + assertThat(logsSymlink.exists()).isFalse(); + assertThat(logsListFile.exists()).isFalse(); + } + + @Test + public void testCompatibilityVersion_versionInstalledFailed() throws IOException { + assertThat(mTestDir.mkdir()).isTrue(); + + CompatibilityVersion compatVersion = + mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION); + File rootDir = compatVersion.getRootDir(); + assertThat(rootDir.mkdir()).isTrue(); + + String existingVersion = "666"; + File existingLogDir = + new File(rootDir, CompatibilityVersion.LOGS_DIR_PREFIX + existingVersion); + assertThat(existingLogDir.mkdir()).isTrue(); + + String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through"; + File logsListFile = new File(existingLogDir, CompatibilityVersion.LOGS_LIST_FILE_NAME); + assertThat(logsListFile.createNewFile()).isTrue(); + writeToFile(logsListFile, existingContent); + + String newContent = "i_am_the_real_content"; + try (InputStream inputStream = asStream(newContent)) { + assertThat(compatVersion.install(inputStream, existingVersion)).isTrue(); + } + + assertThat(readAsString(logsListFile)).isEqualTo(newContent); } @Test public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException { String content = "i_am_a_certificate_and_i_am_transparent"; String version = "666"; - boolean success = false; try (InputStream inputStream = asStream(content)) { - success = mCertificateTransparencyInstaller.install(inputStream, version); + assertThat( + mCertificateTransparencyInstaller.install( + TEST_VERSION, inputStream, version)) + .isTrue(); } - assertThat(success).isTrue(); assertThat(mTestDir.exists()).isTrue(); assertThat(mTestDir.isDirectory()).isTrue(); - assertThat(mTestSymlink.exists()).isTrue(); - assertThat(mTestSymlink.isDirectory()).isTrue(); - - File logsDir = - new File(mTestDir, CertificateTransparencyInstaller.LOGS_DIR_PREFIX + version); + CompatibilityVersion compatVersion = + mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION); + File logsDir = compatVersion.getLogsDir(); assertThat(logsDir.exists()).isTrue(); assertThat(logsDir.isDirectory()).isTrue(); - assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath()); - - File logsListFile = new File(logsDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME); + assertThat(logsDir.getAbsolutePath()) + .startsWith(mTestDir.getAbsolutePath() + "/" + TEST_VERSION); + File logsListFile = compatVersion.getLogsFile(); assertThat(logsListFile.exists()).isTrue(); + assertThat(logsListFile.getAbsolutePath()).startsWith(logsDir.getAbsolutePath()); assertThat(readAsString(logsListFile)).isEqualTo(content); } @Test public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled() - throws IOException, ErrnoException { + throws IOException { String existingVersion = "666"; String existingContent = "i_was_already_installed_successfully"; - File existingLogDir = - new File( - mTestDir, - CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion); - assertThat(mTestDir.mkdir()).isTrue(); - assertThat(existingLogDir.mkdir()).isTrue(); - Os.symlink(existingLogDir.getCanonicalPath(), mTestSymlink.getCanonicalPath()); - File logsListFile = - new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME); - logsListFile.createNewFile(); - writeToFile(logsListFile, existingContent); - boolean success = false; + CompatibilityVersion compatVersion = + mCertificateTransparencyInstaller.getCompatibilityVersion(TEST_VERSION); + + DirectoryUtils.makeDir(mTestDir); + try (InputStream inputStream = asStream(existingContent)) { + assertThat(compatVersion.install(inputStream, existingVersion)).isTrue(); + } try (InputStream inputStream = asStream("i_will_be_ignored")) { - success = mCertificateTransparencyInstaller.install(inputStream, existingVersion); + assertThat( + mCertificateTransparencyInstaller.install( + TEST_VERSION, inputStream, existingVersion)) + .isFalse(); } - assertThat(success).isFalse(); - assertThat(readAsString(logsListFile)).isEqualTo(existingContent); - } - - @Test - public void testCertificateTransparencyInstaller_versionInstalledFailed() - throws IOException, ErrnoException { - String existingVersion = "666"; - String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through"; - String newContent = "i_am_the_real_certificate"; - File existingLogDir = - new File( - mTestDir, - CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion); - assertThat(mTestDir.mkdir()).isTrue(); - assertThat(existingLogDir.mkdir()).isTrue(); - File logsListFile = - new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME); - logsListFile.createNewFile(); - writeToFile(logsListFile, existingContent); - boolean success = false; - - try (InputStream inputStream = asStream(newContent)) { - success = mCertificateTransparencyInstaller.install(inputStream, existingVersion); - } - - assertThat(success).isTrue(); - assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(existingLogDir.getCanonicalPath()); - assertThat(readAsString(logsListFile)).isEqualTo(newContent); + assertThat(readAsString(compatVersion.getLogsFile())).isEqualTo(existingContent); } private static InputStream asStream(String string) throws IOException {