nx_fio.nx source
↩ module page · 410 lines · 17774 B
1// nx_fio.nx -- canonical sovereign file operations: unlink (delete) + existence check. Importable (no main).
2// Retires Remove-Item / rm. rename is already canonical (sys_renameat in nx_syscalls). unlinkat x86_64=263 is passed
3// DIRECTLY (the fsync-74 / fstatat-262 / unlinkat-263 precedent: a raw x86_64 number not in the rv64->x86 swap table
4// passes through untranslated). AT_FDCWD=-100, flags=0. Returns 0 on success, -errno on failure. license_tier: ORIGINAL
5import "nx_syscalls.nx"
6import "nx_sha256.nx"
7
8// Linux syscall ABI results, not admission or retry policy.
9const FIO_EINTR: i64 = 0 - 4
10const FIO_EIO: i64 = 0 - 5
11const FIO_EINVAL: i64 = 0 - 22
12const FIO_EEXIST: i64 = 0 - 17
13const FIO_EBADMSG: i64 = 0 - 74 // Linux ABI: artifact digest mismatch.
14
15struct NxFileWriteResult {
16 stage: *u8,
17 code: i64,
18 written: i64,
19 close_code: i64
20}
21
22// Owns fd until close. Preserve the first failure and the independent close
23// result; never retry close because Linux may already have released the fd.
24func fio_write_sync_fd(fd: i64, body: *u8, n: i64, result: *NxFileWriteResult) -> i64 {
25 result.stage = "write" as *u8
26 result.code = 0
27 result.written = 0
28 result.close_code = 0
29 if n < 0 || ((body as i64) == 0 && n > 0) {
30 result.stage = "input" as *u8
31 result.code = FIO_EINVAL
32 }
33 while result.code == 0 && result.written < n {
34 let w: i64 = sys_write(fd, body + result.written, n - result.written)
35 if w == FIO_EINTR { continue }
36 if w < 0 { result.code = w; break }
37 if w == 0 { result.code = FIO_EIO; break }
38 result.written = result.written + w
39 }
40 if result.code == 0 {
41 result.stage = "fsync" as *u8
42 var synced: i64 = sys_fsync(fd)
43 while synced == FIO_EINTR { synced = sys_fsync(fd) }
44 result.code = synced
45 }
46 result.close_code = sys_close(fd)
47 if result.code == 0 {
48 result.stage = "close" as *u8
49 result.code = result.close_code
50 }
51 if result.code == 0 { result.stage = "complete" as *u8 }
52 return result.code
53}
54
55// Persist the directory entry after rename. A failure here occurs after the
56// visible update: the caller must retain that publication state in its receipt.
57func fio_sync_parent(path: *u8, result: *NxFileWriteResult) -> i64 {
58 var length: i64=0
59 var slash: i64=0-1
60 while path[length]!=(0 as u8) { if path[length]==(47 as u8) { slash=length }; length=length+1 }
61 let parent: *u8=sys_mmap(length+2)
62 if slash<0 { parent[0]=46 as u8; parent[1]=0 as u8 }
63 else {
64 var end: i64=slash
65 if end==0 { end=1 }
66 var i: i64=0
67 while i<end { parent[i]=path[i]; i=i+1 }
68 parent[end]=0 as u8
69 }
70 result.stage="directory-open" as *u8
71 result.close_code=0
72 let fd: i64=sys_openat_directory(parent)
73 sys_munmap(parent,length+2)
74 if fd<0 { result.code=fd; return fd }
75 result.stage="directory-fsync" as *u8
76 var synced: i64=sys_fsync(fd)
77 while synced==FIO_EINTR { synced=sys_fsync(fd) }
78 result.close_code=sys_close(fd)
79 result.code=synced
80 if synced==0 { result.stage="directory-close" as *u8; result.code=result.close_code }
81 if result.code==0 { result.stage="complete" as *u8 }
82 return result.code
83}
84
85// delete a file (unlinkat). Returns 0 on success.
86func fio_unlink(path: *u8) -> i64 { return __syscall(263, 0 - 100, path as i64, 0, 0, 0, 0) }
87
88// 1 if `path` exists (fstatat succeeds), else 0.
89func fio_exists(path: *u8) -> i64 {
90 let st: *u8 = sys_mmap(160)
91 if sys_fstatat(path, st) == 0 { return 1 }
92 return 0
93}
94
95// A caller-owned, single-use read session. Initialize once before open; never
96// reinitialize an open session. Atomic pathname replacement does not change its fd.
97struct NxFileReadRegion {
98 fd: i64,
99 total: i64,
100 start: i64,
101 length: i64,
102 read_bytes: i64,
103 last_read: i64,
104 stage: *u8,
105 code: i64,
106 close_code: i64,
107}
108func fio_region_init(r: *NxFileReadRegion) -> i64 {
109 r.fd=0-1;r.total=0;r.start=0;r.length=0;r.read_bytes=0;r.last_read=0
110 r.stage="initialized";r.code=0;r.close_code=0
111 return 0
112}
113// Linux close consumes ownership even when it reports an error; do not retry it.
114func fio_region_close(r: *NxFileReadRegion) -> i64 {
115 if r.fd>=0 {
116 let fd: i64=r.fd;r.fd=0-1
117 r.close_code=sys_close(fd)
118 if r.code==0 && r.close_code<0 { r.code=r.close_code;r.stage="close" }
119 }
120 return r.code
121}
122func fio_region_fail(r: *NxFileReadRegion,stage: *u8,code: i64) -> i64 {
123 r.stage=stage;r.code=code
124 fio_region_close(r)
125 return code
126}
127func fio_region_open(path: *u8,r: *NxFileReadRegion) -> i64 {
128 if r.fd>=0 { return FIO_EEXIST }
129 fio_region_init(r)
130 if (path as i64)==0 { return fio_region_fail(r,"path",FIO_EINVAL) }
131 if path[0]==(0 as u8) { return fio_region_fail(r,"path",FIO_EINVAL) }
132 r.stage="open";r.fd=sys_openat_rd(path)
133 if r.fd<0 { r.code=r.fd;return r.code }
134 // SEEK_END/SEEK_SET are platform ABI selectors, not transfer-size policy.
135 let size: i64=sys_lseek(r.fd,0,2)
136 if size<0 { return fio_region_fail(r,"size-seek",size) }
137 r.total=size;r.length=size
138 let back: i64=sys_lseek(r.fd,0,0)
139 if back!=0 { if back<0 { return fio_region_fail(r,"initial-seek",back) };return fio_region_fail(r,"initial-seek",FIO_EIO) }
140 r.stage="ready";return 0
141}
142// Bounds use subtraction, so start+length can never wrap before validation.
143func fio_region_select(r: *NxFileReadRegion,start: i64,length: i64) -> i64 {
144 if r.fd<0 || r.code!=0 || r.read_bytes!=0 { return FIO_EINVAL }
145 if start<0 || length<0 || start>r.total { return FIO_EINVAL }
146 if length>r.total-start { return FIO_EINVAL }
147 let at: i64=sys_lseek(r.fd,start,0)
148 if at!=start { if at<0 { return fio_region_fail(r,"region-seek",at) };return fio_region_fail(r,"region-seek",FIO_EIO) }
149 r.start=start;r.length=length;r.stage="ready";return 0
150}
151// The caller supplies its reusable transport buffer. No allocation depends on
152// file size; each read is at most min(buffer capacity, remaining region bytes).
153func fio_region_next(r: *NxFileReadRegion,out: *u8,cap: i64) -> i64 {
154 r.last_read=0
155 if r.code!=0 { return r.code }
156 if r.fd<0 { if r.read_bytes==r.length { return 0 };return FIO_EINVAL }
157 if cap<=0 || (out as i64)==0 { return FIO_EINVAL }
158 let remaining: i64=r.length-r.read_bytes
159 if remaining==0 { r.stage="complete";return fio_region_close(r) }
160 var want: i64=remaining;if want>cap { want=cap }
161 r.stage="read"
162 while r.last_read<want {
163 let got: i64=sys_read(r.fd,out+r.last_read,want-r.last_read)
164 if got==FIO_EINTR { continue }
165 if got<0 { return fio_region_fail(r,"read",got) }
166 if got==0 { return fio_region_fail(r,"read-premature-eof",FIO_EIO) }
167 r.last_read=r.last_read+got;r.read_bytes=r.read_bytes+got
168 }
169 if r.read_bytes==r.length {
170 r.stage="complete"
171 if fio_region_close(r)<0 { return r.code }
172 } else { r.stage="ready" }
173 return r.last_read
174}
175
176
177// Preparation never replaces a pathname. The caller owns an exclusive candidate
178// path and keeps it for diagnosis on failure; publication is a separate operation.
179struct NxFilePrepareResult {
180 stage: *u8,
181 code: i64,
182 copied: i64,
183 created: i64,
184 source_close: i64,
185 destination_close: i64,
186 durable: i64,
187}
188func fio_prepare_copy(source: *u8,candidate: *u8,mode: i64,buffer: *u8,capacity: i64,out: *NxFilePrepareResult) -> i64 {
189 out.stage="input";out.code=FIO_EINVAL;out.copied=0;out.created=0
190 out.source_close=0;out.destination_close=0;out.durable=0
191 if (source as i64)==0 || (candidate as i64)==0 || (buffer as i64)==0 || capacity<=0 { return out.code }
192 if source[0]==(0 as u8) || candidate[0]==(0 as u8) || mode<0 || mode>0x1ff { return out.code }
193 let input: *NxFileReadRegion=sys_mmap(__size_of(NxFileReadRegion)) as *NxFileReadRegion
194 fio_region_init(input)
195 out.code=fio_region_open(source,input);out.stage="source-open"
196 var fd: i64=0-1
197 if out.code==0 {
198 out.stage="candidate-create"
199 fd=sys_openat_exclusive(candidate,mode)
200 if fd<0 { out.code=fd } else { out.created=1 }
201 }
202 while out.code==0 && input.read_bytes<input.length {
203 let n: i64=fio_region_next(input,buffer,capacity)
204 if n<0 { out.stage="source-read";out.code=n;break }
205 var sent: i64=0
206 out.stage="candidate-write"
207 while sent<n {
208 let w: i64=sys_write(fd,buffer+sent,n-sent)
209 if w==FIO_EINTR { continue }
210 if w<0 { out.code=w;break }
211 if w==0 { out.code=FIO_EIO;break }
212 sent=sent+w;out.copied=out.copied+w
213 }
214 }
215 fio_region_close(input);out.source_close=input.close_code
216 if out.code==0 && input.code!=0 { out.code=input.code;out.stage="source-close" }
217 sys_munmap(input as *u8,__size_of(NxFileReadRegion))
218 if out.code==0 {
219 out.stage="candidate-mode"
220 out.code=nx_chmod(candidate,mode)
221 }
222 if out.code==0 {
223 out.stage="candidate-fsync";out.code=sys_fsync(fd)
224 while out.code==FIO_EINTR { out.code=sys_fsync(fd) }
225 }
226 if fd>=0 {
227 out.destination_close=sys_close(fd)
228 if out.code==0 && out.destination_close!=0 { out.code=out.destination_close;out.stage="candidate-close" }
229 }
230 if out.code==0 {
231 let sync: *NxFileWriteResult=sys_mmap(__size_of(NxFileWriteResult)) as *NxFileWriteResult
232 out.code=fio_sync_parent(candidate,sync)
233 if out.code!=0 { out.stage=sync.stage }
234 sys_munmap(sync as *u8,__size_of(NxFileWriteResult))
235 }
236 if out.code==0 { out.stage="prepared";out.durable=1 }
237 return out.code
238}
239
240
241struct NxFilePublishResult {
242 stage: *u8,
243 code: i64,
244 visible: i64,
245 durable: i64,
246}
247// Caller owns the prepared candidate and target's mutation lock. Rename failure
248// leaves live intact; sync failure AFTER rename must retain visible=1.
249func fio_publish_candidate(candidate: *u8,live: *u8,out: *NxFilePublishResult) -> i64 {
250 out.stage="publish-input";out.code=FIO_EINVAL;out.visible=0;out.durable=0
251 if (candidate as i64)==0 || (live as i64)==0 { return out.code }
252 if candidate[0]==(0 as u8) || live[0]==(0 as u8) { return out.code }
253 out.stage="publish-rename";out.code=sys_renameat(candidate,live)
254 if out.code!=0 { return out.code }
255 out.visible=1
256 let sync: *NxFileWriteResult=sys_mmap(__size_of(NxFileWriteResult)) as *NxFileWriteResult
257 out.stage="live-directory-sync";out.code=fio_sync_parent(live,sync)
258 // Both directory entries change if preparation used another directory.
259 if out.code==0 { out.stage="candidate-directory-sync";out.code=fio_sync_parent(candidate,sync) }
260 sys_munmap(sync as *u8,__size_of(NxFileWriteResult))
261 if out.code==0 { out.stage="published";out.durable=1 }
262 return out.code
263}
264
265
266// Stable lock inode: never unlink the lockfile. All cooperating callers must use
267// the same canonical live pathname in an estate-owned directory. This excludes
268// arbitrary writers and pathname aliases from the guarantee.
269struct NxFileTargetLock {
270 fd: i64,
271 stage: *u8,
272 code: i64,
273 unlock_code: i64,
274 close_code: i64,
275}
276func fio_target_lock_init(lock: *NxFileTargetLock) -> i64 {
277 lock.fd=0-1;lock.stage="not-started";lock.code=0;lock.unlock_code=0;lock.close_code=0
278 return 0
279}
280func fio_target_lock_release(lock: *NxFileTargetLock) -> i64 {
281 if lock.fd>=0 {
282 let fd: i64=lock.fd;lock.fd=0-1
283 lock.unlock_code=sys_flock(fd,SYS_LOCK_UN)
284 lock.close_code=sys_close(fd)
285 if lock.code==0 && lock.unlock_code!=0 { lock.code=lock.unlock_code;lock.stage="target-unlock" }
286 if lock.code==0 && lock.close_code!=0 { lock.code=lock.close_code;lock.stage="target-lock-close" }
287 if lock.code==0 { lock.stage="released" }
288 }
289 return lock.code
290}
291func fio_target_lock_acquire(live: *u8,lock: *NxFileTargetLock) -> i64 {
292 if lock.fd>=0 { return FIO_EEXIST }
293 fio_target_lock_init(lock)
294 lock.stage="target-lock-input";lock.code=FIO_EINVAL
295 if (live as i64)==0 { return lock.code }
296 var n: i64=0;while live[n]!=(0 as u8) { n=n+1 }
297 if n==0 { return lock.code }
298 let suffix: *u8=".install.lock"
299 var extra: i64=0;while suffix[extra]!=(0 as u8) { extra=extra+1 }
300 let bytes: i64=n+extra+1
301 if bytes<=n { return lock.code }
302 let path: *u8=sys_mmap(bytes)
303 if (path as i64)<0 { lock.stage="target-lock-allocation";lock.code=path as i64;return lock.code }
304 var i: i64=0;while i<n { path[i]=live[i];i=i+1 }
305 i=0;while i<extra { path[n+i]=suffix[i];i=i+1 };path[n+extra]=0 as u8
306 lock.stage="target-lock-open";lock.fd=sys_openat_lock(path)
307 sys_munmap(path,bytes)
308 if lock.fd<0 { lock.code=lock.fd;return lock.code }
309 lock.stage="target-lock-acquire";lock.code=sys_flock(lock.fd,SYS_LOCK_EX | SYS_LOCK_NB)
310 if lock.code!=0 {
311 let fd: i64=lock.fd;lock.fd=0-1;lock.close_code=sys_close(fd)
312 return lock.code
313 }
314 lock.stage="held";return 0
315}
316struct NxFileReplaceResult {
317 stage: *u8,
318 code: i64,
319 candidate: NxFilePrepareResult,
320 backup: NxFilePrepareResult,
321 publication: NxFilePublishResult,
322 lock: NxFileTargetLock,
323}
324// Internal locked body. Source remains an immutable artifact; history paths
325// are exclusive. The public replacement entry points acquire the target lock.
326func fio_replace_owned(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,expected_candidate: *u8,expected_live: *u8,out: *NxFileReplaceResult) -> i64 {
327 out.stage="prepare-candidate"
328 out.backup.created=0;out.backup.durable=0;out.backup.copied=0;out.backup.code=0
329 out.backup.stage="not-started";out.backup.source_close=0;out.backup.destination_close=0
330 out.publication.stage="not-started";out.publication.code=0;out.publication.visible=0;out.publication.durable=0
331 out.code=fio_prepare_copy(source,candidate,mode,buffer,capacity,&out.candidate)
332 if out.code!=0 { return out.code }
333 if (expected_candidate as i64)!=0 {
334 out.stage="candidate-identity"
335 out.code=fio_verify_sha256(candidate,expected_candidate,buffer,capacity)
336 if out.code!=0 { return out.code }
337 }
338 out.stage="prepare-backup"
339 out.code=fio_prepare_copy(live,backup,mode,buffer,capacity,&out.backup)
340 if out.code!=0 { return out.code }
341 if (expected_live as i64)!=0 {
342 out.stage="live-identity"
343 out.code=fio_verify_sha256(backup,expected_live,buffer,capacity)
344 if out.code!=0 { return out.code }
345 }
346 out.stage="publish"
347 out.code=fio_publish_candidate(candidate,live,&out.publication)
348 if out.code==0 { out.stage="complete" }
349 return out.code
350}
351
352func fio_replace_init(out: *NxFileReplaceResult) -> i64 {
353 let raw: *u8=out as *u8;var i: i64=0
354 while i<__size_of(NxFileReplaceResult) { raw[i]=0 as u8;i=i+1 }
355 out.stage="not-started";out.candidate.stage="not-started"
356 out.backup.stage="not-started";out.publication.stage="not-started"
357 fio_target_lock_init(&out.lock);return 0
358}
359func fio_replace_core(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,expected_candidate: *u8,expected_live: *u8,out: *NxFileReplaceResult) -> i64 {
360 fio_replace_init(out)
361 out.code=fio_target_lock_acquire(live,&out.lock)
362 if out.code!=0 { out.stage=out.lock.stage;return out.code }
363 fio_replace_owned(source,live,candidate,backup,mode,buffer,capacity,expected_candidate,expected_live,out)
364 let released: i64=fio_target_lock_release(&out.lock)
365 if out.code==0 && released!=0 { out.code=released;out.stage=out.lock.stage }
366 return out.code
367}
368func fio_replace_with_backup(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,out: *NxFileReplaceResult) -> i64 {
369 return fio_replace_core(source,live,candidate,backup,mode,buffer,capacity,0 as *u8,0 as *u8,out)
370}
371
372func fio_replace_verified(source: *u8,live: *u8,candidate: *u8,backup: *u8,mode: i64,buffer: *u8,capacity: i64,expected_candidate: *u8,expected_live: *u8,out: *NxFileReplaceResult) -> i64 {
373 if (expected_candidate as i64)==0 || (expected_live as i64)==0 {
374 fio_replace_init(out)
375 out.stage="identity-input";out.code=FIO_EINVAL
376 out.candidate.stage="not-started";out.backup.stage="not-started";out.publication.stage="not-started"
377 return out.code
378 }
379 return fio_replace_core(source,live,candidate,backup,mode,buffer,capacity,expected_candidate,expected_live,out)
380}
381
382
383// Digest comparison is over the prepared bytes, before any live replacement.
384// Expected points to a SHA-256 digest (32 bytes), not a filename or size.
385func fio_verify_sha256(path: *u8,expected: *u8,buffer: *u8,capacity: i64) -> i64 {
386 if (expected as i64)==0 || (buffer as i64)==0 || capacity<=0 { return FIO_EINVAL }
387 let input: *NxFileReadRegion=sys_mmap(__size_of(NxFileReadRegion)) as *NxFileReadRegion
388 fio_region_init(input)
389 var rc: i64=fio_region_open(path,input)
390 let ctx: *Sha256=sys_mmap(__size_of(Sha256)) as *Sha256
391 sha256_init(ctx)
392 while rc==0 && input.read_bytes<input.length {
393 let n: i64=fio_region_next(input,buffer,capacity)
394 if n<0 { rc=n;break }
395 sha256_update(ctx,buffer,n)
396 }
397 fio_region_close(input)
398 if rc==0 { rc=input.code }
399 if rc==0 {
400 let actual: *u8=sys_mmap(32)
401 sha256_final(ctx,actual)
402 var i: i64=0;var differs: i64=0
403 while i<32 { differs=differs | ((actual[i] as i64) ^ (expected[i] as i64));i=i+1 }
404 if differs!=0 { rc=FIO_EBADMSG }
405 sys_munmap(actual,32)
406 }
407 sha256_destroy(ctx);sys_munmap(ctx as *u8,__size_of(Sha256))
408 sys_munmap(input as *u8,__size_of(NxFileReadRegion))
409 return rc
410}