chromium/chrome/browser/web_applications/os_integration/mac/bundle_info_plist.mm

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

#import "chrome/browser/web_applications/os_integration/mac/bundle_info_plist.h"

#import <Cocoa/Cocoa.h>

#include <list>
#include <string>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/strings/sys_string_conversions.h"
#include "base/version.h"
#include "chrome/common/chrome_paths.h"
#import "chrome/common/mac/app_mode_common.h"
#include "url/gurl.h"

namespace web_app {

// static
std::list<BundleInfoPlist> BundleInfoPlist::GetAllInPath(
    const base::FilePath& apps_path,
    bool recursive) {
  std::list<BundleInfoPlist> bundles;
  base::FileEnumerator enumerator(apps_path, recursive,
                                  base::FileEnumerator::DIRECTORIES);
  for (base::FilePath shim_path = enumerator.Next(); !shim_path.empty();
       shim_path = enumerator.Next()) {
    bundles.emplace_back(shim_path);
  }
  return bundles;
}

// static
std::list<BundleInfoPlist> BundleInfoPlist::SearchForBundlesById(
    const std::string& bundle_id,
    const base::FilePath& apps_path) {
  std::list<BundleInfoPlist> infos;

  // First search using LaunchServices.
  NSArray* bundle_urls;
  if (@available(macOS 12.0, *)) {
    bundle_urls = [NSWorkspace.sharedWorkspace
        URLsForApplicationsWithBundleIdentifier:base::SysUTF8ToNSString(
                                                    bundle_id)];
  } else {
    bundle_urls = base::apple::CFToNSOwnershipCast(
        LSCopyApplicationURLsForBundleIdentifier(
            base::SysUTF8ToCFStringRef(bundle_id).get(), /*outError=*/nullptr));
  }
  for (NSURL* url in bundle_urls) {
    base::FilePath bundle_path = base::apple::NSURLToFilePath(url);
    BundleInfoPlist info(bundle_path);
    if (!info.IsForCurrentUserDataDir()) {
      continue;
    }
    infos.push_back(info);
  }
  if (!infos.empty()) {
    return infos;
  }

  // LaunchServices can fail to locate a recently-created bundle. Search
  // for an app in the applications folder to handle this case.
  // https://crbug.com/937703
  infos = GetAllInPath(apps_path,
                       /*recursive=*/true);
  for (auto it = infos.begin(); it != infos.end();) {
    const BundleInfoPlist& info = *it;
    if (info.GetBundleId() == bundle_id && info.IsForCurrentUserDataDir()) {
      ++it;
    } else {
      infos.erase(it++);
    }
  }
  return infos;
}

BundleInfoPlist::BundleInfoPlist(const base::FilePath& bundle_path)
    : bundle_path_(bundle_path) {
  plist_ = [NSDictionary dictionaryWithContentsOfURL:GetPlistURL(bundle_path_)
                                               error:nil];
}
BundleInfoPlist::BundleInfoPlist(const BundleInfoPlist& other) = default;
BundleInfoPlist& BundleInfoPlist::operator=(const BundleInfoPlist& other) =
    default;
BundleInfoPlist::~BundleInfoPlist() = default;

bool BundleInfoPlist::IsForCurrentUserDataDir() const {
  base::FilePath user_data_dir;
  base::PathService::Get(chrome::DIR_USER_DATA, &user_data_dir);
  DCHECK(!user_data_dir.empty());
  return base::StartsWith(
      base::SysNSStringToUTF8(plist_[app_mode::kCrAppModeUserDataDirKey]),
      user_data_dir.value(), base::CompareCase::SENSITIVE);
}

bool BundleInfoPlist::IsForProfile(
    const base::FilePath& test_profile_path) const {
  std::string profile_path =
      base::SysNSStringToUTF8(plist_[app_mode::kCrAppModeProfileDirKey]);
  return profile_path == test_profile_path.BaseName().value();
}

base::FilePath BundleInfoPlist::GetFullProfilePath() const {
  // Figure out the profile_path. Since the user_data_dir could contain the
  // path to the web app data dir.
  base::FilePath user_data_dir = base::apple::NSStringToFilePath(
      plist_[app_mode::kCrAppModeUserDataDirKey]);
  base::FilePath profile_base_name = base::apple::NSStringToFilePath(
      plist_[app_mode::kCrAppModeProfileDirKey]);
  if (user_data_dir.DirName().DirName().BaseName() == profile_base_name) {
    return user_data_dir.DirName().DirName();
  }
  return user_data_dir.Append(profile_base_name);
}

std::string BundleInfoPlist::GetExtensionId() const {
  return base::SysNSStringToUTF8(plist_[app_mode::kCrAppModeShortcutIDKey]);
}

std::string BundleInfoPlist::GetProfileName() const {
  return base::SysNSStringToUTF8(plist_[app_mode::kCrAppModeProfileNameKey]);
}

GURL BundleInfoPlist::GetURL() const {
  return GURL(
      base::SysNSStringToUTF8(plist_[app_mode::kCrAppModeShortcutURLKey]));
}

std::u16string BundleInfoPlist::GetTitle() const {
  return base::SysNSStringToUTF16(plist_[app_mode::kCrAppModeShortcutNameKey]);
}

base::Version BundleInfoPlist::GetVersion() const {
  NSString* version_string = plist_[app_mode::kCrBundleVersionKey];
  if (!version_string) {
    // Older bundles have the Chrome version in the following key.
    version_string = plist_[app_mode::kCFBundleShortVersionStringKey];
  }
  return base::Version(base::SysNSStringToUTF8(version_string));
}

std::string BundleInfoPlist::GetBundleId() const {
  return base::SysNSStringToUTF8(
      plist_[base::apple::CFToNSPtrCast(kCFBundleIdentifierKey)]);
}

// static
NSURL* BundleInfoPlist::GetPlistURL(const base::FilePath& bundle_path) {
  return base::apple::FilePathToNSURL(
      bundle_path.Append("Contents").Append("Info.plist"));
}

}  // namespace web_app