// 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.

// On Mac, shortcuts can't have command-line arguments. Instead, produce small
// app bundles which locate the Chromium framework and load it, passing the
// appropriate data. This is the code for such an app bundle. It should be kept
// minimal and do as little work as possible (with as much work done on
// framework side as possible).

#include <dlfcn.h>

#import <Cocoa/Cocoa.h>

#include "base/allocator/early_zone_registration_apple.h"
#include "base/apple/foundation_util.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/process/launch.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_switches.h"
#import "chrome/common/mac/app_mode_chrome_locator.h"
#include "chrome/common/mac/app_mode_common.h"

namespace {

const int kErrorReturnValue = 1;

typedef int (*StartFun)(const app_mode::ChromeAppModeInfo*);

int LoadFrameworkAndStart(int argc, char** argv) {
  base::CommandLine command_line(argc, argv);

  @autoreleasepool {
    // Get the current main bundle, i.e., that of the app loader that's running.
    NSBundle* app_bundle = NSBundle.mainBundle;
    if (!app_bundle) {
      NSLog(@"Couldn't get loader bundle");
      return kErrorReturnValue;
    }
    const base::FilePath app_mode_bundle_path =
        base::apple::NSStringToFilePath([app_bundle bundlePath]);

    // Get the bundle ID of the browser that created this app bundle.
    NSString* cr_bundle_id = base::apple::ObjCCast<NSString>(
        [app_bundle objectForInfoDictionaryKey:app_mode::kBrowserBundleIDKey]);
    if (!cr_bundle_id) {
      NSLog(@"Couldn't get browser bundle ID");
      return kErrorReturnValue;
    }

    // ** 1: Get path to outer Chrome bundle.
    base::FilePath cr_bundle_path;
    if (command_line.HasSwitch(app_mode::kLaunchedByChromeBundlePath)) {
      // If Chrome launched this app shim, and specified its bundle path on the
      // command line, use that.
      cr_bundle_path = command_line.GetSwitchValuePath(
          app_mode::kLaunchedByChromeBundlePath);
    } else {
      // Otherwise, search for a Chrome bundle to use.
      if (!app_mode::FindChromeBundle(cr_bundle_id, &cr_bundle_path)) {
        // TODO(https://crbug.com/944312): Display UI to inform the user of the
        // reason for failure.
        NSLog(@"Failed to locate browser bundle");
        return kErrorReturnValue;
      }
      if (cr_bundle_path.empty()) {
        NSLog(@"Browser bundle path unexpectedly empty");
        return kErrorReturnValue;
      }
    }

    // ** 2: Read the user data dir.
    base::FilePath user_data_dir;
    {
      // The user_data_dir for shims actually contains the app_data_path.
      // I.e. <user_data_dir>/<profile_dir>/Web Applications/_crx_extensionid/
      base::FilePath app_data_dir = base::apple::NSStringToFilePath([app_bundle
          objectForInfoDictionaryKey:app_mode::kCrAppModeUserDataDirKey]);
      user_data_dir = app_data_dir.DirName().DirName().DirName();
      NSLog(@"Using user data dir %s", user_data_dir.value().c_str());
      if (user_data_dir.empty())
        return kErrorReturnValue;
    }

    // ** 3: Read the Chrome executable, Chrome framework, and Chrome framework
    // dylib paths.
    app_mode::MojoIpczConfig mojo_ipcz_config =
        app_mode::MojoIpczConfig::kUseCommandLineFeatures;
    base::FilePath executable_path;
    base::FilePath framework_path;
    base::FilePath framework_dylib_path;
    if (command_line.HasSwitch(
            app_mode::kLaunchedByChromeFrameworkBundlePath) &&
        command_line.HasSwitch(app_mode::kLaunchedByChromeFrameworkDylibPath)) {
      // If Chrome launched this app shim, then it will specify the framework
      // path and version, as well as flags to enable or disable MojoIpcz as
      // needed. Do not populate `executable_path` (it is used to launch Chrome
      // if Chrome is not running, which is inapplicable here).
      framework_path = command_line.GetSwitchValuePath(
          app_mode::kLaunchedByChromeFrameworkBundlePath);
      framework_dylib_path = command_line.GetSwitchValuePath(
          app_mode::kLaunchedByChromeFrameworkDylibPath);
    } else {
      // Otherwise, read the version from the symbolic link in the user data
      // dir. If the version file does not exist, the version string will be
      // empty and app_mode::GetChromeBundleInfo will default to the latest
      // version, with MojoIpcz disabled.
      app_mode::ChromeConnectionConfig config;
      base::FilePath encoded_config;
      base::ReadSymbolicLink(
          user_data_dir.Append(app_mode::kRunningChromeVersionSymlinkName),
          &encoded_config);
      if (!encoded_config.empty()) {
        config =
            app_mode::ChromeConnectionConfig::DecodeFromPath(encoded_config);
        mojo_ipcz_config = config.is_mojo_ipcz_enabled
                               ? app_mode::MojoIpczConfig::kEnabled
                               : app_mode::MojoIpczConfig::kDisabled;
      }
      // If the version file does exist, it may have been left by a crashed
      // Chrome process. Ensure the process is still running.
      if (!config.framework_version.empty()) {
        NSArray* existing_chrome = [NSRunningApplication
            runningApplicationsWithBundleIdentifier:cr_bundle_id];
        if ([existing_chrome count] == 0) {
          NSLog(@"Disregarding framework version from symlink");
          config.framework_version.clear();
        } else {
          NSLog(@"Framework version from symlink %s",
                config.framework_version.c_str());
        }
      }
      if (!app_mode::GetChromeBundleInfo(
              cr_bundle_path, config.framework_version.c_str(),
              &executable_path, &framework_path, &framework_dylib_path)) {
        NSLog(@"Couldn't ready Chrome bundle info");
        return kErrorReturnValue;
      }
    }

    // Check if `executable_path` was overridden by tests via the command line.
    if (command_line.HasSwitch(app_mode::kLaunchChromeForTest)) {
      executable_path =
          command_line.GetSwitchValuePath(app_mode::kLaunchChromeForTest);
    }

    // ** 4: Read information from the Info.plist.
    // Read information about the this app shortcut from the Info.plist.
    // Don't check for null-ness on optional items.
    NSDictionary* info_plist = [app_bundle infoDictionary];
    if (!info_plist) {
      NSLog(@"Couldn't get loader Info.plist");
      return kErrorReturnValue;
    }

    const std::string app_mode_id =
        base::SysNSStringToUTF8(info_plist[app_mode::kCrAppModeShortcutIDKey]);
    if (!app_mode_id.size()) {
      NSLog(@"Couldn't get app shortcut ID");
      return kErrorReturnValue;
    }

    const std::string app_mode_name = base::SysNSStringToUTF8(
        info_plist[app_mode::kCrAppModeShortcutNameKey]);
    const std::string app_mode_url =
        base::SysNSStringToUTF8(info_plist[app_mode::kCrAppModeShortcutURLKey]);

    base::FilePath plist_user_data_dir = base::apple::NSStringToFilePath(
        info_plist[app_mode::kCrAppModeUserDataDirKey]);

    base::FilePath profile_dir = base::apple::NSStringToFilePath(
        info_plist[app_mode::kCrAppModeProfileDirKey]);

    // ** 5: Open the framework.
    StartFun ChromeAppModeStart = nullptr;
    NSLog(@"Using framework path %s", framework_path.value().c_str());
    NSLog(@"Loading framework dylib %s", framework_dylib_path.value().c_str());
    void* cr_dylib = dlopen(framework_dylib_path.value().c_str(), RTLD_LAZY);
    if (cr_dylib) {
      // Find the entry point.
      ChromeAppModeStart =
          (StartFun)dlsym(cr_dylib, APP_SHIM_ENTRY_POINT_NAME_STRING);
      if (!ChromeAppModeStart)
        NSLog(@"Couldn't get entry point: %s", dlerror());
    } else {
      NSLog(@"Couldn't load framework: %s", dlerror());
    }

    // ** 6: Fill in ChromeAppModeInfo and call into Chrome's framework.
    if (ChromeAppModeStart) {
      // Ensure that the strings pointed to by |info| outlive |info|.
      const std::string framework_path_utf8 = framework_path.AsUTF8Unsafe();
      const std::string cr_bundle_path_utf8 = cr_bundle_path.AsUTF8Unsafe();
      const std::string app_mode_bundle_path_utf8 =
          app_mode_bundle_path.AsUTF8Unsafe();
      const std::string plist_user_data_dir_utf8 =
          plist_user_data_dir.AsUTF8Unsafe();
      const std::string profile_dir_utf8 = profile_dir.AsUTF8Unsafe();
      app_mode::ChromeAppModeInfo info;
      info.argc = argc;
      info.argv = argv;
      info.chrome_framework_path = framework_path_utf8.c_str();
      info.chrome_outer_bundle_path = cr_bundle_path_utf8.c_str();
      info.app_mode_bundle_path = app_mode_bundle_path_utf8.c_str();
      info.app_mode_id = app_mode_id.c_str();
      info.app_mode_name = app_mode_name.c_str();
      info.app_mode_url = app_mode_url.c_str();
      info.user_data_dir = plist_user_data_dir_utf8.c_str();
      info.profile_dir = profile_dir_utf8.c_str();
      info.mojo_ipcz_config = mojo_ipcz_config;
      return ChromeAppModeStart(&info);
    }

    // If the shim was launched by chrome, simply quit. Chrome will detect that
    // the app shim has terminated, rebuild it (if it hadn't try to do so
    // already), and launch it again.
    if (executable_path.empty()) {
      NSLog(@"Loading Chrome failed, terminating");
      return kErrorReturnValue;
    }

    NSLog(@"Loading Chrome failed, launching Chrome with command line at %s",
          executable_path.value().c_str());
    base::CommandLine cr_command_line(executable_path);
    // The user_data_dir from the plist is actually the app data dir.
    cr_command_line.AppendSwitchPath(
        switches::kUserDataDir,
        plist_user_data_dir.DirName().DirName().DirName());
    // If the shim was launched directly (instead of by Chrome), first ask
    // Chrome to launch the app. Chrome will launch the shim again, the same
    // error might occur, after which chrome will try to regenerate the
    // shim.
    cr_command_line.AppendSwitchPath(switches::kProfileDirectory, profile_dir);
    cr_command_line.AppendSwitchASCII(switches::kAppId, app_mode_id);

    // If kLaunchChromeForTest was specified, this is a launch from a test.
    // In this case make sure to tell chrome to use a mock keychain, as
    // otherwise it might hang on startup.
    if (command_line.HasSwitch(app_mode::kLaunchChromeForTest)) {
      cr_command_line.AppendSwitch("use-mock-keychain");
    }

    // Launch the executable directly since base::mac::LaunchApplication doesn't
    // pass command line arguments if the application is already running.
    if (!base::LaunchProcess(cr_command_line, base::LaunchOptions())
             .IsValid()) {
      NSLog(@"Could not launch Chrome: %s",
            cr_command_line.GetCommandLineString().c_str());
      return kErrorReturnValue;
    }

    return 0;
  }
}

} // namespace

__attribute__((visibility("default")))
int main(int argc, char** argv) {
  // The static constructor in //base will have registered PartitionAlloc as the
  // default zone. Allow the //base instance in the main library to register it
  // as well. Otherwise we end up passing memory to free() which was allocated
  // by an unknown zone. See crbug.com/1274236 for details.
  partition_alloc::AllowDoublePartitionAllocZoneRegistration();

  base::CommandLine::Init(argc, argv);

  // Exit instead of returning to avoid the the removal of |main()| from stack
  // backtraces under tail call optimization.
  exit(LoadFrameworkAndStart(argc, argv));
}
