diff --git a/src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props b/src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props
index 295352c23934..d7acba0e5779 100644
--- a/src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props
+++ b/src/BlazorWasmSdk/Targets/Microsoft.NET.Sdk.BlazorWebAssembly.Current.props
@@ -30,6 +30,7 @@ Copyright (c) .NET Foundation. All rights reserved.
$(StaticWebAssetsAdditionalPublishProperties);_PublishingBlazorWasmProject=true
$(StaticWebAssetsAdditionalEmbeddedPublishProperties);_PublishingBlazorWasmProject=true
true
+ true
ComputeFilesToPublish;GetCurrentProjectEmbeddedPublishStaticWebAssetItems
diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets
index 19d3d2b8edcf..03e88aac848d 100644
--- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets
+++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.Compression.targets
@@ -219,16 +219,20 @@ Copyright (c) .NET Foundation. All rights reserved.
+
+
+ <_ResolveBuildCompressedStaticWebAssetsCachePath>$(_StaticWebAssetsManifestBase)rbcswa.dswa.cache.json
+
+
-
@@ -242,7 +246,6 @@ Copyright (c) .NET Foundation. All rights reserved.
diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.JSModules.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.JSModules.targets
index c785a7055c80..b907d48cc7af 100644
--- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.JSModules.targets
+++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.JSModules.targets
@@ -98,6 +98,11 @@ Copyright (c) .NET Foundation. All rights reserved.
to identify them and correctly clasify them. Modules from other projects or packages will already be correctly tagged when we
retrieve them.
-->
+
+
+ <_ResolveJsInitializerModuleStaticWebAssetsCachePath>$(_StaticWebAssetsManifestBase)rjimswa.dswa.cache.json
+
+
@@ -404,6 +410,13 @@ Copyright (c) .NET Foundation. All rights reserved.
<_JSFileModuleCandidates Include="@(_JSFileModuleNoneCandidates)" />
+
+ <_ResolveJSModuleStaticWebAssetsRazorCachePath>$(_StaticWebAssetsManifestBase)rjsmrazor.dswa.cache.json
+
+
+ <_ResolveJSModuleStaticWebAssetsCshtmlCachePath>$(_StaticWebAssetsManifestBase)rjsmcshtml.dswa.cache.json
+
+
+ AssetMergeSource="$(StaticWebAssetMergeTarget)"
+ CacheManifestPath="$(_ResolveJSModuleStaticWebAssetsRazorCachePath)">
@@ -425,7 +439,8 @@ Copyright (c) .NET Foundation. All rights reserved.
ContentRoot="$(MSBuildProjectDirectory)"
SourceType="Discovered"
BasePath="$(StaticWebAssetBasePath)"
- AssetMergeSource="$(StaticWebAssetMergeTarget)">
+ AssetMergeSource="$(StaticWebAssetMergeTarget)"
+ CacheManifestPath="$(_ResolveJSModuleStaticWebAssetsCshtmlCachePath)">
diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.References.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.References.targets
index b4fbaaf70cc7..852cefbb0e78 100644
--- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.References.targets
+++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.References.targets
@@ -181,6 +181,7 @@ Copyright (c) .NET Foundation. All rights reserved.
Patterns="@(_CachedBuildStaticWebAssetDiscoveryPatterns)"
ProjectMode="$(StaticWebAssetProjectMode)"
AssetKind="Build"
+ MakeReferencedAssetOriginalItemSpecAbsolute="$(StaticWebAssetMakeReferencedAssetOriginalItemSpecAbsolute)"
Source="$(PackageId)"
>
@@ -227,6 +228,7 @@ Copyright (c) .NET Foundation. All rights reserved.
ProjectMode="$(StaticWebAssetProjectMode)"
AssetKind="Publish"
Source="$(PackageId)"
+ MakeReferencedAssetOriginalItemSpecAbsolute="$(StaticWebAssetMakeReferencedAssetOriginalItemSpecAbsolute)"
>
diff --git a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
index f5dc8380dd86..64f5b1c0c6d2 100644
--- a/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
+++ b/src/StaticWebAssetsSdk/Targets/Microsoft.NET.Sdk.StaticWebAssets.targets
@@ -671,6 +671,10 @@ Copyright (c) .NET Foundation. All rights reserved.
BeforeTargets="AssignTargetPaths"
DependsOnTargets="ResolveStaticWebAssetsConfiguration;UpdateExistingPackageStaticWebAssets">
+
+ <_ResolveProjectStaticWebAssetsCachePath>$(_StaticWebAssetsManifestBase)rpswa.dswa.cache.json
+
+
+ AssetMergeSource="$(StaticWebAssetMergeTarget)"
+ CacheManifestPath="$(_ResolveProjectStaticWebAssetsCachePath)">
-
+
diff --git a/src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs b/src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs
index ca5849faf7b6..0c4b9c4e1b69 100644
--- a/src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs
+++ b/src/StaticWebAssetsSdk/Tasks/ApplyCompressionNegotiation.cs
@@ -16,27 +16,11 @@ public class ApplyCompressionNegotiation : Task
[Required]
public ITaskItem[] CandidateAssets { get; set; }
- public ITaskItem[] AssetFileDetails { get; set; }
-
[Output]
public ITaskItem[] UpdatedEndpoints { get; set; }
- public Func TestResolveFileLength;
-
- private Dictionary _assetFileDetails;
-
public override bool Execute()
{
- if (AssetFileDetails != null)
- {
- _assetFileDetails = new(AssetFileDetails.Length, OSPath.PathComparer);
- for (int i = 0; i < AssetFileDetails.Length; i++)
- {
- var item = AssetFileDetails[i];
- _assetFileDetails[item.ItemSpec] = item;
- }
- }
-
var assetsById = CandidateAssets.Select(StaticWebAsset.FromTaskItem).ToDictionary(a => a.Identity);
var endpointsByAsset = CandidateEndpoints.Select(StaticWebAssetEndpoint.FromTaskItem)
@@ -213,24 +197,8 @@ public override bool Execute()
return true;
}
- private string ResolveQuality(StaticWebAsset compressedAsset)
- {
- long length;
- if(_assetFileDetails != null && _assetFileDetails.TryGetValue(compressedAsset.Identity, out var assetFileDetail))
- {
- length = long.Parse(assetFileDetail.GetMetadata("FileLength"));
- }
- else if (TestResolveFileLength != null)
- {
- length = TestResolveFileLength(compressedAsset.Identity);
- }
- else
- {
- length = new FileInfo(compressedAsset.Identity).Length;
- }
-
- return Math.Round(1.0 / (length + 1), 12).ToString("F12", CultureInfo.InvariantCulture);
- }
+ private static string ResolveQuality(StaticWebAsset compressedAsset) =>
+ Math.Round(1.0 / (compressedAsset.FileLength + 1), 12).ToString("F12", CultureInfo.InvariantCulture);
private static bool IsCompatible(StaticWebAssetEndpoint compressedEndpoint, StaticWebAssetEndpoint relatedEndpointCandidate)
{
diff --git a/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs b/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs
index 05dd6ed67509..35831a2840f5 100644
--- a/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs
+++ b/src/StaticWebAssetsSdk/Tasks/Compression/ResolveCompressedAssets.cs
@@ -3,7 +3,6 @@
using System.Security.Cryptography;
using Microsoft.Build.Framework;
-using Microsoft.Extensions.FileSystemGlobbing;
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
@@ -60,12 +59,15 @@ public override bool Execute()
var includePatterns = SplitPattern(IncludePatterns);
var excludePatterns = SplitPattern(ExcludePatterns);
- var matcher = new Matcher();
- matcher.AddIncludePatterns(includePatterns);
- matcher.AddExcludePatterns(excludePatterns);
+ var matcher = new StaticWebAssetGlobMatcherBuilder()
+ .AddIncludePatterns(includePatterns)
+ .AddExcludePatterns(excludePatterns)
+ .Build();
var matchingCandidateAssets = new List();
+ var matchContext = StaticWebAssetGlobMatcher.CreateMatchContext();
+
// Add each candidate asset to each compression configuration with a matching pattern.
foreach (var asset in candidates)
{
@@ -80,9 +82,10 @@ public override bool Execute()
}
var relativePath = asset.ComputePathWithoutTokens(asset.RelativePath);
- var match = matcher.Match(relativePath);
+ matchContext.SetPathAndReinitialize(relativePath.AsSpan());
+ var match = matcher.Match(matchContext);
- if (!match.HasMatches)
+ if (!match.IsMatch)
{
Log.LogMessage(
MessageImportance.Low,
@@ -275,7 +278,7 @@ private bool TryCreateCompressedAsset(StaticWebAsset asset, string outputPath, s
OriginalItemSpec = asset.Identity,
RelatedAsset = asset.Identity,
AssetRole = "Alternative",
- AssetTraitName = "Content-Encoding",
+ AssetTraitName = "Content-Encoding",
AssetTraitValue = assetTraitValue,
ContentRoot = outputPath,
// Set integrity and fingerprint to null so that they get recalculated for the compressed asset.
diff --git a/src/StaticWebAssetsSdk/Tasks/ComputeReferenceStaticWebAssetItems.cs b/src/StaticWebAssetsSdk/Tasks/ComputeReferenceStaticWebAssetItems.cs
index fa7f09f58539..147ebc57cd88 100644
--- a/src/StaticWebAssetsSdk/Tasks/ComputeReferenceStaticWebAssetItems.cs
+++ b/src/StaticWebAssetsSdk/Tasks/ComputeReferenceStaticWebAssetItems.cs
@@ -23,6 +23,8 @@ public class ComputeReferenceStaticWebAssetItems : Task
public bool UpdateSourceType { get; set; } = true;
+ public bool MakeReferencedAssetOriginalItemSpecAbsolute { get; set; }
+
[Output]
public ITaskItem[] StaticWebAssets { get; set; }
@@ -60,6 +62,14 @@ public override bool Execute()
if (ShouldIncludeAssetAsReference(selected, out var reason))
{
selected.SourceType = UpdateSourceType ? StaticWebAsset.SourceTypes.Project : selected.SourceType;
+ if (MakeReferencedAssetOriginalItemSpecAbsolute)
+ {
+ selected.OriginalItemSpec = Path.GetFullPath(selected.OriginalItemSpec);
+ }
+ else
+ {
+ selected.OriginalItemSpec = selected.OriginalItemSpec;
+ }
resultAssets.Add(selected);
}
Log.LogMessage(MessageImportance.Low, reason);
diff --git a/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsForCurrentProject.cs b/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsForCurrentProject.cs
index b0e070160b96..171edd1323af 100644
--- a/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsForCurrentProject.cs
+++ b/src/StaticWebAssetsSdk/Tasks/ComputeStaticWebAssetsForCurrentProject.cs
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Framework;
diff --git a/src/StaticWebAssetsSdk/Tasks/Data/ContentTypeMapping.cs b/src/StaticWebAssetsSdk/Tasks/Data/ContentTypeMapping.cs
index cecde3e015e7..c0e10deba4d5 100644
--- a/src/StaticWebAssetsSdk/Tasks/Data/ContentTypeMapping.cs
+++ b/src/StaticWebAssetsSdk/Tasks/Data/ContentTypeMapping.cs
@@ -2,16 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
+using System.Globalization;
using Microsoft.Build.Framework;
-using Microsoft.Extensions.FileSystemGlobbing;
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks
{
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
internal struct ContentTypeMapping(string mimeType, string cache, string pattern, int priority)
{
- private Matcher _matcher;
-
public string Pattern { get; set; } = pattern;
public string MimeType { get; set; } = mimeType;
@@ -24,18 +22,8 @@ internal struct ContentTypeMapping(string mimeType, string cache, string pattern
contentTypeMappings.ItemSpec,
contentTypeMappings.GetMetadata(nameof(Cache)),
contentTypeMappings.GetMetadata(nameof(Pattern)),
- int.Parse(contentTypeMappings.GetMetadata(nameof(Priority))));
-
- internal bool Matches(string identity)
- {
- if (_matcher == null)
- {
- _matcher = new Matcher();
- _matcher.AddInclude(Pattern);
- }
- return _matcher.Match(identity).HasMatches;
- }
+ int.Parse(contentTypeMappings.GetMetadata(nameof(Priority)), CultureInfo.InvariantCulture));
- private string GetDebuggerDisplay() => $"Pattern: {Pattern}, MimeType: {MimeType}, Cache: {Cache}, Priority: {Priority}";
+ private readonly string GetDebuggerDisplay() => $"Pattern: {Pattern}, MimeType: {MimeType}, Cache: {Cache}, Priority: {Priority}";
}
}
diff --git a/src/StaticWebAssetsSdk/Tasks/Data/ContentTypeProvider.cs b/src/StaticWebAssetsSdk/Tasks/Data/ContentTypeProvider.cs
index 0e680b60f537..ba52ac850a8a 100644
--- a/src/StaticWebAssetsSdk/Tasks/Data/ContentTypeProvider.cs
+++ b/src/StaticWebAssetsSdk/Tasks/Data/ContentTypeProvider.cs
@@ -12,432 +12,493 @@
namespace Microsoft.NET.Sdk.StaticWebAssets.Tasks;
-internal class ContentTypeProvider(ContentTypeMapping[] customMappings)
+internal sealed class ContentTypeProvider
{
private static Dictionary _builtInMappings =
new Dictionary()
{
- [".js"] = new ContentTypeMapping("text/javascript", null, "*.js", 1),
- [".css"] = new ContentTypeMapping("text/css", null, "*.css", 1),
- [".html"] = new ContentTypeMapping("text/html", null, "*.html", 1),
- [".json"] = new ContentTypeMapping("application/json", null, "*.json", 1),
- [".mjs"] = new ContentTypeMapping("text/javascript", null, "*.mjs", 1),
- [".xml"] = new ContentTypeMapping("text/xml", null, "*.xml", 1),
- [".htm"] = new ContentTypeMapping("text/html", null, "*.htm", 1),
- [".wasm"] = new ContentTypeMapping("application/wasm", null, "*.wasm", 1),
- [".txt"] = new ContentTypeMapping("text/plain", null, "*.txt", 1),
- [".dll"] = new ContentTypeMapping("application/octet-stream", null, "*.dll", 1),
- [".pdb"] = new ContentTypeMapping("application/octet-stream", null, "*.pdb", 1),
- [".dat"] = new ContentTypeMapping("application/octet-stream", null, "*.dat", 1),
- [".webmanifest"] = new ContentTypeMapping("application/manifest+json", null, "*.webmanifest", 1),
- [".jsx"] = new ContentTypeMapping("text/jscript", null, "*.jsx", 1),
- [".markdown"] = new ContentTypeMapping("text/markdown", null, "*.markdown", 1),
- [".gz"] = new ContentTypeMapping("application/x-gzip", null, "*.gz", 1),
- [".md"] = new ContentTypeMapping("text/markdown", null, "*.md", 1),
- [".bmp"] = new ContentTypeMapping("image/bmp", null, "*.bmp", 1),
- [".jpeg"] = new ContentTypeMapping("image/jpeg", null, "*.jpeg", 1),
- [".jpg"] = new ContentTypeMapping("image/jpeg", null, "*.jpg", 1),
- [".gif"] = new ContentTypeMapping("image/gif", null, "*.gif", 1),
- [".svg"] = new ContentTypeMapping("image/svg+xml", null, "*.svg", 1),
- [".png"] = new ContentTypeMapping("image/png", null, "*.png", 1),
- [".webp"] = new ContentTypeMapping("image/webp", null, "*.webp", 1),
- [".otf"] = new ContentTypeMapping("font/otf", null, "*.otf", 1),
- [".woff2"] = new ContentTypeMapping("font/woff2", null, "*.woff2", 1),
- [".m4v"] = new ContentTypeMapping("video/mp4", null, "*.m4v", 1),
- [".mov"] = new ContentTypeMapping("video/quicktime", null, "*.mov", 1),
- [".movie"] = new ContentTypeMapping("video/x-sgi-movie", null, "*.movie", 1),
- [".mp2"] = new ContentTypeMapping("video/mpeg", null, "*.mp2", 1),
- [".mp4"] = new ContentTypeMapping("video/mp4", null, "*.mp4", 1),
- [".mp4v"] = new ContentTypeMapping("video/mp4", null, "*.mp4v", 1),
- [".mpa"] = new ContentTypeMapping("video/mpeg", null, "*.mpa", 1),
- [".mpe"] = new ContentTypeMapping("video/mpeg", null, "*.mpe", 1),
- [".mpeg"] = new ContentTypeMapping("video/mpeg", null, "*.mpeg", 1),
- [".mpg"] = new ContentTypeMapping("video/mpeg", null, "*.mpg", 1),
- [".mpv2"] = new ContentTypeMapping("video/mpeg", null, "*.mpv2", 1),
- [".nsc"] = new ContentTypeMapping("video/x-ms-asf", null, "*.nsc", 1),
- [".ogg"] = new ContentTypeMapping("video/ogg", null, "*.ogg", 1),
- [".ogv"] = new ContentTypeMapping("video/ogg", null, "*.ogv", 1),
- [".webm"] = new ContentTypeMapping("video/webm", null, "*.webm", 1),
- [".323"] = new ContentTypeMapping("text/h323", null, "*.323", 1),
- [".appcache"] = new ContentTypeMapping("text/cache-manifest", null, "*.appcache", 1),
- [".asm"] = new ContentTypeMapping("text/plain", null, "*.asm", 1),
- [".bas"] = new ContentTypeMapping("text/plain", null, "*.bas", 1),
- [".c"] = new ContentTypeMapping("text/plain", null, "*.c", 1),
- [".cnf"] = new ContentTypeMapping("text/plain", null, "*.cnf", 1),
- [".cpp"] = new ContentTypeMapping("text/plain", null, "*.cpp", 1),
- [".csv"] = new ContentTypeMapping("text/csv", null, "*.csv", 1),
- [".disco"] = new ContentTypeMapping("text/xml", null, "*.disco", 1),
- [".dlm"] = new ContentTypeMapping("text/dlm", null, "*.dlm", 1),
- [".dtd"] = new ContentTypeMapping("text/xml", null, "*.dtd", 1),
- [".etx"] = new ContentTypeMapping("text/x-setext", null, "*.etx", 1),
- [".h"] = new ContentTypeMapping("text/plain", null, "*.h", 1),
- [".hdml"] = new ContentTypeMapping("text/x-hdml", null, "*.hdml", 1),
- [".htc"] = new ContentTypeMapping("text/x-component", null, "*.htc", 1),
- [".htt"] = new ContentTypeMapping("text/webviewhtml", null, "*.htt", 1),
- [".hxt"] = new ContentTypeMapping("text/html", null, "*.hxt", 1),
- [".ical"] = new ContentTypeMapping("text/calendar", null, "*.ical", 1),
- [".icalendar"] = new ContentTypeMapping("text/calendar", null, "*.icalendar", 1),
- [".ics"] = new ContentTypeMapping("text/calendar", null, "*.ics", 1),
- [".ifb"] = new ContentTypeMapping("text/calendar", null, "*.ifb", 1),
- [".map"] = new ContentTypeMapping("text/plain", null, "*.map", 1),
- [".mno"] = new ContentTypeMapping("text/xml", null, "*.mno", 1),
- [".odc"] = new ContentTypeMapping("text/x-ms-odc", null, "*.odc", 1),
- [".rtx"] = new ContentTypeMapping("text/richtext", null, "*.rtx", 1),
- [".sct"] = new ContentTypeMapping("text/scriptlet", null, "*.sct", 1),
- [".sgml"] = new ContentTypeMapping("text/sgml", null, "*.sgml", 1),
- [".tsv"] = new ContentTypeMapping("text/tab-separated-values", null, "*.tsv", 1),
- [".uls"] = new ContentTypeMapping("text/iuls", null, "*.uls", 1),
- [".vbs"] = new ContentTypeMapping("text/vbscript", null, "*.vbs", 1),
- [".vcf"] = new ContentTypeMapping("text/x-vcard", null, "*.vcf", 1),
- [".vcs"] = new ContentTypeMapping("text/plain", null, "*.vcs", 1),
- [".vml"] = new ContentTypeMapping("text/xml", null, "*.vml", 1),
- [".wml"] = new ContentTypeMapping("text/vnd.wap.wml", null, "*.wml", 1),
- [".wmls"] = new ContentTypeMapping("text/vnd.wap.wmlscript", null, "*.wmls", 1),
- [".wsdl"] = new ContentTypeMapping("text/xml", null, "*.wsdl", 1),
- [".xdr"] = new ContentTypeMapping("text/plain", null, "*.xdr", 1),
- [".xsd"] = new ContentTypeMapping("text/xml", null, "*.xsd", 1),
- [".xsf"] = new ContentTypeMapping("text/xml", null, "*.xsf", 1),
- [".xsl"] = new ContentTypeMapping("text/xml", null, "*.xsl", 1),
- [".xslt"] = new ContentTypeMapping("text/xml", null, "*.xslt", 1),
- [".woff"] = new ContentTypeMapping("application/font-woff", null, "*.woff", 1),
- [".art"] = new ContentTypeMapping("image/x-jg", null, "*.art", 1),
- [".cmx"] = new ContentTypeMapping("image/x-cmx", null, "*.cmx", 1),
- [".cod"] = new ContentTypeMapping("image/cis-cod", null, "*.cod", 1),
- [".dib"] = new ContentTypeMapping("image/bmp", null, "*.dib", 1),
- [".ico"] = new ContentTypeMapping("image/x-icon", null, "*.ico", 1),
- [".ief"] = new ContentTypeMapping("image/ief", null, "*.ief", 1),
- [".jfif"] = new ContentTypeMapping("image/pjpeg", null, "*.jfif", 1),
- [".jpe"] = new ContentTypeMapping("image/jpeg", null, "*.jpe", 1),
- [".pbm"] = new ContentTypeMapping("image/x-portable-bitmap", null, "*.pbm", 1),
- [".pgm"] = new ContentTypeMapping("image/x-portable-graymap", null, "*.pgm", 1),
- [".pnm"] = new ContentTypeMapping("image/x-portable-anymap", null, "*.pnm", 1),
- [".pnz"] = new ContentTypeMapping("image/png", null, "*.pnz", 1),
- [".ppm"] = new ContentTypeMapping("image/x-portable-pixmap", null, "*.ppm", 1),
- [".ras"] = new ContentTypeMapping("image/x-cmu-raster", null, "*.ras", 1),
- [".rf"] = new ContentTypeMapping("image/vnd.rn-realflash", null, "*.rf", 1),
- [".rgb"] = new ContentTypeMapping("image/x-rgb", null, "*.rgb", 1),
- [".svgz"] = new ContentTypeMapping("image/svg+xml", null, "*.svgz", 1),
- [".tif"] = new ContentTypeMapping("image/tiff", null, "*.tif", 1),
- [".tiff"] = new ContentTypeMapping("image/tiff", null, "*.tiff", 1),
- [".wbmp"] = new ContentTypeMapping("image/vnd.wap.wbmp", null, "*.wbmp", 1),
- [".xbm"] = new ContentTypeMapping("image/x-xbitmap", null, "*.xbm", 1),
- [".xpm"] = new ContentTypeMapping("image/x-xpixmap", null, "*.xpm", 1),
- [".xwd"] = new ContentTypeMapping("image/x-xwindowdump", null, "*.xwd", 1),
- [".3g2"] = new ContentTypeMapping("video/3gpp2", null, "*.3g2", 1),
- [".3gp2"] = new ContentTypeMapping("video/3gpp2", null, "*.3gp2", 1),
- [".3gp"] = new ContentTypeMapping("video/3gpp", null, "*.3gp", 1),
- [".3gpp"] = new ContentTypeMapping("video/3gpp", null, "*.3gpp", 1),
- [".asf"] = new ContentTypeMapping("video/x-ms-asf", null, "*.asf", 1),
- [".asr"] = new ContentTypeMapping("video/x-ms-asf", null, "*.asr", 1),
- [".asx"] = new ContentTypeMapping("video/x-ms-asf", null, "*.asx", 1),
- [".avi"] = new ContentTypeMapping("video/x-msvideo", null, "*.avi", 1),
- [".dvr"] = new ContentTypeMapping("video/x-ms-dvr", null, "*.dvr", 1),
- [".flv"] = new ContentTypeMapping("video/x-flv", null, "*.flv", 1),
- [".IVF"] = new ContentTypeMapping("video/x-ivf", null, "*.IVF", 1),
- [".lsf"] = new ContentTypeMapping("video/x-la-asf", null, "*.lsf", 1),
- [".lsx"] = new ContentTypeMapping("video/x-la-asf", null, "*.lsx", 1),
- [".m1v"] = new ContentTypeMapping("video/mpeg", null, "*.m1v", 1),
- [".m2ts"] = new ContentTypeMapping("video/vnd.dlna.mpeg-tts", null, "*.m2ts", 1),
- [".qt"] = new ContentTypeMapping("video/quicktime", null, "*.qt", 1),
- [".ts"] = new ContentTypeMapping("video/vnd.dlna.mpeg-tts", null, "*.ts", 1),
- [".tts"] = new ContentTypeMapping("video/vnd.dlna.mpeg-tts", null, "*.tts", 1),
- [".wm"] = new ContentTypeMapping("video/x-ms-wm", null, "*.wm", 1),
- [".wmp"] = new ContentTypeMapping("video/x-ms-wmp", null, "*.wmp", 1),
- [".wmv"] = new ContentTypeMapping("video/x-ms-wmv", null, "*.wmv", 1),
- [".wmx"] = new ContentTypeMapping("video/x-ms-wmx", null, "*.wmx", 1),
- [".wtv"] = new ContentTypeMapping("video/x-ms-wtv", null, "*.wtv", 1),
- [".wvx"] = new ContentTypeMapping("video/x-ms-wvx", null, "*.wvx", 1),
- [".aac"] = new ContentTypeMapping("audio/aac", null, "*.aac", 1),
- [".adt"] = new ContentTypeMapping("audio/vnd.dlna.adts", null, "*.adt", 1),
- [".adts"] = new ContentTypeMapping("audio/vnd.dlna.adts", null, "*.adts", 1),
- [".aif"] = new ContentTypeMapping("audio/x-aiff", null, "*.aif", 1),
- [".aifc"] = new ContentTypeMapping("audio/aiff", null, "*.aifc", 1),
- [".aiff"] = new ContentTypeMapping("audio/aiff", null, "*.aiff", 1),
- [".au"] = new ContentTypeMapping("audio/basic", null, "*.au", 1),
- [".m3u"] = new ContentTypeMapping("audio/x-mpegurl", null, "*.m3u", 1),
- [".m4a"] = new ContentTypeMapping("audio/mp4", null, "*.m4a", 1),
- [".mid"] = new ContentTypeMapping("audio/mid", null, "*.mid", 1),
- [".midi"] = new ContentTypeMapping("audio/mid", null, "*.midi", 1),
- [".mp3"] = new ContentTypeMapping("audio/mpeg", null, "*.mp3", 1),
- [".oga"] = new ContentTypeMapping("audio/ogg", null, "*.oga", 1),
- [".ra"] = new ContentTypeMapping("audio/x-pn-realaudio", null, "*.ra", 1),
- [".ram"] = new ContentTypeMapping("audio/x-pn-realaudio", null, "*.ram", 1),
- [".rmi"] = new ContentTypeMapping("audio/mid", null, "*.rmi", 1),
- [".rpm"] = new ContentTypeMapping("audio/x-pn-realaudio-plugin", null, "*.rpm", 1),
- [".smd"] = new ContentTypeMapping("audio/x-smd", null, "*.smd", 1),
- [".smx"] = new ContentTypeMapping("audio/x-smd", null, "*.smx", 1),
- [".smz"] = new ContentTypeMapping("audio/x-smd", null, "*.smz", 1),
- [".snd"] = new ContentTypeMapping("audio/basic", null, "*.snd", 1),
- [".spx"] = new ContentTypeMapping("audio/ogg", null, "*.spx", 1),
- [".wav"] = new ContentTypeMapping("audio/wav", null, "*.wav", 1),
- [".wax"] = new ContentTypeMapping("audio/x-ms-wax", null, "*.wax", 1),
- [".wma"] = new ContentTypeMapping("audio/x-ms-wma", null, "*.wma", 1),
- [".accdb"] = new ContentTypeMapping("application/msaccess", null, "*.accdb", 1),
- [".accde"] = new ContentTypeMapping("application/msaccess", null, "*.accde", 1),
- [".accdt"] = new ContentTypeMapping("application/msaccess", null, "*.accdt", 1),
- [".acx"] = new ContentTypeMapping("application/internet-property-stream", null, "*.acx", 1),
- [".ai"] = new ContentTypeMapping("application/postscript", null, "*.ai", 1),
- [".application"] = new ContentTypeMapping("application/x-ms-application", null, "*.application", 1),
- [".atom"] = new ContentTypeMapping("application/atom+xml", null, "*.atom", 1),
- [".axs"] = new ContentTypeMapping("application/olescript", null, "*.axs", 1),
- [".bcpio"] = new ContentTypeMapping("application/x-bcpio", null, "*.bcpio", 1),
- [".cab"] = new ContentTypeMapping("application/vnd.ms-cab-compressed", null, "*.cab", 1),
- [".calx"] = new ContentTypeMapping("application/vnd.ms-office.calx", null, "*.calx", 1),
- [".cat"] = new ContentTypeMapping("application/vnd.ms-pki.seccat", null, "*.cat", 1),
- [".cdf"] = new ContentTypeMapping("application/x-cdf", null, "*.cdf", 1),
- [".class"] = new ContentTypeMapping("application/x-java-applet", null, "*.class", 1),
- [".clp"] = new ContentTypeMapping("application/x-msclip", null, "*.clp", 1),
- [".cpio"] = new ContentTypeMapping("application/x-cpio", null, "*.cpio", 1),
- [".crd"] = new ContentTypeMapping("application/x-mscardfile", null, "*.crd", 1),
- [".crl"] = new ContentTypeMapping("application/pkix-crl", null, "*.crl", 1),
- [".crt"] = new ContentTypeMapping("application/x-x509-ca-cert", null, "*.crt", 1),
- [".csh"] = new ContentTypeMapping("application/x-csh", null, "*.csh", 1),
- [".dcr"] = new ContentTypeMapping("application/x-director", null, "*.dcr", 1),
- [".der"] = new ContentTypeMapping("application/x-x509-ca-cert", null, "*.der", 1),
- [".dir"] = new ContentTypeMapping("application/x-director", null, "*.dir", 1),
- [".doc"] = new ContentTypeMapping("application/msword", null, "*.doc", 1),
- [".docm"] = new ContentTypeMapping("application/vnd.ms-word.document.macroEnabled.12", null, "*.docm", 1),
- [".docx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.wordprocessingml.document", null, "*.docx", 1),
- [".dot"] = new ContentTypeMapping("application/msword", null, "*.dot", 1),
- [".dotm"] = new ContentTypeMapping("application/vnd.ms-word.template.macroEnabled.12", null, "*.dotm", 1),
- [".dotx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.wordprocessingml.template", null, "*.dotx", 1),
- [".dvi"] = new ContentTypeMapping("application/x-dvi", null, "*.dvi", 1),
- [".dwf"] = new ContentTypeMapping("drawing/x-dwf", null, "*.dwf", 1),
- [".dxr"] = new ContentTypeMapping("application/x-director", null, "*.dxr", 1),
- [".eml"] = new ContentTypeMapping("message/rfc822", null, "*.eml", 1),
- [".eot"] = new ContentTypeMapping("application/vnd.ms-fontobject", null, "*.eot", 1),
- [".eps"] = new ContentTypeMapping("application/postscript", null, "*.eps", 1),
- [".evy"] = new ContentTypeMapping("application/envoy", null, "*.evy", 1),
- [".exe"] = new ContentTypeMapping("application/vnd.microsoft.portable-executable", null, "*.exe", 1),
- [".fdf"] = new ContentTypeMapping("application/vnd.fdf", null, "*.fdf", 1),
- [".fif"] = new ContentTypeMapping("application/fractals", null, "*.fif", 1),
- [".flr"] = new ContentTypeMapping("x-world/x-vrml", null, "*.flr", 1),
- [".gtar"] = new ContentTypeMapping("application/x-gtar", null, "*.gtar", 1),
- [".hdf"] = new ContentTypeMapping("application/x-hdf", null, "*.hdf", 1),
- [".hhc"] = new ContentTypeMapping("application/x-oleobject", null, "*.hhc", 1),
- [".hlp"] = new ContentTypeMapping("application/winhlp", null, "*.hlp", 1),
- [".hqx"] = new ContentTypeMapping("application/mac-binhex40", null, "*.hqx", 1),
- [".hta"] = new ContentTypeMapping("application/hta", null, "*.hta", 1),
- [".iii"] = new ContentTypeMapping("application/x-iphone", null, "*.iii", 1),
- [".ins"] = new ContentTypeMapping("application/x-internet-signup", null, "*.ins", 1),
- [".isp"] = new ContentTypeMapping("application/x-internet-signup", null, "*.isp", 1),
- [".jar"] = new ContentTypeMapping("application/java-archive", null, "*.jar", 1),
- [".jck"] = new ContentTypeMapping("application/liquidmotion", null, "*.jck", 1),
- [".jcz"] = new ContentTypeMapping("application/liquidmotion", null, "*.jcz", 1),
- [".latex"] = new ContentTypeMapping("application/x-latex", null, "*.latex", 1),
- [".lit"] = new ContentTypeMapping("application/x-ms-reader", null, "*.lit", 1),
- [".m13"] = new ContentTypeMapping("application/x-msmediaview", null, "*.m13", 1),
- [".m14"] = new ContentTypeMapping("application/x-msmediaview", null, "*.m14", 1),
- [".man"] = new ContentTypeMapping("application/x-troff-man", null, "*.man", 1),
- [".manifest"] = new ContentTypeMapping("application/x-ms-manifest", null, "*.manifest", 1),
- [".mdb"] = new ContentTypeMapping("application/x-msaccess", null, "*.mdb", 1),
- [".me"] = new ContentTypeMapping("application/x-troff-me", null, "*.me", 1),
- [".mht"] = new ContentTypeMapping("message/rfc822", null, "*.mht", 1),
- [".mhtml"] = new ContentTypeMapping("message/rfc822", null, "*.mhtml", 1),
- [".mmf"] = new ContentTypeMapping("application/x-smaf", null, "*.mmf", 1),
- [".mny"] = new ContentTypeMapping("application/x-msmoney", null, "*.mny", 1),
- [".mpp"] = new ContentTypeMapping("application/vnd.ms-project", null, "*.mpp", 1),
- [".ms"] = new ContentTypeMapping("application/x-troff-ms", null, "*.ms", 1),
- [".mvb"] = new ContentTypeMapping("application/x-msmediaview", null, "*.mvb", 1),
- [".mvc"] = new ContentTypeMapping("application/x-miva-compiled", null, "*.mvc", 1),
- [".nc"] = new ContentTypeMapping("application/x-netcdf", null, "*.nc", 1),
- [".nws"] = new ContentTypeMapping("message/rfc822", null, "*.nws", 1),
- [".oda"] = new ContentTypeMapping("application/oda", null, "*.oda", 1),
- [".ods"] = new ContentTypeMapping("application/oleobject", null, "*.ods", 1),
- [".ogx"] = new ContentTypeMapping("application/ogg", null, "*.ogx", 1),
- [".one"] = new ContentTypeMapping("application/onenote", null, "*.one", 1),
- [".onea"] = new ContentTypeMapping("application/onenote", null, "*.onea", 1),
- [".onetoc"] = new ContentTypeMapping("application/onenote", null, "*.onetoc", 1),
- [".onetoc2"] = new ContentTypeMapping("application/onenote", null, "*.onetoc2", 1),
- [".onetmp"] = new ContentTypeMapping("application/onenote", null, "*.onetmp", 1),
- [".onepkg"] = new ContentTypeMapping("application/onenote", null, "*.onepkg", 1),
- [".osdx"] = new ContentTypeMapping("application/opensearchdescription+xml", null, "*.osdx", 1),
- [".p10"] = new ContentTypeMapping("application/pkcs10", null, "*.p10", 1),
- [".p12"] = new ContentTypeMapping("application/x-pkcs12", null, "*.p12", 1),
- [".p7b"] = new ContentTypeMapping("application/x-pkcs7-certificates", null, "*.p7b", 1),
- [".p7c"] = new ContentTypeMapping("application/pkcs7-mime", null, "*.p7c", 1),
- [".p7m"] = new ContentTypeMapping("application/pkcs7-mime", null, "*.p7m", 1),
- [".p7r"] = new ContentTypeMapping("application/x-pkcs7-certreqresp", null, "*.p7r", 1),
- [".p7s"] = new ContentTypeMapping("application/pkcs7-signature", null, "*.p7s", 1),
- [".pdf"] = new ContentTypeMapping("application/pdf", null, "*.pdf", 1),
- [".pfx"] = new ContentTypeMapping("application/x-pkcs12", null, "*.pfx", 1),
- [".pko"] = new ContentTypeMapping("application/vnd.ms-pki.pko", null, "*.pko", 1),
- [".pma"] = new ContentTypeMapping("application/x-perfmon", null, "*.pma", 1),
- [".pmc"] = new ContentTypeMapping("application/x-perfmon", null, "*.pmc", 1),
- [".pml"] = new ContentTypeMapping("application/x-perfmon", null, "*.pml", 1),
- [".pmr"] = new ContentTypeMapping("application/x-perfmon", null, "*.pmr", 1),
- [".pmw"] = new ContentTypeMapping("application/x-perfmon", null, "*.pmw", 1),
- [".pot"] = new ContentTypeMapping("application/vnd.ms-powerpoint", null, "*.pot", 1),
- [".potm"] = new ContentTypeMapping("application/vnd.ms-powerpoint.template.macroEnabled.12", null, "*.potm", 1),
- [".potx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.presentationml.template", null, "*.potx", 1),
- [".ppam"] = new ContentTypeMapping("application/vnd.ms-powerpoint.addin.macroEnabled.12", null, "*.ppam", 1),
- [".pps"] = new ContentTypeMapping("application/vnd.ms-powerpoint", null, "*.pps", 1),
- [".ppsm"] = new ContentTypeMapping("application/vnd.ms-powerpoint.slideshow.macroEnabled.12", null, "*.ppsm", 1),
- [".ppsx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.presentationml.slideshow", null, "*.ppsx", 1),
- [".ppt"] = new ContentTypeMapping("application/vnd.ms-powerpoint", null, "*.ppt", 1),
- [".pptm"] = new ContentTypeMapping("application/vnd.ms-powerpoint.presentation.macroEnabled.12", null, "*.pptm", 1),
- [".pptx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.presentationml.presentation", null, "*.pptx", 1),
- [".prf"] = new ContentTypeMapping("application/pics-rules", null, "*.prf", 1),
- [".ps"] = new ContentTypeMapping("application/postscript", null, "*.ps", 1),
- [".pub"] = new ContentTypeMapping("application/x-mspublisher", null, "*.pub", 1),
- [".qtl"] = new ContentTypeMapping("application/x-quicktimeplayer", null, "*.qtl", 1),
- [".rm"] = new ContentTypeMapping("application/vnd.rn-realmedia", null, "*.rm", 1),
- [".roff"] = new ContentTypeMapping("application/x-troff", null, "*.roff", 1),
- [".rtf"] = new ContentTypeMapping("application/rtf", null, "*.rtf", 1),
- [".scd"] = new ContentTypeMapping("application/x-msschedule", null, "*.scd", 1),
- [".setpay"] = new ContentTypeMapping("application/set-payment-initiation", null, "*.setpay", 1),
- [".setreg"] = new ContentTypeMapping("application/set-registration-initiation", null, "*.setreg", 1),
- [".sh"] = new ContentTypeMapping("application/x-sh", null, "*.sh", 1),
- [".shar"] = new ContentTypeMapping("application/x-shar", null, "*.shar", 1),
- [".sit"] = new ContentTypeMapping("application/x-stuffit", null, "*.sit", 1),
- [".sldm"] = new ContentTypeMapping("application/vnd.ms-powerpoint.slide.macroEnabled.12", null, "*.sldm", 1),
- [".sldx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.presentationml.slide", null, "*.sldx", 1),
- [".spc"] = new ContentTypeMapping("application/x-pkcs7-certificates", null, "*.spc", 1),
- [".spl"] = new ContentTypeMapping("application/futuresplash", null, "*.spl", 1),
- [".src"] = new ContentTypeMapping("application/x-wais-source", null, "*.src", 1),
- [".ssm"] = new ContentTypeMapping("application/streamingmedia", null, "*.ssm", 1),
- [".sst"] = new ContentTypeMapping("application/vnd.ms-pki.certstore", null, "*.sst", 1),
- [".stl"] = new ContentTypeMapping("application/vnd.ms-pki.stl", null, "*.stl", 1),
- [".sv4cpio"] = new ContentTypeMapping("application/x-sv4cpio", null, "*.sv4cpio", 1),
- [".sv4crc"] = new ContentTypeMapping("application/x-sv4crc", null, "*.sv4crc", 1),
- [".swf"] = new ContentTypeMapping("application/x-shockwave-flash", null, "*.swf", 1),
- [".t"] = new ContentTypeMapping("application/x-troff", null, "*.t", 1),
- [".tar"] = new ContentTypeMapping("application/x-tar", null, "*.tar", 1),
- [".tcl"] = new ContentTypeMapping("application/x-tcl", null, "*.tcl", 1),
- [".tex"] = new ContentTypeMapping("application/x-tex", null, "*.tex", 1),
- [".texi"] = new ContentTypeMapping("application/x-texinfo", null, "*.texi", 1),
- [".texinfo"] = new ContentTypeMapping("application/x-texinfo", null, "*.texinfo", 1),
- [".tgz"] = new ContentTypeMapping("application/x-compressed", null, "*.tgz", 1),
- [".thmx"] = new ContentTypeMapping("application/vnd.ms-officetheme", null, "*.thmx", 1),
- [".tr"] = new ContentTypeMapping("application/x-troff", null, "*.tr", 1),
- [".trm"] = new ContentTypeMapping("application/x-msterminal", null, "*.trm", 1),
- [".ttc"] = new ContentTypeMapping("application/x-font-ttf", null, "*.ttc", 1),
- [".ttf"] = new ContentTypeMapping("application/x-font-ttf", null, "*.ttf", 1),
- [".ustar"] = new ContentTypeMapping("application/x-ustar", null, "*.ustar", 1),
- [".vdx"] = new ContentTypeMapping("application/vnd.ms-visio.viewer", null, "*.vdx", 1),
- [".vsd"] = new ContentTypeMapping("application/vnd.visio", null, "*.vsd", 1),
- [".vss"] = new ContentTypeMapping("application/vnd.visio", null, "*.vss", 1),
- [".vst"] = new ContentTypeMapping("application/vnd.visio", null, "*.vst", 1),
- [".vsto"] = new ContentTypeMapping("application/x-ms-vsto", null, "*.vsto", 1),
- [".vsw"] = new ContentTypeMapping("application/vnd.visio", null, "*.vsw", 1),
- [".vsx"] = new ContentTypeMapping("application/vnd.visio", null, "*.vsx", 1),
- [".vtx"] = new ContentTypeMapping("application/vnd.visio", null, "*.vtx", 1),
- [".wcm"] = new ContentTypeMapping("application/vnd.ms-works", null, "*.wcm", 1),
- [".wdb"] = new ContentTypeMapping("application/vnd.ms-works", null, "*.wdb", 1),
- [".wks"] = new ContentTypeMapping("application/vnd.ms-works", null, "*.wks", 1),
- [".wmd"] = new ContentTypeMapping("application/x-ms-wmd", null, "*.wmd", 1),
- [".wmf"] = new ContentTypeMapping("application/x-msmetafile", null, "*.wmf", 1),
- [".wmlc"] = new ContentTypeMapping("application/vnd.wap.wmlc", null, "*.wmlc", 1),
- [".wmlsc"] = new ContentTypeMapping("application/vnd.wap.wmlscriptc", null, "*.wmlsc", 1),
- [".wmz"] = new ContentTypeMapping("application/x-ms-wmz", null, "*.wmz", 1),
- [".wps"] = new ContentTypeMapping("application/vnd.ms-works", null, "*.wps", 1),
- [".wri"] = new ContentTypeMapping("application/x-mswrite", null, "*.wri", 1),
- [".wrl"] = new ContentTypeMapping("x-world/x-vrml", null, "*.wrl", 1),
- [".wrz"] = new ContentTypeMapping("x-world/x-vrml", null, "*.wrz", 1),
- [".x"] = new ContentTypeMapping("application/directx", null, "*.x", 1),
- [".xaf"] = new ContentTypeMapping("x-world/x-vrml", null, "*.xaf", 1),
- [".xaml"] = new ContentTypeMapping("application/xaml+xml", null, "*.xaml", 1),
- [".xap"] = new ContentTypeMapping("application/x-silverlight-app", null, "*.xap", 1),
- [".xbap"] = new ContentTypeMapping("application/x-ms-xbap", null, "*.xbap", 1),
- [".xht"] = new ContentTypeMapping("application/xhtml+xml", null, "*.xht", 1),
- [".xhtml"] = new ContentTypeMapping("application/xhtml+xml", null, "*.xhtml", 1),
- [".xla"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xla", 1),
- [".xlam"] = new ContentTypeMapping("application/vnd.ms-excel.addin.macroEnabled.12", null, "*.xlam", 1),
- [".xlc"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xlc", 1),
- [".xlm"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xlm", 1),
- [".xls"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xls", 1),
- [".xlsb"] = new ContentTypeMapping("application/vnd.ms-excel.sheet.binary.macroEnabled.12", null, "*.xlsb", 1),
- [".xlsm"] = new ContentTypeMapping("application/vnd.ms-excel.sheet.macroEnabled.12", null, "*.xlsm", 1),
- [".xlsx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", null, "*.xlsx", 1),
- [".xlt"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xlt", 1),
- [".xltm"] = new ContentTypeMapping("application/vnd.ms-excel.template.macroEnabled.12", null, "*.xltm", 1),
- [".xltx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.spreadsheetml.template", null, "*.xltx", 1),
- [".xlw"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xlw", 1),
- [".xof"] = new ContentTypeMapping("x-world/x-vrml", null, "*.xof", 1),
- [".xps"] = new ContentTypeMapping("application/vnd.ms-xpsdocument", null, "*.xps", 1),
- [".z"] = new ContentTypeMapping("application/x-compress", null, "*.z", 1),
- [".zip"] = new ContentTypeMapping("application/x-zip-compressed", null, "*.zip", 1),
- [".aaf"] = new ContentTypeMapping("application/octet-stream", null, "*.aaf", 1),
- [".aca"] = new ContentTypeMapping("application/octet-stream", null, "*.aca", 1),
- [".afm"] = new ContentTypeMapping("application/octet-stream", null, "*.afm", 1),
- [".asd"] = new ContentTypeMapping("application/octet-stream", null, "*.asd", 1),
- [".asi"] = new ContentTypeMapping("application/octet-stream", null, "*.asi", 1),
- [".bin"] = new ContentTypeMapping("application/octet-stream", null, "*.bin", 1),
- [".chm"] = new ContentTypeMapping("application/octet-stream", null, "*.chm", 1),
- [".cur"] = new ContentTypeMapping("application/octet-stream", null, "*.cur", 1),
- [".deploy"] = new ContentTypeMapping("application/octet-stream", null, "*.deploy", 1),
- [".dsp"] = new ContentTypeMapping("application/octet-stream", null, "*.dsp", 1),
- [".dwp"] = new ContentTypeMapping("application/octet-stream", null, "*.dwp", 1),
- [".emz"] = new ContentTypeMapping("application/octet-stream", null, "*.emz", 1),
- [".fla"] = new ContentTypeMapping("application/octet-stream", null, "*.fla", 1),
- [".hhk"] = new ContentTypeMapping("application/octet-stream", null, "*.hhk", 1),
- [".hhp"] = new ContentTypeMapping("application/octet-stream", null, "*.hhp", 1),
- [".inf"] = new ContentTypeMapping("application/octet-stream", null, "*.inf", 1),
- [".java"] = new ContentTypeMapping("application/octet-stream", null, "*.java", 1),
- [".jpb"] = new ContentTypeMapping("application/octet-stream", null, "*.jpb", 1),
- [".lpk"] = new ContentTypeMapping("application/octet-stream", null, "*.lpk", 1),
- [".lzh"] = new ContentTypeMapping("application/octet-stream", null, "*.lzh", 1),
- [".mdp"] = new ContentTypeMapping("application/octet-stream", null, "*.mdp", 1),
- [".mix"] = new ContentTypeMapping("application/octet-stream", null, "*.mix", 1),
- [".msi"] = new ContentTypeMapping("application/octet-stream", null, "*.msi", 1),
- [".mso"] = new ContentTypeMapping("application/octet-stream", null, "*.mso", 1),
- [".ocx"] = new ContentTypeMapping("application/octet-stream", null, "*.ocx", 1),
- [".pcx"] = new ContentTypeMapping("application/octet-stream", null, "*.pcx", 1),
- [".pcz"] = new ContentTypeMapping("application/octet-stream", null, "*.pcz", 1),
- [".pfb"] = new ContentTypeMapping("application/octet-stream", null, "*.pfb", 1),
- [".pfm"] = new ContentTypeMapping("application/octet-stream", null, "*.pfm", 1),
- [".prm"] = new ContentTypeMapping("application/octet-stream", null, "*.prm", 1),
- [".prx"] = new ContentTypeMapping("application/octet-stream", null, "*.prx", 1),
- [".psd"] = new ContentTypeMapping("application/octet-stream", null, "*.psd", 1),
- [".psm"] = new ContentTypeMapping("application/octet-stream", null, "*.psm", 1),
- [".psp"] = new ContentTypeMapping("application/octet-stream", null, "*.psp", 1),
- [".qxd"] = new ContentTypeMapping("application/octet-stream", null, "*.qxd", 1),
- [".rar"] = new ContentTypeMapping("application/octet-stream", null, "*.rar", 1),
- [".sea"] = new ContentTypeMapping("application/octet-stream", null, "*.sea", 1),
- [".smi"] = new ContentTypeMapping("application/octet-stream", null, "*.smi", 1),
- [".snp"] = new ContentTypeMapping("application/octet-stream", null, "*.snp", 1),
- [".thn"] = new ContentTypeMapping("application/octet-stream", null, "*.thn", 1),
- [".toc"] = new ContentTypeMapping("application/octet-stream", null, "*.toc", 1),
- [".u32"] = new ContentTypeMapping("application/octet-stream", null, "*.u32", 1),
- [".xsn"] = new ContentTypeMapping("application/octet-stream", null, "*.xsn", 1),
- [".xtp"] = new ContentTypeMapping("application/octet-stream", null, "*.xtp", 1),
+ ["*.js"] = new ContentTypeMapping("text/javascript", null, "*.js", 1),
+ ["*.css"] = new ContentTypeMapping("text/css", null, "*.css", 1),
+ ["*.html"] = new ContentTypeMapping("text/html", null, "*.html", 1),
+ ["*.json"] = new ContentTypeMapping("application/json", null, "*.json", 1),
+ ["*.mjs"] = new ContentTypeMapping("text/javascript", null, "*.mjs", 1),
+ ["*.xml"] = new ContentTypeMapping("text/xml", null, "*.xml", 1),
+ ["*.htm"] = new ContentTypeMapping("text/html", null, "*.htm", 1),
+ ["*.wasm"] = new ContentTypeMapping("application/wasm", null, "*.wasm", 1),
+ ["*.txt"] = new ContentTypeMapping("text/plain", null, "*.txt", 1),
+ ["*.dll"] = new ContentTypeMapping("application/octet-stream", null, "*.dll", 1),
+ ["*.pdb"] = new ContentTypeMapping("application/octet-stream", null, "*.pdb", 1),
+ ["*.dat"] = new ContentTypeMapping("application/octet-stream", null, "*.dat", 1),
+ ["*.webmanifest"] = new ContentTypeMapping("application/manifest+json", null, "*.webmanifest", 1),
+ ["*.jsx"] = new ContentTypeMapping("text/jscript", null, "*.jsx", 1),
+ ["*.markdown"] = new ContentTypeMapping("text/markdown", null, "*.markdown", 1),
+ ["*.gz"] = new ContentTypeMapping("application/x-gzip", null, "*.gz", 1),
+ ["*.br"] = new ContentTypeMapping("application/octet-stream", null, "*.br", 1),
+ ["*.md"] = new ContentTypeMapping("text/markdown", null, "*.md", 1),
+ ["*.bmp"] = new ContentTypeMapping("image/bmp", null, "*.bmp", 1),
+ ["*.jpeg"] = new ContentTypeMapping("image/jpeg", null, "*.jpeg", 1),
+ ["*.jpg"] = new ContentTypeMapping("image/jpeg", null, "*.jpg", 1),
+ ["*.gif"] = new ContentTypeMapping("image/gif", null, "*.gif", 1),
+ ["*.svg"] = new ContentTypeMapping("image/svg+xml", null, "*.svg", 1),
+ ["*.png"] = new ContentTypeMapping("image/png", null, "*.png", 1),
+ ["*.webp"] = new ContentTypeMapping("image/webp", null, "*.webp", 1),
+ ["*.otf"] = new ContentTypeMapping("font/otf", null, "*.otf", 1),
+ ["*.woff2"] = new ContentTypeMapping("font/woff2", null, "*.woff2", 1),
+ ["*.m4v"] = new ContentTypeMapping("video/mp4", null, "*.m4v", 1),
+ ["*.mov"] = new ContentTypeMapping("video/quicktime", null, "*.mov", 1),
+ ["*.movie"] = new ContentTypeMapping("video/x-sgi-movie", null, "*.movie", 1),
+ ["*.mp2"] = new ContentTypeMapping("video/mpeg", null, "*.mp2", 1),
+ ["*.mp4"] = new ContentTypeMapping("video/mp4", null, "*.mp4", 1),
+ ["*.mp4v"] = new ContentTypeMapping("video/mp4", null, "*.mp4v", 1),
+ ["*.mpa"] = new ContentTypeMapping("video/mpeg", null, "*.mpa", 1),
+ ["*.mpe"] = new ContentTypeMapping("video/mpeg", null, "*.mpe", 1),
+ ["*.mpeg"] = new ContentTypeMapping("video/mpeg", null, "*.mpeg", 1),
+ ["*.mpg"] = new ContentTypeMapping("video/mpeg", null, "*.mpg", 1),
+ ["*.mpv2"] = new ContentTypeMapping("video/mpeg", null, "*.mpv2", 1),
+ ["*.nsc"] = new ContentTypeMapping("video/x-ms-asf", null, "*.nsc", 1),
+ ["*.ogg"] = new ContentTypeMapping("video/ogg", null, "*.ogg", 1),
+ ["*.ogv"] = new ContentTypeMapping("video/ogg", null, "*.ogv", 1),
+ ["*.webm"] = new ContentTypeMapping("video/webm", null, "*.webm", 1),
+ ["*.323"] = new ContentTypeMapping("text/h323", null, "*.323", 1),
+ ["*.appcache"] = new ContentTypeMapping("text/cache-manifest", null, "*.appcache", 1),
+ ["*.asm"] = new ContentTypeMapping("text/plain", null, "*.asm", 1),
+ ["*.bas"] = new ContentTypeMapping("text/plain", null, "*.bas", 1),
+ ["*.c"] = new ContentTypeMapping("text/plain", null, "*.c", 1),
+ ["*.cnf"] = new ContentTypeMapping("text/plain", null, "*.cnf", 1),
+ ["*.cpp"] = new ContentTypeMapping("text/plain", null, "*.cpp", 1),
+ ["*.csv"] = new ContentTypeMapping("text/csv", null, "*.csv", 1),
+ ["*.disco"] = new ContentTypeMapping("text/xml", null, "*.disco", 1),
+ ["*.dlm"] = new ContentTypeMapping("text/dlm", null, "*.dlm", 1),
+ ["*.dtd"] = new ContentTypeMapping("text/xml", null, "*.dtd", 1),
+ ["*.etx"] = new ContentTypeMapping("text/x-setext", null, "*.etx", 1),
+ ["*.h"] = new ContentTypeMapping("text/plain", null, "*.h", 1),
+ ["*.hdml"] = new ContentTypeMapping("text/x-hdml", null, "*.hdml", 1),
+ ["*.htc"] = new ContentTypeMapping("text/x-component", null, "*.htc", 1),
+ ["*.htt"] = new ContentTypeMapping("text/webviewhtml", null, "*.htt", 1),
+ ["*.hxt"] = new ContentTypeMapping("text/html", null, "*.hxt", 1),
+ ["*.ical"] = new ContentTypeMapping("text/calendar", null, "*.ical", 1),
+ ["*.icalendar"] = new ContentTypeMapping("text/calendar", null, "*.icalendar", 1),
+ ["*.ics"] = new ContentTypeMapping("text/calendar", null, "*.ics", 1),
+ ["*.ifb"] = new ContentTypeMapping("text/calendar", null, "*.ifb", 1),
+ ["*.map"] = new ContentTypeMapping("text/plain", null, "*.map", 1),
+ ["*.mno"] = new ContentTypeMapping("text/xml", null, "*.mno", 1),
+ ["*.odc"] = new ContentTypeMapping("text/x-ms-odc", null, "*.odc", 1),
+ ["*.rtx"] = new ContentTypeMapping("text/richtext", null, "*.rtx", 1),
+ ["*.sct"] = new ContentTypeMapping("text/scriptlet", null, "*.sct", 1),
+ ["*.sgml"] = new ContentTypeMapping("text/sgml", null, "*.sgml", 1),
+ ["*.tsv"] = new ContentTypeMapping("text/tab-separated-values", null, "*.tsv", 1),
+ ["*.uls"] = new ContentTypeMapping("text/iuls", null, "*.uls", 1),
+ ["*.vbs"] = new ContentTypeMapping("text/vbscript", null, "*.vbs", 1),
+ ["*.vcf"] = new ContentTypeMapping("text/x-vcard", null, "*.vcf", 1),
+ ["*.vcs"] = new ContentTypeMapping("text/plain", null, "*.vcs", 1),
+ ["*.vml"] = new ContentTypeMapping("text/xml", null, "*.vml", 1),
+ ["*.wml"] = new ContentTypeMapping("text/vnd.wap.wml", null, "*.wml", 1),
+ ["*.wmls"] = new ContentTypeMapping("text/vnd.wap.wmlscript", null, "*.wmls", 1),
+ ["*.wsdl"] = new ContentTypeMapping("text/xml", null, "*.wsdl", 1),
+ ["*.xdr"] = new ContentTypeMapping("text/plain", null, "*.xdr", 1),
+ ["*.xsd"] = new ContentTypeMapping("text/xml", null, "*.xsd", 1),
+ ["*.xsf"] = new ContentTypeMapping("text/xml", null, "*.xsf", 1),
+ ["*.xsl"] = new ContentTypeMapping("text/xml", null, "*.xsl", 1),
+ ["*.xslt"] = new ContentTypeMapping("text/xml", null, "*.xslt", 1),
+ ["*.woff"] = new ContentTypeMapping("application/font-woff", null, "*.woff", 1),
+ ["*.art"] = new ContentTypeMapping("image/x-jg", null, "*.art", 1),
+ ["*.cmx"] = new ContentTypeMapping("image/x-cmx", null, "*.cmx", 1),
+ ["*.cod"] = new ContentTypeMapping("image/cis-cod", null, "*.cod", 1),
+ ["*.dib"] = new ContentTypeMapping("image/bmp", null, "*.dib", 1),
+ ["*.ico"] = new ContentTypeMapping("image/x-icon", null, "*.ico", 1),
+ ["*.ief"] = new ContentTypeMapping("image/ief", null, "*.ief", 1),
+ ["*.jfif"] = new ContentTypeMapping("image/pjpeg", null, "*.jfif", 1),
+ ["*.jpe"] = new ContentTypeMapping("image/jpeg", null, "*.jpe", 1),
+ ["*.pbm"] = new ContentTypeMapping("image/x-portable-bitmap", null, "*.pbm", 1),
+ ["*.pgm"] = new ContentTypeMapping("image/x-portable-graymap", null, "*.pgm", 1),
+ ["*.pnm"] = new ContentTypeMapping("image/x-portable-anymap", null, "*.pnm", 1),
+ ["*.pnz"] = new ContentTypeMapping("image/png", null, "*.pnz", 1),
+ ["*.ppm"] = new ContentTypeMapping("image/x-portable-pixmap", null, "*.ppm", 1),
+ ["*.ras"] = new ContentTypeMapping("image/x-cmu-raster", null, "*.ras", 1),
+ ["*.rf"] = new ContentTypeMapping("image/vnd.rn-realflash", null, "*.rf", 1),
+ ["*.rgb"] = new ContentTypeMapping("image/x-rgb", null, "*.rgb", 1),
+ ["*.svgz"] = new ContentTypeMapping("image/svg+xml", null, "*.svgz", 1),
+ ["*.tif"] = new ContentTypeMapping("image/tiff", null, "*.tif", 1),
+ ["*.tiff"] = new ContentTypeMapping("image/tiff", null, "*.tiff", 1),
+ ["*.wbmp"] = new ContentTypeMapping("image/vnd.wap.wbmp", null, "*.wbmp", 1),
+ ["*.xbm"] = new ContentTypeMapping("image/x-xbitmap", null, "*.xbm", 1),
+ ["*.xpm"] = new ContentTypeMapping("image/x-xpixmap", null, "*.xpm", 1),
+ ["*.xwd"] = new ContentTypeMapping("image/x-xwindowdump", null, "*.xwd", 1),
+ ["*.3g2"] = new ContentTypeMapping("video/3gpp2", null, "*.3g2", 1),
+ ["*.3gp2"] = new ContentTypeMapping("video/3gpp2", null, "*.3gp2", 1),
+ ["*.3gp"] = new ContentTypeMapping("video/3gpp", null, "*.3gp", 1),
+ ["*.3gpp"] = new ContentTypeMapping("video/3gpp", null, "*.3gpp", 1),
+ ["*.asf"] = new ContentTypeMapping("video/x-ms-asf", null, "*.asf", 1),
+ ["*.asr"] = new ContentTypeMapping("video/x-ms-asf", null, "*.asr", 1),
+ ["*.asx"] = new ContentTypeMapping("video/x-ms-asf", null, "*.asx", 1),
+ ["*.avi"] = new ContentTypeMapping("video/x-msvideo", null, "*.avi", 1),
+ ["*.dvr"] = new ContentTypeMapping("video/x-ms-dvr", null, "*.dvr", 1),
+ ["*.flv"] = new ContentTypeMapping("video/x-flv", null, "*.flv", 1),
+ ["*.IVF"] = new ContentTypeMapping("video/x-ivf", null, "*.IVF", 1),
+ ["*.lsf"] = new ContentTypeMapping("video/x-la-asf", null, "*.lsf", 1),
+ ["*.lsx"] = new ContentTypeMapping("video/x-la-asf", null, "*.lsx", 1),
+ ["*.m1v"] = new ContentTypeMapping("video/mpeg", null, "*.m1v", 1),
+ ["*.m2ts"] = new ContentTypeMapping("video/vnd.dlna.mpeg-tts", null, "*.m2ts", 1),
+ ["*.qt"] = new ContentTypeMapping("video/quicktime", null, "*.qt", 1),
+ ["*.ts"] = new ContentTypeMapping("video/vnd.dlna.mpeg-tts", null, "*.ts", 1),
+ ["*.tts"] = new ContentTypeMapping("video/vnd.dlna.mpeg-tts", null, "*.tts", 1),
+ ["*.wm"] = new ContentTypeMapping("video/x-ms-wm", null, "*.wm", 1),
+ ["*.wmp"] = new ContentTypeMapping("video/x-ms-wmp", null, "*.wmp", 1),
+ ["*.wmv"] = new ContentTypeMapping("video/x-ms-wmv", null, "*.wmv", 1),
+ ["*.wmx"] = new ContentTypeMapping("video/x-ms-wmx", null, "*.wmx", 1),
+ ["*.wtv"] = new ContentTypeMapping("video/x-ms-wtv", null, "*.wtv", 1),
+ ["*.wvx"] = new ContentTypeMapping("video/x-ms-wvx", null, "*.wvx", 1),
+ ["*.aac"] = new ContentTypeMapping("audio/aac", null, "*.aac", 1),
+ ["*.adt"] = new ContentTypeMapping("audio/vnd.dlna.adts", null, "*.adt", 1),
+ ["*.adts"] = new ContentTypeMapping("audio/vnd.dlna.adts", null, "*.adts", 1),
+ ["*.aif"] = new ContentTypeMapping("audio/x-aiff", null, "*.aif", 1),
+ ["*.aifc"] = new ContentTypeMapping("audio/aiff", null, "*.aifc", 1),
+ ["*.aiff"] = new ContentTypeMapping("audio/aiff", null, "*.aiff", 1),
+ ["*.au"] = new ContentTypeMapping("audio/basic", null, "*.au", 1),
+ ["*.m3u"] = new ContentTypeMapping("audio/x-mpegurl", null, "*.m3u", 1),
+ ["*.m4a"] = new ContentTypeMapping("audio/mp4", null, "*.m4a", 1),
+ ["*.mid"] = new ContentTypeMapping("audio/mid", null, "*.mid", 1),
+ ["*.midi"] = new ContentTypeMapping("audio/mid", null, "*.midi", 1),
+ ["*.mp3"] = new ContentTypeMapping("audio/mpeg", null, "*.mp3", 1),
+ ["*.oga"] = new ContentTypeMapping("audio/ogg", null, "*.oga", 1),
+ ["*.ra"] = new ContentTypeMapping("audio/x-pn-realaudio", null, "*.ra", 1),
+ ["*.ram"] = new ContentTypeMapping("audio/x-pn-realaudio", null, "*.ram", 1),
+ ["*.rmi"] = new ContentTypeMapping("audio/mid", null, "*.rmi", 1),
+ ["*.rpm"] = new ContentTypeMapping("audio/x-pn-realaudio-plugin", null, "*.rpm", 1),
+ ["*.smd"] = new ContentTypeMapping("audio/x-smd", null, "*.smd", 1),
+ ["*.smx"] = new ContentTypeMapping("audio/x-smd", null, "*.smx", 1),
+ ["*.smz"] = new ContentTypeMapping("audio/x-smd", null, "*.smz", 1),
+ ["*.snd"] = new ContentTypeMapping("audio/basic", null, "*.snd", 1),
+ ["*.spx"] = new ContentTypeMapping("audio/ogg", null, "*.spx", 1),
+ ["*.wav"] = new ContentTypeMapping("audio/wav", null, "*.wav", 1),
+ ["*.wax"] = new ContentTypeMapping("audio/x-ms-wax", null, "*.wax", 1),
+ ["*.wma"] = new ContentTypeMapping("audio/x-ms-wma", null, "*.wma", 1),
+ ["*.accdb"] = new ContentTypeMapping("application/msaccess", null, "*.accdb", 1),
+ ["*.accde"] = new ContentTypeMapping("application/msaccess", null, "*.accde", 1),
+ ["*.accdt"] = new ContentTypeMapping("application/msaccess", null, "*.accdt", 1),
+ ["*.acx"] = new ContentTypeMapping("application/internet-property-stream", null, "*.acx", 1),
+ ["*.ai"] = new ContentTypeMapping("application/postscript", null, "*.ai", 1),
+ ["*.application"] = new ContentTypeMapping("application/x-ms-application", null, "*.application", 1),
+ ["*.atom"] = new ContentTypeMapping("application/atom+xml", null, "*.atom", 1),
+ ["*.axs"] = new ContentTypeMapping("application/olescript", null, "*.axs", 1),
+ ["*.bcpio"] = new ContentTypeMapping("application/x-bcpio", null, "*.bcpio", 1),
+ ["*.cab"] = new ContentTypeMapping("application/vnd.ms-cab-compressed", null, "*.cab", 1),
+ ["*.calx"] = new ContentTypeMapping("application/vnd.ms-office.calx", null, "*.calx", 1),
+ ["*.cat"] = new ContentTypeMapping("application/vnd.ms-pki.seccat", null, "*.cat", 1),
+ ["*.cdf"] = new ContentTypeMapping("application/x-cdf", null, "*.cdf", 1),
+ ["*.class"] = new ContentTypeMapping("application/x-java-applet", null, "*.class", 1),
+ ["*.clp"] = new ContentTypeMapping("application/x-msclip", null, "*.clp", 1),
+ ["*.cpio"] = new ContentTypeMapping("application/x-cpio", null, "*.cpio", 1),
+ ["*.crd"] = new ContentTypeMapping("application/x-mscardfile", null, "*.crd", 1),
+ ["*.crl"] = new ContentTypeMapping("application/pkix-crl", null, "*.crl", 1),
+ ["*.crt"] = new ContentTypeMapping("application/x-x509-ca-cert", null, "*.crt", 1),
+ ["*.csh"] = new ContentTypeMapping("application/x-csh", null, "*.csh", 1),
+ ["*.dcr"] = new ContentTypeMapping("application/x-director", null, "*.dcr", 1),
+ ["*.der"] = new ContentTypeMapping("application/x-x509-ca-cert", null, "*.der", 1),
+ ["*.dir"] = new ContentTypeMapping("application/x-director", null, "*.dir", 1),
+ ["*.doc"] = new ContentTypeMapping("application/msword", null, "*.doc", 1),
+ ["*.docm"] = new ContentTypeMapping("application/vnd.ms-word.document.macroEnabled.12", null, "*.docm", 1),
+ ["*.docx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.wordprocessingml.document", null, "*.docx", 1),
+ ["*.dot"] = new ContentTypeMapping("application/msword", null, "*.dot", 1),
+ ["*.dotm"] = new ContentTypeMapping("application/vnd.ms-word.template.macroEnabled.12", null, "*.dotm", 1),
+ ["*.dotx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.wordprocessingml.template", null, "*.dotx", 1),
+ ["*.dvi"] = new ContentTypeMapping("application/x-dvi", null, "*.dvi", 1),
+ ["*.dwf"] = new ContentTypeMapping("drawing/x-dwf", null, "*.dwf", 1),
+ ["*.dxr"] = new ContentTypeMapping("application/x-director", null, "*.dxr", 1),
+ ["*.eml"] = new ContentTypeMapping("message/rfc822", null, "*.eml", 1),
+ ["*.eot"] = new ContentTypeMapping("application/vnd.ms-fontobject", null, "*.eot", 1),
+ ["*.eps"] = new ContentTypeMapping("application/postscript", null, "*.eps", 1),
+ ["*.evy"] = new ContentTypeMapping("application/envoy", null, "*.evy", 1),
+ ["*.exe"] = new ContentTypeMapping("application/vnd.microsoft.portable-executable", null, "*.exe", 1),
+ ["*.fdf"] = new ContentTypeMapping("application/vnd.fdf", null, "*.fdf", 1),
+ ["*.fif"] = new ContentTypeMapping("application/fractals", null, "*.fif", 1),
+ ["*.flr"] = new ContentTypeMapping("x-world/x-vrml", null, "*.flr", 1),
+ ["*.gtar"] = new ContentTypeMapping("application/x-gtar", null, "*.gtar", 1),
+ ["*.hdf"] = new ContentTypeMapping("application/x-hdf", null, "*.hdf", 1),
+ ["*.hhc"] = new ContentTypeMapping("application/x-oleobject", null, "*.hhc", 1),
+ ["*.hlp"] = new ContentTypeMapping("application/winhlp", null, "*.hlp", 1),
+ ["*.hqx"] = new ContentTypeMapping("application/mac-binhex40", null, "*.hqx", 1),
+ ["*.hta"] = new ContentTypeMapping("application/hta", null, "*.hta", 1),
+ ["*.iii"] = new ContentTypeMapping("application/x-iphone", null, "*.iii", 1),
+ ["*.ins"] = new ContentTypeMapping("application/x-internet-signup", null, "*.ins", 1),
+ ["*.isp"] = new ContentTypeMapping("application/x-internet-signup", null, "*.isp", 1),
+ ["*.jar"] = new ContentTypeMapping("application/java-archive", null, "*.jar", 1),
+ ["*.jck"] = new ContentTypeMapping("application/liquidmotion", null, "*.jck", 1),
+ ["*.jcz"] = new ContentTypeMapping("application/liquidmotion", null, "*.jcz", 1),
+ ["*.latex"] = new ContentTypeMapping("application/x-latex", null, "*.latex", 1),
+ ["*.lit"] = new ContentTypeMapping("application/x-ms-reader", null, "*.lit", 1),
+ ["*.m13"] = new ContentTypeMapping("application/x-msmediaview", null, "*.m13", 1),
+ ["*.m14"] = new ContentTypeMapping("application/x-msmediaview", null, "*.m14", 1),
+ ["*.man"] = new ContentTypeMapping("application/x-troff-man", null, "*.man", 1),
+ ["*.manifest"] = new ContentTypeMapping("application/x-ms-manifest", null, "*.manifest", 1),
+ ["*.mdb"] = new ContentTypeMapping("application/x-msaccess", null, "*.mdb", 1),
+ ["*.me"] = new ContentTypeMapping("application/x-troff-me", null, "*.me", 1),
+ ["*.mht"] = new ContentTypeMapping("message/rfc822", null, "*.mht", 1),
+ ["*.mhtml"] = new ContentTypeMapping("message/rfc822", null, "*.mhtml", 1),
+ ["*.mmf"] = new ContentTypeMapping("application/x-smaf", null, "*.mmf", 1),
+ ["*.mny"] = new ContentTypeMapping("application/x-msmoney", null, "*.mny", 1),
+ ["*.mpp"] = new ContentTypeMapping("application/vnd.ms-project", null, "*.mpp", 1),
+ ["*.ms"] = new ContentTypeMapping("application/x-troff-ms", null, "*.ms", 1),
+ ["*.mvb"] = new ContentTypeMapping("application/x-msmediaview", null, "*.mvb", 1),
+ ["*.mvc"] = new ContentTypeMapping("application/x-miva-compiled", null, "*.mvc", 1),
+ ["*.nc"] = new ContentTypeMapping("application/x-netcdf", null, "*.nc", 1),
+ ["*.nws"] = new ContentTypeMapping("message/rfc822", null, "*.nws", 1),
+ ["*.oda"] = new ContentTypeMapping("application/oda", null, "*.oda", 1),
+ ["*.ods"] = new ContentTypeMapping("application/oleobject", null, "*.ods", 1),
+ ["*.ogx"] = new ContentTypeMapping("application/ogg", null, "*.ogx", 1),
+ ["*.one"] = new ContentTypeMapping("application/onenote", null, "*.one", 1),
+ ["*.onea"] = new ContentTypeMapping("application/onenote", null, "*.onea", 1),
+ ["*.onetoc"] = new ContentTypeMapping("application/onenote", null, "*.onetoc", 1),
+ ["*.onetoc2"] = new ContentTypeMapping("application/onenote", null, "*.onetoc2", 1),
+ ["*.onetmp"] = new ContentTypeMapping("application/onenote", null, "*.onetmp", 1),
+ ["*.onepkg"] = new ContentTypeMapping("application/onenote", null, "*.onepkg", 1),
+ ["*.osdx"] = new ContentTypeMapping("application/opensearchdescription+xml", null, "*.osdx", 1),
+ ["*.p10"] = new ContentTypeMapping("application/pkcs10", null, "*.p10", 1),
+ ["*.p12"] = new ContentTypeMapping("application/x-pkcs12", null, "*.p12", 1),
+ ["*.p7b"] = new ContentTypeMapping("application/x-pkcs7-certificates", null, "*.p7b", 1),
+ ["*.p7c"] = new ContentTypeMapping("application/pkcs7-mime", null, "*.p7c", 1),
+ ["*.p7m"] = new ContentTypeMapping("application/pkcs7-mime", null, "*.p7m", 1),
+ ["*.p7r"] = new ContentTypeMapping("application/x-pkcs7-certreqresp", null, "*.p7r", 1),
+ ["*.p7s"] = new ContentTypeMapping("application/pkcs7-signature", null, "*.p7s", 1),
+ ["*.pdf"] = new ContentTypeMapping("application/pdf", null, "*.pdf", 1),
+ ["*.pfx"] = new ContentTypeMapping("application/x-pkcs12", null, "*.pfx", 1),
+ ["*.pko"] = new ContentTypeMapping("application/vnd.ms-pki.pko", null, "*.pko", 1),
+ ["*.pma"] = new ContentTypeMapping("application/x-perfmon", null, "*.pma", 1),
+ ["*.pmc"] = new ContentTypeMapping("application/x-perfmon", null, "*.pmc", 1),
+ ["*.pml"] = new ContentTypeMapping("application/x-perfmon", null, "*.pml", 1),
+ ["*.pmr"] = new ContentTypeMapping("application/x-perfmon", null, "*.pmr", 1),
+ ["*.pmw"] = new ContentTypeMapping("application/x-perfmon", null, "*.pmw", 1),
+ ["*.pot"] = new ContentTypeMapping("application/vnd.ms-powerpoint", null, "*.pot", 1),
+ ["*.potm"] = new ContentTypeMapping("application/vnd.ms-powerpoint.template.macroEnabled.12", null, "*.potm", 1),
+ ["*.potx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.presentationml.template", null, "*.potx", 1),
+ ["*.ppam"] = new ContentTypeMapping("application/vnd.ms-powerpoint.addin.macroEnabled.12", null, "*.ppam", 1),
+ ["*.pps"] = new ContentTypeMapping("application/vnd.ms-powerpoint", null, "*.pps", 1),
+ ["*.ppsm"] = new ContentTypeMapping("application/vnd.ms-powerpoint.slideshow.macroEnabled.12", null, "*.ppsm", 1),
+ ["*.ppsx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.presentationml.slideshow", null, "*.ppsx", 1),
+ ["*.ppt"] = new ContentTypeMapping("application/vnd.ms-powerpoint", null, "*.ppt", 1),
+ ["*.pptm"] = new ContentTypeMapping("application/vnd.ms-powerpoint.presentation.macroEnabled.12", null, "*.pptm", 1),
+ ["*.pptx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.presentationml.presentation", null, "*.pptx", 1),
+ ["*.prf"] = new ContentTypeMapping("application/pics-rules", null, "*.prf", 1),
+ ["*.ps"] = new ContentTypeMapping("application/postscript", null, "*.ps", 1),
+ ["*.pub"] = new ContentTypeMapping("application/x-mspublisher", null, "*.pub", 1),
+ ["*.qtl"] = new ContentTypeMapping("application/x-quicktimeplayer", null, "*.qtl", 1),
+ ["*.rm"] = new ContentTypeMapping("application/vnd.rn-realmedia", null, "*.rm", 1),
+ ["*.roff"] = new ContentTypeMapping("application/x-troff", null, "*.roff", 1),
+ ["*.rtf"] = new ContentTypeMapping("application/rtf", null, "*.rtf", 1),
+ ["*.scd"] = new ContentTypeMapping("application/x-msschedule", null, "*.scd", 1),
+ ["*.setpay"] = new ContentTypeMapping("application/set-payment-initiation", null, "*.setpay", 1),
+ ["*.setreg"] = new ContentTypeMapping("application/set-registration-initiation", null, "*.setreg", 1),
+ ["*.sh"] = new ContentTypeMapping("application/x-sh", null, "*.sh", 1),
+ ["*.shar"] = new ContentTypeMapping("application/x-shar", null, "*.shar", 1),
+ ["*.sit"] = new ContentTypeMapping("application/x-stuffit", null, "*.sit", 1),
+ ["*.sldm"] = new ContentTypeMapping("application/vnd.ms-powerpoint.slide.macroEnabled.12", null, "*.sldm", 1),
+ ["*.sldx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.presentationml.slide", null, "*.sldx", 1),
+ ["*.spc"] = new ContentTypeMapping("application/x-pkcs7-certificates", null, "*.spc", 1),
+ ["*.spl"] = new ContentTypeMapping("application/futuresplash", null, "*.spl", 1),
+ ["*.src"] = new ContentTypeMapping("application/x-wais-source", null, "*.src", 1),
+ ["*.ssm"] = new ContentTypeMapping("application/streamingmedia", null, "*.ssm", 1),
+ ["*.sst"] = new ContentTypeMapping("application/vnd.ms-pki.certstore", null, "*.sst", 1),
+ ["*.stl"] = new ContentTypeMapping("application/vnd.ms-pki.stl", null, "*.stl", 1),
+ ["*.sv4cpio"] = new ContentTypeMapping("application/x-sv4cpio", null, "*.sv4cpio", 1),
+ ["*.sv4crc"] = new ContentTypeMapping("application/x-sv4crc", null, "*.sv4crc", 1),
+ ["*.swf"] = new ContentTypeMapping("application/x-shockwave-flash", null, "*.swf", 1),
+ ["*.t"] = new ContentTypeMapping("application/x-troff", null, "*.t", 1),
+ ["*.tar"] = new ContentTypeMapping("application/x-tar", null, "*.tar", 1),
+ ["*.tcl"] = new ContentTypeMapping("application/x-tcl", null, "*.tcl", 1),
+ ["*.tex"] = new ContentTypeMapping("application/x-tex", null, "*.tex", 1),
+ ["*.texi"] = new ContentTypeMapping("application/x-texinfo", null, "*.texi", 1),
+ ["*.texinfo"] = new ContentTypeMapping("application/x-texinfo", null, "*.texinfo", 1),
+ ["*.tgz"] = new ContentTypeMapping("application/x-compressed", null, "*.tgz", 1),
+ ["*.thmx"] = new ContentTypeMapping("application/vnd.ms-officetheme", null, "*.thmx", 1),
+ ["*.tr"] = new ContentTypeMapping("application/x-troff", null, "*.tr", 1),
+ ["*.trm"] = new ContentTypeMapping("application/x-msterminal", null, "*.trm", 1),
+ ["*.ttc"] = new ContentTypeMapping("application/x-font-ttf", null, "*.ttc", 1),
+ ["*.ttf"] = new ContentTypeMapping("application/x-font-ttf", null, "*.ttf", 1),
+ ["*.ustar"] = new ContentTypeMapping("application/x-ustar", null, "*.ustar", 1),
+ ["*.vdx"] = new ContentTypeMapping("application/vnd.ms-visio.viewer", null, "*.vdx", 1),
+ ["*.vsd"] = new ContentTypeMapping("application/vnd.visio", null, "*.vsd", 1),
+ ["*.vss"] = new ContentTypeMapping("application/vnd.visio", null, "*.vss", 1),
+ ["*.vst"] = new ContentTypeMapping("application/vnd.visio", null, "*.vst", 1),
+ ["*.vsto"] = new ContentTypeMapping("application/x-ms-vsto", null, "*.vsto", 1),
+ ["*.vsw"] = new ContentTypeMapping("application/vnd.visio", null, "*.vsw", 1),
+ ["*.vsx"] = new ContentTypeMapping("application/vnd.visio", null, "*.vsx", 1),
+ ["*.vtx"] = new ContentTypeMapping("application/vnd.visio", null, "*.vtx", 1),
+ ["*.wcm"] = new ContentTypeMapping("application/vnd.ms-works", null, "*.wcm", 1),
+ ["*.wdb"] = new ContentTypeMapping("application/vnd.ms-works", null, "*.wdb", 1),
+ ["*.wks"] = new ContentTypeMapping("application/vnd.ms-works", null, "*.wks", 1),
+ ["*.wmd"] = new ContentTypeMapping("application/x-ms-wmd", null, "*.wmd", 1),
+ ["*.wmf"] = new ContentTypeMapping("application/x-msmetafile", null, "*.wmf", 1),
+ ["*.wmlc"] = new ContentTypeMapping("application/vnd.wap.wmlc", null, "*.wmlc", 1),
+ ["*.wmlsc"] = new ContentTypeMapping("application/vnd.wap.wmlscriptc", null, "*.wmlsc", 1),
+ ["*.wmz"] = new ContentTypeMapping("application/x-ms-wmz", null, "*.wmz", 1),
+ ["*.wps"] = new ContentTypeMapping("application/vnd.ms-works", null, "*.wps", 1),
+ ["*.wri"] = new ContentTypeMapping("application/x-mswrite", null, "*.wri", 1),
+ ["*.wrl"] = new ContentTypeMapping("x-world/x-vrml", null, "*.wrl", 1),
+ ["*.wrz"] = new ContentTypeMapping("x-world/x-vrml", null, "*.wrz", 1),
+ ["*.x"] = new ContentTypeMapping("application/directx", null, "*.x", 1),
+ ["*.xaf"] = new ContentTypeMapping("x-world/x-vrml", null, "*.xaf", 1),
+ ["*.xaml"] = new ContentTypeMapping("application/xaml+xml", null, "*.xaml", 1),
+ ["*.xap"] = new ContentTypeMapping("application/x-silverlight-app", null, "*.xap", 1),
+ ["*.xbap"] = new ContentTypeMapping("application/x-ms-xbap", null, "*.xbap", 1),
+ ["*.xht"] = new ContentTypeMapping("application/xhtml+xml", null, "*.xht", 1),
+ ["*.xhtml"] = new ContentTypeMapping("application/xhtml+xml", null, "*.xhtml", 1),
+ ["*.xla"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xla", 1),
+ ["*.xlam"] = new ContentTypeMapping("application/vnd.ms-excel.addin.macroEnabled.12", null, "*.xlam", 1),
+ ["*.xlc"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xlc", 1),
+ ["*.xlm"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xlm", 1),
+ ["*.xls"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xls", 1),
+ ["*.xlsb"] = new ContentTypeMapping("application/vnd.ms-excel.sheet.binary.macroEnabled.12", null, "*.xlsb", 1),
+ ["*.xlsm"] = new ContentTypeMapping("application/vnd.ms-excel.sheet.macroEnabled.12", null, "*.xlsm", 1),
+ ["*.xlsx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", null, "*.xlsx", 1),
+ ["*.xlt"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xlt", 1),
+ ["*.xltm"] = new ContentTypeMapping("application/vnd.ms-excel.template.macroEnabled.12", null, "*.xltm", 1),
+ ["*.xltx"] = new ContentTypeMapping("application/vnd.openxmlformats-officedocument.spreadsheetml.template", null, "*.xltx", 1),
+ ["*.xlw"] = new ContentTypeMapping("application/vnd.ms-excel", null, "*.xlw", 1),
+ ["*.xof"] = new ContentTypeMapping("x-world/x-vrml", null, "*.xof", 1),
+ ["*.xps"] = new ContentTypeMapping("application/vnd.ms-xpsdocument", null, "*.xps", 1),
+ ["*.z"] = new ContentTypeMapping("application/x-compress", null, "*.z", 1),
+ ["*.zip"] = new ContentTypeMapping("application/x-zip-compressed", null, "*.zip", 1),
+ ["*.aaf"] = new ContentTypeMapping("application/octet-stream", null, "*.aaf", 1),
+ ["*.aca"] = new ContentTypeMapping("application/octet-stream", null, "*.aca", 1),
+ ["*.afm"] = new ContentTypeMapping("application/octet-stream", null, "*.afm", 1),
+ ["*.asd"] = new ContentTypeMapping("application/octet-stream", null, "*.asd", 1),
+ ["*.asi"] = new ContentTypeMapping("application/octet-stream", null, "*.asi", 1),
+ ["*.bin"] = new ContentTypeMapping("application/octet-stream", null, "*.bin", 1),
+ ["*.chm"] = new ContentTypeMapping("application/octet-stream", null, "*.chm", 1),
+ ["*.cur"] = new ContentTypeMapping("application/octet-stream", null, "*.cur", 1),
+ ["*.deploy"] = new ContentTypeMapping("application/octet-stream", null, "*.deploy", 1),
+ ["*.dsp"] = new ContentTypeMapping("application/octet-stream", null, "*.dsp", 1),
+ ["*.dwp"] = new ContentTypeMapping("application/octet-stream", null, "*.dwp", 1),
+ ["*.emz"] = new ContentTypeMapping("application/octet-stream", null, "*.emz", 1),
+ ["*.fla"] = new ContentTypeMapping("application/octet-stream", null, "*.fla", 1),
+ ["*.hhk"] = new ContentTypeMapping("application/octet-stream", null, "*.hhk", 1),
+ ["*.hhp"] = new ContentTypeMapping("application/octet-stream", null, "*.hhp", 1),
+ ["*.inf"] = new ContentTypeMapping("application/octet-stream", null, "*.inf", 1),
+ ["*.java"] = new ContentTypeMapping("application/octet-stream", null, "*.java", 1),
+ ["*.jpb"] = new ContentTypeMapping("application/octet-stream", null, "*.jpb", 1),
+ ["*.lpk"] = new ContentTypeMapping("application/octet-stream", null, "*.lpk", 1),
+ ["*.lzh"] = new ContentTypeMapping("application/octet-stream", null, "*.lzh", 1),
+ ["*.mdp"] = new ContentTypeMapping("application/octet-stream", null, "*.mdp", 1),
+ ["*.mix"] = new ContentTypeMapping("application/octet-stream", null, "*.mix", 1),
+ ["*.msi"] = new ContentTypeMapping("application/octet-stream", null, "*.msi", 1),
+ ["*.mso"] = new ContentTypeMapping("application/octet-stream", null, "*.mso", 1),
+ ["*.ocx"] = new ContentTypeMapping("application/octet-stream", null, "*.ocx", 1),
+ ["*.pcx"] = new ContentTypeMapping("application/octet-stream", null, "*.pcx", 1),
+ ["*.pcz"] = new ContentTypeMapping("application/octet-stream", null, "*.pcz", 1),
+ ["*.pfb"] = new ContentTypeMapping("application/octet-stream", null, "*.pfb", 1),
+ ["*.pfm"] = new ContentTypeMapping("application/octet-stream", null, "*.pfm", 1),
+ ["*.prm"] = new ContentTypeMapping("application/octet-stream", null, "*.prm", 1),
+ ["*.prx"] = new ContentTypeMapping("application/octet-stream", null, "*.prx", 1),
+ ["*.psd"] = new ContentTypeMapping("application/octet-stream", null, "*.psd", 1),
+ ["*.psm"] = new ContentTypeMapping("application/octet-stream", null, "*.psm", 1),
+ ["*.psp"] = new ContentTypeMapping("application/octet-stream", null, "*.psp", 1),
+ ["*.qxd"] = new ContentTypeMapping("application/octet-stream", null, "*.qxd", 1),
+ ["*.rar"] = new ContentTypeMapping("application/octet-stream", null, "*.rar", 1),
+ ["*.sea"] = new ContentTypeMapping("application/octet-stream", null, "*.sea", 1),
+ ["*.smi"] = new ContentTypeMapping("application/octet-stream", null, "*.smi", 1),
+ ["*.snp"] = new ContentTypeMapping("application/octet-stream", null, "*.snp", 1),
+ ["*.thn"] = new ContentTypeMapping("application/octet-stream", null, "*.thn", 1),
+ ["*.toc"] = new ContentTypeMapping("application/octet-stream", null, "*.toc", 1),
+ ["*.u32"] = new ContentTypeMapping("application/octet-stream", null, "*.u32", 1),
+ ["*.xsn"] = new ContentTypeMapping("application/octet-stream", null, "*.xsn", 1),
+ ["*.xtp"] = new ContentTypeMapping("application/octet-stream", null, "*.xtp", 1),
};
- internal ContentTypeMapping ResolveContentTypeMapping(string relativePath, TaskLoggingHelper log)
+ private readonly StaticWebAssetGlobMatcher _matcher;
+
+ private readonly Dictionary _customMappings;
+
+ public ContentTypeProvider(ContentTypeMapping[] customMappings)
{
+ _customMappings ??= [];
foreach (var mapping in customMappings)
{
- if (mapping.Matches(Path.GetFileName(relativePath)))
+ _customMappings[mapping.Pattern] = mapping;
+ }
+
+ _matcher = new StaticWebAssetGlobMatcherBuilder()
+ .AddIncludePatternsList(_builtInMappings.Keys)
+ .AddIncludePatternsList(_customMappings.Keys)
+ .Build();
+ }
+
+ // First we strip any compressed extension (e.g. .gz, .br) from the file name
+ // and then we try to match the file name with the existing mappings.
+ // If we don't find a match, we fallback to trying the entire file name.
+ internal ContentTypeMapping ResolveContentTypeMapping(StaticWebAssetGlobMatcher.MatchContext context, TaskLoggingHelper log)
+ {
+#if NET9_0_OR_GREATER
+ var relativePath = context.Path;
+ var fileNameSpan = Path.GetFileName(context.Path);
+ var fileName = relativePath[(relativePath.Length - fileNameSpan.Length)..];
+#else
+ var relativePath = context.PathString;
+ var fileName = Path.GetFileName(relativePath);
+#endif
+ var fileNameNoCompressionExt = ResolvePathWithoutCompressedExtension(fileName, out var hasCompressedExtension);
+
+ context.SetPathAndReinitialize(fileNameNoCompressionExt);
+ if (TryGetMapping(context, log, relativePath, out var mapping))
+ {
+ return mapping;
+ }
+ else if (hasCompressedExtension)
+ {
+ context.SetPathAndReinitialize(fileName);
+ if (hasCompressedExtension && TryGetMapping(context, log, relativePath, out mapping))
{
- // If a custom mapping matches, it wins over the built-in
- log.LogMessage(MessageImportance.Low, $"Matched {relativePath} to {mapping.MimeType} using pattern {mapping.Pattern}");
return mapping;
}
+ }
+
+ return default;
+ }
+
+#if NET9_0_OR_GREATER
+ private bool TryGetMapping(StaticWebAssetGlobMatcher.MatchContext context, TaskLoggingHelper log, ReadOnlySpan relativePath, out ContentTypeMapping mapping)
+#else
+ private bool TryGetMapping(StaticWebAssetGlobMatcher.MatchContext context, TaskLoggingHelper log, string relativePath, out ContentTypeMapping mapping)
+#endif
+ {
+ var match = _matcher.Match(context);
+ if (match.IsMatch)
+ {
+ if (_builtInMappings.TryGetValue(match.Pattern, out mapping) || _customMappings.TryGetValue(match.Pattern, out mapping))
+ {
+ log.LogMessage(MessageImportance.Low, $"Matched {relativePath} to {mapping.MimeType} using pattern {match.Pattern}");
+ return true;
+ }
else
{
- log.LogMessage(MessageImportance.Low, $"No match for {relativePath} using pattern {mapping.Pattern}");
+ throw new InvalidOperationException("Matched pattern but no mapping found.");
}
}
- return ResolveBuiltIn(relativePath, log);
+ mapping = default;
+ return false;
}
- private ContentTypeMapping ResolveBuiltIn(string relativePath, TaskLoggingHelper log)
+#if NET9_0_OR_GREATER
+ private static ReadOnlySpan ResolvePathWithoutCompressedExtension(ReadOnlySpan fileName, out bool hasCompressedExtension)
+#else
+ private static string ResolvePathWithoutCompressedExtension(string fileName, out bool hasCompressedExtension)
+#endif
{
- var extension = Path.GetExtension(relativePath);
- if (extension == ".gz" || extension == ".br")
+ var extension = Path.GetExtension(fileName);
+ hasCompressedExtension = extension.Equals(".gz", StringComparison.OrdinalIgnoreCase) || extension.Equals(".br", StringComparison.OrdinalIgnoreCase);
+ if (hasCompressedExtension)
{
- var fileName = Path.GetFileNameWithoutExtension(relativePath);
- if (Path.GetExtension(fileName) != "")
+ var fileNameNoExtension = Path.GetFileNameWithoutExtension(fileName);
+ if (!Path.GetExtension(fileNameNoExtension).Equals("", StringComparison.Ordinal))
{
- var result = ResolveBuiltIn(fileName, log);
- // If we don't have a specific mapping for the other extension, use any mapping available for `.gz` or `.br`
- return result.MimeType == null && _builtInMappings.TryGetValue(extension, out var compressed) ?
- compressed :
- result;
+#if NET9_0_OR_GREATER
+ return fileName[..fileNameNoExtension.Length];
+#else
+ return fileName.Substring(0, fileNameNoExtension.Length);
+#endif
}
}
- return _builtInMappings.TryGetValue(extension, out var mapping) ? mapping : default;
+ return fileName;
}
}
diff --git a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs
index 476812b17898..987bfbd1542a 100644
--- a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs
+++ b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAsset.cs
@@ -2,7 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
-using System.IO;
+using System.Globalization;
using System.Security.Cryptography;
using System.Security.Principal;
using Microsoft.Build.Framework;
@@ -16,31 +16,35 @@ internal class StaticWebAsset : IEquatable, IComparable, IComparable
#endif
+{
+ public const string DateTimeAssetFormat = "ddd, dd MMM yyyy HH:mm:ss 'GMT'";
+
+ public StaticWebAsset()
{
- public StaticWebAsset()
- {
- }
+ }
- public StaticWebAsset(StaticWebAsset asset)
- {
- Identity = asset.Identity;
- SourceType = asset.SourceType;
- SourceId = asset.SourceId;
- ContentRoot = asset.ContentRoot;
- BasePath = asset.BasePath;
- RelativePath = asset.RelativePath;
- AssetKind = asset.AssetKind;
- AssetMode = asset.AssetMode;
- AssetRole = asset.AssetRole;
- AssetMergeBehavior = asset.AssetMergeBehavior;
- AssetMergeSource = asset.AssetMergeSource;
- RelatedAsset = asset.RelatedAsset;
- AssetTraitName = asset.AssetTraitName;
- AssetTraitValue = asset.AssetTraitValue;
- CopyToOutputDirectory = asset.CopyToOutputDirectory;
- CopyToPublishDirectory = asset.CopyToPublishDirectory;
- OriginalItemSpec = asset.OriginalItemSpec;
- }
+ public StaticWebAsset(StaticWebAsset asset)
+ {
+ Identity = asset.Identity;
+ SourceType = asset.SourceType;
+ SourceId = asset.SourceId;
+ ContentRoot = asset.ContentRoot;
+ BasePath = asset.BasePath;
+ RelativePath = asset.RelativePath;
+ AssetKind = asset.AssetKind;
+ AssetMode = asset.AssetMode;
+ AssetRole = asset.AssetRole;
+ AssetMergeBehavior = asset.AssetMergeBehavior;
+ AssetMergeSource = asset.AssetMergeSource;
+ RelatedAsset = asset.RelatedAsset;
+ AssetTraitName = asset.AssetTraitName;
+ AssetTraitValue = asset.AssetTraitValue;
+ CopyToOutputDirectory = asset.CopyToOutputDirectory;
+ CopyToPublishDirectory = asset.CopyToPublishDirectory;
+ OriginalItemSpec = asset.OriginalItemSpec;
+ FileLength = asset.FileLength;
+ LastWriteTime = asset.LastWriteTime;
+ }
public string Identity { get; set; }
@@ -78,7 +82,11 @@ public StaticWebAsset(StaticWebAsset asset)
public string CopyToPublishDirectory { get; set; }
- public string OriginalItemSpec { get; set; }
+ public string OriginalItemSpec { get; set; }
+
+ public long FileLength { get; set; } = -1;
+
+ public DateTimeOffset LastWriteTime { get; set; } = DateTimeOffset.MinValue;
public static StaticWebAsset FromTaskItem(ITaskItem item)
{
@@ -174,11 +182,11 @@ internal static bool ValidateAssetGroup(string path, IReadOnlyList
AssetKinds.IsKind(AssetKind, assetKind);
- public static StaticWebAsset FromV1TaskItem(ITaskItem item)
- {
- var result = FromTaskItemCore(item);
- result.ApplyDefaults();
- result.OriginalItemSpec = item.GetMetadata("FullPath");
+ public static StaticWebAsset FromV1TaskItem(ITaskItem item)
+ {
+ var result = FromTaskItemCore(item);
+ result.ApplyDefaults();
+ result.OriginalItemSpec = string.IsNullOrEmpty(result.OriginalItemSpec) ? item.GetMetadata("FullPath") : result.OriginalItemSpec;
result.Normalize();
result.Validate();
@@ -186,56 +194,61 @@ public static StaticWebAsset FromV1TaskItem(ITaskItem item)
return result;
}
- private static StaticWebAsset FromTaskItemCore(ITaskItem item) =>
- new()
- {
- // Register the identity as the full path since assets might have come
- // from packages and other sources and the identity (which is typically
- // just the relative path from the project) is not enough to locate them.
- Identity = item.GetMetadata("FullPath"),
- SourceType = item.GetMetadata(nameof(SourceType)),
- SourceId = item.GetMetadata(nameof(SourceId)),
- ContentRoot = item.GetMetadata(nameof(ContentRoot)),
- BasePath = item.GetMetadata(nameof(BasePath)),
- RelativePath = item.GetMetadata(nameof(RelativePath)),
- AssetKind = item.GetMetadata(nameof(AssetKind)),
- AssetMode = item.GetMetadata(nameof(AssetMode)),
- AssetRole = item.GetMetadata(nameof(AssetRole)),
- AssetMergeSource = item.GetMetadata(nameof(AssetMergeSource)),
- AssetMergeBehavior = item.GetMetadata(nameof(AssetMergeBehavior)),
- RelatedAsset = item.GetMetadata(nameof(RelatedAsset)),
- AssetTraitName = item.GetMetadata(nameof(AssetTraitName)),
- AssetTraitValue = item.GetMetadata(nameof(AssetTraitValue)),
- Fingerprint = item.GetMetadata(nameof(Fingerprint)),
- Integrity = item.GetMetadata(nameof(Integrity)),
- CopyToOutputDirectory = item.GetMetadata(nameof(CopyToOutputDirectory)),
- CopyToPublishDirectory = item.GetMetadata(nameof(CopyToPublishDirectory)),
- OriginalItemSpec = item.GetMetadata(nameof(OriginalItemSpec)),
- };
-
- public void ApplyDefaults()
- {
- CopyToOutputDirectory = string.IsNullOrEmpty(CopyToOutputDirectory) ? AssetCopyOptions.Never : CopyToOutputDirectory;
- CopyToPublishDirectory = string.IsNullOrEmpty(CopyToPublishDirectory) ? AssetCopyOptions.PreserveNewest : CopyToPublishDirectory;
- (Fingerprint, Integrity) = ComputeFingerprintAndIntegrity();
- AssetKind = !string.IsNullOrEmpty(AssetKind) ? AssetKind : !ShouldCopyToPublishDirectory() ? AssetKinds.Build : AssetKinds.All;
- AssetMode = string.IsNullOrEmpty(AssetMode) ? AssetModes.All : AssetMode;
- AssetRole = string.IsNullOrEmpty(AssetRole) ? AssetRoles.Primary : AssetRole;
+ private static StaticWebAsset FromTaskItemCore(ITaskItem item) =>
+ new()
+ {
+ // Register the identity as the full path since assets might have come
+ // from packages and other sources and the identity (which is typically
+ // just the relative path from the project) is not enough to locate them.
+ Identity = item.GetMetadata("FullPath"),
+ SourceType = item.GetMetadata(nameof(SourceType)),
+ SourceId = item.GetMetadata(nameof(SourceId)),
+ ContentRoot = item.GetMetadata(nameof(ContentRoot)),
+ BasePath = item.GetMetadata(nameof(BasePath)),
+ RelativePath = item.GetMetadata(nameof(RelativePath)),
+ AssetKind = item.GetMetadata(nameof(AssetKind)),
+ AssetMode = item.GetMetadata(nameof(AssetMode)),
+ AssetRole = item.GetMetadata(nameof(AssetRole)),
+ AssetMergeSource = item.GetMetadata(nameof(AssetMergeSource)),
+ AssetMergeBehavior = item.GetMetadata(nameof(AssetMergeBehavior)),
+ RelatedAsset = item.GetMetadata(nameof(RelatedAsset)),
+ AssetTraitName = item.GetMetadata(nameof(AssetTraitName)),
+ AssetTraitValue = item.GetMetadata(nameof(AssetTraitValue)),
+ Fingerprint = item.GetMetadata(nameof(Fingerprint)),
+ Integrity = item.GetMetadata(nameof(Integrity)),
+ CopyToOutputDirectory = item.GetMetadata(nameof(CopyToOutputDirectory)),
+ CopyToPublishDirectory = item.GetMetadata(nameof(CopyToPublishDirectory)),
+ OriginalItemSpec = item.GetMetadata(nameof(OriginalItemSpec)),
+ FileLength = item.GetMetadata("FileLength") is string fileLengthString &&
+ long.TryParse(fileLengthString, out var fileLength) ? fileLength : -1,
+ LastWriteTime = item.GetMetadata("LastWriteTime") is string lastWriteTimeString &&
+ DateTimeOffset.TryParse(lastWriteTimeString, out var lastWriteTime) ? lastWriteTime : DateTimeOffset.MinValue
+ };
+
+ public void ApplyDefaults()
+ {
+ CopyToOutputDirectory = string.IsNullOrEmpty(CopyToOutputDirectory) ? AssetCopyOptions.Never : CopyToOutputDirectory;
+ CopyToPublishDirectory = string.IsNullOrEmpty(CopyToPublishDirectory) ? AssetCopyOptions.PreserveNewest : CopyToPublishDirectory;
+ AssetKind = !string.IsNullOrEmpty(AssetKind) ? AssetKind : !ShouldCopyToPublishDirectory() ? AssetKinds.Build : AssetKinds.All;
+ AssetMode = string.IsNullOrEmpty(AssetMode) ? AssetModes.All : AssetMode;
+ AssetRole = string.IsNullOrEmpty(AssetRole) ? AssetRoles.Primary : AssetRole;
+ if (string.IsNullOrEmpty(Fingerprint) || string.IsNullOrEmpty(Integrity) || FileLength == -1 || LastWriteTime == DateTimeOffset.MinValue)
+ {
+ var file = ResolveFile(Identity, OriginalItemSpec);
+ (Fingerprint, Integrity) = string.IsNullOrEmpty(Fingerprint) || string.IsNullOrEmpty(Integrity) ?
+ ComputeFingerprintAndIntegrityIfNeeded(file) : (Fingerprint, Integrity);
+ FileLength = FileLength == -1 ? file.Length : FileLength;
+ LastWriteTime = LastWriteTime == DateTimeOffset.MinValue ? file.LastWriteTimeUtc : LastWriteTime;
}
+ }
- private (string Fingerprint, string Integrity) ComputeFingerprintAndIntegrity() =>
- (Fingerprint, Integrity) switch
- {
- ("", "") => ComputeFingerprintAndIntegrity(Identity, OriginalItemSpec),
- (not null, not null) => (Fingerprint, Integrity),
- _ => ComputeFingerprintAndIntegrity(Identity, OriginalItemSpec)
- };
-
- internal static (string fingerprint, string integrity) ComputeFingerprintAndIntegrity(string identity, string originalItemSpec)
+ private (string Fingerprint, string Integrity) ComputeFingerprintAndIntegrityIfNeeded(FileInfo file) =>
+ (Fingerprint, Integrity) switch
{
- var fileInfo = ResolveFile(identity, originalItemSpec);
- return ComputeFingerprintAndIntegrity(fileInfo);
- }
+ ("", "") => ComputeFingerprintAndIntegrity(file),
+ (not null, not null) => (Fingerprint, Integrity),
+ _ => ComputeFingerprintAndIntegrity(file)
+ };
internal static (string fingerprint, string integrity) ComputeFingerprintAndIntegrity(FileInfo fileInfo)
{
@@ -284,42 +297,44 @@ public static string CombineNormalizedPaths(string prefix, string basePath, stri
.TrimStart(separator);
}
- public ITaskItem ToTaskItem()
- {
- var result = new TaskItem(Identity);
- result.SetMetadata(nameof(SourceType), SourceType);
- result.SetMetadata(nameof(SourceId), SourceId);
- result.SetMetadata(nameof(ContentRoot), ContentRoot);
- result.SetMetadata(nameof(BasePath), BasePath);
- result.SetMetadata(nameof(RelativePath), RelativePath);
- result.SetMetadata(nameof(AssetKind), AssetKind);
- result.SetMetadata(nameof(AssetMode), AssetMode);
- result.SetMetadata(nameof(AssetRole), AssetRole);
- result.SetMetadata(nameof(AssetMergeSource), AssetMergeSource);
- result.SetMetadata(nameof(AssetMergeBehavior), AssetMergeBehavior);
- result.SetMetadata(nameof(RelatedAsset), RelatedAsset);
- result.SetMetadata(nameof(AssetTraitName), AssetTraitName);
- result.SetMetadata(nameof(AssetTraitValue), AssetTraitValue);
- result.SetMetadata(nameof(Fingerprint), Fingerprint);
- result.SetMetadata(nameof(Integrity), Integrity);
- result.SetMetadata(nameof(CopyToOutputDirectory), CopyToOutputDirectory);
- result.SetMetadata(nameof(CopyToPublishDirectory), CopyToPublishDirectory);
- result.SetMetadata(nameof(OriginalItemSpec), OriginalItemSpec);
- return result;
- }
+ public ITaskItem ToTaskItem()
+ {
+ var result = new TaskItem(Identity);
+ result.SetMetadata(nameof(SourceType), SourceType);
+ result.SetMetadata(nameof(SourceId), SourceId);
+ result.SetMetadata(nameof(ContentRoot), ContentRoot);
+ result.SetMetadata(nameof(BasePath), BasePath);
+ result.SetMetadata(nameof(RelativePath), RelativePath);
+ result.SetMetadata(nameof(AssetKind), AssetKind);
+ result.SetMetadata(nameof(AssetMode), AssetMode);
+ result.SetMetadata(nameof(AssetRole), AssetRole);
+ result.SetMetadata(nameof(AssetMergeSource), AssetMergeSource);
+ result.SetMetadata(nameof(AssetMergeBehavior), AssetMergeBehavior);
+ result.SetMetadata(nameof(RelatedAsset), RelatedAsset);
+ result.SetMetadata(nameof(AssetTraitName), AssetTraitName);
+ result.SetMetadata(nameof(AssetTraitValue), AssetTraitValue);
+ result.SetMetadata(nameof(Fingerprint), Fingerprint);
+ result.SetMetadata(nameof(Integrity), Integrity);
+ result.SetMetadata(nameof(CopyToOutputDirectory), CopyToOutputDirectory);
+ result.SetMetadata(nameof(CopyToPublishDirectory), CopyToPublishDirectory);
+ result.SetMetadata(nameof(OriginalItemSpec), OriginalItemSpec);
+ result.SetMetadata(nameof(FileLength), FileLength.ToString(CultureInfo.InvariantCulture));
+ result.SetMetadata(nameof(LastWriteTime), LastWriteTime.ToString(DateTimeAssetFormat, CultureInfo.InvariantCulture));
+ return result;
+ }
- public void Validate()
+ public void Validate()
+ {
+ switch (SourceType)
{
- switch (SourceType)
- {
- case SourceTypes.Discovered:
- case SourceTypes.Computed:
- case SourceTypes.Project:
- case SourceTypes.Package:
- break;
- default:
- throw new InvalidOperationException($"Unknown source type '{SourceType}' for '{Identity}'.");
- };
+ case SourceTypes.Discovered:
+ case SourceTypes.Computed:
+ case SourceTypes.Project:
+ case SourceTypes.Package:
+ break;
+ default:
+ throw new InvalidOperationException($"Unknown source type '{SourceType}' for '{Identity}'.");
+ }
if (string.IsNullOrEmpty(SourceId))
{
@@ -346,35 +361,35 @@ public void Validate()
throw new InvalidOperationException($"The '{nameof(OriginalItemSpec)}' for the asset must be defined for '{Identity}'.");
}
- switch (AssetKind)
- {
- case AssetKinds.All:
- case AssetKinds.Build:
- case AssetKinds.Publish:
- break;
- default:
- throw new InvalidOperationException($"Unknown Asset kind '{AssetKind}' for '{Identity}'.");
- };
+ switch (AssetKind)
+ {
+ case AssetKinds.All:
+ case AssetKinds.Build:
+ case AssetKinds.Publish:
+ break;
+ default:
+ throw new InvalidOperationException($"Unknown Asset kind '{AssetKind}' for '{Identity}'.");
+ }
- switch (AssetMode)
- {
- case AssetModes.All:
- case AssetModes.CurrentProject:
- case AssetModes.Reference:
- break;
- default:
- throw new InvalidOperationException($"Unknown Asset mode '{AssetMode}' for '{Identity}'.");
- };
+ switch (AssetMode)
+ {
+ case AssetModes.All:
+ case AssetModes.CurrentProject:
+ case AssetModes.Reference:
+ break;
+ default:
+ throw new InvalidOperationException($"Unknown Asset mode '{AssetMode}' for '{Identity}'.");
+ }
- switch (AssetRole)
- {
- case AssetRoles.Primary:
- case AssetRoles.Related:
- case AssetRoles.Alternative:
- break;
- default:
- throw new InvalidOperationException($"Unknown Asset role '{AssetRole}' for '{Identity}'.");
- };
+ switch (AssetRole)
+ {
+ case AssetRoles.Primary:
+ case AssetRoles.Related:
+ case AssetRoles.Alternative:
+ break;
+ default:
+ throw new InvalidOperationException($"Unknown Asset role '{AssetRole}' for '{Identity}'.");
+ }
if (!IsPrimaryAsset() && string.IsNullOrEmpty(RelatedAsset))
{
@@ -391,53 +406,67 @@ public void Validate()
throw new InvalidOperationException($"Fingerprint for '{Identity}' is not defined.");
}
- if (string.IsNullOrEmpty(Integrity))
- {
- throw new InvalidOperationException($"Integrity for '{Identity}' is not defined.");
- }
- }
-
- internal static StaticWebAsset FromProperties(
- string identity,
- string sourceId,
- string sourceType,
- string basePath,
- string relativePath,
- string contentRoot,
- string assetKind,
- string assetMode,
- string assetRole,
- string assetMergeSource,
- string relatedAsset,
- string assetTraitName,
- string assetTraitValue,
- string fingerprint,
- string integrity,
- string copyToOutputDirectory,
- string copyToPublishDirectory,
- string originalItemSpec)
- {
- var result = new StaticWebAsset
- {
- Identity = identity,
- SourceId = sourceId,
- SourceType = sourceType,
- ContentRoot = contentRoot,
- BasePath = basePath,
- RelativePath = relativePath,
- AssetKind = assetKind,
- AssetMode = assetMode,
- AssetRole = assetRole,
- AssetMergeSource = assetMergeSource,
- RelatedAsset = relatedAsset,
- AssetTraitName = assetTraitName,
- AssetTraitValue = assetTraitValue,
- Fingerprint = fingerprint,
- Integrity = integrity,
- CopyToOutputDirectory = copyToOutputDirectory,
- CopyToPublishDirectory = copyToPublishDirectory,
- OriginalItemSpec = originalItemSpec
- };
+ if (string.IsNullOrEmpty(Integrity))
+ {
+ throw new InvalidOperationException($"Integrity for '{Identity}' is not defined.");
+ }
+
+ if (FileLength < 0)
+ {
+ throw new InvalidOperationException($"File length for '{Identity}' is not defined.");
+ }
+
+ if (LastWriteTime == DateTimeOffset.MinValue)
+ {
+ throw new InvalidOperationException($"Last write time for '{Identity}' is not defined.");
+ }
+ }
+
+ internal static StaticWebAsset FromProperties(
+ string identity,
+ string sourceId,
+ string sourceType,
+ string basePath,
+ string relativePath,
+ string contentRoot,
+ string assetKind,
+ string assetMode,
+ string assetRole,
+ string assetMergeSource,
+ string relatedAsset,
+ string assetTraitName,
+ string assetTraitValue,
+ string fingerprint,
+ string integrity,
+ string copyToOutputDirectory,
+ string copyToPublishDirectory,
+ string originalItemSpec,
+ long fileLength,
+ DateTimeOffset lastWriteTime)
+ {
+ var result = new StaticWebAsset
+ {
+ Identity = identity,
+ SourceId = sourceId,
+ SourceType = sourceType,
+ ContentRoot = contentRoot,
+ BasePath = basePath,
+ RelativePath = relativePath,
+ AssetKind = assetKind,
+ AssetMode = assetMode,
+ AssetRole = assetRole,
+ AssetMergeSource = assetMergeSource,
+ RelatedAsset = relatedAsset,
+ AssetTraitName = assetTraitName,
+ AssetTraitValue = assetTraitValue,
+ Fingerprint = fingerprint,
+ Integrity = integrity,
+ CopyToOutputDirectory = copyToOutputDirectory,
+ CopyToPublishDirectory = copyToPublishDirectory,
+ OriginalItemSpec = originalItemSpec,
+ FileLength = fileLength,
+ LastWriteTime = lastWriteTime
+ };
result.ApplyDefaults();
@@ -549,30 +578,9 @@ public static string ComputeAssetRelativePath(ITaskItem asset, out string metada
return linkPath;
}
- metadataProperty = null;
- return asset.ItemSpec;
- }
-
- // Compares all fields in this order
- // Identity
- // SourceType
- // SourceId
- // ContentRoot
- // BasePath
- // RelativePath
- // AssetKind
- // AssetMode
- // AssetRole
- // AssetMergeSource
- // AssetMergeBehavior
- // RelatedAsset
- // AssetTraitName
- // AssetTraitValue
- // Fingerprint
- // Integrity
- // CopyToOutputDirectory
- // CopyToPublishDirectory
- // OriginalItemSpec
+ metadataProperty = null;
+ return asset.ItemSpec;
+ }
public int CompareTo(StaticWebAsset other)
{
@@ -582,11 +590,23 @@ public int CompareTo(StaticWebAsset other)
return result;
}
- result = string.Compare(SourceType, other.SourceType, StringComparison.Ordinal);
- if (result != 0)
- {
- return result;
- }
+ result = string.Compare(SourceType, other.SourceType, StringComparison.Ordinal);
+ if (result != 0)
+ {
+ return result;
+ }
+
+ result = FileLength.CompareTo(other.FileLength);
+ if (result != 0)
+ {
+ return result;
+ }
+
+ result = LastWriteTime.CompareTo(other.LastWriteTime);
+ if (result != 0)
+ {
+ return result;
+ }
result = string.Compare(SourceId, other.SourceId, StringComparison.Ordinal);
if (result != 0)
@@ -690,26 +710,28 @@ public int CompareTo(StaticWebAsset other)
public override bool Equals(object obj) => obj != null && Equals(obj as StaticWebAsset);
- public bool Equals(StaticWebAsset other) =>
- Identity == other.Identity &&
- SourceType == other.SourceType &&
- SourceId == other.SourceId &&
- ContentRoot == other.ContentRoot &&
- BasePath == other.BasePath &&
- RelativePath == other.RelativePath &&
- AssetKind == other.AssetKind &&
- AssetMode == other.AssetMode &&
- AssetRole == other.AssetRole &&
- AssetMergeSource == other.AssetMergeSource &&
- AssetMergeBehavior == other.AssetMergeBehavior &&
- RelatedAsset == other.RelatedAsset &&
- AssetTraitName == other.AssetTraitName &&
- AssetTraitValue == other.AssetTraitValue &&
- Fingerprint == other.Fingerprint &&
- Integrity == other.Integrity &&
- CopyToOutputDirectory == other.CopyToOutputDirectory &&
- CopyToPublishDirectory == other.CopyToPublishDirectory &&
- OriginalItemSpec == other.OriginalItemSpec;
+ public bool Equals(StaticWebAsset other) =>
+ Identity == other.Identity &&
+ SourceType == other.SourceType &&
+ FileLength == other.FileLength &&
+ LastWriteTime == other.LastWriteTime &&
+ SourceId == other.SourceId &&
+ ContentRoot == other.ContentRoot &&
+ BasePath == other.BasePath &&
+ RelativePath == other.RelativePath &&
+ AssetKind == other.AssetKind &&
+ AssetMode == other.AssetMode &&
+ AssetRole == other.AssetRole &&
+ AssetMergeSource == other.AssetMergeSource &&
+ AssetMergeBehavior == other.AssetMergeBehavior &&
+ RelatedAsset == other.RelatedAsset &&
+ AssetTraitName == other.AssetTraitName &&
+ AssetTraitValue == other.AssetTraitValue &&
+ Fingerprint == other.Fingerprint &&
+ Integrity == other.Integrity &&
+ CopyToOutputDirectory == other.CopyToOutputDirectory &&
+ CopyToPublishDirectory == other.CopyToPublishDirectory &&
+ OriginalItemSpec == other.OriginalItemSpec;
public static class AssetModes
{
@@ -833,73 +855,79 @@ public string ComputePathWithoutTokens(string pathWithTokens)
return pattern.ComputePatternLabel();
}
- public override string ToString() =>
- $"Identity: {Identity}, " +
- $"SourceType: {SourceType}, " +
- $"SourceId: {SourceId}, " +
- $"ContentRoot: {ContentRoot}, " +
- $"BasePath: {BasePath}, " +
- $"RelativePath: {RelativePath}, " +
- $"AssetKind: {AssetKind}, " +
- $"AssetMode: {AssetMode}, " +
- $"AssetRole: {AssetRole}, " +
- $"AssetRole: {AssetMergeSource}, " +
- $"AssetRole: {AssetMergeBehavior}, " +
- $"RelatedAsset: {RelatedAsset}, " +
- $"AssetTraitName: {AssetTraitName}, " +
- $"AssetTraitValue: {AssetTraitValue}, " +
- $"Fingerprint: {Fingerprint}, " +
- $"Integrity: {Integrity}, " +
- $"CopyToOutputDirectory: {CopyToOutputDirectory}, " +
- $"CopyToPublishDirectory: {CopyToPublishDirectory}, " +
- $"OriginalItemSpec: {OriginalItemSpec}";
+ public override string ToString() =>
+ $"Identity: {Identity}, " +
+ $"SourceType: {SourceType}, " +
+ $"SourceId: {SourceId}, " +
+ $"ContentRoot: {ContentRoot}, " +
+ $"BasePath: {BasePath}, " +
+ $"RelativePath: {RelativePath}, " +
+ $"AssetKind: {AssetKind}, " +
+ $"AssetMode: {AssetMode}, " +
+ $"AssetRole: {AssetRole}, " +
+ $"AssetRole: {AssetMergeSource}, " +
+ $"AssetRole: {AssetMergeBehavior}, " +
+ $"RelatedAsset: {RelatedAsset}, " +
+ $"AssetTraitName: {AssetTraitName}, " +
+ $"AssetTraitValue: {AssetTraitValue}, " +
+ $"Fingerprint: {Fingerprint}, " +
+ $"Integrity: {Integrity}, " +
+ $"FileLength: {FileLength}, " +
+ $"LastWriteTime: {LastWriteTime}, " +
+ $"CopyToOutputDirectory: {CopyToOutputDirectory}, " +
+ $"CopyToPublishDirectory: {CopyToPublishDirectory}, " +
+ $"OriginalItemSpec: {OriginalItemSpec}";
public override int GetHashCode()
{
#if NET6_0_OR_GREATER
- var hash = new HashCode();
- hash.Add(Identity);
- hash.Add(SourceType);
- hash.Add(SourceId);
- hash.Add(ContentRoot);
- hash.Add(BasePath);
- hash.Add(RelativePath);
- hash.Add(AssetKind);
- hash.Add(AssetMode);
- hash.Add(AssetRole);
- hash.Add(AssetMergeSource);
- hash.Add(AssetMergeBehavior);
- hash.Add(RelatedAsset);
- hash.Add(AssetTraitName);
- hash.Add(AssetTraitValue);
- hash.Add(Fingerprint);
- hash.Add(Integrity);
- hash.Add(CopyToOutputDirectory);
- hash.Add(CopyToPublishDirectory);
- hash.Add(OriginalItemSpec);
- return hash.ToHashCode();
+ var hash = new HashCode();
+ hash.Add(Identity);
+ hash.Add(SourceType);
+ hash.Add(FileLength);
+ hash.Add(LastWriteTime);
+ hash.Add(SourceId);
+ hash.Add(ContentRoot);
+ hash.Add(BasePath);
+ hash.Add(RelativePath);
+ hash.Add(AssetKind);
+ hash.Add(AssetMode);
+ hash.Add(AssetRole);
+ hash.Add(AssetMergeSource);
+ hash.Add(AssetMergeBehavior);
+ hash.Add(RelatedAsset);
+ hash.Add(AssetTraitName);
+ hash.Add(AssetTraitValue);
+ hash.Add(Fingerprint);
+ hash.Add(Integrity);
+ hash.Add(CopyToOutputDirectory);
+ hash.Add(CopyToPublishDirectory);
+ hash.Add(OriginalItemSpec);
+ return hash.ToHashCode();
#else
- int hashCode = 1447485498;
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Identity);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(SourceType);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(SourceId);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ContentRoot);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(BasePath);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(RelativePath);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetKind);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetMode);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetRole);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetMergeSource);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetMergeBehavior);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(RelatedAsset);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetTraitName);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetTraitValue);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Fingerprint);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Integrity);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(CopyToOutputDirectory);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(CopyToPublishDirectory);
- hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(OriginalItemSpec);
- return hashCode;
+ var hashCode = 1447485498;
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Identity);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(SourceType);
+ hashCode = hashCode * -1521134295 + FileLength.GetHashCode();
+ hashCode = hashCode * -1521134295 + LastWriteTime.GetHashCode();
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(SourceId);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ContentRoot);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(BasePath);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(RelativePath);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetKind);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetMode);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetRole);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetMergeSource);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetMergeBehavior);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(RelatedAsset);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetTraitName);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(AssetTraitValue);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Fingerprint);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Integrity);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(CopyToOutputDirectory);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(CopyToPublishDirectory);
+ hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(OriginalItemSpec);
+ return hashCode;
#endif
}
@@ -919,7 +947,7 @@ internal string EmbedTokens(string relativePath)
var pattern = StaticWebAssetPathPattern.Parse(relativePath, Identity);
var resolver = StaticWebAssetTokenResolver.Instance;
pattern.EmbedTokens(this, resolver);
- return pattern.RawPattern;
+ return pattern.RawPattern.ToString();
}
internal FileInfo ResolveFile() => ResolveFile(Identity, OriginalItemSpec);
diff --git a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs
index 3e9efc767183..fb7436ad32eb 100644
--- a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs
+++ b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetEndpoint.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Concurrent;
using System.Diagnostics;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
@@ -232,6 +233,23 @@ public int CompareTo(StaticWebAssetEndpoint other)
return 0;
}
+ internal static ITaskItem[] ToTaskItems(ConcurrentBag endpoints)
+ {
+ if (endpoints == null || endpoints.IsEmpty)
+ {
+ return [];
+ }
+
+ var endpointItems = new ITaskItem[endpoints.Count];
+ var i = 0;
+ foreach (var endpoint in endpoints)
+ {
+ endpointItems[i++] = endpoint.ToTaskItem();
+ }
+
+ return endpointItems;
+ }
+
private class RouteAndAssetEqualityComparer : IEqualityComparer
{
public bool Equals(StaticWebAssetEndpoint x, StaticWebAssetEndpoint y)
diff --git a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetPathPattern.cs b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetPathPattern.cs
index 6116de749f4b..66268fc9748c 100644
--- a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetPathPattern.cs
+++ b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetPathPattern.cs
@@ -5,15 +5,25 @@
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
-#if WASM_TASKS
[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
-internal class StaticWebAssetPathPattern : IEquatable
+[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0057:Use range operator", Justification = "Can't use range syntax in full framework")]
+#if WASM_TASKS
+internal sealed class StaticWebAssetPathPattern : IEquatable
#else
-[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
-public class StaticWebAssetPathPattern : IEquatable
+public sealed class StaticWebAssetPathPattern : IEquatable
#endif
{
- public StaticWebAssetPathPattern(string path) => RawPattern = path;
+ private const string PatternStart = "#[";
+ private const char PatternEnd = ']';
+ private const char PatternOptional = '?';
+ private const char PatternPreferred = '!';
+ private const char PatternValueSeparator = '=';
+ private const char PatternParameterStart = '{';
+ private const char PatternParameterEnd = '}';
+
+ public StaticWebAssetPathPattern(string path) : this(path.AsMemory()) { }
+
+ public StaticWebAssetPathPattern(ReadOnlyMemory rawPathMemory) => RawPattern = rawPathMemory;
public StaticWebAssetPathPattern(List segments)
{
@@ -21,7 +31,7 @@ public StaticWebAssetPathPattern(List segments)
Segments = segments;
}
- public string RawPattern { get; private set; }
+ public ReadOnlyMemory RawPattern { get; private set; }
public IList Segments { get; set; } = [];
@@ -53,17 +63,18 @@ public StaticWebAssetPathPattern(List segments)
// and other features. This is why we want to bake into the format itself the information that specifies under which paths the file will
// be available at runtime so that tasks/tools can operate independently and produce correct results.
// The current token we support is the 'fingerprint' token, which computes a web friendly version of the hash of the file suitable
- // to be embedded in other contexts.
+ // to be embedded in other contexts.
// We might include other tokens in the future, like `[{basepath}]` to give a file the ability to have its path be relative to the consuming
// project base path, etc.
- public static StaticWebAssetPathPattern Parse(string rawPath, string assetIdentity = null)
+ public static StaticWebAssetPathPattern Parse(ReadOnlyMemory rawPathMemory, string assetIdentity = null)
{
- var pattern = new StaticWebAssetPathPattern(rawPath);
- var nextToken = rawPath.IndexOf("#[", StringComparison.OrdinalIgnoreCase);
+ var pattern = new StaticWebAssetPathPattern(rawPathMemory);
+ var current = rawPathMemory;
+ var nextToken = MemoryExtensions.IndexOf(current.Span, PatternStart.AsSpan(), StringComparison.OrdinalIgnoreCase);
if (nextToken == -1)
{
var literalSegment = new StaticWebAssetPathSegment();
- literalSegment.Parts.Add(new StaticWebAssetSegmentPart { Name = rawPath, IsLiteral = true });
+ literalSegment.Parts.Add(new StaticWebAssetSegmentPart { Name = current, IsLiteral = true });
pattern.Segments.Add(literalSegment);
return pattern;
}
@@ -71,50 +82,58 @@ public static StaticWebAssetPathPattern Parse(string rawPath, string assetIdenti
if (nextToken > 0)
{
var literalSegment = new StaticWebAssetPathSegment();
- literalSegment.Parts.Add(new StaticWebAssetSegmentPart { Name = rawPath.Substring(0, nextToken), IsLiteral = true });
+ literalSegment.Parts.Add(new StaticWebAssetSegmentPart { Name = current.Slice(0, nextToken), IsLiteral = true });
pattern.Segments.Add(literalSegment);
}
+
while (nextToken != -1)
{
- var tokenEnd = rawPath.IndexOf(']', nextToken);
+ current = current.Slice(nextToken);
+ var tokenEnd = MemoryExtensions.IndexOf(current.Span, PatternEnd);
if (tokenEnd == -1)
{
if (assetIdentity != null)
{
// We don't have a closing token, this is likely an error, so throw
- throw new InvalidOperationException($"Invalid relative path '{rawPath}' for asset '{assetIdentity}'. Missing ']' token.");
+ throw new InvalidOperationException($"Invalid relative path '{rawPathMemory}' for asset '{assetIdentity}'. Missing ']' token.");
}
else
{
- throw new InvalidOperationException($"Invalid token expression '{rawPath}'. Missing ']' token.");
+ throw new InvalidOperationException($"Invalid token expression '{rawPathMemory}'. Missing ']' token.");
}
}
- var tokenExpression = rawPath.Substring(nextToken + 2, tokenEnd - nextToken - 2);
+ var tokenExpression = current.Slice(2, tokenEnd - 2);
var token = new StaticWebAssetPathSegment();
AddTokenSegmentParts(tokenExpression, token);
pattern.Segments.Add(token);
// Check if the segment is optional (ends with ? or !)
- if (tokenEnd < rawPath.Length - 1 && (rawPath[tokenEnd + 1] == '?' || rawPath[tokenEnd + 1] == '!'))
+ if (tokenEnd < current.Length - 1 &&
+ (current.Span[tokenEnd + 1] == PatternOptional || current.Span[tokenEnd + 1] == PatternPreferred))
{
token.IsOptional = true;
- if (rawPath[tokenEnd + 1] == '!')
+ if (current.Span[tokenEnd + 1] == PatternPreferred)
{
token.IsPreferred = true;
}
tokenEnd++;
}
- nextToken = rawPath.IndexOf("#[", tokenEnd, comparisonType: StringComparison.OrdinalIgnoreCase);
+ current = current.Slice(tokenEnd + 1);
+ nextToken = MemoryExtensions.IndexOf(current.Span, PatternStart.AsSpan(), StringComparison.OrdinalIgnoreCase);
- // Add a literal segment if there is more content after the token and before the next one
- if ((nextToken != -1 && nextToken > tokenEnd + 1) || (nextToken == -1 && tokenEnd < rawPath.Length - 1))
+ if (nextToken == -1 && current.Length > 0)
+ {
+ var literalSegment = new StaticWebAssetPathSegment();
+ literalSegment.Parts.Add(new StaticWebAssetSegmentPart { Name = current, IsLiteral = true });
+ pattern.Segments.Add(literalSegment);
+ }
+ else if (nextToken > 0)
{
- var literalEnd = nextToken == -1 ? rawPath.Length : nextToken;
var literalSegment = new StaticWebAssetPathSegment();
- literalSegment.Parts.Add(new StaticWebAssetSegmentPart { Name = rawPath.Substring(tokenEnd + 1, literalEnd - tokenEnd - 1), IsLiteral = true });
+ literalSegment.Parts.Add(new StaticWebAssetSegmentPart { Name = current.Slice(0, nextToken), IsLiteral = true });
pattern.Segments.Add(literalSegment);
}
}
@@ -122,6 +141,66 @@ public static StaticWebAssetPathPattern Parse(string rawPath, string assetIdenti
return pattern;
}
+ // Iterate over the token expression and add the parts to the token segment
+ // Some examples are '.{fingerprint}', '{fingerprint}.', '{fingerprint}{fingerprint}', {fingerprint}.{fingerprint}
+ // The '.' represents sample literal content.
+ // The value within the {} represents token variables.
+ private static void AddTokenSegmentParts(ReadOnlyMemory tokenExpression, StaticWebAssetPathSegment token)
+ {
+ var current = tokenExpression;
+ var nextToken = MemoryExtensions.IndexOf(current.Span, PatternParameterStart);
+ if (nextToken is not (-1) and > 0)
+ {
+ var literalPart = new StaticWebAssetSegmentPart { Name = current.Slice(0, nextToken), IsLiteral = true };
+ token.Parts.Add(literalPart);
+ }
+
+ while (nextToken != -1)
+ {
+ current = current.Slice(nextToken);
+ var tokenEnd = MemoryExtensions.IndexOf(current.Span, PatternParameterEnd);
+ if (tokenEnd == -1)
+ {
+ throw new InvalidOperationException($"Invalid token expression '{tokenExpression}'. Missing '}}' token.");
+ }
+
+ var embeddedValue = MemoryExtensions.IndexOf(current.Span, PatternValueSeparator);
+ if (embeddedValue != -1)
+ {
+ var tokenPart = new StaticWebAssetSegmentPart
+ {
+ Name = current.Slice(1, embeddedValue - 1),
+ IsLiteral = false,
+ Value = current.Slice(embeddedValue + 1, tokenEnd - embeddedValue - 1)
+ };
+ token.Parts.Add(tokenPart);
+ }
+ else
+ {
+ var tokenPart = new StaticWebAssetSegmentPart { Name = current.Slice(1, tokenEnd - 1), IsLiteral = false };
+ token.Parts.Add(tokenPart);
+ }
+
+ current = current.Slice(tokenEnd + 1);
+ nextToken = MemoryExtensions.IndexOf(current.Span, PatternParameterStart);
+ if (nextToken == -1 && current.Length > 0)
+ {
+ var literalPart = new StaticWebAssetSegmentPart { Name = current, IsLiteral = true };
+ token.Parts.Add(literalPart);
+ }
+ else if (nextToken > 0)
+ {
+ var literalPart = new StaticWebAssetSegmentPart { Name = current.Slice(0, nextToken), IsLiteral = true };
+ token.Parts.Add(literalPart);
+ }
+ }
+ }
+
+ public static StaticWebAssetPathPattern Parse(string rawPath, string assetIdentity = null)
+ {
+ return Parse(rawPath.AsMemory(), assetIdentity);
+ }
+
// Replaces the tokens in the pattern with values provided in the expression, by the asset, or global resolvers.
// Embedded values allow tasks to define the values that should be used when defining endpoints, while preserving the
// original token information (for example, if its optional or if it should be preferred).
@@ -159,14 +238,15 @@ public static StaticWebAssetPathPattern Parse(string rawPath, string assetIdenti
var missingValue = "";
foreach (var tokenName in tokenNames)
{
- if (!tokens.TryGetValue(staticWebAsset, tokenName, out var tokenValue) || string.IsNullOrEmpty(tokenValue))
+ var tokenNameString = tokenName.ToString();
+ if (!tokens.TryGetValue(staticWebAsset, tokenNameString, out var tokenValue) || string.IsNullOrEmpty(tokenValue))
{
foundAllValues = false;
- missingValue = tokenName;
+ missingValue = tokenNameString;
break;
}
- dictionary[tokenName] = tokenValue;
+ dictionary[tokenNameString] = tokenValue;
}
if (!foundAllValues && !segment.IsOptional)
@@ -188,15 +268,15 @@ public static StaticWebAssetPathPattern Parse(string rawPath, string assetIdenti
{
result.Append(part.Name);
}
- else if (!string.IsNullOrEmpty(part.Value))
+ else if (!part.Value.IsEmpty)
{
// Token was embedded, so add it to the dictionary.
- dictionary[part.Name] = part.Value;
+ dictionary[part.Name.ToString()] = part.Value.ToString();
result.Append(part.Value);
}
else
{
- result.Append(dictionary[part.Name]);
+ result.Append(dictionary[part.Name.ToString()]);
}
}
}
@@ -214,14 +294,13 @@ public static StaticWebAssetPathPattern Parse(string rawPath, string assetIdenti
public IEnumerable ExpandPatternExpression()
{
// We are going to analyze each segment and produce the following:
- // - For literals, we just concatenate
+ // - For literals, we just concatenate
// - For parameter expressions without '?' we return the parameter expression.
// - For parameter expressions with '?' we return
// For example:
// - asset.css produces a single pattern (asset.css).
// - other#[.{fingerprint}].js produces a single pattern asset#[.{fingerprint}].js
// - last#[.{fingerprint}]?.txt produces two patterns last#[.{fingerprint}]?.txt and last.txt
-
var hasOptionalSegments = false;
foreach (var segment in Segments)
{
@@ -336,14 +415,14 @@ internal void EmbedTokens(StaticWebAsset staticWebAsset, StaticWebAssetTokenReso
continue;
}
- if (!resolver.TryGetValue(staticWebAsset, tokenName, out var tokenValue) || string.IsNullOrEmpty(tokenValue))
+ if (!resolver.TryGetValue(staticWebAsset, tokenName.ToString(), out var tokenValue) || string.IsNullOrEmpty(tokenValue))
{
continue;
}
- if (string.Equals(part.Name, tokenName))
+ if (part.Name.Span.SequenceEqual(tokenName.Span))
{
- part.Value = tokenValue;
+ part.Value = tokenValue.AsMemory();
}
}
}
@@ -351,54 +430,7 @@ internal void EmbedTokens(StaticWebAsset staticWebAsset, StaticWebAssetTokenReso
RawPattern = GetRawPattern(Segments);
}
- // Iterate over the token expression and add the parts to the token segment
- // Some examples are '.{fingerprint}', '{fingerprint}.', '{fingerprint}{fingerprint}', {fingerprint}.{fingerprint}
- // The '.' represents sample literal content.
- // The value within the {} represents token variables.
- private static void AddTokenSegmentParts(string tokenExpression, StaticWebAssetPathSegment token)
- {
- var nextToken = tokenExpression.IndexOf('{');
- if (nextToken is not (-1) and > 0)
- {
- var literalPart = new StaticWebAssetSegmentPart { Name = tokenExpression.Substring(0, nextToken), IsLiteral = true };
- token.Parts.Add(literalPart);
- }
- while (nextToken != -1)
- {
- var tokenEnd = tokenExpression.IndexOf('}', nextToken);
- if (tokenEnd == -1)
- {
- throw new InvalidOperationException($"Invalid token expression '{tokenExpression}'. Missing '}}' token.");
- }
-
- var embeddedValue = tokenExpression.IndexOf('=', nextToken);
- if (embeddedValue != -1)
- {
- var tokenPart = new StaticWebAssetSegmentPart
- {
- Name = tokenExpression.Substring(nextToken + 1, embeddedValue - nextToken - 1),
- IsLiteral = false,
- Value = tokenExpression.Substring(embeddedValue + 1, tokenEnd - embeddedValue - 1)
- };
- token.Parts.Add(tokenPart);
- }
- else
- {
- var tokenPart = new StaticWebAssetSegmentPart { Name = tokenExpression.Substring(nextToken + 1, tokenEnd - nextToken - 1), IsLiteral = false };
- token.Parts.Add(tokenPart);
- }
-
- nextToken = tokenExpression.IndexOf('{', tokenEnd);
- if ((nextToken != -1 && nextToken > tokenEnd + 1) || (nextToken == -1 && tokenEnd < tokenExpression.Length - 1))
- {
- var literalEnd = nextToken == -1 ? tokenExpression.Length : nextToken;
- var literalPart = new StaticWebAssetSegmentPart { Name = tokenExpression.Substring(tokenEnd + 1, literalEnd - tokenEnd - 1), IsLiteral = true };
- token.Parts.Add(literalPart);
- }
- }
- }
-
- private static string GetRawPattern(IList segments)
+ private static ReadOnlyMemory GetRawPattern(IList segments)
{
var stringBuilder = new StringBuilder();
for (var i = 0; i < segments.Count; i++)
@@ -407,42 +439,45 @@ private static string GetRawPattern(IList segments)
var isLiteral = IsLiteralSegment(segment);
if (!isLiteral)
{
- stringBuilder.Append("#[");
+ stringBuilder.Append(PatternStart);
}
for (var j = 0; j < segment.Parts.Count; j++)
{
var part = segment.Parts[j];
- stringBuilder.Append(part.IsLiteral ? part.Name : $$"""{{{(!string.IsNullOrEmpty(part.Value) ? $"""{part.Name}={part.Value}""" : part.Name)}}}""");
+ stringBuilder.Append(part.IsLiteral ? part.Name : $$"""{{{(!part.Value.IsEmpty ? $"""{part.Name}{PatternValueSeparator}{part.Value}""" : part.Name)}}}""");
}
if (!isLiteral)
{
- stringBuilder.Append(']');
+ stringBuilder.Append(PatternEnd);
if (segment.IsOptional)
{
if (segment.IsPreferred)
{
- stringBuilder.Append('!');
+ stringBuilder.Append(PatternPreferred);
}
else
{
- stringBuilder.Append('?');
+ stringBuilder.Append(PatternOptional);
}
}
}
}
- return stringBuilder.ToString();
+ return stringBuilder.ToString().AsMemory();
}
public override bool Equals(object obj) => Equals(obj as StaticWebAssetPathPattern);
- public bool Equals(StaticWebAssetPathPattern other) => other is not null && RawPattern == other.RawPattern && Segments.SequenceEqual(other.Segments);
+ public bool Equals(StaticWebAssetPathPattern other) =>
+ other is not null &&
+ MemoryExtensions.Equals(RawPattern.Span, other.RawPattern.Span, StringComparison.Ordinal) &&
+ Segments.SequenceEqual(other.Segments);
#if NET47_OR_GREATER
public override int GetHashCode()
{
var hashCode = 1219904980;
- hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(RawPattern);
+ hashCode = (hashCode * -1521134295) + EqualityComparer>.Default.GetHashCode(RawPattern);
hashCode = (hashCode * -1521134295) + EqualityComparer>.Default.GetHashCode(Segments);
return hashCode;
}
@@ -460,13 +495,12 @@ public override int GetHashCode()
#endif
public static bool operator ==(StaticWebAssetPathPattern left, StaticWebAssetPathPattern right) => EqualityComparer.Default.Equals(left, right);
+
public static bool operator !=(StaticWebAssetPathPattern left, StaticWebAssetPathPattern right) => !(left == right);
private string GetDebuggerDisplay() => string.Concat(Segments.Select(s => s.GetDebuggerDisplay()));
private static bool IsLiteralSegment(StaticWebAssetPathSegment segment) => segment.Parts.Count == 1 && segment.Parts[0].IsLiteral;
- internal static string PathWithoutTokens(string path)
- {
- return Parse(path).ComputePatternLabel();
- }
+
+ internal static string PathWithoutTokens(string path) => Parse(path).ComputePatternLabel();
}
diff --git a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetPathSegment.cs b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetPathSegment.cs
index a0075ebf765c..d41e4d685a0d 100644
--- a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetPathSegment.cs
+++ b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetPathSegment.cs
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
@@ -11,6 +11,7 @@ public class StaticWebAssetPathSegment : IEquatable
public IList Parts { get; set; } = [];
public bool IsOptional { get; set; }
+
public bool IsPreferred { get; set; }
public override bool Equals(object obj) => Equals(obj as StaticWebAssetPathSegment);
@@ -45,18 +46,18 @@ public override int GetHashCode()
internal string GetDebuggerDisplay()
{
- return Parts != null && Parts.Count == 1 && Parts[0].IsLiteral ? Parts[0].Name : ComputeParameterExpression();
+ return Parts != null && Parts.Count == 1 && Parts[0].IsLiteral ? Parts[0].Name.ToString() : ComputeParameterExpression();
string ComputeParameterExpression() =>
- string.Concat(Parts.Select(p => p.IsLiteral ? p.Name : $"{{{p.Name}}}").Prepend("#[").Append($"]{(IsOptional ? (IsPreferred ? "!" : "?") : "")}"));
+ string.Concat(Parts.Select(p => p.IsLiteral ? p.Name.ToString() : $"{{{p.Name}}}").Prepend("#[").Append($"]{(IsOptional ? (IsPreferred ? "!" : "?") : "")}"));
}
- internal ICollection GetTokenNames()
+ internal ICollection> GetTokenNames()
{
- var result = new HashSet();
+ var result = new HashSet>();
foreach (var part in Parts)
{
- if (!part.IsLiteral && string.IsNullOrEmpty(part.Value))
+ if (!part.IsLiteral && part.Name.Length > 0)
{
result.Add(part.Name);
}
diff --git a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetSegmentPart.cs b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetSegmentPart.cs
index 4a7acc06d011..44990bcce9b3 100644
--- a/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetSegmentPart.cs
+++ b/src/StaticWebAssetsSdk/Tasks/Data/StaticWebAssetSegmentPart.cs
@@ -1,33 +1,53 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics;
+
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
+[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
public class StaticWebAssetSegmentPart : IEquatable
{
- public string Name { get; set; }
+ public ReadOnlyMemory Name { get; set; }
- public string Value { get; set; }
+ public ReadOnlyMemory Value { get; set; }
public bool IsLiteral { get; set; }
public override bool Equals(object obj) => Equals(obj as StaticWebAssetSegmentPart);
- public bool Equals(StaticWebAssetSegmentPart other) => other is not null && Name == other.Name && Value == other.Value && IsLiteral == other.IsLiteral;
+ public bool Equals(StaticWebAssetSegmentPart other) => other is not null &&
+ IsLiteral == other.IsLiteral &&
+ Name.Span.SequenceEqual(other.Name.Span) &&
+ Value.Span.SequenceEqual(other.Value.Span);
#if NET47_OR_GREATER
public override int GetHashCode()
{
var hashCode = -62096114;
- hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Name);
- hashCode = (hashCode * -1521134295) + EqualityComparer.Default.GetHashCode(Value);
+ hashCode = (hashCode * -1521134295) + GetSpanHashCode(Name);
+ hashCode = (hashCode * -1521134295) + GetSpanHashCode(Value);
hashCode = (hashCode * -1521134295) + IsLiteral.GetHashCode();
return hashCode;
}
+
+ private int GetSpanHashCode(ReadOnlyMemory memory)
+ {
+ var hashCode = -62096114;
+ var span = memory.Span;
+ for ( var i = 0; i < span.Length; i++)
+ {
+ hashCode = (hashCode * -1521134295) + span[i].GetHashCode();
+ }
+
+ return hashCode;
+ }
#else
public override int GetHashCode() => HashCode.Combine(Name, Value, IsLiteral);
#endif
public static bool operator ==(StaticWebAssetSegmentPart left, StaticWebAssetSegmentPart right) => EqualityComparer.Default.Equals(left, right);
public static bool operator !=(StaticWebAssetSegmentPart left, StaticWebAssetSegmentPart right) => !(left == right);
+
+ private string GetDebuggerDisplay() => IsLiteral ? Value.ToString() : $"{{{Name}}}";
}
diff --git a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssetEndpoints.cs b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssetEndpoints.cs
index 034a56228790..eb76d064d944 100644
--- a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssetEndpoints.cs
+++ b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssetEndpoints.cs
@@ -2,8 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
using Microsoft.NET.Sdk.StaticWebAssets.Tasks;
-using System.Globalization;
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks
{
@@ -12,102 +12,104 @@ public class DefineStaticWebAssetEndpoints : Task
[Required]
public ITaskItem[] CandidateAssets { get; set; }
- [Required]
public ITaskItem[] ExistingEndpoints { get; set; }
[Required]
public ITaskItem[] ContentTypeMappings { get; set; }
- public ITaskItem[] AssetFileDetails { get; set; }
-
[Output]
public ITaskItem[] Endpoints { get; set; }
- public Func TestLengthResolver;
- public Func TestLastWriteResolver;
-
- private Dictionary _assetFileDetails;
-
public override bool Execute()
{
- if (AssetFileDetails != null)
- {
- _assetFileDetails = new(AssetFileDetails.Length, OSPath.PathComparer);
- for (int i = 0; i < AssetFileDetails.Length; i++)
- {
- var item = AssetFileDetails[i];
- _assetFileDetails[item.ItemSpec] = item;
- }
- }
-
- var staticWebAssets = CandidateAssets.Select(StaticWebAsset.FromTaskItem).ToDictionary(a => a.Identity);
- var existingEndpoints = StaticWebAssetEndpoint.FromItemGroup(ExistingEndpoints);
- var existingEndpointsByAssetFile = existingEndpoints
- .GroupBy(e => e.AssetFile, OSPath.PathComparer)
- .ToDictionary(g => g.Key, g => new HashSet(g, StaticWebAssetEndpoint.RouteAndAssetComparer));
-
- var assetsToRemove = new List();
- foreach (var kvp in existingEndpointsByAssetFile)
- {
- var asset = kvp.Key;
- var set = kvp.Value;
- if (!staticWebAssets.ContainsKey(asset))
- {
- assetsToRemove.Remove(asset);
- }
- }
- foreach (var asset in assetsToRemove)
- {
- Log.LogMessage(MessageImportance.Low, $"Removing endpoints for asset '{asset}' because it no longer exists.");
- existingEndpointsByAssetFile.Remove(asset);
- }
-
+ var existingEndpointsByAssetFile = CreateEndpointsByAssetFile();
var contentTypeMappings = ContentTypeMappings.Select(ContentTypeMapping.FromTaskItem).OrderByDescending(m => m.Priority).ToArray();
var contentTypeProvider = new ContentTypeProvider(contentTypeMappings);
var endpoints = new List();
- foreach (var kvp in staticWebAssets)
- {
- var asset = kvp.Value;
+ Parallel.For(
+ 0,
+ CandidateAssets.Length,
+ () => new ParallelWorker(
+ endpoints,
+ new List(),
+ CandidateAssets,
+ existingEndpointsByAssetFile,
+ Log,
+ contentTypeProvider),
+ static (i, loop, state) => state.Process(i, loop),
+ static worker => worker.Finally());
+
+ Endpoints = StaticWebAssetEndpoint.ToTaskItems(endpoints);
+
+ return !Log.HasLoggedErrors;
+ }
- // StaticWebAssets has this behavior where the base path for an asset only gets applied if the asset comes from a
- // package or a referenced project and ignored if it comes from the current project.
- // When we define the endpoint, we apply the path to the asset as if it was coming from the current project.
- // If the endpoint is then passed to a referencing project or packaged into a nuget package, the path will be
- // adjusted at that time.
- var assetEndpoints = CreateEndpoints(asset, contentTypeProvider);
+ private Dictionary> CreateEndpointsByAssetFile()
+ {
+ if (ExistingEndpoints != null && ExistingEndpoints.Length > 0)
+ {
+ Dictionary> existingEndpointsByAssetFile = new(OSPath.PathComparer);
+ var assets = new HashSet(CandidateAssets.Length, OSPath.PathComparer);
+ foreach (var asset in CandidateAssets)
+ {
+ assets.Add(asset.ItemSpec);
+ }
- foreach (var endpoint in assetEndpoints)
+ for (var i = 0; i < ExistingEndpoints.Length; i++)
{
- // Check if the endpoint we are about to define already exists. This can happen during publish as assets defined
- // during the build will have already defined endpoints and we only want to add new ones.
- if (existingEndpointsByAssetFile.TryGetValue(asset.Identity, out var set) &&
- set.TryGetValue(endpoint, out var existingEndpoint))
+ var endpointCandidate = ExistingEndpoints[i];
+ var assetFile = endpointCandidate.GetMetadata(nameof(StaticWebAssetEndpoint.AssetFile));
+ if (!assets.Contains(assetFile))
{
- Log.LogMessage(MessageImportance.Low, $"Skipping asset {asset.Identity} because an endpoint for it already exists at {existingEndpoint.Route}.");
+ Log.LogMessage(MessageImportance.Low, $"Removing endpoints for asset '{assetFile}' because it no longer exists.");
continue;
}
- Log.LogMessage(MessageImportance.Low, $"Adding endpoint {endpoint.Route} for asset {asset.Identity}.");
- endpoints.Add(endpoint);
+ if (!existingEndpointsByAssetFile.TryGetValue(assetFile, out var set))
+ {
+ set = new HashSet(OSPath.PathComparer);
+ existingEndpointsByAssetFile[assetFile] = set;
+ }
+
+ // Add the route
+ set.Add(endpointCandidate.ItemSpec);
}
- }
- Endpoints = StaticWebAssetEndpoint.ToTaskItems(endpoints);
+ return existingEndpointsByAssetFile;
+ }
- return !Log.HasLoggedErrors;
+ return null;
}
- private List CreateEndpoints(StaticWebAsset asset, ContentTypeProvider contentTypeMappings)
+ private readonly struct ParallelWorker(
+ List collectedEndpoints,
+ List currentEndpoints,
+ ITaskItem[] candidateAssets,
+ Dictionary> existingEndpointsByAssetFile,
+ TaskLoggingHelper log,
+ ContentTypeProvider contentTypeProvider)
{
- var routes = asset.ComputeRoutes();
- var (length, lastModified) = ResolveDetails(asset);
- var result = new List();
- foreach (var (label, route, values) in routes)
+ public List CollectedEndpoints { get; } = collectedEndpoints;
+ public List CurrentEndpoints { get; } = currentEndpoints;
+ public ITaskItem[] CandidateAssets { get; } = candidateAssets;
+ public Dictionary> ExistingEndpointsByAssetFile { get; } = existingEndpointsByAssetFile;
+ public TaskLoggingHelper Log { get; } = log;
+ public ContentTypeProvider ContentTypeProvider { get; } = contentTypeProvider;
+
+ private List CreateEndpoints(
+ List routes,
+ StaticWebAsset asset,
+ string length,
+ string lastModified,
+ StaticWebAssetGlobMatcher.MatchContext matchContext)
{
- var (mimeType, cacheSetting) = ResolveContentType(asset, contentTypeMappings);
- List headers = [
- new()
+ var result = new List();
+ foreach (var (label, route, values) in routes)
+ {
+ var (mimeType, cacheSetting) = ResolveContentType(asset, ContentTypeProvider, matchContext, Log);
+ List headers = [
+ new()
{
Name = "Accept-Ranges",
Value = "bytes"
@@ -130,134 +132,117 @@ private List CreateEndpoints(StaticWebAsset asset, Conte
new()
{
Name = "Last-Modified",
- Value = lastModified
+ Value = lastModified,
},
];
- if (values.ContainsKey("fingerprint"))
- {
- // max-age=31536000 is one year in seconds. immutable means that the asset will never change.
- // max-age is for browsers that do not support immutable.
- headers.Add(new() { Name = "Cache-Control", Value = "max-age=31536000, immutable" });
- }
- else
- {
- // Force revalidation on non-fingerprinted assets. We can be more granular here and have rules based on the content type.
- // These values can later be changed at runtime by modifying the endpoint. For example, it might be safer to cache images
- // for a longer period of time than scripts or stylesheets.
- headers.Add(new() { Name = "Cache-Control", Value = !string.IsNullOrEmpty(cacheSetting) ? cacheSetting : "no-cache" });
- }
-
- var properties = values.Select(v => new StaticWebAssetEndpointProperty { Name = v.Key, Value = v.Value });
- if (values.Count > 0)
- {
- // If an endpoint has values from its route replaced, we add a label to the endpoint so that it can be easily identified.
- // The combination of label and list of values should be unique.
- // In this way, we can identify an endpoint resource.fingerprint.ext by its label (for example resource.ext) and its values
- // (fingerprint).
- properties = properties.Append(new StaticWebAssetEndpointProperty { Name = "label", Value = label });
- }
+ if (values.ContainsKey("fingerprint"))
+ {
+ // max-age=31536000 is one year in seconds. immutable means that the asset will never change.
+ // max-age is for browsers that do not support immutable.
+ headers.Add(new() { Name = "Cache-Control", Value = "max-age=31536000, immutable" });
+ }
+ else
+ {
+ // Force revalidation on non-fingerprinted assets. We can be more granular here and have rules based on the content type.
+ // These values can later be changed at runtime by modifying the endpoint. For example, it might be safer to cache images
+ // for a longer period of time than scripts or stylesheets.
+ headers.Add(new() { Name = "Cache-Control", Value = !string.IsNullOrEmpty(cacheSetting) ? cacheSetting : "no-cache" });
+ }
- // We append the integrity in the format expected by the browser so that it can be opaque to the runtime.
- // If in the future we change it to sha384 or sha512, the runtime will not need to be updated.
- properties = properties.Append(new StaticWebAssetEndpointProperty { Name = "integrity", Value = $"sha256-{asset.Integrity}" });
+ var properties = values.Select(v => new StaticWebAssetEndpointProperty { Name = v.Key, Value = v.Value });
+ if (values.Count > 0)
+ {
+ // If an endpoint has values from its route replaced, we add a label to the endpoint so that it can be easily identified.
+ // The combination of label and list of values should be unique.
+ // In this way, we can identify an endpoint resource.fingerprint.ext by its label (for example resource.ext) and its values
+ // (fingerprint).
+ properties = properties.Append(new StaticWebAssetEndpointProperty { Name = "label", Value = label });
+ }
- var finalRoute = asset.IsProject() || asset.IsPackage() ? StaticWebAsset.Normalize(Path.Combine(asset.BasePath, route)) : route;
+ // We append the integrity in the format expected by the browser so that it can be opaque to the runtime.
+ // If in the future we change it to sha384 or sha512, the runtime will not need to be updated.
+ properties = properties.Append(new StaticWebAssetEndpointProperty { Name = "integrity", Value = $"sha256-{asset.Integrity}" });
- var endpoint = new StaticWebAssetEndpoint()
- {
- Route = finalRoute,
- AssetFile = asset.Identity,
- EndpointProperties = [.. properties],
- ResponseHeaders = [.. headers]
- };
- result.Add(endpoint);
- }
+ var finalRoute = asset.IsProject() || asset.IsPackage() ? StaticWebAsset.Normalize(Path.Combine(asset.BasePath, route)) : route;
- return result;
- }
+ var endpoint = new StaticWebAssetEndpoint()
+ {
+ Route = finalRoute,
+ AssetFile = asset.Identity,
+ EndpointProperties = [.. properties],
+ ResponseHeaders = [.. headers]
+ };
+ result.Add(endpoint);
+ }
- // Last-Modified: , :: GMT
- // Directives
- //
- // One of "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", or "Sun" (case-sensitive).
- //
- //
- // 2 digit day number, e.g. "04" or "23".
- //
- //
- // One of "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" (case sensitive).
- //
- //
- // 4 digit year number, e.g. "1990" or "2016".
- //
- //
- // 2 digit hour number, e.g. "09" or "23".
- //
- //
- // 2 digit minute number, e.g. "04" or "59".
- //
- //
- // 2 digit second number, e.g. "04" or "59".
- //
- // GMT
- // Greenwich Mean Time.HTTP dates are always expressed in GMT, never in local time.
- private (string length, string lastModified) ResolveDetails(StaticWebAsset asset)
- {
- if (_assetFileDetails != null && _assetFileDetails.TryGetValue(asset.Identity, out var details))
- {
- return (length: details.GetMetadata("FileLength"), lastModified: details.GetMetadata("LastWriteTimeUtc"));
- }
- else if (_assetFileDetails != null && _assetFileDetails.TryGetValue(asset.OriginalItemSpec, out var originalDetails))
- {
- return (length: originalDetails.GetMetadata("FileLength"), lastModified: originalDetails.GetMetadata("LastWriteTimeUtc"));
- }
- else if (TestLastWriteResolver != null || TestLengthResolver != null)
- {
- return (length: GetTestFileLength(asset), lastModified: GetTestFileLastModified(asset));
+ return result;
}
- else
+
+ private static (string mimeType, string cache) ResolveContentType(StaticWebAsset asset, ContentTypeProvider contentTypeProvider, StaticWebAssetGlobMatcher.MatchContext matchContext, TaskLoggingHelper log)
{
- Log.LogMessage(MessageImportance.High, $"No details found for {asset.Identity}. Using file system to resolve details.");
- var fileInfo = StaticWebAsset.ResolveFile(asset.Identity, asset.OriginalItemSpec);
- var length = fileInfo.Length.ToString(CultureInfo.InvariantCulture);
- var lastModified = fileInfo.LastWriteTimeUtc.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture);
- return (length, lastModified);
- }
- }
+ var relativePath = asset.ComputePathWithoutTokens(asset.RelativePath);
+ matchContext.SetPathAndReinitialize(relativePath);
- // Only used for testing
- private string GetTestFileLastModified(StaticWebAsset asset)
- {
- var lastWrite = TestLastWriteResolver != null ? TestLastWriteResolver(asset.Identity) : asset.ResolveFile().LastWriteTimeUtc;
- return lastWrite.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture);
- }
+ var mapping = contentTypeProvider.ResolveContentTypeMapping(matchContext, log);
- // Only used for testing
- private string GetTestFileLength(StaticWebAsset asset)
- {
- if (TestLengthResolver != null)
- {
- return TestLengthResolver(asset.Identity).ToString(CultureInfo.InvariantCulture);
- }
+ if (mapping.MimeType != null)
+ {
+ return (mapping.MimeType, mapping.Cache);
+ }
- var fileInfo = asset.ResolveFile();
- return fileInfo.Length.ToString(CultureInfo.InvariantCulture);
- }
+ log.LogMessage(MessageImportance.Low, $"No match for {relativePath}. Using default content type 'application/octet-stream'");
- private (string mimeType, string cache) ResolveContentType(StaticWebAsset asset, ContentTypeProvider contentTypeProvider)
- {
- var relativePath = asset.ComputePathWithoutTokens(asset.RelativePath);
- var mapping = contentTypeProvider.ResolveContentTypeMapping(relativePath, Log);
+ return ("application/octet-stream", null);
+ }
- if (mapping.MimeType != null)
+ internal void Finally()
{
- return (mapping.MimeType, mapping.Cache);
+ lock (CollectedEndpoints)
+ {
+ CollectedEndpoints.AddRange(CurrentEndpoints);
+ }
}
- Log.LogMessage(MessageImportance.Low, $"No match for {relativePath}. Using default content type 'application/octet-stream'");
+ internal ParallelWorker Process(int i, ParallelLoopState _)
+ {
+ var asset = StaticWebAsset.FromTaskItem(CandidateAssets[i]);
+ var routes = asset.ComputeRoutes().ToList();
+ // We extract these from the metadata because we avoid the conversion to their typed version and then back to string.
+ var length = CandidateAssets[i].GetMetadata(nameof(StaticWebAsset.FileLength));
+ var lastWriteTime = CandidateAssets[i].GetMetadata(nameof(StaticWebAsset.LastWriteTime));
+ var matchContext = StaticWebAssetGlobMatcher.CreateMatchContext();
+
+ if (ExistingEndpointsByAssetFile != null && ExistingEndpointsByAssetFile.TryGetValue(asset.Identity, out var set))
+ {
+ for (var j = routes.Count - 1; j >= 0; j--)
+ {
+ var (_, route, _) = routes[j];
+ // StaticWebAssets has this behavior where the base path for an asset only gets applied if the asset comes from a
+ // package or a referenced project and ignored if it comes from the current project.
+ // When we define the endpoint, we apply the path to the asset as if it was coming from the current project.
+ // If the endpoint is then passed to a referencing project or packaged into a nuget package, the path will be
+ // adjusted at that time.
+ var finalRoute = asset.IsProject() || asset.IsPackage() ? StaticWebAsset.Normalize(Path.Combine(asset.BasePath, route)) : route;
+
+ // Check if the endpoint we are about to define already exists. This can happen during publish as assets defined
+ // during the build will have already defined endpoints and we only want to add new ones.
+ if (set.Contains(finalRoute))
+ {
+ Log.LogMessage(MessageImportance.Low, $"Skipping asset {asset.Identity} because an endpoint for it already exists at {route}.");
+ routes.RemoveAt(j);
+ }
+ }
+ }
- return ("application/octet-stream", null);
+ foreach (var endpoint in CreateEndpoints(routes, asset, length, lastWriteTime, matchContext))
+ {
+ Log.LogMessage(MessageImportance.Low, $"Adding endpoint {endpoint.Route} for asset {asset.Identity}.");
+ CurrentEndpoints.Add(endpoint);
+ }
+
+ return this;
+ }
}
}
}
diff --git a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs
new file mode 100644
index 000000000000..e1a24954d29a
--- /dev/null
+++ b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.Cache.cs
@@ -0,0 +1,270 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using static Microsoft.AspNetCore.StaticWebAssets.Tasks.FingerprintPatternMatcher;
+
+namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
+
+public partial class DefineStaticWebAssets : Task
+{
+ private DefineStaticWebAssetsCache GetOrCreateAssetsCache()
+ {
+ var assetsCache = DefineStaticWebAssetsCache.ReadOrCreateCache(Log, CacheManifestPath);
+ if (CacheManifestPath == null)
+ {
+ assetsCache.NoCache(CandidateAssets);
+ return assetsCache;
+ }
+
+ var memoryStream = new MemoryStream();
+#if NET9_0_OR_GREATER
+ Span properties = [
+#else
+ var properties = new string[] {
+#endif
+ SourceId, SourceType, BasePath, ContentRoot, RelativePathPattern, RelativePathFilter,
+ AssetKind, AssetMode, AssetRole, AssetMergeSource, AssetMergeBehavior, RelatedAsset,
+ AssetTraitName, AssetTraitValue, CopyToOutputDirectory, CopyToPublishDirectory,
+ FingerprintCandidates.ToString()
+#if NET9_0_OR_GREATER
+ ];
+#else
+ };
+#endif
+ var propertiesHash = HashingUtils.ComputeHash(memoryStream, properties);
+
+ var patternMetadata = new[] { nameof(FingerprintPattern.Pattern), nameof(FingerprintPattern.Expression) };
+ var fingerprintPatternsHash = HashingUtils.ComputeHash(memoryStream, FingerprintPatterns ?? [], patternMetadata);
+
+ var propertyOverridesHash = HashingUtils.ComputeHash(memoryStream, PropertyOverrides, nameof(ITaskItem.GetMetadata));
+
+#if NET9_0_OR_GREATER
+ Span candidateAssetMetadata = [
+#else
+ var candidateAssetMetadata = new[] {
+#endif
+ "FullPath", "RelativePath", "TargetPath", "Link", "ModifiedTime", nameof(StaticWebAsset.SourceId),
+ nameof(StaticWebAsset.SourceType), nameof(StaticWebAsset.BasePath), nameof(StaticWebAsset.ContentRoot),
+ nameof(StaticWebAsset.AssetKind), nameof(StaticWebAsset.AssetMode), nameof(StaticWebAsset.AssetRole),
+ nameof(StaticWebAsset.AssetMergeBehavior), nameof(StaticWebAsset.AssetMergeSource), nameof(StaticWebAsset.RelatedAsset),
+ nameof(StaticWebAsset.AssetTraitName), nameof(StaticWebAsset.AssetTraitValue), nameof(StaticWebAsset.Fingerprint),
+ nameof(StaticWebAsset.Integrity), nameof(StaticWebAsset.CopyToOutputDirectory), nameof(StaticWebAsset.CopyToPublishDirectory),
+ nameof(StaticWebAsset.OriginalItemSpec)
+#if NET9_0_OR_GREATER
+ ];
+#else
+ };
+#endif
+ var inputHashes = HashingUtils.ComputeHashLookup(memoryStream, CandidateAssets ?? [], candidateAssetMetadata);
+
+ assetsCache.Update(propertiesHash, fingerprintPatternsHash, propertyOverridesHash, inputHashes);
+
+ return assetsCache;
+ }
+
+ internal class DefineStaticWebAssetsCache
+ {
+ private readonly List _assets = [];
+ private readonly List _copyCandidates = [];
+ private string _manifestPath;
+ private IDictionary _inputByHash;
+ private ITaskItem[] _noCacheCandidates;
+ private bool _cacheUpToDate;
+ private TaskLoggingHelper _log;
+
+ public DefineStaticWebAssetsCache() { }
+
+ internal DefineStaticWebAssetsCache(TaskLoggingHelper log, string manifestPath) : this()
+ => SetPathAndLogger(manifestPath, log);
+
+ // Inputs for the cache
+ public byte[] GlobalPropertiesHash { get; set; } = [];
+ public byte[] FingerprintPatternsHash { get; set; } = [];
+ public byte[] PropertyOverridesHash { get; set; } = [];
+ public HashSet InputHashes { get; set; } = [];
+
+ // Outputs for the cache
+ public Dictionary CachedAssets { get; set; } = [];
+ public Dictionary CachedCopyCandidates { get; set; } = [];
+
+ internal static DefineStaticWebAssetsCache ReadOrCreateCache(TaskLoggingHelper log, string manifestPath)
+ {
+ if (manifestPath != null && File.Exists(manifestPath))
+ {
+ using var existingManifestFile = File.OpenRead(manifestPath);
+ var cache = JsonSerializer.Deserialize(existingManifestFile, DefineStaticWebAssetsSerializerContext.Default.DefineStaticWebAssetsCache);
+ if (cache == null)
+ {
+ throw new InvalidOperationException($"Failed to deserialize cache from {manifestPath}");
+ }
+ cache.SetPathAndLogger(manifestPath, log);
+ return cache;
+ }
+ else
+ {
+ return new DefineStaticWebAssetsCache(log, manifestPath);
+ }
+ }
+
+ internal void WriteCacheManifest()
+ {
+ if (_manifestPath != null)
+ {
+ using var manifestFile = File.OpenWrite(_manifestPath);
+ manifestFile.SetLength(0);
+ JsonSerializer.Serialize(manifestFile, this, DefineStaticWebAssetsSerializerContext.Default.DefineStaticWebAssetsCache);
+ }
+ }
+
+ internal void AppendAsset(string hash, StaticWebAsset asset, ITaskItem item)
+ {
+ asset.AssetKind = item.GetMetadata(nameof(StaticWebAsset.AssetKind));
+ _assets.Add(item);
+ if (!string.IsNullOrEmpty(hash))
+ {
+ CachedAssets[hash] = asset;
+ }
+ }
+
+ internal void AppendCopyCandidate(string hash, string identity, string targetPath)
+ {
+ var copyCandidate = new CopyCandidate(identity, targetPath);
+ _copyCandidates.Add(copyCandidate.ToTaskItem());
+ if (!string.IsNullOrEmpty(hash))
+ {
+ CachedCopyCandidates[hash] = copyCandidate;
+ }
+ }
+
+ internal void Update(
+ byte[] propertiesHash,
+ byte[] fingerprintPatternsHash,
+ byte[] propertyOverridesHash,
+ Dictionary inputHashes)
+ {
+ if (!propertiesHash.SequenceEqual(GlobalPropertiesHash) ||
+ !fingerprintPatternsHash.SequenceEqual(FingerprintPatternsHash) ||
+ !propertyOverridesHash.SequenceEqual(PropertyOverridesHash))
+ {
+ TotalUpdate(propertiesHash, fingerprintPatternsHash, propertyOverridesHash, inputHashes);
+ }
+ else
+ {
+ PartialUpdate(inputHashes);
+ }
+ }
+
+ private void TotalUpdate(byte[] propertiesHash, byte[] fingerprintPatternsHash, byte[] propertyOverridesHash, IDictionary inputsByHash)
+ {
+ _log?.LogMessage(MessageImportance.Low, "Updating cache completely.");
+ GlobalPropertiesHash = propertiesHash;
+ FingerprintPatternsHash = fingerprintPatternsHash;
+ PropertyOverridesHash = propertyOverridesHash;
+ InputHashes = [.. inputsByHash.Keys];
+ _inputByHash = inputsByHash;
+ }
+
+ private void PartialUpdate(Dictionary inputHashes)
+ {
+ var newHashes = new HashSet(inputHashes.Keys);
+ var oldHashes = InputHashes;
+
+ if (newHashes.SetEquals(oldHashes))
+ {
+ // If all the input hashes match, then we can reuse all the results.
+ foreach (var cachedAsset in CachedAssets)
+ {
+ _assets.Add(cachedAsset.Value.ToTaskItem());
+ }
+ foreach (var cachedCopyCandidate in CachedCopyCandidates)
+ {
+ _copyCandidates.Add(cachedCopyCandidate.Value.ToTaskItem());
+ }
+
+ _cacheUpToDate = true;
+ _log?.LogMessage(MessageImportance.Low, "Cache is fully up to date.");
+ return;
+ }
+
+ var remainingCandidates = new Dictionary();
+ foreach (var kvp in inputHashes)
+ {
+ var candidate = kvp.Value;
+ var hash = kvp.Key;
+ if (!oldHashes.Contains(hash))
+ {
+ remainingCandidates.Add(hash, candidate);
+ }
+ else if (CachedAssets.TryGetValue(hash, out var asset))
+ {
+ _log?.LogMessage(MessageImportance.Low, "Asset {0} is up to date", candidate.ItemSpec);
+ _assets.Add(asset.ToTaskItem());
+ if (CachedCopyCandidates.TryGetValue(hash, out var copyCandidate))
+ {
+ _copyCandidates.Add(copyCandidate.ToTaskItem());
+ }
+ }
+ }
+
+ // Remove any assets that are no longer in the input set
+ InputHashes = newHashes;
+ var assetsToRemove = oldHashes.Except(InputHashes);
+ foreach (var hash in assetsToRemove)
+ {
+ CachedAssets.Remove(hash);
+ CachedCopyCandidates.Remove(hash);
+ }
+
+ _inputByHash = remainingCandidates;
+ }
+
+ internal void SetPathAndLogger(string manifestPath, TaskLoggingHelper log) => (_manifestPath, _log) = (manifestPath, log);
+
+ public (IList CopyCandidates, IList Assets) GetComputedOutputs() => (_copyCandidates, _assets);
+
+ internal void NoCache(ITaskItem[] candidateAssets)
+ {
+ _log?.LogMessage(MessageImportance.Low, "No cache manifest path specified. Cache will not be used.");
+ _cacheUpToDate = false;
+ _noCacheCandidates = candidateAssets;
+ }
+
+ internal IEnumerable> OutOfDateInputs()
+ {
+ if (_noCacheCandidates != null)
+ {
+ return EnumerateNoCache();
+ }
+
+ return _cacheUpToDate || _inputByHash == null ? [] : _inputByHash;
+
+ IEnumerable> EnumerateNoCache()
+ {
+ foreach (var candidate in _noCacheCandidates)
+ {
+ var hash = "";
+ yield return new KeyValuePair(hash, candidate);
+ }
+ }
+ }
+
+ internal bool IsUpToDate() => _cacheUpToDate;
+ }
+
+ internal class CopyCandidate(string identity, string targetPath)
+ {
+ public string Identity { get; set; } = identity;
+ public string TargetPath { get; set; } = targetPath;
+
+ internal ITaskItem ToTaskItem() => new TaskItem(Identity, new Dictionary { ["TargetPath"] = TargetPath });
+ }
+
+ [JsonSerializable(typeof(DefineStaticWebAssetsCache))]
+ [JsonSourceGenerationOptions(WriteIndented = false)]
+ internal partial class DefineStaticWebAssetsSerializerContext : JsonSerializerContext { }
+}
diff --git a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs
index ed00628601c8..3f0b9bf87817 100644
--- a/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs
+++ b/src/StaticWebAssetsSdk/Tasks/DefineStaticWebAssets.cs
@@ -1,19 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Linq.Expressions;
-using System.Net.Http.Headers;
-using System.Reflection.Metadata;
-using System.Reflection.PortableExecutable;
-using System.Text.RegularExpressions;
-using System.Xml.Linq;
using Microsoft.Build.Framework;
-using Microsoft.Build.Utilities;
-using Microsoft.Extensions.FileSystemGlobbing;
-using Microsoft.Extensions.FileSystemGlobbing.Internal;
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks
{
@@ -29,10 +17,8 @@ namespace Microsoft.AspNetCore.StaticWebAssets.Tasks
// There is also a RelativePathPattern that is used to automatically transform the relative path of the candidates to match
// the expected path of the final asset. This is typically use to remove a common path prefix, like `wwwroot` from the target
// path of the assets and so on.
- public class DefineStaticWebAssets : Task
+ public partial class DefineStaticWebAssets : Task
{
- private const string DefaultFingerprintExpression = "#[.{fingerprint}]?";
-
[Required]
public ITaskItem[] CandidateAssets { get; set; }
@@ -74,33 +60,46 @@ public class DefineStaticWebAssets : Task
public string CopyToPublishDirectory { get; set; } = StaticWebAsset.AssetCopyOptions.PreserveNewest;
+ public string CacheManifestPath { get; set; }
+
[Output]
public ITaskItem[] Assets { get; set; }
[Output]
public ITaskItem[] CopyCandidates { get; set; }
- [Output]
- public ITaskItem[] AssetDetails { get; set; }
+ public Func TestResolveFileDetails { get; set; }
public override bool Execute()
{
+ var assetsCache = GetOrCreateAssetsCache();
+
+ if (assetsCache.IsUpToDate())
+ {
+ var outputs = assetsCache.GetComputedOutputs();
+ Assets = [.. outputs.Assets];
+ CopyCandidates = [.. outputs.CopyCandidates];
+
+ return !Log.HasLoggedErrors;
+ }
+
try
{
- var results = new List();
- var copyCandidates = new List();
- var assetDetails = new List();
+ var matcher = !string.IsNullOrEmpty(RelativePathPattern) ?
+ new StaticWebAssetGlobMatcherBuilder().AddIncludePatterns(RelativePathPattern).Build() :
+ null;
- var matcher = !string.IsNullOrEmpty(RelativePathPattern) ? new Matcher().AddInclude(RelativePathPattern) : null;
- var filter = !string.IsNullOrEmpty(RelativePathFilter) ? new Matcher().AddInclude(RelativePathFilter) : null;
- var assetsByRelativePath = new Dictionary>();
- var fingerprintPatterns = (FingerprintPatterns ?? []).Select(p => new FingerprintPattern(p)).ToArray();
- var tokensByPattern = fingerprintPatterns.Where(p => !string.IsNullOrEmpty(p.Expression)).ToDictionary(p => p.Pattern.Substring(1), p => p.Expression);
- Array.Sort(fingerprintPatterns, (a, b) => a.Pattern.Count(c => c == '.').CompareTo(b.Pattern.Count(c => c == '.')));
+ var filter = !string.IsNullOrEmpty(RelativePathFilter) ?
+ new StaticWebAssetGlobMatcherBuilder().AddIncludePatterns(RelativePathFilter).Build() :
+ null;
- for (var i = 0; i < CandidateAssets.Length; i++)
+ var assetsByRelativePath = new Dictionary>();
+ var fingerprintPatternMatcher = new FingerprintPatternMatcher(Log, FingerprintCandidates ? (FingerprintPatterns ?? []) : []);
+ var matchContext = StaticWebAssetGlobMatcher.CreateMatchContext();
+ foreach (var kvp in assetsCache.OutOfDateInputs())
{
- var candidate = CandidateAssets[i];
+ var hash = kvp.Key;
+ var candidate = kvp.Value;
var relativePathCandidate = string.Empty;
if (SourceType == StaticWebAsset.SourceTypes.Discovered)
{
@@ -108,16 +107,17 @@ public override bool Execute()
relativePathCandidate = candidateMatchPath;
if (matcher != null && string.IsNullOrEmpty(candidate.GetMetadata("RelativePath")))
{
- var match = matcher.Match(StaticWebAssetPathPattern.PathWithoutTokens(candidateMatchPath));
- if (!match.HasMatches)
+ matchContext.SetPathAndReinitialize(StaticWebAssetPathPattern.PathWithoutTokens(candidateMatchPath));
+ var match = matcher.Match(matchContext);
+ if (!match.IsMatch)
{
Log.LogMessage(MessageImportance.Low, "Rejected asset '{0}' for pattern '{1}'", candidateMatchPath, RelativePathPattern);
continue;
}
- Log.LogMessage(MessageImportance.Low, "Accepted asset '{0}' for pattern '{1}' with relative path '{2}'", candidateMatchPath, RelativePathPattern, match.Files.Single().Stem);
+ Log.LogMessage(MessageImportance.Low, "Accepted asset '{0}' for pattern '{1}' with relative path '{2}'", candidateMatchPath, RelativePathPattern, match.Stem);
- relativePathCandidate = StaticWebAsset.Normalize(match.Files.Single().Stem);
+ relativePathCandidate = StaticWebAsset.Normalize(match.Stem);
}
}
else
@@ -125,10 +125,11 @@ public override bool Execute()
relativePathCandidate = GetCandidateMatchPath(candidate);
if (matcher != null)
{
- var match = matcher.Match(StaticWebAssetPathPattern.PathWithoutTokens(relativePathCandidate));
- if (match.HasMatches)
+ matchContext.SetPathAndReinitialize(StaticWebAssetPathPattern.PathWithoutTokens(relativePathCandidate));
+ var match = matcher.Match(matchContext);
+ if (match.IsMatch)
{
- var newRelativePathCandidate = match.Files.Single().Stem;
+ var newRelativePathCandidate = match.Stem;
Log.LogMessage(
MessageImportance.Low,
"The relative path '{0}' matched the pattern '{1}'. Replacing relative path with '{2}'.",
@@ -140,16 +141,20 @@ public override bool Execute()
}
}
- if (filter != null && !filter.Match(StaticWebAssetPathPattern.PathWithoutTokens(relativePathCandidate)).HasMatches)
+ if (filter != null)
{
- Log.LogMessage(
- MessageImportance.Low,
- "Skipping '{0}' because the relative path '{1}' did not match the filter '{2}'.",
- candidate.ItemSpec,
- relativePathCandidate,
- RelativePathFilter);
-
- continue;
+ matchContext.SetPathAndReinitialize(StaticWebAssetPathPattern.PathWithoutTokens(relativePathCandidate));
+ if (!filter.Match(matchContext).IsMatch)
+ {
+ Log.LogMessage(
+ MessageImportance.Low,
+ "Skipping '{0}' because the relative path '{1}' did not match the filter '{2}'.",
+ candidate.ItemSpec,
+ relativePathCandidate,
+ RelativePathFilter);
+
+ continue;
+ }
}
}
@@ -181,12 +186,14 @@ public override bool Execute()
// the asset.
var fingerprint = ComputePropertyValue(candidate, nameof(StaticWebAsset.Fingerprint), null, false);
var integrity = ComputePropertyValue(candidate, nameof(StaticWebAsset.Integrity), null, false);
- FileInfo file = null;
+
+ var identity = Path.GetFullPath(candidate.GetMetadata("FullPath"));
+ var (file, fileLength, lastWriteTimeUtc) = ResolveFileDetails(originalItemSpec, identity);
+
switch ((fingerprint, integrity))
{
case (null, null):
Log.LogMessage(MessageImportance.Low, "Computing fingerprint and integrity for asset '{0}'", candidate.ItemSpec);
- file = StaticWebAsset.ResolveFile(candidate.ItemSpec, originalItemSpec);
(fingerprint, integrity) = (StaticWebAsset.ComputeFingerprintAndIntegrity(file));
break;
case (null, not null):
@@ -195,47 +202,33 @@ public override bool Execute()
break;
case (not null, null):
Log.LogMessage(MessageImportance.Low, "Computing integrity for asset '{0}'", candidate.ItemSpec);
- file = StaticWebAsset.ResolveFile(candidate.ItemSpec, originalItemSpec);
integrity = StaticWebAsset.ComputeIntegrity(file);
break;
}
- if (file != null)
- {
- // Record the FileLength and LastWriteTimeUtc for the asset so that we don't have to read it again on other tasks
- // we'll flow this information to them
- assetDetails.Add(new TaskItem(file.FullName, new Dictionary
- {
- ["FileLength"] = file.Length.ToString(CultureInfo.InvariantCulture),
- ["LastWriteTimeUtc"] = file.LastWriteTimeUtc.ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture),
- }));
- }
-
// If we are not able to compute the value based on an existing value or a default, we produce an error and stop.
if (Log.HasLoggedErrors)
{
break;
}
- var identity = Path.GetFullPath(candidate.GetMetadata("FullPath"));
if (!string.Equals(SourceType, StaticWebAsset.SourceTypes.Discovered, StringComparison.OrdinalIgnoreCase))
{
// We ignore the content root for publish only assets since it doesn't matter.
var contentRootPrefix = StaticWebAsset.AssetKinds.IsPublish(assetKind) ? null : contentRoot;
- (identity, var computed) = ComputeCandidateIdentity(candidate, contentRootPrefix, relativePathCandidate, matcher);
+ (identity, var computed) = ComputeCandidateIdentity(candidate, contentRootPrefix, relativePathCandidate, matcher, matchContext);
if (computed)
{
- copyCandidates.Add(new TaskItem(candidate.ItemSpec, new Dictionary
- {
- ["TargetPath"] = identity
- }));
+ assetsCache.AppendCopyCandidate(hash, candidate.ItemSpec, identity);
}
}
- relativePathCandidate = FingerprintCandidates ?
- StaticWebAsset.Normalize(AppendFingerprintPattern(relativePathCandidate, identity, fingerprintPatterns, tokensByPattern)) :
- relativePathCandidate;
+ if (FingerprintCandidates)
+ {
+ matchContext.SetPathAndReinitialize(relativePathCandidate);
+ relativePathCandidate = StaticWebAsset.Normalize(fingerprintPatternMatcher.AppendFingerprintPattern(matchContext, identity));
+ }
var asset = StaticWebAsset.FromProperties(
identity,
@@ -255,9 +248,10 @@ public override bool Execute()
integrity,
copyToOutputDirectory,
copyToPublishDirectory,
- originalItemSpec);
+ originalItemSpec,
+ fileLength,
+ lastWriteTimeUtc);
- asset.Normalize();
var item = asset.ToTaskItem();
if (SourceType == StaticWebAsset.SourceTypes.Discovered)
{
@@ -265,12 +259,16 @@ public override bool Execute()
UpdateAssetKindIfNecessary(assetsByRelativePath, asset.RelativePath, item);
}
- results.Add(item);
+ assetsCache.AppendAsset(hash, asset, item);
}
- Assets = [.. results];
- CopyCandidates = [.. copyCandidates];
- AssetDetails = [.. assetDetails];
+ var outputs = assetsCache.GetComputedOutputs();
+ var results = outputs.Assets;
+
+ assetsCache.WriteCacheManifest();
+
+ Assets = [.. outputs.Assets];
+ CopyCandidates = [.. outputs.CopyCandidates];
}
catch (Exception ex)
{
@@ -280,98 +278,26 @@ public override bool Execute()
return !Log.HasLoggedErrors;
}
- private string AppendFingerprintPattern(
- string relativePathCandidate,
- string identity,
- FingerprintPattern[] fingerprintPatterns,
- IDictionary tokensByPattern)
+ private (FileInfo file, long fileLength, DateTimeOffset lastWriteTimeUtc) ResolveFileDetails(
+ string originalItemSpec,
+ string identity)
{
- if (relativePathCandidate.Contains("#["))
+ if (TestResolveFileDetails != null)
{
- var pattern = StaticWebAssetPathPattern.Parse(relativePathCandidate, identity);
- foreach (var segment in pattern.Segments)
- {
- foreach (var part in segment.Parts)
- {
- foreach (var name in segment.GetTokenNames())
- {
- if (string.Equals(name, "fingerprint", StringComparison.OrdinalIgnoreCase))
- {
- return relativePathCandidate;
- }
- }
- }
- }
- }
-
- // Fingerprinting patterns for content.By default(most common case), we check for a single extension, like.js or.css.
- // In that situation we apply the fingerprint expression directly to the file name, like app.js->app#[.{fingerprint}].js.
- // If we detect more than one extension, for example, Rcl.lib.module.js or Rcl.Razor.js, we retrieve the last extension and
- // check for a mapping in the list below.If we find a match, we apply the fingerprint expression to the file name, like
- // Rcl.lib.module.js->Rcl#[.{fingerprint}].lib.module.js. If we don't find a match, we add the extension to the name and
- // continue matching against the next segment, like Rcl.Razor.js->Rcl.Razor#[.{fingerprint}].js.
- // If we don't find a match, we apply the fingerprint before the first extension, like Rcl.Razor.js -> Rcl.Razor#[.{fingerprint}].js.
- var directoryName = Path.GetDirectoryName(relativePathCandidate);
- relativePathCandidate = Path.GetFileName(relativePathCandidate);
- var extensionCount = 0;
- var stem = relativePathCandidate;
- var extension = Path.GetExtension(relativePathCandidate);
- while (!string.IsNullOrEmpty(extension) || extensionCount < 2)
- {
- extensionCount++;
- stem = stem.Substring(0, stem.Length - extension.Length);
- extension = Path.GetExtension(stem);
- }
-
- // Simple case, single extension or no extension
- // For example:
- // app.js->app#[.{fingerprint}]?.js
- // app->README#[.{fingerprint}]?
- if (extensionCount < 2)
- {
- if (!tokensByPattern.TryGetValue(extension, out var expression))
- {
- expression = DefaultFingerprintExpression;
- }
-
- var simpleExtensionResult = Path.Combine(directoryName, $"{stem}{expression}{extension}");
- Log.LogMessage(MessageImportance.Low, "Fingerprinting asset '{0}' as '{1}'", relativePathCandidate, simpleExtensionResult);
- return simpleExtensionResult;
- }
-
- // Complex case, multiple extensions, try matching against known patterns
- // For example:
- // Rcl.lib.module.js->Rcl#[.{fingerprint}].lib.module.js
- // Rcl.Razor.js->Rcl.Razor#[.{fingerprint}].js
- foreach (var pattern in fingerprintPatterns)
- {
- var matchResult = pattern.Matcher.Match(StaticWebAssetPathPattern.PathWithoutTokens(relativePathCandidate));
- if (matchResult.HasMatches)
- {
- stem = relativePathCandidate.Substring(0, (1 + relativePathCandidate.Length - pattern.Pattern.Length));
- extension = relativePathCandidate.Substring(stem.Length);
- if (!tokensByPattern.TryGetValue(extension, out var expression))
- {
- expression = DefaultFingerprintExpression;
- }
- var patternResult = Path.Combine(directoryName, $"{stem}{expression}{extension}");
- Log.LogMessage(MessageImportance.Low, "Fingerprinting asset '{0}' as '{1}' because it matched pattern '{2}'", relativePathCandidate, patternResult, pattern.Pattern);
- return patternResult;
- }
+ return TestResolveFileDetails(identity, originalItemSpec);
}
-
- // Multiple extensions and no match, apply the fingerprint before the first extension
- // For example:
- // Rcl.Razor.js->Rcl.Razor#[.{fingerprint}].js
- stem = Path.GetFileNameWithoutExtension(relativePathCandidate);
- extension = Path.GetExtension(relativePathCandidate);
- var result = Path.Combine(directoryName, $"{stem}{DefaultFingerprintExpression}{extension}");
- Log.LogMessage(MessageImportance.Low, "Fingerprinting asset '{0}' as '{1}' because it didn't match any pattern", relativePathCandidate, result);
-
- return result;
+ var file = StaticWebAsset.ResolveFile(identity, originalItemSpec);
+ var fileLength = file.Length;
+ var lastWriteTimeUtc = file.LastWriteTimeUtc;
+ return (file, fileLength, lastWriteTimeUtc);
}
- private (string identity, bool computed) ComputeCandidateIdentity(ITaskItem candidate, string contentRoot, string relativePath, Matcher matcher)
+ private (string identity, bool computed) ComputeCandidateIdentity(
+ ITaskItem candidate,
+ string contentRoot,
+ string relativePath,
+ StaticWebAssetGlobMatcher matcher,
+ StaticWebAssetGlobMatcher.MatchContext matchContext)
{
var candidateFullPath = Path.GetFullPath(candidate.GetMetadata("FullPath"));
if (contentRoot == null)
@@ -392,8 +318,12 @@ private string AppendFingerprintPattern(
// publish processes, so we want to allow defining these assets by setting up a different content root path from their
// original location in the project. For example the asset can be wwwroot\my-prod-asset.js, the content root can be
// obj\transform and the final asset identity can be <>\obj\transform\my-prod-asset.js
-
- var matchResult = matcher?.Match(StaticWebAssetPathPattern.PathWithoutTokens(candidate.ItemSpec));
+ GlobMatch matchResult = default;
+ if (matcher != null)
+ {
+ matchContext.SetPathAndReinitialize(StaticWebAssetPathPattern.PathWithoutTokens(candidate.ItemSpec));
+ matchResult = matcher.Match(matchContext);
+ }
if (matcher == null)
{
// If no relative path pattern was specified, we are going to suggest that the identity is `%(ContentRoot)\RelativePath\OriginalFileName`
@@ -405,14 +335,14 @@ private string AppendFingerprintPattern(
Log.LogMessage(MessageImportance.Low, "Identity for candidate '{0}' is '{1}' because it did not start with the content root '{2}'", candidate.ItemSpec, finalIdentity, normalizedContentRoot);
return (finalIdentity, true);
}
- else if (!matchResult.HasMatches)
+ else if (!matchResult.IsMatch)
{
Log.LogMessage(MessageImportance.Low, "Identity for candidate '{0}' is '{1}' because it didn't match the relative path pattern", candidate.ItemSpec, candidateFullPath);
return (candidateFullPath, false);
}
else
{
- var stem = matchResult.Files.Single().Stem;
+ var stem = matchResult.Stem;
var assetIdentity = Path.GetFullPath(Path.Combine(normalizedContentRoot, stem));
Log.LogMessage(MessageImportance.Low, "Computed identity '{0}' for candidate '{1}'", assetIdentity, candidate.ItemSpec);
@@ -609,17 +539,5 @@ private string GetDiscoveryCandidateMatchPath(ITaskItem candidate)
return computedPath;
}
-
- private class FingerprintPattern(ITaskItem pattern)
- {
- Matcher _matcher;
- public string Name { get; set; } = pattern.ItemSpec;
-
- public string Pattern { get; set; } = pattern.GetMetadata(nameof(Pattern));
-
- public string Expression { get; set; } = pattern.GetMetadata(nameof(Expression));
-
- public Matcher Matcher => _matcher ??= new Matcher().AddInclude(Pattern);
- }
}
}
diff --git a/src/StaticWebAssetsSdk/Tasks/FingerprintPatternMatcher.cs b/src/StaticWebAssetsSdk/Tasks/FingerprintPatternMatcher.cs
new file mode 100644
index 000000000000..fbf64112a91a
--- /dev/null
+++ b/src/StaticWebAssetsSdk/Tasks/FingerprintPatternMatcher.cs
@@ -0,0 +1,177 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
+
+internal class FingerprintPatternMatcher
+{
+ private const string DefaultFingerprintExpression = "#[.{fingerprint}]?";
+
+ private readonly TaskLoggingHelper _log;
+ private readonly Dictionary _tokensByPattern;
+ private readonly StaticWebAssetGlobMatcher _matcher;
+
+ public FingerprintPatternMatcher(
+ TaskLoggingHelper log,
+ ITaskItem[] fingerprintPatterns)
+ {
+ var tokensByPattern = fingerprintPatterns
+ .ToDictionary(
+ p => p.GetMetadata("Pattern"),
+ p => p.GetMetadata("Expression") is string expr and not "" ? expr : DefaultFingerprintExpression);
+
+ _log = log;
+ _tokensByPattern = tokensByPattern;
+
+ var builder = new StaticWebAssetGlobMatcherBuilder();
+ foreach (var pattern in fingerprintPatterns)
+ {
+ builder.AddIncludePatterns(pattern.GetMetadata("Pattern"));
+ }
+
+ _matcher = builder.Build();
+ }
+
+ public string AppendFingerprintPattern(StaticWebAssetGlobMatcher.MatchContext context, string identity)
+ {
+ var relativePathCandidateMemory = context.PathString.AsMemory();
+ if (AlreadyContainsFingerprint(relativePathCandidateMemory, identity))
+ {
+ return relativePathCandidateMemory.ToString();
+ }
+
+ var (directoryName, fileName, fileNamePrefix, extension) =
+#if NET9_0_OR_GREATER
+ ComputeFingerprintFragments(relativePathCandidateMemory);
+#else
+ ComputeFingerprintFragments(context.PathString);
+#endif
+
+ context.SetPathAndReinitialize(fileName);
+ var matchResult = _matcher.Match(context);
+ if (!matchResult.IsMatch)
+ {
+#if NET9_0_OR_GREATER
+ var result = Path.Combine(directoryName.ToString(), $"{fileNamePrefix}{DefaultFingerprintExpression}{extension}");
+#else
+ var result = Path.Combine(directoryName, $"{fileNamePrefix}{DefaultFingerprintExpression}{extension}");
+#endif
+ _log.LogMessage(MessageImportance.Low, "Fingerprinting asset '{0}' as '{1}' because it didn't match any pattern", relativePathCandidateMemory, result);
+
+ return result;
+ }
+ else
+ {
+ if (!_tokensByPattern.TryGetValue(matchResult.Pattern, out var expression))
+ {
+ throw new InvalidOperationException($"No expression found for pattern '{matchResult.Pattern}'");
+ }
+ else
+ {
+ var stem = GetMatchStem(fileName, matchResult.Pattern.AsMemory().Slice(2));
+ var matchExtension = GetMatchExtension(fileName, stem);
+
+ var simpleExtensionResult = Path.Combine(directoryName.ToString(), $"{stem}{expression}{matchExtension}");
+ _log.LogMessage(MessageImportance.Low, "Fingerprinting asset '{0}' as '{1}'", relativePathCandidateMemory, simpleExtensionResult);
+ return simpleExtensionResult;
+ }
+ }
+
+ static bool AlreadyContainsFingerprint(ReadOnlyMemory relativePathCandidate, string identity)
+ {
+ if (MemoryExtensions.Contains(relativePathCandidate.Span, "#[".AsSpan(), StringComparison.Ordinal))
+ {
+ var pattern = StaticWebAssetPathPattern.Parse(relativePathCandidate, identity);
+ foreach (var segment in pattern.Segments)
+ {
+ foreach (var part in segment.Parts)
+ {
+ foreach (var name in segment.GetTokenNames())
+ {
+ if (MemoryExtensions.Equals(name.Span, "fingerprint".AsSpan(), StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+#if NET9_0_OR_GREATER
+ static ReadOnlySpan GetMatchExtension(ReadOnlySpan relativePathCandidateMemory, ReadOnlySpan stem) =>
+ relativePathCandidateMemory.Slice(stem.Length);
+ static ReadOnlySpan GetMatchStem(ReadOnlySpan relativePathCandidateMemory, ReadOnlyMemory pattern) =>
+ relativePathCandidateMemory.Slice(0, relativePathCandidateMemory.Length - pattern.Length - 1);
+#else
+ static ReadOnlyMemory GetMatchExtension(ReadOnlyMemory relativePathCandidateMemory, ReadOnlyMemory stem) =>
+ relativePathCandidateMemory.Slice(stem.Length);
+ static ReadOnlyMemory GetMatchStem(ReadOnlyMemory relativePathCandidateMemory, ReadOnlyMemory pattern) =>
+ relativePathCandidateMemory.Slice(0, relativePathCandidateMemory.Length - pattern.Length - 1);
+#endif
+ }
+
+#if NET9_0_OR_GREATER
+ private static FingerprintFragments ComputeFingerprintFragments(
+ ReadOnlyMemory relativePathCandidate)
+ {
+ var fileName = Path.GetFileName(relativePathCandidate.Span);
+ var directoryName = Path.GetDirectoryName(relativePathCandidate.Span);
+ var stem = Path.GetFileNameWithoutExtension(relativePathCandidate.Span);
+ var extension = Path.GetExtension(relativePathCandidate.Span);
+
+ return new(directoryName, fileName, stem, extension);
+ }
+#else
+ private static (string directoryName, ReadOnlyMemory fileName, ReadOnlyMemory fileNamePrefix, ReadOnlyMemory extension) ComputeFingerprintFragments(
+ string relativePathCandidate)
+ {
+ var fileName = Path.GetFileName(relativePathCandidate).AsMemory();
+ var directoryName = Path.GetDirectoryName(relativePathCandidate);
+ var stem = Path.GetFileNameWithoutExtension(relativePathCandidate).AsMemory();
+ var extension = Path.GetExtension(relativePathCandidate).AsMemory();
+
+ return (directoryName, fileName, stem, extension);
+ }
+#endif
+
+ private ref struct FingerprintFragments
+ {
+ public ReadOnlySpan DirectoryName;
+ public ReadOnlySpan FileName;
+ public ReadOnlySpan FileNamePrefix;
+ public ReadOnlySpan Extension;
+
+ public FingerprintFragments(ReadOnlySpan directoryName, ReadOnlySpan fileName, ReadOnlySpan fileNamePrefix, ReadOnlySpan extension)
+ {
+ DirectoryName = directoryName;
+ FileName = fileName;
+ FileNamePrefix = fileNamePrefix;
+ Extension = extension;
+ }
+
+ public void Deconstruct(out ReadOnlySpan directoryName, out ReadOnlySpan fileName, out ReadOnlySpan fileNamePrefix, out ReadOnlySpan extension)
+ {
+ directoryName = DirectoryName;
+ fileName = FileName;
+ fileNamePrefix = FileNamePrefix;
+ extension = Extension;
+ }
+ }
+
+ internal class FingerprintPattern(ITaskItem pattern)
+ {
+ StaticWebAssetGlobMatcher _matcher;
+ public string Name { get; set; } = pattern.ItemSpec;
+
+ public string Pattern { get; set; } = pattern.GetMetadata(nameof(Pattern));
+
+ public string Expression { get; set; } = pattern.GetMetadata(nameof(Expression));
+
+ public StaticWebAssetGlobMatcher Matcher => _matcher ??= new StaticWebAssetGlobMatcherBuilder().AddIncludePatterns(Pattern).Build();
+ }
+}
diff --git a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsDevelopmentManifest.cs b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsDevelopmentManifest.cs
index df4a58644a77..111a74134a83 100644
--- a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsDevelopmentManifest.cs
+++ b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsDevelopmentManifest.cs
@@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.StaticWebAssets.Tasks
public class GenerateStaticWebAssetsDevelopmentManifest : Task
{
private static readonly char[] _separator = ['/'];
-
+
[Required]
public string Source { get; set; }
diff --git a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsPropsFile.cs b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsPropsFile.cs
index bad9da6364b1..56cda37eed16 100644
--- a/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsPropsFile.cs
+++ b/src/StaticWebAssetsSdk/Tasks/GenerateStaticWebAssetsPropsFile.cs
@@ -25,7 +25,8 @@ public class GenerateStaticWebAssetsPropsFile : Task
private const string CopyToOutputDirectory = "CopyToOutputDirectory";
private const string CopyToPublishDirectory = "CopyToPublishDirectory";
private const string OriginalItemSpec = "OriginalItemSpec";
-
+ private const string FileLength = "FileLength";
+ private const string LastWriteTime = "LastWriteTime";
[Required]
public string TargetPropsFilePath { get; set; }
@@ -82,6 +83,8 @@ private bool ExecuteCore()
new XElement(Integrity, element.GetMetadata(Integrity)),
new XElement(CopyToOutputDirectory, element.GetMetadata(CopyToOutputDirectory)),
new XElement(CopyToPublishDirectory, element.GetMetadata(CopyToPublishDirectory)),
+ new XElement(FileLength, element.GetMetadata(FileLength)),
+ new XElement(LastWriteTime, element.GetMetadata(LastWriteTime)),
new XElement(OriginalItemSpec, fullPathExpression)));
}
@@ -138,18 +141,15 @@ private void WriteFile(byte[] data)
private static string ComputeHash(byte[] data)
{
+#if !NET9_0_OR_GREATER
using var sha256 = SHA256.Create();
-
var result = sha256.ComputeHash(data);
+#else
+ var result = SHA256.HashData(data);
+#endif
return Convert.ToBase64String(result);
}
- private XmlWriter GetXmlWriter(XmlWriterSettings settings)
- {
- var fileStream = new FileStream(TargetPropsFilePath, FileMode.Create);
- return XmlWriter.Create(fileStream, settings);
- }
-
private bool ValidateArguments()
{
ITaskItem firstAsset = null;
@@ -226,7 +226,7 @@ private bool EnsureRequiredMetadata(ITaskItem item, string metadataName, bool al
return true;
}
- private bool HasMetadata(ITaskItem item, string metadataName)
+ private static bool HasMetadata(ITaskItem item, string metadataName)
{
foreach (var name in item.MetadataNames)
{
diff --git a/src/StaticWebAssetsSdk/Tasks/Legacy/ValidateStaticWebAssetsUniquePaths.cs b/src/StaticWebAssetsSdk/Tasks/Legacy/ValidateStaticWebAssetsUniquePaths.cs
index 1faacbfda061..1c84919f21be 100644
--- a/src/StaticWebAssetsSdk/Tasks/Legacy/ValidateStaticWebAssetsUniquePaths.cs
+++ b/src/StaticWebAssetsSdk/Tasks/Legacy/ValidateStaticWebAssetsUniquePaths.cs
@@ -17,22 +17,22 @@ public class ValidateStaticWebAssetsUniquePaths : Task
[Required]
public ITaskItem[] WebRootFiles { get; set; }
- public override bool Execute()
+ public override bool Execute()
+ {
+ var assetsByWebRootPaths = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ for (var i = 0; i < StaticWebAssets.Length; i++)
{
- var assetsByWebRootPaths = new Dictionary(StringComparer.OrdinalIgnoreCase);
- for (var i = 0; i < StaticWebAssets.Length; i++)
+ var contentRootDefinition = StaticWebAssets[i];
+ if (!EnsureRequiredMetadata(contentRootDefinition, BasePath) ||
+ !EnsureRequiredMetadata(contentRootDefinition, RelativePath))
{
- var contentRootDefinition = StaticWebAssets[i];
- if (!EnsureRequiredMetadata(contentRootDefinition, BasePath) ||
- !EnsureRequiredMetadata(contentRootDefinition, RelativePath))
- {
- return false;
- }
- else
- {
- var webRootPath = GetWebRootPath("/wwwroot",
- contentRootDefinition.GetMetadata(BasePath),
- contentRootDefinition.GetMetadata(RelativePath));
+ return false;
+ }
+ else
+ {
+ var webRootPath = GetWebRootPath("/wwwroot",
+ contentRootDefinition.GetMetadata(BasePath),
+ contentRootDefinition.GetMetadata(RelativePath));
if (assetsByWebRootPaths.TryGetValue(webRootPath, out var existingWebRootPath))
{
@@ -49,17 +49,17 @@ public override bool Execute()
}
}
- for (var i = 0; i < WebRootFiles.Length; i++)
+ for (var i = 0; i < WebRootFiles.Length; i++)
+ {
+ var webRootFile = WebRootFiles[i];
+ var relativePath = webRootFile.GetMetadata(TargetPath);
+ var webRootFileWebRootPath = GetWebRootPath("", "/", relativePath);
+ if (assetsByWebRootPaths.TryGetValue(webRootFileWebRootPath, out var existingAsset))
{
- var webRootFile = WebRootFiles[i];
- var relativePath = webRootFile.GetMetadata(TargetPath);
- var webRootFileWebRootPath = GetWebRootPath("", "/", relativePath);
- if (assetsByWebRootPaths.TryGetValue(webRootFileWebRootPath, out var existingAsset))
- {
- Log.LogError($"The static web asset '{existingAsset.ItemSpec}' has a conflicting web root path '{webRootFileWebRootPath}' with the project file '{webRootFile.ItemSpec}'.");
- return false;
- }
+ Log.LogError($"The static web asset '{existingAsset.ItemSpec}' has a conflicting web root path '{webRootFileWebRootPath}' with the project file '{webRootFile.ItemSpec}'.");
+ return false;
}
+ }
return true;
}
diff --git a/src/StaticWebAssetsSdk/Tasks/Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj b/src/StaticWebAssetsSdk/Tasks/Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj
index 73f553c83e41..7c3331c2fc35 100644
--- a/src/StaticWebAssetsSdk/Tasks/Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj
+++ b/src/StaticWebAssetsSdk/Tasks/Microsoft.NET.Sdk.StaticWebAssets.Tasks.csproj
@@ -40,6 +40,10 @@
+
+
+
+
@@ -47,12 +51,8 @@
-
-
+
+
@@ -62,61 +62,43 @@
-
+
<_FileSystemGlobbing Include="@(ReferencePath)" Condition="'%(ReferencePath.NuGetPackageId)' == 'Microsoft.Extensions.FileSystemGlobbing'" />
<_FileSystemGlobbingContent Include="@(_FileSystemGlobbing)" TargetPath="tasks\$(TargetFramework)\%(_FileSystemGlobbing.Filename)%(_FileSystemGlobbing.Extension)" />
-
+
-
+
- <_CssParser Include="@(ReferencePath->WithMetadataValue('NuGetPackageId', 'Microsoft.Css.Parser'))" />
+ <_CssParser Include="@(ReferencePath->WithMetadataValue('NuGetPackageId', 'Microsoft.Css.Parser'))" />
<_CssParserContent Include="@(_CssParser)" TargetPath="tasks\$(TargetFramework)\%(_CssParser.Filename)%(_CssParser.Extension)" />
-
+
-
+
-
+
-
+
diff --git a/src/StaticWebAssetsSdk/Tasks/ScopedCss/ComputeCssScope.cs b/src/StaticWebAssetsSdk/Tasks/ScopedCss/ComputeCssScope.cs
index 47ea03306ac9..a938af350570 100644
--- a/src/StaticWebAssetsSdk/Tasks/ScopedCss/ComputeCssScope.cs
+++ b/src/StaticWebAssetsSdk/Tasks/ScopedCss/ComputeCssScope.cs
@@ -23,12 +23,12 @@ public override bool Execute()
{
ScopedCss = new ITaskItem[ScopedCssInput.Length];
- for (var i = 0; i < ScopedCssInput.Length; i++)
- {
- var input = ScopedCssInput[i];
- var relativePath = input.ItemSpec.ToLowerInvariant().Replace("\\", "//");
- var scope = input.GetMetadata("CssScope");
- scope = !string.IsNullOrEmpty(scope) ? scope : GenerateScope(TargetName, relativePath);
+ for (var i = 0; i < ScopedCssInput.Length; i++)
+ {
+ var input = ScopedCssInput[i];
+ var relativePath = input.ItemSpec.ToLowerInvariant().Replace("\\", "//");
+ var scope = input.GetMetadata("CssScope");
+ scope = !string.IsNullOrEmpty(scope) ? scope : GenerateScope(TargetName, relativePath);
var outputItem = new TaskItem(input);
outputItem.SetMetadata("CssScope", scope);
diff --git a/src/StaticWebAssetsSdk/Tasks/ScopedCss/ConcatenateCssFiles.cs b/src/StaticWebAssetsSdk/Tasks/ScopedCss/ConcatenateCssFiles.cs
index 408cd0c1daa4..048f6363d56e 100644
--- a/src/StaticWebAssetsSdk/Tasks/ScopedCss/ConcatenateCssFiles.cs
+++ b/src/StaticWebAssetsSdk/Tasks/ScopedCss/ConcatenateCssFiles.cs
@@ -1,13 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Security.Cryptography;
+#if NET9_0_OR_GREATER
+using System.Globalization;
+#endif
using Microsoft.Build.Framework;
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks
{
public class ConcatenateCssFiles : Task
{
+ private static readonly char[] _separator = ['/'];
+
private static readonly IComparer _fullPathComparer =
Comparer.Create((x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.GetMetadata("FullPath"), y.GetMetadata("FullPath")));
@@ -54,13 +58,17 @@ public override bool Execute()
// We could produce shorter paths if we detected common segments between the final bundle base path and the imported bundle
// base paths, but its more work and it will not have a significant impact on the bundle size size.
var normalizedBasePath = NormalizePath(ScopedCssBundleBasePath);
- var currentBasePathSegments = normalizedBasePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+ var currentBasePathSegments = normalizedBasePath.Split(_separator, StringSplitOptions.RemoveEmptyEntries);
var prefix = string.Join("/", Enumerable.Repeat("..", currentBasePathSegments.Length));
for (var i = 0; i < ProjectBundles.Length; i++)
{
var importPath = NormalizePath(Path.Combine(prefix, ProjectBundles[i].ItemSpec));
+#if !NET9_0_OR_GREATER
builder.AppendLine($"@import '{importPath}';");
+#else
+ builder.AppendLine(CultureInfo.InvariantCulture, $"@import '{importPath}';");
+#endif
}
builder.AppendLine();
@@ -69,7 +77,11 @@ public override bool Execute()
for (var i = 0; i < ScopedCssFiles.Length; i++)
{
var current = ScopedCssFiles[i];
- builder.AppendLine($"/* {NormalizePath(current.GetMetadata("BasePath"))}/{NormalizePath(current.GetMetadata("RelativePath"))} */");
+#if !NET9_0_OR_GREATER
+ builder.AppendLine($"/* {ConcatenateCssFiles.NormalizePath(current.GetMetadata("BasePath"))}/{ConcatenateCssFiles.NormalizePath(current.GetMetadata("RelativePath"))} */");
+#else
+ builder.AppendLine(CultureInfo.InvariantCulture, $"/* {NormalizePath(current.GetMetadata("BasePath"))}/{NormalizePath(current.GetMetadata("RelativePath"))} */");
+#endif
foreach (var line in File.ReadLines(current.GetMetadata("FullPath")))
{
builder.AppendLine(line);
@@ -84,34 +96,15 @@ public override bool Execute()
File.WriteAllText(OutputFile, content);
}
-
return !Log.HasLoggedErrors;
}
- private string NormalizePath(string path) => path.Replace("\\", "/").Trim('/');
+ private static string NormalizePath(string path) => path.Replace("\\", "/").Trim('/');
- private bool SameContent(string content, string outputFilePath)
+ private static bool SameContent(string content, string outputFilePath)
{
- var contentHash = GetContentHash(content);
-
var outputContent = File.ReadAllText(outputFilePath);
- var outputContentHash = GetContentHash(outputContent);
-
- for (int i = 0; i < outputContentHash.Length; i++)
- {
- if (outputContentHash[i] != contentHash[i])
- {
- return false;
- }
- }
-
- return true;
-
- static byte[] GetContentHash(string content)
- {
- using var sha256 = SHA256.Create();
- return sha256.ComputeHash(Encoding.UTF8.GetBytes(content));
- }
+ return string.Equals(content, outputContent);
}
}
}
diff --git a/src/StaticWebAssetsSdk/Tasks/ScopedCss/ConcatenateCssFiles50.cs b/src/StaticWebAssetsSdk/Tasks/ScopedCss/ConcatenateCssFiles50.cs
index 70e59cc09775..b1d584f6d571 100644
--- a/src/StaticWebAssetsSdk/Tasks/ScopedCss/ConcatenateCssFiles50.cs
+++ b/src/StaticWebAssetsSdk/Tasks/ScopedCss/ConcatenateCssFiles50.cs
@@ -1,13 +1,17 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Security.Cryptography;
+#if NET9_0_OR_GREATER
+using System.Globalization;
+#endif
using Microsoft.Build.Framework;
namespace Microsoft.AspNetCore.StaticWebAssets.Tasks
{
public class ConcatenateCssFiles50 : Task
{
+ private static readonly char[] _separator = ['/'];
+
private static readonly IComparer _fullPathComparer =
Comparer.Create((x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.GetMetadata("FullPath"), y.GetMetadata("FullPath")));
@@ -54,7 +58,7 @@ public override bool Execute()
// We could produce shorter paths if we detected common segments between the final bundle base path and the imported bundle
// base paths, but its more work and it will not have a significant impact on the bundle size size.
var normalizedBasePath = NormalizePath(ScopedCssBundleBasePath);
- var currentBasePathSegments = normalizedBasePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+ var currentBasePathSegments = normalizedBasePath.Split(_separator, StringSplitOptions.RemoveEmptyEntries);
var prefix = string.Join("/", Enumerable.Repeat("..", currentBasePathSegments.Length));
for (var i = 0; i < ProjectBundles.Length; i++)
{
@@ -63,7 +67,11 @@ public override bool Execute()
var relativePath = NormalizePath(bundle.GetMetadata("RelativePath"));
var importPath = NormalizePath(Path.Combine(prefix, bundleBasePath, relativePath));
+#if !NET9_0_OR_GREATER
builder.AppendLine($"@import '{importPath}';");
+#else
+ builder.AppendLine(CultureInfo.InvariantCulture, $"@import '{importPath}';");
+#endif
}
builder.AppendLine();
@@ -72,7 +80,11 @@ public override bool Execute()
for (var i = 0; i < ScopedCssFiles.Length; i++)
{
var current = ScopedCssFiles[i];
- builder.AppendLine($"/* {NormalizePath(current.GetMetadata("BasePath"))}/{NormalizePath(current.GetMetadata("RelativePath"))} */");
+#if !NET9_0_OR_GREATER
+ builder.AppendLine($"/* {ConcatenateCssFiles50.NormalizePath(current.GetMetadata("BasePath"))}/{ConcatenateCssFiles50.NormalizePath(current.GetMetadata("RelativePath"))} */");
+#else
+ builder.AppendLine(CultureInfo.InvariantCulture, $"/* {NormalizePath(current.GetMetadata("BasePath"))}/{NormalizePath(current.GetMetadata("RelativePath"))} */");
+#endif
foreach (var line in File.ReadLines(current.GetMetadata("FullPath")))
{
builder.AppendLine(line);
@@ -87,38 +99,15 @@ public override bool Execute()
File.WriteAllText(OutputFile, content);
}
-
return !Log.HasLoggedErrors;
}
- private string NormalizePath(string path) => path.Replace("\\", "/").Trim('/');
+ private static string NormalizePath(string path) => path.Replace("\\", "/").Trim('/');
- private bool SameContent(string content, string outputFilePath)
+ private static bool SameContent(string content, string outputFilePath)
{
- var contentHash = GetContentHash(content);
-
var outputContent = File.ReadAllText(outputFilePath);
- var outputContentHash = GetContentHash(outputContent);
-
- for (int i = 0; i < outputContentHash.Length; i++)
- {
- if (outputContentHash[i] != contentHash[i])
- {
- return false;
- }
- }
-
- return true;
-
- static byte[] GetContentHash(string content)
- {
-#if NET472_OR_GREATER
- using var sha256 = SHA256.Create();
- return sha256.ComputeHash(Encoding.UTF8.GetBytes(content));
-#else
- return SHA256.HashData(Encoding.UTF8.GetBytes(content));
-#endif
- }
+ return string.Equals(content, outputContent);
}
}
}
diff --git a/src/StaticWebAssetsSdk/Tasks/ServiceWorker/GenerateServiceWorkerAssetsManifest.cs b/src/StaticWebAssetsSdk/Tasks/ServiceWorker/GenerateServiceWorkerAssetsManifest.cs
index b4a8ff3ee560..684bb5ebbc7a 100644
--- a/src/StaticWebAssetsSdk/Tasks/ServiceWorker/GenerateServiceWorkerAssetsManifest.cs
+++ b/src/StaticWebAssetsSdk/Tasks/ServiceWorker/GenerateServiceWorkerAssetsManifest.cs
@@ -86,13 +86,13 @@ private static string ComputeVersion(ManifestEntry [] assets)
return version;
}
- private void PersistManifest(ServiceWorkerManifest manifest)
- {
- var data = JsonSerializer.Serialize(manifest, ManifestSerializationOptions);
- var content = $"self.assetsManifest = {data};{Environment.NewLine}";
- var contentHash = ComputeFileHash(content);
- var fileExists = File.Exists(OutputPath);
- var existingManifestHash = fileExists ? ComputeFileHash(File.ReadAllText(OutputPath)) : "";
+ private void PersistManifest(ServiceWorkerManifest manifest)
+ {
+ var data = JsonSerializer.Serialize(manifest, ManifestSerializationOptions);
+ var content = $"self.assetsManifest = {data};{Environment.NewLine}";
+ var contentHash = ComputeFileHash(content);
+ var fileExists = File.Exists(OutputPath);
+ var existingManifestHash = fileExists ? ComputeFileHash(File.ReadAllText(OutputPath)) : "";
if (!fileExists)
{
diff --git a/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/GlobMatch.cs b/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/GlobMatch.cs
new file mode 100644
index 000000000000..c35113f518d6
--- /dev/null
+++ b/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/GlobMatch.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
+
+public struct GlobMatch(bool isMatch, string pattern = null, string stem = null)
+{
+ public bool IsMatch { get; set; } = isMatch;
+
+ public string Pattern { get; set; } = pattern;
+
+ public string Stem { get; set; } = stem;
+}
diff --git a/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/GlobNode.cs b/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/GlobNode.cs
new file mode 100644
index 000000000000..65d7fb27d438
--- /dev/null
+++ b/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/GlobNode.cs
@@ -0,0 +1,113 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+
+namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
+
+[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+public class GlobNode
+{
+ public string Match { get; set; }
+
+#if NET9_0_OR_GREATER
+ public Dictionary LiteralsDictionary { get; set; }
+ public Dictionary.AlternateLookup> Literals { get; set; }
+#else
+ public Dictionary Literals { get; set; }
+#endif
+
+#if NET9_0_OR_GREATER
+ public Dictionary ExtensionsDictionary { get; set; }
+ public Dictionary.AlternateLookup> Extensions { get; set; }
+#else
+ public Dictionary Extensions { get; set; }
+#endif
+
+ public List ComplexGlobSegments { get; set; }
+
+ public GlobNode WildCard { get; set; }
+
+ public GlobNode RecursiveWildCard { get; set; }
+
+ internal bool HasChildren()
+ {
+#if NET9_0_OR_GREATER
+ return LiteralsDictionary?.Count > 0 || ExtensionsDictionary?.Count > 0 || ComplexGlobSegments?.Count > 0 || WildCard != null || RecursiveWildCard != null;
+#else
+ return Literals?.Count > 0 || Extensions?.Count > 0 || ComplexGlobSegments?.Count > 0 || WildCard != null || RecursiveWildCard != null;
+#endif
+ }
+
+ private string GetDebuggerDisplay()
+ {
+ return ToString();
+ }
+
+ public override string ToString()
+ {
+#if NET9_0_OR_GREATER
+ var literals = $$"""{{{string.Join(", ", LiteralsDictionary?.Keys ?? Enumerable.Empty())}}}""";
+ var extensions = $$"""{{{string.Join(", ", ExtensionsDictionary?.Keys ?? Enumerable.Empty())}}}""";
+#else
+ var literals = $$"""{{{string.Join(", ", Literals?.Keys ?? Enumerable.Empty())}}}""";
+ var extensions = $$"""{{{string.Join(", ", Extensions?.Keys ?? Enumerable.Empty())}}}""";
+#endif
+ var wildCard = WildCard != null ? "*" : string.Empty;
+ var recursiveWildCard = RecursiveWildCard != null ? "**" : string.Empty;
+ return $"{literals}|{extensions}|{wildCard}|{recursiveWildCard}";
+ }
+
+ internal bool HasLiterals()
+ {
+#if NET9_0_OR_GREATER
+ return LiteralsDictionary?.Count > 0;
+#else
+ return Literals?.Count > 0;
+#endif
+ }
+
+ internal bool HasExtensions()
+ {
+#if NET9_0_OR_GREATER
+ return ExtensionsDictionary?.Count > 0;
+#else
+ return Extensions?.Count > 0;
+#endif
+ }
+}
+
+[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+public class ComplexGlobSegment
+{
+ public GlobNode Node { get; set; }
+ public List Parts { get; set; }
+
+ private string GetDebuggerDisplay() => ToString();
+
+ public override string ToString() => string.Join("", Parts.Select(p => p.ToString()));
+}
+
+[DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+public class GlobSegmentPart
+{
+ public GlobSegmentPartKind Kind { get; set; }
+ public ReadOnlyMemory Value { get; set; }
+
+ private string GetDebuggerDisplay() => ToString();
+
+ public override string ToString() => Kind switch
+ {
+ GlobSegmentPartKind.Literal => Value.ToString(),
+ GlobSegmentPartKind.WildCard => "*",
+ GlobSegmentPartKind.QuestionMark => "?",
+ _ => throw new InvalidOperationException(),
+ };
+}
+
+public enum GlobSegmentPartKind
+{
+ Literal,
+ WildCard,
+ QuestionMark,
+}
diff --git a/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/PathTokenizer.cs b/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/PathTokenizer.cs
new file mode 100644
index 000000000000..84860aea7ef0
--- /dev/null
+++ b/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/PathTokenizer.cs
@@ -0,0 +1,118 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.StaticWebAssets.Tasks.Utils;
+
+namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
+
+#if !NET9_0_OR_GREATER
+public ref struct PathTokenizer(ReadOnlySpan path)
+{
+ private readonly ReadOnlySpan _path = path;
+ int _index = -1;
+ int _nextSeparatorIndex = -1;
+
+ public readonly Segment Current =>
+ new (_index, (_nextSeparatorIndex == -1 ? _path.Length : _nextSeparatorIndex) - _index);
+
+ public bool MoveNext()
+ {
+ if (_index != -1 && _nextSeparatorIndex == -1)
+ {
+ return false;
+ }
+
+ _index = _nextSeparatorIndex + 1;
+ _nextSeparatorIndex = GetSeparator();
+ return true;
+ }
+
+ internal SegmentCollection Fill(List segments)
+ {
+ while (MoveNext())
+ {
+ if (Current.Length > 0 &&
+ !_path.Slice(Current.Start, Current.Length).Equals(".".AsSpan(), StringComparison.Ordinal) &&
+ !_path.Slice(Current.Start, Current.Length).Equals("..".AsSpan(), StringComparison.Ordinal))
+ {
+ segments.Add(Current);
+ }
+ }
+
+ return new SegmentCollection(_path, segments);
+ }
+
+ private readonly int GetSeparator() => _path.Slice(_index).IndexOfAny(OSPath.DirectoryPathSeparators.Span) switch
+ {
+ -1 => -1,
+ var index => index + _index
+ };
+
+ public struct Segment(int start, int length)
+ {
+ public int Start { get; set; } = start;
+ public int Length { get; set; } = length;
+ }
+
+ public readonly ref struct SegmentCollection(ReadOnlySpan path, List segments)
+ {
+ private readonly ReadOnlySpan _path = path;
+ private readonly int _index = 0;
+
+ private SegmentCollection(ReadOnlySpan path, List segments, int index) : this(path, segments) =>
+ _index = index;
+
+ public int Count => segments.Count - _index;
+
+ public ReadOnlySpan this[int index] => _path.Slice(segments[index + _index].Start, segments[index + _index].Length);
+
+ public ReadOnlyMemory this[ReadOnlyMemory path, int index] => path.Slice(segments[index + _index].Start, segments[index + _index].Length);
+
+ internal SegmentCollection Slice(int segmentIndex) => new (_path, segments, segmentIndex);
+ }
+}
+#else
+public ref struct PathTokenizer(ReadOnlySpan path)
+{
+ private readonly ReadOnlySpan _path = path;
+
+ public struct Segment(int start, int length)
+ {
+ public int Start { get; set; } = start;
+ public int Length { get; set; } = length;
+ }
+
+ internal SegmentCollection Fill(List segments)
+ {
+ foreach (var range in MemoryExtensions.SplitAny(_path, OSPath.DirectoryPathSeparators.Span))
+ {
+ var length = range.End.Value - range.Start.Value;
+ if (length > 0 &&
+ !_path.Slice(range.Start.Value, length).Equals(".".AsSpan(), StringComparison.Ordinal) &&
+ !_path.Slice(range.Start.Value, length).Equals("..".AsSpan(), StringComparison.Ordinal))
+ {
+ segments.Add(new(range.Start.Value, length));
+ }
+ }
+
+ return new SegmentCollection(_path, segments);
+ }
+
+ public readonly ref struct SegmentCollection(ReadOnlySpan path, List segments)
+ {
+ private readonly ReadOnlySpan _path = path;
+ private readonly int _index = 0;
+
+ private SegmentCollection(ReadOnlySpan path, List segments, int index) : this(path, segments) =>
+ _index = index;
+
+ public int Count => segments.Count - _index;
+
+ public ReadOnlySpan this[int index] => _path.Slice(segments[index + _index].Start, segments[index + _index].Length);
+
+ public ReadOnlyMemory this[ReadOnlyMemory path, int index] => path.Slice(segments[index + _index].Start, segments[index + _index].Length);
+
+ internal SegmentCollection Slice(int segmentIndex) => new(_path, segments, segmentIndex);
+ }
+}
+#endif
diff --git a/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/StaticWebAssetGlobMatcher.cs b/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/StaticWebAssetGlobMatcher.cs
new file mode 100644
index 000000000000..18ea7fad699f
--- /dev/null
+++ b/src/StaticWebAssetsSdk/Tasks/Utils/Globbing/StaticWebAssetGlobMatcher.cs
@@ -0,0 +1,551 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+
+namespace Microsoft.AspNetCore.StaticWebAssets.Tasks;
+
+public class StaticWebAssetGlobMatcher(GlobNode includes, GlobNode excludes)
+{
+ // For testing only
+ internal GlobMatch Match(string path)
+ {
+ var context = CreateMatchContext();
+ context.SetPathAndReinitialize(path);
+ return Match(context);
+ }
+
+ public GlobMatch Match(MatchContext context)
+ {
+ var stateStack = context.MatchStates;
+ var tokenizer = new PathTokenizer(context.Path);
+ var segments = tokenizer.Fill(context.Segments);
+ if (segments.Count == 0)
+ {
+ return new(false, string.Empty);
+ }
+
+ if (excludes != null)
+ {
+ var excluded = MatchCore(excludes, segments, stateStack);
+ if (excluded.IsMatch)
+ {
+ return new(false, null);
+ }
+ }
+
+ return MatchCore(includes, segments, stateStack);
+ }
+
+ private static GlobMatch MatchCore(GlobNode includes, PathTokenizer.SegmentCollection segments, Stack stateStack)
+ {
+ stateStack.Push(new(includes));
+ while (stateStack.Count > 0)
+ {
+ var state = stateStack.Pop();
+ var stage = state.Stage;
+ var currentIndex = state.SegmentIndex;
+ var node = state.Node;
+
+ switch (stage)
+ {
+ case MatchStage.Done:
+ if (currentIndex == segments.Count)
+ {
+ if (node.Match != null)
+ {
+ var stem = ComputeStem(segments, state.StemStartIndex);
+ return new(true, node.Match, stem);
+ }
+
+ // We got to the end with no matches, pop the next element on the stack.
+ continue;
+ }
+ break;
+ case MatchStage.Literal:
+ if (currentIndex == segments.Count)
+ {
+ // We ran out of segments to match
+ continue;
+ }
+ PushNextStageIfAvailable(stateStack, state);
+ MatchLiteral(segments, stateStack, state);
+ break;
+ case MatchStage.Extension:
+ if (currentIndex == segments.Count)
+ {
+ // We ran out of segments to match
+ continue;
+ }
+ PushNextStageIfAvailable(stateStack, state);
+ MatchExtension(segments, stateStack, state);
+ break;
+ case MatchStage.Complex:
+ if (currentIndex == segments.Count)
+ {
+ // We ran out of segments to match
+ continue;
+ }
+ PushNextStageIfAvailable(stateStack, state);
+ MatchComplex(segments, stateStack, state);
+ break;
+ case MatchStage.WildCard:
+ if (currentIndex == segments.Count)
+ {
+ // We ran out of segments to match
+ continue;
+ }
+ PushNextStageIfAvailable(stateStack, state);
+ MatchWildCard(stateStack, state);
+ break;
+ case MatchStage.RecursiveWildCard:
+ MatchRecursiveWildCard(segments, stateStack, state);
+ break;
+ }
+ }
+
+ return new(false, null);
+ }
+
+ private static string ComputeStem(PathTokenizer.SegmentCollection segments, int stemStartIndex)
+ {
+ if (stemStartIndex == -1)
+ {
+ return segments[segments.Count - 1].ToString();
+ }
+#if NET9_0_OR_GREATER
+ var stemLength = 0;
+ for (var i = stemStartIndex; i < segments.Count; i++)
+ {
+ stemLength += segments[i].Length;
+ }
+ // Separators
+ stemLength += segments.Count - stemStartIndex - 1;
+
+ return string.Create(stemLength, segments.Slice(stemStartIndex), (span, segments) =>
+ {
+ var index = 0;
+ for (var i = 0; i < segments.Count; i++)
+ {
+ var segment = segments[i];
+ segment.CopyTo(span.Slice(index));
+ index += segment.Length;
+ if (i < segments.Count - 1)
+ {
+ span[index++] = '/';
+ }
+ }
+ });
+#else
+ var stem = new StringBuilder();
+ for (var i = stemStartIndex; i < segments.Count; i++)
+ {
+ stem.Append(segments[i].ToString());
+ if (i < segments.Count - 1)
+ {
+ stem.Append('/');
+ }
+ }
+ return stem.ToString();
+#endif
+ }
+
+ private static void MatchComplex(PathTokenizer.SegmentCollection segments, Stack stateStack, MatchState state)
+ {
+ // We need to try all the complex segments until we find one that matches or we run out of segments to try.
+ // If we find a match for the current segment, we need to make sure that the rest of the segments match the remainder of the pattern.
+ // For that reason, if we find a match, we need to push a state that will try the next complex segment in the list (if any) and one
+ // state that will try the next segment in the current match, so that if for some reason the rest of the pattern doesn't match, we can
+ // continue trying the rest of the complex segments.
+ var complexSegmentIndex = state.ComplexSegmentIndex;
+ var currentIndex = state.SegmentIndex;
+ var node = state.Node;
+ var segment = segments[currentIndex];
+ var complexSegment = node.ComplexGlobSegments[complexSegmentIndex];
+ var parts = complexSegment.Parts;
+
+ if (TryMatchParts(segment, parts))
+ {
+ // We have a match for the current segment
+ if (complexSegmentIndex + 1 < node.ComplexGlobSegments.Count)
+ {
+ // Push a state that will try the next complex segment
+ stateStack.Push(state.NextComplex());
+ }
+
+ // Push a state to try the remainder of the segments
+ stateStack.Push(state.NextSegment(complexSegment.Node));
+ }
+ }
+
+ private static bool TryMatchParts(ReadOnlySpan span, List parts, int index = 0, int partIndex = 0)
+ {
+ for (var i = partIndex; i < parts.Count; i++)
+ {
+ if (index > span.Length)
+ {
+ // No more characters to consume but we still have parts to process
+ return false;
+ }
+
+ var part = parts[i];
+ switch (part.Kind)
+ {
+ case GlobSegmentPartKind.Literal:
+ if (!span.Slice(index).StartsWith(part.Value.Span, StringComparison.OrdinalIgnoreCase))
+ {
+ // Literal didn't match
+ return false;
+ }
+ index += part.Value.Length;
+ break;
+ case GlobSegmentPartKind.QuestionMark:
+ index++;
+ break;
+ case GlobSegmentPartKind.WildCard:
+ // Wildcards require trying to match 0 or more characters, so we need to try matching the rest of the parts after
+ // having consumed 0, 1, 2, ... characters and so on.
+ // Instead of jumping 0, 1, 2, etc, we are going to calculate the next step by finding the next literal on the list.
+ // If we find another * we can discard the current one.
+ // If we find one or moe '?' we can require that at least as many characters as '?' are consumed.
+ // When we find a literal, we can try to find the index of the literal in the remaining string, and if we find it, we can
+ // try to match the rest of the parts, jumping ahead after the literal.
+ // If we happen to not find a literal, we have a match (trailing *) or at most we can require that there are N characters
+ // left in the string, where N is the number of '?' in the remaining parts.
+ var minimumCharactersToConsume = 0;
+ for (var j = i + 1; j < parts.Count; j++)
+ {
+ var nextPart = parts[j];
+ switch (nextPart.Kind)
+ {
+ case GlobSegmentPartKind.Literal:
+ // Start searching after the current index + the minimum characters to consume
+ var remainingSpan = span.Slice(index + minimumCharactersToConsume);
+ var nextLiteralIndex = remainingSpan.IndexOf(nextPart.Value.Span, StringComparison.OrdinalIgnoreCase);
+ while (nextLiteralIndex != -1)
+ {
+ // Consume the characters before the literal and the literal itself before we try
+ // to match the rest of the parts.
+ remainingSpan = remainingSpan.Slice(nextLiteralIndex + nextPart.Value.Length);
+
+ if (remainingSpan.Length == 0 && j == parts.Count - 1)
+ {
+ // We were looking at the last literal, so we have a match
+ return true;
+ }
+
+ if (!TryMatchParts(remainingSpan, parts, 0, j + 1))
+ {
+ // If we couldn't match the rest of the parts, try the next literal
+ nextLiteralIndex = remainingSpan.IndexOf(nextPart.Value.Span, StringComparison.OrdinalIgnoreCase);
+ }
+ else
+ {
+ return true;
+ }
+ }
+ // At this point we couldn't match the next literal, in the list, so this pattern is not a match
+ return false;
+ case GlobSegmentPartKind.QuestionMark:
+ minimumCharactersToConsume++;
+ break;
+ case GlobSegmentPartKind.WildCard:
+ // Ignore any wildcard that comes right after the original one
+ break;
+ }
+ }
+
+ // There were no trailing literals, so we have a match if there are at least as many characters as '?' in the remaining parts
+ return index + minimumCharactersToConsume <= span.Length;
+ }
+ }
+
+ return index == span.Length;
+ }
+
+ private static void MatchRecursiveWildCard(PathTokenizer.SegmentCollection segments, Stack stateStack, MatchState state)
+ {
+ var node = state.Node;
+ for (var i = segments.Count - state.SegmentIndex; i >= 0; i--)
+ {
+ var nextSegment = state.NextSegment(node.RecursiveWildCard, i);
+ // The stem is calculated as the first time the /**/ pattern is matched til the remainder of the path, otherwise, the stem is
+ // the file name.
+ if (nextSegment.StemStartIndex == -1)
+ {
+ nextSegment.StemStartIndex = state.SegmentIndex;
+ }
+
+ stateStack.Push(nextSegment);
+ }
+ }
+
+ private static void MatchWildCard(Stack stateStack, MatchState state)
+ {
+ // A wildcard matches any segment, so we can continue with the next
+ stateStack.Push(state.NextSegment(state.Node.WildCard));
+ }
+
+ private static void MatchExtension(PathTokenizer.SegmentCollection segments, Stack stateStack, MatchState state)
+ {
+ var node = state.Node;
+ var currentIndex = state.SegmentIndex;
+ var extensionIndex = state.ExtensionSegmentIndex;
+ var segment = segments[currentIndex];
+ if (extensionIndex >= segment.Length)
+ {
+ // We couldn't find any path that matched the extensions we have
+ return;
+ }
+
+ // We start from something.else.txt matching.else.txt and then .txt
+ var remaining = segment.Slice(extensionIndex);
+ var indexOfDot = remaining.IndexOf('.');
+ if (indexOfDot != -1)
+ {
+ if (TryMatchExtension(node, remaining.Slice(indexOfDot), out var extensionCandidate))
+ {
+ stateStack.Push(state.NextSegment(extensionCandidate));
+ }
+ else
+ {
+ // If we fail to match, try and match the next extension.
+ stateStack.Push(state.NextExtension(extensionIndex + indexOfDot + 1));
+ }
+ }
+ }
+
+ private static void MatchLiteral(PathTokenizer.SegmentCollection segments, Stack stateStack, MatchState state)
+ {
+ var currentIndex = state.SegmentIndex;
+ var node = state.Node;
+ // Push the next stage to the stack so we can continue searching in case we don't match the entire path
+ PushNextStageIfAvailable(stateStack, state);
+ if (TryMatchLiteral(node, segments[currentIndex], out var literalCandidate))
+ {
+ // Push the found node to the stack to match the remaining path segments
+ stateStack.Push(state.NextSegment(literalCandidate));
+ }
+ }
+
+ private static void PushNextStageIfAvailable(Stack stateStack, MatchState state)
+ {
+ if (state.ExtensionSegmentIndex == 0 && state.ComplexSegmentIndex == 0)
+ {
+ var nextStage = state.NextStage();
+ if (nextStage.HasValue)
+ {
+ stateStack.Push(nextStage);
+ }
+ }
+ }
+
+ [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")]
+ internal struct MatchState(GlobNode node, MatchStage stage, int segmentIndex, int extensionSegmentIndex, int complexSegmentIndex)
+ {
+ public MatchState(GlobNode node) : this(node, GetInitialStage(node), 0, 0, 0) { }
+
+ public GlobNode Node { get; set; } = node;
+
+ public MatchStage Stage { get; set; } = stage;
+
+ // Index on the list of segments for the path
+ public int SegmentIndex { get; set; } = segmentIndex;
+
+ public int ExtensionSegmentIndex { get; set; } = extensionSegmentIndex;
+
+ public int ComplexSegmentIndex { get; set; } = complexSegmentIndex;
+
+ public int StemStartIndex { get; set; } = -1;
+
+ internal readonly bool HasValue => Node != null;
+
+ public readonly void Deconstruct(out GlobNode node, out MatchStage stage, out int segmentIndex, out int extensionIndex, out int complexIndex)
+ {
+ node = Node;
+ stage = Stage;
+ segmentIndex = SegmentIndex;
+ extensionIndex = ExtensionSegmentIndex;
+ complexIndex = ComplexSegmentIndex;
+ }
+
+ internal MatchState NextSegment(GlobNode candidate, int elements = 1, int complexIndex = 0) =>
+ new(candidate, GetInitialStage(candidate), SegmentIndex + elements, 0, complexIndex) { StemStartIndex = StemStartIndex };
+
+ internal MatchState NextStage()
+ {
+ switch (Stage)
+ {
+ case MatchStage.Literal:
+ if (Node.HasExtensions())
+ {
+ return new(Node, MatchStage.Extension, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+
+ if (Node.ComplexGlobSegments != null && Node.ComplexGlobSegments.Count > 0)
+ {
+ return new(Node, MatchStage.Complex, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+
+ if (Node.WildCard != null)
+ {
+ return new(Node, MatchStage.WildCard, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+
+ if (Node.RecursiveWildCard != null)
+ {
+ return new(Node, MatchStage.RecursiveWildCard, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+ break;
+ case MatchStage.Extension:
+ if (Node.ComplexGlobSegments != null && Node.ComplexGlobSegments.Count > 0)
+ {
+ return new(Node, MatchStage.Complex, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+
+ if (Node.WildCard != null)
+ {
+ return new(Node, MatchStage.WildCard, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+
+ if (Node.RecursiveWildCard != null)
+ {
+ return new(Node, MatchStage.RecursiveWildCard, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+ break;
+ case MatchStage.Complex:
+ if (Node.WildCard != null)
+ {
+ return new(Node, MatchStage.WildCard, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+ if (Node.RecursiveWildCard != null)
+ {
+ return new(Node, MatchStage.RecursiveWildCard, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+ break;
+ case MatchStage.WildCard:
+ if (Node.RecursiveWildCard != null)
+ {
+ return new(Node, MatchStage.RecursiveWildCard, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+ break;
+ case MatchStage.RecursiveWildCard:
+ return new(Node, MatchStage.Done, SegmentIndex, 0, 0)
+ { StemStartIndex = StemStartIndex };
+ }
+
+ return default;
+ }
+
+ private static MatchStage GetInitialStage(GlobNode node)
+ {
+ if (node.HasLiterals())
+ {
+ return MatchStage.Literal;
+ }
+
+ if (node.HasExtensions())
+ {
+ return MatchStage.Extension;
+ }
+
+ if (node.ComplexGlobSegments != null && node.ComplexGlobSegments.Count > 0)
+ {
+ return MatchStage.Complex;
+ }
+
+ if (node.WildCard != null)
+ {
+ return MatchStage.WildCard;
+ }
+
+ if (node.RecursiveWildCard != null)
+ {
+ return MatchStage.RecursiveWildCard;
+ }
+
+ return MatchStage.Done;
+ }
+
+ internal readonly MatchState NextExtension(int extensionIndex) => new(Node, MatchStage.Extension, SegmentIndex, extensionIndex, ComplexSegmentIndex);
+
+ internal readonly MatchState NextComplex() => new(Node, MatchStage.Complex, SegmentIndex, ExtensionSegmentIndex, ComplexSegmentIndex + 1);
+
+ private readonly string GetDebuggerDisplay()
+ {
+ return $"Node: {Node}, Stage: {Stage}, SegmentIndex: {SegmentIndex}, ExtensionIndex: {ExtensionSegmentIndex}, ComplexSegmentIndex: {ComplexSegmentIndex}";
+ }
+
+ }
+
+ internal enum MatchStage
+ {
+ Done,
+ Literal,
+ Extension,
+ Complex,
+ WildCard,
+ RecursiveWildCard
+ }
+
+ private static bool TryMatchExtension(GlobNode node, ReadOnlySpan extension, out GlobNode extensionCandidate) =>
+#if NET9_0_OR_GREATER
+ node.Extensions.TryGetValue(extension, out extensionCandidate);
+#else
+ node.Extensions.TryGetValue(extension.ToString(), out extensionCandidate);
+#endif
+
+ private static bool TryMatchLiteral(GlobNode node, ReadOnlySpan current, out GlobNode nextNode) =>
+#if NET9_0_OR_GREATER
+ node.Literals.TryGetValue(current, out nextNode);
+#else
+ node.Literals.TryGetValue(current.ToString(), out nextNode);
+#endif
+
+ // The matchContext holds all the state for the underlying matching algorithm.
+ // It is reused so that we avoid allocating memory for each match.
+ // It is not thread-safe and should not be shared across threads.
+ public static MatchContext CreateMatchContext() => new();
+
+ public ref struct MatchContext()
+ {
+ public ReadOnlySpan Path;
+ public string PathString;
+
+ internal List Segments { get; set; } = [];
+ internal Stack MatchStates { get; set; } = [];
+
+ public void SetPathAndReinitialize(string path)
+ {
+ PathString = path;
+ Path = path.AsSpan();
+ Segments.Clear();
+ MatchStates.Clear();
+ }
+
+ public void SetPathAndReinitialize(ReadOnlySpan path)
+ {
+ Path = path;
+ Segments.Clear();
+ MatchStates.Clear();
+ }
+
+ public void SetPathAndReinitialize(ReadOnlyMemory