using System.Data; using System.Diagnostics.CodeAnalysis; namespace SimpleHttpServer.Types; internal class PathTree where T : class { private readonly Node? rootNode = null; public PathTree() : this(new()) { } public PathTree(Dictionary dict) { if (dict == null || dict.Count == 0) return; rootNode = new(); var currNode = rootNode; var unpackedPaths = dict.Keys.Select(p => p.Split('/').ToArray()).ToArray(); var unpackedLeafData = dict.Values.ToArray(); for (int i = 0; i < unpackedPaths.Length; i++) { var path = unpackedPaths[i]; var catchallidx = Array.IndexOf(path, "$*"); if (catchallidx != -1 && catchallidx != path.Length - 1) { throw new Exception($"Found illegal catchall-wildcard in path: '{string.Join('/', path)}'"); } var leafdata = unpackedLeafData[i] ?? throw new ArgumentNullException("Leafdata must not be null!"); rootNode.AddSuccessor(path, leafdata); } } internal bool TryGetPath(string reqPath, [MaybeNullWhen(false)] out T endpoint) { if (rootNode == null) { endpoint = null; return false; } // try to find path-match Node currNode = rootNode; Assert(reqPath[0] == '/'); var splittedPath = reqPath[1..].Split("/"); Node? lastCatchallNode = null; for (int i = 0; i < splittedPath.Length; i++) { // keep track of the current best catchallNode if (currNode.catchAllNext != null) { lastCatchallNode = currNode.catchAllNext; } var seg = splittedPath[i]; if (currNode.next?.TryGetValue(seg, out var next) == true) { // look for an explicit path to follow greedily currNode = next; } else if (currNode.pathWildcardNext != null) { // otherwise look for a single-wildcard to follow currNode = currNode.pathWildcardNext; } else { // otherwise we are done, there is no valid path --> fall back to the most specific catchall endpoint = lastCatchallNode?.leafData; return lastCatchallNode != null; } } // return found path endpoint = currNode.leafData; return endpoint != null; } private class Node { public T? leafData = null; // null means that this is a node without a value (e.g. when it is just part of a path) public Dictionary? next = null; public Node? pathWildcardNext = null; // path wildcard public Node? catchAllNext = null; // trailing-catchall wildcard public void AddSuccessor(string[] segments, T newLeafData) { if (segments.Length == 0) { // actually add the data to this node Assert(leafData == null); leafData = newLeafData; return; } var seg = segments[0]; bool newIsWildcard = seg.Length > 1 && seg[0] == '$'; if (newIsWildcard) { bool newIsCatchallWildcard = newIsWildcard && seg.Length == 2 && seg[1] == '*'; if (newIsCatchallWildcard) { // this is a catchall wildcard Assert(catchAllNext == null); catchAllNext = new(); catchAllNext.AddSuccessor(segments[1..], newLeafData); return; } else { // must be single wildcard otherwise Assert(pathWildcardNext == null); pathWildcardNext = new(); pathWildcardNext.AddSuccessor(segments[1..], newLeafData); return; } } // otherwise we want to add a new constant path successor if (next == null) { next = new(); } if (next.TryGetValue(seg, out var existingNode)) { existingNode.AddSuccessor(segments[1..], newLeafData); } else { var newNode = next[seg] = new(); newNode.AddSuccessor(segments[1..], newLeafData); } } } }