// Copyright (c) 2013 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 "gn/filesystem_utils.h"

#include <algorithm>

#include "base/files/file_util.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "gn/file_writer.h"
#include "gn/location.h"
#include "gn/settings.h"
#include "gn/source_dir.h"
#include "gn/target.h"
#include "util/build_config.h"

#if defined(OS_WIN)
#include <direct.h>
#include <windows.h>
#endif

namespace {

enum DotDisposition {
  // The given dot is just part of a filename and is not special.
  NOT_A_DIRECTORY,

  // The given dot is the current directory.
  DIRECTORY_CUR,

  // The given dot is the first of a double dot that should take us up one.
  DIRECTORY_UP
};

// When we find a dot, this function is called with the character following
// that dot to see what it is. The return value indicates what type this dot is
// (see above). This code handles the case where the dot is at the end of the
// input.
//
// |*consumed_len| will contain the number of characters in the input that
// express what we found.
DotDisposition ClassifyAfterDot(const std::string& path,
                                size_t after_dot,
                                size_t* consumed_len) {
  if (after_dot == path.size()) {
    // Single dot at the end.
    *consumed_len = 1;
    return DIRECTORY_CUR;
  }
  if (IsSlash(path[after_dot])) {
    // Single dot followed by a slash.
    *consumed_len = 2;  // Consume the slash
    return DIRECTORY_CUR;
  }

  if (path[after_dot] == '.') {
    // Two dots.
    if (after_dot + 1 == path.size()) {
      // Double dot at the end.
      *consumed_len = 2;
      return DIRECTORY_UP;
    }
    if (IsSlash(path[after_dot + 1])) {
      // Double dot folowed by a slash.
      *consumed_len = 3;
      return DIRECTORY_UP;
    }
  }

  // The dots are followed by something else, not a directory.
  *consumed_len = 1;
  return NOT_A_DIRECTORY;
}

#if defined(OS_WIN)
inline char NormalizeWindowsPathChar(char c) {
  if (c == '/')
    return '\\';
  return base::ToLowerASCII(c);
}

// Attempts to do a case and slash-insensitive comparison of two 8-bit Windows
// paths.
bool AreAbsoluteWindowsPathsEqual(std::string_view a, std::string_view b) {
  if (a.size() != b.size())
    return false;

  // For now, just do a case-insensitive ASCII comparison. We could convert to
  // UTF-16 and use ICU if necessary.
  for (size_t i = 0; i < a.size(); i++) {
    if (NormalizeWindowsPathChar(a[i]) != NormalizeWindowsPathChar(b[i]))
      return false;
  }
  return true;
}

bool DoesBeginWindowsDriveLetter(std::string_view path) {
  if (path.size() < 3)
    return false;

  // Check colon first, this will generally fail fastest.
  if (path[1] != ':')
    return false;

  // Check drive letter.
  if (!base::IsAsciiAlpha(path[0]))
    return false;

  if (!IsSlash(path[2]))
    return false;
  return true;
}
#endif

// A wrapper around FilePath.GetComponents that works the way we need. This is
// not super efficient since it does some O(n) transformations on the path. If
// this is called a lot, we might want to optimize.
std::vector<base::FilePath::StringType> GetPathComponents(
    const base::FilePath& path) {
  std::vector<base::FilePath::StringType> result;
  path.GetComponents(&result);

  if (result.empty())
    return result;

  // GetComponents will preserve the "/" at the beginning, which confuses us.
  // We don't expect to have relative paths in this function.
  // Don't use IsSeparator since we always want to allow backslashes.
  if (result[0] == FILE_PATH_LITERAL("/") ||
      result[0] == FILE_PATH_LITERAL("\\"))
    result.erase(result.begin());

#if defined(OS_WIN)
  // On Windows, GetComponents will give us [ "C:", "/", "foo" ], and we
  // don't want the slash in there. This doesn't support input like "C:foo"
  // which means foo relative to the current directory of the C drive but
  // that's basically legacy DOS behavior we don't need to support.
  if (result.size() >= 2 && result[1].size() == 1 &&
      IsSlash(static_cast<char>(result[1][0])))
    result.erase(result.begin() + 1);
#endif

  return result;
}

// Provides the equivalent of == for filesystem strings, trying to do
// approximately the right thing with case.
bool FilesystemStringsEqual(const base::FilePath::StringType& a,
                            const base::FilePath::StringType& b) {
#if defined(OS_WIN)
  // Assume case-insensitive filesystems on Windows. We use the CompareString
  // function to do a case-insensitive comparison based on the current locale
  // (we don't want GN to depend on ICU which is large and requires data
  // files). This isn't perfect, but getting this perfectly right is very
  // difficult and requires I/O, and this comparison should cover 99.9999% of
  // all cases.
  //
  // Note: The documentation for CompareString says it runs fastest on
  // null-terminated strings with -1 passed for the length, so we do that here.
  // There should not be embedded nulls in filesystem strings.
  return ::CompareString(LOCALE_USER_DEFAULT, LINGUISTIC_IGNORECASE,
                         reinterpret_cast<LPCWSTR>(a.c_str()), -1,
                         reinterpret_cast<LPCWSTR>(b.c_str()),
                         -1) == CSTR_EQUAL;
#else
  // Assume case-sensitive filesystems on non-Windows.
  return a == b;
#endif
}

// Helper function for computing subdirectories in the build directory
// corresponding to absolute paths. This will try to resolve the absolute
// path as a source-relative path first, and otherwise it creates a
// special subdirectory for absolute paths to keep them from colliding with
// other generated sources and outputs.
void AppendFixedAbsolutePathSuffix(const BuildSettings* build_settings,
                                   const SourceDir& source_dir,
                                   OutputFile* result) {
  const std::string& build_dir = build_settings->build_dir().value();

  if (source_dir.value().starts_with(build_dir)) {
    size_t build_dir_size = build_dir.size();
    result->value().append(&source_dir.value()[build_dir_size],
                           source_dir.value().size() - build_dir_size);
  } else {
    result->value().append("ABS_PATH");
#if defined(OS_WIN)
    // Windows absolute path contains ':' after drive letter. Remove it to
    // avoid inserting ':' in the middle of path (eg. "ABS_PATH/C:/").
    std::string src_dir_value = source_dir.value();
    const auto colon_pos = src_dir_value.find(':');
    if (colon_pos != std::string::npos)
      src_dir_value.erase(src_dir_value.begin() + colon_pos);
#else
    const std::string& src_dir_value = source_dir.value();
#endif
    result->value().append(src_dir_value);
  }
}

size_t AbsPathLenWithNoTrailingSlash(std::string_view path) {
  size_t len = path.size();
#if defined(OS_WIN)
  size_t min_len = 3;
#else
  // On posix system. The minimal abs path is "/".
  size_t min_len = 1;
#endif
  for (; len > min_len && IsSlash(path[len - 1]); len--)
    ;
  return len;
}
}  // namespace

std::string FilePathToUTF8(const base::FilePath::StringType& str) {
#if defined(OS_WIN)
  return base::UTF16ToUTF8(str);
#else
  return str;
#endif
}

base::FilePath UTF8ToFilePath(std::string_view sp) {
#if defined(OS_WIN)
  return base::FilePath(base::UTF8ToUTF16(sp));
#else
  return base::FilePath(sp);
#endif
}

size_t FindExtensionOffset(const std::string& path) {
  for (int i = static_cast<int>(path.size()); i >= 0; i--) {
    if (IsSlash(path[i]))
      break;
    if (path[i] == '.')
      return i + 1;
  }
  return std::string::npos;
}

std::string_view FindExtension(const std::string* path) {
  size_t extension_offset = FindExtensionOffset(*path);
  if (extension_offset == std::string::npos)
    return std::string_view();
  return std::string_view(&path->data()[extension_offset],
                          path->size() - extension_offset);
}

size_t FindFilenameOffset(const std::string& path) {
  for (int i = static_cast<int>(path.size()) - 1; i >= 0; i--) {
    if (IsSlash(path[i]))
      return i + 1;
  }
  return 0;  // No filename found means everything was the filename.
}

std::string_view FindFilename(const std::string* path) {
  size_t filename_offset = FindFilenameOffset(*path);
  if (filename_offset == 0)
    return std::string_view(*path);  // Everything is the file name.
  return std::string_view(&(*path).data()[filename_offset],
                          path->size() - filename_offset);
}

std::string_view FindFilenameNoExtension(const std::string* path) {
  if (path->empty())
    return std::string_view();
  size_t filename_offset = FindFilenameOffset(*path);
  size_t extension_offset = FindExtensionOffset(*path);

  size_t name_len;
  if (extension_offset == std::string::npos)
    name_len = path->size() - filename_offset;
  else
    name_len = extension_offset - filename_offset - 1;

  return std::string_view(&(*path).data()[filename_offset], name_len);
}

void RemoveFilename(std::string* path) {
  path->resize(FindFilenameOffset(*path));
}

bool EndsWithSlash(std::string_view s) {
  return !s.empty() && IsSlash(s[s.size() - 1]);
}

std::string_view FindDir(const std::string* path) {
  size_t filename_offset = FindFilenameOffset(*path);
  if (filename_offset == 0u)
    return std::string_view();
  return std::string_view(path->data(), filename_offset);
}

std::string_view FindLastDirComponent(const SourceDir& dir) {
  const std::string& dir_string = dir.value();

  if (dir_string.empty())
    return std::string_view();
  int cur = static_cast<int>(dir_string.size()) - 1;
  DCHECK(dir_string[cur] == '/');
  int end = cur;
  cur--;  // Skip before the last slash.

  for (; cur >= 0; cur--) {
    if (dir_string[cur] == '/')
      return std::string_view(&dir_string[cur + 1], end - cur - 1);
  }
  return std::string_view(&dir_string[0], end);
}

bool IsStringInOutputDir(const SourceDir& output_dir, const std::string& str) {
  // This check will be wrong for all proper prefixes "e.g. "/output" will
  // match "/out" but we don't really care since this is just a sanity check.
  const std::string& dir_str = output_dir.value();
  return str.compare(0, dir_str.length(), dir_str) == 0;
}

bool EnsureStringIsInOutputDir(const SourceDir& output_dir,
                               const std::string& str,
                               const ParseNode* origin,
                               Err* err) {
  if (IsStringInOutputDir(output_dir, str))
    return true;  // Output directory is hardcoded.

  *err = Err(
      origin, "File is not inside output directory.",
      "The given file should be in the output directory. Normally you would "
      "specify\n\"$target_out_dir/foo\" or "
      "\"$target_gen_dir/foo\". I interpreted this as\n\"" +
          str + "\".");
  return false;
}

bool IsPathAbsolute(std::string_view path) {
  if (path.empty())
    return false;

  if (!IsSlash(path[0])) {
#if defined(OS_WIN)
    // Check for Windows system paths like "C:\foo".
    if (path.size() > 2 && path[1] == ':' && IsSlash(path[2]))
      return true;
#endif
    return false;  // Doesn't begin with a slash, is relative.
  }

  // Double forward slash at the beginning means source-relative (we don't
  // allow backslashes for denoting this).
  if (path.size() > 1 && path[1] == '/')
    return false;

  return true;
}

bool IsPathSourceAbsolute(std::string_view path) {
  return (path.size() >= 2 && path[0] == '/' && path[1] == '/');
}

bool MakeAbsolutePathRelativeIfPossible(std::string_view source_root,
                                        std::string_view path,
                                        std::string* dest) {
  DCHECK(IsPathAbsolute(source_root));
  DCHECK(IsPathAbsolute(path));

  dest->clear();

  // There is no specification of how many slashes may be at the end of
  // source_root or path. Trim them off for easier string manipulation.
  size_t path_len = AbsPathLenWithNoTrailingSlash(path);
  size_t source_root_len = AbsPathLenWithNoTrailingSlash(source_root);

  if (source_root_len > path_len)
    return false;  // The source root is longer: the path can never be inside.
#if defined(OS_WIN)
  // Source root should be canonical on Windows. Note that the initial slash
  // must be forward slash, but that the other ones can be either forward or
  // backward.
  DCHECK(source_root.size() > 2 && source_root[0] != '/' &&
         source_root[1] == ':' && IsSlash(source_root[2]));

  size_t after_common_index = std::string::npos;
  if (DoesBeginWindowsDriveLetter(path)) {
    // Handle "C:\foo"
    if (AreAbsoluteWindowsPathsEqual(source_root.substr(0, source_root_len),
                                     path.substr(0, source_root_len))) {
      after_common_index = source_root_len;
      if (path_len == source_root_len) {
        *dest = "//";
        return true;
      }
    } else {
      return false;
    }
  } else if (path[0] == '/' && source_root_len <= path_len - 1 &&
             DoesBeginWindowsDriveLetter(path.substr(1))) {
    // Handle "/C:/foo"
    if (AreAbsoluteWindowsPathsEqual(source_root.substr(0, source_root_len),
                                     path.substr(1, source_root_len))) {
      after_common_index = source_root_len + 1;
      if (path_len + 1 == source_root_len) {
        *dest = "//";
        return true;
      }
    } else {
      return false;
    }
  } else {
    return false;
  }

  // If we get here, there's a match and after_common_index identifies the
  // part after it.

  if (!IsSlash(path[after_common_index])) {
    // path is ${source-root}SUFFIX/...
    return false;
  }
  // A source-root relative path, The input may have an unknown number of
  // slashes after the previous match. Skip over them.
  size_t first_after_slash = after_common_index + 1;
  while (first_after_slash < path_len && IsSlash(path[first_after_slash]))
    first_after_slash++;
  dest->assign("//");  // Result is source root relative.
  dest->append(&path.data()[first_after_slash],
               path.size() - first_after_slash);
  return true;

#else

  // On non-Windows this is easy. Since we know both are absolute, just do a
  // prefix check.

  if (path.substr(0, source_root_len) ==
      source_root.substr(0, source_root_len)) {
    if (path_len == source_root_len) {
      // path is equivalent to source_root.
      *dest = "//";
      return true;
    } else if (!IsSlash(path[source_root_len])) {
      // path is ${source-root}SUFFIX/...
      return false;
    }
    // A source-root relative path, The input may have an unknown number of
    // slashes after the previous match. Skip over them.
    size_t first_after_slash = source_root_len + 1;
    while (first_after_slash < path_len && IsSlash(path[first_after_slash]))
      first_after_slash++;

    dest->assign("//");  // Result is source root relative.
    dest->append(&path.data()[first_after_slash],
                 path.size() - first_after_slash);
    return true;
  }
  return false;
#endif
}

base::FilePath MakeAbsoluteFilePathRelativeIfPossible(
    const base::FilePath& base,
    const base::FilePath& target) {
  DCHECK(base.IsAbsolute());
  DCHECK(target.IsAbsolute());
  std::vector<base::FilePath::StringType> base_components;
  std::vector<base::FilePath::StringType> target_components;
  base.GetComponents(&base_components);
  target.GetComponents(&target_components);
#if defined(OS_WIN)
  // On Windows, it's impossible to have a relative path from C:\foo to D:\bar,
  // so return the target as an absolute path instead.
  if (base_components[0] != target_components[0])
    return target;

  // GetComponents() returns the first slash after the root. Set it to the
  // same value in both component lists so that relative paths between
  // "C:/foo/..." and "C:\foo\..." are computed correctly.
  target_components[1] = base_components[1];
#endif
  size_t i;
  for (i = 0; i < base_components.size() && i < target_components.size(); i++) {
    if (base_components[i] != target_components[i])
      break;
  }
  std::vector<base::FilePath::StringType> relative_components;
  for (size_t j = i; j < base_components.size(); j++)
    relative_components.push_back(base::FilePath::kParentDirectory);
  for (size_t j = i; j < target_components.size(); j++)
    relative_components.push_back(target_components[j]);
  if (relative_components.size() <= 1) {
    // In case the file pointed-to is an executable, prepend the current
    // directory to the path -- if the path was "gn", use "./gn" instead.  If
    // the file path is used in a command, this prevents issues where "gn" might
    // not be in the PATH (or it is in the path, and the wrong gn is used).
    relative_components.insert(relative_components.begin(),
                               base::FilePath::kCurrentDirectory);
  }
  // base::FilePath::Append(component) replaces the file path with |component|
  // if the path is base::Filepath::kCurrentDirectory.  We want to preserve the
  // leading "./", so we build the path ourselves and use that to construct the
  // base::FilePath.
  base::FilePath::StringType separator(&base::FilePath::kSeparators[0], 1);
  return base::FilePath(base::JoinString(relative_components, separator));
}

void NormalizePath(std::string* path, std::string_view source_root) {
  char* pathbuf = path->empty() ? nullptr : &(*path)[0];

  // top_index is the first character we can modify in the path. Anything
  // before this indicates where the path is relative to.
  size_t top_index = 0;
  bool is_relative = true;
  if (!path->empty() && pathbuf[0] == '/') {
    is_relative = false;

    if (path->size() > 1 && pathbuf[1] == '/') {
      // Two leading slashes, this is a path into the source dir.
      top_index = 2;
    } else {
      // One leading slash, this is a system-absolute path.
      top_index = 1;
    }
  }

  size_t dest_i = top_index;
  for (size_t src_i = top_index; src_i < path->size(); /* nothing */) {
    if (pathbuf[src_i] == '.') {
      if (src_i == 0 || IsSlash(pathbuf[src_i - 1])) {
        // Slash followed by a dot, see if it's something special.
        size_t consumed_len;
        switch (ClassifyAfterDot(*path, src_i + 1, &consumed_len)) {
          case NOT_A_DIRECTORY:
            // Copy the dot to the output, it means nothing special.
            pathbuf[dest_i++] = pathbuf[src_i++];
            break;
          case DIRECTORY_CUR:
            // Current directory, just skip the input.
            src_i += consumed_len;
            break;
          case DIRECTORY_UP:
            // Back up over previous directory component. If we're already
            // at the top, preserve the "..".
            if (dest_i > top_index) {
              // The previous char was a slash, remove it.
              dest_i--;
            }

            if (dest_i == top_index) {
              if (is_relative) {
                // We're already at the beginning of a relative input, copy the
                // ".." and continue. We need the trailing slash if there was
                // one before (otherwise we're at the end of the input).
                pathbuf[dest_i++] = '.';
                pathbuf[dest_i++] = '.';
                if (consumed_len == 3)
                  pathbuf[dest_i++] = '/';

                // This also makes a new "root" that we can't delete by going
                // up more levels.  Otherwise "../.." would collapse to
                // nothing.
                top_index = dest_i;
              } else if (top_index == 2 && !source_root.empty()) {
                // |path| was passed in as a source-absolute path. Prepend
                // |source_root| to make |path| absolute. |source_root| must not
                // end with a slash unless we are at root.
                DCHECK(source_root.size() == 1u ||
                       !IsSlash(source_root[source_root.size() - 1u]));
                size_t source_root_len = source_root.size();

#if defined(OS_WIN)
                // On Windows, if the source_root does not start with a slash,
                // append one here for consistency.
                if (!IsSlash(source_root[0])) {
                  path->insert(0, "/" + std::string(source_root));
                  source_root_len++;
                } else {
                  path->insert(0, source_root.data(), source_root_len);
                }

                // Normalize slashes in source root portion.
                for (size_t i = 0; i < source_root_len; ++i) {
                  if ((*path)[i] == '\\')
                    (*path)[i] = '/';
                }
#else
                path->insert(0, source_root.data(), source_root_len);
#endif

                // |path| is now absolute, so |top_index| is 1. |dest_i| and
                // |src_i| should be incremented to keep the same relative
                // position. Consume the leading "//" by decrementing |dest_i|.
                top_index = 1;
                pathbuf = &(*path)[0];
                dest_i += source_root_len - 2;
                src_i += source_root_len;

                // Just find the previous slash or the beginning of input.
                while (dest_i > 0 && !IsSlash(pathbuf[dest_i - 1]))
                  dest_i--;
              }
              // Otherwise we're at the beginning of a system-absolute path, or
              // a source-absolute path for which we don't know the absolute
              // path. Don't allow ".." to go up another level, and just eat it.
            } else {
              // Just find the previous slash or the beginning of input.
              while (dest_i > 0 && !IsSlash(pathbuf[dest_i - 1]))
                dest_i--;
            }
            src_i += consumed_len;
        }
      } else {
        // Dot not preceded by a slash, copy it literally.
        pathbuf[dest_i++] = pathbuf[src_i++];
      }
    } else if (IsSlash(pathbuf[src_i])) {
      if (src_i > 0 && IsSlash(pathbuf[src_i - 1])) {
        // Two slashes in a row, skip over it.
        src_i++;
      } else {
        // Just one slash, copy it, normalizing to forward slash.
        pathbuf[dest_i] = '/';
        dest_i++;
        src_i++;
      }
    } else {
      // Input nothing special, just copy it.
      pathbuf[dest_i++] = pathbuf[src_i++];
    }
  }
  path->resize(dest_i);
}

void ConvertPathToSystem(std::string* path) {
#if defined(OS_WIN)
  for (size_t i = 0; i < path->size(); i++) {
    if ((*path)[i] == '/')
      (*path)[i] = '\\';
  }
#endif
}

#if defined(OS_WIN)
std::string GetPathWithDriveLetter(std::string_view path) {
  if (!IsPathAbsolute(path) || !IsSlash(path[0]))
    return std::string(path);

  int drive = _getdrive();
  DCHECK(drive > 0 && drive <= 26);

  std::string ret;
  ret.reserve(2 + path.size());
  ret.push_back('A' + drive - 1);
  ret.push_back(':');
  ret += path;
  return ret;
}

// Regulate path if it is an absolute path.
std::string RegulatePathIfAbsolute(std::string_view path) {
  CHECK(!path.empty());
  bool is_start_slash = IsSlash(path[0]);

  // 1. /C:/ -> C:/
  if (path.size() > 3 && is_start_slash && base::IsAsciiAlpha(path[1]) &&
      path[2] == ':') {
    return RegulatePathIfAbsolute(path.substr(1));
  }

  bool is_path_absolute = IsPathAbsolute(path);

  // 2. /Path -> ($PWD's Drive):/Path
  if (is_path_absolute && is_start_slash) {
    return GetPathWithDriveLetter(path);
  }

  // 3. c:/ -> C:/
  std::string ret(path);
  if (is_path_absolute && !is_start_slash) {
    ret[0] = base::ToUpperASCII(path[0]);
  }

  return ret;
}
#endif

std::string MakeRelativePath(std::string_view input, std::string_view dest) {
#if defined(OS_WIN)
  // Regulate the paths.
  std::string input_regulated = RegulatePathIfAbsolute(input);
  std::string dest_regulated = RegulatePathIfAbsolute(dest);

  input = input_regulated;
  dest = dest_regulated;

  // On Windows, it is invalid to make a relative path across different
  // drive letters. A relative path cannot span over different drives.
  // For example:
  //    Input          : D:/Path/Any/Where
  //    Dest           : C:/Path/In/Another/Drive
  //    Invalid Result : ../../../../../D:/Path/Any/Where
  //    Correct Result : D:/Path/Any/Where
  // It will at least make ninja fail.
  // See: https://bugs.chromium.org/p/gn/issues/detail?id=317
  if (IsPathAbsolute(input) && IsPathAbsolute(dest) && input.size() > 1 &&
      dest.size() > 1) {
    if (input[0] != dest[0]) {
      // If the drive letters are differnet, we have no choice but use
      // the absolute path of input for correctness.
      return input_regulated;
    }
  }
#endif

  DCHECK(EndsWithSlash(dest));
  std::string ret;

  // Skip the common prefixes of the source and dest as long as they end in
  // a [back]slash or end the string. dest always ends with a (back)slash in
  // this function, so checking dest for just that is sufficient.
  size_t common_prefix_len = 0;
  size_t max_common_length = std::min(input.size(), dest.size());
  for (size_t i = common_prefix_len; i <= max_common_length; i++) {
    if (dest.size() == i)
      break;
    if ((input.size() == i || IsSlash(input[i])) && IsSlash(dest[i]))
      common_prefix_len = i + 1;
    else if (input.size() == i || input[i] != dest[i])
      break;
  }

  // Invert the dest dir starting from the end of the common prefix.
  for (size_t i = common_prefix_len; i < dest.size(); i++) {
    if (IsSlash(dest[i]))
      ret.append("../");
  }

  // Append any remaining unique input.
  if (common_prefix_len <= input.size())
    ret.append(input.begin() + common_prefix_len, input.end());
  else if (input.back() != '/' && !ret.empty())
    ret.pop_back();

  // If the result is still empty, the paths are the same.
  if (ret.empty())
    ret.push_back('.');

  return ret;
}

std::string RebasePath(const std::string& input,
                       const SourceDir& dest_dir,
                       std::string_view source_root) {
  std::string ret;
  DCHECK(source_root.empty() || !source_root.ends_with("/"));

  bool input_is_source_path =
      (input.size() >= 2 && input[0] == '/' && input[1] == '/');

  if (!source_root.empty() &&
      (!input_is_source_path || !dest_dir.is_source_absolute())) {
    std::string input_full;
    std::string dest_full;
    if (input_is_source_path) {
      input_full.append(source_root);
      input_full.push_back('/');
      input_full.append(input, 2, std::string::npos);
    } else {
      input_full.append(input);
    }
    if (dest_dir.is_source_absolute()) {
      dest_full.append(source_root);
      dest_full.push_back('/');
      dest_full.append(dest_dir.value(), 2, std::string::npos);
    } else {
#if defined(OS_WIN)
      // On Windows, SourceDir system-absolute paths start
      // with /, e.g. "/C:/foo/bar".
      const std::string& value = dest_dir.value();
      if (value.size() > 2 && value[2] == ':')
        dest_full.append(dest_dir.value().substr(1));
      else
        dest_full.append(dest_dir.value());
#else
      dest_full.append(dest_dir.value());
#endif
    }
    bool remove_slash = false;
    if (!EndsWithSlash(input_full)) {
      input_full.push_back('/');
      remove_slash = true;
    }
    ret = MakeRelativePath(input_full, dest_full);
    if (remove_slash && ret.size() > 1)
      ret.pop_back();
    return ret;
  }

  ret = MakeRelativePath(input, dest_dir.value());
  return ret;
}

base::FilePath ResolvePath(const std::string& value,
                           bool as_file,
                           const base::FilePath& source_root) {
  if (value.empty())
    return base::FilePath();

  std::string converted;
  if (!IsPathSourceAbsolute(value)) {
    if (value.size() > 2 && value[2] == ':') {
      // Windows path, strip the leading slash.
      converted.assign(&value[1], value.size() - 1);
    } else {
      converted.assign(value);
    }
    return base::FilePath(UTF8ToFilePath(converted));
  }

  // String the double-leading slash for source-relative paths.
  converted.assign(&value[2], value.size() - 2);

  if (as_file && source_root.empty())
    return UTF8ToFilePath(converted).NormalizePathSeparatorsTo('/');

  return source_root.Append(UTF8ToFilePath(converted))
      .NormalizePathSeparatorsTo('/');
}

std::string ResolveRelative(std::string_view input,
                            const std::string& value,
                            bool as_file,
                            std::string_view source_root) {
  std::string result;

  if (input.size() >= 2 && input[0] == '/' && input[1] == '/') {
    // Source-relative.
    result.assign(input.data(), input.size());
    if (!as_file) {
      if (!EndsWithSlash(result))
        result.push_back('/');
    }
    NormalizePath(&result, source_root);
    return result;
  } else if (IsPathAbsolute(input)) {
    if (source_root.empty() ||
        !MakeAbsolutePathRelativeIfPossible(source_root, input, &result)) {
#if defined(OS_WIN)
      if (input[0] != '/')  // See the file case for why we do this check.
        result = "/";
#endif
      result.append(input.data(), input.size());
    }
    NormalizePath(&result);
    if (!as_file) {
      if (!EndsWithSlash(result))
        result.push_back('/');
    }
    return result;
  }

  if (!source_root.empty()) {
    std::string absolute =
        FilePathToUTF8(ResolvePath(value, as_file, UTF8ToFilePath(source_root))
                           .AppendASCII(input)
                           .value());
    NormalizePath(&absolute);
    if (!MakeAbsolutePathRelativeIfPossible(source_root, absolute, &result)) {
#if defined(OS_WIN)
      if (absolute[0] != '/')  // See the file case for why we do this check.
        result = "/";
#endif
      result.append(absolute.data(), absolute.size());
    }
    if (!as_file && !EndsWithSlash(result))
      result.push_back('/');
    return result;
  }

  // With no source_root, there's nothing we can do about
  // e.g. input=../../../path/to/file and value=//source and we'll
  // erroneously return //file.
  result.reserve(value.size() + input.size());
  result.assign(value);
  result.append(input.data(), input.size());

  NormalizePath(&result);
  if (!as_file && !EndsWithSlash(result))
    result.push_back('/');

  return result;
}

std::string DirectoryWithNoLastSlash(const SourceDir& dir) {
  std::string ret;

  if (dir.value().empty()) {
    // Just keep input the same.
  } else if (dir.value() == "/") {
    ret.assign("/.");
  } else if (dir.value() == "//") {
    ret.assign("//.");
  } else {
    ret.assign(dir.value());
    ret.resize(ret.size() - 1);
  }
  return ret;
}

SourceDir SourceDirForPath(const base::FilePath& source_root,
                           const base::FilePath& path) {
  std::vector<base::FilePath::StringType> source_comp =
      GetPathComponents(source_root);
  std::vector<base::FilePath::StringType> path_comp = GetPathComponents(path);

  // See if path is inside the source root by looking for each of source root's
  // components at the beginning of path.
  bool is_inside_source;
  if (path_comp.size() < source_comp.size() || source_root.empty()) {
    // Too small to fit.
    is_inside_source = false;
  } else {
    is_inside_source = true;
    for (size_t i = 0; i < source_comp.size(); i++) {
      if (!FilesystemStringsEqual(source_comp[i], path_comp[i])) {
        is_inside_source = false;
        break;
      }
    }
  }

  std::string result_str;
  size_t initial_path_comp_to_use;
  if (is_inside_source) {
    // Construct a source-relative path beginning in // and skip all of the
    // shared directories.
    result_str = "//";
    initial_path_comp_to_use = source_comp.size();
  } else {
    // Not inside source code, construct a system-absolute path.
    result_str = "/";
    initial_path_comp_to_use = 0;
  }

  for (size_t i = initial_path_comp_to_use; i < path_comp.size(); i++) {
    result_str.append(FilePathToUTF8(path_comp[i]));
    result_str.push_back('/');
  }
  return SourceDir(std::move(result_str));
}

SourceDir SourceDirForCurrentDirectory(const base::FilePath& source_root) {
  base::FilePath cd;
  base::GetCurrentDirectory(&cd);
  return SourceDirForPath(source_root, cd);
}

std::string GetOutputSubdirName(const Label& toolchain_label, bool is_default) {
  // The default toolchain has no subdir.
  if (is_default)
    return std::string();

  // For now just assume the toolchain name is always a valid dir name. We may
  // want to clean up the in the future.
  return toolchain_label.name() + "/";
}

bool ContentsEqual(const base::FilePath& file_path, const std::string& data) {
  // Compare file and stream sizes first. Quick and will save us some time if
  // they are different sizes.
  int64_t file_size;
  if (!base::GetFileSize(file_path, &file_size) ||
      static_cast<size_t>(file_size) != data.size()) {
    return false;
  }

  std::string file_data;
  file_data.resize(file_size);
  if (!base::ReadFileToString(file_path, &file_data))
    return false;

  return file_data == data;
}

bool WriteFile(const base::FilePath& file_path,
               const std::string& data,
               Err* err) {
  // Create the directory if necessary.
  if (!base::CreateDirectory(file_path.DirName())) {
    if (err) {
      *err =
          Err(Location(), "Unable to create directory.",
              "I was using \"" + FilePathToUTF8(file_path.DirName()) + "\".");
    }
    return false;
  }

  FileWriter writer;
  writer.Create(file_path);
  writer.Write(data);
  bool write_success = writer.Close();

  if (!write_success && err) {
    *err = Err(Location(), "Unable to write file.",
               "I was writing \"" + FilePathToUTF8(file_path) + "\".");
  }

  return write_success;
}

BuildDirContext::BuildDirContext(const Target* target)
    : BuildDirContext(target->settings()) {}

BuildDirContext::BuildDirContext(const Settings* settings)
    : BuildDirContext(settings->build_settings(),
                      settings->toolchain_label(),
                      settings->is_default()) {}

BuildDirContext::BuildDirContext(const Scope* execution_scope)
    : BuildDirContext(execution_scope->settings()) {}

BuildDirContext::BuildDirContext(const Scope* execution_scope,
                                 const Label& toolchain_label)
    : BuildDirContext(execution_scope->settings()->build_settings(),
                      toolchain_label,
                      execution_scope->settings()->default_toolchain_label() ==
                          toolchain_label) {}

BuildDirContext::BuildDirContext(const BuildSettings* in_build_settings,
                                 const Label& in_toolchain_label,
                                 bool in_is_default_toolchain)
    : build_settings(in_build_settings),
      toolchain_label(in_toolchain_label),
      is_default_toolchain(in_is_default_toolchain) {}

SourceDir GetBuildDirAsSourceDir(const BuildDirContext& context,
                                 BuildDirType type) {
  return GetBuildDirAsOutputFile(context, type)
      .AsSourceDir(context.build_settings);
}

OutputFile GetBuildDirAsOutputFile(const BuildDirContext& context,
                                   BuildDirType type) {
  OutputFile result(GetOutputSubdirName(context.toolchain_label,
                                        context.is_default_toolchain));
  DCHECK(result.value().empty() || result.value().back() == '/');

  if (type == BuildDirType::GEN)
    result.value().append("gen/");
  else if (type == BuildDirType::OBJ)
    result.value().append("obj/");
  else if (type == BuildDirType::PHONY)
    result.value().append("phony/");
  return result;
}

SourceDir GetSubBuildDirAsSourceDir(const BuildDirContext& context,
                                    const SourceDir& source_dir,
                                    BuildDirType type) {
  return GetSubBuildDirAsOutputFile(context, source_dir, type)
      .AsSourceDir(context.build_settings);
}

OutputFile GetSubBuildDirAsOutputFile(const BuildDirContext& context,
                                      const SourceDir& source_dir,
                                      BuildDirType type) {
  DCHECK(type != BuildDirType::TOOLCHAIN_ROOT);
  OutputFile result = GetBuildDirAsOutputFile(context, type);

  if (source_dir.is_source_absolute()) {
    std::string_view build_dir = context.build_settings->build_dir().value();
    std::string_view source_dir_path = source_dir.value();
    if (source_dir_path.substr(0, build_dir.size()) == build_dir) {
      // The source dir is source-absolute, but in the build directory
      // (e.g. `//out/Debug/gen/src/foo.cc` or
      // `//out/Debug/toolchain1/gen/foo.cc`), which happens for generated
      // sources. In this case, remove the build directory prefix, and replace
      // it with `BUILD_DIR`. This will create results like `obj/BUILD_DIR/gen`
      // or `toolchain2/obj/BUILD_DIR/toolchain1/gen` which look surprising,
      // but guarantee unicity.
      result.value().append("BUILD_DIR/");
      result.value().append(source_dir_path.substr(build_dir.size()));

    } else {
      // The source dir is source-absolute, so we trim off the two leading
      // slashes to append to the toolchain object directory.
      result.value().append(&source_dir.value()[2],
                            source_dir.value().size() - 2);
    }
  } else {
    // System-absolute.
    AppendFixedAbsolutePathSuffix(context.build_settings, source_dir, &result);
  }
  return result;
}

SourceDir GetBuildDirForTargetAsSourceDir(const Target* target,
                                          BuildDirType type) {
  return GetSubBuildDirAsSourceDir(BuildDirContext(target),
                                   target->label().dir(), type);
}

OutputFile GetBuildDirForTargetAsOutputFile(const Target* target,
                                            BuildDirType type) {
  return GetSubBuildDirAsOutputFile(BuildDirContext(target),
                                    target->label().dir(), type);
}

SourceDir GetScopeCurrentBuildDirAsSourceDir(const Scope* scope,
                                             BuildDirType type) {
  if (type == BuildDirType::TOOLCHAIN_ROOT)
    return GetBuildDirAsSourceDir(BuildDirContext(scope), type);
  return GetSubBuildDirAsSourceDir(BuildDirContext(scope),
                                   scope->GetSourceDir(), type);
}
