chromium/chrome/browser/extensions/updater/local_extension_cache.h

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef CHROME_BROWSER_EXTENSIONS_UPDATER_LOCAL_EXTENSION_CACHE_H_
#define CHROME_BROWSER_EXTENSIONS_UPDATER_LOCAL_EXTENSION_CACHE_H_

#include <stddef.h>
#include <stdint.h>

#include <map>
#include <memory>
#include <set>
#include <string>

#include "base/files/file_path.h"
#include "base/functional/callback_forward.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "base/version.h"

namespace base {
class SequencedTaskRunner;
}

namespace extensions {

// Cache .crx files in some local dir for future use. Cache keeps only latest
// version of the extensions. Only one instance of LocalExtensionCache can work
// with the same directory. But LocalExtensionCache instance can be shared
// between multiple clients. Public interface can be used only from UI thread.
class LocalExtensionCache {
 public:
  // Callback invoked on UI thread when PutExtension is completed.
  using PutExtensionCallback =
      base::OnceCallback<void(const base::FilePath& file_path,
                              bool file_ownership_passed)>;

  // |cache_dir| - directory that will be used for caching CRX files.
  // |max_cache_size| - maximum disk space that cache can use, 0 means no limit.
  // |max_cache_age| - maximum age that unused item can be kept in cache, 0 age
  // means that all unused cache items will be removed on Shutdown.
  // All file I/O is done via the |backend_task_runner|.
  LocalExtensionCache(
      const base::FilePath& cache_dir,
      uint64_t max_cache_size,
      const base::TimeDelta& max_cache_age,
      const scoped_refptr<base::SequencedTaskRunner>& backend_task_runner);

  LocalExtensionCache(const LocalExtensionCache&) = delete;
  LocalExtensionCache& operator=(const LocalExtensionCache&) = delete;

  ~LocalExtensionCache();

  // Name of flag file that indicates that cache is ready (import finished).
  static const char kCacheReadyFlagFileName[];
  // Name of the file that lists invalid cache that should be removed.
  static const char kInvalidCacheIdsFileName[];

  // Initialize cache. If |wait_for_cache_initialization| is |true|, the cache
  // contents will not be read until a flag file appears in the cache directory,
  // signaling that the cache is ready. The |callback| is called when cache is
  // ready and cache dir content was already checked.
  void Init(bool wait_for_cache_initialization, base::OnceClosure callback);

  // Shut down the cache. The |callback| will be invoked when the cache has shut
  // down completely and there are no more pending file I/O operations.
  void Shutdown(base::OnceClosure callback);

  // If extension with |id| and |expected_hash| exists in the cache (or there
  // is an extension with the same |id|, but without expected hash sum),
  // returns |true|, |file_path| and |version| for the found extension.
  // If |file_path| was requested, then extension will be marked as used with
  // current timestamp.
  bool GetExtension(const std::string& id,
                    const std::string& expected_hash,
                    base::FilePath* file_path,
                    std::string* version);

  // Returns |true| if there is a file with |id| and |expected_hash| in the
  // cache, and its hash sum is actually empty. After removing it from cache and
  // re-downloading, the new entry will have some non-empty hash sum.
  bool ShouldRetryDownload(const std::string& id,
                           const std::string& expected_hash);

  // Put extension with |id|, |version| and |expected_hash| into local cache.
  // Older version in the cache will be deleted on next run so it can be safely
  // used. Extension will be marked as used with current timestamp. The file
  // will be available via GetExtension when |callback| is called. PutExtension
  // may get ownership of |file_path| or return it back via |callback|.
  void PutExtension(const std::string& id,
                    const std::string& expected_hash,
                    const base::FilePath& file_path,
                    const base::Version& version,
                    PutExtensionCallback callback);

  // Remove extension with |id| and |expected_hash| from local cache,
  // corresponding crx file will be removed from disk too. If |expected_hash| is
  // empty, all files corresponding to that |id| will be removed.
  bool RemoveExtension(const std::string& id, const std::string& expected_hash);

  // Mark extension with |id| as obsolete and schedule removal for the next
  // initialization. Returns true if removal was scheduled successfully.
  bool RemoveOnNextInit(const std::string& id);

  // Return cache statistics. Returns |false| if cache is not ready.
  bool GetStatistics(uint64_t* cache_size, size_t* extensions_count);

  // Outputs properly formatted extension file name, as it will be stored in
  // cache. If |expected_hash| is empty, it will be <id>-<version>.crx,
  // otherwise the name format is <id>-<version>-<hash>.crx.
  static std::string ExtensionFileName(const std::string& id,
                                       const std::string& version,
                                       const std::string& expected_hash);

  bool is_ready() const { return state_ == kReady; }
  bool is_uninitialized() const { return state_ == kUninitialized; }
  bool is_shutdown() const { return state_ == kShutdown; }

  // For tests only!
  void SetCacheStatusPollingDelayForTests(const base::TimeDelta& delay);

 private:
  struct CacheItemInfo {
    base::Version version;
    std::string expected_hash;
    base::Time last_used;
    uint64_t size;
    base::FilePath file_path;

    CacheItemInfo(const base::Version& version,
                  const std::string& expected_hash,
                  const base::Time& last_used,
                  uint64_t size,
                  const base::FilePath& file_path);
    CacheItemInfo(const CacheItemInfo& other);
    ~CacheItemInfo();
  };
  typedef std::multimap<std::string, CacheItemInfo> CacheMap;
  typedef std::pair<CacheMap::iterator, CacheMap::iterator> CacheHit;

  enum State {
    kUninitialized,
    kWaitInitialization,
    kReady,
    kShutdown
  };

  // Helper function that searches the cache map for an extension with the
  // specified |id| and |expected_hash|. If there is an extension with empty
  // hash in the map, it will be returned. If |expected_hash| is empty, returns
  // the first extension with the same |id|.
  static CacheMap::iterator FindExtension(CacheMap& cache,
                                          const std::string& id,
                                          const std::string& expected_hash);

  // Helper function that compares a cache entry (typically returned from
  // FindExtension) with an incoming |version| and |expected_hash|. Comparison
  // is based on the version number (newer is better) and hash sum (it is
  // better to have a file with an expected hash sum than without it).
  // Return value of this function is |true| if we already have a 'better'
  // entry in cache (considering both version number and hash sum), and the
  // value of |compare| is set to the version number comparison result (as
  // returned by Version::CompareTo).
  static bool NewerOrSame(const CacheMap::iterator& entry,
                          const base::Version& version,
                          const std::string& expected_hash,
                          int* compare);

  // Helper function that checks if there is already a newer version of the
  // extension we want to add to the cache, or if there is already a file with a
  // hash sum (and we are trying to add one without it), or vice versa. Keeps
  // the invariant of having only one version of each extension, and either only
  // unhashed (single) or only hashed (multiple) variants of that version.
  // |delete_files| specifies if this function is called on startup (in which
  // case we will clean up files we don't need), or on extension install.
  // Returns cache.end() if the extension is already cached, or an iterator to
  // the inserted cache entry otherwise.
  static CacheMap::iterator InsertCacheEntry(CacheMap& cache,
                                             const std::string& id,
                                             const CacheItemInfo& info,
                                             const bool delete_files);

  // Remove extension at a specified iterator. This is necessary because
  // removing an extension by |id| and |expected_hash| taken by reference from
  // an iterator leads to use-after-free. On the other hand, when passing the
  // iterator itself we avoid lookup as such, at all.
  // For external calls from RemoveExtension without expected hash we will
  // ignore the hash in iterator by setting |match_hash| to false.
  bool RemoveExtensionAt(const CacheMap::iterator& it, bool match_hash);

  // Sends BackendCheckCacheStatus task on backend thread.
  void CheckCacheStatus(base::OnceClosure callback);

  // Checks whether a flag file exists in the |cache_dir|, indicating that the
  // cache is ready. This method is invoked via the |backend_task_runner_| and
  // posts its result back to the |local_cache| on the UI thread.
  static void BackendCheckCacheStatus(
      base::WeakPtr<LocalExtensionCache> local_cache,
      const base::FilePath& cache_dir,
      base::OnceClosure callback);

  // Invoked on the UI thread after checking whether the cache is ready. If the
  // cache is not ready yet, posts a delayed task that will repeat the check,
  // thus polling for cache readiness.
  void OnCacheStatusChecked(bool ready, base::OnceClosure callback);

  // Checks the cache contents. This is a helper that invokes the actual check
  // by posting to the |backend_task_runner_|.
  void CheckCacheContents(base::OnceClosure callback);

  // Checks the cache contents. This method is invoked via the
  // |backend_task_runner_| and posts back a list of cache entries to the
  // |local_cache| on the UI thread.
  static void BackendCheckCacheContents(
      base::WeakPtr<LocalExtensionCache> local_cache,
      const base::FilePath& cache_dir,
      base::OnceClosure callback);

  // Helper for BackendCheckCacheContents() that updates |cache_content|.
  static void BackendCheckCacheContentsInternal(
      const base::FilePath& cache_dir,
      CacheMap* cache_content);

  // Invoked when the cache content on disk has been checked. |cache_content|
  // contains all the currently valid crx files in the cache.
  void OnCacheContentsChecked(std::unique_ptr<CacheMap> cache_content,
                              base::OnceClosure callback);

  // Update timestamp for the file to mark it as "used". This method is invoked
  // via the |backend_task_runner_|.
  static void BackendMarkFileUsed(const base::FilePath& file_path,
                                  const base::Time& time);

  // Installs the downloaded crx file at |path| in the |cache_dir|. This method
  // is invoked via the |backend_task_runner_|.
  static void BackendInstallCacheEntry(
      base::WeakPtr<LocalExtensionCache> local_cache,
      const base::FilePath& cache_dir,
      const std::string& id,
      const std::string& expected_hash,
      const base::FilePath& file_path,
      const base::Version& version,
      PutExtensionCallback callback);

  // Invoked on the UI thread when a new entry has been installed in the cache.
  void OnCacheEntryInstalled(const std::string& id,
                             const CacheItemInfo& info,
                             bool was_error,
                             PutExtensionCallback callback);

  // Remove cached crx files(all versions) under |cached_dir| for extension with
  // |id|. This method is invoked via the |backend_task_runner_|.
  static void BackendRemoveCacheEntry(const base::FilePath& cache_dir,
                                      const std::string& expected_hash,
                                      const std::string& id);

  // Appends the extension id to |kInvalidCacheIdsFileName| file so that it is
  // deleted on the next startup and a new version is retrieved.
  static void BackendMarkCacheInvalid(
      base::WeakPtr<LocalExtensionCache> local_cache,
      const base::FilePath& cache_dir,
      const std::string& extension_id);

  // Invoked on the UI thread if BackendMarkCacheInvalid() fails.
  void OnMarkCacheInvalidFailed(const std::string& extension_id);

  // Gets extension ids from |kInvalidCacheIdsFileName| file.
  static std::set<std::string> BackendGetInvalidCache(
      const base::FilePath& cache_dir);

  // Compare two cache items returns true if first item is older.
  static bool CompareCacheItemsAge(const CacheMap::iterator& lhs,
                                   const CacheMap::iterator& rhs);

  // Calculate which files need to be deleted and schedule files deletion.
  void CleanUp();

  // Path to the directory where the extension cache is stored.
  base::FilePath cache_dir_;

  // Maximum size of cache dir on disk.
  uint64_t max_cache_size_;

  // Minimal age of unused item in cache, items prior to this age will be
  // deleted on shutdown.
  base::Time min_cache_age_;

  // Task runner for executing file I/O tasks.
  scoped_refptr<base::SequencedTaskRunner> backend_task_runner_;

  // Track state of the instance.
  State state_;

  // This contains info about all cached extensions.
  CacheMap cached_extensions_;

  // This contains extension ids of invalid cache that should be removed on the
  // next initialization.
  std::set<std::string> invalid_cache_ids_;

  // Delay between polling cache status.
  base::TimeDelta cache_status_polling_delay_;

  // Weak factory for callbacks from the backend and delayed tasks.
  base::WeakPtrFactory<LocalExtensionCache> weak_ptr_factory_{this};
};

}  // namespace extensions

#endif  // CHROME_BROWSER_EXTENSIONS_UPDATER_LOCAL_EXTENSION_CACHE_H_