107 lines
4.2 KiB
C#
107 lines
4.2 KiB
C#
using System.Data;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
|
|
namespace SimpleHttpServer.Types;
|
|
|
|
internal class PathTree<T> where T : class {
|
|
private readonly Node? rootNode = null;
|
|
|
|
public PathTree() : this(new()) { }
|
|
public PathTree(Dictionary<string, T> 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<string, Node>? 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);
|
|
}
|
|
}
|
|
}
|
|
} |