kobako 0.29.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +217 -10
  4. data/Cargo.lock +139 -141
  5. data/README.md +96 -574
  6. data/ROADMAP.md +2 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +269 -1
  8. data/crates/kobako-runtime/Cargo.toml +25 -15
  9. data/crates/kobako-runtime/README.md +7 -3
  10. data/crates/kobako-runtime/src/dispatch.rs +1 -1
  11. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  12. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  13. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  14. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  15. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  16. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  17. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  18. data/crates/kobako-runtime/src/filesystem/{directory/spelling.rs → bind/lookup.rs} +91 -49
  19. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  20. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  21. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +227 -734
  29. data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
  30. data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
  31. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  32. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  33. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  37. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  38. data/crates/kobako-runtime/src/filesystem.rs +21 -387
  39. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  40. data/crates/kobako-runtime/src/lib.rs +11 -6
  41. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  42. data/crates/kobako-runtime/src/locking.rs +218 -0
  43. data/crates/kobako-runtime/src/mount.rs +31 -104
  44. data/crates/kobako-runtime/src/profile.rs +137 -1
  45. data/crates/kobako-runtime/src/sched.rs +210 -0
  46. data/crates/kobako-runtime/src/security.rs +696 -0
  47. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  48. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  49. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  50. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  51. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  52. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  53. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  54. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  55. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  56. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  57. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  58. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  59. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  60. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  61. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  62. data/crates/kobako-runtime/src/vfs.rs +60 -0
  63. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  64. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  65. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  66. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  67. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  68. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  70. data/crates/kobako-transport/CHANGELOG.md +8 -2
  71. data/crates/kobako-transport/Cargo.toml +3 -3
  72. data/crates/kobako-transport/README.md +6 -5
  73. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  74. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  75. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  76. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  77. data/crates/kobako-transport/src/envelope/fault.rs +2 -2
  78. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  79. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  80. data/crates/kobako-transport/src/envelope/reply.rs +2 -2
  81. data/crates/kobako-transport/src/envelope/request.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  83. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  84. data/crates/kobako-transport/src/envelope.rs +2 -2
  85. data/crates/kobako-wasmtime/CHANGELOG.md +154 -8
  86. data/crates/kobako-wasmtime/Cargo.toml +15 -14
  87. data/crates/kobako-wasmtime/README.md +9 -5
  88. data/crates/kobako-wasmtime/src/abi.rs +12 -20
  89. data/crates/kobako-wasmtime/src/ambient.rs +1 -2
  90. data/crates/kobako-wasmtime/src/cache.rs +5 -5
  91. data/crates/kobako-wasmtime/src/config.rs +22 -7
  92. data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
  93. data/crates/kobako-wasmtime/src/driver.rs +29 -96
  94. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  95. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  96. data/crates/kobako-wasmtime/src/imports.rs +14 -8
  97. data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
  98. data/crates/kobako-wasmtime/src/invocation.rs +26 -46
  99. data/crates/kobako-wasmtime/src/lib.rs +1 -4
  100. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  101. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
  102. data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
  103. data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
  104. data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
  105. data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
  106. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  107. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
  108. data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
  109. data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
  110. data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
  111. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  112. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  113. data/crates/kobako-wasmtime/src/preview1.rs +464 -413
  114. data/crates/kobako-wasmtime/src/trap.rs +9 -9
  115. data/crates/kobako-wasmtime/src/wasi.rs +53 -37
  116. data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
  117. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  118. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
  119. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
  120. data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
  121. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -1
  122. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  123. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  124. data/data/kobako.wasm +0 -0
  125. data/ext/kobako/Cargo.toml +1 -1
  126. data/ext/kobako/src/runtime/bridge.rs +1 -1
  127. data/ext/kobako/src/runtime/errors.rs +1 -1
  128. data/ext/kobako/src/runtime/escape.rs +28 -14
  129. data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
  130. data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
  131. data/ext/kobako/src/runtime/filesystem.rs +109 -137
  132. data/ext/kobako/src/runtime/gvl.rs +43 -8
  133. data/ext/kobako/src/runtime/snapshot.rs +1 -1
  134. data/ext/kobako/src/runtime.rs +6 -1
  135. data/lib/kobako/catalog/extensions.rb +5 -5
  136. data/lib/kobako/catalog/flags.rb +2 -2
  137. data/lib/kobako/catalog/handles.rb +6 -6
  138. data/lib/kobako/catalog/mount.rb +2 -10
  139. data/lib/kobako/catalog/services.rb +2 -2
  140. data/lib/kobako/catalog/snippets.rb +7 -7
  141. data/lib/kobako/catalog.rb +1 -1
  142. data/lib/kobako/codec/decoder.rb +10 -10
  143. data/lib/kobako/codec/ext_types.rb +3 -5
  144. data/lib/kobako/codec/handle_walk.rb +4 -6
  145. data/lib/kobako/codec/nesting.rb +7 -7
  146. data/lib/kobako/codec/utils.rb +1 -1
  147. data/lib/kobako/codec/value_budget.rb +3 -3
  148. data/lib/kobako/context.rb +5 -5
  149. data/lib/kobako/filesystem.rb +44 -13
  150. data/lib/kobako/payload/arguments.rb +2 -2
  151. data/lib/kobako/pool.rb +4 -4
  152. data/lib/kobako/sandbox.rb +28 -28
  153. data/lib/kobako/sandbox_options.rb +2 -3
  154. data/lib/kobako/snippet/bytecode.rb +1 -1
  155. data/lib/kobako/snippet/source.rb +1 -1
  156. data/lib/kobako/transport/dispatcher.rb +5 -5
  157. data/lib/kobako/transport/exposure.rb +2 -2
  158. data/lib/kobako/transport/reflection.rb +7 -7
  159. data/lib/kobako/transport/run.rb +5 -5
  160. data/lib/kobako/transport/yielder.rb +1 -1
  161. data/lib/kobako/version.rb +1 -1
  162. data/lib/kobako.rb +34 -0
  163. data/release-please-config.json +1 -0
  164. data/sig/kobako/catalog/extensions.rbs +3 -3
  165. data/sig/kobako/catalog/handles.rbs +3 -3
  166. data/sig/kobako/catalog/mount.rbs +0 -4
  167. data/sig/kobako/catalog/services.rbs +1 -1
  168. data/sig/kobako/catalog/snippets.rbs +3 -3
  169. data/sig/kobako/catalog.rbs +1 -1
  170. data/sig/kobako/codec/decoder.rbs +1 -1
  171. data/sig/kobako/codec/handle_walk.rbs +1 -1
  172. data/sig/kobako/codec/nesting.rbs +2 -2
  173. data/sig/kobako/codec/utils.rbs +1 -1
  174. data/sig/kobako/codec/value_budget.rbs +1 -1
  175. data/sig/kobako/codec.rbs +1 -2
  176. data/sig/kobako/context.rbs +1 -1
  177. data/sig/kobako/filesystem.rbs +19 -14
  178. data/sig/kobako/guest_yielder.rbs +1 -1
  179. data/sig/kobako/payload/arguments.rbs +1 -1
  180. data/sig/kobako/pool.rbs +2 -2
  181. data/sig/kobako/runtime.rbs +1 -1
  182. data/sig/kobako/sandbox.rbs +3 -5
  183. data/sig/kobako/sandbox_options.rbs +1 -1
  184. data/sig/kobako/transport/dispatcher.rbs +2 -2
  185. data/sig/kobako/transport/exposure.rbs +1 -1
  186. data/sig/kobako/transport/reflection.rbs +2 -2
  187. data/sig/kobako/transport/run.rbs +3 -3
  188. data/sig/kobako/transport.rbs +1 -1
  189. metadata +67 -19
  190. data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
  191. data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
  192. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
  193. data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
  194. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  195. data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
  196. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
  197. data/crates/kobako-wasmtime/src/io.rs +0 -89
  198. data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
  199. data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
  200. /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
  201. /data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +0 -0
@@ -2,18 +2,20 @@
2
2
  //! writes beneath a mounted path, and how it fails where nothing is
3
3
  //! mounted, nothing is there, or the Mount only reads.
4
4
 
5
- mod support;
5
+ use crate::support;
6
6
 
7
7
  use std::sync::atomic::{AtomicUsize, Ordering};
8
8
  use std::sync::Arc;
9
9
 
10
- use kobako_runtime::filesystem::Access;
11
10
  use kobako_runtime::filesystem::{
12
- DirEntry, File, Filesystem, FilesystemError, Memory, Metadata, OpenOptions, Rename,
11
+ FileOperations, FileSystemType, FileType, Filesystem, FilesystemError, Finish, InodeOperations,
12
+ Kiocb, Kstat, Lookup, OpenHow, Tmpfs,
13
13
  };
14
14
  use kobako_runtime::grant::Grant;
15
- use kobako_runtime::mount::Mount;
16
- use kobako_runtime::Hold;
15
+ use kobako_runtime::mount::{Access, Mount};
16
+ use kobako_runtime::security::prepare_creds_within;
17
+ use kobako_runtime::vfs::Inode;
18
+ use kobako_runtime::vfs::{self, Dentry, Path};
17
19
  use kobako_runtime::{Completion, Entry, Runtime};
18
20
  use kobako_transport::envelope::{Flag, Outcome, Preamble};
19
21
 
@@ -43,24 +45,42 @@ fn run(source: &str, grants: &[Grant], flags: Vec<Flag>) -> Option<Result<String
43
45
  })
44
46
  }
45
47
 
46
- fn mount(filesystem: Arc<dyn Filesystem>, access: Access) -> Grant {
47
- Grant::Mount(Mount::new("/data", filesystem, access).expect("/data is a guest path"))
48
+ fn mount(filesystem: Arc<dyn FileSystemType>, access: Access) -> Grant {
49
+ Grant::Mount(
50
+ Mount::new(
51
+ "/data",
52
+ Filesystem::new(filesystem).expect("the instance must stand"),
53
+ access,
54
+ )
55
+ .expect("/data is a guest path"),
56
+ )
48
57
  }
49
58
 
50
- fn note_memory() -> Arc<Memory> {
59
+ fn note_tmpfs() -> Arc<Tmpfs> {
51
60
  Arc::new([("note.txt", "hello\nworld\n")].into_iter().collect())
52
61
  }
53
62
 
54
- fn contents(filesystem: &dyn Filesystem, path: &str) -> Vec<u8> {
55
- let mut file = filesystem
56
- .open(path, &{
57
- let mut options = OpenOptions::default();
58
- options.read = true;
59
- options
60
- })
61
- .expect("the file must exist");
63
+ /// The root of `filesystem` as the host reaches it through the VFS.
64
+ fn host<F: FileSystemType>(filesystem: &Arc<F>) -> Path {
65
+ let filesystem = Filesystem::new(filesystem.clone()).expect("the instance must stand");
66
+ let mount = Mount::new("/data", filesystem, Access::Read).expect("/data is a guest path");
67
+ Path::root(mount.mnt())
68
+ }
69
+
70
+ fn contents<F: FileSystemType>(filesystem: &Arc<F>, path: &str) -> Vec<u8> {
71
+ let mut how = OpenHow::default();
72
+ how.read = true;
73
+ let op = vfs::build_open_flags(&how).expect("a read means an open");
74
+ let Ok(mut file) = vfs::do_file_open(
75
+ &prepare_creds_within(&[], None),
76
+ &host(filesystem),
77
+ path,
78
+ &op,
79
+ ) else {
80
+ panic!("the file must exist");
81
+ };
62
82
  let mut buf = vec![0; 64];
63
- let read = file.read_at(&mut buf, 0).expect("the file must read");
83
+ let read = file.pread(&mut buf, 0).expect("the file must read");
64
84
  buf.truncate(read);
65
85
  buf
66
86
  }
@@ -70,7 +90,7 @@ fn contents(filesystem: &dyn Filesystem, path: &str) -> Vec<u8> {
70
90
  fn a_script_reads_a_file_beneath_a_mounted_path() {
71
91
  let Some(answer) = run(
72
92
  r#"print File.read("/data/note.txt")"#,
73
- &[mount(note_memory(), Access::Read)],
93
+ &[mount(note_tmpfs(), Access::Read)],
74
94
  Vec::new(),
75
95
  ) else {
76
96
  return;
@@ -85,10 +105,10 @@ fn a_script_reads_a_file_beneath_a_mounted_path() {
85
105
  // @behavior FL-002
86
106
  #[test]
87
107
  fn a_script_writes_a_file_beneath_a_read_write_mount() {
88
- let memory = Arc::new(Memory::new());
108
+ let tmpfs = Arc::new(Tmpfs::new());
89
109
  let Some(answer) = run(
90
110
  r#"print File.open("/data/out.txt", "w") { |f| f.write("kept"); :done }"#,
91
- &[mount(memory.clone(), Access::ReadWrite)],
111
+ &[mount(tmpfs.clone(), Access::ReadWrite)],
92
112
  Vec::new(),
93
113
  ) else {
94
114
  return;
@@ -99,9 +119,9 @@ fn a_script_writes_a_file_beneath_a_read_write_mount() {
99
119
  "File.open must answer its block's value"
100
120
  );
101
121
  assert_eq!(
102
- contents(memory.as_ref(), "out.txt"),
122
+ contents(&tmpfs, "out.txt"),
103
123
  b"kept",
104
- "a file written in File.open's block through the bundled guest must land in the Memory"
124
+ "a file written in File.open's block through the bundled guest must land in the Tmpfs"
105
125
  );
106
126
  }
107
127
 
@@ -110,7 +130,7 @@ fn a_script_writes_a_file_beneath_a_read_write_mount() {
110
130
  fn a_script_reads_a_file_line_by_line() {
111
131
  let Some(answer) = run(
112
132
  r#"File.open("/data/note.txt") { |f| f.each_line { |line| print line.upcase } }; nil"#,
113
- &[mount(note_memory(), Access::Read)],
133
+ &[mount(note_tmpfs(), Access::Read)],
114
134
  Vec::new(),
115
135
  ) else {
116
136
  return;
@@ -141,7 +161,7 @@ fn opening_a_file_with_nothing_mounted_is_unimplemented() {
141
161
  fn opening_a_missing_file_raises_enoent() {
142
162
  let Some(answer) = run(
143
163
  r#"File.read("/data/missing.txt")"#,
144
- &[mount(note_memory(), Access::Read)],
164
+ &[mount(note_tmpfs(), Access::Read)],
145
165
  Vec::new(),
146
166
  ) else {
147
167
  return;
@@ -159,7 +179,7 @@ fn opening_a_missing_file_raises_enoent() {
159
179
  fn writing_beneath_a_read_only_mount_raises_erofs() {
160
180
  let Some(answer) = run(
161
181
  r#"File.write("/data/note.txt", "changed")"#,
162
- &[mount(note_memory(), Access::Read)],
182
+ &[mount(note_tmpfs(), Access::Read)],
163
183
  Vec::new(),
164
184
  ) else {
165
185
  return;
@@ -176,7 +196,7 @@ fn writing_beneath_a_read_only_mount_raises_erofs() {
176
196
  // @behavior FL-012
177
197
  #[test]
178
198
  fn a_write_past_a_mounts_space_raises_enospc() {
179
- let full = Arc::new(Memory::new().with_limit(Some(4)));
199
+ let full = Arc::new(Tmpfs::new().with_size(Some(4)));
180
200
  let Some(answer) = run(
181
201
  r#"File.open("/data/note.txt", "w") { |file| file.write("five!") }"#,
182
202
  &[mount(full, Access::ReadWrite)],
@@ -187,7 +207,7 @@ fn a_write_past_a_mounts_space_raises_enospc() {
187
207
  assert_eq!(
188
208
  answer,
189
209
  Err("Errno::ENOSPC".to_string()),
190
- "IO#write past a Memory's limit through the bundled guest must raise Errno::ENOSPC"
210
+ "IO#write past a Tmpfs's limit through the bundled guest must raise Errno::ENOSPC"
191
211
  );
192
212
  }
193
213
 
@@ -214,7 +234,7 @@ fn a_name_or_path_past_linuxs_limits_raises_enametoolong() {
214
234
  "#;
215
235
  let Some(answer) = run(
216
236
  script,
217
- &[mount(Arc::new(Memory::new()), Access::ReadWrite)],
237
+ &[mount(Arc::new(Tmpfs::new()), Access::ReadWrite)],
218
238
  Vec::new(),
219
239
  ) else {
220
240
  return;
@@ -233,7 +253,7 @@ fn a_name_or_path_past_linuxs_limits_raises_enametoolong() {
233
253
  fn a_code_the_engine_answers_raises_its_own_errno() {
234
254
  let Some(answer) = run(
235
255
  r#"File.rename("/data", "/data/moved")"#,
236
- &[mount(note_memory(), Access::ReadWrite)],
256
+ &[mount(note_tmpfs(), Access::ReadWrite)],
237
257
  Vec::new(),
238
258
  ) else {
239
259
  return;
@@ -281,7 +301,7 @@ fn the_disable_file_flag_removes_file() {
281
301
  fn an_io_may_stand_for_a_file_the_guest_opened() {
282
302
  let Some(answer) = run(
283
303
  r#"File.open("/data/note.txt") { |f| print IO.new(f.fileno, "r").read }; nil"#,
284
- &[mount(note_memory(), Access::Read)],
304
+ &[mount(note_tmpfs(), Access::Read)],
285
305
  Vec::new(),
286
306
  ) else {
287
307
  return;
@@ -298,16 +318,16 @@ fn an_io_may_stand_for_a_file_the_guest_opened() {
298
318
  // @behavior FL-024
299
319
  #[test]
300
320
  fn an_exclusive_create_refuses_a_file_already_there() {
301
- let memory = note_memory();
321
+ let tmpfs = note_tmpfs();
302
322
  let Some(answer) = run(
303
323
  r#"File.open("/data/note.txt", "wx") { |f| f.write("lost") }"#,
304
- &[mount(memory.clone(), Access::ReadWrite)],
324
+ &[mount(tmpfs.clone(), Access::ReadWrite)],
305
325
  Vec::new(),
306
326
  ) else {
307
327
  return;
308
328
  };
309
329
  assert_eq!(
310
- (answer, contents(memory.as_ref(), "note.txt")),
330
+ (answer, contents(&tmpfs, "note.txt")),
311
331
  (Err("Errno::EEXIST".to_string()), b"hello\nworld\n".to_vec()),
312
332
  "File.open with wx of a file already there through the bundled guest must raise \
313
333
  Errno::EEXIST and leave the file as it was"
@@ -317,16 +337,16 @@ fn an_exclusive_create_refuses_a_file_already_there() {
317
337
  // @behavior FL-025
318
338
  #[test]
319
339
  fn an_exclusive_create_makes_a_file_nothing_is_at() {
320
- let memory = Arc::new(Memory::new());
340
+ let tmpfs = Arc::new(Tmpfs::new());
321
341
  let Some(answer) = run(
322
342
  r#"File.open("/data/lock", "w+x") { |f| f.write("mine") }; nil"#,
323
- &[mount(memory.clone(), Access::ReadWrite)],
343
+ &[mount(tmpfs.clone(), Access::ReadWrite)],
324
344
  Vec::new(),
325
345
  ) else {
326
346
  return;
327
347
  };
328
348
  assert_eq!(
329
- (answer, contents(memory.as_ref(), "lock")),
349
+ (answer, contents(&tmpfs, "lock")),
330
350
  (Ok(String::new()), b"mine".to_vec()),
331
351
  "File.open with w+x of a path nothing is at through the bundled guest must create the \
332
352
  file with what was written"
@@ -351,7 +371,7 @@ fn an_access_mode_cruby_refuses_raises_argument_error() {
351
371
  "#;
352
372
  let Some(answer) = run(
353
373
  script,
354
- &[mount(note_memory(), Access::ReadWrite)],
374
+ &[mount(note_tmpfs(), Access::ReadWrite)],
355
375
  Vec::new(),
356
376
  ) else {
357
377
  return;
@@ -371,16 +391,16 @@ fn an_access_mode_cruby_refuses_raises_argument_error() {
371
391
  // @behavior FL-027
372
392
  #[test]
373
393
  fn an_integer_mode_opens_by_its_file_constants_flags() {
374
- let memory = note_memory();
394
+ let tmpfs = note_tmpfs();
375
395
  let Some(answer) = run(
376
396
  r#"File.open("/data/note.txt", File::WRONLY | File::CREAT | File::EXCL) { }"#,
377
- &[mount(memory.clone(), Access::ReadWrite)],
397
+ &[mount(tmpfs.clone(), Access::ReadWrite)],
378
398
  Vec::new(),
379
399
  ) else {
380
400
  return;
381
401
  };
382
402
  assert_eq!(
383
- (answer, contents(memory.as_ref(), "note.txt")),
403
+ (answer, contents(&tmpfs, "note.txt")),
384
404
  (Err("Errno::EEXIST".to_string()), b"hello\nworld\n".to_vec()),
385
405
  "File.open with WRONLY | CREAT | EXCL of a file already there through the bundled guest \
386
406
  must raise Errno::EEXIST and leave the file as it was"
@@ -392,16 +412,25 @@ fn an_integer_mode_opens_by_its_file_constants_flags() {
392
412
  // @behavior FL-028
393
413
  #[test]
394
414
  fn an_exclusive_open_without_create_makes_no_file() {
395
- let memory = Arc::new(Memory::new());
415
+ let tmpfs = Arc::new(Tmpfs::new());
396
416
  let Some(answer) = run(
397
417
  r#"File.open("/data/lock", File::RDONLY | File::EXCL) { }"#,
398
- &[mount(memory.clone(), Access::ReadWrite)],
418
+ &[mount(tmpfs.clone(), Access::ReadWrite)],
399
419
  Vec::new(),
400
420
  ) else {
401
421
  return;
402
422
  };
403
423
  assert_eq!(
404
- (answer, memory.metadata("lock").is_err()),
424
+ (
425
+ answer,
426
+ vfs::vfs_fstatat(
427
+ &prepare_creds_within(&[], None),
428
+ &host(&tmpfs),
429
+ "lock",
430
+ false
431
+ )
432
+ .is_err()
433
+ ),
405
434
  (Err("Errno::ENOENT".to_string()), true),
406
435
  "File.open with RDONLY | EXCL of a path nothing is at through the bundled guest must \
407
436
  raise Errno::ENOENT and make no file"
@@ -427,7 +456,7 @@ fn an_integer_mode_with_a_flag_file_constants_lacks_raises_einval() {
427
456
  "#;
428
457
  let Some(answer) = run(
429
458
  script,
430
- &[mount(note_memory(), Access::ReadWrite)],
459
+ &[mount(note_tmpfs(), Access::ReadWrite)],
431
460
  Vec::new(),
432
461
  ) else {
433
462
  return;
@@ -444,7 +473,7 @@ fn an_integer_mode_with_a_flag_file_constants_lacks_raises_einval() {
444
473
  // @behavior FL-031
445
474
  #[test]
446
475
  fn rdonly_beside_wronly_opens_to_write_only() {
447
- let memory = note_memory();
476
+ let tmpfs = note_tmpfs();
448
477
  let script = r#"
449
478
  File.open("/data/note.txt", File::RDONLY | File::WRONLY) do |file|
450
479
  file.write("J")
@@ -457,13 +486,13 @@ fn rdonly_beside_wronly_opens_to_write_only() {
457
486
  "#;
458
487
  let Some(answer) = run(
459
488
  script,
460
- &[mount(memory.clone(), Access::ReadWrite)],
489
+ &[mount(tmpfs.clone(), Access::ReadWrite)],
461
490
  Vec::new(),
462
491
  ) else {
463
492
  return;
464
493
  };
465
494
  assert_eq!(
466
- (answer, contents(memory.as_ref(), "note.txt")),
495
+ (answer, contents(&tmpfs, "note.txt")),
467
496
  (Ok("IOError".to_string()), b"Jello\nworld\n".to_vec()),
468
497
  "File.open with RDONLY | WRONLY through the bundled guest must write and raise IOError \
469
498
  on a read"
@@ -472,18 +501,18 @@ fn rdonly_beside_wronly_opens_to_write_only() {
472
501
 
473
502
  // Neither open asks to write, so only a mode that reaches the host as it
474
503
  // stands, rather than one refused before it leaves the guest, changes
475
- // the Memory.
504
+ // the Tmpfs.
476
505
  // @behavior FL-032
477
506
  #[test]
478
507
  fn an_integer_mode_may_create_or_truncate_without_asking_to_write() {
479
- let memory = note_memory();
508
+ let tmpfs = note_tmpfs();
480
509
  let script = r#"
481
510
  File.open("/data/lock", File::CREAT | File::EXCL) { }
482
511
  File.open("/data/note.txt", File::RDONLY | File::TRUNC) { }
483
512
  "#;
484
513
  let Some(answer) = run(
485
514
  script,
486
- &[mount(memory.clone(), Access::ReadWrite)],
515
+ &[mount(tmpfs.clone(), Access::ReadWrite)],
487
516
  Vec::new(),
488
517
  ) else {
489
518
  return;
@@ -491,8 +520,8 @@ fn an_integer_mode_may_create_or_truncate_without_asking_to_write() {
491
520
  assert_eq!(
492
521
  (
493
522
  answer,
494
- contents(memory.as_ref(), "lock"),
495
- contents(memory.as_ref(), "note.txt")
523
+ contents(&tmpfs, "lock"),
524
+ contents(&tmpfs, "note.txt")
496
525
  ),
497
526
  (Ok(String::new()), Vec::new(), Vec::new()),
498
527
  "File.open with CREAT | EXCL of a new path and with RDONLY | TRUNC of a file through the \
@@ -503,17 +532,27 @@ fn an_integer_mode_may_create_or_truncate_without_asking_to_write() {
503
532
  // @behavior FL-033
504
533
  #[test]
505
534
  fn a_path_holding_a_nul_byte_raises_argument_error() {
535
+ let script = r#"
536
+ path = "/data/note.txt\0x"
537
+ calls = [-> { File.open(path) { } }, -> { File.read(path) }, -> { File.write(path, "x") },
538
+ -> { File.exist?(path) }, -> { File.stat(path) }, -> { Dir.exist?(path) },
539
+ -> { Dir.children(path) }]
540
+ print calls.map { |call| begin; call.call; "none"; rescue Exception => e; e.class.to_s; end }.join(",")
541
+ "#;
506
542
  let Some(answer) = run(
507
- r#"File.open("/data/note.txt\0x") { }"#,
508
- &[mount(note_memory(), Access::ReadWrite)],
543
+ script,
544
+ &[mount(note_tmpfs(), Access::ReadWrite)],
509
545
  Vec::new(),
510
546
  ) else {
511
547
  return;
512
548
  };
513
549
  assert_eq!(
514
550
  answer,
515
- Err("ArgumentError".to_string()),
516
- "File.open of a path holding a NUL byte through the bundled guest must raise ArgumentError"
551
+ Ok("ArgumentError,ArgumentError,ArgumentError,ArgumentError,ArgumentError,ArgumentError,\
552
+ ArgumentError"
553
+ .to_string()),
554
+ "File.open, File.read, File.write, File.exist?, File.stat, Dir.exist?, and Dir.children of a \
555
+ path holding a NUL byte through the bundled guest must each raise ArgumentError"
517
556
  );
518
557
  }
519
558
 
@@ -538,104 +577,122 @@ fn file_constants_names_the_flags_an_open_honors() {
538
577
  );
539
578
  }
540
579
 
541
- /// A Memory whose every open file is handed through `wrap` first.
580
+ /// A Tmpfs whose every open file is handed through `wrap` first.
542
581
  struct WrapsFiles<W> {
543
- memory: Memory,
544
- wrap: W,
582
+ inner: Arc<dyn InodeOperations>,
583
+ wrap: Arc<W>,
545
584
  }
546
585
 
547
586
  impl<W> WrapsFiles<W> {
548
587
  fn over_note(contents: &str, wrap: W) -> Arc<Self> {
588
+ let tmpfs: Tmpfs = [("note.txt", contents)].into_iter().collect();
549
589
  Arc::new(Self {
550
- memory: [("note.txt", contents)].into_iter().collect(),
551
- wrap,
590
+ inner: Arc::new(tmpfs),
591
+ wrap: Arc::new(wrap),
552
592
  })
553
593
  }
554
594
  }
555
595
 
556
- impl<W, F> Filesystem for WrapsFiles<W>
596
+ impl<W, F> WrapsFiles<W>
557
597
  where
558
- W: Fn(Box<dyn File>) -> F + Send + Sync,
559
- F: File + 'static,
598
+ W: Fn(Box<dyn FileOperations>) -> F + Send + Sync + 'static,
599
+ F: FileOperations + 'static,
560
600
  {
561
- fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
562
- Ok(Box::new((self.wrap)(self.memory.open(path, options)?)))
563
- }
564
-
565
- fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
566
- self.memory.metadata(path)
567
- }
568
-
569
- fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
570
- self.memory.symlink_metadata(path)
571
- }
572
-
573
- fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
574
- self.memory.stored_path(path)
575
- }
576
-
577
- fn canonicalize_beneath(&self, base: &str, path: &str) -> Result<String, FilesystemError> {
578
- self.memory.canonicalize_beneath(base, path)
579
- }
580
-
581
- fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
582
- self.memory.for_policy()
583
- }
584
-
585
- fn hold_directory(&self, path: &str) -> Result<Option<Hold>, FilesystemError> {
586
- self.memory.hold_directory(path)
601
+ /// `found` with the entry it names handing its files through `wrap` too.
602
+ fn wrap_found(&self, found: Lookup) -> Lookup {
603
+ match found {
604
+ Lookup::Found(inode) => Lookup::Found(inode.with_ops(Arc::new(Self {
605
+ inner: inode.ops().clone(),
606
+ wrap: self.wrap.clone(),
607
+ }))),
608
+ other => other,
609
+ }
587
610
  }
611
+ }
588
612
 
589
- fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
590
- self.memory.read_dir(path)
613
+ impl<W, F> FileSystemType for WrapsFiles<W>
614
+ where
615
+ W: Fn(Box<dyn FileOperations>) -> F + Send + Sync + 'static,
616
+ F: FileOperations + 'static,
617
+ {
618
+ fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
619
+ sb.d_make_root(Inode::new(&Kstat::new(FileType::Dir, 0), self));
620
+ Ok(())
591
621
  }
622
+ }
592
623
 
593
- fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
594
- self.memory.create_dir(path)
624
+ impl<W, F> InodeOperations for WrapsFiles<W>
625
+ where
626
+ W: Fn(Box<dyn FileOperations>) -> F + Send + Sync + 'static,
627
+ F: FileOperations + 'static,
628
+ {
629
+ fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
630
+ self.inner.getattr(dentry)
595
631
  }
596
632
 
597
- fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
598
- self.memory.remove_dir(path)
633
+ fn setattr(
634
+ &self,
635
+ dentry: &Dentry,
636
+ attr: &kobako_runtime::filesystem::Iattr,
637
+ ) -> Result<(), FilesystemError> {
638
+ self.inner.setattr(dentry, attr)
599
639
  }
600
640
 
601
- fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
602
- self.memory.remove_file(path)
641
+ fn lookup(&self, dentry: &Dentry) -> Result<Lookup, FilesystemError> {
642
+ Ok(self.wrap_found(self.inner.lookup(dentry)?))
603
643
  }
604
644
 
605
- fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
606
- self.memory.rename(from, to, rename)
645
+ fn open(
646
+ &self,
647
+ dentry: &Dentry,
648
+ how: &OpenHow,
649
+ ) -> Result<Box<dyn FileOperations>, FilesystemError> {
650
+ let opened = self.inner.open(dentry, how)?;
651
+ if dentry
652
+ .inode()
653
+ .is_none_or(|inode| inode.kind() == kobako_runtime::filesystem::FileType::Dir)
654
+ {
655
+ return Ok(opened);
656
+ }
657
+ Ok(Box::new((self.wrap)(opened)))
607
658
  }
608
659
 
609
- fn for_invocation(&self) -> Option<Arc<dyn Filesystem>> {
610
- self.memory.for_invocation()
660
+ /// Opens what it made, as a Filesystem ending in `finish_open` does, so the
661
+ /// new file is wrapped too; what it only looked up is handed back wrapped.
662
+ fn atomic_open(&self, dentry: &Dentry, how: &OpenHow) -> Result<Finish, FilesystemError> {
663
+ let (made, created) = match self.inner.atomic_open(dentry, how)? {
664
+ Finish::Open { file, created } => (file, created),
665
+ Finish::NoOpen { created } => (self.inner.open(dentry, how)?, created),
666
+ Finish::Lookup(found) => return Ok(Finish::Lookup(self.wrap_found(found))),
667
+ Finish::Spelled { file, name, inode } => {
668
+ let Lookup::Found(inode) = self.wrap_found(Lookup::Found(inode)) else {
669
+ unreachable!("a found entry stays found once wrapped");
670
+ };
671
+ let file = Box::new((self.wrap)(file));
672
+ return Ok(Finish::Spelled { file, name, inode });
673
+ }
674
+ _ => unreachable!("Finish has no other end"),
675
+ };
676
+ Ok(Finish::Open {
677
+ file: Box::new((self.wrap)(made)),
678
+ created,
679
+ })
611
680
  }
612
681
  }
613
682
 
614
683
  struct CountedFile {
615
- file: Box<dyn File>,
684
+ file: Box<dyn FileOperations>,
616
685
  reads: Arc<AtomicUsize>,
617
686
  }
618
687
 
619
- impl File for CountedFile {
620
- fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
688
+ impl FileOperations for CountedFile {
689
+ fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
621
690
  self.reads.fetch_add(1, Ordering::Relaxed);
622
- self.file.read_at(buf, offset)
623
- }
624
-
625
- fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
626
- self.file.write_at(buf, offset)
627
- }
628
-
629
- fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
630
- self.file.append(buf)
631
- }
632
-
633
- fn metadata(&self) -> Result<Metadata, FilesystemError> {
634
- self.file.metadata()
691
+ self.file.read_iter(iocb, buf)
635
692
  }
636
693
 
637
- fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
638
- self.file.set_len(len)
694
+ fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
695
+ self.file.write_iter(iocb, buf)
639
696
  }
640
697
  }
641
698
 
@@ -667,56 +724,32 @@ fn a_read_of_a_length_asks_the_filesystem_once() {
667
724
 
668
725
  /// An open file whose Filesystem refuses to release it, as a database
669
726
  /// refusing the commit a close stands for.
670
- struct RefusesClose(Box<dyn File>);
727
+ struct RefusesClose(Box<dyn FileOperations>);
671
728
 
672
- impl File for RefusesClose {
673
- fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
674
- self.0.read_at(buf, offset)
729
+ impl FileOperations for RefusesClose {
730
+ fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
731
+ self.0.read_iter(iocb, buf)
675
732
  }
676
733
 
677
- fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
678
- self.0.write_at(buf, offset)
734
+ fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
735
+ self.0.write_iter(iocb, buf)
679
736
  }
680
737
 
681
- fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
682
- self.0.append(buf)
683
- }
684
-
685
- fn metadata(&self) -> Result<Metadata, FilesystemError> {
686
- self.0.metadata()
687
- }
688
-
689
- fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
690
- self.0.set_len(len)
691
- }
692
-
693
- fn close(self: Box<Self>) -> Result<(), FilesystemError> {
738
+ fn flush(&mut self) -> Result<(), FilesystemError> {
694
739
  Err(FilesystemError::PermissionDenied)
695
740
  }
696
741
  }
697
742
 
698
743
  /// An open file whose every read fails in a way no error code names.
699
- struct FailsReads(Box<dyn File>);
744
+ struct FailsReads(Box<dyn FileOperations>);
700
745
 
701
- impl File for FailsReads {
702
- fn read_at(&mut self, _buf: &mut [u8], _offset: u64) -> Result<usize, FilesystemError> {
746
+ impl FileOperations for FailsReads {
747
+ fn read_iter(&mut self, _iocb: &mut Kiocb, _buf: &mut [u8]) -> Result<usize, FilesystemError> {
703
748
  Err(FilesystemError::Other("the disk went away".to_string()))
704
749
  }
705
750
 
706
- fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
707
- self.0.write_at(buf, offset)
708
- }
709
-
710
- fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
711
- self.0.append(buf)
712
- }
713
-
714
- fn metadata(&self) -> Result<Metadata, FilesystemError> {
715
- self.0.metadata()
716
- }
717
-
718
- fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
719
- self.0.set_len(len)
751
+ fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
752
+ self.0.write_iter(iocb, buf)
720
753
  }
721
754
  }
722
755
 
@@ -758,27 +791,15 @@ fn a_close_the_filesystem_refuses_fails_the_scripts_close() {
758
791
 
759
792
  /// An open file taking at most three bytes per write, as a Filesystem a
760
793
  /// Host App writes in Ruby may.
761
- struct WritesInParts(Box<dyn File>);
762
-
763
- impl File for WritesInParts {
764
- fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
765
- self.0.read_at(buf, offset)
766
- }
794
+ struct WritesInParts(Box<dyn FileOperations>);
767
795
 
768
- fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
769
- self.0.write_at(&buf[..buf.len().min(3)], offset)
796
+ impl FileOperations for WritesInParts {
797
+ fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
798
+ self.0.read_iter(iocb, buf)
770
799
  }
771
800
 
772
- fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
773
- self.0.append(&buf[..buf.len().min(3)])
774
- }
775
-
776
- fn metadata(&self) -> Result<Metadata, FilesystemError> {
777
- self.0.metadata()
778
- }
779
-
780
- fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
781
- self.0.set_len(len)
801
+ fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
802
+ self.0.write_iter(iocb, &buf[..buf.len().min(3)])
782
803
  }
783
804
  }
784
805
 
@@ -793,7 +814,7 @@ fn an_errno_a_script_raises_reads_as_crubys() {
793
814
  out << e.class.to_s << e.errno
794
815
  print out.inspect
795
816
  "#;
796
- let Some(answer) = run(script, &[mount(note_memory(), Access::Read)], Vec::new()) else {
817
+ let Some(answer) = run(script, &[mount(note_tmpfs(), Access::Read)], Vec::new()) else {
797
818
  return;
798
819
  };
799
820
  assert_eq!(
@@ -812,7 +833,7 @@ fn an_errno_a_script_raises_reads_as_crubys() {
812
833
  fn a_system_call_error_keeps_a_number_no_class_names() {
813
834
  let Some(answer) = run(
814
835
  r#"e = SystemCallError.new(9999); print [e.class.to_s, e.message, e.errno].inspect"#,
815
- &[mount(note_memory(), Access::Read)],
836
+ &[mount(note_tmpfs(), Access::Read)],
816
837
  Vec::new(),
817
838
  ) else {
818
839
  return;
@@ -835,7 +856,7 @@ fn a_read_length_past_64_bits_raises_range_error() {
835
856
  print e.class.to_s
836
857
  end
837
858
  "#;
838
- let Some(answer) = run(script, &[mount(note_memory(), Access::Read)], Vec::new()) else {
859
+ let Some(answer) = run(script, &[mount(note_tmpfs(), Access::Read)], Vec::new()) else {
839
860
  return;
840
861
  };
841
862
  assert_eq!(
@@ -859,7 +880,7 @@ fn a_system_call_error_number_past_the_errno_range_raises_range_error() {
859
880
  end
860
881
  print out.inspect
861
882
  "#;
862
- let Some(answer) = run(script, &[mount(note_memory(), Access::Read)], Vec::new()) else {
883
+ let Some(answer) = run(script, &[mount(note_tmpfs(), Access::Read)], Vec::new()) else {
863
884
  return;
864
885
  };
865
886
  assert_eq!(
@@ -884,7 +905,7 @@ fn a_read_or_write_a_stream_was_not_opened_for_raises_ioerror() {
884
905
  "#;
885
906
  let Some(answer) = run(
886
907
  script,
887
- &[mount(note_memory(), Access::ReadWrite)],
908
+ &[mount(note_tmpfs(), Access::ReadWrite)],
888
909
  Vec::new(),
889
910
  ) else {
890
911
  return;
@@ -905,7 +926,7 @@ fn a_read_or_write_a_stream_was_not_opened_for_raises_ioerror() {
905
926
  fn a_path_that_is_no_valid_utf8_raises_eilseq() {
906
927
  let Some(answer) = run(
907
928
  r#"File.read("/data/\xff")"#,
908
- &[mount(note_memory(), Access::Read)],
929
+ &[mount(note_tmpfs(), Access::Read)],
909
930
  Vec::new(),
910
931
  ) else {
911
932
  return;
@@ -929,7 +950,7 @@ fn a_write_lands_whole_when_the_filesystem_takes_it_in_parts() {
929
950
  return;
930
951
  };
931
952
  assert_eq!(
932
- (answer, contents(filesystem.as_ref(), "note.txt")),
953
+ (answer, contents(&filesystem,"note.txt")),
933
954
  (Ok("11".to_string()), b"hello world".to_vec()),
934
955
  "IO#write to a file whose Filesystem takes three bytes per write through the bundled guest \
935
956
  must answer every byte and land them all"
@@ -938,35 +959,45 @@ fn a_write_lands_whole_when_the_filesystem_takes_it_in_parts() {
938
959
 
939
960
  /// An open file reporting a length it does not hold, so a script meets a
940
961
  /// file of several GiB without the test allocating one.
941
- struct ReportsLen(Box<dyn File>, u64);
962
+ struct ReportsLen(Arc<dyn InodeOperations>, u64);
942
963
 
943
- impl File for ReportsLen {
944
- fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
945
- self.0.read_at(buf, offset)
946
- }
947
-
948
- fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
949
- self.0.write_at(buf, offset)
964
+ impl FileSystemType for ReportsLen {
965
+ fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
966
+ sb.d_make_root(Inode::new(&Kstat::new(FileType::Dir, 0), self));
967
+ Ok(())
950
968
  }
969
+ }
951
970
 
952
- fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
953
- self.0.append(buf)
971
+ impl InodeOperations for ReportsLen {
972
+ fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
973
+ let mut stat = self.0.getattr(dentry)?;
974
+ if stat.file_type == FileType::RegFile {
975
+ stat.size = self.1;
976
+ }
977
+ Ok(stat)
954
978
  }
955
979
 
956
- fn metadata(&self) -> Result<Metadata, FilesystemError> {
957
- let mut metadata = self.0.metadata()?;
958
- metadata.len = self.1;
959
- Ok(metadata)
980
+ fn lookup(&self, dentry: &Dentry) -> Result<Lookup, FilesystemError> {
981
+ Ok(match self.0.lookup(dentry)? {
982
+ Lookup::Found(inode) => {
983
+ Lookup::Found(inode.with_ops(Arc::new(Self(inode.ops().clone(), self.1))))
984
+ }
985
+ other => other,
986
+ })
960
987
  }
961
988
 
962
- fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
963
- self.0.set_len(len)
989
+ fn open(
990
+ &self,
991
+ dentry: &Dentry,
992
+ how: &OpenHow,
993
+ ) -> Result<Box<dyn FileOperations>, FilesystemError> {
994
+ self.0.open(dentry, how)
964
995
  }
965
996
  }
966
997
 
967
998
  fn three_gib_note() -> Grant {
968
- let filesystem = WrapsFiles::over_note("hello", |file| ReportsLen(file, 3 << 30));
969
- mount(filesystem, Access::Read)
999
+ let tmpfs: Tmpfs = [("note.txt", "hello")].into_iter().collect();
1000
+ mount(Arc::new(ReportsLen(Arc::new(tmpfs), 3 << 30)), Access::Read)
970
1001
  }
971
1002
 
972
1003
  // @behavior FL-020
@@ -1010,7 +1041,7 @@ fn a_position_past_2_gib_is_reported_whole() {
1010
1041
  fn setting_a_files_position_moves_where_the_next_read_starts() {
1011
1042
  let Some(answer) = run(
1012
1043
  r#"File.open("/data/note.txt") { |f| f.pos = 2; print f.read }"#,
1013
- &[mount(note_memory(), Access::Read)],
1044
+ &[mount(note_tmpfs(), Access::Read)],
1014
1045
  Vec::new(),
1015
1046
  ) else {
1016
1047
  return;
@@ -1040,7 +1071,7 @@ fn a_seek_before_the_start_or_by_an_unknown_whence_raises_einval() {
1040
1071
  end
1041
1072
  end
1042
1073
  nil"#,
1043
- &[mount(note_memory(), Access::Read)],
1074
+ &[mount(note_tmpfs(), Access::Read)],
1044
1075
  Vec::new(),
1045
1076
  ) else {
1046
1077
  return;