// Copyright (c) 2012 The Chromium 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 "chrome/common/mac/mock_launchd.h"

#include <CoreFoundation/CoreFoundation.h>
#include <sys/socket.h>
#include <sys/un.h>

#include "base/basictypes.h"
#include "base/file_util.h"
#include "base/files/file_path.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/common/chrome_version_info.h"
#include "chrome/common/mac/launchd.h"
#include "chrome/common/service_process_util.h"
#include "testing/gtest/include/gtest/gtest.h"

static sockaddr_un* throwaway_sockaddr_un;
static const size_t kMaxPipeNameLength =
    sizeof(throwaway_sockaddr_un->sun_path);

// static
bool MockLaunchd::MakeABundle(const base::FilePath& dst,
                              const std::string& name,
                              base::FilePath* bundle_root,
                              base::FilePath* executable) {
  *bundle_root = dst.Append(name + std::string(".app"));
  base::FilePath contents = bundle_root->AppendASCII("Contents");
  base::FilePath mac_os = contents.AppendASCII("MacOS");
  *executable = mac_os.Append(name);
  base::FilePath info_plist = contents.Append("Info.plist");

  if (!base::CreateDirectory(mac_os)) {
    return false;
  }
  const char *data = "#! testbundle\n";
  int len = strlen(data);
  if (base::WriteFile(*executable, data, len) != len) {
    return false;
  }
  if (chmod(executable->value().c_str(), 0555) != 0) {
    return false;
  }

  chrome::VersionInfo version_info;

  const char* info_plist_format =
      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
      "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" "
          "\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n"
      "<plist version=\"1.0\">\n"
      "<dict>\n"
      "  <key>CFBundleDevelopmentRegion</key>\n"
      "  <string>English</string>\n"
      "  <key>CFBundleExecutable</key>\n"
      "  <string>%s</string>\n"
      "  <key>CFBundleIdentifier</key>\n"
      "  <string>com.test.%s</string>\n"
      "  <key>CFBundleInfoDictionaryVersion</key>\n"
      "  <string>6.0</string>\n"
      "  <key>CFBundleShortVersionString</key>\n"
      "  <string>%s</string>\n"
      "  <key>CFBundleVersion</key>\n"
      "  <string>1</string>\n"
      "</dict>\n"
      "</plist>\n";
  std::string info_plist_data =
      base::StringPrintf(info_plist_format,
                         name.c_str(),
                         name.c_str(),
                         version_info.Version().c_str());
  len = info_plist_data.length();
  if (base::WriteFile(info_plist, info_plist_data.c_str(), len) != len) {
    return false;
  }
  const UInt8* bundle_root_path =
      reinterpret_cast<const UInt8*>(bundle_root->value().c_str());
  base::ScopedCFTypeRef<CFURLRef> url(
      CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault,
                                              bundle_root_path,
                                              bundle_root->value().length(),
                                              true));
  base::ScopedCFTypeRef<CFBundleRef> bundle(
      CFBundleCreate(kCFAllocatorDefault, url));
  return bundle.get();
}

MockLaunchd::MockLaunchd(const base::FilePath& file,
                         base::MessageLoop* loop,
                         bool create_socket,
                         bool as_service)
    : file_(file),
      message_loop_(loop),
      create_socket_(create_socket),
      as_service_(as_service),
      restart_called_(false),
      remove_called_(false),
      checkin_called_(false),
      write_called_(false),
      delete_called_(false) {
  std::string pipe_suffix("_SOCKET");
  base::FilePath socket_path = file_;
  while (socket_path.value().length() + pipe_suffix.length() >
         kMaxPipeNameLength - 2) {
    socket_path = socket_path.DirName();
  }
  pipe_name_ = socket_path.value() + pipe_suffix;
}

MockLaunchd::~MockLaunchd() {
}

CFDictionaryRef MockLaunchd::CopyExports() {
  if (!create_socket_) {
    ADD_FAILURE();
    return NULL;
  }

  CFStringRef env_var =
      base::mac::NSToCFCast(GetServiceProcessLaunchDSocketEnvVar());
  base::ScopedCFTypeRef<CFStringRef> socket_path(CFStringCreateWithCString(
      kCFAllocatorDefault, pipe_name_.c_str(), kCFStringEncodingUTF8));
  const void *keys[] = { env_var };
  const void *values[] = { socket_path };
  COMPILE_ASSERT(arraysize(keys) == arraysize(values), array_sizes_must_match);
  return CFDictionaryCreate(kCFAllocatorDefault,
                            keys,
                            values,
                            arraysize(keys),
                            &kCFTypeDictionaryKeyCallBacks,
                            &kCFTypeDictionaryValueCallBacks);
}

CFDictionaryRef MockLaunchd::CopyJobDictionary(CFStringRef label) {
  if (!as_service_) {
    scoped_ptr<MultiProcessLock> running_lock(
        TakeNamedLock(pipe_name_, false));
    if (running_lock.get())
      return NULL;
  }

  CFStringRef program = CFSTR(LAUNCH_JOBKEY_PROGRAM);
  CFStringRef program_pid = CFSTR(LAUNCH_JOBKEY_PID);
  const void *keys[] = { program, program_pid };
  base::ScopedCFTypeRef<CFStringRef> path(
      base::SysUTF8ToCFStringRef(file_.value()));
  int process_id = base::GetCurrentProcId();
  base::ScopedCFTypeRef<CFNumberRef> pid(
      CFNumberCreate(NULL, kCFNumberIntType, &process_id));
  const void *values[] = { path, pid };
  COMPILE_ASSERT(arraysize(keys) == arraysize(values), array_sizes_must_match);
  return CFDictionaryCreate(kCFAllocatorDefault,
                            keys,
                            values,
                            arraysize(keys),
                            &kCFTypeDictionaryKeyCallBacks,
                            &kCFTypeDictionaryValueCallBacks);
}

CFDictionaryRef MockLaunchd::CopyDictionaryByCheckingIn(CFErrorRef* error) {
  checkin_called_ = true;
  CFStringRef program = CFSTR(LAUNCH_JOBKEY_PROGRAM);
  CFStringRef program_args = CFSTR(LAUNCH_JOBKEY_PROGRAMARGUMENTS);
  base::ScopedCFTypeRef<CFStringRef> path(
      base::SysUTF8ToCFStringRef(file_.value()));
  const void *array_values[] = { path.get() };
  base::ScopedCFTypeRef<CFArrayRef> args(CFArrayCreate(
      kCFAllocatorDefault, array_values, 1, &kCFTypeArrayCallBacks));

  if (!create_socket_) {
    const void *keys[] = { program, program_args };
    const void *values[] = { path, args };
    COMPILE_ASSERT(arraysize(keys) == arraysize(values),
                   array_sizes_must_match);
    return CFDictionaryCreate(kCFAllocatorDefault,
                              keys,
                              values,
                              arraysize(keys),
                              &kCFTypeDictionaryKeyCallBacks,
                              &kCFTypeDictionaryValueCallBacks);
  }

  CFStringRef socket_key = CFSTR(LAUNCH_JOBKEY_SOCKETS);
  int local_pipe = -1;
  EXPECT_TRUE(as_service_);

  // Create unix_addr structure.
  struct sockaddr_un unix_addr = {0};
  unix_addr.sun_family = AF_UNIX;
  size_t path_len =
      base::strlcpy(unix_addr.sun_path, pipe_name_.c_str(), kMaxPipeNameLength);
  DCHECK_EQ(pipe_name_.length(), path_len);
  unix_addr.sun_len = SUN_LEN(&unix_addr);

  CFSocketSignature signature;
  signature.protocolFamily = PF_UNIX;
  signature.socketType = SOCK_STREAM;
  signature.protocol = 0;
  size_t unix_addr_len = offsetof(struct sockaddr_un,
                                  sun_path) + path_len + 1;
  base::ScopedCFTypeRef<CFDataRef> address(
      CFDataCreate(NULL, reinterpret_cast<UInt8*>(&unix_addr), unix_addr_len));
  signature.address = address;

  CFSocketRef socket =
      CFSocketCreateWithSocketSignature(NULL, &signature, 0, NULL, NULL);

  local_pipe = CFSocketGetNative(socket);
  EXPECT_NE(-1, local_pipe);
  if (local_pipe == -1) {
    if (error) {
      *error = CFErrorCreate(kCFAllocatorDefault, kCFErrorDomainPOSIX,
                             errno, NULL);
    }
    return NULL;
  }

  base::ScopedCFTypeRef<CFNumberRef> socket_fd(
      CFNumberCreate(NULL, kCFNumberIntType, &local_pipe));
  const void *socket_array_values[] = { socket_fd };
  base::ScopedCFTypeRef<CFArrayRef> sockets(CFArrayCreate(
      kCFAllocatorDefault, socket_array_values, 1, &kCFTypeArrayCallBacks));
  CFStringRef socket_dict_key = CFSTR("ServiceProcessSocket");
  const void *socket_keys[] = { socket_dict_key };
  const void *socket_values[] = { sockets };
  COMPILE_ASSERT(arraysize(socket_keys) == arraysize(socket_values),
                 socket_array_sizes_must_match);
  base::ScopedCFTypeRef<CFDictionaryRef> socket_dict(
      CFDictionaryCreate(kCFAllocatorDefault,
                         socket_keys,
                         socket_values,
                         arraysize(socket_keys),
                         &kCFTypeDictionaryKeyCallBacks,
                         &kCFTypeDictionaryValueCallBacks));
  const void *keys[] = { program, program_args, socket_key };
  const void *values[] = { path, args, socket_dict };
  COMPILE_ASSERT(arraysize(keys) == arraysize(values), array_sizes_must_match);
  return CFDictionaryCreate(kCFAllocatorDefault,
                            keys,
                            values,
                            arraysize(keys),
                            &kCFTypeDictionaryKeyCallBacks,
                            &kCFTypeDictionaryValueCallBacks);
}

bool MockLaunchd::RemoveJob(CFStringRef label, CFErrorRef* error) {
  remove_called_ = true;
  message_loop_->PostTask(FROM_HERE, base::MessageLoop::QuitClosure());
  return true;
}

bool MockLaunchd::RestartJob(Domain domain,
                             Type type,
                             CFStringRef name,
                             CFStringRef session_type) {
  restart_called_ = true;
  message_loop_->PostTask(FROM_HERE, base::MessageLoop::QuitClosure());
  return true;
}

CFMutableDictionaryRef MockLaunchd::CreatePlistFromFile(
    Domain domain,
    Type type,
    CFStringRef name)  {
  base::ScopedCFTypeRef<CFDictionaryRef> dict(CopyDictionaryByCheckingIn(NULL));
  return CFDictionaryCreateMutableCopy(kCFAllocatorDefault, 0, dict);
}

bool MockLaunchd::WritePlistToFile(Domain domain,
                                   Type type,
                                   CFStringRef name,
                                   CFDictionaryRef dict) {
  write_called_ = true;
  return true;
}

bool MockLaunchd::DeletePlist(Domain domain,
                              Type type,
                              CFStringRef name) {
  delete_called_ = true;
  return true;
}

void MockLaunchd::SignalReady() {
  ASSERT_TRUE(as_service_);
  running_lock_.reset(TakeNamedLock(pipe_name_, true));
}