PyEventEngine 0.6.2__cp315-cp315-win_amd64.whl

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.
Files changed (56) hide show
  1. event_engine/__infra__.pxd +2 -0
  2. event_engine/__init__.pxd +2 -0
  3. event_engine/__init__.py +62 -0
  4. event_engine/base/__infra__.pxd +9 -0
  5. event_engine/base/__init__.pxd +9 -0
  6. event_engine/base/__init__.py +31 -0
  7. event_engine/base/c_allocator_protocol.c +14310 -0
  8. event_engine/base/c_allocator_protocol.cp315-win_amd64.pyd +0 -0
  9. event_engine/base/c_allocator_protocol.h +23 -0
  10. event_engine/base/c_allocator_protocol.pxd +31 -0
  11. event_engine/base/c_allocator_protocol.pyx +193 -0
  12. event_engine/capi/__infra__.pxd +189 -0
  13. event_engine/capi/__init__.pxd +189 -0
  14. event_engine/capi/__init__.py +84 -0
  15. event_engine/capi/c_engine.c +32542 -0
  16. event_engine/capi/c_engine.cp315-win_amd64.pyd +0 -0
  17. event_engine/capi/c_engine.h +393 -0
  18. event_engine/capi/c_engine.pxd +84 -0
  19. event_engine/capi/c_engine.pyi +478 -0
  20. event_engine/capi/c_engine.pyx +673 -0
  21. event_engine/capi/c_event.c +22826 -0
  22. event_engine/capi/c_event.cp315-win_amd64.pyd +0 -0
  23. event_engine/capi/c_event.h +342 -0
  24. event_engine/capi/c_event.pxd +159 -0
  25. event_engine/capi/c_event.pyi +265 -0
  26. event_engine/capi/c_event.pyx +444 -0
  27. event_engine/capi/c_topic.c +34545 -0
  28. event_engine/capi/c_topic.cp315-win_amd64.pyd +0 -0
  29. event_engine/capi/c_topic.h +1007 -0
  30. event_engine/capi/c_topic.pxd +147 -0
  31. event_engine/capi/c_topic.pyi +644 -0
  32. event_engine/capi/c_topic.pyx +818 -0
  33. event_engine/capi/fallback_engine.py +707 -0
  34. event_engine/config_view.c +10271 -0
  35. event_engine/config_view.cp315-win_amd64.pyd +0 -0
  36. event_engine/config_view.pyx +55 -0
  37. event_engine/includes/event_engine/base/c_allocator_protocol.c +14310 -0
  38. event_engine/includes/event_engine/base/c_allocator_protocol.h +23 -0
  39. event_engine/includes/event_engine/capi/c_engine.c +32542 -0
  40. event_engine/includes/event_engine/capi/c_engine.h +393 -0
  41. event_engine/includes/event_engine/capi/c_event.c +22826 -0
  42. event_engine/includes/event_engine/capi/c_event.h +342 -0
  43. event_engine/includes/event_engine/capi/c_topic.c +34545 -0
  44. event_engine/includes/event_engine/capi/c_topic.h +1007 -0
  45. event_engine/includes/event_engine/config_view.c +10271 -0
  46. event_engine/native/__init__.py +60 -0
  47. event_engine/native/_event.py +420 -0
  48. event_engine/native/_topic.py +171 -0
  49. event_engine/native/engine.py +793 -0
  50. event_engine/native/event.py +469 -0
  51. event_engine/native/topic.py +943 -0
  52. pyeventengine-0.6.2.dist-info/METADATA +189 -0
  53. pyeventengine-0.6.2.dist-info/RECORD +56 -0
  54. pyeventengine-0.6.2.dist-info/WHEEL +5 -0
  55. pyeventengine-0.6.2.dist-info/licenses/LICENSE +21 -0
  56. pyeventengine-0.6.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1007 @@
1
+ #ifndef C_TOPIC_H
2
+ #define C_TOPIC_H
3
+
4
+ #include <stddef.h>
5
+ #include <stdint.h>
6
+ #include <stdlib.h>
7
+ #include <string.h>
8
+ #ifdef _WIN32
9
+ #include <cbase/nt/pthread_nt_compat.h>
10
+ #include <cbase/nt/regex_nt_compat.h>
11
+ #else
12
+ #include <pthread.h>
13
+ #include <regex.h>
14
+ #endif
15
+
16
+ #include <cbase/allocator_protocol/c_allocator_protocol.h>
17
+ #include <cbase/bytemap/c_bytemap.h>
18
+ #include <cbase/bytemap/xxh3.h>
19
+
20
+ #ifndef DEFAULT_TOPIC_SEP
21
+ #define DEFAULT_TOPIC_SEP '.'
22
+ #endif
23
+
24
+ #ifndef DEFAULT_OPTION_SEP
25
+ #define DEFAULT_OPTION_SEP '|'
26
+ #endif
27
+
28
+ #ifndef DEFAULT_RANGE_BRACKETS
29
+ #define DEFAULT_RANGE_BRACKETS "()"
30
+ #endif
31
+
32
+ #ifndef DEFAULT_WILDCARD_BRACKETS
33
+ #define DEFAULT_WILDCARD_BRACKETS "{}"
34
+ #endif
35
+
36
+ #ifndef DEFAULT_WILDCARD_MARKER
37
+ #define DEFAULT_WILDCARD_MARKER '+'
38
+ #endif
39
+
40
+ #ifndef DEFAULT_PATTERN_DELIM
41
+ #define DEFAULT_PATTERN_DELIM '/'
42
+ #endif
43
+
44
+ static bytemap* GLOBAL_INTERNAL_MAP = NULL;
45
+
46
+ typedef enum evt_topic_type {
47
+ TOPIC_PART_EXACT = 0,
48
+ TOPIC_PART_ANY = 1,
49
+ TOPIC_PART_RANGE = 2,
50
+ TOPIC_PART_PATTERN = 3
51
+ } evt_topic_type;
52
+
53
+ union evt_topic_part_variant;
54
+
55
+ // Common header — must be first in every variant
56
+ typedef struct evt_topic_part {
57
+ evt_topic_type ttype;
58
+ union evt_topic_part_variant* next;
59
+ } evt_topic_part;
60
+
61
+ // Exact match: literal string segment
62
+ typedef struct evt_topic_exact {
63
+ evt_topic_part header;
64
+ char* part;
65
+ size_t part_len;
66
+ } evt_topic_exact;
67
+
68
+ // Wildcard: matches any single segment (e.g., '+')
69
+ // Optional name for binding (e.g., "+user_id")
70
+ typedef struct evt_topic_any {
71
+ evt_topic_part header;
72
+ char* name; // may be NULL
73
+ size_t name_len;
74
+ } evt_topic_any;
75
+
76
+ // Range: matches one of several literals, like "(user|admin|guest)"
77
+ typedef struct evt_topic_range {
78
+ evt_topic_part header;
79
+ char** options; // array of option string.
80
+ size_t* option_length; // length of each option
81
+ size_t num_options; // count
82
+ char* literal; // original literal string (for reference)
83
+ size_t literal_len; // length of literal string
84
+ } evt_topic_range;
85
+
86
+ // Pattern: full regex on the entire topic key (not per-part!)
87
+ // NOTE: Typically only allowed as a *single* part in a topic,
88
+ // e.g., evt_topic = [ evt_topic_pattern ]
89
+ typedef struct evt_topic_pattern {
90
+ evt_topic_part header;
91
+ char* pattern;
92
+ size_t pattern_len;
93
+ } evt_topic_pattern;
94
+
95
+ // Unified evt_topic_part_variant as a tagged union
96
+ typedef union evt_topic_part_variant {
97
+ evt_topic_part header;
98
+ evt_topic_exact exact;
99
+ evt_topic_any any;
100
+ evt_topic_range range;
101
+ evt_topic_pattern pattern;
102
+ } evt_topic_part_variant;
103
+
104
+ // Full topic = sequence of parts + metadata
105
+ typedef struct evt_topic {
106
+ evt_topic_part_variant* parts; // head of linked list
107
+ size_t n; // number of parts
108
+ uint64_t hash; // cached hash (for fast compare)
109
+ char* key; // interned key string, e.g., "a.b.c"
110
+ size_t key_len;
111
+ int is_exact;
112
+ } evt_topic;
113
+
114
+ typedef struct evt_topic_match {
115
+ int matched; // 1 if matched, for this part alone, 0 otherwise
116
+ evt_topic_part_variant* part_a; // matched part from topic_a
117
+ evt_topic_part_variant* part_b; // matched part from topic_b
118
+ char* literal; // matched literal string, which is a borrowed pointer from part_a or part_b, if there is any.
119
+ size_t literal_len; // length of matched literal
120
+ struct evt_topic_match* next; // next match result in linked list
121
+ } evt_topic_match;
122
+
123
+ // --- Function Declarations ---
124
+
125
+ /*
126
+ * @brief Get the global internal map for topic key deduplication.
127
+ *
128
+ * @param allocator The allocator_protocol to use if the map needs to be created.
129
+ * @return The global bytemap for internalized topics.
130
+ */
131
+ static inline bytemap* c_get_global_internal_map(allocator_protocol* allocator);
132
+
133
+ /*
134
+ * @brief Create a new evt_topic from a key string using the given allocator.
135
+ *
136
+ * @param key The topic key string.
137
+ * @param key_len The length of the key string. If 0, the length is determined using strlen.
138
+ * @param allocator The allocator_protocol to use for internal allocations. If NULL, standard malloc/free are used.
139
+ * @return Pointer to a new evt_topic on success; NULL on failure.
140
+ */
141
+ static inline evt_topic* c_topic_new(const char* key, size_t key_len, allocator_protocol* allocator);
142
+
143
+ /*
144
+ * @brief Clear all parts and internal state of a evt_topic without freeing the topic structure itself.
145
+ *
146
+ * @param topic The evt_topic to clear.
147
+ */
148
+ static inline void c_topic_clear(evt_topic* topic);
149
+
150
+ /*
151
+ * @brief Free a evt_topic and all its parts, including the topic structure itself.
152
+ *
153
+ * @param topic The evt_topic to free.
154
+ */
155
+ static inline void c_topic_free(evt_topic* topic);
156
+
157
+ /*
158
+ * @brief Internalize the topic key string into a global map for deduplication.
159
+ *
160
+ * @param topic The evt_topic to internalize.
161
+ * @param key The topic key string.
162
+ * @param key_len The length of the key string.
163
+ * @return 0 on success, -1 on failure.
164
+ */
165
+ static inline int c_topic_internalize(evt_topic* topic, const char* key, size_t key_len);
166
+
167
+ /*
168
+ * @brief Append a part to the evt_topic.
169
+ * For a manual control of initializing evt_topic parts.
170
+ * Note that in this way, the evt_topic is not internalized automatically and key literal is not updated.
171
+ *
172
+ * @param topic The evt_topic to append to.
173
+ * @param s The part string.
174
+ * @param len The length of the part string. If 0, the length is determined using strlen.
175
+ * @param ttype The evt_topic_type of the part.
176
+ * @return 0 on success, -1 on failure.
177
+ */
178
+ static inline int c_topic_append(evt_topic* topic, const char* s, size_t len, evt_topic_type ttype);
179
+
180
+ /*
181
+ * @brief Parse a topic key string into its constituent parts and populate the evt_topic structure.
182
+ *
183
+ * @note for a Range topic part, the option[] array does not owns the memory, but the literal field does.
184
+ *
185
+ * @param topic The evt_topic to populate.
186
+ * @param key The topic key string.
187
+ * @param key_len The length of the key string. If 0, the length is determined using strlen.
188
+ * @return 0 on success, -1 on failure.
189
+ */
190
+ static inline int c_topic_parse(evt_topic* topic, const char* key, size_t key_len);
191
+
192
+ /*
193
+ * @brief Assign a new key to an existing evt_topic, re-parsing and internalizing it.
194
+ * Suitable for lazy initialization or reassignment.
195
+ *
196
+ * @param topic The evt_topic to assign to.
197
+ * @param key The new topic key string.
198
+ * @param key_len The length of the key string. If 0, the length is determined using strlen.
199
+ * @return 0 on success, -1 on failure.
200
+ */
201
+ static inline int c_topic_assign(evt_topic* topic, const char* key, size_t key_len);
202
+
203
+ /*
204
+ * @brief Update the internalized key literal of the evt_topic based on its parts.
205
+ * Useful after manual modifications to the parts.
206
+ *
207
+ * @param topic The evt_topic to update.
208
+ * @return 0 on success, -1 on failure.
209
+ */
210
+ static inline int c_topic_update_literal(evt_topic* topic);
211
+
212
+ /*
213
+ * @brief Match two evt_topics and produce a linked list of evt_topic_match.
214
+ *
215
+ * @param topic_a The first evt_topic to match.
216
+ * @param topic_b The second evt_topic to match.
217
+ * @param out Pointer to an existing evt_topic_match to reuse as the head of the result list.
218
+ * If NULL, a new list is created using the allocator from topic_a.
219
+ * @return Pointer to the head of the evt_topic_match linked list; NULL on failure.
220
+ */
221
+ static inline evt_topic_match* c_topic_match(evt_topic* topic_a, evt_topic* topic_b, evt_topic_match* out);
222
+
223
+ /*
224
+ * @brief New a evt_topic_match and link to the previous node, if provided.
225
+ *
226
+ * @param prev The previous node, can be NULL if there is not any.
227
+ * @param allocator The allocator_protocol to use for allocation.
228
+ * @return Pointer to a new evt_topic_match on success; NULL on failure.
229
+ */
230
+ static inline evt_topic_match* c_topic_match_new(evt_topic_match* prev, allocator_protocol* allocator);
231
+
232
+ /*
233
+ * @brief Free a linked list of evt_topic_match.
234
+ *
235
+ * @param res The head of the evt_topic_match linked list to free.
236
+ */
237
+ static inline void c_topic_match_free(evt_topic_match* res);
238
+
239
+ /*
240
+ * @brief Efficiently match two evt_topics and return 1 if they match, 0 otherwise.
241
+ * This function does not allocate or fill match result structures.
242
+ *
243
+ * @param topic_a The first evt_topic to match.
244
+ * @param topic_b The second evt_topic to match.
245
+ * @return 1 if matched, 0 otherwise.
246
+ */
247
+ static inline int c_topic_match_bool(evt_topic* topic_a, evt_topic* topic_b);
248
+
249
+ // --- Implementations ---
250
+
251
+ static inline bytemap* c_get_global_internal_map(allocator_protocol* allocator) {
252
+ if (!GLOBAL_INTERNAL_MAP) GLOBAL_INTERNAL_MAP = c_bytemap_new(0, allocator);
253
+ return GLOBAL_INTERNAL_MAP;
254
+ }
255
+
256
+ static inline evt_topic* c_topic_new(const char* key, size_t key_len, allocator_protocol* allocator) {
257
+ // Note that when key is NULL, key_len is ignored
258
+ if (key && key_len == 0) key_len = strlen(key);
259
+ else if (!key) key_len = 0;
260
+
261
+ if (key && GLOBAL_INTERNAL_MAP) {
262
+ evt_topic* internalized = NULL;
263
+ if (c_bytemap_get(GLOBAL_INTERNAL_MAP, key, key_len, (void**) &internalized) == BYTEMAP_OK) return internalized;
264
+ }
265
+
266
+ evt_topic* topic = (evt_topic*) c_ap_alloc(sizeof(evt_topic), allocator);
267
+ if (!topic) return NULL;
268
+
269
+ topic->parts = NULL;
270
+ topic->n = 0;
271
+ topic->hash = 0;
272
+ topic->key = NULL;
273
+ topic->key_len = 0;
274
+ topic->is_exact = 1; // Always initialize as exact
275
+
276
+ // If no key provided, return empty topic and not internalized
277
+ if (!key || key_len == 0) return topic;
278
+
279
+ if (c_topic_parse(topic, key, key_len) != 0) {
280
+ c_topic_free(topic);
281
+ topic = NULL;
282
+ return NULL;
283
+ }
284
+
285
+ // Assign and internalize key
286
+ if (c_topic_internalize(topic, key, key_len) != 0) {
287
+ c_topic_free(topic);
288
+ topic = NULL;
289
+ }
290
+
291
+ return topic;
292
+ }
293
+
294
+ static inline void c_topic_clear(evt_topic* topic) {
295
+ if (!topic) return;
296
+
297
+ evt_topic_part_variant* curr = topic->parts;
298
+ while (curr) {
299
+ evt_topic_part_variant* next = curr->header.next;
300
+
301
+ switch (curr->header.ttype) {
302
+ case TOPIC_PART_EXACT: {
303
+ c_ap_free(curr->exact.part);
304
+ c_ap_free(curr);
305
+ break;
306
+ }
307
+ case TOPIC_PART_ANY: {
308
+ c_ap_free(curr->any.name);
309
+ c_ap_free(curr);
310
+ break;
311
+ }
312
+ case TOPIC_PART_RANGE: {
313
+ c_ap_free(curr->range.options);
314
+ c_ap_free(curr->range.literal);
315
+ c_ap_free(curr);
316
+ break;
317
+ }
318
+ case TOPIC_PART_PATTERN: {
319
+ c_ap_free(curr->pattern.pattern);
320
+ c_ap_free(curr);
321
+ break;
322
+ }
323
+ default:
324
+ // Unknown type
325
+ c_ap_free(curr);
326
+ break;
327
+ }
328
+
329
+ curr = next;
330
+ }
331
+
332
+ if (GLOBAL_INTERNAL_MAP) {
333
+ // Note: c_bytemap_pop will also free the key string from the map.
334
+ evt_topic* internalized = NULL;
335
+ if (c_bytemap_get(GLOBAL_INTERNAL_MAP, topic->key, topic->key_len, (void**) &internalized) == BYTEMAP_OK && internalized == topic) {
336
+ c_bytemap_pop(GLOBAL_INTERNAL_MAP, topic->key, topic->key_len, NULL);
337
+ }
338
+ }
339
+
340
+ topic->key = NULL;
341
+ topic->parts = NULL;
342
+ topic->n = 0;
343
+ topic->hash = 0;
344
+ topic->is_exact = 1; // Always reset to exact
345
+ }
346
+
347
+ static inline void c_topic_free(evt_topic* topic) {
348
+ c_topic_clear(topic);
349
+ c_ap_free(topic);
350
+ }
351
+
352
+ static inline int c_topic_internalize(evt_topic* topic, const char* key, size_t key_len) {
353
+ if (!topic || !key) return -1;
354
+
355
+ allocator_protocol* allocator = c_ap_protocol_from_ptr(topic);
356
+ int ret_code = -1;
357
+
358
+ // Step 1: Get global internal map
359
+ if (!GLOBAL_INTERNAL_MAP) {
360
+ c_get_global_internal_map(allocator);
361
+ if (!GLOBAL_INTERNAL_MAP) goto exit;
362
+ }
363
+
364
+ // Step 2: Deregister previous key if any
365
+ if (topic->key) {
366
+ evt_topic* existing = NULL;
367
+ c_bytemap_get(GLOBAL_INTERNAL_MAP, topic->key, topic->key_len, (void**) &existing);
368
+ if (existing == topic) {
369
+ c_bytemap_pop(GLOBAL_INTERNAL_MAP, topic->key, topic->key_len, NULL);
370
+ }
371
+ // To avoid dangling pointers on c_bytemap_set failed, clear previous key info.
372
+ topic->key = NULL;
373
+ topic->key_len = 0;
374
+ topic->hash = 0;
375
+ }
376
+
377
+ // Step 3: Register topic
378
+ bytemap_entry* entry = NULL;
379
+ if (c_bytemap_set(GLOBAL_INTERNAL_MAP, key, key_len, (void*) topic, &entry) != BYTEMAP_OK) goto exit;
380
+
381
+ // Step 3: Log the internalized key
382
+ topic->key = (char*) entry->key;
383
+ topic->key_len = entry->key_length;
384
+ topic->hash = entry->hash;
385
+
386
+ // Already internalized
387
+ ret_code = 0;
388
+
389
+ exit:
390
+ return ret_code;
391
+ }
392
+
393
+ static inline int c_topic_append(evt_topic* topic, const char* s, size_t len, evt_topic_type ttype) {
394
+ if (!topic || !s) return -1;
395
+ if (len == 0) len = strlen(s);
396
+ if (!len) return -1;
397
+
398
+ allocator_protocol* allocator = c_ap_protocol_from_ptr(topic);
399
+ int ret_code = -1;
400
+ char* internal = (char*) c_ap_alloc(len + 1, allocator);
401
+
402
+ if (!internal) goto exit;
403
+ memcpy(internal, s, len);
404
+ internal[len] = '\0';
405
+
406
+ // Append to topic parts
407
+ evt_topic_part_variant* tp = (evt_topic_part_variant*) c_ap_alloc(sizeof(evt_topic_part_variant), allocator);
408
+ if (!tp) {
409
+ c_ap_free(internal);
410
+ goto exit;
411
+ }
412
+
413
+ switch (ttype) {
414
+ case TOPIC_PART_EXACT: {
415
+ tp->header.ttype = TOPIC_PART_EXACT;
416
+ tp->header.next = NULL;
417
+ tp->exact.part = internal;
418
+ tp->exact.part_len = len;
419
+ break;
420
+ }
421
+ case TOPIC_PART_ANY: {
422
+ tp->header.ttype = TOPIC_PART_ANY;
423
+ tp->header.next = NULL;
424
+ tp->any.name = internal;
425
+ tp->any.name_len = len;
426
+ break;
427
+ }
428
+ case TOPIC_PART_RANGE: {
429
+ // Note: Options are expected to be separated by DEFAULT_OPTION_SEP = '|' or '\0' if reconstructed from internal string.
430
+ // The Options array are allocated with a quick count of DEFAULT_OPTION_SEP or NUL, the allocated buffer might be larger than needed
431
+ size_t option_count = 1;
432
+ for (size_t i = 0; i < len; i++) {
433
+ if (internal[i] == DEFAULT_OPTION_SEP || internal[i] == '\0') {
434
+ option_count++;
435
+ }
436
+ }
437
+
438
+ // Allocate options array
439
+ char** options = (char**) c_ap_alloc(option_count * (sizeof(char*) + sizeof(size_t)), allocator);
440
+ if (!options) {
441
+ c_ap_free(internal);
442
+ c_ap_free(tp);
443
+ goto exit;
444
+ }
445
+
446
+ // Assign options array pointing to internal strings
447
+ size_t* option_length = (size_t*) ((char*) options + option_count * sizeof(char*));
448
+ size_t opt_idx = 0;
449
+ char* start = internal;
450
+ option_count = 0; // Reset counting
451
+ for (size_t i = 0; i <= len; i++) {
452
+ if (i == len || internal[i] == DEFAULT_OPTION_SEP || internal[i] == '\0') {
453
+ size_t opt_len = &internal[i] - start;
454
+ // Case 1: valid option string
455
+ if (opt_len) {
456
+ options[opt_idx] = start;
457
+ option_length[opt_idx] = opt_len;
458
+ option_count++;
459
+ opt_idx++;
460
+ }
461
+ // an empty string
462
+ else {
463
+ ;
464
+ }
465
+ // If DEFAULT_OPTION_SEP is not NUL, which normally is not.
466
+ // The internal literal string is modified to make each char* option to be nul-terminated.
467
+ internal[i] = '\0';
468
+ if (i < len) {
469
+ start = &internal[i + 1];
470
+ }
471
+ }
472
+ }
473
+
474
+ tp->header.ttype = TOPIC_PART_RANGE;
475
+ tp->header.next = NULL;
476
+ tp->range.options = options;
477
+ tp->range.option_length = option_length;
478
+ tp->range.num_options = option_count;
479
+ tp->range.literal = internal;
480
+ tp->range.literal_len = len;
481
+ break;
482
+ }
483
+ case TOPIC_PART_PATTERN: {
484
+ tp->header.ttype = TOPIC_PART_PATTERN;
485
+ tp->header.next = NULL;
486
+ tp->pattern.pattern = internal;
487
+ tp->pattern.pattern_len = len;
488
+ break;
489
+ }
490
+ default: {
491
+ // Unknown type
492
+ c_ap_free(internal);
493
+ c_ap_free(tp);
494
+ goto exit;
495
+ }
496
+ }
497
+
498
+ // Append to linked list
499
+ if (!topic->parts) {
500
+ topic->parts = tp;
501
+ }
502
+ else {
503
+ evt_topic_part_variant* curr = topic->parts;
504
+ while (curr->header.next) curr = curr->header.next;
505
+ curr->header.next = tp;
506
+ }
507
+
508
+ topic->n += 1;
509
+ if (ttype != TOPIC_PART_EXACT) topic->is_exact = 0;
510
+ ret_code = 0;
511
+
512
+ exit:
513
+ return ret_code;
514
+ }
515
+
516
+ static inline int c_topic_parse(evt_topic* topic, const char* key, size_t key_len) {
517
+ if (!topic || !key) return -1;
518
+ if (key_len == 0) key_len = strlen(key);
519
+ if (key_len == 0) return -1;
520
+
521
+ int ret_code = -1;
522
+
523
+ size_t i = 0;
524
+ while (i < key_len) {
525
+ /* Check for pattern: "./" */
526
+ if ((i == 0 && key[0] == DEFAULT_PATTERN_DELIM) ||
527
+ (i + 1 < key_len && key[i] == DEFAULT_TOPIC_SEP && key[i + 1] == DEFAULT_PATTERN_DELIM)) {
528
+
529
+ size_t content_start = key[i] == DEFAULT_TOPIC_SEP ? i + 2 : 1;
530
+ size_t j = content_start;
531
+ uint8_t found_close = 0;
532
+
533
+ while (j < key_len) {
534
+ if ((j == key_len - 1 && key[j] == DEFAULT_PATTERN_DELIM) ||
535
+ (j + 1 < key_len && key[j] == DEFAULT_PATTERN_DELIM && key[j + 1] == DEFAULT_TOPIC_SEP)) {
536
+ /* Found closing "/." */
537
+ size_t content_len = j - content_start;
538
+ if (c_topic_append(topic, key + content_start, content_len, TOPIC_PART_PATTERN) != 0) {
539
+ goto exit;
540
+ }
541
+ i = j + 2; /* advance past "/." */
542
+ found_close = 1;
543
+ break;
544
+ }
545
+ j++;
546
+ }
547
+
548
+ if (!found_close) {
549
+ goto exit; /* unclosed pattern */
550
+ }
551
+ continue;
552
+ }
553
+
554
+ /* Parse normal token up to next '.' that is not part of "./" */
555
+ size_t token_start = i;
556
+ size_t token_len = 0;
557
+
558
+ while (i < key_len) {
559
+ if (key[i] == DEFAULT_TOPIC_SEP) {
560
+ /* If next char is '/', this '.' is part of "./" → stop here */
561
+ if (i + 1 < key_len && key[i + 1] == DEFAULT_PATTERN_DELIM) {
562
+ break;
563
+ }
564
+ /* Otherwise, this '.' ends the current token */
565
+ break;
566
+ }
567
+ i++;
568
+ token_len++;
569
+ }
570
+
571
+ /* Append token if non-empty */
572
+ if (token_len > 0) {
573
+ const char* tok = key + token_start;
574
+
575
+ if (token_len >= 2 &&
576
+ tok[0] == DEFAULT_WILDCARD_MARKER) {
577
+ if (c_topic_append(topic, tok + 1, token_len - 1, TOPIC_PART_ANY) != 0) {
578
+ goto exit;
579
+ }
580
+ }
581
+ else if (token_len >= 3 && tok[0] == DEFAULT_WILDCARD_BRACKETS[0] && tok[token_len - 1] == DEFAULT_WILDCARD_BRACKETS[1]) {
582
+ if (c_topic_append(topic, tok + 1, token_len - 2, TOPIC_PART_ANY) != 0) {
583
+ goto exit;
584
+ }
585
+ }
586
+ else if (token_len >= 3 && tok[0] == DEFAULT_RANGE_BRACKETS[0] && tok[token_len - 1] == DEFAULT_RANGE_BRACKETS[1]) {
587
+ if (c_topic_append(topic, tok + 1, token_len - 2, TOPIC_PART_RANGE) != 0) {
588
+ goto exit;
589
+ }
590
+ }
591
+ else {
592
+ if (c_topic_append(topic, tok, token_len, TOPIC_PART_EXACT) != 0) {
593
+ goto exit;
594
+ }
595
+ }
596
+ }
597
+
598
+ /* Consume the terminating '.' only if it's not part of "./" */
599
+ if (i < key_len && key[i] == DEFAULT_TOPIC_SEP) {
600
+ if (i + 1 >= key_len || key[i + 1] != DEFAULT_PATTERN_DELIM) {
601
+ i++; /* consume the separator */
602
+ }
603
+ /* else: leave i at '.' so next loop sees "./" */
604
+ }
605
+ }
606
+ ret_code = 0;
607
+
608
+ exit:
609
+ return ret_code;
610
+ }
611
+
612
+ static inline int c_topic_assign(evt_topic* topic, const char* key, size_t key_len) {
613
+ if (!topic || !key) return -1;
614
+
615
+ int ret_code = -1;
616
+
617
+ // Free existing parts
618
+ c_topic_clear(topic);
619
+
620
+ topic->parts = NULL;
621
+ topic->n = 0;
622
+ topic->hash = 0;
623
+ topic->key = NULL;
624
+ topic->key_len = 0;
625
+
626
+ // Parse new key
627
+ if (c_topic_parse(topic, key, key_len) != 0) goto exit;
628
+
629
+ // Internalize new key
630
+ if (c_topic_internalize(topic, key, key_len) != 0) goto exit;
631
+ ret_code = 0;
632
+
633
+ exit:
634
+ return ret_code;
635
+ }
636
+
637
+ static inline int c_topic_update_literal(evt_topic* topic) {
638
+ if (!topic) return -1;
639
+
640
+ allocator_protocol* allocator = c_ap_protocol_from_ptr(topic);
641
+ int ret_code = -1;
642
+
643
+ // Reconstruct key literal from parts
644
+ size_t total_len = 0;
645
+ evt_topic_part_variant* curr = topic->parts;
646
+ while (curr) {
647
+ switch (curr->header.ttype) {
648
+ case TOPIC_PART_EXACT:
649
+ total_len += curr->exact.part_len;
650
+ break;
651
+ case TOPIC_PART_ANY:
652
+ total_len += 2 + curr->any.name_len; // +2 for brackets
653
+ break;
654
+ case TOPIC_PART_RANGE:
655
+ total_len += 2 + curr->range.literal_len; // +2 for brackets
656
+ break;
657
+ case TOPIC_PART_PATTERN:
658
+ total_len += 2 + curr->pattern.pattern_len; // +2 for delimiters
659
+ break;
660
+ default:
661
+ goto exit;
662
+ }
663
+ if (curr->header.next) {
664
+ total_len += 1; // for separator
665
+ }
666
+ curr = curr->header.next;
667
+ }
668
+
669
+ char* key_literal = (char*) c_ap_alloc(total_len + 1, allocator);
670
+ if (!key_literal) goto exit;
671
+
672
+ size_t pos = 0;
673
+ curr = topic->parts;
674
+ while (curr) {
675
+ switch (curr->header.ttype) {
676
+ case TOPIC_PART_EXACT:
677
+ memcpy(&key_literal[pos], curr->exact.part, curr->exact.part_len);
678
+ pos += curr->exact.part_len;
679
+ break;
680
+ case TOPIC_PART_ANY:
681
+ key_literal[pos++] = DEFAULT_WILDCARD_BRACKETS[0];
682
+ memcpy(&key_literal[pos], curr->any.name, curr->any.name_len);
683
+ pos += curr->any.name_len;
684
+ key_literal[pos++] = DEFAULT_WILDCARD_BRACKETS[1];
685
+ break;
686
+ case TOPIC_PART_RANGE:
687
+ key_literal[pos++] = DEFAULT_RANGE_BRACKETS[0];
688
+ memcpy(&key_literal[pos], curr->range.literal, curr->range.literal_len);
689
+ // literal of the range part has been modified replacing '|' with '\0', so we need to reconstruct it here.
690
+ for (size_t i = 0; i < curr->range.literal_len; i++) {
691
+ if (key_literal[pos + i] == '\0') {
692
+ key_literal[pos + i] = DEFAULT_OPTION_SEP;
693
+ }
694
+ }
695
+ pos += curr->range.literal_len;
696
+ key_literal[pos++] = DEFAULT_RANGE_BRACKETS[1];
697
+ break;
698
+ case TOPIC_PART_PATTERN:
699
+ key_literal[pos++] = DEFAULT_PATTERN_DELIM;
700
+ memcpy(&key_literal[pos], curr->pattern.pattern, curr->pattern.pattern_len);
701
+ pos += curr->pattern.pattern_len;
702
+ key_literal[pos++] = DEFAULT_PATTERN_DELIM;
703
+ break;
704
+ default:
705
+ c_ap_free(key_literal);
706
+ goto exit;
707
+ }
708
+ if (curr->header.next) {
709
+ key_literal[pos++] = DEFAULT_TOPIC_SEP;
710
+ }
711
+ curr = curr->header.next;
712
+ }
713
+ key_literal[pos] = '\0';
714
+
715
+ // Internalize new key, the temp key_literal must be freed afterwards
716
+ if (c_topic_internalize(topic, key_literal, total_len) != 0) {
717
+ c_ap_free(key_literal);
718
+ goto exit;
719
+ }
720
+ // c_topic_internalize will always create a copy in the global map for safety,
721
+ // so we can free the temporary key_literal here.
722
+ c_ap_free(key_literal);
723
+ ret_code = 0;
724
+
725
+ exit:
726
+ return ret_code;
727
+ }
728
+
729
+ static inline evt_topic_match* c_topic_match(evt_topic* topic_a, evt_topic* topic_b, evt_topic_match* out) {
730
+ if (!topic_a || !topic_b) {
731
+ return NULL;
732
+ }
733
+
734
+ allocator_protocol* allocator = c_ap_protocol_from_ptr(topic_a);
735
+
736
+ // Short-circuit 0: Same topic address or same topic literal
737
+ if (topic_a == topic_b || (topic_a->key && topic_b->key && topic_a->key_len && topic_a->key_len == topic_b->key_len && !strcmp(topic_a->key, topic_b->key))) {
738
+ if (out) {
739
+ out->part_a = topic_a->parts;
740
+ out->part_b = topic_b->parts;
741
+ c_topic_match_free(out->next);
742
+ out->next = NULL;
743
+ out->matched = 1;
744
+ out->literal = topic_a->key;
745
+ out->literal_len = topic_a->key_len;
746
+ return out;
747
+ }
748
+
749
+ evt_topic_match* res = c_topic_match_new(NULL, allocator);
750
+ if (!res) return NULL;
751
+
752
+ res->part_a = topic_a->parts;
753
+ res->part_b = topic_b->parts;
754
+ res->next = NULL;
755
+ res->matched = 1;
756
+ res->literal = topic_a->key;
757
+ res->literal_len = topic_a->key_len;
758
+ return res;
759
+ }
760
+
761
+ evt_topic_part_variant* part_a = topic_a->parts;
762
+ evt_topic_part_variant* part_b = topic_b->parts;
763
+ evt_topic_match* res = out;
764
+ evt_topic_match* tmp_alloc = NULL;
765
+ evt_topic_match* head = res;
766
+ evt_topic_match* tail = NULL;
767
+ evt_topic_part_variant* part_exact;
768
+ evt_topic_part_variant* part_other;
769
+
770
+ while (part_a && part_b) {
771
+ // Step 0: Prepare out node
772
+ if (!res) {
773
+ res = c_topic_match_new(NULL, allocator);
774
+ if (!res) {
775
+ if (tmp_alloc) c_topic_match_free(tmp_alloc);
776
+ goto unlock_and_null;
777
+ }
778
+
779
+ // Link to tail if exists
780
+ if (tail) {
781
+ tail->next = res;
782
+ }
783
+
784
+ // Track head and tail and temp allocations
785
+ if (!head) head = res;
786
+
787
+ if (!tmp_alloc) {
788
+ tmp_alloc = res;
789
+ }
790
+ }
791
+ res->part_a = part_a;
792
+ res->part_b = part_b;
793
+ res->matched = 0;
794
+
795
+ // Step 1: Determine which part is exact
796
+ if (part_a->header.ttype == TOPIC_PART_EXACT) {
797
+ part_exact = part_a;
798
+ part_other = part_b;
799
+ }
800
+ else if (part_b->header.ttype == TOPIC_PART_EXACT) {
801
+ part_exact = part_b;
802
+ part_other = part_a;
803
+ }
804
+ else {
805
+ // Either part is an exact, match fails and early exits
806
+ res->matched = 0;
807
+ goto unlock_and_return;
808
+ }
809
+
810
+ // Step 2: Match exact part against other part
811
+ switch (part_other->header.ttype) {
812
+ case TOPIC_PART_EXACT: {
813
+ if (part_exact->exact.part_len == part_other->exact.part_len &&
814
+ memcmp(part_exact->exact.part, part_other->exact.part, part_exact->exact.part_len) == 0) {
815
+ res->matched = 1;
816
+ res->literal = part_exact->exact.part;
817
+ res->literal_len = part_exact->exact.part_len;
818
+ }
819
+ else {
820
+ res->matched = 0;
821
+ goto unlock_and_return;
822
+ }
823
+ break;
824
+ }
825
+ case TOPIC_PART_ANY: {
826
+ res->matched = 1;
827
+ res->literal = part_exact->exact.part;
828
+ res->literal_len = part_exact->exact.part_len;
829
+ break;
830
+ }
831
+ case TOPIC_PART_RANGE: {
832
+ int found = 0;
833
+ for (size_t i = 0; i < part_other->range.num_options; i++) {
834
+ if (part_other->range.option_length[i] == part_exact->exact.part_len && memcmp(part_other->range.options[i], part_exact->exact.part, part_exact->exact.part_len) == 0) {
835
+ res->matched = 1;
836
+ res->literal = part_exact->exact.part;
837
+ res->literal_len = part_exact->exact.part_len;
838
+ found = 1;
839
+ break;
840
+ }
841
+ }
842
+ if (!found) {
843
+ res->matched = 0;
844
+ goto unlock_and_return;
845
+ }
846
+ break;
847
+ }
848
+ case TOPIC_PART_PATTERN: {
849
+ regex_t regex;
850
+ int compile_ret = regcomp(&regex, part_other->pattern.pattern, REG_EXTENDED);
851
+ if (compile_ret) {
852
+ res->matched = 0;
853
+ goto unlock_and_return;
854
+ }
855
+
856
+ int regex_ret = regexec(&regex, part_exact->exact.part, 0, NULL, 0);
857
+ regfree(&regex);
858
+ if (!regex_ret) {
859
+ res->matched = 1;
860
+ }
861
+ else {
862
+ res->matched = 0;
863
+ goto unlock_and_return;
864
+ }
865
+ break;
866
+ }
867
+ default: {
868
+ res->matched = 0;
869
+ goto unlock_and_return;
870
+ }
871
+ }
872
+
873
+ // Step 3: Onward the loops
874
+ part_a = part_a->header.next;
875
+ part_b = part_b->header.next;
876
+ tail = res;
877
+ res = res->next;
878
+ }
879
+
880
+ // Any residual part is consider a mis-match
881
+ if (part_a || part_b) {
882
+ if (!res) {
883
+ res = c_topic_match_new(NULL, allocator);
884
+ if (!res) {
885
+ if (tmp_alloc) c_topic_match_free(tmp_alloc);
886
+ goto unlock_and_null;
887
+ }
888
+
889
+ // Link to tail if exists
890
+ if (tail) {
891
+ tail->next = res;
892
+ }
893
+
894
+ // Track head and tail and temp allocations
895
+ if (!head) head = res;
896
+ }
897
+
898
+ res->part_a = part_a;
899
+ res->part_b = part_b;
900
+ res->matched = 0;
901
+ }
902
+
903
+ unlock_and_return:
904
+ return head;
905
+
906
+ unlock_and_null:
907
+ return NULL;
908
+ }
909
+
910
+ static inline evt_topic_match* c_topic_match_new(evt_topic_match* prev, allocator_protocol* allocator) {
911
+ evt_topic_match* res = (evt_topic_match*) c_ap_alloc(sizeof(evt_topic_match), allocator);
912
+ if (!res) return NULL;
913
+ if (prev) prev->next = res;
914
+ return res;
915
+ }
916
+
917
+ static inline void c_topic_match_free(evt_topic_match* res) {
918
+ if (!res) return;
919
+
920
+ evt_topic_match* curr = res;
921
+ while (curr) {
922
+ evt_topic_match* next = curr->next;
923
+ c_ap_free(curr);
924
+ curr = next;
925
+ }
926
+ }
927
+
928
+ static inline int c_topic_match_bool(evt_topic* topic_a, evt_topic* topic_b) {
929
+ if (!topic_a || !topic_b) {
930
+ return 0;
931
+ }
932
+ // Short-circuit: Same topic address or same topic literal
933
+ if (topic_a == topic_b || (topic_a->key && topic_b->key && topic_a->key_len && topic_a->key_len == topic_b->key_len && !strcmp(topic_a->key, topic_b->key))) {
934
+ return 1;
935
+ }
936
+ evt_topic_part_variant* part_a = topic_a->parts;
937
+ evt_topic_part_variant* part_b = topic_b->parts;
938
+ evt_topic_part_variant* part_exact;
939
+ evt_topic_part_variant* part_other;
940
+ while (part_a && part_b) {
941
+ // Determine which part is exact
942
+ if (part_a->header.ttype == TOPIC_PART_EXACT) {
943
+ part_exact = part_a;
944
+ part_other = part_b;
945
+ }
946
+ else if (part_b->header.ttype == TOPIC_PART_EXACT) {
947
+ part_exact = part_b;
948
+ part_other = part_a;
949
+ }
950
+ else {
951
+ // If neither is exact, match fails
952
+ return 0;
953
+ }
954
+ // Match exact part against other part
955
+ switch (part_other->header.ttype) {
956
+ case TOPIC_PART_EXACT:
957
+ if (part_exact->exact.part_len == part_other->exact.part_len &&
958
+ memcmp(part_exact->exact.part, part_other->exact.part, part_exact->exact.part_len) == 0) {
959
+ // matched
960
+ }
961
+ else {
962
+ return 0;
963
+ }
964
+ break;
965
+ case TOPIC_PART_ANY:
966
+ // always matches
967
+ break;
968
+ case TOPIC_PART_RANGE: {
969
+ int found = 0;
970
+ for (size_t i = 0; i < part_other->range.num_options; i++) {
971
+ if (part_other->range.option_length[i] == part_exact->exact.part_len && memcmp(part_other->range.options[i], part_exact->exact.part, part_exact->exact.part_len) == 0) {
972
+ found = 1;
973
+ break;
974
+ }
975
+ }
976
+ if (!found) {
977
+ return 0;
978
+ }
979
+ break;
980
+ }
981
+ case TOPIC_PART_PATTERN: {
982
+ regex_t regex;
983
+ int compile_ret = regcomp(&regex, part_other->pattern.pattern, REG_EXTENDED);
984
+ if (compile_ret) {
985
+ return 0;
986
+ }
987
+ int regex_ret = regexec(&regex, part_exact->exact.part, 0, NULL, 0);
988
+ regfree(&regex);
989
+ if (regex_ret) {
990
+ return 0;
991
+ }
992
+ break;
993
+ }
994
+ default:
995
+ return 0;
996
+ }
997
+ part_a = part_a->header.next;
998
+ part_b = part_b->header.next;
999
+ }
1000
+ // Any residual part is a mismatch
1001
+ if (part_a || part_b) {
1002
+ return 0;
1003
+ }
1004
+ return 1;
1005
+ }
1006
+
1007
+ #endif // C_TOPIC_H