fs.nx source
↩ module page · 201 lines · 7280 B
1// fs.nx -- file I/O stdlib (Phase G1 in the roadmap).
2//
3// Typed wrappers around Linux / NishiOS file syscalls. Every fallible
4// operation returns `*Result<i64, FsError>`. Callers match on the
5// variant -- the `-errno` sentinel convention is gone from the public
6// API. Raw-errno helpers remain as `fs_*_raw` for the rare case
7// where a caller wants the bare syscall return.
8//
9// Syscall numbers (Linux RV64, NishiOS matches):
10// 56 openat
11// 57 close
12// 63 read
13// 64 write
14// 80 fstat
15// 34 mkdirat
16// 82 renameat
17//
18// AT_FDCWD = -100 is the "current working directory" magic fd used by
19// the *at syscalls when the path is relative.
20
21import "syscalls.nx"
22import "stdlib.nx"
23
24// ---- flags ---------------------------------------------------------
25
26const O_RDONLY: i64 = 0
27const O_WRONLY: i64 = 1
28const O_RDWR: i64 = 2
29const O_CREAT: i64 = 0x40
30const O_TRUNC: i64 = 0x200
31const O_APPEND: i64 = 0x400
32
33const SYS_FSTAT: i64 = 80
34const SYS_MKDIRAT: i64 = 34
35const SYS_RENAMEAT: i64 = 82
36
37// ---- FsError -------------------------------------------------------
38//
39// Named error kinds that the syscall layer maps `-errno` onto. Stays
40// aligned with Linux errno numbers where one kind exists (NotFound =
41// ENOENT, PermDenied = EACCES, ...). Unknown lumps everything else
42// so callers can still pattern-match exhaustively.
43enum FsError {
44 NotFound, // -ENOENT = -2
45 PermDenied, // -EACCES = -13
46 Exists, // -EEXIST = -17
47 NotDir, // -ENOTDIR = -20
48 IsDir, // -EISDIR = -21
49 InvalidArg, // -EINVAL = -22
50 NoSpace, // -ENOSPC = -28
51 Io, // -EIO = -5
52 Unknown,
53}
54
55// Translate a syscall errno (as negative i64) into an FsError
56// discriminant. Callers that want the original numeric errno can
57// still use `*_raw`.
58func fs_errno_to(e: i64) -> i64 {
59 let err: i64 = 0 - e
60 if err == 2 { return FsError::NotFound }
61 if err == 13 { return FsError::PermDenied }
62 if err == 17 { return FsError::Exists }
63 if err == 20 { return FsError::NotDir }
64 if err == 21 { return FsError::IsDir }
65 if err == 22 { return FsError::InvalidArg }
66 if err == 28 { return FsError::NoSpace }
67 if err == 5 { return FsError::Io }
68 return FsError::Unknown
69}
70
71// ---- open / close --------------------------------------------------
72
73// Raw variants expose the bare syscall rc (fd >= 0 or -errno). Kept
74// public so rare callers can reach under the Result wrapper.
75func fs_open_rd_raw(path: *u8) -> i64 {
76 return __syscall(SYS_OPENAT, AT_FDCWD, path, O_RDONLY, 0, 0, 0)
77}
78
79func fs_open_wr_raw(path: *u8, mode: i64) -> i64 {
80 let flags: i64 = O_WRONLY | O_CREAT | O_TRUNC
81 return __syscall(SYS_OPENAT, AT_FDCWD, path, flags, mode, 0, 0)
82}
83
84func fs_open_append_raw(path: *u8, mode: i64) -> i64 {
85 let flags: i64 = O_WRONLY | O_CREAT | O_APPEND
86 return __syscall(SYS_OPENAT, AT_FDCWD, path, flags, mode, 0, 0)
87}
88
89// Typed API. Return Result<fd, FsError>.
90func fs_open_rd(path: *u8) -> *Result<i64, FsError> {
91 let rc: i64 = fs_open_rd_raw(path)
92 if rc < 0 { return Result::Err(fs_errno_to(rc)) }
93 return Result::Ok(rc)
94}
95
96func fs_open_wr(path: *u8, mode: i64) -> *Result<i64, FsError> {
97 let rc: i64 = fs_open_wr_raw(path, mode)
98 if rc < 0 { return Result::Err(fs_errno_to(rc)) }
99 return Result::Ok(rc)
100}
101
102func fs_open_append(path: *u8, mode: i64) -> *Result<i64, FsError> {
103 let rc: i64 = fs_open_append_raw(path, mode)
104 if rc < 0 { return Result::Err(fs_errno_to(rc)) }
105 return Result::Ok(rc)
106}
107
108// Close an fd. Returns Result<0, FsError>; the payload on Ok is a
109// conventional placeholder so callers that don't care can ignore it.
110func fs_close(fd: i64) -> *Result<i64, FsError> {
111 let rc: i64 = sys_close(fd)
112 if rc < 0 { return Result::Err(fs_errno_to(rc)) }
113 return Result::Ok(0)
114}
115
116// ---- read / write --------------------------------------------------
117
118// Read up to `n` bytes into `buf`. Ok payload is bytes read (0 on
119// EOF); Err captures any i/o failure.
120func fs_read(fd: i64, buf: *u8, n: i64) -> *Result<i64, FsError> {
121 let rc: i64 = sys_read(fd, buf, n)
122 if rc < 0 { return Result::Err(fs_errno_to(rc)) }
123 return Result::Ok(rc)
124}
125
126// Write `n` bytes. Ok payload is bytes written.
127func fs_write(fd: i64, buf: *u8, n: i64) -> *Result<i64, FsError> {
128 let rc: i64 = sys_write(fd, buf, n)
129 if rc < 0 { return Result::Err(fs_errno_to(rc)) }
130 return Result::Ok(rc)
131}
132
133// Read ALL of a file into a fresh mmap'd buffer. The buffer pointer
134// is returned in *out_len bundled (buf, len). Null pointer indicates
135// open failure and out_len is untouched; callers can route this into
136// a Result<*u8, FsError> in a future pass when pointer payloads carry
137// their type under match.
138func fs_read_all(path: *u8, out_len: *i64) -> *u8 {
139 return sys_read_file(path, out_len)
140}
141
142// Write a full buffer to a file (creating / truncating). Ok payload
143// is bytes written; closes the fd on its way out.
144func fs_write_all(path: *u8, buf: *u8, n: i64, mode: i64) -> *Result<i64, FsError> {
145 let fd_raw: i64 = fs_open_wr_raw(path, mode)
146 if fd_raw < 0 { return Result::Err(fs_errno_to(fd_raw)) }
147 var total: i64 = 0
148 while total < n {
149 let want: i64 = n - total
150 let tail_addr: i64 = (buf as i64) + total
151 let tail: *u8 = tail_addr as *u8
152 let got: i64 = sys_write(fd_raw, tail, want)
153 if got <= 0 {
154 sys_close(fd_raw)
155 if got < 0 { return Result::Err(fs_errno_to(got)) }
156 return Result::Ok(total)
157 }
158 total = total + got
159 }
160 sys_close(fd_raw)
161 return Result::Ok(total)
162}
163
164// ---- metadata ------------------------------------------------------
165
166// Size of a file in bytes. Opens, stats, closes.
167func fs_size(path: *u8) -> *Result<i64, FsError> {
168 let fd: i64 = fs_open_rd_raw(path)
169 if fd < 0 { return Result::Err(fs_errno_to(fd)) }
170 let scratch: *u8 = sys_mmap(160)
171 let rc: i64 = __syscall(SYS_FSTAT, fd, scratch as i64, 0, 0, 0, 0)
172 sys_close(fd)
173 if rc < 0 { return Result::Err(fs_errno_to(rc)) }
174 let sb: i64 = scratch as i64
175 let sp: *i64 = (sb + 48) as *i64
176 return Result::Ok(*sp)
177}
178
179// Does `path` exist? Infallible boolean -- stays i64 because "file
180// missing" is the expected success path, not an error condition.
181func fs_exists(path: *u8) -> i64 {
182 let fd: i64 = fs_open_rd_raw(path)
183 if fd < 0 { return 0 }
184 sys_close(fd)
185 return 1
186}
187
188// ---- directory ops -------------------------------------------------
189
190func fs_mkdir(path: *u8, mode: i64) -> *Result<i64, FsError> {
191 let rc: i64 = __syscall(SYS_MKDIRAT, AT_FDCWD, path, mode, 0, 0, 0)
192 if rc < 0 { return Result::Err(fs_errno_to(rc)) }
193 return Result::Ok(0)
194}
195
196func fs_rename(old_path: *u8, new_path: *u8) -> *Result<i64, FsError> {
197 let rc: i64 = __syscall(SYS_RENAMEAT, AT_FDCWD, old_path,
198 AT_FDCWD, new_path, 0, 0)
199 if rc < 0 { return Result::Err(fs_errno_to(rc)) }
200 return Result::Ok(0)
201}