// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "storage/browser/file_system/file_system_url.h"

#include <compare>
#include <sstream>

#include "base/check.h"
#include "base/files/safe_base_name.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/strings/escape.h"
#include "base/strings/string_util.h"
#include "storage/common/file_system/file_system_types.h"
#include "storage/common/file_system/file_system_util.h"
#include "third_party/blink/public/common/storage_key/storage_key.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace storage {

namespace {

bool AreSameStorageKey(const FileSystemURL& a, const FileSystemURL& b) {
  // TODO(crbug.com/40249324): Make the `storage_key_` member optional.
  // This class improperly uses a StorageKey with an opaque origin to indicate a
  // lack of origin for FileSystemURLs corresponding to non-sandboxed file
  // systems. This leads to unexpected behavior when comparing two non-sandboxed
  // FileSystemURLs which differ only in the nonce of their default-constructed
  // StorageKey.
  return a.storage_key() == b.storage_key() ||
         (a.type() == b.type() &&
          (a.type() == storage::kFileSystemTypeExternal ||
           a.type() == storage::kFileSystemTypeLocal) &&
          a.storage_key().origin().opaque() &&
          b.storage_key().origin().opaque());
}

}  // namespace

FileSystemURL::FileSystemURL()
    : is_null_(true),
      is_valid_(false),
      mount_type_(kFileSystemTypeUnknown),
      type_(kFileSystemTypeUnknown),
      mount_option_(FlushPolicy::NO_FLUSH_ON_COMPLETION) {}

FileSystemURL::FileSystemURL(const FileSystemURL&) = default;

FileSystemURL::FileSystemURL(FileSystemURL&&) noexcept = default;

FileSystemURL& FileSystemURL::operator=(const FileSystemURL&) = default;

FileSystemURL& FileSystemURL::operator=(FileSystemURL&&) noexcept = default;

FileSystemURL::~FileSystemURL() = default;

FileSystemURL FileSystemURL::CreateSibling(
    const base::SafeBaseName& sibling_name) const {
  const base::FilePath& new_base_name = sibling_name.path();
  if (!is_valid_ ||
      new_base_name.empty()
#if BUILDFLAG(IS_ANDROID)
      // Android content-URIs do not support siblings.
      || path().IsContentUri()
#endif
  ) {
    return FileSystemURL();
  }

  const base::FilePath old_base_name = VirtualPath::BaseName(virtual_path_);
  if (!path_.empty() && (path_.BaseName() != old_base_name)) {
    return FileSystemURL();
  }

  FileSystemURL sibling(*this);
  sibling.virtual_path_ =
      VirtualPath::DirName(virtual_path_).Append(new_base_name);
  if (!path_.empty()) {
    sibling.path_ = path_.DirName().Append(new_base_name);
  }
  return sibling;
}

// static
FileSystemURL FileSystemURL::CreateForTest(const GURL& url) {
  return FileSystemURL(
      url, blink::StorageKey::CreateFirstParty(url::Origin::Create(url)));
}

// static
FileSystemURL FileSystemURL::CreateForTest(const blink::StorageKey& storage_key,
                                           FileSystemType mount_type,
                                           const base::FilePath& virtual_path) {
  return FileSystemURL(storage_key, mount_type, virtual_path);
}

// static
FileSystemURL FileSystemURL::CreateForTest(
    const blink::StorageKey& storage_key,
    FileSystemType mount_type,
    const base::FilePath& virtual_path,
    const std::string& mount_filesystem_id,
    FileSystemType cracked_type,
    const base::FilePath& cracked_path,
    const std::string& filesystem_id,
    const FileSystemMountOption& mount_option) {
  return FileSystemURL(storage_key, mount_type, virtual_path,
                       mount_filesystem_id, cracked_type, cracked_path,
                       filesystem_id, mount_option);
}

// static
bool FileSystemURL::TypeImpliesPathIsReal(FileSystemType type) {
  switch (type) {
    // Public enum values, also exposed to JavaScript.
    case kFileSystemTypeTemporary:
    case kFileSystemTypePersistent:
    case kFileSystemTypeIsolated:
    case kFileSystemTypeExternal:
      break;

      // Everything else is a private (also known as internal) enum value.

    case kFileSystemInternalTypeEnumStart:
    case kFileSystemInternalTypeEnumEnd:
      NOTREACHED();

    case kFileSystemTypeLocal:
    case kFileSystemTypeLocalMedia:
    case kFileSystemTypeLocalForPlatformApp:
    case kFileSystemTypeDriveFs:
    case kFileSystemTypeSmbFs:
    case kFileSystemTypeFuseBox:
      return true;

    case kFileSystemTypeUnknown:
    case kFileSystemTypeTest:
    case kFileSystemTypeDragged:
    case kFileSystemTypeDeviceMedia:
    case kFileSystemTypeSyncable:
    case kFileSystemTypeSyncableForInternalSync:
    case kFileSystemTypeForTransientFile:
    case kFileSystemTypeProvided:
    case kFileSystemTypeDeviceMediaAsFileStorage:
    case kFileSystemTypeArcContent:
    case kFileSystemTypeArcDocumentsProvider:
      break;

      // We don't use a "default:" case. Whenever `FileSystemType` gains a
      // new enum value, raise a compiler error (with -Werror,-Wswitch) unless
      // this switch statement is also updated.
  }
  return false;
}

FileSystemURL::FileSystemURL(const GURL& url,
                             const blink::StorageKey& storage_key)
    : is_null_(false),
      mount_type_(kFileSystemTypeUnknown),
      type_(kFileSystemTypeUnknown),
      mount_option_(FlushPolicy::NO_FLUSH_ON_COMPLETION) {
  GURL origin_url;
  // URL should be able to be parsed and the parsed origin should match the
  // StorageKey's origin member.
  is_valid_ = ParseFileSystemSchemeURL(url, &origin_url, &mount_type_,
                                       &virtual_path_) &&
              storage_key.origin().IsSameOriginWith(origin_url);
  storage_key_ = storage_key;
  path_ = virtual_path_;
  type_ = mount_type_;
}

FileSystemURL::FileSystemURL(const blink::StorageKey& storage_key,
                             FileSystemType mount_type,
                             const base::FilePath& virtual_path)
    : is_null_(false),
      is_valid_(true),
      storage_key_(storage_key),
      mount_type_(mount_type),
      virtual_path_(virtual_path.NormalizePathSeparators()),
      type_(mount_type),
      path_(virtual_path.NormalizePathSeparators()),
      mount_option_(FlushPolicy::NO_FLUSH_ON_COMPLETION) {}

FileSystemURL::FileSystemURL(const blink::StorageKey& storage_key,
                             FileSystemType mount_type,
                             const base::FilePath& virtual_path,
                             const std::string& mount_filesystem_id,
                             FileSystemType cracked_type,
                             const base::FilePath& cracked_path,
                             const std::string& filesystem_id,
                             const FileSystemMountOption& mount_option)
    : is_null_(false),
      is_valid_(true),
      storage_key_(storage_key),
      mount_type_(mount_type),
      virtual_path_(virtual_path.NormalizePathSeparators()),
      mount_filesystem_id_(mount_filesystem_id),
      type_(cracked_type),
      path_(cracked_path.NormalizePathSeparators()),
      filesystem_id_(filesystem_id),
      mount_option_(mount_option) {}

GURL FileSystemURL::ToGURL() const {
  if (!is_valid_) {
    return GURL();
  }

  GURL url = GetFileSystemRootURI(storage_key_.origin().GetURL(), mount_type_);
  if (!url.is_valid()) {
    return GURL();
  }

  std::string url_string = url.spec();

  // Exactly match with DOMFileSystemBase::createFileSystemURL()'s encoding
  // behavior, where the path is escaped by KURL::encodeWithURLEscapeSequences
  // which is essentially encodeURIComponent except '/'.
  std::string escaped = base::EscapeQueryParamValue(
      virtual_path_.NormalizePathSeparatorsTo('/').AsUTF8Unsafe(),
      false /* use_plus */);
  base::ReplaceSubstringsAfterOffset(&escaped, 0, "%2F", "/");
  url_string.append(escaped);

  // Build nested GURL.
  return GURL(url_string);
}

std::string FileSystemURL::DebugString() const {
  if (!is_valid_) {
    return "invalid filesystem: URL";
  }
  std::ostringstream ss;
  switch (mount_type_) {
    // Include GURL if GURL serialization is possible.
    case kFileSystemTypeTemporary:
    case kFileSystemTypePersistent:
    case kFileSystemTypeExternal:
    case kFileSystemTypeIsolated:
    case kFileSystemTypeTest:
      ss << "{ uri: ";
      ss << GetFileSystemRootURI(storage_key_.origin().GetURL(), mount_type_);
      break;
    // Otherwise list the origin and path separately.
    default:
      ss << "{ path: ";
  }

  // filesystem_id_ will be non empty for (and only for) cracked URLs.
  if (!filesystem_id_.empty()) {
    ss << virtual_path_.value();
    ss << " (";
    ss << GetFileSystemTypeString(type_) << "@" << filesystem_id_ << ":";
    ss << path_.value();
    ss << ")";
  } else {
    ss << path_.value();
  }
  ss << ", storage key: " << storage_key_.GetDebugString();
  if (bucket_.has_value()) {
    ss << ", bucket id: " << bucket_->id;
  }
  ss << " }";
  return ss.str();
}

BucketLocator FileSystemURL::GetBucket() const {
  if (bucket()) {
    return *bucket_;
  }

  auto bucket = storage::BucketLocator::ForDefaultBucket(storage_key());
  return bucket;
}

bool FileSystemURL::IsParent(const FileSystemURL& child) const {
  return IsInSameFileSystem(child) &&
         (path().IsParent(child.path()) ||
          (VirtualPath::IsRootPath(path()) &&
           !VirtualPath::IsRootPath(child.path())));
}

bool FileSystemURL::IsInSameFileSystem(const FileSystemURL& other) const {
  // Invalid FileSystemURLs should never be considered of the same file system.
  return AreSameStorageKey(*this, other) && is_valid() && other.is_valid() &&
         type() == other.type() && filesystem_id() == other.filesystem_id() &&
         bucket() == other.bucket()
#if BUILDFLAG(IS_ANDROID)
         // Android content-URIs do not support same-FS ops such as rename().
         && !path().IsContentUri() && !other.path().IsContentUri()
#endif
      ;
}

bool FileSystemURL::operator==(const FileSystemURL& that) const {
  if (is_null_ && that.is_null_) {
    return true;
  }

  return AreSameStorageKey(*this, that) && type_ == that.type_ &&
         path_ == that.path_ && filesystem_id_ == that.filesystem_id_ &&
         is_valid_ == that.is_valid_ && bucket_ == that.bucket_;
}

std::weak_ordering FileSystemURL::operator<=>(const FileSystemURL& that) const {
  if (is_null_ && that.is_null_) {
    return std::weak_ordering::equivalent;
  }

  if (!AreSameStorageKey(*this, that)) {
    return storage_key_ < that.storage_key_ ? std::weak_ordering::less
                                            : std::weak_ordering::greater;
  }

  return std::tie(type_, path_, filesystem_id_, is_valid_, bucket_) <=>
         std::tie(that.type_, that.path_, that.filesystem_id_, that.is_valid_,
                  that.bucket_);
}

bool FileSystemURL::Comparator::operator()(const FileSystemURL& lhs,
                                           const FileSystemURL& rhs) const {
  DCHECK(lhs.is_valid_ && rhs.is_valid_);
  if (!AreSameStorageKey(lhs, rhs)) {
    return lhs.storage_key() < rhs.storage_key();
  }
  if (lhs.type_ != rhs.type_) {
    return lhs.type_ < rhs.type_;
  }
  if (lhs.filesystem_id_ != rhs.filesystem_id_) {
    return lhs.filesystem_id_ < rhs.filesystem_id_;
  }
  if (lhs.bucket_ != rhs.bucket_) {
    return lhs.bucket_ < rhs.bucket_;
  }
  return lhs.path_ < rhs.path_;
}

}  // namespace storage
