Skip to content

Commit

Permalink
Refactor virtual path management of StaticFilesModule (fixes #272 et…
Browse files Browse the repository at this point in the history
… al.) (#275)

* Avoid creating a temporary Dictionary.

* Remove excess parenthesis.

* Refactor virtual path management of StaticFilesModule (fixes #272 et al.)

* Determinism:
  - always evaluate virtual path in reverse ordinal order;
  - normalize URL paths to simplify code and ensure consistent mapping.

* Separation of concerns:
  - have methods that deal with mapping URL paths to local paths,
    and other methods that deal with the existence of local paths.

* Caching:
  - keep track of _how_ and _why_ paths were mapped;
  - discard cached mapped paths whose generating data (virtual path,
    default extension, default documkent name) has changed.

* Support for dynamic file systems:
  - allow the user to deactivate path caching in situations where the
    contents of served directories may change over time.

* Compatibility:
  - keep existing constructors, methods, and properties of
    StaticFilesModule;
  - keep existing exception semantics (e.g. throw
    InvalidOperationException instead of ArgumentException for invalid
    paths passed to RegisterVirtualPath).

* Behavior changes:
  - scenarios where files could be added to a served directory after
    being requested (resulting in error 404) are no longer supported:
    said files will continue to give error 404, unless path caching
    is disabled.
  - scenarios where files are continuously added and/or deleted,
    which previously resulted in spurious error 404s (#272), can now be
    supported by disabling path caching.

* Modify test that would not throw on machines where drive E: actually exists.
  • Loading branch information
rdeago authored and geoperez committed Apr 22, 2019
1 parent 198a364 commit 622948b
Show file tree
Hide file tree
Showing 8 changed files with 644 additions and 238 deletions.
95 changes: 95 additions & 0 deletions src/Unosquare.Labs.EmbedIO/Core/PathHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
namespace Unosquare.Labs.EmbedIO.Core
{
using System;
using System.IO;
using System.Text.RegularExpressions;

internal static class PathHelper
{
private static readonly Regex _multipleSlashRegex = new Regex("//+", RegexOptions.Compiled | RegexOptions.CultureInvariant);

static readonly char[] _invalidLocalPathChars = GetInvalidLocalPathChars();

public static bool IsValidUrlPath(string urlPath) => !string.IsNullOrEmpty(urlPath) && urlPath[0] == '/';

// urlPath must be a valid URL path
// (not null, not empty, starting with a slash.)
public static string NormalizeUrlPath(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);
}

public static string EnsureValidUrlPath(string urlPath, bool isBasePath)
{
if (urlPath == null)
{
throw new InvalidOperationException("URL path is null,");
}

if (urlPath.Length == 0)
{
throw new InvalidOperationException("URL path is empty.");
}

if (urlPath[0] != '/')
{
throw new InvalidOperationException($"URL path \"{urlPath}\"does not start with a slash.");
}

return NormalizeUrlPath(urlPath, isBasePath);
}

public static string EnsureValidLocalPath(string localPath)
{
if (localPath == null)
{
throw new InvalidOperationException("Local path is null.");
}

if (localPath.Length == 0)
{
throw new InvalidOperationException("Local path is empty.");
}

if (string.IsNullOrWhiteSpace(localPath))
{
throw new InvalidOperationException("Local path contains only white space.");
}

if (localPath.IndexOfAny(_invalidLocalPathChars) >= 0)
{
throw new InvalidOperationException($"Local path \"{localPath}\"contains one or more invalid characters.");
}

return localPath;
}

private static char[] GetInvalidLocalPathChars()
{
var systemChars = Path.GetInvalidPathChars();
var p = systemChars.Length;
var result = new char[p + 2];
Array.Copy(systemChars, result, p);
result[p++] = '*';
result[p] = '?';
return result;
}
}
}
38 changes: 38 additions & 0 deletions src/Unosquare.Labs.EmbedIO/Core/PathMappingResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
namespace Unosquare.Labs.EmbedIO.Core
{
using System;

[Flags]
internal enum PathMappingResult
{
/// <summary>
/// The mask used to extract the mapping result.
/// </summary>
MappingMask = 0xF,

/// <summary>
/// The path was not found.
/// </summary>
NotFound = 0,

/// <summary>
/// The path was mapped to a file.
/// </summary>
IsFile = 0x1,

/// <summary>
/// The path was mapped to a directory.
/// </summary>
IsDirectory = 0x2,

/// <summary>
/// The default extension has been appended to the path.
/// </summary>
DefaultExtensionUsed = 0x1000,

/// <summary>
/// The default document name has been appended to the path.
/// </summary>
DefaultDocumentUsed = 0x2000,
}
}
19 changes: 19 additions & 0 deletions src/Unosquare.Labs.EmbedIO/Core/ReverseOrdinalStringComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace Unosquare.Labs.EmbedIO.Core
{
using System;
using System.Collections.Generic;

// Sorts strings in reverse order to obtain the evaluation order of virtual paths
internal sealed class ReverseOrdinalStringComparer : IComparer<string>
{
private static readonly IComparer<string> _directComparer = StringComparer.Ordinal;

private ReverseOrdinalStringComparer()
{
}

public static IComparer<string> Instance { get; } = new ReverseOrdinalStringComparer();

public int Compare(string x, string y) => _directComparer.Compare(y, x);
}
}
42 changes: 42 additions & 0 deletions src/Unosquare.Labs.EmbedIO/Core/VirtualPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Unosquare.Labs.EmbedIO.Core
{
using System;
using System.IO;

internal sealed class VirtualPath
{
public VirtualPath(string baseUrlPath, string baseLocalPath)
{
BaseUrlPath = PathHelper.EnsureValidUrlPath(baseUrlPath, true);
try
{
BaseLocalPath = Path.GetFullPath(PathHelper.EnsureValidLocalPath(baseLocalPath));
}
#pragma warning disable CA1031
catch (Exception e)
{
throw new InvalidOperationException($"Cannot determine the full local path for \"{baseLocalPath}\".", e);
}
#pragma warning restore CA1031
}

public string BaseUrlPath { get; }

public string BaseLocalPath { get; }

internal bool CanMapUrlPath(string urlPath) => urlPath.StartsWith(BaseUrlPath, StringComparison.Ordinal);

internal bool TryMapUrlPathLoLocalPath(string urlPath, out string localPath)
{
if (!CanMapUrlPath(urlPath))
{
localPath = null;
return false;
}

var relativeUrlPath = urlPath.Substring(BaseUrlPath.Length);
localPath = Path.Combine(BaseLocalPath, relativeUrlPath.Replace('/', Path.DirectorySeparatorChar));
return true;
}
}
}
Loading

0 comments on commit 622948b

Please sign in to comment.