kobako 0.28.0 → 0.29.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 (137) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +170 -0
  4. data/Cargo.lock +111 -82
  5. data/README.md +154 -36
  6. data/ROADMAP.md +5 -0
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +86 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +13 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  13. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  14. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  15. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  18. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  19. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  20. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  21. data/crates/kobako-runtime/src/grant.rs +20 -0
  22. data/crates/kobako-runtime/src/lib.rs +11 -1
  23. data/crates/kobako-runtime/src/mount.rs +178 -0
  24. data/crates/kobako-runtime/src/profile.rs +4 -4
  25. data/crates/kobako-runtime/src/runtime.rs +12 -0
  26. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  27. data/crates/kobako-runtime/src/yielder.rs +11 -6
  28. data/crates/kobako-transport/CHANGELOG.md +17 -0
  29. data/crates/kobako-transport/Cargo.toml +1 -1
  30. data/crates/kobako-transport/README.md +1 -1
  31. data/crates/kobako-transport/src/abi.rs +1 -1
  32. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  33. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  34. data/crates/kobako-transport/src/envelope/reply.rs +32 -14
  35. data/crates/kobako-transport/src/envelope/request.rs +18 -8
  36. data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
  37. data/crates/kobako-wasmtime/Cargo.toml +21 -16
  38. data/crates/kobako-wasmtime/README.md +5 -3
  39. data/crates/kobako-wasmtime/src/abi.rs +66 -32
  40. data/crates/kobako-wasmtime/src/ambient.rs +63 -4
  41. data/crates/kobako-wasmtime/src/cache.rs +185 -74
  42. data/crates/kobako-wasmtime/src/config.rs +58 -1
  43. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  44. data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
  45. data/crates/kobako-wasmtime/src/driver.rs +46 -44
  46. data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
  47. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  48. data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
  49. data/crates/kobako-wasmtime/src/invocation.rs +35 -46
  50. data/crates/kobako-wasmtime/src/io.rs +5 -202
  51. data/crates/kobako-wasmtime/src/lib.rs +4 -0
  52. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  53. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  54. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  55. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  56. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  57. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  58. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  59. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  60. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  61. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  62. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  63. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  64. data/crates/kobako-wasmtime/src/trap.rs +39 -19
  65. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  66. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  67. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  68. data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
  69. data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
  70. data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
  71. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  72. data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
  73. data/data/kobako.wasm +0 -0
  74. data/ext/kobako/Cargo.toml +4 -2
  75. data/ext/kobako/src/runtime/bridge.rs +154 -81
  76. data/ext/kobako/src/runtime/escape.rs +53 -0
  77. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  78. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  79. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  80. data/ext/kobako/src/runtime/gvl.rs +7 -1
  81. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  82. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  83. data/ext/kobako/src/runtime.rs +120 -199
  84. data/lib/kobako/catalog/flags.rb +2 -14
  85. data/lib/kobako/catalog/grants.rb +30 -0
  86. data/lib/kobako/catalog/mount.rb +27 -0
  87. data/lib/kobako/catalog/services.rb +11 -0
  88. data/lib/kobako/catalog/snippets.rb +5 -9
  89. data/lib/kobako/catalog.rb +11 -0
  90. data/lib/kobako/codec/decoder.rb +1 -0
  91. data/lib/kobako/codec/ext_types.rb +1 -1
  92. data/lib/kobako/codec/state.rb +19 -3
  93. data/lib/kobako/codec/value_budget.rb +86 -0
  94. data/lib/kobako/codec.rb +7 -0
  95. data/lib/kobako/context.rb +38 -11
  96. data/lib/kobako/errors.rb +15 -14
  97. data/lib/kobako/filesystem.rb +38 -0
  98. data/lib/kobako/pool.rb +2 -2
  99. data/lib/kobako/sandbox.rb +40 -12
  100. data/lib/kobako/sandbox_options.rb +39 -12
  101. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
  102. data/lib/kobako/snippet.rb +1 -1
  103. data/lib/kobako/transport/block_failure.rb +35 -0
  104. data/lib/kobako/transport/dispatcher.rb +30 -26
  105. data/lib/kobako/transport/exposure.rb +5 -7
  106. data/lib/kobako/transport/reflection.rb +79 -23
  107. data/lib/kobako/transport/yielder.rb +8 -30
  108. data/lib/kobako/transport.rb +1 -0
  109. data/lib/kobako/version.rb +1 -1
  110. data/lib/kobako.rb +1 -0
  111. data/sig/kobako/catalog/flags.rbs +0 -4
  112. data/sig/kobako/catalog/grants.rbs +13 -0
  113. data/sig/kobako/catalog/mount.rbs +28 -0
  114. data/sig/kobako/catalog/services.rbs +4 -0
  115. data/sig/kobako/catalog/snippets.rbs +1 -1
  116. data/sig/kobako/catalog.rbs +1 -0
  117. data/sig/kobako/codec/state.rbs +7 -1
  118. data/sig/kobako/codec/value_budget.rbs +23 -0
  119. data/sig/kobako/codec.rbs +2 -0
  120. data/sig/kobako/context.rbs +14 -6
  121. data/sig/kobako/errors.rbs +4 -4
  122. data/sig/kobako/filesystem.rbs +88 -0
  123. data/sig/kobako/guest_yielder.rbs +9 -0
  124. data/sig/kobako/pool.rbs +1 -1
  125. data/sig/kobako/runtime.rbs +21 -49
  126. data/sig/kobako/sandbox.rbs +7 -1
  127. data/sig/kobako/sandbox_options.rbs +24 -2
  128. data/sig/kobako/snapshot.rbs +20 -0
  129. data/sig/kobako/snippet/bytecode.rbs +12 -0
  130. data/sig/kobako/transport/block_failure.rbs +13 -0
  131. data/sig/kobako/transport/dispatcher.rbs +3 -3
  132. data/sig/kobako/transport/exposure.rbs +1 -1
  133. data/sig/kobako/transport/reflection.rbs +14 -1
  134. data/sig/kobako/transport/yielder.rbs +3 -7
  135. data/sig/kobako/transport.rbs +2 -2
  136. metadata +60 -7
  137. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -0,0 +1,91 @@
1
+ //! A Sandbox's Mounts as contract Grants: refused at `mount` time and
2
+ //! built afresh for every invocation by the same rules, each mount's
3
+ //! block standing in for its access.
4
+
5
+ use std::sync::Arc;
6
+
7
+ use magnus::value::{Opaque, ReprValue};
8
+ use magnus::{prelude::*, Error as MagnusError, RArray, RString, Ruby, Value};
9
+
10
+ use kobako_runtime::filesystem::Access;
11
+ use kobako_runtime::grant::Grant;
12
+ use kobako_runtime::mount::{Mount, Policy};
13
+
14
+ use super::{to_access, to_filesystem};
15
+ use crate::runtime::pinned_value::PinnedValue;
16
+ use crate::runtime::{escape, gvl};
17
+
18
+ /// Refuse a Mount at `mount` time by the same rules an invocation builds
19
+ /// it with, so a mistake raises where it was made.
20
+ pub(in crate::runtime) fn validate_mount(
21
+ at: Value,
22
+ access: Value,
23
+ filesystem: Value,
24
+ ) -> Result<(), MagnusError> {
25
+ to_mount(at, access, filesystem).map(drop)
26
+ }
27
+
28
+ /// The Mount a Sandbox's `[at, access, filesystem]` stands for: one owner
29
+ /// of the path, access, and Filesystem rules.
30
+ fn to_mount(at: Value, access: Value, filesystem: Value) -> Result<Mount, MagnusError> {
31
+ let ruby = Ruby::get().expect("Ruby thread");
32
+ let at = RString::try_convert(at)?.to_string()?;
33
+ let access = to_access(access)?;
34
+ Mount::new(at, to_filesystem(filesystem)?, access)
35
+ .map_err(|err| MagnusError::new(ruby.exception_arg_error(), err.to_string()))
36
+ }
37
+
38
+ /// A mount's block, asked in its access's place. A block raising a
39
+ /// StandardError refuses, so the guest's open fails and its script goes
40
+ /// on; anything else it raises is held for the host.
41
+ struct RubyPolicy {
42
+ block: Opaque<Value>,
43
+ }
44
+
45
+ impl Policy for RubyPolicy {
46
+ fn allows(&self, path: &str, access: Access) -> bool {
47
+ gvl::reenter(|| {
48
+ let ruby = Ruby::get().expect("Ruby handle unavailable in a mount's block");
49
+ let block: Value = ruby.get_inner(self.block);
50
+ let access = ruby.to_symbol(match access {
51
+ Access::Read => "read",
52
+ Access::ReadWrite => "read_write",
53
+ });
54
+ block
55
+ .funcall::<_, _, Value>("call", (ruby.str_new(path), access))
56
+ .map(|answer| answer.to_bool())
57
+ .unwrap_or_else(|error| {
58
+ let _ = escape::hold(&ruby, error);
59
+ false
60
+ })
61
+ })
62
+ }
63
+ }
64
+
65
+ /// The Grants one invocation carries, from the Sandbox's Mounts as
66
+ /// `[at, access, filesystem, block]` entries, beside the pins keeping
67
+ /// each Mount's filesystem and block in place while the guest may reach
68
+ /// them; the caller holds both until the invocation returns.
69
+ pub(in crate::runtime) fn to_grants(
70
+ ruby: &Ruby,
71
+ mounts: RArray,
72
+ ) -> Result<(Vec<Grant>, Vec<PinnedValue>), MagnusError> {
73
+ let mut grants = Vec::with_capacity(mounts.len());
74
+ let mut pins = Vec::with_capacity(mounts.len() * 2);
75
+ for index in 0..mounts.len() as isize {
76
+ let entry: RArray = mounts.entry(index)?;
77
+ let filesystem: Value = entry.entry(2)?;
78
+ pins.push(PinnedValue::new(ruby, filesystem));
79
+ let mount = to_mount(entry.entry(0)?, entry.entry(1)?, filesystem)?;
80
+ let block: Value = entry.entry(3)?;
81
+ grants.push(Grant::Mount(if block.is_nil() {
82
+ mount
83
+ } else {
84
+ pins.push(PinnedValue::new(ruby, block));
85
+ mount.with_policy(Arc::new(RubyPolicy {
86
+ block: Opaque::from(block),
87
+ }))
88
+ }));
89
+ }
90
+ Ok((grants, pins))
91
+ }
@@ -0,0 +1,400 @@
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::{kwargs, prelude::*, Error as MagnusError, RClass, RModule, RString, Ruby, Value};
13
+
14
+ use std::sync::Arc;
15
+
16
+ use kobako_runtime::filesystem::{
17
+ DirEntry, File, FileType, Filesystem, FilesystemError, Hold, Metadata, OpenOptions, Rename,
18
+ };
19
+
20
+ use crate::runtime::pinned_value::PinnedValue;
21
+ use crate::runtime::{escape, gvl};
22
+
23
+ /// The Ruby object behind a Mount or a Layer. Whatever holds it on the
24
+ /// Ruby side keeps it alive and in place: the invocation pinning its
25
+ /// Mounts, or the Overlay marking its Layers.
26
+ pub(crate) struct RubyFilesystem {
27
+ object: Opaque<Value>,
28
+ /// The device a stat without a `dev` names, the object's own.
29
+ dev: u64,
30
+ }
31
+
32
+ impl RubyFilesystem {
33
+ /// The object as a Filesystem, when it answers `open` and `stat`.
34
+ pub(crate) fn from_value(value: Value) -> Option<Self> {
35
+ let answers = |name| value.respond_to(name, false).unwrap_or(false);
36
+ (answers("open") && answers("stat")).then(|| Self {
37
+ object: Opaque::from(value),
38
+ dev: Metadata::unique_device(),
39
+ })
40
+ }
41
+ }
42
+
43
+ /// Ask `object` its `name` method with the GVL held, `answer` making the
44
+ /// call and reading what it returns before Ruby runs anything else. An
45
+ /// object lacking the method fails as `missing`.
46
+ fn ask<T>(
47
+ object: Opaque<Value>,
48
+ name: &str,
49
+ missing: FilesystemError,
50
+ answer: impl FnOnce(&Ruby, Value) -> Result<T, MagnusError>,
51
+ ) -> Result<T, FilesystemError> {
52
+ ask_or(object, name, Err(missing), answer)
53
+ }
54
+
55
+ /// As `ask`, an object lacking the method answering `missing`.
56
+ fn ask_or<T>(
57
+ object: Opaque<Value>,
58
+ name: &str,
59
+ missing: Result<T, FilesystemError>,
60
+ answer: impl FnOnce(&Ruby, Value) -> Result<T, MagnusError>,
61
+ ) -> Result<T, FilesystemError> {
62
+ gvl::reenter(|| {
63
+ let ruby = Ruby::get().expect("Ruby handle unavailable in a Filesystem call");
64
+ let object: Value = ruby.get_inner(object);
65
+ if !object.respond_to(name, false).unwrap_or(false) {
66
+ return missing;
67
+ }
68
+ answer(&ruby, object).map_err(|err| match escape::hold(&ruby, err) {
69
+ Err(err) => to_error(err),
70
+ // The invocation ends before the guest reads this.
71
+ Ok(()) => FilesystemError::Other(String::new()),
72
+ })
73
+ })
74
+ }
75
+
76
+ /// Ask `object` a change, answering nothing more than whether it was made.
77
+ fn change(
78
+ object: Opaque<Value>,
79
+ name: &str,
80
+ args: impl magnus::ArgList,
81
+ missing: FilesystemError,
82
+ ) -> Result<(), FilesystemError> {
83
+ ask(object, name, missing, |_, object| {
84
+ object.funcall::<_, _, Value>(name, args).map(drop)
85
+ })
86
+ }
87
+
88
+ /// Ask `object` a change it may lack, an object lacking `name` having
89
+ /// nothing of its own to change, while one that answers and refuses still
90
+ /// fails, whatever `Errno` it raises.
91
+ fn change_if_answered(object: Opaque<Value>, name: &str) -> Result<(), FilesystemError> {
92
+ ask_or(object, name, Ok(()), |_, object| {
93
+ object.funcall::<_, _, Value>(name, ()).map(drop)
94
+ })
95
+ }
96
+
97
+ /// The FilesystemError the nearest `Errno` class of `err` stands for, by
98
+ /// its POSIX name; anything else is `Other`, which reaches the guest as
99
+ /// `EIO`.
100
+ fn to_error(err: MagnusError) -> FilesystemError {
101
+ let named = err.value().and_then(|exception| {
102
+ exception
103
+ .class()
104
+ .ancestors()
105
+ .into_iter()
106
+ .find_map(|module| {
107
+ let name: Option<String> = module.funcall("name", ()).ok()?;
108
+ FilesystemError::from_posix_name(name?.strip_prefix("Errno::")?)
109
+ })
110
+ });
111
+ named.unwrap_or_else(|| FilesystemError::Other(err.to_string()))
112
+ }
113
+
114
+ /// `File::Constants` flags for `options`, read from Ruby since their
115
+ /// values differ by platform.
116
+ fn to_flags(ruby: &Ruby, options: &OpenOptions) -> Result<i64, MagnusError> {
117
+ let constants: RModule = ruby
118
+ .class_object()
119
+ .const_get::<_, RClass>("File")?
120
+ .const_get("Constants")?;
121
+ let flag = |name| constants.const_get::<_, i64>(name);
122
+ let mut flags = match (options.read, options.write || options.truncate) {
123
+ (_, false) => flag("RDONLY")?,
124
+ (false, true) => flag("WRONLY")?,
125
+ (true, true) => flag("RDWR")?,
126
+ };
127
+ if options.create || options.create_new {
128
+ flags |= flag("CREAT")?;
129
+ }
130
+ if options.create_new {
131
+ flags |= flag("EXCL")?;
132
+ }
133
+ if options.truncate {
134
+ flags |= flag("TRUNC")?;
135
+ }
136
+ if options.nofollow {
137
+ flags |= flag("NOFOLLOW")?;
138
+ }
139
+ Ok(flags)
140
+ }
141
+
142
+ fn join(dir: &str, name: &str) -> String {
143
+ if dir.is_empty() {
144
+ name.to_string()
145
+ } else {
146
+ format!("{dir}/{name}")
147
+ }
148
+ }
149
+
150
+ impl Filesystem for RubyFilesystem {
151
+ fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
152
+ ask(
153
+ self.object,
154
+ "open",
155
+ FilesystemError::Unsupported,
156
+ |ruby, fs| {
157
+ let file: Value = fs.funcall("open", (path, to_flags(ruby, options)?))?;
158
+ Ok(Box::new(RubyFile::pin(ruby, file)) as Box<dyn File>)
159
+ },
160
+ )
161
+ }
162
+
163
+ fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
164
+ let own_dev = self.dev;
165
+ ask(
166
+ self.object,
167
+ "stat",
168
+ FilesystemError::Unsupported,
169
+ |_, fs| {
170
+ let stat: Value = fs.funcall("stat", (path,))?;
171
+ let metadata = if stat.funcall::<_, _, bool>("directory?", ())? {
172
+ Metadata::new(FileType::Directory, 0)
173
+ } else {
174
+ Metadata::new(FileType::File, stat.funcall("size", ())?)
175
+ };
176
+ // A stat names its identity as `File::Stat` does; without a
177
+ // `dev` the Filesystem stands for a device of its own, so it
178
+ // shares an identity with no other Filesystem.
179
+ let dev = if stat.respond_to("dev", false)? {
180
+ stat.funcall("dev", ())?
181
+ } else {
182
+ own_dev
183
+ };
184
+ Ok(metadata.with_identity(dev, stat.funcall("ino", ())?))
185
+ },
186
+ )
187
+ }
188
+
189
+ /// A Ruby object answers `stat` alone, so it holds no links.
190
+ fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
191
+ self.metadata(path)
192
+ }
193
+
194
+ /// A Ruby object matches names exactly, as its contract asks.
195
+ fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
196
+ Ok(path.to_string())
197
+ }
198
+
199
+ /// A Ruby object holds no links to follow.
200
+ fn canonicalize_beneath(&self, _base: &str, path: &str) -> Result<String, FilesystemError> {
201
+ Ok(path.to_string())
202
+ }
203
+
204
+ /// A Ruby object holds no links a Policy needs kept from being followed.
205
+ fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
206
+ None
207
+ }
208
+
209
+ /// A Ruby object keeps an `ino` its own while open, as its contract
210
+ /// asks, so nothing is held.
211
+ fn hold_directory(&self, _path: &str) -> Result<Option<Hold>, FilesystemError> {
212
+ Ok(None)
213
+ }
214
+
215
+ fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
216
+ let names: Vec<String> = ask(
217
+ self.object,
218
+ "children",
219
+ FilesystemError::Unsupported,
220
+ |_, fs| fs.funcall("children", (path,)),
221
+ )?;
222
+ names
223
+ .into_iter()
224
+ .map(|name| {
225
+ let file_type = self.metadata(&join(path, &name))?.file_type;
226
+ Ok(DirEntry::new(name, file_type))
227
+ })
228
+ .collect()
229
+ }
230
+
231
+ fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
232
+ change(
233
+ self.object,
234
+ "mkdir",
235
+ (path,),
236
+ FilesystemError::ReadOnlyFilesystem,
237
+ )
238
+ }
239
+
240
+ fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
241
+ change(
242
+ self.object,
243
+ "rmdir",
244
+ (path,),
245
+ FilesystemError::ReadOnlyFilesystem,
246
+ )
247
+ }
248
+
249
+ fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
250
+ change(
251
+ self.object,
252
+ "unlink",
253
+ (path,),
254
+ FilesystemError::ReadOnlyFilesystem,
255
+ )
256
+ }
257
+
258
+ /// `rename(from, to, directory:)`: `true` moves only a directory,
259
+ /// `false` only a file, `nil` either. What stands at `from` meanwhile
260
+ /// is the object's to keep still.
261
+ fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
262
+ // A move the keyword cannot tell the object is refused rather than
263
+ // widened into one of either kind.
264
+ let directory = match rename {
265
+ Rename::Any => None,
266
+ Rename::File => Some(false),
267
+ Rename::Directory => Some(true),
268
+ _ => return Err(FilesystemError::Unsupported),
269
+ };
270
+ change(
271
+ self.object,
272
+ "rename",
273
+ (from, to, kwargs!("directory" => directory)),
274
+ FilesystemError::ReadOnlyFilesystem,
275
+ )
276
+ }
277
+
278
+ /// Every Invocation shares the Ruby object.
279
+ fn for_invocation(&self) -> Option<Arc<dyn Filesystem>> {
280
+ None
281
+ }
282
+ }
283
+
284
+ /// One file the Ruby object opened, which only the guest's descriptor
285
+ /// refers to.
286
+ struct RubyFile {
287
+ file: PinnedValue,
288
+ closed: bool,
289
+ }
290
+
291
+ impl RubyFile {
292
+ fn pin(ruby: &Ruby, file: Value) -> Self {
293
+ Self {
294
+ file: PinnedValue::new(ruby, file),
295
+ closed: false,
296
+ }
297
+ }
298
+
299
+ /// Ask the file to close, a file lacking `close` having nothing to
300
+ /// release.
301
+ fn release(&mut self) -> Result<(), FilesystemError> {
302
+ self.closed = true;
303
+ change_if_answered(self.file.get(), "close")
304
+ }
305
+ }
306
+
307
+ impl File for RubyFile {
308
+ fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
309
+ ask(
310
+ self.file.get(),
311
+ "pread",
312
+ FilesystemError::Unsupported,
313
+ |ruby, file| {
314
+ // `IO#pread` raises `EOFError` at the end of a file, which reads as nothing.
315
+ let read: Option<RString> = match file.funcall("pread", (buf.len(), offset)) {
316
+ Err(err) if err.is_kind_of(ruby.exception_eof_error()) => None,
317
+ read => read?,
318
+ };
319
+ let Some(read) = read else { return Ok(0) };
320
+ // SAFETY: the bytes are copied out before Ruby runs again.
321
+ let bytes = unsafe { read.as_slice() };
322
+ let len = bytes.len().min(buf.len());
323
+ buf[..len].copy_from_slice(&bytes[..len]);
324
+ Ok(len)
325
+ },
326
+ )
327
+ }
328
+
329
+ fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
330
+ ask(
331
+ self.file.get(),
332
+ "pwrite",
333
+ FilesystemError::ReadOnlyFilesystem,
334
+ |ruby, file| file.funcall("pwrite", (ruby.str_from_slice(buf), offset)),
335
+ )
336
+ .and_then(|written: usize| match written {
337
+ written if written <= buf.len() => Ok(written),
338
+ written => Err(FilesystemError::Other(format!(
339
+ "pwrite answered {written} bytes written of the {} it was handed",
340
+ buf.len()
341
+ ))),
342
+ })
343
+ }
344
+
345
+ /// The file's `append` writes all of `buf` at its end in one step and
346
+ /// answers where the file then ends, as `IO#pos` does after a write to
347
+ /// a file opened to append. A file lacking it cannot append, since its
348
+ /// `size` and `pwrite` are two steps another writer can land between.
349
+ fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
350
+ ask(
351
+ self.file.get(),
352
+ "append",
353
+ FilesystemError::Unsupported,
354
+ |ruby, file| file.funcall("append", (ruby.str_from_slice(buf),)),
355
+ )
356
+ .and_then(|end: u64| match end.checked_sub(buf.len() as u64) {
357
+ Some(at) => Ok((at, buf.len())),
358
+ None => Err(FilesystemError::Other(format!(
359
+ "append answered the file ending at {end}, before the {} bytes it wrote",
360
+ buf.len()
361
+ ))),
362
+ })
363
+ }
364
+
365
+ fn metadata(&self) -> Result<Metadata, FilesystemError> {
366
+ ask(
367
+ self.file.get(),
368
+ "size",
369
+ FilesystemError::Unsupported,
370
+ |_, file| Ok(Metadata::new(FileType::File, file.funcall("size", ())?)),
371
+ )
372
+ }
373
+
374
+ fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
375
+ change(
376
+ self.file.get(),
377
+ "truncate",
378
+ (len,),
379
+ FilesystemError::ReadOnlyFilesystem,
380
+ )
381
+ }
382
+
383
+ /// A file lacking `fsync` has nothing of its own to flush.
384
+ fn sync(&mut self) -> Result<(), FilesystemError> {
385
+ change_if_answered(self.file.get(), "fsync")
386
+ }
387
+
388
+ fn close(mut self: Box<Self>) -> Result<(), FilesystemError> {
389
+ self.release()
390
+ }
391
+ }
392
+
393
+ /// A file the guest never closed is closed here, its failure discarded.
394
+ impl Drop for RubyFile {
395
+ fn drop(&mut self) {
396
+ if !self.closed {
397
+ let _ = self.release();
398
+ }
399
+ }
400
+ }