Skip to content

Commit 622948b

Browse files
rdeagogeoperez
authored andcommitted
Refactor virtual path management of StaticFilesModule (fixes #272 et 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.
1 parent 198a364 commit 622948b

File tree

8 files changed

+644
-238
lines changed

8 files changed

+644
-238
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
namespace Unosquare.Labs.EmbedIO.Core
2+
{
3+
using System;
4+
using System.IO;
5+
using System.Text.RegularExpressions;
6+
7+
internal static class PathHelper
8+
{
9+
private static readonly Regex _multipleSlashRegex = new Regex("//+", RegexOptions.Compiled | RegexOptions.CultureInvariant);
10+
11+
static readonly char[] _invalidLocalPathChars = GetInvalidLocalPathChars();
12+
13+
public static bool IsValidUrlPath(string urlPath) => !string.IsNullOrEmpty(urlPath) && urlPath[0] == '/';
14+
15+
// urlPath must be a valid URL path
16+
// (not null, not empty, starting with a slash.)
17+
public static string NormalizeUrlPath(string urlPath, bool isBasePath)
18+
{
19+
// Replace each run of multiple slashes with a single slash
20+
urlPath = _multipleSlashRegex.Replace(urlPath, "/");
21+
22+
// The root path needs no further checking.
23+
var length = urlPath.Length;
24+
if (length == 1)
25+
return urlPath;
26+
27+
// Base URL paths must end with a slash;
28+
// non-base URL paths must NOT end with a slash.
29+
// The final slash is irrelevant for the URL itself
30+
// (it has to map the same way with or without it)
31+
// but makes comparing and mapping URls a lot simpler.
32+
var finalPosition = length - 1;
33+
var endsWithSlash = urlPath[finalPosition] == '/';
34+
return isBasePath
35+
? (endsWithSlash ? urlPath : urlPath + "/")
36+
: (endsWithSlash ? urlPath.Substring(0, finalPosition) : urlPath);
37+
}
38+
39+
public static string EnsureValidUrlPath(string urlPath, bool isBasePath)
40+
{
41+
if (urlPath == null)
42+
{
43+
throw new InvalidOperationException("URL path is null,");
44+
}
45+
46+
if (urlPath.Length == 0)
47+
{
48+
throw new InvalidOperationException("URL path is empty.");
49+
}
50+
51+
if (urlPath[0] != '/')
52+
{
53+
throw new InvalidOperationException($"URL path \"{urlPath}\"does not start with a slash.");
54+
}
55+
56+
return NormalizeUrlPath(urlPath, isBasePath);
57+
}
58+
59+
public static string EnsureValidLocalPath(string localPath)
60+
{
61+
if (localPath == null)
62+
{
63+
throw new InvalidOperationException("Local path is null.");
64+
}
65+
66+
if (localPath.Length == 0)
67+
{
68+
throw new InvalidOperationException("Local path is empty.");
69+
}
70+
71+
if (string.IsNullOrWhiteSpace(localPath))
72+
{
73+
throw new InvalidOperationException("Local path contains only white space.");
74+
}
75+
76+
if (localPath.IndexOfAny(_invalidLocalPathChars) >= 0)
77+
{
78+
throw new InvalidOperationException($"Local path \"{localPath}\"contains one or more invalid characters.");
79+
}
80+
81+
return localPath;
82+
}
83+
84+
private static char[] GetInvalidLocalPathChars()
85+
{
86+
var systemChars = Path.GetInvalidPathChars();
87+
var p = systemChars.Length;
88+
var result = new char[p + 2];
89+
Array.Copy(systemChars, result, p);
90+
result[p++] = '*';
91+
result[p] = '?';
92+
return result;
93+
}
94+
}
95+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
namespace Unosquare.Labs.EmbedIO.Core
2+
{
3+
using System;
4+
5+
[Flags]
6+
internal enum PathMappingResult
7+
{
8+
/// <summary>
9+
/// The mask used to extract the mapping result.
10+
/// </summary>
11+
MappingMask = 0xF,
12+
13+
/// <summary>
14+
/// The path was not found.
15+
/// </summary>
16+
NotFound = 0,
17+
18+
/// <summary>
19+
/// The path was mapped to a file.
20+
/// </summary>
21+
IsFile = 0x1,
22+
23+
/// <summary>
24+
/// The path was mapped to a directory.
25+
/// </summary>
26+
IsDirectory = 0x2,
27+
28+
/// <summary>
29+
/// The default extension has been appended to the path.
30+
/// </summary>
31+
DefaultExtensionUsed = 0x1000,
32+
33+
/// <summary>
34+
/// The default document name has been appended to the path.
35+
/// </summary>
36+
DefaultDocumentUsed = 0x2000,
37+
}
38+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace Unosquare.Labs.EmbedIO.Core
2+
{
3+
using System;
4+
using System.Collections.Generic;
5+
6+
// Sorts strings in reverse order to obtain the evaluation order of virtual paths
7+
internal sealed class ReverseOrdinalStringComparer : IComparer<string>
8+
{
9+
private static readonly IComparer<string> _directComparer = StringComparer.Ordinal;
10+
11+
private ReverseOrdinalStringComparer()
12+
{
13+
}
14+
15+
public static IComparer<string> Instance { get; } = new ReverseOrdinalStringComparer();
16+
17+
public int Compare(string x, string y) => _directComparer.Compare(y, x);
18+
}
19+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace Unosquare.Labs.EmbedIO.Core
2+
{
3+
using System;
4+
using System.IO;
5+
6+
internal sealed class VirtualPath
7+
{
8+
public VirtualPath(string baseUrlPath, string baseLocalPath)
9+
{
10+
BaseUrlPath = PathHelper.EnsureValidUrlPath(baseUrlPath, true);
11+
try
12+
{
13+
BaseLocalPath = Path.GetFullPath(PathHelper.EnsureValidLocalPath(baseLocalPath));
14+
}
15+
#pragma warning disable CA1031
16+
catch (Exception e)
17+
{
18+
throw new InvalidOperationException($"Cannot determine the full local path for \"{baseLocalPath}\".", e);
19+
}
20+
#pragma warning restore CA1031
21+
}
22+
23+
public string BaseUrlPath { get; }
24+
25+
public string BaseLocalPath { get; }
26+
27+
internal bool CanMapUrlPath(string urlPath) => urlPath.StartsWith(BaseUrlPath, StringComparison.Ordinal);
28+
29+
internal bool TryMapUrlPathLoLocalPath(string urlPath, out string localPath)
30+
{
31+
if (!CanMapUrlPath(urlPath))
32+
{
33+
localPath = null;
34+
return false;
35+
}
36+
37+
var relativeUrlPath = urlPath.Substring(BaseUrlPath.Length);
38+
localPath = Path.Combine(BaseLocalPath, relativeUrlPath.Replace('/', Path.DirectorySeparatorChar));
39+
return true;
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)