chromium/chrome/browser/ash/smb_client/smb_url.cc

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

#include "chrome/browser/ash/smb_client/smb_url.h"

#include <string_view>
#include <vector>

#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "chrome/browser/ash/smb_client/smb_constants.h"
#include "url/url_canon_stdstring.h"

namespace ash::smb_client {

namespace {

const char kSingleBackslash[] = "\\";
const char kDoubleBackslash[] = "\\\\";

// Returns true if |url| starts with "smb://" or "\\".
bool ShouldProcessUrl(const std::string& url) {
  return base::StartsWith(url, kSmbSchemePrefix,
                          base::CompareCase::INSENSITIVE_ASCII) ||
         base::StartsWith(url, kDoubleBackslash,
                          base::CompareCase::INSENSITIVE_ASCII);
}

// Adds "smb://" to the beginning of |url| if not present.
std::string AddSmbSchemeIfMissing(const std::string& url) {
  DCHECK(ShouldProcessUrl(url));

  if (base::StartsWith(url, kSmbSchemePrefix,
                       base::CompareCase::INSENSITIVE_ASCII)) {
    return url;
  }

  return std::string(kSmbSchemePrefix) + url;
}

// Returns true if |parsed| contains a username, password, port, query, or ref.
bool ContainsUnnecessaryComponents(const url::Parsed& parsed) {
  return parsed.username.is_nonempty() || parsed.password.is_nonempty() ||
         parsed.port.is_nonempty() || parsed.query.is_nonempty() ||
         parsed.ref.is_nonempty();
}

// Parses |url| into |parsed|. Returns true if the URL does not contain
// unnecessary components.
bool ParseAndValidateUrl(const std::string& url, url::Parsed* parsed) {
  DCHECK(parsed);
  DCHECK(ShouldProcessUrl(url));

  *parsed = url::ParseStandardURL(url);
  return !ContainsUnnecessaryComponents(*parsed);
}

}  // namespace

SmbUrl::SmbUrl(const std::string& raw_url) {
  // Only process |url| if it starts with "smb://" or "\\".
  if (ShouldProcessUrl(raw_url)) {
    // Add "smb://" if |url| starts with "\\" and canonicalize the URL.
    CanonicalizeSmbUrl(AddSmbSchemeIfMissing(raw_url));
    // Create the Windows UNC for the url.
    CreateWindowsUnc(raw_url);
  }
}

SmbUrl::~SmbUrl() = default;

SmbUrl::SmbUrl(SmbUrl&& smb_url) = default;

SmbUrl::SmbUrl(const SmbUrl& smb_url) = default;

SmbUrl& SmbUrl::operator=(const SmbUrl& smb_url) = default;

std::string SmbUrl::GetHost() const {
  DCHECK(IsValid());

  return url_.substr(host_.begin, host_.len);
}

std::string SmbUrl::GetShare() const {
  DCHECK(IsValid());

  return share_;
}

const std::string& SmbUrl::ToString() const {
  DCHECK(IsValid());

  return url_;
}

SmbUrl SmbUrl::ReplaceHost(const std::string& new_host) const {
  DCHECK(IsValid());

  std::string temp = url_;
  temp.replace(host_.begin, host_.len, new_host);
  SmbUrl new_url(temp);
  DCHECK(new_url.IsValid());
  return new_url;
}

bool SmbUrl::IsValid() const {
  // The URL is valid as long as it has a host, but some users (eg.
  // SmbService) may also require that the share_ is defined.
  return !url_.empty() && host_.is_valid();
}

std::string SmbUrl::GetWindowsUNCString() const {
  DCHECK(IsValid());

  return windows_unc_;
}

void SmbUrl::CanonicalizeSmbUrl(const std::string& url) {
  DCHECK(!IsValid());
  DCHECK(ShouldProcessUrl(url));

  // Get the initial parse of |url| and ensure that it does not contain
  // unnecessary components.
  url::Parsed initial_parsed;
  if (!ParseAndValidateUrl(url, &initial_parsed)) {
    return;
  }

  // Try to canonicalize the input URL into |url_|. IsValid() returns
  // false if this is unsuccessful.
  url::StdStringCanonOutput canonical_output(&url_);

  url::Component scheme;
  if (!url::CanonicalizeScheme(url.c_str(), initial_parsed.scheme,
                               &canonical_output, &scheme)) {
    Reset();
    return;
  }

  canonical_output.push_back('/');
  canonical_output.push_back('/');

  url::Component path;
  if (!(url::CanonicalizeHost(url.c_str(), initial_parsed.host,
                              &canonical_output, &host_) &&
        url::CanonicalizePath(url.c_str(), initial_parsed.path,
                              &canonical_output, &path))) {
    Reset();
    return;
  }

  // Check IsValid here since url::Canonicalize* may return true even if it did
  // not actually parse the component.
  if (!IsValid()) {
    Reset();
    return;
  }

  canonical_output.Complete();

  // |url_| is now valid, parse out any (optional) share and path component.
  if (path.is_nonempty()) {
    // Extract share name, which is the first path element.
    // Paths always start with '/', but extra '/'s are not removed.
    // So both "smb://foo" and "smb://foo//bar/" have the share name "", but
    // "smb://foo/bar/" has the share name "bar".
    std::string path_str = url_.substr(path.begin, path.len);
    std::vector<std::string_view> split_path = base::SplitStringPiece(
        path_str, "/", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
    if (split_path.size() >= 2) {
      DCHECK_EQ(split_path[0], "");
      share_ = std::string(split_path[1]);
    }
  }

  // Valid SmbUrls never have trailing slashes.
  while (url_.size() && url_.back() == '/') {
    url_.pop_back();
  }

  DCHECK(host_.is_nonempty());
  DCHECK_EQ(url_.substr(scheme.begin, scheme.len), kSmbScheme);
}

void SmbUrl::CreateWindowsUnc(const std::string& url) {
  url::Parsed parsed;
  if (!ParseAndValidateUrl(url, &parsed)) {
    return;
  }

  const std::string host = url.substr(parsed.host.begin, parsed.host.len);
  std::string path = url.substr(parsed.path.begin, parsed.path.len);

  // Turn any forward slashes into escaped backslashes.
  base::ReplaceChars(path, "/", kSingleBackslash, &path);

  windows_unc_ = base::StrCat({kDoubleBackslash, host, path});
}

void SmbUrl::Reset() {
  host_.reset();
  url_.clear();
}

}  // namespace ash::smb_client