/* -*- Mode: C++; tab-width: 3; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * 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 <sys/types.h>
#include <sys/stat.h>

#include "nsOSHelperAppService.h"
#include "nsMIMEInfoUnix.h"
#ifdef MOZ_WIDGET_GTK
#  include "nsGNOMERegistry.h"
#  ifdef MOZ_BUILD_APP_IS_BROWSER
#    include "nsIToolkitShellService.h"
#    include "nsIGNOMEShellService.h"
#  endif
#endif
#include "nsISupports.h"
#include "nsString.h"
#include "nsReadableUtils.h"
#include "nsUnicharUtils.h"
#include "nsIFileStreams.h"
#include "nsILineInputStream.h"
#include "nsIFile.h"
#include "nsIProcess.h"
#include "nsLocalFile.h"
#include "nsNetCID.h"
#include "nsXPCOM.h"
#include "nsComponentManagerUtils.h"
#include "nsCRT.h"
#include "nsDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "nsXULAppAPI.h"
#include "ContentHandlerService.h"
#include "prenv.h"  // for PR_GetEnv()
#include "mozilla/EnumeratedRange.h"
#include "mozilla/Preferences.h"
#include "mozilla/ClearOnShutdown.h"
#include "nsMimeTypes.h"
#include <mutex>

using namespace mozilla;

#define LOG(...) \
  MOZ_LOG(nsOSHelperAppService::sLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
#define LOG_ENABLED() \
  MOZ_LOG_TEST(nsOSHelperAppService::sLog, mozilla::LogLevel::Debug)

static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter,
                              const nsAString::const_iterator& aEnd_iter);
static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter,
                              nsAString::const_iterator& aMajorTypeStart,
                              nsAString::const_iterator& aMajorTypeEnd,
                              nsAString::const_iterator& aMinorTypeStart,
                              nsAString::const_iterator& aMinorTypeEnd,
                              const nsAString::const_iterator& aEnd_iter);

inline bool IsNetscapeFormat(const nsACString& aBuffer);

nsOSHelperAppService::~nsOSHelperAppService() {}

/*
 * Take a command with all the mailcap escapes in it and unescape it
 * Ideally this needs the mime type, mime type options, and location of the
 * temporary file, but this last can't be got from here
 */
// static
nsresult nsOSHelperAppService::UnescapeCommand(const nsAString& aEscapedCommand,
                                               const nsAString& aMajorType,
                                               const nsAString& aMinorType,
                                               nsACString& aUnEscapedCommand) {
  LOG("-- UnescapeCommand");
  LOG("Command to escape: '%s'\n",
      NS_LossyConvertUTF16toASCII(aEscapedCommand).get());
  //  XXX This function will need to get the mime type and various stuff like
  //  that being passed in to work properly

  LOG(
      ("UnescapeCommand really needs some work -- it should actually do some "
       "unescaping\n"));

  CopyUTF16toUTF8(aEscapedCommand, aUnEscapedCommand);
  LOG("Escaped command: '%s'\n", PromiseFlatCString(aUnEscapedCommand).get());
  return NS_OK;
}

/* Put aSemicolon_iter at the first non-escaped semicolon after
 * aStart_iter but before aEnd_iter
 */

static nsresult FindSemicolon(nsAString::const_iterator& aSemicolon_iter,
                              const nsAString::const_iterator& aEnd_iter) {
  bool semicolonFound = false;
  while (aSemicolon_iter != aEnd_iter && !semicolonFound) {
    switch (*aSemicolon_iter) {
      case '\\':
        aSemicolon_iter.advance(2);
        break;
      case ';':
        semicolonFound = true;
        break;
      default:
        ++aSemicolon_iter;
        break;
    }
  }
  return NS_OK;
}

static nsresult ParseMIMEType(const nsAString::const_iterator& aStart_iter,
                              nsAString::const_iterator& aMajorTypeStart,
                              nsAString::const_iterator& aMajorTypeEnd,
                              nsAString::const_iterator& aMinorTypeStart,
                              nsAString::const_iterator& aMinorTypeEnd,
                              const nsAString::const_iterator& aEnd_iter) {
  nsAString::const_iterator iter(aStart_iter);

  // skip leading whitespace
  while (iter != aEnd_iter && nsCRT::IsAsciiSpace(*iter)) {
    ++iter;
  }

  if (iter == aEnd_iter) {
    return NS_ERROR_INVALID_ARG;
  }

  aMajorTypeStart = iter;

  // find major/minor separator ('/')
  while (iter != aEnd_iter && *iter != '/') {
    ++iter;
  }

  if (iter == aEnd_iter) {
    return NS_ERROR_INVALID_ARG;
  }

  aMajorTypeEnd = iter;

  // skip '/'
  ++iter;

  if (iter == aEnd_iter) {
    return NS_ERROR_INVALID_ARG;
  }

  aMinorTypeStart = iter;

  // find end of minor type, delimited by whitespace or ';'
  while (iter != aEnd_iter && !nsCRT::IsAsciiSpace(*iter) && *iter != ';') {
    ++iter;
  }

  aMinorTypeEnd = iter;

  return NS_OK;
}

// TODO: We should consider not only caching the file location but maybe the
// file contents too?
enum class FileKind {
  PrivateMimeTypes = 0,
  GlobalMimeTypes,
  PrivateMailCap,
  GlobalMailCap,

  Count,
};

struct FileLocationCache {
  struct Entry {
    bool mIsCached = false;
    nsresult mResult = NS_OK;
    nsString mLocation;

    void Clear() { *this = {}; }
  };

  EnumeratedArray<FileKind, Entry, size_t(FileKind::Count)> mEntries;

  static const char* PrefFor(FileKind aKind) {
    switch (aKind) {
      case FileKind::GlobalMimeTypes:
        return "helpers.global_mime_types_file";
      case FileKind::PrivateMimeTypes:
        return "helpers.private_mime_types_file";
      case FileKind::GlobalMailCap:
        return "helpers.global_mailcap_file";
      case FileKind::PrivateMailCap:
        return "helpers.private_mailcap_file";
      case FileKind::Count:
        break;
    }
    MOZ_ASSERT_UNREACHABLE("Unknown file kind");
    return "";
  }

  void Clear() {
    for (auto& entry : mEntries) {
      entry.Clear();
    }
  }

  static void PrefChangeCallback(const char*, void*) { Get().Clear(); }

  static FileLocationCache& Get() {
    static FileLocationCache sCache;
    static std::once_flag flag;
    std::call_once(flag, [&] {
      for (auto kind :
           MakeEnumeratedRange(FileKind::PrivateMimeTypes, FileKind::Count)) {
        Preferences::RegisterCallback(PrefChangeCallback,
                                      nsDependentCString(PrefFor(kind)));
      }
      RunOnShutdown([] {
        sCache.Clear();
        for (auto kind :
             MakeEnumeratedRange(FileKind::PrivateMimeTypes, FileKind::Count)) {
          Preferences::UnregisterCallback(PrefChangeCallback,
                                          nsDependentCString(PrefFor(kind)));
        }
      });
    });
    return sCache;
  }

  Entry& EntryFor(FileKind aKind) { return mEntries[aKind]; }
};

// The lookup order is:
//    1) user pref
//    2) env var (if any)
//    3) pref
static nsresult DoGetFileLocation(FileKind aKind, nsAString& aFileLocation) {
  LOG("-- GetFileLocation(%d)\n", int(aKind));

  aFileLocation.Truncate();

  const char* envVar = [&]() -> const char* {
    switch (aKind) {
      case FileKind::PrivateMailCap:
        return "PERSONAL_MAILCAP";
      case FileKind::GlobalMailCap:
        return "MAILCAP";
      default:
        return nullptr;
    }
  }();

  const char* prefName = FileLocationCache::PrefFor(aKind);

  if (envVar) {
    // If we have an env var we should check whether the pref is a user pref. If
    // we do not, we don't care.
    if (Preferences::HasUserValue(prefName) &&
        NS_SUCCEEDED(Preferences::GetString(prefName, aFileLocation))) {
      return NS_OK;
    }

    char* envValue = PR_GetEnv(envVar);
    if (envValue && *envValue) {
      // the pref is in the system charset and it's a filepath... The
      // natural way to do the charset conversion is by just initing
      // an nsIFile with the native path and asking it for the Unicode
      // version.
      nsCOMPtr<nsIFile> file;
      nsresult rv = NS_NewNativeLocalFile(nsDependentCString(envValue),
                                          getter_AddRefs(file));
      NS_ENSURE_SUCCESS(rv, rv);
      return file->GetPath(aFileLocation);
    }
  }

  return Preferences::GetString(prefName, aFileLocation);
}

static nsresult GetFileLocation(FileKind aKind, nsAString& aFileLocation) {
  MOZ_ASSERT(NS_IsMainThread());
  auto& entry = FileLocationCache::Get().EntryFor(aKind);
  if (!entry.mIsCached) {
    entry.mIsCached = true;
    entry.mResult = DoGetFileLocation(aKind, entry.mLocation);
  }
  aFileLocation = entry.mLocation;
  return entry.mResult;
}

/* Get the mime.types file names from prefs and look up info in them
   based on extension */
// static
nsresult nsOSHelperAppService::LookUpTypeAndDescription(
    const nsAString& aFileExtension, nsAString& aMajorType,
    nsAString& aMinorType, nsAString& aDescription, bool aUserData) {
  LOG("-- LookUpTypeAndDescription for extension '%s'\n",
      NS_LossyConvertUTF16toASCII(aFileExtension).get());
  nsAutoString mimeFileName;

  auto kind =
      aUserData ? FileKind::PrivateMimeTypes : FileKind::GlobalMimeTypes;
  nsresult rv = GetFileLocation(kind, mimeFileName);
  if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) {
    rv = GetTypeAndDescriptionFromMimetypesFile(
        mimeFileName, aFileExtension, aMajorType, aMinorType, aDescription);
  } else {
    rv = NS_ERROR_NOT_AVAILABLE;
  }

  return rv;
}

inline bool IsNetscapeFormat(const nsACString& aBuffer) {
  return StringBeginsWith(
             aBuffer,
             nsLiteralCString(
                 "#--Netscape Communications Corporation MIME Information")) ||
         StringBeginsWith(aBuffer, "#--MCOM MIME Information"_ns);
}

/*
 * Create a file stream and line input stream for the filename.
 * Leaves the first line of the file in aBuffer and sets the format to
 *  true for netscape files and false for normail ones
 */
// static
nsresult nsOSHelperAppService::CreateInputStream(
    const nsAString& aFilename, nsIFileInputStream** aFileInputStream,
    nsILineInputStream** aLineInputStream, nsACString& aBuffer,
    bool* aNetscapeFormat, bool* aMore) {
  LOG("-- CreateInputStream");
  nsresult rv = NS_OK;

  nsCOMPtr<nsIFile> file;
  MOZ_TRY(NS_NewLocalFile(aFilename, getter_AddRefs(file)));

  nsCOMPtr<nsIFileInputStream> fileStream(
      do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv));
  if (NS_FAILED(rv)) return rv;
  rv = fileStream->Init(file, -1, -1, false);
  if (NS_FAILED(rv)) return rv;

  nsCOMPtr<nsILineInputStream> lineStream(do_QueryInterface(fileStream, &rv));

  if (NS_FAILED(rv)) {
    LOG("Interface trouble in stream land!");
    return rv;
  }

  rv = lineStream->ReadLine(aBuffer, aMore);
  if (NS_FAILED(rv)) {
    fileStream->Close();
    return rv;
  }

  *aNetscapeFormat = IsNetscapeFormat(aBuffer);

  *aFileInputStream = fileStream;
  NS_ADDREF(*aFileInputStream);
  *aLineInputStream = lineStream;
  NS_ADDREF(*aLineInputStream);

  return NS_OK;
}

/* Open the file, read the first line, decide what type of file it is,
   then get info based on extension */
// static
nsresult nsOSHelperAppService::GetTypeAndDescriptionFromMimetypesFile(
    const nsAString& aFilename, const nsAString& aFileExtension,
    nsAString& aMajorType, nsAString& aMinorType, nsAString& aDescription) {
  LOG("-- GetTypeAndDescriptionFromMimetypesFile\n");
  LOG("Getting type and description from types file '%s'\n",
      NS_LossyConvertUTF16toASCII(aFilename).get());
  LOG("Using extension '%s'\n",
      NS_LossyConvertUTF16toASCII(aFileExtension).get());
  nsCOMPtr<nsIFileInputStream> mimeFile;
  nsCOMPtr<nsILineInputStream> mimeTypes;
  bool netscapeFormat;
  nsAutoString buf;
  nsAutoCString cBuf;
  bool more = false;
  nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile),
                                  getter_AddRefs(mimeTypes), cBuf,
                                  &netscapeFormat, &more);

  if (NS_FAILED(rv)) {
    return rv;
  }

  nsAutoString extensions;
  nsAutoStringN<101> entry;
  nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart,
      minorTypeEnd, descriptionStart, descriptionEnd;

  do {
    CopyASCIItoUTF16(cBuf, buf);
    // read through, building up an entry.  If we finish an entry, check for
    // a match and return out of the loop if we match

    // skip comments and empty lines
    if (!buf.IsEmpty() && buf.First() != '#') {
      entry.Append(buf);
      if (entry.Last() == '\\') {
        entry.Truncate(entry.Length() - 1);
        entry.Append(char16_t(
            ' '));  // in case there is no trailing whitespace on this line
      } else {      // we have a full entry
        LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get());
        if (netscapeFormat) {
          rv = ParseNetscapeMIMETypesEntry(
              entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd,
              extensions, descriptionStart, descriptionEnd);
          if (NS_FAILED(rv)) {
            // We sometimes get things like RealPlayer appending
            // "normal" entries to "Netscape" .mime.types files.  Try
            // to handle that.  Bug 106381.
            LOG("Bogus entry; trying 'normal' mode\n");
            rv = ParseNormalMIMETypesEntry(
                entry, majorTypeStart, majorTypeEnd, minorTypeStart,
                minorTypeEnd, extensions, descriptionStart, descriptionEnd);
          }
        } else {
          rv = ParseNormalMIMETypesEntry(
              entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd,
              extensions, descriptionStart, descriptionEnd);
          if (NS_FAILED(rv)) {
            // We sometimes get things like StarOffice prepending
            // "normal" entries to "Netscape" .mime.types files.  Try
            // to handle that.  Bug 136670.
            LOG("Bogus entry; trying 'Netscape' mode\n");
            rv = ParseNetscapeMIMETypesEntry(
                entry, majorTypeStart, majorTypeEnd, minorTypeStart,
                minorTypeEnd, extensions, descriptionStart, descriptionEnd);
          }
        }

        if (NS_SUCCEEDED(rv)) {  // entry parses
          nsAString::const_iterator start, end;
          extensions.BeginReading(start);
          extensions.EndReading(end);
          nsAString::const_iterator iter(start);

          while (start != end) {
            FindCharInReadable(',', iter, end);
            if (Substring(start, iter)
                    .Equals(aFileExtension,
                            nsCaseInsensitiveStringComparator)) {
              // it's a match.  Assign the type and description and run
              aMajorType.Assign(Substring(majorTypeStart, majorTypeEnd));
              aMinorType.Assign(Substring(minorTypeStart, minorTypeEnd));
              aDescription.Assign(Substring(descriptionStart, descriptionEnd));
              mimeFile->Close();
              return NS_OK;
            }
            if (iter != end) {
              ++iter;
            }
            start = iter;
          }
        } else {
          LOG("Failed to parse entry: %s\n",
              NS_LossyConvertUTF16toASCII(entry).get());
        }
        // truncate the entry for the next iteration
        entry.Truncate();
      }
    }
    if (!more) {
      rv = NS_ERROR_NOT_AVAILABLE;
      break;
    }
    // read the next line
    rv = mimeTypes->ReadLine(cBuf, &more);
  } while (NS_SUCCEEDED(rv));

  mimeFile->Close();
  return rv;
}

/* Get the mime.types file names from prefs and look up info in them
   based on mimetype  */
// static
nsresult nsOSHelperAppService::LookUpExtensionsAndDescription(
    const nsAString& aMajorType, const nsAString& aMinorType,
    nsAString& aFileExtensions, nsAString& aDescription) {
  LOG("-- LookUpExtensionsAndDescription for type '%s/%s'\n",
      NS_LossyConvertUTF16toASCII(aMajorType).get(),
      NS_LossyConvertUTF16toASCII(aMinorType).get());
  nsAutoString mimeFileName;

  nsresult rv = GetFileLocation(FileKind::PrivateMimeTypes, mimeFileName);
  if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) {
    rv = GetExtensionsAndDescriptionFromMimetypesFile(
        mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription);
  } else {
    rv = NS_ERROR_NOT_AVAILABLE;
  }
  if (NS_FAILED(rv) || aFileExtensions.IsEmpty()) {
    rv = GetFileLocation(FileKind::GlobalMimeTypes, mimeFileName);
    if (NS_SUCCEEDED(rv) && !mimeFileName.IsEmpty()) {
      rv = GetExtensionsAndDescriptionFromMimetypesFile(
          mimeFileName, aMajorType, aMinorType, aFileExtensions, aDescription);
    } else {
      rv = NS_ERROR_NOT_AVAILABLE;
    }
  }
  return rv;
}

/* Open the file, read the first line, decide what type of file it is,
   then get info based on extension */
// static
nsresult nsOSHelperAppService::GetExtensionsAndDescriptionFromMimetypesFile(
    const nsAString& aFilename, const nsAString& aMajorType,
    const nsAString& aMinorType, nsAString& aFileExtensions,
    nsAString& aDescription) {
  LOG("-- GetExtensionsAndDescriptionFromMimetypesFile\n");
  LOG("Getting extensions and description from types file '%s'\n",
      NS_LossyConvertUTF16toASCII(aFilename).get());
  LOG("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(),
      NS_LossyConvertUTF16toASCII(aMinorType).get());
  nsCOMPtr<nsIFileInputStream> mimeFile;
  nsCOMPtr<nsILineInputStream> mimeTypes;
  bool netscapeFormat;
  nsAutoCString cBuf;
  nsAutoString buf;
  bool more = false;
  nsresult rv = CreateInputStream(aFilename, getter_AddRefs(mimeFile),
                                  getter_AddRefs(mimeTypes), cBuf,
                                  &netscapeFormat, &more);
  if (NS_FAILED(rv)) {
    return rv;
  }

  nsAutoString extensions;
  nsAutoStringN<101> entry;
  nsAString::const_iterator majorTypeStart, majorTypeEnd, minorTypeStart,
      minorTypeEnd, descriptionStart, descriptionEnd;

  do {
    CopyASCIItoUTF16(cBuf, buf);
    // read through, building up an entry.  If we finish an entry, check for
    // a match and return out of the loop if we match

    // skip comments and empty lines
    if (!buf.IsEmpty() && buf.First() != '#') {
      entry.Append(buf);
      if (entry.Last() == '\\') {
        entry.Truncate(entry.Length() - 1);
        entry.Append(char16_t(
            ' '));  // in case there is no trailing whitespace on this line
      } else {      // we have a full entry
        LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get());
        if (netscapeFormat) {
          rv = ParseNetscapeMIMETypesEntry(
              entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd,
              extensions, descriptionStart, descriptionEnd);

          if (NS_FAILED(rv)) {
            // We sometimes get things like RealPlayer appending
            // "normal" entries to "Netscape" .mime.types files.  Try
            // to handle that.  Bug 106381.
            LOG("Bogus entry; trying 'normal' mode\n");
            rv = ParseNormalMIMETypesEntry(
                entry, majorTypeStart, majorTypeEnd, minorTypeStart,
                minorTypeEnd, extensions, descriptionStart, descriptionEnd);
          }
        } else {
          rv = ParseNormalMIMETypesEntry(
              entry, majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd,
              extensions, descriptionStart, descriptionEnd);

          if (NS_FAILED(rv)) {
            // We sometimes get things like StarOffice prepending
            // "normal" entries to "Netscape" .mime.types files.  Try
            // to handle that.  Bug 136670.
            LOG("Bogus entry; trying 'Netscape' mode\n");
            rv = ParseNetscapeMIMETypesEntry(
                entry, majorTypeStart, majorTypeEnd, minorTypeStart,
                minorTypeEnd, extensions, descriptionStart, descriptionEnd);
          }
        }

        if (NS_SUCCEEDED(rv) &&
            Substring(majorTypeStart, majorTypeEnd)
                .Equals(aMajorType, nsCaseInsensitiveStringComparator) &&
            Substring(minorTypeStart, minorTypeEnd)
                .Equals(aMinorType, nsCaseInsensitiveStringComparator)) {
          // it's a match
          aFileExtensions.Assign(extensions);
          aDescription.Assign(Substring(descriptionStart, descriptionEnd));
          mimeFile->Close();
          return NS_OK;
        }
        if (NS_FAILED(rv)) {
          LOG("Failed to parse entry: %s\n",
              NS_LossyConvertUTF16toASCII(entry).get());
        }

        entry.Truncate();
      }
    }
    if (!more) {
      rv = NS_ERROR_NOT_AVAILABLE;
      break;
    }
    // read the next line
    rv = mimeTypes->ReadLine(cBuf, &more);
  } while (NS_SUCCEEDED(rv));

  mimeFile->Close();
  return rv;
}

/*
 * This parses a Netscape format mime.types entry.  There are two
 * possible formats:
 *
 * type=foo/bar; options exts="baz" description="Some type"
 *
 * and
 *
 * type=foo/bar; options description="Some type" exts="baz"
 */
// static
nsresult nsOSHelperAppService::ParseNetscapeMIMETypesEntry(
    const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart,
    nsAString::const_iterator& aMajorTypeEnd,
    nsAString::const_iterator& aMinorTypeStart,
    nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions,
    nsAString::const_iterator& aDescriptionStart,
    nsAString::const_iterator& aDescriptionEnd) {
  LOG("-- ParseNetscapeMIMETypesEntry\n");
  NS_ASSERTION(!aEntry.IsEmpty(),
               "Empty Netscape MIME types entry being parsed.");

  nsAString::const_iterator start_iter, end_iter, match_start, match_end;

  aEntry.BeginReading(start_iter);
  aEntry.EndReading(end_iter);

  // skip trailing whitespace
  do {
    --end_iter;
  } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter));
  // if we're pointing to a quote, don't advance -- we don't want to
  // include the quote....
  if (*end_iter != '"') ++end_iter;
  match_start = start_iter;
  match_end = end_iter;

  // Get the major and minor types
  // First the major type
  if (!FindInReadable(u"type="_ns, match_start, match_end)) {
    return NS_ERROR_FAILURE;
  }

  match_start = match_end;

  while (match_end != end_iter && *match_end != '/') {
    ++match_end;
  }
  if (match_end == end_iter) {
    return NS_ERROR_FAILURE;
  }

  aMajorTypeStart = match_start;
  aMajorTypeEnd = match_end;

  // now the minor type
  if (++match_end == end_iter) {
    return NS_ERROR_FAILURE;
  }

  match_start = match_end;

  while (match_end != end_iter && !nsCRT::IsAsciiSpace(*match_end) &&
         *match_end != ';') {
    ++match_end;
  }
  if (match_end == end_iter) {
    return NS_ERROR_FAILURE;
  }

  aMinorTypeStart = match_start;
  aMinorTypeEnd = match_end;

  // ignore everything up to the end of the mime type from here on
  start_iter = match_end;

  // get the extensions
  match_start = match_end;
  match_end = end_iter;
  if (FindInReadable(u"exts="_ns, match_start, match_end)) {
    nsAString::const_iterator extStart, extEnd;

    if (match_end == end_iter ||
        (*match_end == '"' && ++match_end == end_iter)) {
      return NS_ERROR_FAILURE;
    }

    extStart = match_end;
    match_start = extStart;
    match_end = end_iter;
    if (FindInReadable(u"desc=\""_ns, match_start, match_end)) {
      // exts= before desc=, so we have to find the actual end of the extensions
      extEnd = match_start;
      if (extEnd == extStart) {
        return NS_ERROR_FAILURE;
      }

      do {
        --extEnd;
      } while (extEnd != extStart && nsCRT::IsAsciiSpace(*extEnd));

      if (extEnd != extStart && *extEnd == '"') {
        --extEnd;
      }
    } else {
      // desc= before exts=, so we can use end_iter as the end of the extensions
      extEnd = end_iter;
    }
    aExtensions = Substring(extStart, extEnd);
  } else {
    // no extensions
    aExtensions.Truncate();
  }

  // get the description
  match_start = start_iter;
  match_end = end_iter;
  if (FindInReadable(u"desc=\""_ns, match_start, match_end)) {
    aDescriptionStart = match_end;
    match_start = aDescriptionStart;
    match_end = end_iter;
    if (FindInReadable(u"exts="_ns, match_start, match_end)) {
      // exts= after desc=, so have to find actual end of description
      aDescriptionEnd = match_start;
      if (aDescriptionEnd == aDescriptionStart) {
        return NS_ERROR_FAILURE;
      }

      do {
        --aDescriptionEnd;
      } while (aDescriptionEnd != aDescriptionStart &&
               nsCRT::IsAsciiSpace(*aDescriptionEnd));
    } else {
      // desc= after exts=, so use end_iter for the description end
      aDescriptionEnd = end_iter;
    }
  } else {
    // no description
    aDescriptionStart = start_iter;
    aDescriptionEnd = start_iter;
  }

  return NS_OK;
}

/*
 * This parses a normal format mime.types entry.  The format is:
 *
 * major/minor    ext1 ext2 ext3
 */
// static
nsresult nsOSHelperAppService::ParseNormalMIMETypesEntry(
    const nsAString& aEntry, nsAString::const_iterator& aMajorTypeStart,
    nsAString::const_iterator& aMajorTypeEnd,
    nsAString::const_iterator& aMinorTypeStart,
    nsAString::const_iterator& aMinorTypeEnd, nsAString& aExtensions,
    nsAString::const_iterator& aDescriptionStart,
    nsAString::const_iterator& aDescriptionEnd) {
  LOG("-- ParseNormalMIMETypesEntry\n");
  NS_ASSERTION(!aEntry.IsEmpty(),
               "Empty Normal MIME types entry being parsed.");

  nsAString::const_iterator start_iter, end_iter, iter;

  aEntry.BeginReading(start_iter);
  aEntry.EndReading(end_iter);

  // no description
  aDescriptionStart = start_iter;
  aDescriptionEnd = start_iter;

  // skip leading whitespace
  while (start_iter != end_iter && nsCRT::IsAsciiSpace(*start_iter)) {
    ++start_iter;
  }
  if (start_iter == end_iter) {
    return NS_ERROR_FAILURE;
  }
  // skip trailing whitespace
  do {
    --end_iter;
  } while (end_iter != start_iter && nsCRT::IsAsciiSpace(*end_iter));

  ++end_iter;  // point to first whitespace char (or to end of string)
  iter = start_iter;

  // get the major type
  if (!FindCharInReadable('/', iter, end_iter)) return NS_ERROR_FAILURE;

  nsAString::const_iterator equals_sign_iter(start_iter);
  if (FindCharInReadable('=', equals_sign_iter, iter))
    return NS_ERROR_FAILURE;  // see bug 136670

  aMajorTypeStart = start_iter;
  aMajorTypeEnd = iter;

  // get the minor type
  if (++iter == end_iter) {
    return NS_ERROR_FAILURE;
  }
  start_iter = iter;

  while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) {
    ++iter;
  }
  aMinorTypeStart = start_iter;
  aMinorTypeEnd = iter;

  // get the extensions
  aExtensions.Truncate();
  while (iter != end_iter) {
    while (iter != end_iter && nsCRT::IsAsciiSpace(*iter)) {
      ++iter;
    }

    start_iter = iter;
    while (iter != end_iter && !nsCRT::IsAsciiSpace(*iter)) {
      ++iter;
    }
    aExtensions.Append(Substring(start_iter, iter));
    if (iter != end_iter) {  // not the last extension
      aExtensions.Append(char16_t(','));
    }
  }

  return NS_OK;
}

// static
nsresult nsOSHelperAppService::LookUpHandlerAndDescription(
    const nsAString& aMajorType, const nsAString& aMinorType,
    nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags) {
  // The mailcap lookup is two-pass to handle the case of mailcap files
  // that have something like:
  //
  // text/*; emacs %s
  // text/rtf; soffice %s
  //
  // in that order.  We want to pick up "soffice" for text/rtf in such cases
  nsresult rv = DoLookUpHandlerAndDescription(
      aMajorType, aMinorType, aHandler, aDescription, aMozillaFlags, true);
  if (NS_FAILED(rv)) {
    rv = DoLookUpHandlerAndDescription(aMajorType, aMinorType, aHandler,
                                       aDescription, aMozillaFlags, false);
  }

  // maybe we have an entry for "aMajorType/*"?
  if (NS_FAILED(rv)) {
    rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler,
                                       aDescription, aMozillaFlags, true);
  }

  if (NS_FAILED(rv)) {
    rv = DoLookUpHandlerAndDescription(aMajorType, u"*"_ns, aHandler,
                                       aDescription, aMozillaFlags, false);
  }

  return rv;
}

// static
nsresult nsOSHelperAppService::DoLookUpHandlerAndDescription(
    const nsAString& aMajorType, const nsAString& aMinorType,
    nsAString& aHandler, nsAString& aDescription, nsAString& aMozillaFlags,
    bool aUserData) {
  LOG("-- LookUpHandlerAndDescription for type '%s/%s'\n",
      NS_LossyConvertUTF16toASCII(aMajorType).get(),
      NS_LossyConvertUTF16toASCII(aMinorType).get());
  nsAutoString mailcapFileName;

  const auto kind =
      aUserData ? FileKind::PrivateMailCap : FileKind::GlobalMailCap;
  nsresult rv = GetFileLocation(kind, mailcapFileName);
  if (NS_FAILED(rv) || mailcapFileName.IsEmpty()) {
    return NS_ERROR_NOT_AVAILABLE;
  }
  return GetHandlerAndDescriptionFromMailcapFile(mailcapFileName, aMajorType,
                                                 aMinorType, aHandler,
                                                 aDescription, aMozillaFlags);
}

// static
nsresult nsOSHelperAppService::GetHandlerAndDescriptionFromMailcapFile(
    const nsAString& aFilename, const nsAString& aMajorType,
    const nsAString& aMinorType, nsAString& aHandler, nsAString& aDescription,
    nsAString& aMozillaFlags) {
  LOG("-- GetHandlerAndDescriptionFromMailcapFile\n");
  LOG("Getting handler and description from mailcap file '%s'\n",
      NS_LossyConvertUTF16toASCII(aFilename).get());
  LOG("Using type '%s/%s'\n", NS_LossyConvertUTF16toASCII(aMajorType).get(),
      NS_LossyConvertUTF16toASCII(aMinorType).get());

  nsresult rv = NS_OK;
  bool more = false;

  nsCOMPtr<nsIFile> file;
  MOZ_TRY(NS_NewLocalFile(aFilename, getter_AddRefs(file)));

  nsCOMPtr<nsIFileInputStream> mailcapFile(
      do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv));
  if (NS_FAILED(rv)) return rv;
  rv = mailcapFile->Init(file, -1, -1, false);
  if (NS_FAILED(rv)) return rv;

  nsCOMPtr<nsILineInputStream> mailcap(do_QueryInterface(mailcapFile, &rv));

  if (NS_FAILED(rv)) {
    LOG("Interface trouble in stream land!");
    return rv;
  }

  nsAutoStringN<129> entry;
  nsAutoStringN<81> buffer;
  nsAutoCStringN<81> cBuffer;
  rv = mailcap->ReadLine(cBuffer, &more);
  if (NS_FAILED(rv)) {
    mailcapFile->Close();
    return rv;
  }

  do {  // return on end-of-file in the loop

    CopyASCIItoUTF16(cBuffer, buffer);
    if (!buffer.IsEmpty() && buffer.First() != '#') {
      entry.Append(buffer);
      if (entry.Last() == '\\') {  // entry continues on next line
        entry.Truncate(entry.Length() - 1);
        entry.Append(char16_t(
            ' '));  // in case there is no trailing whitespace on this line
      } else {      // we have a full entry in entry.  Check it for the type
        LOG("Current entry: '%s'\n", NS_LossyConvertUTF16toASCII(entry).get());

        nsAString::const_iterator semicolon_iter, start_iter, end_iter,
            majorTypeStart, majorTypeEnd, minorTypeStart, minorTypeEnd;
        entry.BeginReading(start_iter);
        entry.EndReading(end_iter);
        semicolon_iter = start_iter;
        FindSemicolon(semicolon_iter, end_iter);
        if (semicolon_iter !=
            end_iter) {  // we have something resembling a valid entry
          rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd,
                             minorTypeStart, minorTypeEnd, semicolon_iter);
          if (NS_SUCCEEDED(rv) &&
              Substring(majorTypeStart, majorTypeEnd)
                  .Equals(aMajorType, nsCaseInsensitiveStringComparator) &&
              Substring(minorTypeStart, minorTypeEnd)
                  .Equals(aMinorType, nsCaseInsensitiveStringComparator)) {
            // we have a match
            bool match = true;
            ++semicolon_iter;  // point at the first char past the semicolon
            start_iter = semicolon_iter;  // handler string starts here
            FindSemicolon(semicolon_iter, end_iter);
            while (start_iter != semicolon_iter &&
                   nsCRT::IsAsciiSpace(*start_iter)) {
              ++start_iter;
            }

            LOG("The real handler is: '%s'\n",
                NS_LossyConvertUTF16toASCII(
                    Substring(start_iter, semicolon_iter))
                    .get());

            // XXX ugly hack.  Just grab the executable name
            nsAString::const_iterator end_handler_iter = semicolon_iter;
            nsAString::const_iterator end_executable_iter = start_iter;
            while (end_executable_iter != end_handler_iter &&
                   !nsCRT::IsAsciiSpace(*end_executable_iter)) {
              ++end_executable_iter;
            }
            // XXX End ugly hack

            aHandler = Substring(start_iter, end_executable_iter);

            nsAString::const_iterator start_option_iter, end_optionname_iter,
                equal_sign_iter;
            bool equalSignFound;
            while (match && semicolon_iter != end_iter &&
                   ++semicolon_iter !=
                       end_iter) {  // there are options left and we still match
              start_option_iter = semicolon_iter;
              // skip over leading whitespace
              while (start_option_iter != end_iter &&
                     nsCRT::IsAsciiSpace(*start_option_iter)) {
                ++start_option_iter;
              }
              if (start_option_iter == end_iter) {  // nothing actually here
                break;
              }
              semicolon_iter = start_option_iter;
              FindSemicolon(semicolon_iter, end_iter);
              equal_sign_iter = start_option_iter;
              equalSignFound = false;
              while (equal_sign_iter != semicolon_iter && !equalSignFound) {
                switch (*equal_sign_iter) {
                  case '\\':
                    equal_sign_iter.advance(2);
                    break;
                  case '=':
                    equalSignFound = true;
                    break;
                  default:
                    ++equal_sign_iter;
                    break;
                }
              }
              end_optionname_iter = start_option_iter;
              // find end of option name
              while (end_optionname_iter != equal_sign_iter &&
                     !nsCRT::IsAsciiSpace(*end_optionname_iter)) {
                ++end_optionname_iter;
              }
              nsDependentSubstring optionName(start_option_iter,
                                              end_optionname_iter);
              if (equalSignFound) {
                // This is an option that has a name and value
                if (optionName.EqualsLiteral("description")) {
                  aDescription = Substring(++equal_sign_iter, semicolon_iter);
                } else if (optionName.EqualsLiteral("x-mozilla-flags")) {
                  aMozillaFlags = Substring(++equal_sign_iter, semicolon_iter);
                } else if (optionName.EqualsLiteral("test")) {
                  nsAutoCString testCommand;
                  rv = UnescapeCommand(
                      Substring(++equal_sign_iter, semicolon_iter), aMajorType,
                      aMinorType, testCommand);
                  if (NS_FAILED(rv)) continue;
                  nsCOMPtr<nsIProcess> process =
                      do_CreateInstance(NS_PROCESS_CONTRACTID, &rv);
                  if (NS_FAILED(rv)) continue;
                  nsCOMPtr<nsIFile> file;
                  rv =
                      NS_NewNativeLocalFile("/bin/sh"_ns, getter_AddRefs(file));
                  if (NS_FAILED(rv)) continue;
                  rv = process->Init(file);
                  if (NS_FAILED(rv)) continue;
                  const char* args[] = {"-c", testCommand.get()};
                  LOG("Running Test: %s\n", testCommand.get());
                  rv = process->Run(true, args, 2);
                  if (NS_FAILED(rv)) continue;
                  int32_t exitValue;
                  rv = process->GetExitValue(&exitValue);
                  if (NS_FAILED(rv)) continue;
                  LOG("Exit code: %d\n", exitValue);
                  if (exitValue) {
                    match = false;
                  }
                }
              } else {
                // This is an option that just has a name but no value (eg
                // "copiousoutput")
                if (optionName.EqualsLiteral("needsterminal")) {
                  match = false;
                }
              }
            }

            if (match) {  // we did not fail any test clauses; all is good
              // get out of here
              mailcapFile->Close();
              return NS_OK;
            }
            // pretend that this match never happened
            aDescription.Truncate();
            aMozillaFlags.Truncate();
            aHandler.Truncate();
          }
        }
        // zero out the entry for the next cycle
        entry.Truncate();
      }
    }
    if (!more) {
      rv = NS_ERROR_NOT_AVAILABLE;
      break;
    }
    rv = mailcap->ReadLine(cBuffer, &more);
  } while (NS_SUCCEEDED(rv));
  mailcapFile->Close();
  return rv;
}

nsresult nsOSHelperAppService::OSProtocolHandlerExists(
    const char* aProtocolScheme, bool* aHandlerExists) {
  nsresult rv = NS_OK;

  if (!XRE_IsContentProcess()) {
#ifdef MOZ_WIDGET_GTK
    // Check the GNOME registry for a protocol handler
    *aHandlerExists = nsGNOMERegistry::HandlerExists(aProtocolScheme);
#else
    *aHandlerExists = false;
#endif
  } else {
    *aHandlerExists = false;
    nsCOMPtr<nsIHandlerService> handlerSvc =
        do_GetService(NS_HANDLERSERVICE_CONTRACTID, &rv);
    if (NS_SUCCEEDED(rv) && handlerSvc) {
      rv = handlerSvc->ExistsForProtocolOS(nsCString(aProtocolScheme),
                                           aHandlerExists);
    }
  }

  return rv;
}

NS_IMETHODIMP nsOSHelperAppService::GetApplicationDescription(
    const nsACString& aScheme, nsAString& _retval) {
#ifdef MOZ_WIDGET_GTK
  nsGNOMERegistry::GetAppDescForScheme(aScheme, _retval);
  return _retval.IsEmpty() ? NS_ERROR_NOT_AVAILABLE : NS_OK;
#else
  return NS_ERROR_NOT_AVAILABLE;
#endif
}

NS_IMETHODIMP nsOSHelperAppService::IsCurrentAppOSDefaultForProtocol(
    const nsACString& aScheme, bool* _retval) {
  *_retval = false;
#if defined(MOZ_BUILD_APP_IS_BROWSER) && defined(MOZ_WIDGET_GTK)
  if (nsCOMPtr<nsIGNOMEShellService> shell =
          do_GetService(NS_TOOLKITSHELLSERVICE_CONTRACTID)) {
    return shell->IsDefaultForScheme(aScheme, _retval);
  }
#endif
  return NS_OK;
}

nsresult nsOSHelperAppService::GetFileTokenForPath(
    const char16_t* platformAppPath, nsIFile** aFile) {
  LOG("-- nsOSHelperAppService::GetFileTokenForPath: '%s'\n",
      NS_LossyConvertUTF16toASCII(platformAppPath).get());
  if (!*platformAppPath) {  // empty filename--return error
    NS_WARNING("Empty filename passed in.");
    return NS_ERROR_INVALID_ARG;
  }

  // first check if the base class implementation finds anything
  nsresult rv =
      nsExternalHelperAppService::GetFileTokenForPath(platformAppPath, aFile);
  if (NS_SUCCEEDED(rv)) return rv;
  // If the reason for failure was that the file doesn't exist, return too
  // (because it means the path was absolute, and so that we shouldn't search in
  // the path)
  if (rv == NS_ERROR_FILE_NOT_FOUND) return rv;

  // If we get here, we really should have a relative path.
  NS_ASSERTION(*platformAppPath != char16_t('/'), "Unexpected absolute path");

  bool exists = false;
  // ugly hack.  Walk the PATH variable...
  char* unixpath = PR_GetEnv("PATH");
  nsAutoCString path(unixpath);

  const char* start_iter = path.BeginReading(start_iter);
  const char* colon_iter = start_iter;
  const char* end_iter = path.EndReading(end_iter);

  nsCOMPtr<nsIFile> localFile = new nsLocalFile();
  while (start_iter != end_iter && !exists) {
    while (colon_iter != end_iter && *colon_iter != ':') {
      ++colon_iter;
    }
    rv = localFile->InitWithNativePath(Substring(start_iter, colon_iter));
    NS_ENSURE_SUCCESS(rv, rv);
    rv = localFile->AppendRelativePath(nsDependentString(platformAppPath));
    // Failing AppendRelativePath is a bad thing - it should basically always
    // succeed given a relative path. Show a warning if it does fail.
    // To prevent infinite loops when it does fail, return at this point.
    NS_ENSURE_SUCCESS(rv, rv);
    localFile->Exists(&exists);
    if (!exists) {
      if (colon_iter == end_iter) {
        break;
      }
      ++colon_iter;
      start_iter = colon_iter;
    }
  }

  if (exists) {
    rv = NS_OK;
  } else {
    rv = NS_ERROR_NOT_AVAILABLE;
  }

  *aFile = localFile;
  NS_IF_ADDREF(*aFile);

  return rv;
}

already_AddRefed<nsMIMEInfoBase> nsOSHelperAppService::GetFromExtension(
    const nsCString& aFileExt) {
  // if the extension is empty, return immediately
  if (aFileExt.IsEmpty()) {
    return nullptr;
  }

  LOG("Here we do an extension lookup for '%s'\n", aFileExt.get());

  nsAutoString majorType, minorType, mime_types_description,
      mailcap_description, handler, mozillaFlags;

  nsresult rv =
      LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType,
                               minorType, mime_types_description, true);

  if (NS_FAILED(rv) || majorType.IsEmpty()) {
#ifdef MOZ_WIDGET_GTK
    LOG("Looking in GNOME registry\n");
    RefPtr<nsMIMEInfoBase> gnomeInfo =
        nsGNOMERegistry::GetFromExtension(aFileExt);
    if (gnomeInfo) {
      LOG("Got MIMEInfo from GNOME registry\n");
      return gnomeInfo.forget();
    }
#endif

    rv = LookUpTypeAndDescription(NS_ConvertUTF8toUTF16(aFileExt), majorType,
                                  minorType, mime_types_description, false);
  }

  if (NS_FAILED(rv)) {
    return nullptr;
  }

  NS_LossyConvertUTF16toASCII asciiMajorType(majorType);
  NS_LossyConvertUTF16toASCII asciiMinorType(minorType);

  LOG("Type/Description results:  majorType='%s', minorType='%s', "
      "description='%s'\n",
      asciiMajorType.get(), asciiMinorType.get(),
      NS_LossyConvertUTF16toASCII(mime_types_description).get());

  if (majorType.IsEmpty() && minorType.IsEmpty()) {
    // we didn't get a type mapping, so we can't do anything useful
    return nullptr;
  }

  nsAutoCString mimeType(asciiMajorType + "/"_ns + asciiMinorType);
  RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(mimeType);

  mimeInfo->AppendExtension(aFileExt);
  rv = LookUpHandlerAndDescription(majorType, minorType, handler,
                                   mailcap_description, mozillaFlags);
  LOG("Handler/Description results:  handler='%s', description='%s', "
      "mozillaFlags='%s'\n",
      NS_LossyConvertUTF16toASCII(handler).get(),
      NS_LossyConvertUTF16toASCII(mailcap_description).get(),
      NS_LossyConvertUTF16toASCII(mozillaFlags).get());
  mailcap_description.Trim(" \t\"");
  mozillaFlags.Trim(" \t");
  if (!mime_types_description.IsEmpty()) {
    mimeInfo->SetDescription(mime_types_description);
  } else {
    mimeInfo->SetDescription(mailcap_description);
  }

  if (NS_SUCCEEDED(rv) && handler.IsEmpty()) {
    rv = NS_ERROR_NOT_AVAILABLE;
  }

  if (NS_SUCCEEDED(rv)) {
    nsCOMPtr<nsIFile> handlerFile;
    rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile));

    if (NS_SUCCEEDED(rv)) {
      mimeInfo->SetDefaultApplication(handlerFile);
      mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
      mimeInfo->SetDefaultDescription(handler);
    }
  }

  if (NS_FAILED(rv)) {
    mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
  }

  return mimeInfo.forget();
}

already_AddRefed<nsMIMEInfoBase> nsOSHelperAppService::GetFromType(
    const nsCString& aMIMEType) {
  // if the type is empty, return immediately
  if (aMIMEType.IsEmpty()) {
    return nullptr;
  }

  LOG("Here we do a mimetype lookup for '%s'\n", aMIMEType.get());

  // extract the major and minor types
  NS_ConvertASCIItoUTF16 mimeType(aMIMEType);
  nsAString::const_iterator start_iter, end_iter, majorTypeStart, majorTypeEnd,
      minorTypeStart, minorTypeEnd;

  mimeType.BeginReading(start_iter);
  mimeType.EndReading(end_iter);

  // XXX FIXME: add typeOptions parsing in here
  nsresult rv = ParseMIMEType(start_iter, majorTypeStart, majorTypeEnd,
                              minorTypeStart, minorTypeEnd, end_iter);

  if (NS_FAILED(rv)) {
    return nullptr;
  }

  nsDependentSubstring majorType(majorTypeStart, majorTypeEnd);
  nsDependentSubstring minorType(minorTypeStart, minorTypeEnd);

  // First check the user's private mailcap file
  nsAutoString mailcap_description, handler, mozillaFlags;
  DoLookUpHandlerAndDescription(majorType, minorType, handler,
                                mailcap_description, mozillaFlags, true);

  LOG("Private Handler/Description results:  handler='%s', description='%s'\n",
      NS_LossyConvertUTF16toASCII(handler).get(),
      NS_LossyConvertUTF16toASCII(mailcap_description).get());

  // Now look up our extensions
  nsAutoString extensions, mime_types_description;
  LookUpExtensionsAndDescription(majorType, minorType, extensions,
                                 mime_types_description);

#ifdef MOZ_WIDGET_GTK
  if (handler.IsEmpty()) {
    RefPtr<nsMIMEInfoBase> gnomeInfo = nsGNOMERegistry::GetFromType(aMIMEType);
    if (gnomeInfo) {
      LOG("Got MIMEInfo from GNOME registry without extensions; setting them "
          "to %s\n",
          NS_LossyConvertUTF16toASCII(extensions).get());

      NS_ASSERTION(!gnomeInfo->HasExtensions(), "How'd that happen?");
      gnomeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions));
      return gnomeInfo.forget();
    }
  }
#endif

  if (handler.IsEmpty()) {
    DoLookUpHandlerAndDescription(majorType, minorType, handler,
                                  mailcap_description, mozillaFlags, false);
  }

  if (handler.IsEmpty()) {
    DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler,
                                  mailcap_description, mozillaFlags, true);
  }

  if (handler.IsEmpty()) {
    DoLookUpHandlerAndDescription(majorType, u"*"_ns, handler,
                                  mailcap_description, mozillaFlags, false);
  }

  LOG("Handler/Description results:  handler='%s', description='%s', "
      "mozillaFlags='%s'\n",
      NS_LossyConvertUTF16toASCII(handler).get(),
      NS_LossyConvertUTF16toASCII(mailcap_description).get(),
      NS_LossyConvertUTF16toASCII(mozillaFlags).get());

  mailcap_description.Trim(" \t\"");
  mozillaFlags.Trim(" \t");

  if (handler.IsEmpty() && extensions.IsEmpty() &&
      mailcap_description.IsEmpty() && mime_types_description.IsEmpty()) {
    // No real useful info
    return nullptr;
  }

  RefPtr<nsMIMEInfoUnix> mimeInfo = new nsMIMEInfoUnix(aMIMEType);

  mimeInfo->SetFileExtensions(NS_ConvertUTF16toUTF8(extensions));
  if (!mime_types_description.IsEmpty()) {
    mimeInfo->SetDescription(mime_types_description);
  } else {
    mimeInfo->SetDescription(mailcap_description);
  }

  rv = NS_ERROR_NOT_AVAILABLE;
  nsCOMPtr<nsIFile> handlerFile;
  if (!handler.IsEmpty()) {
    rv = GetFileTokenForPath(handler.get(), getter_AddRefs(handlerFile));
  }

  if (NS_SUCCEEDED(rv)) {
    mimeInfo->SetDefaultApplication(handlerFile);
    mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
    mimeInfo->SetDefaultDescription(handler);
  } else {
    mimeInfo->SetPreferredAction(nsIMIMEInfo::saveToDisk);
  }

  return mimeInfo.forget();
}

nsresult nsOSHelperAppService::GetMIMEInfoFromOS(const nsACString& aType,
                                                 const nsACString& aFileExt,
                                                 bool* aFound,
                                                 nsIMIMEInfo** aMIMEInfo) {
  *aFound = true;
  RefPtr<nsMIMEInfoBase> retval;
  // Fallback to lookup by extension when generic 'application/octet-stream'
  // content type is received.
  if (!aType.EqualsLiteral(APPLICATION_OCTET_STREAM)) {
    retval = GetFromType(PromiseFlatCString(aType));
  }
  bool hasDefault = false;
  if (retval) retval->GetHasDefaultHandler(&hasDefault);
  if (!retval || !hasDefault) {
    RefPtr<nsMIMEInfoBase> miByExt =
        GetFromExtension(PromiseFlatCString(aFileExt));
    // If we had no extension match, but a type match, use that
    if (!miByExt && retval) {
      retval.forget(aMIMEInfo);
      return NS_OK;
    }
    // If we had an extension match but no type match, set the mimetype and use
    // it
    if (!retval && miByExt) {
      if (!aType.IsEmpty()) miByExt->SetMIMEType(aType);
      miByExt.swap(retval);

      retval.forget(aMIMEInfo);
      return NS_OK;
    }
    // If we got nothing, make a new mimeinfo
    if (!retval) {
      *aFound = false;
      retval = new nsMIMEInfoUnix(aType);
      if (retval) {
        if (!aFileExt.IsEmpty()) retval->AppendExtension(aFileExt);
      }

      retval.forget(aMIMEInfo);
      return NS_OK;
    }

    // Copy the attributes of retval (mimeinfo from type) onto miByExt, to
    // return it
    // but reset to just collected mDefaultAppDescription (from ext)
    nsAutoString byExtDefault;
    miByExt->GetDefaultDescription(byExtDefault);
    retval->SetDefaultDescription(byExtDefault);
    retval->CopyBasicDataTo(miByExt);

    miByExt.swap(retval);
  }
  retval.forget(aMIMEInfo);
  return NS_OK;
}

NS_IMETHODIMP
nsOSHelperAppService::GetProtocolHandlerInfoFromOS(const nsACString& aScheme,
                                                   bool* found,
                                                   nsIHandlerInfo** _retval) {
  NS_ASSERTION(!aScheme.IsEmpty(), "No scheme was specified!");

  nsresult rv =
      OSProtocolHandlerExists(nsPromiseFlatCString(aScheme).get(), found);
  if (NS_FAILED(rv)) return rv;

  nsMIMEInfoUnix* handlerInfo =
      new nsMIMEInfoUnix(aScheme, nsMIMEInfoBase::eProtocolInfo);
  NS_ENSURE_TRUE(handlerInfo, NS_ERROR_OUT_OF_MEMORY);
  NS_ADDREF(*_retval = handlerInfo);

  if (!*found) {
    // Code that calls this requires an object regardless if the OS has
    // something for us, so we return the empty object.
    return NS_OK;
  }

  nsAutoString desc;
  GetApplicationDescription(aScheme, desc);
  handlerInfo->SetDefaultDescription(desc);

  return NS_OK;
}
