Create WireMock.Net.MimePart project (#1300)

* Create WireMock.Net.MimePart project

* .

* REFACTOR

* ILRepack

* --

* ...

* x

* x

* .

* fix

* public class MimePartMatcher

* shared

* min

* .

* <!--<DelaySign>true</DelaySign>-->

* Update README.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Stef Heyenrath
2025-05-24 12:17:42 +02:00
committed by GitHub
parent c15206ecd8
commit 96eca4262a
306 changed files with 9746 additions and 9285 deletions

View File

@@ -0,0 +1,95 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
namespace WireMock.Util;
/// <summary>
/// A special Collection that overrides methods of <see cref="ObservableCollection{T}"/> to make them thread safe.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
/// <inheritdoc cref="ObservableCollection{T}" />
internal class ConcurrentObservableCollection<T> : ObservableCollection<T>
{
private readonly object _lockObject = new object();
/// <summary>
/// Initializes a new instance of the <see cref="T:WireMock.Util.ConcurrentObservableCollection`1" /> class.
/// </summary>
public ConcurrentObservableCollection() { }
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentObservableCollection{T}"/> class that contains elements copied from the specified list.
/// </summary>
/// <param name="list">The list from which the elements are copied.</param>
public ConcurrentObservableCollection(List<T> list) : base(list) { }
/// <summary>
/// Initializes a new instance of the <see cref="ConcurrentObservableCollection{T}"/> class that contains elements copied from the specified collection.
/// </summary>
/// <param name="collection">The collection from which the elements are copied.</param>
public ConcurrentObservableCollection(IEnumerable<T> collection) : base(collection) { }
/// <inheritdoc cref="ObservableCollection{T}.ClearItems"/>
protected override void ClearItems()
{
lock (_lockObject)
{
base.ClearItems();
}
}
/// <inheritdoc cref="ObservableCollection{T}.RemoveItem"/>
protected override void RemoveItem(int index)
{
lock (_lockObject)
{
base.RemoveItem(index);
}
}
/// <inheritdoc cref="ObservableCollection{T}.InsertItem"/>
protected override void InsertItem(int index, T item)
{
lock (_lockObject)
{
base.InsertItem(index, item);
}
}
/// <inheritdoc cref="ObservableCollection{T}.SetItem"/>
protected override void SetItem(int index, T item)
{
lock (_lockObject)
{
base.SetItem(index, item);
}
}
/// <inheritdoc cref="ObservableCollection{T}.MoveItem"/>
protected override void MoveItem(int oldIndex, int newIndex)
{
lock (_lockObject)
{
base.MoveItem(oldIndex, newIndex);
}
}
public List<T> ToList()
{
lock (_lockObject)
{
return Items.ToList();
}
}
public T[] ToArray()
{
lock (_lockObject)
{
return Items.ToArray();
}
}
}

View File

@@ -0,0 +1,44 @@
// Copyright © WireMock.Net
using System;
using System.Globalization;
namespace WireMock.Util;
internal static class CultureInfoUtils
{
public static readonly CultureInfo CultureInfoEnUS = new("en-US");
public static CultureInfo Parse(string? value)
{
if (value is null)
{
return CultureInfo.CurrentCulture;
}
try
{
#if !NETSTANDARD1_3
if (int.TryParse(value, out var culture))
{
return new CultureInfo(culture);
}
#endif
if (string.Equals(value, nameof(CultureInfo.CurrentCulture), StringComparison.OrdinalIgnoreCase))
{
return CultureInfo.CurrentCulture;
}
if (string.Equals(value, nameof(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase))
{
return CultureInfo.InvariantCulture;
}
return new CultureInfo(value);
}
catch
{
return CultureInfo.CurrentCulture;
}
}
}

View File

@@ -0,0 +1,15 @@
// Copyright © WireMock.Net
using System;
namespace WireMock.Util;
internal interface IDateTimeUtils
{
DateTime UtcNow { get; }
}
internal class DateTimeUtils : IDateTimeUtils
{
public DateTime UtcNow => DateTime.UtcNow;
}

View File

@@ -0,0 +1,34 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using Stef.Validation;
namespace WireMock.Util;
/// <summary>
/// Some IDictionary Extensions
/// </summary>
public static class DictionaryExtensions
{
/// <summary>
/// Loops the dictionary and executes the specified action.
/// </summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TValue">The type of the value.</typeparam>
/// <param name="dictionary">The dictionary to loop (can be null).</param>
/// <param name="action">The action.</param>
public static void Loop<TKey, TValue>(this IDictionary<TKey, TValue>? dictionary, Action<TKey, TValue> action)
where TKey : notnull
{
Guard.NotNull(action);
if (dictionary != null)
{
foreach (var entry in dictionary)
{
action(entry.Key, entry.Value);
}
}
}
}

View File

@@ -0,0 +1,255 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Concurrent;
using System.IO;
using JetBrains.Annotations;
using Stef.Validation;
namespace WireMock.Util;
/// <summary>
/// An EnhancedFileSystemWatcher, which can be used to suppress duplicate events that fire on a single change to the file.
/// </summary>
/// <seealso cref="FileSystemWatcher" />
public class EnhancedFileSystemWatcher : FileSystemWatcher
{
#region Private Members
// Default Watch Interval in Milliseconds
private const int DefaultWatchInterval = 100;
// This Dictionary keeps the track of when an event occurred last for a particular file
private ConcurrentDictionary<string, DateTime> _lastFileEvent = new();
// Watch Interval in Milliseconds
private int _interval;
// Timespan created when interval is set
private TimeSpan _recentTimeSpan;
#endregion
#region Public Properties
/// <summary>
/// Interval, in milliseconds, within which events are considered "recent".
/// </summary>
[PublicAPI]
public int Interval
{
get => _interval;
set
{
_interval = value;
// Set timespan based on the value passed
_recentTimeSpan = new TimeSpan(0, 0, 0, 0, value);
}
}
/// <summary>
/// Allows user to set whether to filter recent events.
/// If this is set a false, this class behaves like System.IO.FileSystemWatcher class.
/// </summary>
[PublicAPI]
public bool FilterRecentEvents { get; set; }
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="EnhancedFileSystemWatcher"/> class.
/// </summary>
/// <param name="interval">The interval.</param>
public EnhancedFileSystemWatcher(int interval = DefaultWatchInterval)
{
Guard.Condition(interval, i => i >= 0);
InitializeMembers(interval);
}
/// <summary>
/// Initializes a new instance of the <see cref="EnhancedFileSystemWatcher"/> class.
/// </summary>
/// <param name="path">The directory to monitor, in standard or Universal Naming Convention (UNC) notation.</param>
/// <param name="interval">The interval.</param>
public EnhancedFileSystemWatcher(string path, int interval = DefaultWatchInterval) : base(path)
{
Guard.NotNullOrEmpty(path);
Guard.Condition(interval, i => i >= 0);
InitializeMembers(interval);
}
/// <summary>
/// Initializes a new instance of the <see cref="EnhancedFileSystemWatcher"/> class.
/// </summary>
/// <param name="path">The directory to monitor, in standard or Universal Naming Convention (UNC) notation.</param>
/// <param name="filter">The type of files to watch. For example, "*.txt" watches for changes to all text files.</param>
/// <param name="interval">The interval.</param>
public EnhancedFileSystemWatcher(string path, string filter, int interval = DefaultWatchInterval) : base(path, filter)
{
Guard.NotNullOrEmpty(path);
Guard.NotNullOrEmpty(filter);
Guard.Condition(interval, i => i >= 0);
InitializeMembers(interval);
}
#endregion
#region Events
// These events hide the events from the base class.
// We want to raise these events appropriately and we do not want the
// users of this class subscribing to these events of the base class accidentally
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is changed.
/// </summary>
public new event FileSystemEventHandler? Changed;
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is created.
/// </summary>
public new event FileSystemEventHandler? Created;
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is deleted.
/// </summary>
public new event FileSystemEventHandler? Deleted;
/// <summary>
/// Occurs when a file or directory in the specified <see cref="P:System.IO.FileSystemWatcher.Path" /> is renamed.
/// </summary>
public new event RenamedEventHandler? Renamed;
#endregion
#region Protected Methods to raise the Events for this class
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Changed" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.FileSystemEventArgs" /> that contains the event data.</param>
protected new virtual void OnChanged(FileSystemEventArgs e)
{
Changed?.Invoke(this, e);
}
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Created" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.FileSystemEventArgs" /> that contains the event data.</param>
protected new virtual void OnCreated(FileSystemEventArgs e)
{
Created?.Invoke(this, e);
}
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Deleted" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.FileSystemEventArgs" /> that contains the event data.</param>
protected new virtual void OnDeleted(FileSystemEventArgs e)
{
Deleted?.Invoke(this, e);
}
/// <summary>
/// Raises the <see cref="E:System.IO.FileSystemWatcher.Renamed" /> event.
/// </summary>
/// <param name="e">A <see cref="T:System.IO.RenamedEventArgs" /> that contains the event data.</param>
protected new virtual void OnRenamed(RenamedEventArgs e)
{
Renamed?.Invoke(this, e);
}
#endregion
#region Private Methods
/// <summary>
/// This Method Initializes the private members.
/// Interval is set to its default value of 100 millisecond.
/// FilterRecentEvents is set to true, _lastFileEvent dictionary is initialized.
/// We subscribe to the base class events.
/// </summary>
private void InitializeMembers(int interval = 100)
{
Interval = interval;
FilterRecentEvents = true;
_lastFileEvent = new ConcurrentDictionary<string, DateTime>();
base.Created += OnCreated;
base.Changed += OnChanged;
base.Deleted += OnDeleted;
base.Renamed += OnRenamed;
}
/// <summary>
/// This method searches the dictionary to find out when the last event occurred
/// for a particular file. If that event occurred within the specified timespan
/// it returns true, else false
/// </summary>
/// <param name="fileName">The filename to be checked</param>
/// <returns>True if an event has occurred within the specified interval, False otherwise</returns>
private bool HasAnotherFileEventOccurredRecently(string fileName)
{
// Check dictionary only if user wants to filter recent events otherwise return value stays false.
if (!FilterRecentEvents)
{
return false;
}
bool retVal = false;
if (_lastFileEvent.ContainsKey(fileName))
{
// If dictionary contains the filename, check how much time has elapsed
// since the last event occurred. If the timespan is less that the
// specified interval, set return value to true
// and store current datetime in dictionary for this file
DateTime lastEventTime = _lastFileEvent[fileName];
DateTime currentTime = DateTime.Now;
TimeSpan timeSinceLastEvent = currentTime - lastEventTime;
retVal = timeSinceLastEvent < _recentTimeSpan;
_lastFileEvent[fileName] = currentTime;
}
else
{
// If dictionary does not contain the filename,
// no event has occurred in past for this file, so set return value to false
// and append filename along with current datetime to the dictionary
_lastFileEvent.TryAdd(fileName, DateTime.Now);
}
return retVal;
}
#region FileSystemWatcher EventHandlers
// Base class Event Handlers. Check if an event has occurred recently and call method
// to raise appropriate event only if no recent event is detected
private void OnChanged(object sender, FileSystemEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.FullPath))
{
OnChanged(e);
}
}
private void OnCreated(object sender, FileSystemEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.FullPath))
{
OnCreated(e);
}
}
private void OnDeleted(object sender, FileSystemEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.FullPath))
{
OnDeleted(e);
}
}
private void OnRenamed(object sender, RenamedEventArgs e)
{
if (!HasAnotherFileEventOccurredRecently(e.OldFullPath))
{
OnRenamed(e);
}
}
#endregion
#endregion
}

View File

@@ -0,0 +1,37 @@
// Copyright © WireMock.Net
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Stef.Validation;
using WireMock.Handlers;
namespace WireMock.Util;
internal static class FileHelper
{
private const int NumberOfRetries = 3;
private const int DelayOnRetry = 500;
public static bool TryReadMappingFileWithRetryAndDelay(IFileSystemHandler handler, string path, [NotNullWhen(true)] out string? value)
{
Guard.NotNull(handler);
Guard.NotNullOrEmpty(path);
value = null;
for (int i = 1; i <= NumberOfRetries; ++i)
{
try
{
value = handler.ReadMappingFile(path);
return true;
}
catch
{
Thread.Sleep(DelayOnRetry);
}
}
return false;
}
}

View File

@@ -0,0 +1,86 @@
// Copyright © WireMock.Net
using System.IO;
using Stef.Validation;
namespace WireMock.Util;
internal static class FilePathUtils
{
/// <summary>
/// Robust handling of the user defined path.
/// Also supports Unix and Windows platforms
/// </summary>
/// <param name="path">The path to clean</param>
public static string? CleanPath(string? path)
{
return path?.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar);
}
/// <summary>
/// Removes leading directory separator chars from the filepath, which could break Path.Combine
/// </summary>
/// <param name="path">The path to remove the loading DirectorySeparatorChars</param>
public static string? RemoveLeadingDirectorySeparators(string? path)
{
return path?.TrimStart(Path.DirectorySeparatorChar);
}
/// <summary>
/// Combine two paths
/// </summary>
/// <param name="root">The root path</param>
/// <param name="path">The path</param>
public static string Combine(string root, string? path)
{
Guard.NotNull(root);
var result = RemoveLeadingDirectorySeparators(path);
return result == null ? root : Path.Combine(root, result);
}
/// <summary>
/// Returns a relative path from one path to another.
/// </summary>
/// <param name="relativeTo">The source path the result should be relative to. This path is always considered to be a directory..</param>
/// <param name="path">The destination path.</param>
/// <returns>The relative path, or path if the paths don't share the same root.</returns>
public static string GetRelativePath(string relativeTo, string path)
{
#if NETCOREAPP3_1 || NET5_0_OR_GREATER || NETSTANDARD2_1
return Path.GetRelativePath(relativeTo, path);
#else
Guard.NotNull(relativeTo);
Guard.NotNull(path);
static string AppendDirectorySeparatorChar(string path)
{
// Append a slash only if the path is a directory and does not have a slash.
if (!Path.HasExtension(path) && !path.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
return path + Path.DirectorySeparatorChar;
}
return path;
}
var fromUri = new System.Uri(AppendDirectorySeparatorChar(relativeTo));
var toUri = new System.Uri(AppendDirectorySeparatorChar(path));
if (fromUri.Scheme != toUri.Scheme)
{
return path;
}
var relativeUri = fromUri.MakeRelativeUri(toUri);
var relativePath = System.Uri.UnescapeDataString(relativeUri.ToString());
if (string.Equals(toUri.Scheme, "FILE", System.StringComparison.OrdinalIgnoreCase))
{
relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
}
return relativePath;
#endif
}
}

View File

@@ -0,0 +1,18 @@
// Copyright © WireMock.Net
using System;
namespace WireMock.Util;
internal interface IGuidUtils
{
Guid NewGuid();
}
internal class GuidUtils : IGuidUtils
{
public Guid NewGuid()
{
return Guid.NewGuid();
}
}

View File

@@ -0,0 +1,88 @@
// Copyright © WireMock.Net
using System;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
namespace WireMock.Util;
/// <summary>
/// Based on https://github.com/tmenier/Flurl/blob/129565361e135e639f1d44a35a78aea4302ac6ca/src/Flurl.Http/HttpStatusRangeParser.cs
/// </summary>
internal static class HttpStatusRangeParser
{
/// <summary>
/// Determines whether the specified pattern is match.
/// </summary>
/// <param name="pattern">The pattern. (Can be null, in that case it's allowed.)</param>
/// <param name="httpStatusCode">The value.</param>
/// <exception cref="ArgumentException"><paramref name="pattern"/> is invalid.</exception>
public static bool IsMatch(string? pattern, object? httpStatusCode)
{
return httpStatusCode switch
{
int statusCodeAsInteger => IsMatch(pattern, statusCodeAsInteger),
string statusCodeAsString => IsMatch(pattern, int.Parse(statusCodeAsString)),
_ => false
};
}
/// <summary>
/// Determines whether the specified pattern is match.
/// </summary>
/// <param name="pattern">The pattern. (Can be null, in that case it's allowed.)</param>
/// <param name="httpStatusCode">The value.</param>
/// <exception cref="ArgumentException"><paramref name="pattern"/> is invalid.</exception>
public static bool IsMatch(string pattern, HttpStatusCode httpStatusCode)
{
return IsMatch(pattern, (int)httpStatusCode);
}
/// <summary>
/// Determines whether the specified pattern is match.
/// </summary>
/// <param name="pattern">The pattern. (Can be null, in that case it's allowed.)</param>
/// <param name="httpStatusCode">The value.</param>
/// <exception cref="ArgumentException"><paramref name="pattern"/> is invalid.</exception>
public static bool IsMatch(string? pattern, int httpStatusCode)
{
if (pattern == null)
{
return true;
}
foreach (var range in pattern.Split(',').Select(p => p.Trim()))
{
switch (range)
{
case "":
continue;
case "*":
return true; // special case - allow everything
}
string[] bounds = range.Split('-');
int lower = 0;
int upper = 0;
bool valid =
bounds.Length <= 2 &&
int.TryParse(Regex.Replace(bounds.First().Trim(), "[*xX]", "0"), out lower) &&
int.TryParse(Regex.Replace(bounds.Last().Trim(), "[*xX]", "9"), out upper);
if (!valid)
{
throw new ArgumentException($"Invalid range pattern: \"{pattern}\". Examples of allowed patterns: \"400\", \"4xx\", \"300,400-403\", \"*\".");
}
if (httpStatusCode >= lower && httpStatusCode <= upper)
{
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,26 @@
// Copyright © WireMock.Net
using System.Text.RegularExpressions;
using Stef.Validation;
using WireMock.Constants;
namespace WireMock.Util;
/// <summary>
/// https://en.wikipedia.org/wiki/HTTP
/// </summary>
internal static class HttpVersionParser
{
private static readonly Regex HttpVersionRegex = new(@"HTTP/(\d+(\.\d+)?(?!\.))", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, RegexConstants.DefaultTimeout);
/// <summary>
/// Try to extract the version (as a string) from the protocol.
/// </summary>
/// <param name="protocol">The protocol, something like "HTTP/1.1" or "HTTP/2".</param>
/// <returns>The version ("1.1" or "2") if found and valid, else empty string.</returns>
internal static string Parse(string protocol)
{
var match = HttpVersionRegex.Match(Guard.NotNull(protocol));
return match.Success ? match.Groups[1].Value : string.Empty;
}
}

View File

@@ -0,0 +1,110 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using WireMock.Constants;
namespace WireMock.Util;
/// <summary>
/// Port Utility class
/// </summary>
internal static class PortUtils
{
private static readonly Regex UrlDetailsRegex = new(@"^((?<proto>\w+)://)(?<host>[^/]+?):(?<port>\d+)\/?$", RegexOptions.Compiled, RegexConstants.DefaultTimeout);
/// <summary>
/// Finds a random, free port to be listened on.
/// </summary>
/// <returns>A random, free port to be listened on.</returns>
/// <remarks>https://github.com/SeleniumHQ/selenium/blob/trunk/dotnet/src/webdriver/Internal/PortUtilities.cs</remarks>
public static int FindFreeTcpPort()
{
// Locate a free port on the local machine by binding a socket to an IPEndPoint using IPAddress.Any and port 0.
// The socket will select a free port.
var portSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
var socketEndPoint = new IPEndPoint(IPAddress.Any, 0);
portSocket.Bind(socketEndPoint);
socketEndPoint = (IPEndPoint)portSocket.LocalEndPoint!;
return socketEndPoint.Port;
}
finally
{
#if !NETSTANDARD1_3
portSocket.Close();
#endif
portSocket.Dispose();
}
}
/// <summary>
/// Finds a specified number of random, free ports to be listened on.
/// </summary>
/// <param name="count">The number of free ports to find.</param>
/// <returns>A list of random, free ports to be listened on.</returns>
public static IReadOnlyList<int> FindFreeTcpPorts(int count)
{
var sockets = Enumerable
.Range(0, count)
.Select(_ => new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
.ToArray();
var freePorts = new List<int>();
try
{
foreach (var socket in sockets)
{
var socketEndPoint = new IPEndPoint(IPAddress.Any, 0);
socket.Bind(socketEndPoint);
socketEndPoint = (IPEndPoint)socket.LocalEndPoint!;
freePorts.Add(socketEndPoint.Port);
}
return freePorts;
}
finally
{
foreach (var socket in sockets)
{
#if !NETSTANDARD1_3
socket.Close();
#endif
socket.Dispose();
}
}
}
/// <summary>
/// Extract the isHttps, isHttp2, protocol, host and port from a URL.
/// </summary>
public static bool TryExtract(string url, out bool isHttps, out bool isHttp2, [NotNullWhen(true)] out string? protocol, [NotNullWhen(true)] out string? host, out int port)
{
isHttps = false;
isHttp2 = false;
protocol = null;
host = null;
port = 0;
var match = UrlDetailsRegex.Match(url);
if (match.Success)
{
protocol = match.Groups["proto"].Value;
isHttps = protocol.StartsWith("https", StringComparison.OrdinalIgnoreCase) || protocol.StartsWith("grpcs", StringComparison.OrdinalIgnoreCase);
isHttp2 = protocol.StartsWith("grpc", StringComparison.OrdinalIgnoreCase);
host = match.Groups["host"].Value;
return int.TryParse(match.Groups["port"].Value, out port);
}
return false;
}
}

View File

@@ -0,0 +1,37 @@
// Copyright © WireMock.Net
#if PROTOBUF
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using JsonConverter.Abstractions;
using ProtoBufJsonConverter;
using ProtoBufJsonConverter.Models;
namespace WireMock.Util;
internal static class ProtoBufUtils
{
internal static async Task<byte[]> GetProtoBufMessageWithHeaderAsync(
IReadOnlyList<string>? protoDefinitions,
string? messageType,
object? value,
IJsonConverter? jsonConverter = null,
CancellationToken cancellationToken = default
)
{
if (protoDefinitions == null || string.IsNullOrWhiteSpace(messageType) || value is null)
{
return [];
}
var resolver = new WireMockProtoFileResolver(protoDefinitions);
var request = new ConvertToProtoBufRequest(protoDefinitions[0], messageType!, value, true)
.WithProtoFileResolver(resolver);
return await SingletonFactory<Converter>
.GetInstance()
.ConvertAsync(request, cancellationToken).ConfigureAwait(false);
}
}
#endif

View File

@@ -0,0 +1,101 @@
// Copyright © WireMock.Net
#if PROTOBUF
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using ProtoBufJsonConverter;
using ProtoBufJsonConverter.Models;
using Stef.Validation;
using WireMock.Models;
using WireMock.Settings;
namespace WireMock.Util;
/// <summary>
/// Some helper methods for Proto Definitions.
/// </summary>
public static class ProtoDefinitionHelper
{
/// <summary>
/// Builds a dictionary of ProtoDefinitions from a directory.
/// - The key will be the filename without extension.
/// - The value will be the ProtoDefinition with an extra comment with the relative path to each <c>.proto</c> file so it can be used by the WireMockProtoFileResolver.
/// </summary>
/// <param name="directory">The directory to start from.</param>
/// <param name="cancellationToken">The token to monitor for cancellation requests. The default value is <c>System.Threading.CancellationToken.None</c>.</param>
public static async Task<ProtoDefinitionData> FromDirectory(string directory, CancellationToken cancellationToken = default)
{
Guard.NotNullOrEmpty(directory);
var fileNameMappedToProtoDefinition = new Dictionary<string, string>();
var filePaths = Directory.EnumerateFiles(directory, "*.proto", SearchOption.AllDirectories);
foreach (var filePath in filePaths)
{
// Get the relative path to the directory (note that this will be OS specific).
var relativePath = FilePathUtils.GetRelativePath(directory, filePath);
// Make it a valid proto import path
var protoRelativePath = relativePath.Replace(Path.DirectorySeparatorChar, '/');
// Build comment and get content from file.
var comment = $"// {protoRelativePath}";
#if NETSTANDARD2_0
var content = File.ReadAllText(filePath);
#else
var content = await File.ReadAllTextAsync(filePath, cancellationToken);
#endif
// Only add the comment if it's not already defined.
var modifiedContent = !content.StartsWith(comment) ? $"{comment}\n{content}" : content;
var key = Path.GetFileNameWithoutExtension(filePath);
fileNameMappedToProtoDefinition.Add(key, modifiedContent);
}
var converter = SingletonFactory<Converter>.GetInstance();
var resolver = new WireMockProtoFileResolver(fileNameMappedToProtoDefinition.Values);
var messageTypeMappedToWithProtoDefinition = new Dictionary<string, string>();
foreach (var protoDefinition in fileNameMappedToProtoDefinition.Values)
{
var infoRequest = new GetInformationRequest(protoDefinition, resolver);
try
{
var info = await converter.GetInformationAsync(infoRequest, cancellationToken);
foreach (var messageType in info.MessageTypes)
{
messageTypeMappedToWithProtoDefinition[messageType.Key] = protoDefinition;
}
}
catch
{
// Ignore
}
}
return new ProtoDefinitionData(fileNameMappedToProtoDefinition);
}
internal static IdOrTexts GetIdOrTexts(WireMockServerSettings settings, params string[] protoDefinitionOrId)
{
switch (protoDefinitionOrId.Length)
{
case 1:
var idOrText = protoDefinitionOrId[0];
if (settings.ProtoDefinitions?.TryGetValue(idOrText, out var protoDefinitions) == true)
{
return new(idOrText, protoDefinitions);
}
return new(null, protoDefinitionOrId);
default:
return new(null, protoDefinitionOrId);
}
}
}
#endif

View File

@@ -0,0 +1,50 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
namespace WireMock.Util;
internal static class ReflectionUtils
{
private const string DynamicModuleName = "WireMockDynamicModule";
private static readonly AssemblyName AssemblyName = new("WireMockDynamicAssembly");
private const TypeAttributes ClassAttributes =
TypeAttributes.Public |
TypeAttributes.Class |
TypeAttributes.AutoClass |
TypeAttributes.AnsiClass |
TypeAttributes.BeforeFieldInit |
TypeAttributes.AutoLayout;
private static readonly ConcurrentDictionary<string, Type> TypeCache = new();
public static Type CreateType(string typeName, Type? parentType = null)
{
return TypeCache.GetOrAdd(typeName, key =>
{
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule(DynamicModuleName);
var typeBuilder = moduleBuilder.DefineType(key, ClassAttributes, parentType);
// Create the type and cache it
return typeBuilder.CreateTypeInfo()!.AsType();
});
}
public static Type CreateGenericType(string typeName, Type genericTypeDefinition, params Type[] typeArguments)
{
var genericKey = $"{typeName}_{genericTypeDefinition.Name}_{string.Join(", ", typeArguments.Select(t => t.Name))}";
return TypeCache.GetOrAdd(genericKey, _ =>
{
var genericType = genericTypeDefinition.MakeGenericType(typeArguments);
// Create the type based on the genericType and cache it
return CreateType(typeName, genericType);
});
}
}

View File

@@ -0,0 +1,51 @@
// Copyright © WireMock.Net
using System.Collections.Generic;
using System.Text.RegularExpressions;
using WireMock.Constants;
using WireMock.RegularExpressions;
namespace WireMock.Util;
internal static class RegexUtils
{
public static Dictionary<string, string> GetNamedGroups(Regex regex, string input)
{
var namedGroupsDictionary = new Dictionary<string, string>();
GroupCollection groups = regex.Match(input).Groups;
foreach (string groupName in regex.GetGroupNames())
{
if (groups[groupName].Captures.Count > 0)
{
namedGroupsDictionary.Add(groupName, groups[groupName].Value);
}
}
return namedGroupsDictionary;
}
public static (bool IsValid, bool Result) MatchRegex(string? pattern, string input, bool useRegexExtended = true)
{
if (string.IsNullOrEmpty(pattern))
{
return (false, false);
}
try
{
if (useRegexExtended)
{
var regexExtended = new RegexExtended(pattern!, RegexOptions.None, RegexConstants.DefaultTimeout);
return (true, regexExtended.IsMatch(input));
}
var regex = new Regex(pattern, RegexOptions.None, RegexConstants.DefaultTimeout);
return (true, regex.IsMatch(input));
}
catch
{
return (false, false);
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright © WireMock.Net
namespace WireMock.Util;
internal static class SingletonLock
{
public static readonly object Lock = new();
}
internal static class SingletonFactory<T> where T : class, new()
{
private static T? _instance;
public static T GetInstance()
{
if (_instance == null)
{
lock (SingletonLock.Lock)
{
_instance ??= new T();
}
}
return _instance;
}
}

View File

@@ -0,0 +1,14 @@
// Copyright © WireMock.Net
using System.IO;
using System.Text;
namespace WireMock.Util;
internal static class StreamUtils
{
public static Stream CreateStream(string s)
{
return new MemoryStream(Encoding.UTF8.GetBytes(s));
}
}

View File

@@ -0,0 +1,95 @@
// Copyright © WireMock.Net
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.RegularExpressions;
using WireMock.Matchers;
namespace WireMock.Util;
internal static class StringUtils
{
private static readonly string[] ValidUriSchemes =
[
"ftp://",
"http://",
"https://"
];
private static readonly Func<string, (bool IsConverted, object ConvertedValue)>[] ConversionsFunctions =
[
s => bool.TryParse(s, out var result) ? (true, result) : (false, s),
s => int.TryParse(s, out var result) ? (true, result) : (false, s),
s => long.TryParse(s, out var result) ? (true, result) : (false, s),
s => double.TryParse(s, out var result) ? (true, result) : (false, s),
s => Guid.TryParseExact(s, "D", out var result) ? (true, result) : (false, s),
s => TimeSpan.TryParse(s, out var result) ? (true, result) : (false, s),
s => DateTime.TryParse(s, out var result) ? (true, result) : (false, s),
s =>
{
if (ValidUriSchemes.Any(u => s.StartsWith(u, StringComparison.OrdinalIgnoreCase)) &&
Uri.TryCreate(s, UriKind.RelativeOrAbsolute, out var uri))
{
return (true, uri);
}
return (false, s);
}
];
public static (bool IsConverted, object ConvertedValue) TryConvertToKnownType(string value)
{
foreach (var func in ConversionsFunctions)
{
var result = func(value);
if (result.IsConverted)
{
return result;
}
}
return (false, value);
}
public static MatchOperator ParseMatchOperator(string? value)
{
return value != null && Enum.TryParse<MatchOperator>(value, out var matchOperator)
? matchOperator
: MatchOperator.Or;
}
public static bool TryParseQuotedString(string? value, [NotNullWhen(true)] out string? result, out char quote)
{
result = null;
quote = '\0';
if (value == null || value.Length < 2)
{
return false;
}
quote = value[0]; // This can be single or a double quote
if (quote != '"' && quote != '\'')
{
return false;
}
if (value.Last() != quote)
{
return false;
}
try
{
result = Regex.Unescape(value.Substring(1, value.Length - 2));
return true;
}
catch
{
// Ignore Exception, just continue and return false.
}
return false;
}
}

View File

@@ -0,0 +1,10 @@
// Copyright © WireMock.Net
using System.Reflection;
namespace WireMock.Util;
internal static class SystemUtils
{
public static readonly string Version = typeof(SystemUtils).GetTypeInfo().Assembly.GetName().Version!.ToString();
}

View File

@@ -0,0 +1,56 @@
// Copyright © WireMock.Net
using Nelibur.ObjectMapper;
using WireMock.Admin.Mappings;
using WireMock.Admin.Settings;
using WireMock.Settings;
namespace WireMock.Util;
internal sealed class TinyMapperUtils
{
public static TinyMapperUtils Instance { get; } = new();
private TinyMapperUtils()
{
TinyMapper.Bind<ProxyAndRecordSettings, ProxyAndRecordSettingsModel>();
TinyMapper.Bind<WebProxySettings, WebProxySettingsModel>();
TinyMapper.Bind<WebProxySettings, WebProxyModel>();
TinyMapper.Bind<ProxyUrlReplaceSettings, ProxyUrlReplaceSettingsModel>();
TinyMapper.Bind<ProxyAndRecordSettingsModel, ProxyAndRecordSettings>();
TinyMapper.Bind<WebProxySettingsModel, WebProxySettings>();
TinyMapper.Bind<WebProxyModel, WebProxySettings>();
TinyMapper.Bind<ProxyUrlReplaceSettingsModel, ProxyUrlReplaceSettings>();
}
public ProxyAndRecordSettingsModel? Map(ProxyAndRecordSettings? instance)
{
return instance == null ? null : TinyMapper.Map<ProxyAndRecordSettingsModel>(instance);
}
public ProxyAndRecordSettings? Map(ProxyAndRecordSettingsModel? model)
{
return model == null ? null : TinyMapper.Map<ProxyAndRecordSettings>(model);
}
public ProxyUrlReplaceSettingsModel? Map(ProxyUrlReplaceSettings? instance)
{
return instance == null ? null : TinyMapper.Map<ProxyUrlReplaceSettingsModel>(instance);
}
public ProxyUrlReplaceSettings? Map(ProxyUrlReplaceSettingsModel? model)
{
return model == null ? null : TinyMapper.Map<ProxyUrlReplaceSettings>(model);
}
public WebProxyModel? Map(WebProxySettings? instance)
{
return instance == null ? null : TinyMapper.Map<WebProxyModel>(instance);
}
public WebProxySettings? Map(WebProxyModel? model)
{
return model == null ? null : TinyMapper.Map<WebProxySettings>(model);
}
}

View File

@@ -0,0 +1,120 @@
// Copyright © WireMock.Net
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using Stef.Validation;
namespace WireMock.Util;
internal static class TypeLoader
{
private static readonly ConcurrentDictionary<string, Type> Assemblies = new();
private static readonly ConcurrentDictionary<Type, object> Instances = new();
public static TInterface LoadNewInstance<TInterface>(params object?[] args) where TInterface : class
{
var pluginType = GetPluginType<TInterface>();
return (TInterface)Activator.CreateInstance(pluginType, args)!;
}
public static TInterface LoadStaticInstance<TInterface>(params object?[] args) where TInterface : class
{
var pluginType = GetPluginType<TInterface>();
return (TInterface)Instances.GetOrAdd(pluginType, key => Activator.CreateInstance(key, args)!);
}
public static TInterface LoadNewInstanceByFullName<TInterface>(string implementationTypeFullName, params object?[] args) where TInterface : class
{
Guard.NotNullOrEmpty(implementationTypeFullName);
var pluginType = GetPluginTypeByFullName<TInterface>(implementationTypeFullName);
return (TInterface)Activator.CreateInstance(pluginType, args)!;
}
public static TInterface LoadStaticInstanceByFullName<TInterface>(string implementationTypeFullName, params object?[] args) where TInterface : class
{
Guard.NotNullOrEmpty(implementationTypeFullName);
var pluginType = GetPluginTypeByFullName<TInterface>(implementationTypeFullName);
return (TInterface)Instances.GetOrAdd(pluginType, key => Activator.CreateInstance(key, args)!);
}
private static Type GetPluginType<TInterface>() where TInterface : class
{
var key = typeof(TInterface).FullName!;
var pluginType = Assemblies.GetOrAdd(key, _ =>
{
if (TryFindTypeInDlls<TInterface>(null, out var foundType))
{
return foundType;
}
throw new DllNotFoundException($"No dll found which implements interface '{key}'.");
});
return pluginType;
}
private static Type GetPluginTypeByFullName<TInterface>(string implementationTypeFullName) where TInterface : class
{
var @interface = typeof(TInterface).FullName;
var key = $"{@interface}_{implementationTypeFullName}";
var pluginType = Assemblies.GetOrAdd(key, _ =>
{
if (TryFindTypeInDlls<TInterface>(implementationTypeFullName, out var foundType))
{
return foundType;
}
throw new DllNotFoundException($"No dll found which implements Interface '{@interface}' and has FullName '{implementationTypeFullName}'.");
});
return pluginType;
}
private static bool TryFindTypeInDlls<TInterface>(string? implementationTypeFullName, [NotNullWhen(true)] out Type? pluginType) where TInterface : class
{
foreach (var file in Directory.GetFiles(Directory.GetCurrentDirectory(), "*.dll"))
{
try
{
var assembly = Assembly.Load(new AssemblyName
{
Name = Path.GetFileNameWithoutExtension(file)
});
if (TryGetImplementationTypeByInterfaceAndOptionalFullName<TInterface>(assembly, implementationTypeFullName, out pluginType))
{
return true;
}
}
catch
{
// no-op: just try next .dll
}
}
pluginType = null;
return false;
}
private static bool TryGetImplementationTypeByInterfaceAndOptionalFullName<T>(Assembly assembly, string? implementationTypeFullName, [NotNullWhen(true)] out Type? type)
{
type = assembly
.GetTypes()
.FirstOrDefault(t =>
typeof(T).IsAssignableFrom(t) && !t.GetTypeInfo().IsInterface &&
(implementationTypeFullName == null || string.Equals(t.FullName, implementationTypeFullName, StringComparison.OrdinalIgnoreCase))
);
return type != null;
}
}

View File

@@ -0,0 +1,41 @@
// Copyright © WireMock.Net
using System;
using WireMock.Models;
using Stef.Validation;
#if !USE_ASPNETCORE
using Microsoft.Owin;
#else
using Microsoft.AspNetCore.Http;
#endif
namespace WireMock.Util;
internal static class UrlUtils
{
public static UrlDetails Parse(Uri uri, PathString pathBase)
{
Guard.NotNull(uri);
if (!pathBase.HasValue)
{
return new UrlDetails(uri, uri);
}
var builder = new UriBuilder(uri);
builder.Path = RemoveFirst(builder.Path, pathBase.Value);
return new UrlDetails(uri, builder.Uri);
}
private static string RemoveFirst(string text, string search)
{
int pos = text.IndexOf(search, StringComparison.Ordinal);
if (pos < 0)
{
return text;
}
return text.Substring(0, pos) + text.Substring(pos + search.Length);
}
}

View File

@@ -0,0 +1,75 @@
// Copyright © WireMock.Net
#if PROTOBUF
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using ProtoBufJsonConverter;
using Stef.Validation;
using WireMock.Extensions;
namespace WireMock.Util;
/// <summary>
/// This resolver is used to resolve the extra ProtoDefinition files.
///
/// It assumes that:
/// - The first commented line of each ProtoDefinition file is the filepath which is used in the import of the other ProtoDefinition file(s).
/// </summary>
internal class WireMockProtoFileResolver : IProtoFileResolver
{
private readonly Dictionary<string, string> _files = [];
public WireMockProtoFileResolver(IReadOnlyCollection<string> protoDefinitions)
{
if (Guard.NotNullOrEmpty(protoDefinitions).Count() <= 1)
{
return;
}
foreach (var extraProtoDefinition in protoDefinitions)
{
var firstNonEmptyLine = extraProtoDefinition.Split(['\r', '\n']).FirstOrDefault(l => !string.IsNullOrEmpty(l));
if (firstNonEmptyLine != null)
{
if (TryGetValidPath(firstNonEmptyLine.TrimStart(['/', ' ']), out var validPath))
{
_files.Add(validPath, extraProtoDefinition);
}
else
{
_files.Add(extraProtoDefinition.GetDeterministicHashCodeAsString(), extraProtoDefinition);
}
}
}
}
public bool Exists(string path)
{
return _files.ContainsKey(path);
}
public TextReader OpenText(string path)
{
if (_files.TryGetValue(path, out var extraProtoDefinition))
{
return new StringReader(extraProtoDefinition);
}
throw new FileNotFoundException($"The ProtoDefinition '{path}' was not found.");
}
private static bool TryGetValidPath(string path, [NotNullWhen(true)] out string? validPath)
{
if (!path.Any(c => Path.GetInvalidPathChars().Contains(c)))
{
validPath = path;
return true;
}
validPath = null;
return false;
}
}
#endif