code wiki / (root) / nx_fio.nx

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}