blob: e15afb074993aaabe68a2a39b3b48199bad4976b [file] [log] [blame]
// Copyright 2018 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "arc/apk-cache/cache_cleaner.h"
#include <set>
#include <string>
#include <vector>
#include <base/files/file_enumerator.h>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
#include <base/strings/stringprintf.h>
#include <base/time/time.h>
#include <gtest/gtest.h>
namespace apk_cache {
namespace {
constexpr char kPackage0[] = "com.example.package0";
constexpr char kPackage1[] = "com.example.package1";
constexpr int kObbVersion = 1;
bool CreateFile(const base::FilePath& file_path) {
return base::WriteFile(file_path, "", 0) == 0;
}
bool DeleteFile(const base::FilePath& file_path) {
return base::DeleteFile(file_path);
}
bool CreateDir(const base::FilePath& dir_path) {
return base::CreateDirectory(dir_path);
}
std::string GetApkFileName(const std::string& package_name) {
return package_name + kApkExtension;
}
std::string GetMainObbFileName(const std::string& package_name, int version) {
return base::StringPrintf("%s%d.%s%s", kMainObbPrefix, version,
package_name.c_str(), kObbExtension);
}
std::string GetPatchObbFileName(const std::string& package_name, int version) {
return base::StringPrintf("%s%d.%s%s", kPatchObbPrefix, version,
package_name.c_str(), kObbExtension);
}
std::string TimeToString(const base::Time& time) {
base::Time::Exploded exploded_time;
time.UTCExplode(&exploded_time);
// We can't use base::Time::operator<< here because the ARC cache app writes
// access time into JSON file following the defined time format
// that is different from what operator<< provides.
return base::StringPrintf(
"%04d-%02d-%02d %02d:%02d:%02d.%03d", exploded_time.year,
exploded_time.month, exploded_time.day_of_month, exploded_time.hour,
exploded_time.minute, exploded_time.second, exploded_time.millisecond);
}
bool WriteAttributes(const base::FilePath& dir_path,
const std::string& package_name,
const base::Time& time) {
const std::string json_content = base::StringPrintf(
"{\n"
" \"attributes\": {\n"
" \"package_name\": \"%s\",\n"
" \"atime\": \"%s\"\n"
" }\n"
"}",
package_name.c_str(), TimeToString(time).c_str());
const int written_bytes = base::WriteFile(
dir_path.Append(kAttrJson), json_content.c_str(), json_content.length());
return written_bytes == json_content.length();
}
bool CreateValidPackage(const base::FilePath& cache_root_path,
const std::string& package_name) {
const base::FilePath package_path = cache_root_path.Append(package_name);
return CreateDir(package_path) &&
CreateFile(package_path.Append(GetApkFileName(package_name))) &&
CreateFile(package_path.Append(
GetMainObbFileName(package_name, kObbVersion))) &&
CreateFile(package_path.Append(
GetPatchObbFileName(package_name, kObbVersion))) &&
WriteAttributes(package_path, package_name, base::Time::Now());
}
void VerifyCache(const base::FilePath& cache_root_path,
const std::set<std::string>& expected_package_name_set) {
{
base::FileEnumerator files(
cache_root_path, false /* recursive */,
base::FileEnumerator::FILES | base::FileEnumerator::SHOW_SYM_LINKS);
base::FilePath unnecessary_file_path = files.Next();
if (!unnecessary_file_path.empty())
FAIL() << "Cache root should not contain any files but it contains "
<< unnecessary_file_path.value();
}
{
base::FileEnumerator dirs(cache_root_path, false /* recursive */,
base::FileEnumerator::DIRECTORIES |
base::FileEnumerator::SHOW_SYM_LINKS);
std::set<std::string> available_package_name_set;
for (base::FilePath dir_path = dirs.Next(); !dir_path.empty();
dir_path = dirs.Next()) {
available_package_name_set.insert(dir_path.BaseName().value());
}
EXPECT_EQ(available_package_name_set, expected_package_name_set);
}
}
} // namespace
class CacheCleanerTest : public testing::Test {
public:
const base::FilePath& temp_path() const { return temp_dir_.GetPath(); }
protected:
CacheCleanerTest() = default;
CacheCleanerTest(const CacheCleanerTest&) = delete;
CacheCleanerTest& operator=(const CacheCleanerTest&) = delete;
void SetUp() override { ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); }
void TearDown() override { ASSERT_TRUE(temp_dir_.Delete()); }
private:
base::ScopedTempDir temp_dir_;
};
// Creates 2 valid packages and checks that none of them is deleted.
TEST_F(CacheCleanerTest, ValidPackage) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage1));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {kPackage0, kPackage1});
}
// Checks that absence of main OBB file does not lead to the deletion.
TEST_F(CacheCleanerTest, NoMainObb) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(apk_cache::DeleteFile(temp_path().Append(kPackage0).Append(
GetMainObbFileName(kPackage0, kObbVersion))));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {kPackage0});
}
// Checks that absence of patch OBB file does not lead to the deletion.
TEST_F(CacheCleanerTest, NoPatchObb) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(apk_cache::DeleteFile(temp_path().Append(kPackage0).Append(
GetPatchObbFileName(kPackage0, kObbVersion))));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {kPackage0});
}
// Checks that the files in cache root are deleted.
TEST_F(CacheCleanerTest, OddFileInRoot) {
ASSERT_TRUE(CreateFile(temp_path().Append("odd.file.1")));
ASSERT_TRUE(CreateFile(temp_path().Append("odd.file.2")));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// Checks that empty package directory is deleted.
TEST_F(CacheCleanerTest, EmptyPackage) {
ASSERT_TRUE(CreateDir(temp_path().Append(kPackage0)));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// No attr.json must lead to the package removal.
TEST_F(CacheCleanerTest, NoAttrJson) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(
apk_cache::DeleteFile(temp_path().Append(kPackage0).Append(kAttrJson)));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// No APK file leads to the package removal.
TEST_F(CacheCleanerTest, NoApk) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(apk_cache::DeleteFile(
temp_path().Append(kPackage0).Append(GetApkFileName(kPackage0))));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// If there are 2 or more main OBB files in the package, it must be deleted.
TEST_F(CacheCleanerTest, ExtraMainObbFile) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(CreateFile(
temp_path().Append(kPackage0).Append(GetMainObbFileName(kPackage0, 99))));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// If there are 2 or more patch OBB files in the package, it must be deleted.
TEST_F(CacheCleanerTest, ExtraPatchObbFile) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(CreateFile(temp_path().Append(kPackage0).Append(
GetPatchObbFileName(kPackage0, 99))));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// If package contains any odd file, the package is to be deleted.
TEST_F(CacheCleanerTest, ExtraRandomFile) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(CreateFile(temp_path().Append(kPackage0).Append("blabla.file")));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// If package contains any odd dir, the package is to be deleted.
TEST_F(CacheCleanerTest, ExtraRandomDir) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(CreateDir(temp_path().Append(kPackage0).Append("blabla.dir")));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// If the name of the APK does not match com.example.package.name.apk pattern,
// then the package must be deleted.
TEST_F(CacheCleanerTest, ApkNameMismatch) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(apk_cache::DeleteFile(
temp_path().Append(kPackage0).Append(GetApkFileName(kPackage0))));
ASSERT_TRUE(CreateFile(
temp_path().Append(kPackage0).Append(GetApkFileName("blabla"))));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// If the name of the main OBB does not match
// main.123.com.example.package.name.obb pattern, then the package must
// be deleted.
TEST_F(CacheCleanerTest, MainObbNameMismatch) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(apk_cache::DeleteFile(temp_path().Append(kPackage0).Append(
GetMainObbFileName(kPackage0, kObbVersion))));
ASSERT_TRUE(CreateFile(temp_path().Append(kPackage0).Append(
GetMainObbFileName("blabla", kObbVersion))));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// If the name of the main OBB does not match
// patch.123.com.example.package.name.obb pattern, then the package must
// be deleted.
TEST_F(CacheCleanerTest, PatchObbNameMismatch) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(apk_cache::DeleteFile(temp_path().Append(kPackage0).Append(
GetPatchObbFileName(kPackage0, kObbVersion))));
ASSERT_TRUE(CreateFile(temp_path().Append(kPackage0).Append(
GetPatchObbFileName("blabla", kObbVersion))));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
}
// Verify that expired package is deleted.
TEST_F(CacheCleanerTest, Outdated) {
// Package0 is expired. Should be deleted.
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
base::Time atime_0 =
base::Time::Now() - (kValidityPeriod + base::TimeDelta::FromDays(1));
WriteAttributes(temp_path().Append(kPackage0), kPackage0, atime_0);
// Package1 is 1 more day before expire. Should not be deleted.
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage1));
base::Time atime_1 =
base::Time::Now() - (kValidityPeriod - base::TimeDelta::FromDays(1));
WriteAttributes(temp_path().Append(kPackage1), kPackage1, atime_1);
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {kPackage1});
}
// Check that symbolic links are deleted.
TEST_F(CacheCleanerTest, Symlink) {
base::ScopedTempDir temp_dir_1;
ASSERT_TRUE(temp_dir_1.CreateUniqueTempDir());
// Create symlink to a file in the cache root.
const base::FilePath symlink_file_target_path =
temp_dir_1.GetPath().Append("symlink-target-file");
ASSERT_TRUE(CreateFile(symlink_file_target_path));
ASSERT_TRUE(CreateSymbolicLink(symlink_file_target_path,
temp_path().Append("symlink-file")));
// Create symlink to the valid package directory in the cache root.
// We don't follow symlinks and they must be removed from the cache root.
const base::FilePath symlink_dir_target_path =
temp_dir_1.GetPath().Append(kPackage0);
ASSERT_TRUE(CreateValidPackage(temp_dir_1.GetPath(), kPackage0));
ASSERT_TRUE(CreateSymbolicLink(symlink_dir_target_path,
temp_path().Append(kPackage0)));
// Create symlink in the package.
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage1));
ASSERT_TRUE(
CreateSymbolicLink(symlink_file_target_path,
temp_path().Append(kPackage1).Append("symlink")));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {});
EXPECT_TRUE(base::PathExists(symlink_file_target_path));
EXPECT_TRUE(base::DirectoryExists(symlink_dir_target_path));
}
// Verify that deletion of the a package or file does not lead to
// the deletion of the valid package.
TEST_F(CacheCleanerTest, Combined) {
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage0));
ASSERT_TRUE(CreateValidPackage(temp_path(), kPackage1));
base::Time atime =
base::Time::Now() - (kValidityPeriod + base::TimeDelta::FromDays(1));
WriteAttributes(temp_path().Append(kPackage1), kPackage1, atime);
ASSERT_TRUE(CreateFile(temp_path().Append("odd.file")));
EXPECT_TRUE(Clean(temp_path()));
VerifyCache(temp_path(), {kPackage0});
}
} // namespace apk_cache