nx_buf.nx source
↩ module page · 238 lines · 8113 B
1// nx_buf.nx -- bounded append-only byte buffer with overflow detection.
2//
3// Why this exists:
4// Every diagnostic emitter (nx_log, nx_panic, nx_hex, nx_metrics)
5// currently calls sys_write directly multiple times per record.
6// Three problems with that:
7//
8// 1. Records can be interleaved by other writers between the
9// first and last sys_write of a single logical record.
10// On a single-threaded process today this is rare; on a
11// multi-threaded NishiOS userspace tomorrow, every writer
12// racing to fd=2 will produce shredded log lines.
13//
14// 2. Every sys_write is a syscall. For a hot path (parser
15// guard fires, every fuzz iteration crashing) we pay
16// thousands of context switches that we could batch.
17//
18// 3. There is no centralised "did this record fit?" check.
19// Truncated diagnostic output is worse than dropped --
20// truncated misleads.
21//
22// nx_buf solves all three: build the record in a fixed-capacity
23// byte buffer, hit overflow visibly (length > cap sets `overflow=1`
24// and stops appending), then flush atomically with one sys_write.
25//
26// Design references:
27// Rust std::io::BufWriter
28// Linux pr_cont() vs pr_info() -- per-record buffering
29// Go bytes.Buffer
30// C stdio fwrite-then-fflush pattern
31//
32// Discipline:
33// * caller picks capacity; nx_buf never reallocates (predictable)
34// * overflow is sticky -- once tripped, stays tripped until clear()
35// * never returns a partial write -- flush is all-or-nothing
36// * append family is intentionally minimal (bytes / cstr / int /
37// hex8); higher-level formatters compose from these.
38
39// nx_safety_envelope:
40// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
41// sil_target: SIL1
42// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
43// verdict: NOT_YET_EVALUATED
44
45import "syscalls.nx"
46const NX_MAGIC_12345: i64 = 12345
47
48struct NxBuf {
49 data: *u8, // backing storage
50 cap: i64, // capacity bytes
51 len: i64, // bytes written so far
52 overflow: i64, // 1 once any append has been refused
53}
54
55const NX_BUF_BYTES: i64 = 32 // 4 fields x 8
56
57// --- construction --------------------------------------------------
58
59func nx_buf_new(cap: i64) -> *NxBuf {
60 let raw: *u8 = sys_mmap(NX_BUF_BYTES)
61 let b: *NxBuf = raw as *NxBuf
62 b.data = sys_mmap(cap)
63 b.cap = cap
64 b.len = 0
65 b.overflow = 0
66 return b
67}
68
69// --- state -------------------------------------------------------------
70
71func nx_buf_clear(b: *NxBuf) -> i64 {
72 b.len = 0
73 b.overflow = 0
74 return 0
75}
76
77func nx_buf_len(b: *NxBuf) -> i64 { return b.len }
78func nx_buf_cap(b: *NxBuf) -> i64 { return b.cap }
79func nx_buf_overflow(b: *NxBuf) -> i64 { return b.overflow }
80
81// --- appenders -----------------------------------------------------
82
83func nx_buf_append_byte(b: *NxBuf, x: i64) -> i64 {
84 if b.overflow != 0 { return -1 }
85 if b.len + 1 > b.cap { b.overflow = 1; return -1 }
86 b.data[b.len] = x
87 b.len = b.len + 1
88 return 0
89}
90
91func nx_buf_append_bytes(b: *NxBuf, src: *u8, n: i64) -> i64 {
92 if b.overflow != 0 { return -1 }
93 if n < 0 { return -1 }
94 if b.len + n > b.cap { b.overflow = 1; return -1 }
95 var i: i64 = 0
96 while i < n {
97 b.data[b.len + i] = src[i]
98 i = i + 1
99 }
100 b.len = b.len + n
101 return 0
102}
103
104// nx_buf_append_cstr -- append NUL-terminated string up to cap bytes.
105// Stops at NUL or overflow, whichever comes first.
106func nx_buf_append_cstr(b: *NxBuf, s: *u8) -> i64 {
107 if b.overflow != 0 { return -1 }
108 var i: i64 = 0
109 while s[i] != 0 {
110 if b.len + 1 > b.cap {
111 b.overflow = 1
112 return -1
113 }
114 b.data[b.len] = s[i]
115 b.len = b.len + 1
116 i = i + 1
117 }
118 return 0
119}
120
121// nx_buf_append_i64 -- decimal, signed.
122func nx_buf_append_i64(b: *NxBuf, v: i64) -> i64 {
123 if b.overflow != 0 { return -1 }
124 if v == 0 {
125 return nx_buf_append_byte(b, 0x30)
126 }
127 var negative: i64 = 0
128 var n: i64 = v
129 if n < 0 { negative = 1; n = -n }
130
131 // Up to 20 digits for i64 (-9223372036854775808 = 19 digits + sign).
132 let tmp: *u8 = sys_mmap(32)
133 var ti: i64 = 0
134 while n > 0 {
135 tmp[ti] = 0x30 + (n - (n / 10) * 10)
136 n = n / 10
137 ti = ti + 1
138 }
139 if negative != 0 {
140 if nx_buf_append_byte(b, 0x2D) < 0 { return -1 } // '-'
141 }
142 var ri: i64 = ti - 1
143 while ri >= 0 {
144 if nx_buf_append_byte(b, tmp[ri]) < 0 { return -1 }
145 ri = ri - 1
146 }
147 return 0
148}
149
150// nx_buf_append_u8_hex -- two hex chars (00..FF) for one byte.
151func nx_buf_append_u8_hex(b: *NxBuf, v: i64) -> i64 {
152 let masked: i64 = v - (v / 256) * 256
153 let hi: i64 = masked / 16
154 let lo: i64 = masked - hi * 16
155 var ch_hi: i64 = 0
156 if hi < 10 { ch_hi = 0x30 + hi } else { ch_hi = 0x41 + (hi - 10) }
157 var ch_lo: i64 = 0
158 if lo < 10 { ch_lo = 0x30 + lo } else { ch_lo = 0x41 + (lo - 10) }
159 if nx_buf_append_byte(b, ch_hi) < 0 { return -1 }
160 if nx_buf_append_byte(b, ch_lo) < 0 { return -1 }
161 return 0
162}
163
164// --- atomic flush --------------------------------------------------
165
166// nx_buf_flush -- one sys_write with the whole record.
167// Returns bytes written, or -1 on syscall failure.
168// Buffer length is reset to 0 after a successful flush; overflow
169// flag is preserved so callers can detect mid-record truncation
170// across flush boundaries.
171func nx_buf_flush(b: *NxBuf, fd: i64) -> i64 {
172 if b.len == 0 { return 0 }
173 let n: i64 = sys_write(fd, b.data, b.len)
174 if n >= 0 { b.len = 0 }
175 return n
176}
177
178// --- self-test ------------------------------------------------------
179
180func main() -> i64 {
181 let b: *NxBuf = nx_buf_new(64)
182
183 // Basic byte/string append.
184 nx_buf_append_cstr(b, "hello " as *u8)
185 nx_buf_append_cstr(b, "world" as *u8)
186 if nx_buf_len(b) != 11 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
187 if nx_buf_overflow(b) != 0 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
188
189 // Append i64.
190 nx_buf_clear(b)
191 nx_buf_append_i64(b, 0)
192 if nx_buf_len(b) != 1 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
193 if b.data[0] != 0x30 { return __syscall(93, 21, 0, 0, 0, 0, 0) }
194
195 nx_buf_clear(b)
196 nx_buf_append_i64(b, NX_MAGIC_12345)
197 if nx_buf_len(b) != 5 { return __syscall(93, 22, 0, 0, 0, 0, 0) }
198 if b.data[0] != 0x31 { return __syscall(93, 23, 0, 0, 0, 0, 0) }
199 if b.data[4] != 0x35 { return __syscall(93, 24, 0, 0, 0, 0, 0) }
200
201 nx_buf_clear(b)
202 nx_buf_append_i64(b, -42)
203 if nx_buf_len(b) != 3 { return __syscall(93, 25, 0, 0, 0, 0, 0) }
204 if b.data[0] != 0x2D { return __syscall(93, 26, 0, 0, 0, 0, 0) }
205 if b.data[1] != 0x34 { return __syscall(93, 27, 0, 0, 0, 0, 0) }
206 if b.data[2] != 0x32 { return __syscall(93, 28, 0, 0, 0, 0, 0) }
207
208 // Append hex byte.
209 nx_buf_clear(b)
210 nx_buf_append_u8_hex(b, 0xAB)
211 if nx_buf_len(b) != 2 { return __syscall(93, 30, 0, 0, 0, 0, 0) }
212 if b.data[0] != 0x41 { return __syscall(93, 31, 0, 0, 0, 0, 0) }
213 if b.data[1] != 0x42 { return __syscall(93, 32, 0, 0, 0, 0, 0) }
214
215 nx_buf_clear(b)
216 nx_buf_append_u8_hex(b, 0x05)
217 if b.data[0] != 0x30 { return __syscall(93, 33, 0, 0, 0, 0, 0) }
218 if b.data[1] != 0x35 { return __syscall(93, 34, 0, 0, 0, 0, 0) }
219
220 // Overflow trip + sticky flag.
221 let small: *NxBuf = nx_buf_new(4)
222 nx_buf_append_cstr(small, "hi" as *u8)
223 if nx_buf_overflow(small) != 0 { return __syscall(93, 40, 0, 0, 0, 0, 0) }
224 nx_buf_append_cstr(small, "moremore" as *u8)
225 if nx_buf_overflow(small) == 0 { return __syscall(93, 41, 0, 0, 0, 0, 0) }
226 // Subsequent appends should be refused.
227 let rc: i64 = nx_buf_append_byte(small, 0x41)
228 if rc >= 0 { return __syscall(93, 42, 0, 0, 0, 0, 0) }
229
230 // Flush emits the whole record + resets len.
231 nx_buf_clear(b)
232 nx_buf_append_cstr(b, "flushed-record\n" as *u8)
233 let n: i64 = nx_buf_flush(b, 2)
234 if n != 15 { return __syscall(93, 50, 0, 0, 0, 0, 0) }
235 if nx_buf_len(b) != 0 { return __syscall(93, 51, 0, 0, 0, 0, 0) }
236
237 return 0
238}