blob: 2ed297d611183de124086e327ee09d55c572146b [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 <fnmatch.h>
#include <memory>
#include <string>
#include <vector>
#include <base/files/file.h>
#include <base/files/file_enumerator.h>
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/json/json_reader.h>
#include <base/logging.h>
#include <base/time/time.h>
#include <base/values.h>
namespace apk_cache {
const char kAttrJson[] = "attr.json";
const char kApkExtension[] = ".apk";
const char kObbExtension[] = ".obb";
const char kMainObbPrefix[] = "main.";
const char kPatchObbPrefix[] = "patch.";
const base::TimeDelta kValidityPeriod = base::TimeDelta::FromDays(30);
namespace {
constexpr char kKeyAttributesAtime[] = "attributes.atime";
// Removes all the files (if any) from cache root. Does not remove
// directories. Returns true if all the intended files were deleted.
bool RemoveUnexpectedFilesFromCacheRoot(const base::FilePath& cache_root) {
bool success = true;
base::FileEnumerator unexpected_files(
cache_root, false /* recursive */,
base::FileEnumerator::FILES | base::FileEnumerator::SHOW_SYM_LINKS);
for (base::FilePath unexpected_file_path = unexpected_files.Next();
!unexpected_file_path.empty();
unexpected_file_path = unexpected_files.Next()) {
LOG(INFO) << "Deleting file " << unexpected_file_path.value();
if (!base::DeleteFile(unexpected_file_path, false /* recursive */)) {
LOG(ERROR) << "Could not delete file " << unexpected_file_path.value();
success = false;
}
}
return success;
}
// Returns if |file_name| matches the |pattern|.
bool IsMatch(const base::FilePath::StringType& file_name,
const base::FilePath::StringType& pattern) {
return fnmatch(pattern.c_str(), file_name.c_str(), FNM_NOESCAPE) == 0;
}
// Parses contents of the attributes JSON file and verifies that last access
// time of the package was at most 30 days ago.
bool IsAccessTimeValid(const base::StringPiece& json_message) {
std::string error_msg;
int error_code = 0;
const std::unique_ptr<base::Value> root(base::JSONReader::ReadAndReturnError(
json_message, base::JSON_PARSE_RFC, &error_code, &error_msg));
if (!root.get()) {
LOG(ERROR) << "Reading attributes JSON failed (error code: " << error_code
<< "; error message: " << error_msg << ").";
return false;
}
base::DictionaryValue* root_dict = nullptr;
if (!root->GetAsDictionary(&root_dict)) {
LOG(ERROR) << "Could not interpret the JSON as a dictionary.";
return false;
}
std::string atime_str;
if (!root_dict->GetString(kKeyAttributesAtime, &atime_str)) {
LOG(ERROR) << "Could not read the value of the access time with the "
<< kKeyAttributesAtime << " key.";
return false;
}
base::Time atime;
if (!base::Time::FromString(atime_str.c_str(), &atime)) {
LOG(ERROR) << "Can not parse the date: " << atime_str;
return false;
}
const base::TimeDelta age = base::Time::Now() - atime;
return age < kValidityPeriod;
}
// Verifies that package directory contains all the necessary files, does
// not contain any extra files/directories and was accessed within last 30 days.
// Returns true if the package is valid and should be kept in the cache.
bool IsPackageValid(const base::FilePath& package_path) {
// Package directory must contain:
// 1. One .apk file
// 2. One attr.json file
// 3. No or one main .obb file.
// 4. No or one patch .obb file.
// 5. No other files or directories.
base::FileEnumerator files(package_path, false /* recursive */,
base::FileEnumerator::DIRECTORIES |
base::FileEnumerator::FILES |
base::FileEnumerator::SHOW_SYM_LINKS);
int apk_count = 0;
int attr_count = 0;
int main_obb_count = 0;
int patch_obb_count = 0;
const std::string package_name = package_path.BaseName().value();
const std::string apk_file_name = package_name + kApkExtension;
const std::string main_obb_file_name =
std::string(kMainObbPrefix) + "*" + package_name + kObbExtension;
const std::string patch_obb_file_name =
std::string(kPatchObbPrefix) + "*" + package_name + kObbExtension;
for (base::FilePath file_path = files.Next(); !file_path.empty();
file_path = files.Next()) {
if (base::DirectoryExists(file_path)) {
LOG(INFO) << "There are directories in " << package_path.value();
return false;
}
const base::FilePath::StringType file_name = file_path.BaseName().value();
if (IsMatch(file_name, apk_file_name)) {
apk_count++;
} else if (IsMatch(file_name, kAttrJson)) {
attr_count++;
} else if (IsMatch(file_name, main_obb_file_name)) {
main_obb_count++;
} else if (IsMatch(file_name, patch_obb_file_name)) {
patch_obb_count++;
} else {
LOG(INFO) << package_name << " contains unnecessary files.";
return false;
}
}
if (apk_count != 1) {
LOG(INFO) << "Number of APK files is not equal to 1 in " << package_name;
return false;
}
if (attr_count != 1) {
LOG(INFO) << "Number of JSON attributes files is " << attr_count
<< " which not equal to 1 in " << package_name;
return false;
}
if (main_obb_count > 1) {
LOG(INFO) << "Number of patch OBB files is " << main_obb_count
<< ", which greater then 1 in " << package_name;
return false;
}
if (patch_obb_count > 1) {
LOG(INFO) << "Number of patch OBB files is " << patch_obb_count
<< ", which greater then 1 in " << package_name;
return false;
}
const base::FilePath attr_file_path = package_path.Append(kAttrJson);
std::string attr_json_contents;
if (!base::ReadFileToString(attr_file_path, &attr_json_contents)) {
LOG(ERROR) << "Could not read the attributes file.";
return false;
}
return IsAccessTimeValid(attr_json_contents);
}
} // namespace
bool Clean(const base::FilePath& cache_path) {
if (!base::DirectoryExists(cache_path)) {
LOG(ERROR) << "APK cache directory " << cache_path.value()
<< " does not exist";
return false;
}
bool success = RemoveUnexpectedFilesFromCacheRoot(cache_path);
base::FileEnumerator packages(
cache_path, false /* recursive */,
base::FileEnumerator::DIRECTORIES | base::FileEnumerator::SHOW_SYM_LINKS);
for (base::FilePath package_path = packages.Next(); !package_path.empty();
package_path = packages.Next()) {
if (!IsPackageValid(package_path)) {
if (!base::DeleteFile(package_path, true /* recursive */)) {
LOG(ERROR) << "Error deletion path " << package_path.value();
success = false;
}
} else {
LOG(INFO) << "Package " << package_path.value() << " looks OK.";
}
}
return success;
}
} // namespace apk_cache