/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "nsMacUtilsImpl.h"

#include "base/command_line.h"
#include "base/process_util.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Logging.h"
#include "mozilla/Omnijar.h"
#include "nsCOMPtr.h"
#include "nsComponentManagerUtils.h"
#include "nsDirectoryServiceDefs.h"
#include "nsIFile.h"
#include "nsReadableUtils.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsThreadUtils.h"
#include "nsXULAppAPI.h"
#include "prenv.h"

#if defined(MOZ_SANDBOX)
#  include "mozilla/SandboxSettings.h"
#endif

#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#if defined(__aarch64__)
#  include <dlfcn.h>
#endif
#include <sys/sysctl.h>

using mozilla::StaticMutexAutoLock;
using namespace nsMacUtilsImpl;

static mozilla::LazyLogModule sMacUtilsLog("macutils");
#define LOG(args...) MOZ_LOG(sMacUtilsLog, mozilla::LogLevel::Debug, (args))

#if defined(MOZ_SANDBOX) || defined(__aarch64__)
// For thread safe setting/checking of sCachedAppPath
static StaticMutex sCachedAppPathMutex;

// Cache the appDir returned from GetAppPath to avoid doing I/O
static StaticAutoPtr<nsCString> sCachedAppPath
    MOZ_GUARDED_BY(sCachedAppPathMutex);
#endif

// The cached machine architectures of the .app bundle which can
// be multiple architectures for universal binaries.
static std::atomic<uint32_t> sBundleArchMaskAtomic = 0;

#if defined(__aarch64__)
// Limit XUL translation to one attempt
static std::atomic<bool> sIsXULTranslated = false;
#endif

// Info.plist key associated with the developer repo path
#define MAC_DEV_REPO_KEY "MozillaDeveloperRepoPath"
// Info.plist key associated with the developer repo object directory
#define MAC_DEV_OBJ_KEY "MozillaDeveloperObjPath"

// Workaround this constant not being available in the macOS SDK
#define kCFBundleExecutableArchitectureARM64 0x0100000c

enum TCSMStatus { TCSM_Unknown = 0, TCSM_Available, TCSM_Unavailable };

// Initialize with Unknown until we've checked if TCSM is available to set
static Atomic<TCSMStatus> sTCSMStatus(TCSM_Unknown);

#if defined(MOZ_SANDBOX) || defined(__aarch64__)

// Utility method to call ClearOnShutdown() on the main thread
static nsresult ClearCachedAppPathOnShutdown() {
  MOZ_ASSERT(NS_IsMainThread());
  ClearOnShutdown(&sCachedAppPath);
  return NS_OK;
}

// Get the path to the .app directory (aka bundle) for the parent process.
// When executing in the child process, this is the outer .app (such as
// Firefox.app) and not the inner .app containing the child process
// executable. We don't rely on the actual .app extension to allow for the
// bundle being renamed.
bool nsMacUtilsImpl::GetAppPath(nsCString& aAppPath) {
  StaticMutexAutoLock lock(sCachedAppPathMutex);
  if (sCachedAppPath) {
    aAppPath.Assign(*sCachedAppPath);
    return true;
  }

  nsAutoCString appPath;
  nsAutoCString appBinaryPath(
      (CommandLine::ForCurrentProcess()->argv()[0]).c_str());

  // The binary path resides within the .app dir in Contents/MacOS,
  // e.g., Firefox.app/Contents/MacOS/firefox. Search backwards in
  // the binary path for the end of .app path.
  auto pattern = "/Contents/MacOS/"_ns;
  nsAutoCString::const_iterator start, end;
  appBinaryPath.BeginReading(start);
  appBinaryPath.EndReading(end);
  if (CaseInsensitiveRFindInReadable(pattern, start, end)) {
    end = start;
    appBinaryPath.BeginReading(start);

    // If we're executing in a child process, get the parent .app path
    // by searching backwards once more. The child executable resides
    // in Firefox.app/Contents/MacOS/plugin-container/Contents/MacOS.
    if (!XRE_IsParentProcess()) {
      if (CaseInsensitiveRFindInReadable(pattern, start, end)) {
        end = start;
        appBinaryPath.BeginReading(start);
      } else {
        return false;
      }
    }

    appPath.Assign(Substring(start, end));
  } else {
    return false;
  }

  nsCOMPtr<nsIFile> app;
  nsresult rv = NS_NewNativeLocalFile(appPath, getter_AddRefs(app));
  if (NS_FAILED(rv)) {
    return false;
  }

  rv = app->Normalize();
  if (NS_FAILED(rv)) {
    return false;
  }
  app->GetNativePath(aAppPath);

  if (!sCachedAppPath) {
    sCachedAppPath = new nsCString(aAppPath);

    if (NS_IsMainThread()) {
      ClearCachedAppPathOnShutdown();
    } else {
      NS_DispatchToMainThread(
          NS_NewRunnableFunction("ClearCachedAppPathOnShutdown",
                                 [] { ClearCachedAppPathOnShutdown(); }));
    }
  }

  return true;
}

#endif /* MOZ_SANDBOX || __aarch64__ */

#if defined(MOZ_SANDBOX) && defined(DEBUG)
// If XPCOM_MEM_BLOAT_LOG or XPCOM_MEM_LEAK_LOG is set to a log file
// path, return the path to the parent directory (where sibling log
// files will be saved.)
nsresult nsMacUtilsImpl::GetBloatLogDir(nsCString& aDirectoryPath) {
  nsAutoCString bloatLog(PR_GetEnv("XPCOM_MEM_BLOAT_LOG"));
  if (bloatLog.IsEmpty()) {
    bloatLog = PR_GetEnv("XPCOM_MEM_LEAK_LOG");
  }
  if (!bloatLog.IsEmpty() && bloatLog != "1" && bloatLog != "2") {
    return GetDirectoryPath(bloatLog.get(), aDirectoryPath);
  }
  return NS_OK;
}

// Given a path to a file, return the directory which contains it.
nsresult nsMacUtilsImpl::GetDirectoryPath(const char* aPath,
                                          nsCString& aDirectoryPath) {
  nsresult rv = NS_OK;
  nsCOMPtr<nsIFile> file;
  rv = NS_NewNativeLocalFile(nsDependentCString(aPath), getter_AddRefs(file));
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIFile> directoryFile;
  rv = file->GetParent(getter_AddRefs(directoryFile));
  NS_ENSURE_SUCCESS(rv, rv);

  rv = directoryFile->Normalize();
  NS_ENSURE_SUCCESS(rv, rv);

  if (NS_FAILED(directoryFile->GetNativePath(aDirectoryPath))) {
    MOZ_CRASH("Failed to get path for an nsIFile");
  }
  return NS_OK;
}
#endif /* MOZ_SANDBOX  && DEBUG */

/* static */
bool nsMacUtilsImpl::IsTCSMAvailable() {
  if (sTCSMStatus == TCSM_Unknown) {
    uint32_t oldVal = 0;
    size_t oldValSize = sizeof(oldVal);
    int rv =
        sysctlbyname("kern.tcsm_available", &oldVal, &oldValSize, nullptr, 0);
    TCSMStatus newStatus;
    if (rv < 0 || oldVal == 0) {
      newStatus = TCSM_Unavailable;
    } else {
      newStatus = TCSM_Available;
    }
    // The value of sysctl kern.tcsm_available is the same for all
    // threads within the same process. If another thread raced with us
    // and initialized sTCSMStatus first (changing it from
    // TCSM_Unknown), we can continue without needing to update it
    // again. Hence, we ignore compareExchange's return value.
    (void)sTCSMStatus.compareExchange(TCSM_Unknown, newStatus);
  }
  return (sTCSMStatus == TCSM_Available);
}

static nsresult EnableTCSM() {
  uint32_t newVal = 1;
  int rv = sysctlbyname("kern.tcsm_enable", nullptr, nullptr, &newVal,
                        sizeof(newVal));
  if (rv < 0) {
    return NS_ERROR_UNEXPECTED;
  }
  return NS_OK;
}

#if defined(DEBUG)
static bool IsTCSMEnabled() {
  uint32_t oldVal = 0;
  size_t oldValSize = sizeof(oldVal);
  int rv = sysctlbyname("kern.tcsm_enable", &oldVal, &oldValSize, nullptr, 0);
  return (rv == 0) && (oldVal != 0);
}
#endif

/*
 * Intentionally return void so that failures will be ignored in non-debug
 * builds. This method uses new sysctls which may not be as thoroughly tested
 * and we don't want to cause crashes handling the failure due to an OS bug.
 */
/* static */
void nsMacUtilsImpl::EnableTCSMIfAvailable() {
  if (IsTCSMAvailable()) {
    if (NS_FAILED(EnableTCSM())) {
      NS_WARNING("Failed to enable TCSM");
    }
    MOZ_ASSERT(IsTCSMEnabled());
  }
}

// Returns 0 on error.
/* static */
uint32_t nsMacUtilsImpl::GetPhysicalCPUCount() {
  uint32_t oldVal = 0;
  size_t oldValSize = sizeof(oldVal);
  int rv = sysctlbyname("hw.physicalcpu_max", &oldVal, &oldValSize, nullptr, 0);
  if (rv == -1) {
    return 0;
  }
  return oldVal;
}

/*
 * Helper function to read a string value for a given key from the .app's
 * Info.plist.
 */
static nsresult GetStringValueFromBundlePlist(const nsAString& aKey,
                                              nsAutoCString& aValue) {
  CFBundleRef mainBundle = CFBundleGetMainBundle();
  if (mainBundle == nullptr) {
    return NS_ERROR_FAILURE;
  }

  // Read this app's bundle Info.plist as a dictionary
  CFDictionaryRef bundleInfoDict = CFBundleGetInfoDictionary(mainBundle);
  if (bundleInfoDict == nullptr) {
    return NS_ERROR_FAILURE;
  }

  nsAutoCString keyAutoCString = NS_ConvertUTF16toUTF8(aKey);
  CFStringRef key = CFStringCreateWithCString(
      kCFAllocatorDefault, keyAutoCString.get(), kCFStringEncodingUTF8);
  if (key == nullptr) {
    return NS_ERROR_FAILURE;
  }

  CFStringRef value = (CFStringRef)CFDictionaryGetValue(bundleInfoDict, key);
  CFRelease(key);
  if (value == nullptr) {
    return NS_ERROR_FAILURE;
  }

  CFIndex valueLength = CFStringGetLength(value);
  if (valueLength == 0) {
    return NS_ERROR_FAILURE;
  }

  const char* valueCString =
      CFStringGetCStringPtr(value, kCFStringEncodingUTF8);
  if (valueCString) {
    aValue.Assign(valueCString);
    return NS_OK;
  }

  CFIndex maxLength =
      CFStringGetMaximumSizeForEncoding(valueLength, kCFStringEncodingUTF8) + 1;
  char* valueBuffer = static_cast<char*>(moz_xmalloc(maxLength));

  if (!CFStringGetCString(value, valueBuffer, maxLength,
                          kCFStringEncodingUTF8)) {
    free(valueBuffer);
    return NS_ERROR_FAILURE;
  }

  aValue.Assign(valueBuffer);
  free(valueBuffer);
  return NS_OK;
}

/*
 * Helper function for reading a path string from the .app's Info.plist
 * and returning a directory object for that path with symlinks resolved.
 */
static nsresult GetDirFromBundlePlist(const nsAString& aKey, nsIFile** aDir) {
  nsresult rv;

  nsAutoCString dirPath;
  rv = GetStringValueFromBundlePlist(aKey, dirPath);
  NS_ENSURE_SUCCESS(rv, rv);

  nsCOMPtr<nsIFile> dir;
  rv = NS_NewNativeLocalFile(dirPath, getter_AddRefs(dir));
  NS_ENSURE_SUCCESS(rv, rv);

  rv = dir->Normalize();
  NS_ENSURE_SUCCESS(rv, rv);

  bool isDirectory = false;
  rv = dir->IsDirectory(&isDirectory);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!isDirectory) {
    return NS_ERROR_FILE_NOT_DIRECTORY;
  }

  dir.swap(*aDir);
  return NS_OK;
}

nsresult nsMacUtilsImpl::GetRepoDir(nsIFile** aRepoDir) {
#if defined(MOZ_SANDBOX)
  MOZ_ASSERT(!mozilla::IsPackagedBuild());
#endif
  return GetDirFromBundlePlist(NS_LITERAL_STRING_FROM_CSTRING(MAC_DEV_REPO_KEY),
                               aRepoDir);
}

nsresult nsMacUtilsImpl::GetObjDir(nsIFile** aObjDir) {
#if defined(MOZ_SANDBOX)
  MOZ_ASSERT(!mozilla::IsPackagedBuild());
#endif
  return GetDirFromBundlePlist(NS_LITERAL_STRING_FROM_CSTRING(MAC_DEV_OBJ_KEY),
                               aObjDir);
}

/* static */
nsresult nsMacUtilsImpl::GetArchitecturesForBundle(uint32_t* aArchMask) {
  MOZ_ASSERT(aArchMask);

  *aArchMask = sBundleArchMaskAtomic;
  if (*aArchMask != 0) {
    return NS_OK;
  }

  CFBundleRef mainBundle = ::CFBundleGetMainBundle();
  if (!mainBundle) {
    return NS_ERROR_FAILURE;
  }

  CFArrayRef archList = ::CFBundleCopyExecutableArchitectures(mainBundle);
  if (!archList) {
    return NS_ERROR_FAILURE;
  }

  CFIndex archCount = ::CFArrayGetCount(archList);
  for (CFIndex i = 0; i < archCount; i++) {
    CFNumberRef arch =
        static_cast<CFNumberRef>(::CFArrayGetValueAtIndex(archList, i));

    int archInt = 0;
    if (!::CFNumberGetValue(arch, kCFNumberIntType, &archInt)) {
      ::CFRelease(archList);
      return NS_ERROR_FAILURE;
    }

    if (archInt == kCFBundleExecutableArchitecturePPC) {
      *aArchMask |= base::PROCESS_ARCH_PPC;
    } else if (archInt == kCFBundleExecutableArchitectureI386) {
      *aArchMask |= base::PROCESS_ARCH_I386;
    } else if (archInt == kCFBundleExecutableArchitecturePPC64) {
      *aArchMask |= base::PROCESS_ARCH_PPC_64;
    } else if (archInt == kCFBundleExecutableArchitectureX86_64) {
      *aArchMask |= base::PROCESS_ARCH_X86_64;
    } else if (archInt == kCFBundleExecutableArchitectureARM64) {
      *aArchMask |= base::PROCESS_ARCH_ARM_64;
    }
  }

  ::CFRelease(archList);

  sBundleArchMaskAtomic = *aArchMask;

  return NS_OK;
}

/* static */
nsresult nsMacUtilsImpl::GetArchitecturesForBinary(const char* aPath,
                                                   uint32_t* aArchMask) {
  MOZ_ASSERT(aArchMask);

  *aArchMask = 0;

  CFURLRef url = ::CFURLCreateFromFileSystemRepresentation(
      kCFAllocatorDefault, (const UInt8*)aPath, strlen(aPath), false);
  if (!url) {
    return NS_ERROR_FAILURE;
  }

  CFArrayRef archs = ::CFBundleCopyExecutableArchitecturesForURL(url);
  if (!archs) {
    CFRelease(url);
    return NS_ERROR_FAILURE;
  }

  CFIndex archCount = ::CFArrayGetCount(archs);
  for (CFIndex i = 0; i < archCount; i++) {
    CFNumberRef currentArch =
        static_cast<CFNumberRef>(::CFArrayGetValueAtIndex(archs, i));
    int currentArchInt = 0;
    if (!::CFNumberGetValue(currentArch, kCFNumberIntType, &currentArchInt)) {
      continue;
    }
    switch (currentArchInt) {
      case kCFBundleExecutableArchitectureX86_64:
        *aArchMask |= base::PROCESS_ARCH_X86_64;
        break;
      case kCFBundleExecutableArchitectureARM64:
        *aArchMask |= base::PROCESS_ARCH_ARM_64;
        break;
      default:
        break;
    }
  }

  CFRelease(url);
  CFRelease(archs);

  // We expect x86 or ARM64 or both.
  if (*aArchMask == 0) {
    return NS_ERROR_UNEXPECTED;
  }

  return NS_OK;
}

#if defined(__aarch64__)
// Pre-translate XUL so that x64 child processes launched after this
// translation will not incur the translation overhead delaying startup.
// Returns 1 if translation is in progress, -1 on an error encountered before
// translation, and otherwise returns the result of rosetta_translate_binaries.
/* static */
int nsMacUtilsImpl::PreTranslateXUL() {
  bool expected = false;
  if (!sIsXULTranslated.compare_exchange_strong(expected, true)) {
    // Translation is already done or in progress.
    return 1;
  }

  // Get the path to XUL by first getting the
  // outer .app path and appending the path to XUL.
  nsCString xulPath;
  if (!GetAppPath(xulPath)) {
    return -1;
  }
  xulPath.Append("/Contents/MacOS/XUL");

  return PreTranslateBinary(xulPath);
}

// Use Chromium's method to pre-translate the provided binary using the
// undocumented function "rosetta_translate_binaries" from libRosetta.dylib.
// Re-translating the same binary does not cause translation to occur again.
// Returns -1 on an error encountered before translation, otherwise returns
// the rosetta_translate_binaries result. This method is partly copied from
// Chromium code.
/* static */
int nsMacUtilsImpl::PreTranslateBinary(nsCString aBinaryPath) {
  // Do not attempt to use this in child processes. Child
  // processes executing should already be translated and
  // sandboxing may interfere with translation.
  MOZ_ASSERT(XRE_IsParentProcess());
  if (!XRE_IsParentProcess()) {
    return -1;
  }

  // Translation can take several seconds and therefore
  // should not be done on the main thread.
  MOZ_ASSERT(!NS_IsMainThread());
  if (NS_IsMainThread()) {
    return -1;
  }

  // @available() is not available for macOS 11 at this time so use
  // -Wunguarded-availability-new to avoid compiler warnings caused
  // by an earlier minimum SDK. ARM64 builds require the 11.0 SDK and
  // can not be run on earlier OS versions so this is not a concern.
#  pragma clang diagnostic push
#  pragma clang diagnostic ignored "-Wunguarded-availability-new"
  // If Rosetta is not installed, do not proceed.
  if (!CFBundleIsArchitectureLoadable(CPU_TYPE_X86_64)) {
    return -1;
  }
#  pragma clang diagnostic pop

  if (aBinaryPath.IsEmpty()) {
    return -1;
  }

  // int rosetta_translate_binaries(const char*[] paths, int npaths)
  using rosetta_translate_binaries_t = int (*)(const char*[], int);

  static auto rosetta_translate_binaries = []() {
    void* libRosetta =
        dlopen("/usr/lib/libRosetta.dylib", RTLD_LAZY | RTLD_LOCAL);
    if (!libRosetta) {
      return static_cast<rosetta_translate_binaries_t>(nullptr);
    }

    return reinterpret_cast<rosetta_translate_binaries_t>(
        dlsym(libRosetta, "rosetta_translate_binaries"));
  }();

  if (!rosetta_translate_binaries) {
    return -1;
  }

  const char* pathPtr = aBinaryPath.get();
  return rosetta_translate_binaries(&pathPtr, 1);
}
#endif

std::string CFStringToStdString(CFStringRef aString) {
  if (!aString) {
    return std::string();
  }

  CFIndex length = CFStringGetLength(aString);
  CFIndex maxSize =
      CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
  mozilla::UniquePtr<char[]> buffer = mozilla::MakeUnique<char[]>(maxSize);
  if (!buffer) {
    return std::string();
  }

  if (CFStringGetCString(aString, buffer.get(), maxSize,
                         kCFStringEncodingUTF8)) {
    std::string result(buffer.get());
    return result;
  }

  return std::string();
}

// Read the code signature information of the binary at `aPath` and return
// A string description of the signature type in `aSignatureType`. Returns
// CodeSignatureType::UnexpectedError if a failure occurs while reading the
// signature information.
//
CodeSignatureType GetSignatureTypeImpl(const nsCString& aPath) {
  class CFTypeRefAutoDeleter {
   public:
    explicit CFTypeRefAutoDeleter(CFTypeRef ref) : mRef(ref) {}
    ~CFTypeRefAutoDeleter() {
      if (mRef != nullptr) ::CFRelease(mRef);
    }

   private:
    CFTypeRef mRef;
  };

  LOG("Reading code signature: %s", aPath.get());
  CFStringRef pathRef = CFStringCreateWithCString(
      kCFAllocatorDefault, aPath.get(), kCFStringEncodingUTF8);
  if (!pathRef) {
    return CodeSignatureType::UnexpectedError;
  }
  CFTypeRefAutoDeleter cfPathRefAuto((CFTypeRef)pathRef);

  CFURLRef fileURLRef = CFURLCreateWithFileSystemPath(
      kCFAllocatorDefault, pathRef, kCFURLPOSIXPathStyle, false);
  CFTypeRefAutoDeleter fileURLRefAuto((CFTypeRef)fileURLRef);
  if (!fileURLRef) {
    return CodeSignatureType::UnexpectedError;
  }

  // Create a static code object. Due to return value semantics being
  // underspecified in the API documentation, we have some duplicate checks
  // for unsigned code. We expect SecStaticCodeCheckValidity to return
  // errSecCSUnsigned. In testing, SecStaticCodeCreateWithPath succeeds for
  // unsigned code. SecStaticCodeCheckValidity fails for unsigned code.
  // Having no kSecCodeInfoFlags is documented as an indicator of unsigned
  // code.
  SecStaticCodeRef staticCode = nullptr;
  OSStatus status =
      SecStaticCodeCreateWithPath(fileURLRef, kSecCSDefaultFlags, &staticCode);
  CFTypeRefAutoDeleter cfStaticCodeAuto((CFTypeRef)staticCode);
  if (status != errSecSuccess) {
    if (status == errSecCSUnsigned) {
      return CodeSignatureType::Unsigned;
    }
    LOG("SecStaticCodeCreateWithPath failure: %d", (int)status);
    return CodeSignatureType::UnexpectedError;
  }

  // Check validity and determine if unsigned
  status = SecStaticCodeCheckValidity(staticCode, kSecCSDefaultFlags, nullptr);
  if (status != errSecSuccess) {
    if (status == errSecCSUnsigned) {
      return CodeSignatureType::Unsigned;
    }
    LOG("SecStaticCodeCheckValidity failure: %d", (int)status);
    return CodeSignatureType::UnexpectedError;
  }

  // Retrieve more detailed signing information dictionary
  CFDictionaryRef signingInfo = nullptr;
  status = SecCodeCopySigningInformation(staticCode, kSecCSSigningInformation,
                                         &signingInfo);
  CFTypeRefAutoDeleter cfSigningInfoAuto((CFTypeRef)signingInfo);
  if (status != errSecSuccess || !signingInfo) {
    LOG("SecCodeCopySigningInformation failure: %d", (int)status);
    return CodeSignatureType::UnexpectedError;
  }

  CFNumberRef flagsRef =
      (CFNumberRef)CFDictionaryGetValue(signingInfo, kSecCodeInfoFlags);
  if (!flagsRef) {
    // Is it signed? No kSecCodeInfoFlags key indicates unsigned
    // code per SecCodeCopySigningInformation documentation.
    return CodeSignatureType::Unsigned;
  }

  // Check Code Directory flags for ad-hoc signing
  uint32_t codeDirectoryFlags = 0;
  CFNumberGetValue(flagsRef, kCFNumberSInt32Type, &codeDirectoryFlags);

  CFArrayRef certificates =
      (CFArrayRef)CFDictionaryGetValue(signingInfo, kSecCodeInfoCertificates);
  if (!certificates) {
    CodeSignatureType rv;
    if (codeDirectoryFlags & kSecCodeSignatureAdhoc) {
      // Ad-hoc signature with no certificates
      rv = CodeSignatureType::AdHoc;
    } else {
      // No certificates found and not ad-hoc
      LOG("NULL certificates array");
      rv = CodeSignatureType::Other;
    }
    return rv;
  }

  if (CFArrayGetCount(certificates) == 0) {
    LOG("Zero length certificates array");
    return CodeSignatureType::Other;
  }

  // Extract the leaf certificate common name.
  // The leaf certificate is the first one in the array.
  SecCertificateRef leafCert =
      (SecCertificateRef)CFArrayGetValueAtIndex(certificates, 0);
  CFStringRef commonNameRef = nullptr;
  std::string commonName;
  status = SecCertificateCopyCommonName(leafCert, &commonNameRef);
  CFTypeRefAutoDeleter cfCommonNameAuto((CFTypeRef)commonNameRef);
  if (status != errSecSuccess || !commonNameRef) {
    // No leaf common name
    LOG("No leaf common name");
    return CodeSignatureType::Other;
  }

  commonName = CFStringToStdString(commonNameRef);
  LOG("Leaf common name: %s", commonName.c_str());

  // Classify signature based on leaf certificate common name
  if (commonName == "Apple Mac OS Application Signing") {
    return CodeSignatureType::AppStore;
  } else if (commonName == "Software Signing") {
    return CodeSignatureType::AppleSystem;
  } else if (commonName.find("Developer ID Application:") !=
             std::string::npos) {
    return CodeSignatureType::DeveloperID;
  } else if (commonName.find("Apple Development:") != std::string::npos) {
    return CodeSignatureType::Development;
  } else {
    return CodeSignatureType::Other;
  }
}

CodeSignatureType nsMacUtilsImpl::GetSignatureType(const nsCString& aPath) {
  CodeSignatureType signatureType = GetSignatureTypeImpl(aPath);
  LOG("Code signature type for module %s: %s", aPath.get(),
      CodeSignatureTypeToString(signatureType).get());
  return signatureType;
}

nsCString nsMacUtilsImpl::CodeSignatureTypeToString(CodeSignatureType aType) {
  switch (aType) {
    case CodeSignatureType::UnexpectedError:
      return "Unexpected Error"_ns;
    case CodeSignatureType::Unsigned:
      return "Unsigned"_ns;
    case CodeSignatureType::AdHoc:
      return "Ad-Hoc"_ns;
    case CodeSignatureType::DeveloperID:
      return "Developer ID"_ns;
    case CodeSignatureType::AppStore:
      return "App Store"_ns;
    case CodeSignatureType::AppleSystem:
      return "Apple System"_ns;
    case CodeSignatureType::Development:
      return "Development"_ns;
    case CodeSignatureType::Other:
      return "Other"_ns;
  }
  return "Unknown"_ns;
}
