// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using Microsoft.ClearScript.Util;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace Microsoft.ClearScript
{
///
/// Represents a document loader.
///
public abstract class DocumentLoader
{
// ReSharper disable EmptyConstructor
///
/// Initializes a new instance.
///
protected DocumentLoader()
{
// the help file builder (SHFB) insists on an empty constructor here
}
// ReSharper restore EmptyConstructor
///
/// Gets the default document loader.
///
public static DocumentLoader Default => DefaultImpl.Instance;
///
/// Gets or sets the maximum size of the document loader's cache.
///
///
/// This property specifies the maximum number of documents to be cached by the document
/// loader. For the default document loader, its initial value is 1024.
///
///
public virtual uint MaxCacheSize
{
get => 0;
set => throw new NotSupportedException("Loader does not support caching");
}
///
/// Loads a document.
///
/// Document access settings for the operation.
/// An optional structure containing meta-information for the requesting document.
/// A string specifying the document to be loaded.
/// An optional category for the requested document.
/// An optional context callback for the requested document.
/// A instance that represents the loaded document.
///
/// A loaded document must have an absolute URI. Once a
/// load operation has completed successfully, subsequent requests that resolve to the same
/// URI are expected to return the same reference, although loaders
/// are not required to manage document caches of unlimited size.
///
public virtual Document LoadDocument(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier, DocumentCategory category, DocumentContextCallback contextCallback)
{
MiscHelpers.VerifyNonBlankArgument(specifier, nameof(specifier), "Invalid document specifier");
try
{
return LoadDocumentAsync(settings, sourceInfo, specifier, category, contextCallback).Result;
}
catch (AggregateException exception)
{
exception = exception.Flatten();
if (exception.InnerExceptions.Count == 1)
{
throw new FileLoadException(null, specifier, exception.InnerExceptions[0]);
}
throw new FileLoadException(null, specifier, exception);
}
}
///
/// Loads a document asynchronously.
///
/// Document access settings for the operation.
/// An optional structure containing meta-information for the requesting document.
/// A string specifying the document to be loaded.
/// An optional category for the requested document.
/// An optional context callback for the requested document.
/// A task that represents the asynchronous operation. Upon completion, the task's result is a instance that represents the loaded document.
///
/// A loaded document must have an absolute URI. Once a
/// load operation has completed successfully, subsequent requests that resolve to the same
/// URI are expected to return the same reference, although loaders
/// are not required to manage document caches of unlimited size.
///
public abstract Task LoadDocumentAsync(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier, DocumentCategory category, DocumentContextCallback contextCallback);
///
/// Discards all cached documents.
///
public virtual void DiscardCachedDocuments()
{
}
#region Nested type: DefaultImpl
private sealed class DefaultImpl : DocumentLoader
{
public static readonly DefaultImpl Instance = new DefaultImpl();
private static readonly IReadOnlyCollection relativePrefixes = new List
{
"." + Path.DirectorySeparatorChar,
"." + Path.AltDirectorySeparatorChar,
".." + Path.DirectorySeparatorChar,
".." + Path.AltDirectorySeparatorChar,
};
private readonly List cache = new List();
private DefaultImpl()
{
MaxCacheSize = 1024;
}
private static async Task> GetCandidateUrisAsync(DocumentSettings settings, DocumentInfo? sourceInfo, Uri uri)
{
var candidateUris = new List();
if (string.IsNullOrWhiteSpace(settings.FileNameExtensions))
{
candidateUris.Add(uri);
}
else
{
foreach (var testUri in ApplyFileNameExtensions(sourceInfo, uri, settings.FileNameExtensions))
{
if (await IsCandidateUriAsync(settings, testUri).ConfigureAwait(false))
{
candidateUris.Add(testUri);
}
}
}
return candidateUris;
}
private static async Task> GetCandidateUrisAsync(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier)
{
var candidateUris = new List();
var rawUris = GetRawUris(settings, sourceInfo, specifier).Distinct();
if (!string.IsNullOrWhiteSpace(settings.FileNameExtensions))
{
rawUris = rawUris.SelectMany(uri => ApplyFileNameExtensions(sourceInfo, uri, settings.FileNameExtensions));
}
foreach (var testUri in rawUris)
{
if (await IsCandidateUriAsync(settings, testUri).ConfigureAwait(false))
{
candidateUris.Add(testUri);
}
}
return candidateUris;
}
private static IEnumerable GetRawUris(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier)
{
Uri baseUri;
Uri uri;
if (sourceInfo.HasValue && SpecifierMayBeRelative(settings, specifier))
{
baseUri = GetBaseUri(sourceInfo.Value);
if ((baseUri != null) && Uri.TryCreate(baseUri, specifier, out uri))
{
yield return uri;
}
}
var searchPath = settings.SearchPath;
if (!string.IsNullOrWhiteSpace(searchPath))
{
foreach (var url in searchPath.SplitSearchPath())
{
if (Uri.TryCreate(url, UriKind.Absolute, out baseUri) && TryCombineSearchUri(baseUri, specifier, out uri))
{
yield return uri;
}
}
}
if (MiscHelpers.Try(out var path, () => Path.Combine(Directory.GetCurrentDirectory(), specifier)) && Uri.TryCreate(path, UriKind.Absolute, out uri))
{
yield return uri;
}
if (MiscHelpers.Try(out path, () => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, specifier)) && Uri.TryCreate(path, UriKind.Absolute, out uri))
{
yield return uri;
}
using (var process = Process.GetCurrentProcess())
{
var module = process.MainModule;
if ((module != null) && Uri.TryCreate(module.FileName, UriKind.Absolute, out baseUri) && Uri.TryCreate(baseUri, specifier, out uri))
{
yield return uri;
}
}
}
private static IEnumerable ApplyFileNameExtensions(DocumentInfo? sourceInfo, Uri uri, string fileNameExtensions)
{
yield return uri;
var builder = new UriBuilder(uri);
var path = builder.Path;
if (!string.IsNullOrEmpty(Path.GetFileName(path)) && !Path.HasExtension(path))
{
string sourceFileNameExtension = null;
if (sourceInfo.HasValue)
{
sourceFileNameExtension = Path.GetExtension((sourceInfo.Value.Uri != null) ? new UriBuilder(sourceInfo.Value.Uri).Path : sourceInfo.Value.Name);
if (!string.IsNullOrEmpty(sourceFileNameExtension))
{
builder.Path = Path.ChangeExtension(path, sourceFileNameExtension);
yield return builder.Uri;
}
}
foreach (var fileNameExtension in fileNameExtensions.SplitSearchPath())
{
var testFileNameExtension = fileNameExtension.StartsWith(".", StringComparison.Ordinal) ? fileNameExtension : "." + fileNameExtension;
if (!testFileNameExtension.Equals(sourceFileNameExtension, StringComparison.OrdinalIgnoreCase))
{
builder.Path = Path.ChangeExtension(path, testFileNameExtension);
yield return builder.Uri;
}
}
}
}
private static bool SpecifierMayBeRelative(DocumentSettings settings, string specifier)
{
return !settings.AccessFlags.HasFlag(DocumentAccessFlags.EnforceRelativePrefix) || relativePrefixes.Any(specifier.StartsWith);
}
private static Uri GetBaseUri(DocumentInfo sourceInfo)
{
var sourceUri = sourceInfo.Uri;
if ((sourceUri == null) && !Uri.TryCreate(sourceInfo.Name, UriKind.RelativeOrAbsolute, out sourceUri))
{
return null;
}
if (!sourceUri.IsAbsoluteUri)
{
return null;
}
return sourceUri;
}
private static bool TryCombineSearchUri(Uri searchUri, string specifier, out Uri uri)
{
var searchUrl = searchUri.AbsoluteUri;
if (!searchUrl.EndsWith("/", StringComparison.Ordinal))
{
searchUri = new Uri(searchUrl + "/");
}
return Uri.TryCreate(searchUri, specifier, out uri);
}
private static async Task IsCandidateUriAsync(DocumentSettings settings, Uri uri)
{
return uri.IsFile ?
settings.AccessFlags.HasFlag(DocumentAccessFlags.EnableFileLoading) && File.Exists(uri.LocalPath) :
settings.AccessFlags.HasFlag(DocumentAccessFlags.EnableWebLoading) && await WebDocumentExistsAsync(uri).ConfigureAwait(false);
}
private static async Task WebDocumentExistsAsync(Uri uri)
{
using (var client = new HttpClient())
{
using (var request = new HttpRequestMessage(HttpMethod.Head, uri))
{
using (var response = await client.SendAsync(request).ConfigureAwait(false))
{
return response.IsSuccessStatusCode;
}
}
}
}
private async Task LoadDocumentAsync(DocumentSettings settings, Uri uri, DocumentCategory category, DocumentContextCallback contextCallback)
{
var cachedDocument = GetCachedDocument(uri);
if (cachedDocument != null)
{
return cachedDocument;
}
string contents;
var flags = settings.AccessFlags;
if (uri.IsFile)
{
if (!flags.HasFlag(DocumentAccessFlags.EnableFileLoading))
{
throw new UnauthorizedAccessException("The script engine is not configured for loading documents from the file system");
}
using (var reader = new StreamReader(uri.LocalPath))
{
contents = await reader.ReadToEndAsync().ConfigureAwait(false);
}
}
else
{
if (!flags.HasFlag(DocumentAccessFlags.EnableWebLoading))
{
throw new UnauthorizedAccessException("The script engine is not configured for downloading documents from the Web");
}
using (var client = new WebClient())
{
contents = await client.DownloadStringTaskAsync(uri).ConfigureAwait(false);
}
}
var documentInfo = new DocumentInfo(uri) { Category = category, ContextCallback = contextCallback };
var callback = settings.LoadCallback;
callback?.Invoke(ref documentInfo);
return CacheDocument(new StringDocument(documentInfo, contents));
}
private Document GetCachedDocument(Uri uri)
{
lock (cache)
{
for (var index = 0; index < cache.Count; index++)
{
var cachedDocument = cache[index];
if (cachedDocument.Info.Uri == uri)
{
cache.RemoveAt(index);
cache.Insert(0, cachedDocument);
return cachedDocument;
}
}
return null;
}
}
private Document CacheDocument(Document document)
{
lock (cache)
{
var cachedDocument = cache.FirstOrDefault(testDocument => testDocument.Info.Uri == document.Info.Uri);
if (cachedDocument != null)
{
Debug.Assert(cachedDocument.Contents.ReadToEnd().SequenceEqual(document.Contents.ReadToEnd()));
return cachedDocument;
}
var maxCacheSize = Math.Max(16, Convert.ToInt32(Math.Min(MaxCacheSize, int.MaxValue)));
while (cache.Count >= maxCacheSize)
{
cache.RemoveAt(cache.Count - 1);
}
cache.Insert(0, document);
return document;
}
}
#region DocumentLoader overrides
public override uint MaxCacheSize { get; set; }
public override async Task LoadDocumentAsync(DocumentSettings settings, DocumentInfo? sourceInfo, string specifier, DocumentCategory category, DocumentContextCallback contextCallback)
{
MiscHelpers.VerifyNonNullArgument(settings, nameof(settings));
MiscHelpers.VerifyNonBlankArgument(specifier, nameof(specifier), "Invalid document specifier");
if ((settings.AccessFlags & DocumentAccessFlags.EnableAllLoading) == DocumentAccessFlags.None)
{
throw new UnauthorizedAccessException("The script engine is not configured for loading documents");
}
if (category == null)
{
category = sourceInfo.HasValue ? sourceInfo.Value.Category : DocumentCategory.Script;
}
List candidateUris;
if (Uri.TryCreate(specifier, UriKind.RelativeOrAbsolute, out var uri) && uri.IsAbsoluteUri)
{
candidateUris = await GetCandidateUrisAsync(settings, sourceInfo, uri).ConfigureAwait(false);
}
else
{
candidateUris = await GetCandidateUrisAsync(settings, sourceInfo, specifier).ConfigureAwait(false);
}
if (candidateUris.Count < 1)
{
throw new FileNotFoundException(null, specifier);
}
if (candidateUris.Count == 1)
{
return await LoadDocumentAsync(settings, candidateUris[0], category, contextCallback).ConfigureAwait(false);
}
var exceptions = new List(candidateUris.Count);
foreach (var candidateUri in candidateUris)
{
var task = LoadDocumentAsync(settings, candidateUri, category, contextCallback);
try
{
return await task.ConfigureAwait(false);
}
catch (Exception exception)
{
if ((task.Exception != null) && task.Exception.InnerExceptions.Count == 1)
{
Debug.Assert(ReferenceEquals(task.Exception.InnerExceptions[0], exception));
exceptions.Add(exception);
}
else
{
exceptions.Add(task.Exception);
}
}
}
if (exceptions.Count < 1)
{
MiscHelpers.AssertUnreachable();
throw new FileNotFoundException(null, specifier);
}
if (exceptions.Count == 1)
{
MiscHelpers.AssertUnreachable();
throw new FileLoadException(exceptions[0].Message, specifier, exceptions[0]);
}
throw new AggregateException(exceptions).Flatten();
}
public override void DiscardCachedDocuments()
{
lock (cache)
{
cache.Clear();
}
base.DiscardCachedDocuments();
}
#endregion
}
#endregion
}
}