kobako 0.28.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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +377 -0
  4. data/Cargo.lock +181 -154
  5. data/README.md +99 -459
  6. data/ROADMAP.md +7 -1
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +354 -0
  9. data/crates/kobako-runtime/Cargo.toml +38 -7
  10. data/crates/kobako-runtime/README.md +17 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +9 -1
  12. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  13. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  14. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  15. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  16. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  17. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  18. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  19. data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
  20. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  21. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  29. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
  30. data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
  31. data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
  32. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  33. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  37. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  38. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  39. data/crates/kobako-runtime/src/filesystem.rs +26 -0
  40. data/crates/kobako-runtime/src/grant.rs +20 -0
  41. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  42. data/crates/kobako-runtime/src/lib.rs +16 -1
  43. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  44. data/crates/kobako-runtime/src/locking.rs +218 -0
  45. data/crates/kobako-runtime/src/mount.rs +105 -0
  46. data/crates/kobako-runtime/src/profile.rs +140 -4
  47. data/crates/kobako-runtime/src/runtime.rs +12 -0
  48. data/crates/kobako-runtime/src/sched.rs +210 -0
  49. data/crates/kobako-runtime/src/security.rs +696 -0
  50. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  51. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  52. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  53. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  54. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  55. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  56. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  57. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  58. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  59. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  60. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  61. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  62. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  63. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  64. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  65. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  66. data/crates/kobako-runtime/src/vfs.rs +60 -0
  67. data/crates/kobako-runtime/src/yielder.rs +11 -6
  68. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  70. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  71. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  72. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  73. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  74. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  75. data/crates/kobako-transport/CHANGELOG.md +23 -0
  76. data/crates/kobako-transport/Cargo.toml +3 -3
  77. data/crates/kobako-transport/README.md +6 -5
  78. data/crates/kobako-transport/src/abi.rs +1 -1
  79. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  80. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  81. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  83. data/crates/kobako-transport/src/envelope/fault.rs +4 -3
  84. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  85. data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
  86. data/crates/kobako-transport/src/envelope/reply.rs +34 -16
  87. data/crates/kobako-transport/src/envelope/request.rs +19 -9
  88. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  89. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  90. data/crates/kobako-transport/src/envelope.rs +2 -2
  91. data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
  92. data/crates/kobako-wasmtime/Cargo.toml +25 -19
  93. data/crates/kobako-wasmtime/README.md +13 -7
  94. data/crates/kobako-wasmtime/src/abi.rs +75 -49
  95. data/crates/kobako-wasmtime/src/ambient.rs +64 -6
  96. data/crates/kobako-wasmtime/src/cache.rs +188 -77
  97. data/crates/kobako-wasmtime/src/config.rs +77 -5
  98. data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
  99. data/crates/kobako-wasmtime/src/driver.rs +70 -135
  100. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  101. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  102. data/crates/kobako-wasmtime/src/imports.rs +52 -0
  103. data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
  104. data/crates/kobako-wasmtime/src/invocation.rs +43 -74
  105. data/crates/kobako-wasmtime/src/lib.rs +5 -4
  106. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  107. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
  108. data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
  109. data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
  110. data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
  111. data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
  112. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  113. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
  114. data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
  115. data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
  116. data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
  117. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  118. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  119. data/crates/kobako-wasmtime/src/preview1.rs +512 -0
  120. data/crates/kobako-wasmtime/src/trap.rs +45 -25
  121. data/crates/kobako-wasmtime/src/wasi.rs +333 -0
  122. data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
  123. data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
  124. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  125. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
  126. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
  127. data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
  128. data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
  129. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
  130. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  131. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  132. data/data/kobako.wasm +0 -0
  133. data/ext/kobako/Cargo.toml +4 -2
  134. data/ext/kobako/src/runtime/bridge.rs +154 -81
  135. data/ext/kobako/src/runtime/errors.rs +1 -1
  136. data/ext/kobako/src/runtime/escape.rs +67 -0
  137. data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
  138. data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
  139. data/ext/kobako/src/runtime/filesystem.rs +208 -0
  140. data/ext/kobako/src/runtime/gvl.rs +49 -8
  141. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  142. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  143. data/ext/kobako/src/runtime.rs +125 -199
  144. data/lib/kobako/catalog/extensions.rb +5 -5
  145. data/lib/kobako/catalog/flags.rb +2 -14
  146. data/lib/kobako/catalog/grants.rb +30 -0
  147. data/lib/kobako/catalog/handles.rb +6 -6
  148. data/lib/kobako/catalog/mount.rb +19 -0
  149. data/lib/kobako/catalog/services.rb +13 -2
  150. data/lib/kobako/catalog/snippets.rb +11 -15
  151. data/lib/kobako/catalog.rb +11 -0
  152. data/lib/kobako/codec/decoder.rb +10 -9
  153. data/lib/kobako/codec/ext_types.rb +4 -6
  154. data/lib/kobako/codec/handle_walk.rb +4 -6
  155. data/lib/kobako/codec/nesting.rb +7 -7
  156. data/lib/kobako/codec/state.rb +19 -3
  157. data/lib/kobako/codec/utils.rb +1 -1
  158. data/lib/kobako/codec/value_budget.rb +86 -0
  159. data/lib/kobako/codec.rb +7 -0
  160. data/lib/kobako/context.rb +40 -13
  161. data/lib/kobako/errors.rb +15 -14
  162. data/lib/kobako/filesystem.rb +69 -0
  163. data/lib/kobako/payload/arguments.rb +2 -2
  164. data/lib/kobako/pool.rb +6 -6
  165. data/lib/kobako/sandbox.rb +55 -27
  166. data/lib/kobako/sandbox_options.rb +39 -13
  167. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
  168. data/lib/kobako/snippet/source.rb +1 -1
  169. data/lib/kobako/snippet.rb +1 -1
  170. data/lib/kobako/transport/block_failure.rb +35 -0
  171. data/lib/kobako/transport/dispatcher.rb +33 -29
  172. data/lib/kobako/transport/exposure.rb +7 -9
  173. data/lib/kobako/transport/reflection.rb +79 -23
  174. data/lib/kobako/transport/run.rb +5 -5
  175. data/lib/kobako/transport/yielder.rb +9 -31
  176. data/lib/kobako/transport.rb +1 -0
  177. data/lib/kobako/version.rb +1 -1
  178. data/lib/kobako.rb +35 -0
  179. data/release-please-config.json +1 -0
  180. data/sig/kobako/catalog/extensions.rbs +3 -3
  181. data/sig/kobako/catalog/flags.rbs +0 -4
  182. data/sig/kobako/catalog/grants.rbs +13 -0
  183. data/sig/kobako/catalog/handles.rbs +3 -3
  184. data/sig/kobako/catalog/mount.rbs +24 -0
  185. data/sig/kobako/catalog/services.rbs +5 -1
  186. data/sig/kobako/catalog/snippets.rbs +4 -4
  187. data/sig/kobako/catalog.rbs +1 -0
  188. data/sig/kobako/codec/decoder.rbs +1 -1
  189. data/sig/kobako/codec/handle_walk.rbs +1 -1
  190. data/sig/kobako/codec/nesting.rbs +2 -2
  191. data/sig/kobako/codec/state.rbs +7 -1
  192. data/sig/kobako/codec/utils.rbs +1 -1
  193. data/sig/kobako/codec/value_budget.rbs +23 -0
  194. data/sig/kobako/codec.rbs +3 -2
  195. data/sig/kobako/context.rbs +14 -6
  196. data/sig/kobako/errors.rbs +4 -4
  197. data/sig/kobako/filesystem.rbs +93 -0
  198. data/sig/kobako/guest_yielder.rbs +9 -0
  199. data/sig/kobako/payload/arguments.rbs +1 -1
  200. data/sig/kobako/pool.rbs +3 -3
  201. data/sig/kobako/runtime.rbs +21 -49
  202. data/sig/kobako/sandbox.rbs +9 -5
  203. data/sig/kobako/sandbox_options.rbs +25 -3
  204. data/sig/kobako/snapshot.rbs +20 -0
  205. data/sig/kobako/snippet/bytecode.rbs +12 -0
  206. data/sig/kobako/transport/block_failure.rbs +13 -0
  207. data/sig/kobako/transport/dispatcher.rbs +4 -4
  208. data/sig/kobako/transport/exposure.rbs +2 -2
  209. data/sig/kobako/transport/reflection.rbs +14 -1
  210. data/sig/kobako/transport/run.rbs +3 -3
  211. data/sig/kobako/transport/yielder.rbs +3 -7
  212. data/sig/kobako/transport.rbs +3 -3
  213. metadata +116 -15
  214. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  215. data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
  216. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
  217. data/crates/kobako-wasmtime/src/io.rs +0 -286
  218. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -0,0 +1,708 @@
1
+ //! A Filesystem a Host App writes in Ruby: any object answering `open`
2
+ //! and `stat`, asked through `gvl::reenter` on every call the guest makes.
3
+ //!
4
+ //! Its methods take the names and shapes of Ruby's own `File`, `Dir`, and
5
+ //! `IO`, so a Host App can hand an open call on to a real `::File`. A
6
+ //! change method the object lacks answers as read-only; any other method
7
+ //! it lacks answers as unsupported. A file's `EOFError` from `pread` is
8
+ //! its end, an `Errno` the guest has a class for reaches the guest as
9
+ //! that code, and any other `StandardError` reaches the guest as `EIO`.
10
+
11
+ use magnus::value::{Opaque, ReprValue};
12
+ use magnus::{prelude::*, Error as MagnusError, RClass, RModule, RString, Ruby, Value};
13
+
14
+ use std::sync::{Arc, Mutex, PoisonError, Weak};
15
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
16
+
17
+ use kobako_runtime::filesystem::Rdir;
18
+ use kobako_runtime::filesystem::{
19
+ DentryOperations, DirEntry, FileOperations, FileSystemType, FileType, Filesystem,
20
+ FilesystemError, Finish, Iattr, InodeOperations, Kiocb, Kstat, Lookup, OpenHow, RenameFlag,
21
+ };
22
+ use kobako_runtime::vfs::{
23
+ generic_file_llseek, generic_read_dir, generic_write_sync, verify_dirent_name, Dentry,
24
+ DirContext, Identity, Inode, Whence, NAME_MAX,
25
+ };
26
+
27
+ use crate::runtime::pinned_value::PinnedValue;
28
+ use crate::runtime::{escape, gvl};
29
+
30
+ /// The Ruby object behind a Mount or a Layer. Whatever holds it on the
31
+ /// Ruby side keeps it alive and in place: the invocation pinning its
32
+ /// Mounts, or the Overlay marking its Layers. The object answers by path,
33
+ /// so each call asks it by the path the dentry stands at now, as CIFS
34
+ /// builds one from the dentry, and a directory opened beneath it is the
35
+ /// same object.
36
+ #[derive(Clone, Copy)]
37
+ pub(crate) struct RubyFilesystem {
38
+ object: Opaque<Value>,
39
+ /// The device a stat without a `dev` names, the object's own, shared by
40
+ /// every directory opened beneath it.
41
+ dev: u64,
42
+ }
43
+
44
+ impl RubyFilesystem {
45
+ /// The object as a Filesystem, when it answers `open` and `stat`.
46
+ pub(crate) fn from_value(value: Value) -> Option<Self> {
47
+ let answers = |name| value.respond_to(name, false).unwrap_or(false);
48
+ (answers("open") && answers("stat")).then(|| Self {
49
+ object: Opaque::from(value),
50
+ dev: Kstat::unique_device(),
51
+ })
52
+ }
53
+
54
+ /// What the object's `stat` answers for `path`.
55
+ fn stat(&self, path: String) -> Result<Kstat, FilesystemError> {
56
+ let own_dev = self.dev;
57
+ ask(
58
+ self.object,
59
+ "stat",
60
+ FilesystemError::Unsupported,
61
+ |_, fs| {
62
+ let stat: Value = fs.funcall("stat", (path,))?;
63
+ let kstat = Kstat::new(kind_of(stat)?, stat.funcall("size", ())?);
64
+ // A stat names its identity as `File::Stat` does; without a
65
+ // `dev` the Filesystem stands for a device of its own, so it
66
+ // shares an identity with no other Filesystem.
67
+ let dev = if stat.respond_to("dev", false)? {
68
+ stat.funcall("dev", ())?
69
+ } else {
70
+ own_dev
71
+ };
72
+ with_stat_record(stat, kstat.with_identity(dev, stat.funcall("ino", ())?))
73
+ },
74
+ )
75
+ }
76
+ }
77
+
78
+ /// What a stat says its entry is. A `File::Stat` of a special file or a link
79
+ /// says so through `ftype`, so it is judged as what it is rather than opened.
80
+ fn kind_of(stat: Value) -> Result<FileType, MagnusError> {
81
+ if stat.funcall::<_, _, bool>("directory?", ())? {
82
+ return Ok(FileType::Dir);
83
+ }
84
+ if !stat.respond_to("ftype", false)? {
85
+ return Ok(FileType::RegFile);
86
+ }
87
+ let ftype: String = stat.funcall("ftype", ())?;
88
+ Ok(match ftype.as_str() {
89
+ "link" => FileType::Symlink,
90
+ "fifo" => FileType::Fifo,
91
+ "socket" => FileType::Sock,
92
+ "blockSpecial" => FileType::Blkdev,
93
+ "characterSpecial" => FileType::Chrdev,
94
+ _ => FileType::RegFile,
95
+ })
96
+ }
97
+
98
+ /// `kstat` with the times and the count of names `stat` records.
99
+ fn with_stat_record(stat: Value, kstat: Kstat) -> Result<Kstat, MagnusError> {
100
+ let kstat = kstat.with_times(
101
+ time_of(stat, "atime")?,
102
+ time_of(stat, "mtime")?,
103
+ time_of(stat, "ctime")?,
104
+ );
105
+ Ok(if stat.respond_to("nlink", false)? {
106
+ kstat.with_nlink(stat.funcall("nlink", ())?)
107
+ } else {
108
+ kstat
109
+ })
110
+ }
111
+
112
+ /// Ask `object` its `name` method with the GVL held, `answer` making the
113
+ /// call and reading what it returns before Ruby runs anything else. An
114
+ /// object lacking the method fails as `missing`.
115
+ fn ask<T>(
116
+ object: Opaque<Value>,
117
+ name: &str,
118
+ missing: FilesystemError,
119
+ answer: impl FnOnce(&Ruby, Value) -> Result<T, MagnusError>,
120
+ ) -> Result<T, FilesystemError> {
121
+ ask_or(object, name, Err(missing), answer)
122
+ }
123
+
124
+ /// As `ask`, an object lacking the method answering `missing`.
125
+ fn ask_or<T>(
126
+ object: Opaque<Value>,
127
+ name: &str,
128
+ missing: Result<T, FilesystemError>,
129
+ answer: impl FnOnce(&Ruby, Value) -> Result<T, MagnusError>,
130
+ ) -> Result<T, FilesystemError> {
131
+ gvl::reenter(|| {
132
+ let ruby = Ruby::get().expect("Ruby handle unavailable in a Filesystem call");
133
+ let object: Value = ruby.get_inner(object);
134
+ if !object.respond_to(name, false).unwrap_or(false) {
135
+ return missing;
136
+ }
137
+ answer(&ruby, object).map_err(|err| match escape::hold(&ruby, err) {
138
+ Err(err) => to_error(err),
139
+ // The invocation ends before the guest reads this.
140
+ Ok(()) => FilesystemError::Other(String::new()),
141
+ })
142
+ })
143
+ }
144
+
145
+ /// Ask `object` a change, answering nothing more than whether it was made.
146
+ fn change(
147
+ object: Opaque<Value>,
148
+ name: &str,
149
+ args: impl magnus::ArgList,
150
+ missing: FilesystemError,
151
+ ) -> Result<(), FilesystemError> {
152
+ ask(object, name, missing, |_, object| {
153
+ object.funcall::<_, _, Value>(name, args).map(drop)
154
+ })
155
+ }
156
+
157
+ /// Ask `object` a change it may lack, an object lacking `name` having
158
+ /// nothing of its own to change, while one that answers and refuses still
159
+ /// fails, whatever `Errno` it raises.
160
+ fn change_if_answered(object: Opaque<Value>, name: &str) -> Result<(), FilesystemError> {
161
+ ask_or(object, name, Ok(()), |_, object| {
162
+ object.funcall::<_, _, Value>(name, ()).map(drop)
163
+ })
164
+ }
165
+
166
+ /// The FilesystemError the nearest `Errno` class of `err` stands for, by
167
+ /// its POSIX name; anything else is `Other`, which reaches the guest as
168
+ /// `EIO`.
169
+ fn to_error(err: MagnusError) -> FilesystemError {
170
+ let named = err.value().and_then(|exception| {
171
+ exception
172
+ .class()
173
+ .ancestors()
174
+ .into_iter()
175
+ .find_map(|module| {
176
+ let name: Option<String> = module.funcall("name", ()).ok()?;
177
+ FilesystemError::from_posix_name(name?.strip_prefix("Errno::")?)
178
+ })
179
+ });
180
+ named.unwrap_or_else(|| FilesystemError::Other(err.to_string()))
181
+ }
182
+
183
+ /// `File::Constants` flags for `how`, read from Ruby since their
184
+ /// values differ by platform.
185
+ fn to_flags(ruby: &Ruby, how: &OpenHow, create: bool) -> Result<i64, MagnusError> {
186
+ let constants: RModule = ruby
187
+ .class_object()
188
+ .const_get::<_, RClass>("File")?
189
+ .const_get("Constants")?;
190
+ let flag = |name| constants.const_get::<_, i64>(name);
191
+ let mut flags = match (how.read, how.write || how.truncate) {
192
+ (_, false) => flag("RDONLY")?,
193
+ (false, true) => flag("WRONLY")?,
194
+ (true, true) => flag("RDWR")?,
195
+ };
196
+ if create {
197
+ flags |= flag("CREAT")? | flag("EXCL")?;
198
+ }
199
+ if how.truncate {
200
+ flags |= flag("TRUNC")?;
201
+ }
202
+ // The runtime follows a link itself, so no open follows one, wherever
203
+ // the host's Ruby can say so: Windows defines no `NOFOLLOW`.
204
+ if constants.funcall::<_, _, bool>("const_defined?", ("NOFOLLOW",))? {
205
+ flags |= flag("NOFOLLOW")?;
206
+ }
207
+ Ok(flags)
208
+ }
209
+
210
+ /// The `Time` a stat answers to `name`, as `File::Stat#mtime` gives one;
211
+ /// `None` where it answers none, or a time before the Unix epoch.
212
+ fn time_of(stat: Value, name: &str) -> Result<Option<SystemTime>, MagnusError> {
213
+ if !stat.respond_to(name, false)? {
214
+ return Ok(None);
215
+ }
216
+ let time: Value = stat.funcall(name, ())?;
217
+ let seconds: i64 = time.funcall("to_i", ())?;
218
+ let nanoseconds: u32 = time.funcall("nsec", ())?;
219
+ Ok(u64::try_from(seconds)
220
+ .ok()
221
+ .and_then(|seconds| UNIX_EPOCH.checked_add(Duration::new(seconds, nanoseconds))))
222
+ }
223
+
224
+ /// The path the object is asked about for `dentry`: where it stands beneath the
225
+ /// root, with no leading `/`, so an object joining it to a directory of its own
226
+ /// stays within that directory, empty for the root.
227
+ fn path_of(dentry: &Dentry) -> String {
228
+ dentry.path_raw()[1..].to_owned()
229
+ }
230
+
231
+ fn join(dir: &str, name: &str) -> String {
232
+ if dir.is_empty() {
233
+ name.to_string()
234
+ } else {
235
+ format!("{dir}/{name}")
236
+ }
237
+ }
238
+
239
+ impl RubyFilesystem {
240
+ /// Open the file `dentry` names and keep it on the inode, so the entry
241
+ /// answers from it however its name changes, as 9p keeps an open fid.
242
+ /// What this made is recorded first, as `->atomic_open` calls `d_instantiate`.
243
+ fn open_at(
244
+ &self,
245
+ dentry: &Dentry,
246
+ how: &OpenHow,
247
+ create: bool,
248
+ ) -> Result<Box<dyn FileOperations>, FilesystemError> {
249
+ let path = path_of(dentry);
250
+ let file = ask(
251
+ self.object,
252
+ "open",
253
+ FilesystemError::Unsupported,
254
+ |ruby, fs| {
255
+ let file: Value = fs.funcall("open", (path, to_flags(ruby, how, create)?))?;
256
+ Ok(RubyFile::pin(ruby, file))
257
+ },
258
+ )?;
259
+ match dentry.inode().map(|inode| inode.identity()) {
260
+ None => {
261
+ let made = self.stat(path_of(dentry))?;
262
+ dentry.add(Some(Inode::new(&made, Arc::new(RubyRegular(*self)))));
263
+ }
264
+ // The object opens by path, so whatever stands there now may be
265
+ // another entry than the one judged, as `Bind::open_file` finds.
266
+ Some(Some(identity)) => {
267
+ if Identity::of(&self.stat(path_of(dentry))?) != Some(identity) {
268
+ return Err(FilesystemError::NotFound);
269
+ }
270
+ }
271
+ Some(None) => {}
272
+ }
273
+ let inode = dentry.inode().ok_or(FilesystemError::NotFound)?;
274
+ let kept = inode
275
+ .private()
276
+ .get_or_init(|| Arc::new(OpenRubyFile::default()))
277
+ .clone();
278
+ if let Ok(open) = kept.downcast::<OpenRubyFile>() {
279
+ *open.0.lock().unwrap_or_else(PoisonError::into_inner) = Arc::downgrade(&file.file);
280
+ }
281
+ Ok(Box::new(file))
282
+ }
283
+
284
+ /// The entries of the directory. No name a directory cannot hold is asked
285
+ /// about, as it could name a path the object would answer for, and a name
286
+ /// gone by the time it is asked about is one the listing no longer holds, as
287
+ /// any listing is a moment that may have passed.
288
+ fn entries_at(&self, dentry: &Dentry) -> Result<Vec<DirEntry>, FilesystemError> {
289
+ let at = path_of(dentry);
290
+ let names: Vec<String> = ask(
291
+ self.object,
292
+ "children",
293
+ FilesystemError::Unsupported,
294
+ |_, fs| fs.funcall("children", (at.as_str(),)),
295
+ )?;
296
+ names.iter().try_for_each(|name| verify_dirent_name(name))?;
297
+ let mut entries = Vec::with_capacity(names.len());
298
+ for name in names {
299
+ match self.stat(join(&at, &name)) {
300
+ Ok(stat) => entries
301
+ .push(DirEntry::new(name, stat.file_type).with_identity(stat.dev, stat.ino)),
302
+ Err(FilesystemError::NotFound) => {}
303
+ Err(err) => return Err(err),
304
+ }
305
+ }
306
+ Ok(entries)
307
+ }
308
+ }
309
+
310
+ /// A link a stat reports. A Ruby object answers no `readlink`, so no walk goes
311
+ /// on through one, as a path past too many links stops.
312
+ struct RubyLink(RubyFilesystem);
313
+
314
+ impl InodeOperations for RubyLink {
315
+ fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
316
+ self.0.stat(path_of(dentry))
317
+ }
318
+
319
+ fn get_link(&self, _dentry: &Dentry) -> Result<String, FilesystemError> {
320
+ Err(FilesystemError::FilesystemLoop)
321
+ }
322
+ }
323
+
324
+ /// Any entry of a Ruby Filesystem but a directory.
325
+ struct RubyRegular(RubyFilesystem);
326
+
327
+ impl InodeOperations for RubyRegular {
328
+ /// A file open here answers for itself even once its name is gone, as 9p
329
+ /// asks an open fid: its `size`, and its `stat` where it has one. It stays
330
+ /// the entry its lookup recorded; anything else is asked by its path.
331
+ fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
332
+ let Some(file) = find_open_file(dentry) else {
333
+ return self.0.stat(path_of(dentry));
334
+ };
335
+ let metadata = ask(
336
+ file.get(),
337
+ "size",
338
+ FilesystemError::Unsupported,
339
+ |_, file| {
340
+ let metadata = Kstat::new(FileType::RegFile, file.funcall("size", ())?);
341
+ if file.respond_to("stat", false)? {
342
+ with_stat_record(file.funcall("stat", ())?, metadata)
343
+ } else {
344
+ Ok(metadata)
345
+ }
346
+ },
347
+ )?;
348
+ Ok(match dentry.inode().and_then(|inode| inode.identity()) {
349
+ Some(identity) => identity.onto(metadata),
350
+ None => metadata,
351
+ })
352
+ }
353
+
354
+ /// A Ruby object was handed `TRUNC` as it opened the file, so an open asking
355
+ /// to truncate has nothing left to ask, as `fuse_do_setattr` finds with
356
+ /// `atomic_o_trunc`.
357
+ fn setattr(&self, dentry: &Dentry, attr: &Iattr) -> Result<(), FilesystemError> {
358
+ let Some(len) = attr.size else {
359
+ return Err(FilesystemError::Unsupported);
360
+ };
361
+ if attr.open {
362
+ return Ok(());
363
+ }
364
+ let file = find_open_file(dentry).ok_or(FilesystemError::InvalidInput)?;
365
+ change(
366
+ file.get(),
367
+ "truncate",
368
+ (len,),
369
+ FilesystemError::ReadOnlyFilesystem,
370
+ )
371
+ }
372
+
373
+ fn open(
374
+ &self,
375
+ dentry: &Dentry,
376
+ how: &OpenHow,
377
+ ) -> Result<Box<dyn FileOperations>, FilesystemError> {
378
+ self.0.open_at(dentry, how, false)
379
+ }
380
+ }
381
+
382
+ /// Its root reports no identity without asking the Ruby object, as FUSE makes its
383
+ /// root from fixed attributes rather than ask the server.
384
+ impl FileSystemType for RubyFilesystem {
385
+ fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
386
+ sb.set_default_d_op(self.clone());
387
+ sb.d_make_root(Inode::new(&Kstat::new(FileType::Dir, 0), self));
388
+ Ok(())
389
+ }
390
+ }
391
+
392
+ /// A directory of a Ruby Filesystem, asked by the path a dentry stands at, the
393
+ /// path a Policy judges.
394
+ impl InodeOperations for RubyFilesystem {
395
+ fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
396
+ self.stat(path_of(dentry))
397
+ }
398
+
399
+ /// A Ruby object matches names exactly, as its contract asks. A lookup
400
+ /// asks its `stat` what stands at the name, as FUSE asks its daemon.
401
+ fn lookup(&self, dentry: &Dentry) -> Result<Lookup, FilesystemError> {
402
+ if dentry.name().len() > NAME_MAX {
403
+ return Err(FilesystemError::InvalidFilename);
404
+ }
405
+ match self.stat(path_of(dentry)) {
406
+ Ok(found) => {
407
+ let ops: Arc<dyn InodeOperations> = match found.file_type {
408
+ FileType::Dir => Arc::new(*self),
409
+ FileType::Symlink => Arc::new(RubyLink(*self)),
410
+ _ => Arc::new(RubyRegular(*self)),
411
+ };
412
+ Ok(Lookup::Found(Inode::new(&found, ops)))
413
+ }
414
+ Err(FilesystemError::NotFound) => Ok(Lookup::Absent),
415
+ Err(err) => Err(err),
416
+ }
417
+ }
418
+
419
+ /// A directory opens without asking the object anything; it is asked
420
+ /// for the names only when the guest lists them.
421
+ fn open(
422
+ &self,
423
+ _dentry: &Dentry,
424
+ _how: &OpenHow,
425
+ ) -> Result<Box<dyn FileOperations>, FilesystemError> {
426
+ Ok(Box::new(RubyDir(*self, Rdir::default())))
427
+ }
428
+
429
+ /// A name still in lookup is asked of the object's `stat` first, and any
430
+ /// name is left to the runtime unless the open may make it and nothing
431
+ /// stands there, as FUSE's `fuse_atomic_open` looks the name up before it
432
+ /// creates.
433
+ fn atomic_open(&self, dentry: &Dentry, how: &OpenHow) -> Result<Finish, FilesystemError> {
434
+ if dentry.in_lookup() {
435
+ let found = self.lookup(dentry)?;
436
+ if !how.create || !matches!(found, Lookup::Absent) {
437
+ return Ok(Finish::Lookup(found));
438
+ }
439
+ }
440
+ if !how.create {
441
+ return Ok(Finish::NoOpen { created: false });
442
+ }
443
+ let file = self.open_at(dentry, how, true)?;
444
+ Ok(Finish::Open {
445
+ file,
446
+ created: true,
447
+ })
448
+ }
449
+
450
+ fn mkdir(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
451
+ change(
452
+ self.object,
453
+ "mkdir",
454
+ (path_of(dentry),),
455
+ FilesystemError::ReadOnlyFilesystem,
456
+ )
457
+ }
458
+
459
+ fn rmdir(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
460
+ change(
461
+ self.object,
462
+ "rmdir",
463
+ (path_of(dentry),),
464
+ FilesystemError::ReadOnlyFilesystem,
465
+ )
466
+ }
467
+
468
+ fn unlink(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
469
+ change(
470
+ self.object,
471
+ "unlink",
472
+ (path_of(dentry),),
473
+ FilesystemError::ReadOnlyFilesystem,
474
+ )
475
+ }
476
+
477
+ /// `rename(from, to)`, as `File.rename` takes it. What stands at `from`
478
+ /// after the runtime checked it is the object's to keep still. Both
479
+ /// directories are of one object, which their shared device names.
480
+ fn rename(
481
+ &self,
482
+ from: &Dentry,
483
+ _to_dir: &dyn InodeOperations,
484
+ to: &Dentry,
485
+ flag: Option<RenameFlag>,
486
+ ) -> Result<(), FilesystemError> {
487
+ if flag.is_some() {
488
+ return Err(FilesystemError::InvalidInput);
489
+ }
490
+ change(
491
+ self.object,
492
+ "rename",
493
+ (path_of(from), path_of(to)),
494
+ FilesystemError::ReadOnlyFilesystem,
495
+ )
496
+ }
497
+ }
498
+
499
+ /// A directory of the Ruby object, open: it asks the object by the path the
500
+ /// dentry it is handed stands at.
501
+ struct RubyDir(RubyFilesystem, Rdir);
502
+
503
+ impl FileOperations for RubyDir {
504
+ fn llseek(
505
+ &mut self,
506
+ dentry: &Dentry,
507
+ pos: u64,
508
+ offset: i64,
509
+ whence: Whence,
510
+ ) -> Result<u64, FilesystemError> {
511
+ generic_file_llseek(dentry, pos, offset, whence)
512
+ }
513
+
514
+ fn read_iter(&mut self, _iocb: &mut Kiocb, _buf: &mut [u8]) -> Result<usize, FilesystemError> {
515
+ generic_read_dir()
516
+ }
517
+
518
+ /// What a listing began with stays on the open directory until it ends, so
519
+ /// a caller reading in small steps asks the object once, as FUSE's readdir
520
+ /// cache spares asking the server each time.
521
+ fn iterate_shared(
522
+ &mut self,
523
+ dentry: &Dentry,
524
+ ctx: &mut DirContext<'_>,
525
+ ) -> Result<(), FilesystemError> {
526
+ let filesystem = self.0;
527
+ self.1
528
+ .iterate(dentry, ctx, || filesystem.entries_at(dentry))
529
+ }
530
+
531
+ /// The object has nothing of a directory's own to flush, as FUSE's
532
+ /// `fuse_dir_fsync` answers for a server without `fsyncdir`.
533
+ fn fsync(&mut self, _datasync: bool) -> Result<(), FilesystemError> {
534
+ Ok(())
535
+ }
536
+ }
537
+
538
+ /// One file the Ruby object opened, which only the guest's descriptor
539
+ /// refers to.
540
+ struct RubyFile {
541
+ file: Arc<PinnedValue>,
542
+ closed: bool,
543
+ }
544
+
545
+ /// The file open on a dentry. A dentry holds at most one, as `revalidate`
546
+ /// has the next walk take a dentry of its own once one is open.
547
+ #[derive(Default)]
548
+ struct OpenRubyFile(Mutex<Weak<PinnedValue>>);
549
+
550
+ /// The file open on `dentry`, while one is.
551
+ fn find_open_file(dentry: &Dentry) -> Option<Arc<PinnedValue>> {
552
+ let inode = dentry.inode()?;
553
+ let open = inode
554
+ .private()
555
+ .get()?
556
+ .clone()
557
+ .downcast::<OpenRubyFile>()
558
+ .ok()?;
559
+ let file = open.0.lock().unwrap_or_else(PoisonError::into_inner);
560
+ file.upgrade()
561
+ }
562
+
563
+ /// The backend changes outside the runtime, so a dentry someone holds is
564
+ /// asked again, as FUSE compares what a fresh lookup finds, and one no longer
565
+ /// naming what it recorded gets a new one. An open file answers for itself,
566
+ /// so a walk finding a dentry one is open on takes one of its own.
567
+ impl DentryOperations for RubyFilesystem {
568
+ fn revalidate(
569
+ &self,
570
+ _dir: &dyn InodeOperations,
571
+ dentry: &Dentry,
572
+ ) -> Result<bool, FilesystemError> {
573
+ let Some(inode) = dentry.inode().filter(|_| find_open_file(dentry).is_none()) else {
574
+ return Ok(false);
575
+ };
576
+ match self.stat(path_of(dentry)) {
577
+ Ok(found) => {
578
+ Ok(Identity::of(&found) == inode.identity() && found.file_type == inode.kind())
579
+ }
580
+ Err(FilesystemError::NotFound | FilesystemError::NotADirectory) => Ok(false),
581
+ Err(err) => Err(err),
582
+ }
583
+ }
584
+
585
+ /// Answering whether a name holds costs a `stat`, as finding it does, so
586
+ /// nothing is gained by keeping it, as FUSE's `entry_timeout=0` keeps none.
587
+ fn delete(&self, _dentry: &Dentry) -> bool {
588
+ true
589
+ }
590
+ }
591
+
592
+ impl RubyFile {
593
+ fn pin(ruby: &Ruby, file: Value) -> Self {
594
+ Self {
595
+ file: Arc::new(PinnedValue::new(ruby, file)),
596
+ closed: false,
597
+ }
598
+ }
599
+
600
+ /// The file's `append` writes all of `buf` at its end in one step and
601
+ /// answers where the file then ends, as `IO#pos` does after a write to
602
+ /// a file opened to append. A file lacking it cannot append, since its
603
+ /// `size` and `pwrite` are two steps another writer can land between.
604
+ fn pwrite(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
605
+ let pos = iocb.pos;
606
+ let written = ask(
607
+ self.file.get(),
608
+ "pwrite",
609
+ FilesystemError::ReadOnlyFilesystem,
610
+ |ruby, file| file.funcall("pwrite", (ruby.str_from_slice(buf), pos)),
611
+ )
612
+ .and_then(|written: usize| match written {
613
+ written if written <= buf.len() => Ok(written),
614
+ written => Err(FilesystemError::Other(format!(
615
+ "pwrite answered {written} bytes written of the {} it was handed",
616
+ buf.len()
617
+ ))),
618
+ })?;
619
+ iocb.pos += written as u64;
620
+ Ok(written)
621
+ }
622
+
623
+ fn append(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
624
+ let end: u64 = ask(
625
+ self.file.get(),
626
+ "append",
627
+ FilesystemError::Unsupported,
628
+ |ruby, file| file.funcall("append", (ruby.str_from_slice(buf),)),
629
+ )?;
630
+ if end < buf.len() as u64 {
631
+ return Err(FilesystemError::Other(format!(
632
+ "append answered the file ending at {end}, before the {} bytes it wrote",
633
+ buf.len()
634
+ )));
635
+ }
636
+ iocb.pos = end;
637
+ Ok(buf.len())
638
+ }
639
+
640
+ /// Ask the file to close, a file lacking `close` having nothing to
641
+ /// release.
642
+ fn close(&mut self) -> Result<(), FilesystemError> {
643
+ self.closed = true;
644
+ change_if_answered(self.file.get(), "close")
645
+ }
646
+ }
647
+
648
+ impl FileOperations for RubyFile {
649
+ fn llseek(
650
+ &mut self,
651
+ dentry: &Dentry,
652
+ pos: u64,
653
+ offset: i64,
654
+ whence: Whence,
655
+ ) -> Result<u64, FilesystemError> {
656
+ generic_file_llseek(dentry, pos, offset, whence)
657
+ }
658
+
659
+ fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
660
+ let pos = iocb.pos;
661
+ let read = ask(
662
+ self.file.get(),
663
+ "pread",
664
+ FilesystemError::Unsupported,
665
+ |ruby, file| {
666
+ // `IO#pread` raises `EOFError` at the end of a file, which reads as nothing.
667
+ let read: Option<RString> = match file.funcall("pread", (buf.len(), pos)) {
668
+ Err(err) if err.is_kind_of(ruby.exception_eof_error()) => None,
669
+ read => read?,
670
+ };
671
+ let Some(read) = read else { return Ok(0) };
672
+ // SAFETY: the bytes are copied out before Ruby runs again.
673
+ let bytes = unsafe { read.as_slice() };
674
+ let len = bytes.len().min(buf.len());
675
+ buf[..len].copy_from_slice(&bytes[..len]);
676
+ Ok(len)
677
+ },
678
+ )?;
679
+ iocb.pos += read as u64;
680
+ Ok(read)
681
+ }
682
+
683
+ fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
684
+ let written = match iocb.append {
685
+ true => self.append(iocb, buf)?,
686
+ false => self.pwrite(iocb, buf)?,
687
+ };
688
+ generic_write_sync(self, iocb, written)
689
+ }
690
+
691
+ /// A file lacking `fsync` has nothing of its own to flush.
692
+ fn fsync(&mut self, _datasync: bool) -> Result<(), FilesystemError> {
693
+ change_if_answered(self.file.get(), "fsync")
694
+ }
695
+
696
+ fn flush(&mut self) -> Result<(), FilesystemError> {
697
+ self.close()
698
+ }
699
+ }
700
+
701
+ /// A file the guest never closed is closed here, its failure discarded.
702
+ impl Drop for RubyFile {
703
+ fn drop(&mut self) {
704
+ if !self.closed {
705
+ let _ = self.close();
706
+ }
707
+ }
708
+ }