summaryrefslogtreecommitdiffstats
path: root/net/shunt/src/match.uc
blob: 165c8b6bbff76cac9c404bf7b3c42afd1dc488ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// shunt - domain matcher
//
// Compiles the policies' domain patterns into an exact and a wildcard map
// and answers which policies claim a queried name.
//
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2026 Dirk Brenken <dev@brenken.org>

export const LIM = {
	name: 253,
	label: 63
};

function ok_label(s) {
	if (!length(s) || length(s) > LIM.label)
		return false;

	for (let i = 0; i < length(s); i++) {
		let c = ord(s, i);
		if ((c >= 0x61 && c <= 0x7a) || (c >= 0x30 && c <= 0x39) ||
			c == 0x2d || c == 0x5f)
			continue;
		return false;
	}

	return true;
}

export function normalize(s) {
	s = lc(trim(s ?? ''));

	while (length(s) && substr(s, -1) == '.')
		s = substr(s, 0, length(s) - 1);

	return s;
};

function validate(name) {
	if (!length(name))
		return 'empty';
	if (length(name) > LIM.name)
		return 'too long';

	for (let l in split(name, '.'))
		if (!ok_label(l))
			return `bad label '${l}'`;

	return null;
}

export function compile(policies) {
	let exact = {}, wild = {}, issues = [];

	function reject(policy, pattern, reason) {
		push(issues, { policy, pattern, reason });
	}

	for (let pi = 0; pi < length(policies ?? []); pi++) {
		let p = policies[pi];
		let pname = p?.name ?? `#${pi}`;

		for (let raw in (p?.domains ?? [])) {
			let pat = normalize(raw);
			let is_wild = false;

			if (substr(pat, 0, 2) == '*.') {
				is_wild = true;
				pat = substr(pat, 2);
			}

			if (index(pat, '*') >= 0) {
				reject(pname, raw, 'wildcard only allowed as leading *. label');
				continue;
			}

			let bad = validate(pat);
			if (bad) {
				reject(pname, raw, bad);
				continue;
			}

			let map = is_wild ? wild : exact;

			if (!map[pat])
				map[pat] = [];

			let dup = false;

			for (let owner in map[pat])
				if (owner == pname)
					dup = true;

			if (!dup)
				push(map[pat], pname);
		}
	}

	// A list even for one element - a caller that has to distinguish shapes
	// gets it wrong exactly once, in the rare case, in production.
	function test(qname) {
		let q = normalize(qname);

		if (!length(q) || length(q) > LIM.name)
			return null;

		if (exists(exact, q))
			return exact[q];

		let off = index(q, '.');

		while (off >= 0) {
			let sfx = substr(q, off + 1);

			if (exists(wild, sfx))
				return wild[sfx];

			let nxt = index(sfx, '.');
			off = (nxt < 0) ? -1 : off + 1 + nxt;
		}

		return null;
	}

	return {
		test,
		issues,
		size: { exact: length(keys(exact)), wild: length(keys(wild)) }
	};
};