// Copyright © WireMock.Net using System; using System.Collections.Generic; using System.Linq; using AnyOfTypes; using SimMetrics.Net; using Stef.Validation; using WireMock.Admin.Mappings; using WireMock.Extensions; using WireMock.Matchers; using WireMock.Models; using WireMock.Models.GraphQL; using WireMock.Settings; using WireMock.Util; namespace WireMock.Serialization; internal class MatcherMapper { private readonly WireMockServerSettings _settings; public MatcherMapper(WireMockServerSettings settings) { _settings = Guard.NotNull(settings); } public IMatcher[]? Map(IEnumerable? matchers) { return matchers?.Select(Map).OfType().ToArray(); } public IMatcher? Map(MatcherModel? matcherModel) { if (matcherModel == null) { return null; } string[] parts = matcherModel.Name.Split('.'); string matcherName = parts[0]; string? matcherType = parts.Length > 1 ? parts[1] : null; var stringPatterns = ParseStringPatterns(matcherModel); var matchBehaviour = matcherModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch; var matchOperator = StringUtils.ParseMatchOperator(matcherModel.MatchOperator); bool ignoreCase = matcherModel.IgnoreCase == true; bool useRegexExtended = _settings.UseRegexExtended == true; bool useRegex = matcherModel.Regex == true; switch (matcherName) { case nameof(NotNullOrEmptyMatcher): return new NotNullOrEmptyMatcher(matchBehaviour); case "CSharpCodeMatcher": if (_settings.AllowCSharpCodeMatcher == true) { if (TypeLoader.TryLoadNewInstance(out var csharpCodeMatcher, matchBehaviour, matchOperator, stringPatterns)) { return csharpCodeMatcher; } throw new InvalidOperationException("The 'CSharpCodeMatcher' cannot be loaded. Please install the WireMock.Net.Matchers.CSharpCode package."); } throw new NotSupportedException("It's not allowed to use the 'CSharpCodeMatcher' because WireMockServerSettings.AllowCSharpCodeMatcher is not set to 'true'."); case "LinqMatcher": throw new NotSupportedException("It's not allowed to use the 'LinqMatcher' due to CVE."); //case nameof(LinqMatcher): // return new LinqMatcher(matchBehaviour, matchOperator, stringPatterns); case nameof(ExactMatcher): return new ExactMatcher(matchBehaviour, ignoreCase, matchOperator, stringPatterns); case nameof(ExactObjectMatcher): return CreateExactObjectMatcher(matchBehaviour, stringPatterns[0]); case "GraphQLMatcher": var patternAsString = stringPatterns[0].GetPattern(); var schema = new AnyOf(patternAsString); if (TypeLoader.TryLoadNewInstance(out var graphQLMatcher, schema, matcherModel.CustomScalars, matchBehaviour, matchOperator)) { return graphQLMatcher; } throw new InvalidOperationException("The 'GraphQLMatcher' cannot be loaded. Please install the WireMock.Net.GraphQL package."); case "MimePartMatcher": return CreateMimePartMatcher(matchBehaviour, matcherModel); case "ProtoBufMatcher": return CreateProtoBufMatcher(matchBehaviour, stringPatterns.GetPatterns(), matcherModel); case nameof(RegexMatcher): return new RegexMatcher(matchBehaviour, stringPatterns, ignoreCase, useRegexExtended, matchOperator); case nameof(JsonMatcher): var valueForJsonMatcher = matcherModel.Pattern ?? matcherModel.Patterns; return new JsonMatcher(matchBehaviour, valueForJsonMatcher!, ignoreCase, useRegex); case nameof(JsonPartialMatcher): var valueForJsonPartialMatcher = matcherModel.Pattern ?? matcherModel.Patterns; return new JsonPartialMatcher(matchBehaviour, valueForJsonPartialMatcher!, ignoreCase, useRegex); case nameof(JsonPartialWildcardMatcher): var valueForJsonPartialWildcardMatcher = matcherModel.Pattern ?? matcherModel.Patterns; return new JsonPartialWildcardMatcher(matchBehaviour, valueForJsonPartialWildcardMatcher!, ignoreCase, useRegex); case nameof(JsonPathMatcher): return new JsonPathMatcher(matchBehaviour, matchOperator, stringPatterns); case nameof(JmesPathMatcher): return new JmesPathMatcher(matchBehaviour, matchOperator, stringPatterns); case nameof(XPathMatcher): return new XPathMatcher(matchBehaviour, matchOperator, matcherModel.XmlNamespaceMap, stringPatterns); case nameof(WildcardMatcher): return new WildcardMatcher(matchBehaviour, stringPatterns, ignoreCase, matchOperator); case nameof(ContentTypeMatcher): return new ContentTypeMatcher(matchBehaviour, stringPatterns, ignoreCase); case nameof(FormUrlEncodedMatcher): return new FormUrlEncodedMatcher(matchBehaviour, stringPatterns, ignoreCase); case nameof(SimMetricsMatcher): SimMetricType type = SimMetricType.Levenstein; if (!string.IsNullOrEmpty(matcherType) && !Enum.TryParse(matcherType, out type)) { throw new NotSupportedException($"Matcher '{matcherName}' with Type '{matcherType}' is not supported."); } return new SimMetricsMatcher(matchBehaviour, stringPatterns, type); default: if (_settings.CustomMatcherMappings != null && _settings.CustomMatcherMappings.ContainsKey(matcherName)) { return _settings.CustomMatcherMappings[matcherName](matcherModel); } throw new NotSupportedException($"Matcher '{matcherName}' is not supported."); } } public MatcherModel[]? Map(IEnumerable? matchers, Action? afterMap = null) { return matchers?.Select(m => Map(m, afterMap)).OfType().ToArray(); } public MatcherModel? Map(IMatcher? matcher, Action? afterMap = null) { if (matcher == null) { return null; } bool? ignoreCase = matcher is IIgnoreCaseMatcher ignoreCaseMatcher ? ignoreCaseMatcher.IgnoreCase : null; bool? rejectOnMatch = matcher.MatchBehaviour == MatchBehaviour.RejectOnMatch ? true : null; var model = new MatcherModel { RejectOnMatch = rejectOnMatch, IgnoreCase = ignoreCase, Name = matcher.Name }; switch (matcher) { case JsonMatcher jsonMatcher: model.Regex = jsonMatcher.Regex; break; case XPathMatcher xpathMatcher: model.XmlNamespaceMap = xpathMatcher.XmlNamespaceMap; break; case IGraphQLMatcher graphQLMatcher: model.CustomScalars = graphQLMatcher.CustomScalars; break; } switch (matcher) { // If the matcher is a IStringMatcher, get the operator & patterns. case IStringMatcher stringMatcher: var stringPatterns = stringMatcher.GetPatterns(); if (stringPatterns.Length == 1) { if (stringPatterns[0].IsFirst) { model.Pattern = stringPatterns[0].First; } else { model.Pattern = stringPatterns[0].Second.Pattern; model.PatternAsFile = stringPatterns[0].Second.PatternAsFile; } } else { model.Patterns = stringPatterns.Select(p => p.GetPattern()).Cast().ToArray(); model.MatchOperator = stringMatcher.MatchOperator.ToString(); } break; // If the matcher is a IObjectMatcher, get the value (can be string or object or byte[]). case IObjectMatcher objectMatcher: model.Pattern = objectMatcher.Value; break; case IMimePartMatcher mimePartMatcher: model.ContentDispositionMatcher = Map(mimePartMatcher.ContentDispositionMatcher); model.ContentMatcher = Map(mimePartMatcher.ContentMatcher); model.ContentTransferEncodingMatcher = Map(mimePartMatcher.ContentTransferEncodingMatcher); model.ContentTypeMatcher = Map(mimePartMatcher.ContentTypeMatcher); break; #if PROTOBUF case IProtoBufMatcher protoBufMatcher: protoBufMatcher.ProtoDefinition().Value(id => model.Pattern = id, texts => { if (texts.Count == 1) { model.Pattern = texts[0]; } else if (texts.Count > 1) { model.Patterns = texts.Cast().ToArray(); } }); model.ProtoBufMessageType = protoBufMatcher.MessageType; model.ContentMatcher = Map(protoBufMatcher.Matcher); break; #endif } afterMap?.Invoke(model); return model; } private AnyOf[] ParseStringPatterns(MatcherModel matcher) { if (matcher.Pattern is string patternAsString) { return [new AnyOf(patternAsString)]; } if (matcher.Pattern is IEnumerable patternAsStringArray) { return patternAsStringArray.ToAnyOfPatterns(); } if (matcher.Patterns?.OfType() is { } patternsAsStringArray) { return patternsAsStringArray.ToAnyOfPatterns(); } if (!string.IsNullOrEmpty(matcher.PatternAsFile)) { var patternAsFile = matcher.PatternAsFile!; var pattern = _settings.FileSystemHandler.ReadFileAsString(patternAsFile); return [new AnyOf(new StringPattern { Pattern = pattern, PatternAsFile = patternAsFile })]; } return []; } private static ExactObjectMatcher CreateExactObjectMatcher(MatchBehaviour matchBehaviour, AnyOf stringPattern) { byte[] bytePattern; try { bytePattern = Convert.FromBase64String(stringPattern.GetPattern()); } catch { throw new ArgumentException($"Matcher 'ExactObjectMatcher' has invalid pattern. The pattern value '{stringPattern}' is not a Base64String.", nameof(stringPattern)); } return new ExactObjectMatcher(matchBehaviour, bytePattern); } private IMimePartMatcher CreateMimePartMatcher(MatchBehaviour matchBehaviour, MatcherModel matcher) { var contentTypeMatcher = Map(matcher.ContentTypeMatcher) as IStringMatcher; var contentDispositionMatcher = Map(matcher.ContentDispositionMatcher) as IStringMatcher; var contentTransferEncodingMatcher = Map(matcher.ContentTransferEncodingMatcher) as IStringMatcher; var contentMatcher = Map(matcher.ContentMatcher); if (TypeLoader.TryLoadNewInstance( out var mimePartMatcher, matchBehaviour, contentTypeMatcher, contentDispositionMatcher, contentTransferEncodingMatcher, contentMatcher)) { return mimePartMatcher; } throw new InvalidOperationException("The 'MimePartMatcher' cannot be loaded. Please install the WireMock.Net.MimePart package."); } private IProtoBufMatcher CreateProtoBufMatcher(MatchBehaviour? matchBehaviour, IReadOnlyList protoDefinitions, MatcherModel matcher) { var objectMatcher = Map(matcher.ContentMatcher) as IObjectMatcher; if (TypeLoader.TryLoadNewInstance( out var protobufMatcher, () => ProtoDefinitionUtils.GetIdOrTexts(_settings, protoDefinitions.ToArray()), matcher.ProtoBufMessageType!, matchBehaviour ?? MatchBehaviour.AcceptOnMatch, objectMatcher)) { return protobufMatcher; } throw new InvalidOperationException("The 'ProtoBufMatcher' cannot be loaded. Please install the WireMock.Net.ProtoBuf package."); } }