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,60 @@
|
|
|
1
|
+
//! Paths as a Filesystem receives them: relative to its root, components
|
|
2
|
+
//! joined by `/`, the empty path being the root.
|
|
3
|
+
|
|
4
|
+
use super::{FileType, FilesystemError};
|
|
5
|
+
|
|
6
|
+
/// The directory holding `path`; the root holds every top-level entry.
|
|
7
|
+
pub(crate) fn parent(path: &str) -> &str {
|
|
8
|
+
path.rsplit_once('/').map_or("", |(parent, _)| parent)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// The last name of `path`, within its parent.
|
|
12
|
+
pub(crate) fn name(path: &str) -> &str {
|
|
13
|
+
path.rsplit_once('/').map_or(path, |(_, name)| name)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// The path of `name` within the directory `dir`.
|
|
17
|
+
pub(crate) fn join(dir: &str, name: &str) -> String {
|
|
18
|
+
if dir.is_empty() {
|
|
19
|
+
name.to_string()
|
|
20
|
+
} else {
|
|
21
|
+
format!("{dir}/{name}")
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// The names of `path`, outermost first; the root has none.
|
|
26
|
+
pub(crate) fn components(path: &str) -> impl Iterator<Item = &str> {
|
|
27
|
+
path.split('/').filter(|name| !name.is_empty())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Every directory above `path`, outermost first, the root left out.
|
|
31
|
+
pub(crate) fn ancestors(path: &str) -> impl Iterator<Item = &str> {
|
|
32
|
+
path.match_indices('/').map(move |(end, _)| &path[..end])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Whether `path` lies beneath `dir`, `dir` itself left out.
|
|
36
|
+
pub(crate) fn is_beneath(path: &str, dir: &str) -> bool {
|
|
37
|
+
dir.is_empty() && !path.is_empty()
|
|
38
|
+
|| path
|
|
39
|
+
.strip_prefix(dir)
|
|
40
|
+
.is_some_and(|rest| rest.starts_with('/'))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Why an entry of `file_type` cannot move beneath itself: a directory
|
|
44
|
+
/// cannot hold itself, and a file stands where the new path needs a
|
|
45
|
+
/// directory, as `rename(2)` answers each.
|
|
46
|
+
pub(crate) fn beneath_itself(file_type: FileType) -> FilesystemError {
|
|
47
|
+
match file_type {
|
|
48
|
+
FileType::Directory => FilesystemError::InvalidInput,
|
|
49
|
+
_ => FilesystemError::NotADirectory,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// A path a Host App seeds a Filesystem with, in the form a runtime hands
|
|
54
|
+
/// over: no leading `/`, and no empty or `.` component.
|
|
55
|
+
pub(crate) fn normalize_seed(path: &str) -> String {
|
|
56
|
+
path.split('/')
|
|
57
|
+
.filter(|component| !component.is_empty() && *component != ".")
|
|
58
|
+
.collect::<Vec<_>>()
|
|
59
|
+
.join("/")
|
|
60
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//! `Temporary`: a Filesystem held in memory that every Invocation starts
|
|
2
|
+
//! empty.
|
|
3
|
+
|
|
4
|
+
use std::sync::Arc;
|
|
5
|
+
|
|
6
|
+
use super::memory::Memory;
|
|
7
|
+
use super::{DirEntry, File, Filesystem, FilesystemError, Hold, Metadata, OpenOptions, Rename};
|
|
8
|
+
|
|
9
|
+
/// Files and directories held in memory for one Invocation. The next one
|
|
10
|
+
/// is handed a new, empty Temporary in its place, so nothing one writes
|
|
11
|
+
/// reaches another. Each one holds as much as a Memory does.
|
|
12
|
+
#[derive(Default)]
|
|
13
|
+
pub struct Temporary {
|
|
14
|
+
memory: Memory,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl Temporary {
|
|
18
|
+
pub fn new() -> Self {
|
|
19
|
+
Self::default()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// This Temporary, and every one handed out in its place, with its
|
|
23
|
+
/// files holding at most `limit` bytes together, or without bound for
|
|
24
|
+
/// `None`, as `Memory::with_limit` bounds a Memory.
|
|
25
|
+
pub fn with_limit(self, limit: Option<usize>) -> Self {
|
|
26
|
+
Self {
|
|
27
|
+
memory: self.memory.with_limit(limit),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// This Temporary, and every one handed out in its place, holding at
|
|
32
|
+
/// most `inodes` entries, as `Memory::with_inodes` bounds a Memory.
|
|
33
|
+
pub fn with_inodes(self, inodes: Option<usize>) -> Self {
|
|
34
|
+
Self {
|
|
35
|
+
memory: self.memory.with_inodes(inodes),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl Filesystem for Temporary {
|
|
41
|
+
fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
|
|
42
|
+
self.memory.open(path, options)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
46
|
+
self.memory.metadata(path)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
50
|
+
self.memory.symlink_metadata(path)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
|
|
54
|
+
self.memory.stored_path(path)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn canonicalize_beneath(&self, base: &str, path: &str) -> Result<String, FilesystemError> {
|
|
58
|
+
self.memory.canonicalize_beneath(base, path)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
|
|
62
|
+
self.memory.for_policy()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn hold_directory(&self, path: &str) -> Result<Option<Hold>, FilesystemError> {
|
|
66
|
+
self.memory.hold_directory(path)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
70
|
+
self.memory.read_dir(path)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
74
|
+
self.memory.create_dir(path)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
78
|
+
self.memory.remove_dir(path)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
82
|
+
self.memory.remove_file(path)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
86
|
+
self.memory.rename(from, to, rename)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn for_invocation(&self) -> Option<Arc<dyn Filesystem>> {
|
|
90
|
+
Some(Arc::new(Self {
|
|
91
|
+
memory: self.memory.to_empty(),
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[cfg(test)]
|
|
97
|
+
mod tests {
|
|
98
|
+
use super::*;
|
|
99
|
+
|
|
100
|
+
// @behavior FS-004
|
|
101
|
+
#[test]
|
|
102
|
+
fn a_temporary_starts_every_invocation_empty() {
|
|
103
|
+
let temporary = Temporary::new();
|
|
104
|
+
temporary
|
|
105
|
+
.open(
|
|
106
|
+
"scratch",
|
|
107
|
+
&OpenOptions {
|
|
108
|
+
write: true,
|
|
109
|
+
create: true,
|
|
110
|
+
..OpenOptions::default()
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
.expect("a new file must be created")
|
|
114
|
+
.write_at(b"gone", 0)
|
|
115
|
+
.expect("the write must land");
|
|
116
|
+
|
|
117
|
+
let next = temporary
|
|
118
|
+
.for_invocation()
|
|
119
|
+
.expect("a Temporary must hand the next Invocation a Filesystem of its own");
|
|
120
|
+
assert_eq!(
|
|
121
|
+
next.read_dir("").expect("the root must list"),
|
|
122
|
+
Vec::new(),
|
|
123
|
+
"Temporary::for_invocation must hand the next Invocation a Filesystem holding no file"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// @behavior FS-033
|
|
128
|
+
#[test]
|
|
129
|
+
fn a_temporary_keeps_its_inodes_in_every_invocation() {
|
|
130
|
+
let next = Temporary::new()
|
|
131
|
+
.with_inodes(Some(1))
|
|
132
|
+
.for_invocation()
|
|
133
|
+
.expect("a Temporary must hand the next Invocation a Filesystem of its own");
|
|
134
|
+
assert_eq!(
|
|
135
|
+
next.create_dir("dir"),
|
|
136
|
+
Err(FilesystemError::StorageFull),
|
|
137
|
+
"a directory past a Temporary's inodes in the Filesystem Temporary::for_invocation \
|
|
138
|
+
hands the next Invocation must answer StorageFull"
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// @behavior FS-027
|
|
143
|
+
#[test]
|
|
144
|
+
fn a_temporary_keeps_its_limit_in_every_invocation() {
|
|
145
|
+
let next = Temporary::new()
|
|
146
|
+
.with_limit(Some(4))
|
|
147
|
+
.for_invocation()
|
|
148
|
+
.expect("a Temporary must hand the next Invocation a Filesystem of its own");
|
|
149
|
+
let written = next
|
|
150
|
+
.open(
|
|
151
|
+
"scratch",
|
|
152
|
+
&OpenOptions {
|
|
153
|
+
write: true,
|
|
154
|
+
create: true,
|
|
155
|
+
..OpenOptions::default()
|
|
156
|
+
},
|
|
157
|
+
)
|
|
158
|
+
.expect("a new file must be created")
|
|
159
|
+
.write_at(b"five!", 0);
|
|
160
|
+
assert_eq!(
|
|
161
|
+
written,
|
|
162
|
+
Err(FilesystemError::StorageFull),
|
|
163
|
+
"a write past a Temporary's limit in the Filesystem Temporary::for_invocation hands \
|
|
164
|
+
the next Invocation must answer StorageFull"
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
//! The Filesystem behind a Mount: what answers every call a
|
|
2
|
+
//! guest makes beneath that Mount's path.
|
|
3
|
+
//!
|
|
4
|
+
//! A runtime owns the descriptors and path resolution, and the Mount
|
|
5
|
+
//! decides access; a `Filesystem` only answers for the files and directories
|
|
6
|
+
//! themselves, so a third party brings one without implementing any of
|
|
7
|
+
//! that. Every path a runtime hands over is relative to the Filesystem's
|
|
8
|
+
//! own root, its components joined by `/`, with no `.` or `..` left and
|
|
9
|
+
//! no leading `/`; the empty path is the root, which a runtime never asks
|
|
10
|
+
//! to remove or move.
|
|
11
|
+
|
|
12
|
+
mod directory;
|
|
13
|
+
mod memory;
|
|
14
|
+
mod overlay;
|
|
15
|
+
mod path;
|
|
16
|
+
mod temporary;
|
|
17
|
+
|
|
18
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
19
|
+
use std::sync::Arc;
|
|
20
|
+
|
|
21
|
+
pub use directory::Directory;
|
|
22
|
+
pub use memory::{Memory, SeedError};
|
|
23
|
+
pub use overlay::{Blocking, Overlay};
|
|
24
|
+
pub use temporary::Temporary;
|
|
25
|
+
|
|
26
|
+
/// What the guest may do beneath a Mount, or an Overlay in one of its
|
|
27
|
+
/// Layers.
|
|
28
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
29
|
+
pub enum Access {
|
|
30
|
+
Read,
|
|
31
|
+
ReadWrite,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// The files and directories behind a Mount. One may wrap
|
|
35
|
+
/// another and answer by delegating, so it sees every call on the way
|
|
36
|
+
/// through.
|
|
37
|
+
///
|
|
38
|
+
/// Six methods keep a guarantee the runtime gives the guest, so each
|
|
39
|
+
/// Filesystem answers them itself and a wrapper forwards them:
|
|
40
|
+
/// `symlink_metadata` keeps a link from being followed where a Mount
|
|
41
|
+
/// refuses links, `stored_path` keeps a Policy judging the spelling the
|
|
42
|
+
/// host stores, `canonicalize_beneath` keeps a link within the directory
|
|
43
|
+
/// a descriptor opened, `hold_directory` keeps an open directory's
|
|
44
|
+
/// identity its own, `for_invocation` keeps one Invocation's changes
|
|
45
|
+
/// from the next, and `for_policy` keeps a call to the path a Policy
|
|
46
|
+
/// judged.
|
|
47
|
+
pub trait Filesystem: Send + Sync {
|
|
48
|
+
/// Open the file at `path` as `options` asks, creating or truncating it
|
|
49
|
+
/// where they say to.
|
|
50
|
+
fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError>;
|
|
51
|
+
|
|
52
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError>;
|
|
53
|
+
|
|
54
|
+
/// What `path` names without following a link standing there, as
|
|
55
|
+
/// `lstat` answers. A link is reported as one and nothing of where it
|
|
56
|
+
/// points: no size, and no way to read it. A Filesystem holding no
|
|
57
|
+
/// links answers `metadata`.
|
|
58
|
+
fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError>;
|
|
59
|
+
|
|
60
|
+
/// `path` with each name this Filesystem holds spelled as it stores
|
|
61
|
+
/// it, so a name a host reaches by more than one spelling is judged by
|
|
62
|
+
/// one; names past the last it holds stay as given. A Filesystem
|
|
63
|
+
/// matching names exactly answers `path`.
|
|
64
|
+
fn stored_path(&self, path: &str) -> Result<String, FilesystemError>;
|
|
65
|
+
|
|
66
|
+
/// `path`, relative to the directory at `base`, with every link on it
|
|
67
|
+
/// followed, as cap-std's `Dir::canonicalize` answers: still relative
|
|
68
|
+
/// to `base`, `NotPermitted` where a link leads out of `base`, and
|
|
69
|
+
/// `NotFound` where nothing stands. A descriptor opened on `base`
|
|
70
|
+
/// reaches nothing above it, as a capability never widens. A
|
|
71
|
+
/// Filesystem holding no links answers `path`.
|
|
72
|
+
fn canonicalize_beneath(&self, base: &str, path: &str) -> Result<String, FilesystemError>;
|
|
73
|
+
|
|
74
|
+
/// Keep the directory at `path` from handing its identity to another
|
|
75
|
+
/// while the Hold lives, as an open descriptor keeps a host's inode
|
|
76
|
+
/// from being reused. A Filesystem whose identities never repeat
|
|
77
|
+
/// holds nothing.
|
|
78
|
+
fn hold_directory(&self, path: &str) -> Result<Option<Hold>, FilesystemError>;
|
|
79
|
+
|
|
80
|
+
/// The entries of the directory at `path`, without `.` or `..`.
|
|
81
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError>;
|
|
82
|
+
|
|
83
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError>;
|
|
84
|
+
|
|
85
|
+
/// Remove the empty directory at `path`.
|
|
86
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError>;
|
|
87
|
+
|
|
88
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError>;
|
|
89
|
+
|
|
90
|
+
/// Move the entry at `from` to `to`, refusing one `rename` does not
|
|
91
|
+
/// allow; a move onto its own path changes nothing and is allowed.
|
|
92
|
+
/// The Filesystem checks what stands at `from` as it moves it, under
|
|
93
|
+
/// the same lock, so nothing replaces it in between.
|
|
94
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError>;
|
|
95
|
+
|
|
96
|
+
/// The Filesystem a new Invocation starts from in this one's place, or
|
|
97
|
+
/// `None` when every Invocation shares this one.
|
|
98
|
+
fn for_invocation(&self) -> Option<Arc<dyn Filesystem>>;
|
|
99
|
+
|
|
100
|
+
/// The Filesystem a Mount with a Policy hands its calls in this one's
|
|
101
|
+
/// place, following no link anywhere on a path, each name reached
|
|
102
|
+
/// from the directory already held, so a link another writer swaps in
|
|
103
|
+
/// after the Policy judged the path is a link loop rather than
|
|
104
|
+
/// followed. A Filesystem holding no links answers `None`.
|
|
105
|
+
fn for_policy(&self) -> Option<Arc<dyn Filesystem>>;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// One open file. The runtime keeps its position, so every read and write
|
|
109
|
+
/// names the offset it starts at.
|
|
110
|
+
pub trait File: Send {
|
|
111
|
+
fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError>;
|
|
112
|
+
|
|
113
|
+
fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError>;
|
|
114
|
+
|
|
115
|
+
/// Write `buf` at the file's end as it stands when the write lands,
|
|
116
|
+
/// answering the offset it landed at and how much was written. It
|
|
117
|
+
/// keeps another writer's append from landing over this one, as Linux
|
|
118
|
+
/// takes an append under the file's lock, so a file that cannot read
|
|
119
|
+
/// its end and write there in one step answers `Unsupported`.
|
|
120
|
+
fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError>;
|
|
121
|
+
|
|
122
|
+
fn metadata(&self) -> Result<Metadata, FilesystemError>;
|
|
123
|
+
|
|
124
|
+
/// Truncate or extend the file to `len` bytes, extending with zeros.
|
|
125
|
+
fn set_len(&mut self, len: u64) -> Result<(), FilesystemError>;
|
|
126
|
+
|
|
127
|
+
/// Make what was written durable. The default has nothing to flush,
|
|
128
|
+
/// which is the truthful answer of a file with no durability of its
|
|
129
|
+
/// own.
|
|
130
|
+
fn sync(&mut self) -> Result<(), FilesystemError> {
|
|
131
|
+
Ok(())
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Release the file once the guest closes it, answering whether what
|
|
135
|
+
/// it held was kept. The default has nothing to release; a file the
|
|
136
|
+
/// guest never closes is dropped instead.
|
|
137
|
+
fn close(self: Box<Self>) -> Result<(), FilesystemError> {
|
|
138
|
+
Ok(())
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// How `Filesystem::open` opens a file.
|
|
143
|
+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
144
|
+
#[non_exhaustive]
|
|
145
|
+
pub struct OpenOptions {
|
|
146
|
+
pub read: bool,
|
|
147
|
+
pub write: bool,
|
|
148
|
+
/// Create the file when it does not exist.
|
|
149
|
+
pub create: bool,
|
|
150
|
+
/// Create the file, refusing one that already exists.
|
|
151
|
+
pub create_new: bool,
|
|
152
|
+
/// Empty the file once it is open.
|
|
153
|
+
pub truncate: bool,
|
|
154
|
+
/// Refuse a symbolic link standing at the path, as `O_NOFOLLOW` does;
|
|
155
|
+
/// a Filesystem holding no links has none to refuse.
|
|
156
|
+
pub nofollow: bool,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// What `Filesystem::rename` may move, as `renameat2` flags ask more of
|
|
160
|
+
/// a rename than its two paths.
|
|
161
|
+
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
162
|
+
#[non_exhaustive]
|
|
163
|
+
pub enum Rename {
|
|
164
|
+
/// Whatever stands at the source.
|
|
165
|
+
#[default]
|
|
166
|
+
Any,
|
|
167
|
+
/// Only a file. A directory is refused as `CrossesDevices`, so a
|
|
168
|
+
/// caller copies what is beneath it one entry at a time instead.
|
|
169
|
+
File,
|
|
170
|
+
/// Only a directory, as a trailing `/` asks. A file is refused as
|
|
171
|
+
/// `NotADirectory`.
|
|
172
|
+
Directory,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
impl Rename {
|
|
176
|
+
/// Refuse moving an entry of `file_type` this rename may not move.
|
|
177
|
+
pub fn check(self, file_type: FileType) -> Result<(), FilesystemError> {
|
|
178
|
+
match (self, file_type) {
|
|
179
|
+
(Self::File, FileType::Directory) => Err(FilesystemError::CrossesDevices),
|
|
180
|
+
(Self::Directory, file_type) if file_type != FileType::Directory => {
|
|
181
|
+
Err(FilesystemError::NotADirectory)
|
|
182
|
+
}
|
|
183
|
+
_ => Ok(()),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/// What a path names.
|
|
189
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
190
|
+
#[non_exhaustive]
|
|
191
|
+
pub enum FileType {
|
|
192
|
+
File,
|
|
193
|
+
Directory,
|
|
194
|
+
/// A link, as only `symlink_metadata` and a listing report one.
|
|
195
|
+
Symlink,
|
|
196
|
+
/// Anything else a host holds, such as a FIFO, a socket, or a device,
|
|
197
|
+
/// which no Filesystem opens.
|
|
198
|
+
Unknown,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
202
|
+
#[non_exhaustive]
|
|
203
|
+
pub struct Metadata {
|
|
204
|
+
pub file_type: FileType,
|
|
205
|
+
/// The length in bytes; zero for a directory or a link.
|
|
206
|
+
pub len: u64,
|
|
207
|
+
/// The device and inode numbers naming what the path holds, as
|
|
208
|
+
/// `st_dev` and `st_ino` do. An `ino` of zero reports none, so a
|
|
209
|
+
/// descriptor of such a directory follows its path alone.
|
|
210
|
+
pub dev: u64,
|
|
211
|
+
pub ino: u64,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
impl Metadata {
|
|
215
|
+
/// Metadata reporting no identity.
|
|
216
|
+
pub fn new(file_type: FileType, len: u64) -> Self {
|
|
217
|
+
Self {
|
|
218
|
+
file_type,
|
|
219
|
+
len,
|
|
220
|
+
dev: 0,
|
|
221
|
+
ino: 0,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/// This metadata naming `dev` and `ino` as what the path holds.
|
|
226
|
+
pub fn with_identity(self, dev: u64, ino: u64) -> Self {
|
|
227
|
+
Self { dev, ino, ..self }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/// A device number for a Filesystem with none of its own, as each
|
|
231
|
+
/// tmpfs mount takes one: no earlier call returned it, and its top bit,
|
|
232
|
+
/// which no host's `st_dev` sets, keeps it apart from a host's.
|
|
233
|
+
pub fn unique_device() -> u64 {
|
|
234
|
+
static NEXT: AtomicU64 = AtomicU64::new(1);
|
|
235
|
+
(1 << 63) | NEXT.fetch_add(1, Ordering::Relaxed)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// What keeps a directory's identity its own while a descriptor has the
|
|
240
|
+
/// directory open, released when it drops.
|
|
241
|
+
pub struct Hold {
|
|
242
|
+
_kept: Box<dyn Send + Sync>,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
impl Hold {
|
|
246
|
+
/// A Hold keeping `kept`, such as a host directory's descriptor, until
|
|
247
|
+
/// it drops.
|
|
248
|
+
pub fn new(kept: impl Send + Sync + 'static) -> Self {
|
|
249
|
+
Self {
|
|
250
|
+
_kept: Box::new(kept),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
256
|
+
#[non_exhaustive]
|
|
257
|
+
pub struct DirEntry {
|
|
258
|
+
pub name: String,
|
|
259
|
+
pub file_type: FileType,
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
impl DirEntry {
|
|
263
|
+
/// The entry `name` in its directory, naming a `file_type`.
|
|
264
|
+
pub fn new(name: impl Into<String>, file_type: FileType) -> Self {
|
|
265
|
+
Self {
|
|
266
|
+
name: name.into(),
|
|
267
|
+
file_type,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Why a Filesystem call failed. The runtime hands the guest the matching
|
|
273
|
+
/// error code.
|
|
274
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
275
|
+
#[non_exhaustive]
|
|
276
|
+
pub enum FilesystemError {
|
|
277
|
+
NotFound,
|
|
278
|
+
AlreadyExists,
|
|
279
|
+
NotADirectory,
|
|
280
|
+
IsADirectory,
|
|
281
|
+
DirectoryNotEmpty,
|
|
282
|
+
PermissionDenied,
|
|
283
|
+
ReadOnlyFilesystem,
|
|
284
|
+
InvalidInput,
|
|
285
|
+
Unsupported,
|
|
286
|
+
/// A change past what the Filesystem can hold.
|
|
287
|
+
StorageFull,
|
|
288
|
+
/// A file grown past the largest the Filesystem keeps.
|
|
289
|
+
FileTooLarge,
|
|
290
|
+
/// A move that cannot happen in one step, as between two devices.
|
|
291
|
+
CrossesDevices,
|
|
292
|
+
/// A call that would wait on a lock its own thread holds.
|
|
293
|
+
Deadlock,
|
|
294
|
+
/// A symbolic link where the call asked to follow none.
|
|
295
|
+
FilesystemLoop,
|
|
296
|
+
/// A call only a privileged caller may make, as apart from one the
|
|
297
|
+
/// file's permissions deny.
|
|
298
|
+
NotPermitted,
|
|
299
|
+
BadDescriptor,
|
|
300
|
+
ResourceBusy,
|
|
301
|
+
/// Bytes that are no valid character where the call needs one.
|
|
302
|
+
IllegalSequence,
|
|
303
|
+
Interrupted,
|
|
304
|
+
TooManyOpenFiles,
|
|
305
|
+
/// A name too long for the Filesystem.
|
|
306
|
+
InvalidFilename,
|
|
307
|
+
OutOfMemory,
|
|
308
|
+
NotASocket,
|
|
309
|
+
/// A value too large for the type the call answers in.
|
|
310
|
+
ValueTooLarge,
|
|
311
|
+
NotSeekable,
|
|
312
|
+
/// Any other failure, with what the Filesystem knows of it.
|
|
313
|
+
Other(String),
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/// Each failure under the POSIX error name it stands for. `ENOTSUP` comes
|
|
317
|
+
/// before `EOPNOTSUPP`, a name some hosts give the same error, so
|
|
318
|
+
/// `Unsupported` answers to the first.
|
|
319
|
+
const POSIX_NAMES: [(&str, FilesystemError); 26] = [
|
|
320
|
+
("ENOENT", FilesystemError::NotFound),
|
|
321
|
+
("EEXIST", FilesystemError::AlreadyExists),
|
|
322
|
+
("ENOTDIR", FilesystemError::NotADirectory),
|
|
323
|
+
("EISDIR", FilesystemError::IsADirectory),
|
|
324
|
+
("ENOTEMPTY", FilesystemError::DirectoryNotEmpty),
|
|
325
|
+
("EACCES", FilesystemError::PermissionDenied),
|
|
326
|
+
("EROFS", FilesystemError::ReadOnlyFilesystem),
|
|
327
|
+
("EINVAL", FilesystemError::InvalidInput),
|
|
328
|
+
("ENOTSUP", FilesystemError::Unsupported),
|
|
329
|
+
("EOPNOTSUPP", FilesystemError::Unsupported),
|
|
330
|
+
("ENOSPC", FilesystemError::StorageFull),
|
|
331
|
+
("EFBIG", FilesystemError::FileTooLarge),
|
|
332
|
+
("EXDEV", FilesystemError::CrossesDevices),
|
|
333
|
+
("EDEADLK", FilesystemError::Deadlock),
|
|
334
|
+
("ELOOP", FilesystemError::FilesystemLoop),
|
|
335
|
+
("EPERM", FilesystemError::NotPermitted),
|
|
336
|
+
("EBADF", FilesystemError::BadDescriptor),
|
|
337
|
+
("EBUSY", FilesystemError::ResourceBusy),
|
|
338
|
+
("EILSEQ", FilesystemError::IllegalSequence),
|
|
339
|
+
("EINTR", FilesystemError::Interrupted),
|
|
340
|
+
("EMFILE", FilesystemError::TooManyOpenFiles),
|
|
341
|
+
("ENAMETOOLONG", FilesystemError::InvalidFilename),
|
|
342
|
+
("ENOMEM", FilesystemError::OutOfMemory),
|
|
343
|
+
("ENOTSOCK", FilesystemError::NotASocket),
|
|
344
|
+
("EOVERFLOW", FilesystemError::ValueTooLarge),
|
|
345
|
+
("ESPIPE", FilesystemError::NotSeekable),
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
impl FilesystemError {
|
|
349
|
+
/// The POSIX error name this failure stands for, as `ENOENT`. `Other`
|
|
350
|
+
/// has none.
|
|
351
|
+
pub fn posix_name(&self) -> Option<&'static str> {
|
|
352
|
+
POSIX_NAMES
|
|
353
|
+
.iter()
|
|
354
|
+
.find(|(_, error)| error == self)
|
|
355
|
+
.map(|(name, _)| *name)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// The failure a POSIX error name stands for, if it is one of these.
|
|
359
|
+
pub fn from_posix_name(name: &str) -> Option<Self> {
|
|
360
|
+
POSIX_NAMES
|
|
361
|
+
.iter()
|
|
362
|
+
.find(|(known, _)| *known == name)
|
|
363
|
+
.map(|(_, error)| error.clone())
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[cfg(test)]
|
|
368
|
+
mod tests {
|
|
369
|
+
use super::*;
|
|
370
|
+
|
|
371
|
+
#[test]
|
|
372
|
+
fn every_posix_name_answers_back_to_its_failure() {
|
|
373
|
+
for (name, error) in &POSIX_NAMES {
|
|
374
|
+
assert_eq!(
|
|
375
|
+
FilesystemError::from_posix_name(name)
|
|
376
|
+
.as_ref()
|
|
377
|
+
.and_then(FilesystemError::posix_name)
|
|
378
|
+
.and_then(FilesystemError::from_posix_name),
|
|
379
|
+
Some(error.clone()),
|
|
380
|
+
"{name} through FilesystemError::from_posix_name and posix_name must name {error:?} again"
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
assert_eq!(
|
|
384
|
+
(
|
|
385
|
+
FilesystemError::Unsupported.posix_name(),
|
|
386
|
+
FilesystemError::Other("x".into()).posix_name()
|
|
387
|
+
),
|
|
388
|
+
(Some("ENOTSUP"), None),
|
|
389
|
+
"Unsupported through FilesystemError::posix_name must answer ENOTSUP, and Other none"
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//! A Grant: a host resource the Host App hands the runtime instead of
|
|
2
|
+
//! binding it by name. The Sandbox holds it and each Invocation receives it
|
|
3
|
+
//! afresh; the guest reaches it through its system interface.
|
|
4
|
+
|
|
5
|
+
use crate::mount::Mount;
|
|
6
|
+
|
|
7
|
+
/// One resource handed over for an Invocation. Each kind carries what it
|
|
8
|
+
/// is and decides its own access.
|
|
9
|
+
#[derive(Clone)]
|
|
10
|
+
#[non_exhaustive]
|
|
11
|
+
pub enum Grant {
|
|
12
|
+
Mount(Mount),
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// The kinds of Grant a runtime may support.
|
|
16
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
17
|
+
#[non_exhaustive]
|
|
18
|
+
pub enum GrantKind {
|
|
19
|
+
Mount,
|
|
20
|
+
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
//!
|
|
3
3
|
//! The surface where a wasm engine implementation and a host frontend
|
|
4
4
|
//! meet: the `Runtime` trait, the isolation `Profile` a runtime
|
|
5
|
-
//! declares, the
|
|
5
|
+
//! declares, the Grants it presents to the guest and the `Filesystem` a
|
|
6
|
+
//! Mount places, the neutral per-invocation value types, and the dispatch /
|
|
6
7
|
//! yield re-entry traits a frontend supplies. The messages these carry
|
|
7
8
|
//! are `kobako-transport`'s, which this crate takes as given.
|
|
8
9
|
//! Nothing here depends on an engine or a frontend type — each engine
|
|
@@ -13,6 +14,9 @@
|
|
|
13
14
|
|
|
14
15
|
pub mod dispatch;
|
|
15
16
|
pub mod error;
|
|
17
|
+
pub mod filesystem;
|
|
18
|
+
pub mod grant;
|
|
19
|
+
pub mod mount;
|
|
16
20
|
pub mod profile;
|
|
17
21
|
pub mod runtime;
|
|
18
22
|
pub mod snapshot;
|
|
@@ -23,6 +27,12 @@ pub mod yielder;
|
|
|
23
27
|
// a reader of `kobako_runtime::Runtime` was missing.
|
|
24
28
|
pub use dispatch::DispatchHandler;
|
|
25
29
|
pub use error::{InvokeError, SetupError, Trap};
|
|
30
|
+
pub use filesystem::{
|
|
31
|
+
Access, Blocking, DirEntry, Directory, File, FileType, Filesystem, FilesystemError, Hold,
|
|
32
|
+
Memory, Metadata, OpenOptions, Overlay, Rename, SeedError, Temporary,
|
|
33
|
+
};
|
|
34
|
+
pub use grant::{Grant, GrantKind};
|
|
35
|
+
pub use mount::{Denial, Mount, MountPathError, Policy};
|
|
26
36
|
pub use profile::Profile;
|
|
27
37
|
pub use runtime::{Entry, Runtime};
|
|
28
38
|
pub use snapshot::{Capture, Completion, Snapshot, Usage};
|