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,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 neutral per-invocation value types, and the dispatch /
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};