nx_phrase_match.nx source
↩ module page · 154 lines · 5264 B
1// nx_phrase_match.nx -- case-insensitive substring matcher.
2//
3// Tiny utility used by the Tier-2 element extractors
4// (nx_outfit_extract / nx_location_extract / nx_domain_classify)
5// when scanning beat text for known phrases.
6//
7// Why dedicated and not nx_str_find: the canonical nx_string_ops
8// search is case-sensitive (matches C strstr semantics) and pulls
9// in a longer import chain. This file is a small, self-contained
10// case-INSENSITIVE substring detector that handles ASCII upper /
11// lower equivalence inline.
12//
13// Limitations (deliberate):
14// - ASCII only. Multi-byte UTF-8 case folding is NOT done; CJK
15// phrases are matched byte-exact (they have no case anyway).
16// - Word-boundary aware via optional require_boundary flag: if
17// true, match only when neither end is adjacent to an ASCII
18// letter / digit (prevents "apron" matching inside "apronk").
19//
20// Per the bounded-loop cardinal: outer + inner loops both ride
21// explicit JPL Rule 2 BUDGETS.
22//
23// nx_safety_envelope:
24// intended_use: "Case-insensitive substring search inside a
25// byte range, used by element extractors."
26// sil_target: SIL2
27// asil_target: QM
28// dal_target: DAL C
29// iec_62304_class: NONE
30// evidence: [no_floating_point,
31// bounded_outer_loop_jpl_rule_2,
32// bounded_inner_loop_jpl_rule_2,
33// ascii_only_case_folding_documented]
34// hazard_register: [bug-tape-utf8-case-fold-not-supported,
35// bug-tape-boundary-check-permissive-on-punct]
36// residual_risk: "ASCII-only fold; CJK phrases match
37// byte-exact. Boundary check treats non-
38// alphanumeric (incl. punctuation) as a
39// boundary -- intended."
40// verdict: NOT_YET_EVALUATED
41
42import "nx_syscalls.nx"
43
44// ===== byte helpers ===============================================
45
46func nx_pm_load_u8(p: *u8, i: i64) -> i64 {
47 let q: *u8 = ((p as i64) + i) as *u8
48 return *q
49}
50
51// Lowercase an ASCII byte; non-ASCII / non-letter returned unchanged.
52func nx_pm_to_lower(c: i64) -> i64 {
53 if c >= 0x41 {
54 if c <= 0x5A { return c | 0x20 }
55 }
56 return c
57}
58
59// 1 if c is ASCII letter or digit (boundary classifier).
60func nx_pm_is_alnum(c: i64) -> i64 {
61 if c >= 0x30 {
62 if c <= 0x39 { return 1 }
63 }
64 if c >= 0x41 {
65 if c <= 0x5A { return 1 }
66 }
67 if c >= 0x61 {
68 if c <= 0x7A { return 1 }
69 }
70 return 0
71}
72
73// ===== core matcher ===============================================
74//
75// Search `hay[0..nh)` for case-insensitive occurrence of
76// `needle[0..nn)`. Returns offset of first match, or -1 if none.
77//
78// require_boundary: when nonzero, only match when neither the
79// preceding nor following byte is alphanumeric (prevents inside-
80// word matches).
81//
82// Empty needle returns 0 (vacuously matches at start).
83
84func nx_phrase_find_ci(hay: *u8, nh: i64,
85 needle: *u8, nn: i64,
86 require_boundary: i64) -> i64 {
87 if nn <= 0 { return 0 }
88 if nh < nn { return -1 }
89 let last_start: i64 = nh - nn
90 let OUTER_BUDGET: i64 = nh + 1
91 var outer: i64 = 0
92 var i: i64 = 0
93 while i <= last_start {
94 if outer >= OUTER_BUDGET { return -1 }
95
96 // Optional left-boundary check.
97 var ok_left: i64 = 1
98 if require_boundary != 0 {
99 if i > 0 {
100 let left: i64 = nx_pm_load_u8(hay, i - 1)
101 if nx_pm_is_alnum(left) == 1 { ok_left = 0 }
102 }
103 }
104
105 if ok_left == 1 {
106 // Inner compare.
107 var matched: i64 = 1
108 let INNER_BUDGET: i64 = nn + 1
109 var inner: i64 = 0
110 var j: i64 = 0
111 while j < nn {
112 if inner >= INNER_BUDGET { matched = 0; j = nn }
113 if matched == 1 {
114 let h: i64 = nx_pm_to_lower(nx_pm_load_u8(hay, i + j))
115 let p: i64 = nx_pm_to_lower(nx_pm_load_u8(needle, j))
116 if h != p { matched = 0; j = nn }
117 }
118 j = j + 1
119 inner = inner + 1
120 }
121 if matched == 1 {
122 // Optional right-boundary check.
123 var ok_right: i64 = 1
124 if require_boundary != 0 {
125 if i + nn < nh {
126 let right: i64 = nx_pm_load_u8(hay, i + nn)
127 if nx_pm_is_alnum(right) == 1 { ok_right = 0 }
128 }
129 }
130 if ok_right == 1 { return i }
131 }
132 }
133 i = i + 1
134 outer = outer + 1
135 }
136 return -1
137}
138
139// Convenience: case-insensitive contains. Returns 1 / 0.
140func nx_phrase_contains_ci(hay: *u8, nh: i64,
141 needle: *u8, nn: i64) -> i64 {
142 if nx_phrase_find_ci(hay, nh, needle, nn, 1) >= 0 { return 1 }
143 return 0
144}
145
146// Compute C string length (NUL-terminated, bounded by max).
147func nx_phrase_strlen(s: *u8, max: i64) -> i64 {
148 var i: i64 = 0
149 while i < max {
150 if nx_pm_load_u8(s, i) == 0 { return i }
151 i = i + 1
152 }
153 return max
154}