chromium/rlz/mac/lib/rlz_value_store_mac.mm

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

#include "rlz/mac/lib/rlz_value_store_mac.h"

#import <Foundation/Foundation.h>

#include <tuple>

#include "base/apple/foundation_util.h"
#include "base/check.h"
#include "base/files/file_path.h"
#include "base/notreached.h"
#include "base/strings/sys_string_conversions.h"
#include "rlz/lib/assert.h"
#include "rlz/lib/lib_values.h"
#include "rlz/lib/recursive_cross_process_lock_posix.h"
#include "rlz/lib/supplementary_branding.h"

using base::apple::ObjCCast;

namespace rlz_lib {

// These are written to disk and should not be changed.
NSString* const kPingTimeKey = @"pingTime";
NSString* const kAccessPointKey = @"accessPoints";
NSString* const kProductEventKey = @"productEvents";
NSString* const kStatefulEventKey = @"statefulEvents";

namespace {

NSString* GetNSProductName(Product product) {
  return base::SysUTF8ToNSString(GetProductName(product));
}

NSString* GetNSAccessPointName(AccessPoint p) {
  return base::SysUTF8ToNSString(GetAccessPointName(p));
}

// Retrieves a subdictionary in |p| for key |k|, creating it if necessary.
// If the dictionary contains an object for |k| that is not a mutable
// dictionary, that object is replaced with an empty mutable dictionary.
NSMutableDictionary* GetOrCreateDict(NSMutableDictionary* p, NSString* k) {
  NSMutableDictionary* d = ObjCCast<NSMutableDictionary>(p[k]);
  if (!d) {
    d = [NSMutableDictionary dictionary];
    p[k] = d;
  }
  return d;
}

}  // namespace

RlzValueStoreMac::RlzValueStoreMac(NSMutableDictionary* dict,
                                   NSString* plist_path)
    : dict_(dict), plist_path_(plist_path) {}

RlzValueStoreMac::~RlzValueStoreMac() = default;

bool RlzValueStoreMac::HasAccess(AccessType type) {
  switch (type) {
    case kReadAccess:
      return [NSFileManager.defaultManager isReadableFileAtPath:plist_path_];
    case kWriteAccess:
      return [NSFileManager.defaultManager isWritableFileAtPath:plist_path_];
  }
}

bool RlzValueStoreMac::WritePingTime(Product product, int64_t time) {
  ProductDict(product)[kPingTimeKey] = @(time);
  return true;
}

bool RlzValueStoreMac::ReadPingTime(Product product, int64_t* time) {
  if (NSNumber* n = ObjCCast<NSNumber>(ProductDict(product)[kPingTimeKey])) {
    *time = n.longLongValue;
    return true;
  }
  return false;
}

bool RlzValueStoreMac::ClearPingTime(Product product) {
  [ProductDict(product) removeObjectForKey:kPingTimeKey];
  return true;
}

bool RlzValueStoreMac::WriteAccessPointRlz(AccessPoint access_point,
                                           const char* new_rlz) {
  NSMutableDictionary* d = GetOrCreateDict(WorkingDict(), kAccessPointKey);
  d[GetNSAccessPointName(access_point)] = base::SysUTF8ToNSString(new_rlz);
  return true;
}

bool RlzValueStoreMac::ReadAccessPointRlz(AccessPoint access_point,
                                          char* rlz,
                                          size_t rlz_size) {
  // Reading a non-existent access point counts as success.
  if (NSDictionary* d =
          ObjCCast<NSDictionary>(WorkingDict()[kAccessPointKey])) {
    NSString* val = ObjCCast<NSString>(d[GetNSAccessPointName(access_point)]);
    if (!val) {
      if (rlz_size > 0) {
        rlz[0] = '\0';
      }
      return true;
    }

    std::string s = base::SysNSStringToUTF8(val);
    if (s.size() >= rlz_size) {
      rlz[0] = 0;
      ASSERT_STRING("GetAccessPointRlz: Insufficient buffer size");
      return false;
    }
    strncpy(rlz, s.c_str(), rlz_size);
    return true;
  }
  if (rlz_size > 0) {
    rlz[0] = '\0';
  }
  return true;
}

bool RlzValueStoreMac::ClearAccessPointRlz(AccessPoint access_point) {
  if (NSMutableDictionary* d =
          ObjCCast<NSMutableDictionary>(WorkingDict()[kAccessPointKey])) {
    [d removeObjectForKey:GetNSAccessPointName(access_point)];
  }
  return true;
}

bool RlzValueStoreMac::UpdateExistingAccessPointRlz(const std::string& brand) {
  return false;
}

bool RlzValueStoreMac::AddProductEvent(Product product, const char* event_rlz) {
  GetOrCreateDict(ProductDict(product),
                  kProductEventKey)[base::SysUTF8ToNSString(event_rlz)] = @YES;
  return true;
}

bool RlzValueStoreMac::ReadProductEvents(Product product,
                                         std::vector<std::string>* events) {
  if (NSDictionary* d =
          ObjCCast<NSDictionary>(ProductDict(product)[kProductEventKey])) {
    for (NSString* s in d) {
      events->push_back(base::SysNSStringToUTF8(s));
    }
    return true;
  }
  return true;
}

bool RlzValueStoreMac::ClearProductEvent(Product product,
                                         const char* event_rlz) {
  if (NSMutableDictionary* d = ObjCCast<NSMutableDictionary>(
          ProductDict(product)[kProductEventKey])) {
    [d removeObjectForKey:base::SysUTF8ToNSString(event_rlz)];
    return true;
  }
  return false;
}

bool RlzValueStoreMac::ClearAllProductEvents(Product product) {
  [ProductDict(product) removeObjectForKey:kProductEventKey];
  return true;
}

bool RlzValueStoreMac::AddStatefulEvent(Product product,
                                        const char* event_rlz) {
  GetOrCreateDict(ProductDict(product),
                  kStatefulEventKey)[base::SysUTF8ToNSString(event_rlz)] = @YES;
  return true;
}

bool RlzValueStoreMac::IsStatefulEvent(Product product, const char* event_rlz) {
  if (NSDictionary* d =
          ObjCCast<NSDictionary>(ProductDict(product)[kStatefulEventKey])) {
    return d[base::SysUTF8ToNSString(event_rlz)] != nil;
  }
  return false;
}

bool RlzValueStoreMac::ClearAllStatefulEvents(Product product) {
  [ProductDict(product) removeObjectForKey:kStatefulEventKey];
  return true;
}

void RlzValueStoreMac::CollectGarbage() {
  NOTIMPLEMENTED();
}

NSDictionary* RlzValueStoreMac::dictionary() {
  return dict_;
}

NSMutableDictionary* RlzValueStoreMac::WorkingDict() {
  std::string brand(SupplementaryBranding::GetBrand());
  if (brand.empty()) {
    return dict_;
  }

  NSString* brand_ns =
      [@"brand_" stringByAppendingString:base::SysUTF8ToNSString(brand)];

  return GetOrCreateDict(dict_, brand_ns);
}

NSMutableDictionary* RlzValueStoreMac::ProductDict(Product p) {
  return GetOrCreateDict(WorkingDict(), GetNSProductName(p));
}

namespace {

RecursiveCrossProcessLock g_recursive_lock =
    RECURSIVE_CROSS_PROCESS_LOCK_INITIALIZER;

// This is set during test execution, to write RLZ files into a temporary
// directory instead of the user's Application Support folder.
NSString* __strong g_test_folder;

// RlzValueStoreMac keeps its data in memory and only writes it to disk when
// ScopedRlzValueStoreLock goes out of scope. Hence, if several
// ScopedRlzValueStoreLocks are nested, they all need to use the same store
// object.

// This counts the nesting depth.
int g_lock_depth = 0;

// This is the store object that might be shared. Only set if g_lock_depth > 0.
RlzValueStoreMac* g_store_object = nullptr;

NSURL* CreateRlzDirectory() {
  NSArray* paths = NSSearchPathForDirectoriesInDomains(
      NSApplicationSupportDirectory, NSUserDomainMask, /*expandTilde=*/YES);
  NSString* folder = nil;
  if (paths.count > 0) {
    folder = ObjCCast<NSString>(paths[0]);
  }
  if (!folder) {
    folder = [@"~/Library/Application Support" stringByStandardizingPath];
  }
  folder = [folder stringByAppendingPathComponent:@"Google/RLZ"];

  if (g_test_folder) {
    folder = [g_test_folder stringByAppendingPathComponent:folder];
  }

  [NSFileManager.defaultManager createDirectoryAtPath:folder
                          withIntermediateDirectories:YES
                                           attributes:nil
                                                error:nil];
  return [NSURL fileURLWithPath:folder];
}

// Returns the path of the rlz plist store, also creates the parent directory
// path if it doesn't exist.
NSURL* RlzPlistPathURL() {
  NSString* const kRlzFile = @"RlzStore.plist";
  return [CreateRlzDirectory() URLByAppendingPathComponent:kRlzFile];
}

// Returns the path of the rlz lock file, also creates the parent directory
// path if it doesn't exist.
NSURL* RlzLockFileURL() {
  NSString* const kRlzLockfile = @"flockfile";
  return [CreateRlzDirectory() URLByAppendingPathComponent:kRlzLockfile];
}

}  // namespace

ScopedRlzValueStoreLock::ScopedRlzValueStoreLock() {
  bool got_distributed_lock = g_recursive_lock.TryGetCrossProcessLock(
      base::apple::NSURLToFilePath(RlzLockFileURL()));
  // At this point, we hold the in-process lock, no matter the value of
  // |got_distributed_lock|.

  ++g_lock_depth;

  if (!got_distributed_lock) {
    // Give up. |store_| isn't set, which signals to callers that acquiring
    // the lock failed. |g_recursive_lock| will be released by the
    // destructor.
    CHECK(!g_store_object);
    return;
  }

  if (g_lock_depth > 1) {
    // Reuse the already existing store object. Note that it can be NULL when
    // lock acquisition succeeded but the rlz data file couldn't be read.
    store_.reset(g_store_object);
    return;
  }

  CHECK(!g_store_object);

  NSURL* plist = RlzPlistPathURL();

  // Create an empty file if none exists yet.
  if (![NSFileManager.defaultManager fileExistsAtPath:plist.path
                                          isDirectory:nil]) {
    [[NSDictionary dictionary] writeToURL:plist error:nil];
  }

  NSMutableDictionary* dict =
      [NSMutableDictionary dictionaryWithContentsOfURL:plist];
  VERIFY(dict);

  if (dict) {
    store_.reset(new RlzValueStoreMac(dict, plist.path));
    g_store_object = (RlzValueStoreMac*)store_.get();
  }
}

ScopedRlzValueStoreLock::~ScopedRlzValueStoreLock() {
  --g_lock_depth;
  CHECK(g_lock_depth >= 0);

  if (g_lock_depth > 0) {
    // Other locks are still using store_, don't free it yet.
    std::ignore = store_.release();
    return;
  }

  if (store_.get()) {
    g_store_object = nullptr;

    NSDictionary* dict =
        static_cast<RlzValueStoreMac*>(store_.get())->dictionary();
    VERIFY([dict writeToURL:RlzPlistPathURL() error:nil]);
  }

  // Check that "store_ set" => "file_lock acquired". The converse isn't true,
  // for example if the rlz data file can't be read.
  if (store_.get()) {
    CHECK(g_recursive_lock.file_lock_ != -1);
  }
  if (g_recursive_lock.file_lock_ == -1) {
    CHECK(!store_.get());
  }

  g_recursive_lock.ReleaseLock();
}

RlzValueStore* ScopedRlzValueStoreLock::GetStore() {
  return store_.get();
}

namespace testing {

void SetRlzStoreDirectory(const base::FilePath& directory) {
  @autoreleasepool {
    if (directory.empty()) {
      g_test_folder = nil;
    } else {
      g_test_folder = base::apple::FilePathToNSString(directory);
    }
  }
}

std::string RlzStoreFilenameStr() {
  @autoreleasepool {
    return std::string(RlzPlistPathURL().fileSystemRepresentation);
  }
}

}  // namespace testing

}  // namespace rlz_lib