using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace EmbedIO.Utilities
{
///
/// Provides utility methods to work with URL paths.
///
public static class UrlPath
{
///
/// The root URL path value, i.e. "/".
///
public const string Root = "/";
private static readonly Regex MultipleSlashRegex = new Regex("//+", RegexOptions.Compiled | RegexOptions.CultureInvariant);
///
/// Determines whether a string is a valid URL path.
///
/// The URL path.
///
/// if the specified URL path is valid; otherwise, .
///
///
/// For a string to be a valid URL path, it must not be ,
/// must not be empty, and must start with a slash (/) character.
/// To ensure that a method parameter is a valid URL path, use .
///
///
///
///
public static bool IsValid(string urlPath) => ValidateInternal(nameof(urlPath), urlPath) == null;
///
/// Normalizes the specified URL path.
///
/// The URL path.
/// if set to , treat the URL path
/// as a base path, i.e. ensure it ends with a slash (/) character;
/// otherwise, ensure that it does NOT end with a slash character.
/// The normalized path.
///
/// is not a valid URL path.
///
///
/// A normalized URL path is one where each run of two or more slash
/// (/) characters has been replaced with a single slash character.
/// This method does NOT try to decode URL-encoded characters.
/// If you are sure that is a valid URL path,
/// for example because you have called and it returned
/// , then you may call
/// instead of this method. is slightly faster because
/// it skips the initial validity check.
/// There is no need to call this method for a method parameter
/// for which you have already called .
///
///
///
///
public static string Normalize(string urlPath, bool isBasePath)
{
var exception = ValidateInternal(nameof(urlPath), urlPath);
if (exception != null)
throw exception;
return UnsafeNormalize(urlPath, isBasePath);
}
///
/// Normalizes the specified URL path, assuming that it is valid.
///
/// The URL path.
/// if set to , treat the URL path
/// as a base path, i.e. ensure it ends with a slash (/) character;
/// otherwise, ensure that it does NOT end with a slash character.
/// The normalized path.
///
/// A normalized URL path is one where each run of two or more slash
/// (/) characters has been replaced with a single slash character.
/// This method does NOT try to decode URL-encoded characters.
/// If is not valid, the behavior of
/// this method is unspecified. You should call this method only after
/// has returned
/// for the same .
/// You should call instead of this method
/// if you are not sure that is valid.
/// There is no need to call this method for a method parameter
/// for which you have already called .
///
///
///
///
public static string UnsafeNormalize(string urlPath, bool isBasePath)
{
// Replace each run of multiple slashes with a single slash
urlPath = MultipleSlashRegex.Replace(urlPath, "/");
// The root path needs no further checking.
var length = urlPath.Length;
if (length == 1)
return urlPath;
// Base URL paths must end with a slash;
// non-base URL paths must NOT end with a slash.
// The final slash is irrelevant for the URL itself
// (it has to map the same way with or without it)
// but makes comparing and mapping URLs a lot simpler.
var finalPosition = length - 1;
var endsWithSlash = urlPath[finalPosition] == '/';
return isBasePath
? (endsWithSlash ? urlPath : urlPath + "/")
: (endsWithSlash ? urlPath.Substring(0, finalPosition) : urlPath);
}
///
/// Determines whether the specified URL path is prefixed by the specified base URL path.
///
/// The URL path.
/// The base URL path.
///
/// if is prefixed by ;
/// otherwise, .
///
///
/// is not a valid URL path.
/// - or -
/// is not a valid base URL path.
///
///
/// This method returns even if the two URL paths are equivalent,
/// for example if both are "/", or if is "/download" and
/// is "/download/".
/// If you are sure that both and
/// are valid and normalized, for example because you have called ,
/// then you may call instead of this method.
/// is slightly faster because it skips validity checks.
///
///
///
///
///
public static bool HasPrefix(string urlPath, string baseUrlPath)
=> UnsafeHasPrefix(
Validate.UrlPath(nameof(urlPath), urlPath, false),
Validate.UrlPath(nameof(baseUrlPath), baseUrlPath, true));
///
/// Determines whether the specified URL path is prefixed by the specified base URL path,
/// assuming both paths are valid and normalized.
///
/// The URL path.
/// The base URL path.
///
/// if is prefixed by ;
/// otherwise, .
///
///
/// Unless both and are valid,
/// normalized URL paths, the behavior of this method is unspecified. You should call this method
/// only after calling either or
/// to check and normalize both parameters.
/// If you are not sure about the validity and/or normalization of parameters,
/// call instead of this method.
/// This method returns even if the two URL paths are equivalent,
/// for example if both are "/", or if is "/download" and
/// is "/download/".
///
///
///
///
///
public static bool UnsafeHasPrefix(string urlPath, string baseUrlPath)
=> urlPath.StartsWith(baseUrlPath, StringComparison.Ordinal)
|| (urlPath.Length == baseUrlPath.Length - 1 && baseUrlPath.StartsWith(urlPath, StringComparison.Ordinal));
///
/// Strips a base URL path fom a URL path, obtaining a relative path.
///
/// The URL path.
/// The base URL path.
/// The relative path, or if
/// is not prefixed by .
///
/// is not a valid URL path.
/// - or -
/// is not a valid base URL path.
///
///
/// The returned relative path is NOT prefixed by a slash (/) character.
/// If and are equivalent,
/// for example if both are "/", or if is "/download"
/// and is "/download/", this method returns an empty string.
/// If you are sure that both and
/// are valid and normalized, for example because you have called ,
/// then you may call instead of this method.
/// is slightly faster because it skips validity checks.
///
///
///
///
///
public static string? StripPrefix(string urlPath, string baseUrlPath)
=> UnsafeStripPrefix(
Validate.UrlPath(nameof(urlPath), urlPath, false),
Validate.UrlPath(nameof(baseUrlPath), baseUrlPath, true));
///
/// Strips a base URL path fom a URL path, obtaining a relative path,
/// assuming both paths are valid and normalized.
///
/// The URL path.
/// The base URL path.
/// The relative path, or if
/// is not prefixed by .
///
/// Unless both and are valid,
/// normalized URL paths, the behavior of this method is unspecified. You should call this method
/// only after calling either or
/// to check and normalize both parameters.
/// If you are not sure about the validity and/or normalization of parameters,
/// call instead of this method.
/// The returned relative path is NOT prefixed by a slash (/) character.
/// If and are equivalent,
/// for example if both are "/", or if is "/download"
/// and is "/download/", this method returns an empty string.
///
///
///
///
///
public static string? UnsafeStripPrefix(string urlPath, string baseUrlPath)
{
if (!UnsafeHasPrefix(urlPath, baseUrlPath))
return null;
// The only case where UnsafeHasPrefix returns true for a urlPath shorter than baseUrlPath
// is urlPath == (baseUrlPath minus the final slash).
return urlPath.Length < baseUrlPath.Length
? string.Empty
: urlPath.Substring(baseUrlPath.Length);
}
///
/// Splits the specified URL path into segments.
///
/// The URL path.
/// An enumeration of path segments.
/// is not a valid URL path.
///
/// A root URL path (/) will result in an empty enumeration.
/// The returned enumeration will be the same whether is a base URL path or not.
/// If you are sure that is valid and normalized,
/// for example because you have called ,
/// then you may call instead of this method.
/// is slightly faster because it skips validity checks.
///
///
///
///
public static IEnumerable Split(string urlPath)
=> UnsafeSplit(Validate.UrlPath(nameof(urlPath), urlPath, false));
///
/// Splits the specified URL path into segments, assuming it is valid and normalized.
///
/// The URL path.
/// An enumeration of path segments.
///
/// Unless is a valid, normalized URL path,
/// the behavior of this method is unspecified. You should call this method
/// only after calling either or
/// to check and normalize both parameters.
/// If you are not sure about the validity and/or normalization of ,
/// call instead of this method.
/// A root URL path (/) will result in an empty enumeration.
/// The returned enumeration will be the same whether is a base URL path or not.
///
///
///
///
public static IEnumerable UnsafeSplit(string urlPath)
{
var length = urlPath.Length;
var position = 1; // Skip initial slash
while (position < length)
{
var slashPosition = urlPath.IndexOf('/', position);
if (slashPosition < 0)
{
yield return urlPath.Substring(position);
break;
}
yield return urlPath.Substring(position, slashPosition - position);
position = slashPosition + 1;
}
}
internal static Exception? ValidateInternal(string argumentName, string value)
{
if (value == null)
return new ArgumentNullException(argumentName);
if (value.Length == 0)
return new ArgumentException("URL path is empty.", argumentName);
if (value[0] != '/')
return new ArgumentException("URL path does not start with a slash.", argumentName);
return null;
}
}
}