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.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +170 -0
- data/Cargo.lock +111 -82
- data/README.md +154 -36
- data/ROADMAP.md +5 -0
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +86 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +13 -8
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
- data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
- data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
- data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
- data/crates/kobako-runtime/src/filesystem.rs +392 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/lib.rs +11 -1
- data/crates/kobako-runtime/src/mount.rs +178 -0
- data/crates/kobako-runtime/src/profile.rs +4 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-transport/CHANGELOG.md +17 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +1 -1
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/fault.rs +2 -1
- data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
- data/crates/kobako-transport/src/envelope/reply.rs +32 -14
- data/crates/kobako-transport/src/envelope/request.rs +18 -8
- data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
- data/crates/kobako-wasmtime/Cargo.toml +21 -16
- data/crates/kobako-wasmtime/README.md +5 -3
- data/crates/kobako-wasmtime/src/abi.rs +66 -32
- data/crates/kobako-wasmtime/src/ambient.rs +63 -4
- data/crates/kobako-wasmtime/src/cache.rs +185 -74
- data/crates/kobako-wasmtime/src/config.rs +58 -1
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
- data/crates/kobako-wasmtime/src/driver.rs +46 -44
- data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
- data/crates/kobako-wasmtime/src/invocation.rs +35 -46
- data/crates/kobako-wasmtime/src/io.rs +5 -202
- data/crates/kobako-wasmtime/src/lib.rs +4 -0
- data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
- data/crates/kobako-wasmtime/src/preview1.rs +461 -0
- data/crates/kobako-wasmtime/src/trap.rs +39 -19
- data/crates/kobako-wasmtime/src/wasi.rs +317 -0
- data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
- data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +154 -81
- data/ext/kobako/src/runtime/escape.rs +53 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
- data/ext/kobako/src/runtime/filesystem.rs +236 -0
- data/ext/kobako/src/runtime/gvl.rs +7 -1
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +120 -199
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +11 -0
- data/lib/kobako/catalog/snippets.rb +5 -9
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +1 -0
- data/lib/kobako/codec/ext_types.rb +1 -1
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +38 -11
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +38 -0
- data/lib/kobako/pool.rb +2 -2
- data/lib/kobako/sandbox.rb +40 -12
- data/lib/kobako/sandbox_options.rb +39 -12
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +30 -26
- data/lib/kobako/transport/exposure.rb +5 -7
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/yielder.rb +8 -30
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +4 -0
- data/sig/kobako/catalog/snippets.rbs +1 -1
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +2 -0
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +7 -1
- data/sig/kobako/sandbox_options.rbs +24 -2
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +3 -3
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +2 -2
- metadata +60 -7
- 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
|
+
}
|