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; } } }