Nothing to see here, move along meow
1#![no_std]
2#![no_main]
3
4use lancer_user::fs::{self, FsClient, FsRights, FsStatus, READDIR_ENTRY_SIZE};
5use lancer_user::net;
6use lancer_user::show;
7use lancer_user::syscall;
8use lancer_vfs_proto::ReadDirEntry;
9
10struct TestCtx {
11 client: FsClient,
12 pass_count: u32,
13 fail_count: u32,
14}
15
16impl TestCtx {
17 fn check(&mut self, name: &str, ok: bool) {
18 match ok {
19 true => {
20 self.pass_count += 1;
21 }
22 false => {
23 self.fail_count += 1;
24 show!(fstest, error, "{name}");
25 }
26 }
27 }
28
29 fn check_ok<T>(&mut self, name: &str, result: &Result<T, FsStatus>) {
30 match result {
31 Ok(_) => self.check(name, true),
32 Err(e) => {
33 self.fail_count += 1;
34 let ename = e.name();
35 show!(fstest, error, "{name} ({ename})");
36 }
37 }
38 }
39
40 fn check_err(&mut self, name: &str, result: &Result<(), FsStatus>, expected: FsStatus) {
41 match result {
42 Err(e) if *e == expected => self.check(name, true),
43 Ok(()) => {
44 self.fail_count += 1;
45 show!(fstest, error, "{name} (expected error, got ok)");
46 }
47 Err(e) => {
48 self.fail_count += 1;
49 let exp = expected.name();
50 let got = e.name();
51 show!(fstest, error, "{name} (expected {exp}, got {got})");
52 }
53 }
54 }
55}
56
57fn bytes_match(a: &[u8], b: &[u8]) -> bool {
58 a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x == y)
59}
60
61fn make_name(prefix: &[u8], idx: u8, buf: &mut [u8; 8]) -> usize {
62 let p_len = prefix.len().min(5);
63 buf[..p_len].copy_from_slice(&prefix[..p_len]);
64 buf[p_len] = b'0' + (idx / 10);
65 buf[p_len + 1] = b'0' + (idx % 10);
66 p_len + 2
67}
68
69fn handle_leak_check(ctx: &mut TestCtx, count: u8) {
70 let mut handles = [0u8; 62];
71 let all_ok = (0..count).all(|i| {
72 let mut name = [0u8; 8];
73 let nlen = make_name(b"leak", i, &mut name);
74 match ctx.client.open(0, &name[..nlen], FsRights::ALL) {
75 Ok(h) => {
76 handles[i as usize] = h;
77 true
78 }
79 Err(_) => false,
80 }
81 });
82 ctx.check("handle leak check (open)", all_ok);
83
84 let close_ok = (0..count).all(|i| ctx.client.close(handles[i as usize]).is_ok());
85 ctx.check("handle leak check (close)", close_ok);
86
87 let cleanup_ok = (0..count).all(|i| {
88 let mut name = [0u8; 8];
89 let nlen = make_name(b"leak", i, &mut name);
90 ctx.client.unlink(0, &name[..nlen]).is_ok()
91 });
92 ctx.check("handle leak check (cleanup)", cleanup_ok);
93}
94
95fn test_single_fs_smoke(ctx: &mut TestCtx) {
96 show!(fstest, "single fs smoke");
97
98 let _ = ctx.client.unlink(0, b"test");
99 let mkdir_r = ctx.client.mkdir(0, b"test");
100 ctx.check_ok("mkdir /test", &mkdir_r);
101
102 let test_h = match ctx.client.open(0, b"test", FsRights::ALL) {
103 Ok(h) => h,
104 Err(_) => {
105 ctx.check("open /test dir", false);
106 return;
107 }
108 };
109 ctx.check("open /test dir", true);
110
111 let hello_h = match ctx.client.open(test_h, b"hello", FsRights::ALL) {
112 Ok(h) => h,
113 Err(_) => {
114 ctx.check("create /test/hello", false);
115 let _ = ctx.client.close(test_h);
116 return;
117 }
118 };
119 ctx.check("create /test/hello", true);
120
121 let wr = ctx.client.write(hello_h, 0, b"world");
122 ctx.check_ok("write 'world'", &wr);
123 ctx.check(
124 "write returned 5 bytes",
125 wr.map(|n| n == 5).unwrap_or(false),
126 );
127
128 let mut rbuf = [0u8; 64];
129 let rr = ctx.client.read(hello_h, 0, &mut rbuf);
130 ctx.check_ok("read back", &rr);
131 match rr {
132 Ok(n) => ctx.check(
133 "read data matches 'world'",
134 bytes_match(&rbuf[..n], b"world"),
135 ),
136 Err(_) => ctx.check("read data matches 'world'", false),
137 }
138
139 let _ = ctx.client.close(hello_h);
140
141 let mut dir_buf = [0u8; READDIR_ENTRY_SIZE * 4];
142 let readdir_r = ctx.client.readdir(test_h, 0, &mut dir_buf);
143 ctx.check_ok("readdir /test", &readdir_r);
144 match readdir_r {
145 Ok((count, _)) => {
146 ctx.check("readdir returns 1 entry", count == 1);
147 if count >= 1 {
148 let entry = ReadDirEntry::from_bytes(&dir_buf);
149 match entry {
150 Some(e) => {
151 ctx.check(
152 "entry name is 'hello'",
153 bytes_match(e.name_slice(), b"hello"),
154 );
155 ctx.check("entry type is file", e.inode_type == 0);
156 ctx.check("entry size is 5", e.size == 5);
157 }
158 None => ctx.check("parse readdir entry", false),
159 }
160 }
161 }
162 Err(_) => ctx.check("readdir returns 1 entry", false),
163 }
164
165 let unlink_r = ctx.client.unlink(test_h, b"hello");
166 ctx.check_ok("unlink hello", &unlink_r);
167
168 let readdir_r2 = ctx.client.readdir(test_h, 0, &mut dir_buf);
169 ctx.check_ok("readdir after unlink", &readdir_r2);
170 match readdir_r2 {
171 Ok((count, _)) => ctx.check("readdir returns 0 entries", count == 0),
172 Err(_) => ctx.check("readdir returns 0 entries", false),
173 }
174
175 let _ = ctx.client.close(test_h);
176
177 let unlink_dir_r = ctx.client.unlink(0, b"test");
178 ctx.check_ok("unlink /test dir", &unlink_dir_r);
179
180 handle_leak_check(ctx, 62);
181}
182
183fn test_multi_mount_smoke(ctx: &mut TestCtx) {
184 show!(fstest, "multi-mount smoke");
185
186 let tmp_h = match fs::open_path(&mut ctx.client, 0, b"/tmp") {
187 Ok(h) => h,
188 Err(_) => {
189 ctx.check("open /tmp", false);
190 return;
191 }
192 };
193 ctx.check("open /tmp", true);
194
195 let mkdir_r = ctx.client.mkdir(tmp_h, b"scratch");
196 ctx.check_ok("mkdir /tmp/scratch", &mkdir_r);
197
198 let scratch_h = match ctx.client.open(tmp_h, b"scratch", FsRights::ALL) {
199 Ok(h) => h,
200 Err(_) => {
201 ctx.check("open /tmp/scratch", false);
202 let _ = ctx.client.close(tmp_h);
203 return;
204 }
205 };
206 ctx.check("open /tmp/scratch", true);
207
208 let data_h = match ctx.client.open(scratch_h, b"data", FsRights::ALL) {
209 Ok(h) => h,
210 Err(_) => {
211 ctx.check("create /tmp/scratch/data", false);
212 let _ = ctx.client.close(scratch_h);
213 let _ = ctx.client.close(tmp_h);
214 return;
215 }
216 };
217 ctx.check("create /tmp/scratch/data", true);
218
219 let wr = ctx.client.write(data_h, 0, b"test");
220 ctx.check_ok("write to /tmp/scratch/data", &wr);
221 let _ = ctx.client.close(data_h);
222
223 let data_h2 = match ctx.client.open(scratch_h, b"data", FsRights::READ) {
224 Ok(h) => h,
225 Err(_) => {
226 ctx.check("reopen /tmp/scratch/data", false);
227 let _ = ctx.client.close(scratch_h);
228 let _ = ctx.client.close(tmp_h);
229 return;
230 }
231 };
232 ctx.check("reopen /tmp/scratch/data", true);
233
234 let mut rbuf = [0u8; 64];
235 let rr = ctx.client.read(data_h2, 0, &mut rbuf);
236 ctx.check_ok("read /tmp/scratch/data", &rr);
237 match rr {
238 Ok(n) => ctx.check("data matches 'test'", bytes_match(&rbuf[..n], b"test")),
239 Err(_) => ctx.check("data matches 'test'", false),
240 }
241 let _ = ctx.client.close(data_h2);
242
243 let persist_h = match ctx.client.open(0, b"persist_check", FsRights::ALL) {
244 Ok(h) => h,
245 Err(_) => {
246 ctx.check("create /persist_check on root", false);
247 let _ = ctx.client.close(scratch_h);
248 let _ = ctx.client.close(tmp_h);
249 return;
250 }
251 };
252 ctx.check("create /persist_check on root", true);
253 let _ = ctx.client.write(persist_h, 0, b"root_data");
254 let _ = ctx.client.close(persist_h);
255
256 let vol_h = match ctx.client.open(tmp_h, b"vol_check", FsRights::ALL) {
257 Ok(h) => h,
258 Err(_) => {
259 ctx.check("create /tmp/vol_check", false);
260 let _ = ctx.client.unlink(0, b"persist_check");
261 let _ = ctx.client.close(scratch_h);
262 let _ = ctx.client.close(tmp_h);
263 return;
264 }
265 };
266 ctx.check("create /tmp/vol_check", true);
267 let _ = ctx.client.write(vol_h, 0, b"tmp_data");
268 let _ = ctx.client.close(vol_h);
269
270 let cross = ctx.client.open(tmp_h, b"persist_check", FsRights::READ);
271 ctx.check("persist_check not visible under /tmp", cross.is_err());
272 if let Ok(h) = cross {
273 let _ = ctx.client.close(h);
274 }
275
276 let cross2 = ctx.client.open(0, b"vol_check", FsRights::READ);
277 ctx.check("vol_check not visible at root", cross2.is_err());
278 if let Ok(h) = cross2 {
279 let _ = ctx.client.close(h);
280 }
281
282 let rename_r = ctx.client.rename(scratch_h, b"data", 0, b"moved");
283 ctx.check_err(
284 "cross-mount rename returns CrossDevice",
285 &rename_r,
286 FsStatus::CrossDevice,
287 );
288
289 let _ = ctx.client.unlink(scratch_h, b"data");
290 let _ = ctx.client.close(scratch_h);
291 let _ = ctx.client.unlink(tmp_h, b"scratch");
292 let _ = ctx.client.unlink(tmp_h, b"vol_check");
293 let _ = ctx.client.close(tmp_h);
294 let _ = ctx.client.unlink(0, b"persist_check");
295}
296
297fn test_handle_exhaustion(ctx: &mut TestCtx) {
298 show!(fstest, "handle exhaustion");
299
300 let mut handles = [0u8; 63];
301 let all_opened = (0..63u8).all(|i| {
302 let mut name = [0u8; 8];
303 let nlen = make_name(b"h", i, &mut name);
304 match ctx.client.open(0, &name[..nlen], FsRights::ALL) {
305 Ok(h) => {
306 handles[i as usize] = h;
307 true
308 }
309 Err(_) => false,
310 }
311 });
312 ctx.check("open 63 files all succeed", all_opened);
313
314 let unique = (0..63usize).all(|i| (i + 1..63).all(|j| handles[i] != handles[j]));
315 ctx.check("all 63 handles are unique", unique);
316
317 let overflow_r = ctx.client.open(0, b"overflow", FsRights::ALL);
318 ctx.check(
319 "64th open returns HandleTableFull",
320 matches!(overflow_r, Err(FsStatus::HandleTableFull)),
321 );
322
323 let _ = ctx.client.unlink(0, b"overflow");
324 let _ = ctx.client.close(handles[0]);
325
326 let retry_r = ctx.client.open(0, b"h00", FsRights::READ);
327 ctx.check("retry after close succeeds", retry_r.is_ok());
328 if let Ok(h) = retry_r {
329 handles[0] = h;
330 }
331
332 let close_ok = (0..63u8).all(|i| ctx.client.close(handles[i as usize]).is_ok());
333 ctx.check("close all 63 handles", close_ok);
334
335 let unlink_ok = (0..63u8).all(|i| {
336 let mut name = [0u8; 8];
337 let nlen = make_name(b"h", i, &mut name);
338 ctx.client.unlink(0, &name[..nlen]).is_ok()
339 });
340 ctx.check("unlink all 63 files", unlink_ok);
341
342 let fresh_h = ctx.client.open(0, b"fresh_test", FsRights::ALL);
343 ctx.check_ok("post-exhaustion fresh open", &fresh_h);
344 if let Ok(h) = fresh_h {
345 let _ = ctx.client.close(h);
346 let _ = ctx.client.unlink(0, b"fresh_test");
347 }
348}
349
350fn test_path_resolution_stress(ctx: &mut TestCtx) {
351 show!(fstest, "path resolution");
352
353 const DEPTH: u8 = 30;
354
355 let create_ok = create_deep_dirs(ctx, 0, 0, DEPTH);
356 ctx.check("create 30-level directory tree", create_ok);
357
358 let mut path_buf = [0u8; 256];
359 let path_len = build_deep_path(&mut path_buf, DEPTH, b"deep_file");
360
361 let (parent_h, _leaf) = match fs::open_parent(&mut ctx.client, 0, &path_buf[..path_len]) {
362 Ok(result) => result,
363 Err(_) => {
364 ctx.check("open parent of deep file", false);
365 delete_deep_dirs(ctx, DEPTH);
366 return;
367 }
368 };
369 ctx.check("open parent of deep file", true);
370
371 let deep_h = match ctx.client.open(parent_h, b"deep_file", FsRights::ALL) {
372 Ok(h) => h,
373 Err(_) => {
374 ctx.check("create deep file", false);
375 let _ = ctx.client.close(parent_h);
376 delete_deep_dirs(ctx, DEPTH);
377 return;
378 }
379 };
380 ctx.check("create deep file", true);
381
382 let wr = ctx.client.write(deep_h, 0, b"deep content");
383 ctx.check_ok("write to deep file", &wr);
384
385 let mut rbuf = [0u8; 64];
386 let rr = ctx.client.read(deep_h, 0, &mut rbuf);
387 ctx.check_ok("read deep file", &rr);
388 match rr {
389 Ok(n) => ctx.check(
390 "deep file content matches",
391 bytes_match(&rbuf[..n], b"deep content"),
392 ),
393 Err(_) => ctx.check("deep file content matches", false),
394 }
395
396 let _ = ctx.client.close(deep_h);
397
398 let unlink_r = ctx.client.unlink(parent_h, b"deep_file");
399 ctx.check_ok("unlink deep file", &unlink_r);
400 let _ = ctx.client.close(parent_h);
401
402 delete_deep_dirs(ctx, DEPTH);
403
404 handle_leak_check(ctx, 62);
405}
406
407fn create_deep_dirs(ctx: &mut TestCtx, parent_handle: u8, level: u8, max: u8) -> bool {
408 match level >= max {
409 true => true,
410 false => {
411 let mut name = [0u8; 8];
412 let nlen = make_name(b"d", level, &mut name);
413 match ctx.client.mkdir(parent_handle, &name[..nlen]) {
414 Ok(()) => match ctx.client.open(parent_handle, &name[..nlen], FsRights::ALL) {
415 Ok(dir_h) => {
416 let result = create_deep_dirs(ctx, dir_h, level + 1, max);
417 let _ = ctx.client.close(dir_h);
418 result
419 }
420 Err(_) => false,
421 },
422 Err(_) => false,
423 }
424 }
425 }
426}
427
428fn build_deep_path(buf: &mut [u8; 256], depth: u8, leaf: &[u8]) -> usize {
429 buf[0] = b'/';
430 let mut pos = 1;
431 (0..depth).for_each(|i| {
432 let mut name = [0u8; 8];
433 let nlen = make_name(b"d", i, &mut name);
434 buf[pos..pos + nlen].copy_from_slice(&name[..nlen]);
435 pos += nlen;
436 buf[pos] = b'/';
437 pos += 1;
438 });
439 if !leaf.is_empty() {
440 let llen = leaf.len().min(buf.len() - pos);
441 buf[pos..pos + llen].copy_from_slice(&leaf[..llen]);
442 pos += llen;
443 }
444 pos
445}
446
447fn delete_deep_dirs(ctx: &mut TestCtx, depth: u8) {
448 delete_deep_dirs_inner(ctx, depth);
449}
450
451fn delete_deep_dirs_inner(ctx: &mut TestCtx, remaining: u8) {
452 match remaining {
453 0 => {}
454 _ => {
455 let level = remaining - 1;
456 let mut parent_path = [0u8; 256];
457 let parent_len = build_deep_path(&mut parent_path, level, b"");
458 let trimmed = match parent_len > 1 && parent_path[parent_len - 1] == b'/' {
459 true => parent_len - 1,
460 false => parent_len,
461 };
462
463 if let Ok(parent_h) = fs::open_path(&mut ctx.client, 0, &parent_path[..trimmed]) {
464 let mut name = [0u8; 8];
465 let nlen = make_name(b"d", level, &mut name);
466 let _ = ctx.client.unlink(parent_h, &name[..nlen]);
467 let _ = ctx.client.close(parent_h);
468 }
469
470 delete_deep_dirs_inner(ctx, remaining - 1);
471 }
472 }
473}
474
475fn test_concurrent_a(ctx: &mut TestCtx) {
476 let h = match ctx.client.open(0, b"conc_a", FsRights::ALL) {
477 Ok(h) => h,
478 Err(_) => {
479 show!(fstest, error, "concurrent-a open failed");
480 return;
481 }
482 };
483 let _ = ctx.client.write(h, 0, b"alpha_data");
484 let _ = ctx.client.close(h);
485 show!(fstest, "concurrent-a done");
486}
487
488fn test_concurrent_b(ctx: &mut TestCtx) {
489 let h = match ctx.client.open(0, b"conc_b", FsRights::ALL) {
490 Ok(h) => h,
491 Err(_) => {
492 show!(fstest, error, "concurrent-b open failed");
493 return;
494 }
495 };
496 let _ = ctx.client.write(h, 0, b"bravo_data");
497 let _ = ctx.client.close(h);
498 show!(fstest, "concurrent-b done");
499}
500
501fn print_results(ctx: &TestCtx) {
502 let p = ctx.pass_count;
503 let f = ctx.fail_count;
504 match f {
505 0 => show!(fstest, "results: {p} pass, {f} fail - ok"),
506 _ => show!(fstest, error, "results: {p} pass, {f} fail - failed"),
507 }
508}
509
510#[unsafe(no_mangle)]
511pub extern "C" fn lancer_main() -> ! {
512 let (rx, _tx) = match net::init() {
513 Some(pair) => pair,
514 None => {
515 show!(fstest, error, "netsock init failed");
516 syscall::exit();
517 }
518 };
519
520 let mut args_buf = [0u8; 256];
521 let args_len = net::recv_args(&rx, &mut args_buf);
522 let args = &args_buf[..args_len];
523
524 let (_cmd, rest) = net::next_token(args);
525 let (subcmd, _) = net::next_token(rest);
526
527 let client = unsafe { fs::init() };
528
529 let mut ctx = TestCtx {
530 client,
531 pass_count: 0,
532 fail_count: 0,
533 };
534
535 match subcmd {
536 b"single" => test_single_fs_smoke(&mut ctx),
537 b"multi" => test_multi_mount_smoke(&mut ctx),
538 b"handles" => test_handle_exhaustion(&mut ctx),
539 b"paths" => test_path_resolution_stress(&mut ctx),
540 b"concurrent-a" => {
541 test_concurrent_a(&mut ctx);
542 syscall::exit();
543 }
544 b"concurrent-b" => {
545 test_concurrent_b(&mut ctx);
546 syscall::exit();
547 }
548 _ => {
549 test_single_fs_smoke(&mut ctx);
550 test_multi_mount_smoke(&mut ctx);
551 test_handle_exhaustion(&mut ctx);
552 test_path_resolution_stress(&mut ctx);
553 }
554 }
555
556 print_results(&ctx);
557 syscall::exit()
558}