rax-radix-tree 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/radix-tree.js ADDED
@@ -0,0 +1,284 @@
1
+ const { RadixTree: RaxRadixTree } = require('./build/Release/rax_radix_tree.node');
2
+ const assert = require('assert');
3
+
4
+ const MATCH_TYPE_EQUALS = 'equals';
5
+ const MATCH_TYPE_PREFIX = 'prefix';
6
+ const MATCH_TYPE_SUFFIX = 'suffix';
7
+
8
+ /**
9
+ * Parse path, path supports several patterns:
10
+ * 1. Without '*', full match
11
+ * 2. With a single '*', '*' matches any text not containing '/'
12
+ * 3. With consecutive '**', '**' matches any text
13
+ * Parsing function, the parsed result includes the following parts:
14
+ * treePath: The part before the first '*'
15
+ * path: The input path
16
+ * matchType: Match type, equals => path without '*'. prefix => path with '*'
17
+ * pathPattern: Pattern used for secondary regex matching
18
+ * @param {string} path
19
+ * @returns {Object} Parsing result
20
+ */
21
+ function parsePath(path) {
22
+ const originPath = path;
23
+ let matchType = MATCH_TYPE_EQUALS;
24
+ let treePath = path;
25
+ let pathPattern = undefined;
26
+
27
+ const starPattern = /(\*\*|\*)/g;
28
+ let parts = undefined;
29
+ if (path.startsWith("*")) {
30
+ matchType = MATCH_TYPE_SUFFIX;
31
+ path = path.split('').reverse().join('');
32
+ parts = path.split(starPattern);
33
+ } else {
34
+ parts = path.split(starPattern);
35
+ matchType = parts.length > 1 ? MATCH_TYPE_PREFIX : MATCH_TYPE_EQUALS;
36
+ }
37
+ treePath = parts[0];
38
+ if (matchType === MATCH_TYPE_PREFIX || matchType === MATCH_TYPE_SUFFIX) {
39
+ pathPattern = '^' + parts.map((part, index) => {
40
+ if (part === '**') {
41
+ return '.*';
42
+ } else if (part === '*') {
43
+ return '[^/]*';
44
+ }
45
+ return part.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&');
46
+ }).join('') + '$';
47
+ }
48
+
49
+ return {
50
+ treePath: treePath,
51
+ path: originPath,
52
+ pathPattern: pathPattern,
53
+ matchType: matchType,
54
+ };
55
+ }
56
+
57
+ class RadixTree {
58
+ constructor(routes, matchFunc) {
59
+ this.originRoutes = routes
60
+ // Initialize routes array with undefined at index 0 to ensure indexing starts from 1
61
+ this.routes = [undefined]
62
+ this.prefixTree = new RaxRadixTree() // Prefix matching
63
+ this.suffixTree = new RaxRadixTree() // Suffix matching
64
+ this.equalsMap = {}
65
+ this.prefixRouteCount = 0;
66
+ this.suffixRouteCount = 0;
67
+ this.equalsRouteCount = 0;
68
+ this.matchFunc = matchFunc
69
+ this._initRoutes()
70
+ }
71
+
72
+ _initRoute(route, routePath) {
73
+ const pathInfo = parsePath(routePath)
74
+ if (!route.methods) {
75
+ route.methods = ['ALL']
76
+ }
77
+ const treePath = pathInfo.treePath
78
+ const path = pathInfo.path
79
+ delete pathInfo.path
80
+
81
+ if (pathInfo.matchType == MATCH_TYPE_EQUALS) {
82
+ const routeInfo = Object.assign({pathInfo, path}, route);
83
+ let index = this.equalsMap[treePath]
84
+ if (index === undefined) {
85
+ this.routes.push([routeInfo]);
86
+ index = this.routes.length - 1;
87
+ this.equalsMap[treePath] = index;
88
+ } else {
89
+ this.routes[index].push(routeInfo);
90
+ }
91
+ this.equalsRouteCount++;
92
+ return
93
+ }
94
+
95
+ pathInfo.pattern = new RegExp(pathInfo.pathPattern);
96
+ const routeInfo = Object.assign({pathInfo, path}, route);
97
+ const tree = pathInfo.matchType === MATCH_TYPE_SUFFIX ? this.suffixTree : this.prefixTree;
98
+ // console.log("pathInfo.matchType: %s, tree: %s", pathInfo.matchType, treePath)
99
+ let index = tree.find(treePath)
100
+ if (index === null) {
101
+ this.routes.push([routeInfo]);
102
+ index = this.routes.length - 1;
103
+ tree.insert(treePath, index)
104
+ } else {
105
+ assert(index >= 0 && index < this.routes.length);
106
+ this.routes[index].push(routeInfo);
107
+ }
108
+ if (pathInfo.matchType === MATCH_TYPE_PREFIX) {
109
+ this.prefixRouteCount++;
110
+ } else if (pathInfo.matchType === MATCH_TYPE_SUFFIX) {
111
+ this.suffixRouteCount++;
112
+ }
113
+ }
114
+
115
+ _initRoutes() {
116
+ for (let route of this.originRoutes) {
117
+ route = Object.assign({}, route);
118
+ const routePaths = route.paths;
119
+ delete route.paths;
120
+ for(let routePath of routePaths) {
121
+ this._initRoute(route, routePath)
122
+ }
123
+ }
124
+
125
+ this._sortAllRoutes();
126
+ }
127
+
128
+ _sortAllRoutes() {
129
+ for (let routeArray of this.routes) {
130
+ if (!routeArray) {
131
+ continue
132
+ }
133
+ routeArray.sort((a, b) => {
134
+ const lengthDiff = b.path.length - a.path.length;
135
+ if (lengthDiff !== 0) {
136
+ return lengthDiff;
137
+ }
138
+ // Compare methods only when both a and b have exactly one method
139
+ if (a.methods.length === 1 && b.methods.length === 1) {
140
+ if (a.methods[0] === 'ALL' && b.methods[0] !== 'ALL') {
141
+ return 1; // a should be placed after
142
+ } else if (a.methods[0] !== 'ALL' && b.methods[0] === 'ALL') {
143
+ return -1; // b should be placed after
144
+ }
145
+ }
146
+
147
+ // Compare hosts (when hosts length is 1)
148
+ if (a.hosts && a.hosts.length === 1 && b.hosts && b.hosts.length === 1) {
149
+ const aIsWildcard = a.hosts[0].startsWith('*');
150
+ const bIsWildcard = b.hosts[0].startsWith('*');
151
+ if (aIsWildcard && !bIsWildcard) {
152
+ return 1; // a is a wildcard domain, should be placed after
153
+ } else if (!aIsWildcard && bIsWildcard) {
154
+ return -1; // b is a wildcard domain, should be placed after
155
+ }
156
+ }
157
+
158
+ // If both are 'ALL' or neither is 'ALL', consider them equal
159
+ return 0;
160
+ });
161
+ }
162
+ }
163
+ _routeSimpleMatch(routeInfo, reqPath, method) {
164
+ const { treePath, pattern, matchType } = routeInfo.pathInfo;
165
+ const routeMethods = routeInfo.methods;
166
+ // Check method matching
167
+ if (!routeMethods.includes('ALL') && !routeMethods.includes(method)) {
168
+ return false;
169
+ }
170
+
171
+ // Path matching logic
172
+ if (matchType === MATCH_TYPE_EQUALS) {
173
+ return reqPath === routeInfo.path;
174
+ } else if (matchType === MATCH_TYPE_SUFFIX) {
175
+ if (!reqPath.startsWith(treePath)) {
176
+ return false;
177
+ }
178
+ return pattern.test(reqPath);
179
+ } else if (matchType === MATCH_TYPE_PREFIX) {
180
+ if (!reqPath.startsWith(treePath)) {
181
+ return false;
182
+ }
183
+ return pattern.test(reqPath);
184
+ }
185
+
186
+ // If no case is matched, return false
187
+ console.error("No case matched, {matchType: %s, path: %s}", matchType, reqPath)
188
+ return false;
189
+ }
190
+
191
+ /**
192
+ * Find all routes that match the given path and method.
193
+ * @param {string} path - The path to match against.
194
+ * @param {string} [method=''] - The HTTP method to match (optional).
195
+ * @returns {Array} An array of matching routes.
196
+ */
197
+ findAllRoutes(path, method='') {
198
+ const allRoutes = [];
199
+
200
+ // Hash lookup logic
201
+ if (this.equalsRouteCount > 0) {
202
+ const hashIndex = this.equalsMap[path];
203
+ if (hashIndex !== undefined) {
204
+ const exactMatchRoutes = this.routes[hashIndex];
205
+ for (const route of exactMatchRoutes) {
206
+ if (this._routeSimpleMatch(route, path, method)) {
207
+ allRoutes.push(route);
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ // Suffix tree lookup logic
214
+ if (this.suffixRouteCount > 0) {
215
+ const reversePath = path.split('').reverse().join('');
216
+ const treeIt = this.suffixTree.search(reversePath);
217
+ if (treeIt) {
218
+ let idx;
219
+ while ((idx = treeIt.up()) !== -1) {
220
+ if (idx >= this.routes.length) {
221
+ console.error("Index out of range");
222
+ break;
223
+ }
224
+ const routesArr = this.routes[idx];
225
+ for (const route of routesArr) {
226
+ if (this._routeSimpleMatch(route, reversePath, method)) {
227
+ allRoutes.push(route);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ // Prefix tree lookup logic
235
+ if (this.prefixRouteCount > 0) {
236
+ const treeIt = this.prefixTree.search(path);
237
+ if (treeIt) {
238
+ let idx;
239
+ while ((idx = treeIt.up()) !== -1) {
240
+ if (idx >= this.routes.length) {
241
+ console.error("Index out of range");
242
+ break;
243
+ }
244
+ const routesArr = this.routes[idx];
245
+ for (const route of routesArr) {
246
+ if (this._routeSimpleMatch(route, path, method)) {
247
+ allRoutes.push(route);
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ return allRoutes;
255
+ }
256
+
257
+ /**
258
+ * Find the first matching route for the given request.
259
+ * @param {Object} req - The request object containing path and method.
260
+ * @returns {Object|null} The first matching route, or null if no match is found.
261
+ */
262
+ findRoute(req) {
263
+ const allRoutes = this.findAllRoutes(req.path, req.method, false);
264
+ if (allRoutes.length === 0) {
265
+ return null;
266
+ }
267
+ if (!this.matchFunc) {
268
+ return allRoutes[0];
269
+ }
270
+
271
+ // Use matchFunc to test for a match
272
+ for (const route of allRoutes) {
273
+ const matched = this.matchFunc(route, req)
274
+ if (matched) {
275
+ return route;
276
+ }
277
+ }
278
+
279
+ return null;
280
+ }
281
+ };
282
+
283
+ exports.parsePath = parsePath;
284
+ exports.RadixTree = RadixTree
@@ -0,0 +1,169 @@
1
+ #include <stdlib.h>
2
+ #include <string.h>
3
+ #include <netinet/in.h>
4
+ #include <arpa/inet.h>
5
+ #include "radix_tree.h"
6
+ #include "rax/rax.h"
7
+
8
+
9
+ radix_tree* raxtree_new()
10
+ {
11
+ return (radix_tree *)raxNew();
12
+ }
13
+
14
+ int raxtree_remove(radix_tree* t, rt_str_t *buf)
15
+ {
16
+ if (t == NULL) {
17
+ return -1;
18
+ }
19
+
20
+ if (buf == NULL || buf->buf == NULL) {
21
+ return -2;
22
+ }
23
+
24
+ return raxRemove((rax *)t, buf->buf, buf->len, NULL);
25
+ }
26
+
27
+ int raxtree_destroy(radix_tree* t)
28
+ {
29
+ if (t == NULL) {
30
+ return 0;
31
+ }
32
+
33
+ raxFree(t);
34
+ return 0;
35
+ }
36
+
37
+
38
+ int raxtree_insert(radix_tree* t, const rt_str_t *buf, int idx)
39
+ {
40
+ void *data = (void *)(intptr_t)idx;
41
+
42
+ if (t == NULL) {
43
+ return -1;
44
+ }
45
+
46
+ if (buf == NULL || buf->buf == NULL) {
47
+ return -2;
48
+ }
49
+
50
+ return raxInsert((rax *)t, buf->buf, buf->len, data, NULL);
51
+ }
52
+
53
+
54
+ void* raxtree_find(radix_tree* t, const rt_str_t *buf)
55
+ {
56
+ if (t == NULL) {
57
+ return NULL;
58
+ }
59
+
60
+ if (buf == NULL || buf->buf == NULL) {
61
+ return NULL;
62
+ }
63
+
64
+ void *res = raxFind((rax *)t, buf->buf, buf->len);
65
+ if (res == raxNotFound) {
66
+ return NULL;
67
+ }
68
+
69
+ return res;
70
+ }
71
+
72
+
73
+ radix_iterator* raxtree_new_it(radix_tree* t)
74
+ {
75
+ raxIterator *it = malloc(sizeof(raxIterator));
76
+ if (it == NULL) {
77
+ return NULL;
78
+ }
79
+
80
+ raxStart(it, (rax *)t);
81
+ return (radix_iterator *)it;
82
+ }
83
+
84
+
85
+ void* raxtree_search(radix_tree* t, radix_iterator* it, const rt_str_t *buf)
86
+ {
87
+ raxIterator *iter = (raxIterator *)it;
88
+ if (it == NULL) {
89
+ return NULL;
90
+ }
91
+
92
+ raxSeek(iter, "<=", buf->buf, buf->len);
93
+ return (void *)iter;
94
+ }
95
+
96
+
97
+ int raxtree_next(radix_iterator* it, const rt_str_t *buf)
98
+ {
99
+ raxIterator *iter = it;
100
+
101
+ int res = raxNext(iter);
102
+ if (!res) {
103
+ return -1;
104
+ }
105
+
106
+ if (iter->key_len > buf->len ||
107
+ memcmp(buf->buf, iter->key, iter->key_len) != 0) {
108
+ return -1;
109
+ }
110
+
111
+ return (int)(intptr_t)iter->data;
112
+ }
113
+
114
+
115
+ int raxtree_prev(radix_iterator* it, const rt_str_t *buf)
116
+ {
117
+ raxIterator *iter = it;
118
+ int res;
119
+
120
+ while (1) {
121
+ res = raxPrev(iter);
122
+ if (!res) {
123
+ return -1;
124
+ }
125
+
126
+ if (iter->key_len > buf->len ||
127
+ memcmp(buf->buf, iter->key, iter->key_len) != 0) {
128
+ continue;
129
+ }
130
+
131
+ break;
132
+ }
133
+
134
+ return (int)(intptr_t)iter->data;
135
+ }
136
+
137
+
138
+ int raxtree_up(radix_iterator* it, const rt_str_t *buf)
139
+ {
140
+ raxIterator *iter = it;
141
+ int res;
142
+
143
+ while (1) {
144
+ res = raxUp(iter);
145
+ if (!res) {
146
+ return -1;
147
+ }
148
+
149
+ if (iter->key_len > buf->len ||
150
+ memcmp(buf->buf, iter->key, iter->key_len) != 0) {
151
+ continue;
152
+ }
153
+
154
+ break;
155
+ }
156
+
157
+ return (int)(intptr_t)iter->data;
158
+ }
159
+
160
+
161
+ int raxtree_stop(radix_iterator* it)
162
+ {
163
+ if (!it) {
164
+ return 0;
165
+ }
166
+
167
+ raxStop(it);
168
+ return 0;
169
+ }
@@ -0,0 +1,39 @@
1
+ #ifndef __RAX_TREE_H
2
+ #define __RAX_TREE_H
3
+
4
+ #ifdef __cplusplus
5
+ extern "C" {
6
+ #endif
7
+
8
+ #include <stdint.h>
9
+ #include <stdio.h>
10
+ #include <ctype.h>
11
+ #include "rax/rax.h"
12
+
13
+ typedef rax radix_tree;
14
+ typedef raxIterator radix_iterator;
15
+
16
+ typedef struct {
17
+ size_t len;
18
+ unsigned char* buf;
19
+ } rt_str_t;
20
+
21
+ radix_tree *raxtree_new();
22
+ int raxtree_destroy(radix_tree* t);
23
+ int raxtree_insert(radix_tree* t, const rt_str_t *buf, int idx);
24
+
25
+ void *raxtree_find(radix_tree* t, const rt_str_t *buf);
26
+ void *raxtree_search(radix_tree* t, radix_iterator* it, const rt_str_t *buf);
27
+
28
+ radix_iterator* raxtree_new_it(radix_tree* t);
29
+ int raxtree_prev(radix_iterator* it, const rt_str_t *buf);
30
+ int raxtree_next(radix_iterator* it, const rt_str_t *buf);
31
+ int raxtree_up(radix_iterator* it, const rt_str_t *buf);
32
+ int raxtree_stop(radix_iterator* it);
33
+ int raxtree_remove(radix_tree* t, rt_str_t *buf);
34
+
35
+ #ifdef __cplusplus
36
+ }
37
+ #endif
38
+
39
+ #endif
@@ -0,0 +1,9 @@
1
+ Copyright (c) 2017, Salvatore Sanfilippo <antirez@gmail.com>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+
9
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,26 @@
1
+ DEBUG?= -g -ggdb
2
+ CFLAGS?= -O2 -Wall -W -std=c99
3
+ LDFLAGS= -lm
4
+
5
+ # Uncomment the following two lines for coverage testing
6
+ #
7
+ # CFLAGS+=-fprofile-arcs -ftest-coverage
8
+ # LDFLAGS+=-lgcov
9
+
10
+ all: rax-test rax-oom-test
11
+
12
+ rax.o: rax.h
13
+ rax-test.o: rax.h
14
+ rax-oom-test.o: rax.h
15
+
16
+ rax-test: rax-test.o rax.o rc4rand.o crc16.o
17
+ $(CC) -o $@ $^ $(LDFLAGS) $(DEBUG)
18
+
19
+ rax-oom-test: rax-oom-test.o rax.o
20
+ $(CC) -o $@ $^ $(LDFLAGS) $(DEBUG)
21
+
22
+ .c.o:
23
+ $(CC) -c $(CFLAGS) $(DEBUG) $<
24
+
25
+ clean:
26
+ rm -f rax-test rax-oom-test *.gcda *.gcov *.gcno *.o