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,1100 @@
|
|
|
1
|
+
//! `Memory`: a Filesystem held in memory, living as long as the Host
|
|
2
|
+
//! App's object does.
|
|
3
|
+
|
|
4
|
+
use std::collections::BTreeMap;
|
|
5
|
+
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
|
6
|
+
use std::sync::{Arc, Mutex, MutexGuard};
|
|
7
|
+
|
|
8
|
+
use super::path::{
|
|
9
|
+
ancestors, beneath_itself, components, is_beneath, name, normalize_seed, parent,
|
|
10
|
+
};
|
|
11
|
+
use super::{
|
|
12
|
+
DirEntry, File, FileType, Filesystem, FilesystemError, Hold, Metadata, OpenOptions, Rename,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/// Files and directories held in memory. Every Invocation it is handed to
|
|
16
|
+
/// shares it, so what one writes the next reads. What its files hold
|
|
17
|
+
/// together, and how many entries it holds, are bounded as a tmpfs mount
|
|
18
|
+
/// bounds its size and its inodes, so a guest cannot grow it past what
|
|
19
|
+
/// the Host App allowed.
|
|
20
|
+
pub struct Memory {
|
|
21
|
+
/// Each entry is held under its own name in its directory, as a tmpfs
|
|
22
|
+
/// holds one name to each entry, so the names it holds stay bounded
|
|
23
|
+
/// by its inodes however its directories move.
|
|
24
|
+
root: Mutex<Directory>,
|
|
25
|
+
space: Arc<Space>,
|
|
26
|
+
inodes_follow_limit: bool,
|
|
27
|
+
/// The device number this Memory reports, its own as each tmpfs mount
|
|
28
|
+
/// has its own, so it shares an identity with no other Filesystem.
|
|
29
|
+
dev: u64,
|
|
30
|
+
/// The inode number the next directory takes, so each one made has an
|
|
31
|
+
/// identity no other directory of this Memory had.
|
|
32
|
+
next_ino: AtomicU64,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
enum Node {
|
|
36
|
+
File(Arc<Mutex<Content>>),
|
|
37
|
+
Directory(Directory),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl Node {
|
|
41
|
+
fn file_type(&self) -> FileType {
|
|
42
|
+
match self {
|
|
43
|
+
Self::File(_) => FileType::File,
|
|
44
|
+
Self::Directory(_) => FileType::Directory,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// A directory, its inode number, and its entries by name.
|
|
50
|
+
struct Directory {
|
|
51
|
+
ino: u64,
|
|
52
|
+
entries: BTreeMap<String, Node>,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// What a path names beneath a directory, the directory itself for the
|
|
56
|
+
/// empty path.
|
|
57
|
+
enum Found<'a> {
|
|
58
|
+
File(&'a Arc<Mutex<Content>>),
|
|
59
|
+
Directory(&'a Directory),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
impl Found<'_> {
|
|
63
|
+
fn file_type(&self) -> FileType {
|
|
64
|
+
match self {
|
|
65
|
+
Self::File(_) => FileType::File,
|
|
66
|
+
Self::Directory(_) => FileType::Directory,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
impl Directory {
|
|
72
|
+
/// What `path` names beneath this directory, or why it names nothing:
|
|
73
|
+
/// a file standing where a directory above it should be, or simply no
|
|
74
|
+
/// entry.
|
|
75
|
+
fn find(&self, path: &str) -> Result<Found<'_>, FilesystemError> {
|
|
76
|
+
let mut found = Found::Directory(self);
|
|
77
|
+
for name in components(path) {
|
|
78
|
+
found = match found {
|
|
79
|
+
Found::Directory(dir) => match dir.entries.get(name) {
|
|
80
|
+
Some(Node::File(data)) => Found::File(data),
|
|
81
|
+
Some(Node::Directory(dir)) => Found::Directory(dir),
|
|
82
|
+
None => return Err(FilesystemError::NotFound),
|
|
83
|
+
},
|
|
84
|
+
Found::File(_) => return Err(FilesystemError::NotADirectory),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
Ok(found)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// The directory `path` names beneath this one.
|
|
91
|
+
fn directory_mut(&mut self, path: &str) -> Result<&mut Directory, FilesystemError> {
|
|
92
|
+
let mut dir = self;
|
|
93
|
+
for name in components(path) {
|
|
94
|
+
dir = match dir.entries.get_mut(name) {
|
|
95
|
+
Some(Node::Directory(inner)) => inner,
|
|
96
|
+
Some(Node::File(_)) => return Err(FilesystemError::NotADirectory),
|
|
97
|
+
None => return Err(FilesystemError::NotFound),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
Ok(dir)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// The entries of the directory a new entry at `path` goes into, which
|
|
104
|
+
/// must already be one, and the entry's name there.
|
|
105
|
+
fn parent_mut<'a>(
|
|
106
|
+
&mut self,
|
|
107
|
+
path: &'a str,
|
|
108
|
+
) -> Result<(&mut BTreeMap<String, Node>, &'a str), FilesystemError> {
|
|
109
|
+
Ok((&mut self.directory_mut(parent(path))?.entries, name(path)))
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// What a Memory holds: the bytes of its files, and its entries, each
|
|
114
|
+
/// file and directory and the root counting one as an inode does.
|
|
115
|
+
struct Space {
|
|
116
|
+
bytes: Budget,
|
|
117
|
+
inodes: Budget,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// How much of one thing is used, and the most that may be.
|
|
121
|
+
struct Budget {
|
|
122
|
+
limit: AtomicUsize,
|
|
123
|
+
used: AtomicUsize,
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
impl Budget {
|
|
127
|
+
fn new(limit: Option<usize>) -> Self {
|
|
128
|
+
Self {
|
|
129
|
+
limit: AtomicUsize::new(limit.unwrap_or(usize::MAX)),
|
|
130
|
+
used: AtomicUsize::new(0),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
fn limit(&self) -> Option<usize> {
|
|
135
|
+
Some(self.limit.load(Ordering::Relaxed)).filter(|&limit| limit != usize::MAX)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn set_limit(&self, limit: Option<usize>) {
|
|
139
|
+
self.limit
|
|
140
|
+
.store(limit.unwrap_or(usize::MAX), Ordering::Relaxed);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Count `amount` more, refusing when it would pass the limit.
|
|
144
|
+
fn take(&self, amount: usize) -> Result<(), FilesystemError> {
|
|
145
|
+
let limit = self.limit.load(Ordering::Relaxed);
|
|
146
|
+
self.used
|
|
147
|
+
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |used| {
|
|
148
|
+
used.checked_add(amount).filter(|&total| total <= limit)
|
|
149
|
+
})
|
|
150
|
+
.map(drop)
|
|
151
|
+
.map_err(|_| FilesystemError::StorageFull)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Count `amount` more without refusing, for what the Host App seeds.
|
|
155
|
+
fn add(&self, amount: usize) {
|
|
156
|
+
self.used.fetch_add(amount, Ordering::Relaxed);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn give(&self, amount: usize) {
|
|
160
|
+
self.used.fetch_sub(amount, Ordering::Relaxed);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// One file's bytes, counted against its Memory's space, with the file's
|
|
165
|
+
/// inode, for as long as they live, which outlasts the file's name while
|
|
166
|
+
/// a handle holds them.
|
|
167
|
+
struct Content {
|
|
168
|
+
bytes: Vec<u8>,
|
|
169
|
+
space: Arc<Space>,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
impl Content {
|
|
173
|
+
/// A file the Host App seeds, counted but never refused.
|
|
174
|
+
fn seed(bytes: Vec<u8>, space: &Arc<Space>) -> Arc<Mutex<Self>> {
|
|
175
|
+
space.bytes.add(bytes.len());
|
|
176
|
+
space.inodes.add(1);
|
|
177
|
+
Self::new(bytes, space)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// An empty file the guest creates, refused once no inode is left.
|
|
181
|
+
fn create(space: &Arc<Space>) -> Result<Arc<Mutex<Self>>, FilesystemError> {
|
|
182
|
+
space.inodes.take(1)?;
|
|
183
|
+
Ok(Self::new(Vec::new(), space))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn new(bytes: Vec<u8>, space: &Arc<Space>) -> Arc<Mutex<Self>> {
|
|
187
|
+
Arc::new(Mutex::new(Self {
|
|
188
|
+
bytes,
|
|
189
|
+
space: space.clone(),
|
|
190
|
+
}))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// Resize to `len`, refusing a growth past the space left or past what
|
|
194
|
+
/// can be allocated, rather than aborting the host process.
|
|
195
|
+
fn resize(&mut self, len: usize) -> Result<(), FilesystemError> {
|
|
196
|
+
let growth = len.saturating_sub(self.bytes.len());
|
|
197
|
+
self.space.bytes.take(growth)?;
|
|
198
|
+
if self.bytes.try_reserve_exact(growth).is_err() {
|
|
199
|
+
self.space.bytes.give(growth);
|
|
200
|
+
return Err(FilesystemError::StorageFull);
|
|
201
|
+
}
|
|
202
|
+
self.space.bytes.give(self.bytes.len().saturating_sub(len));
|
|
203
|
+
self.bytes.resize(len, 0);
|
|
204
|
+
Ok(())
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
impl Drop for Content {
|
|
209
|
+
fn drop(&mut self) {
|
|
210
|
+
self.space.bytes.give(self.bytes.len());
|
|
211
|
+
self.space.inodes.give(1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/// The inodes a limit of `bytes` allows unless given its own: one for
|
|
216
|
+
/// each 4 KiB page the bytes span, as a tmpfs counts one page to each
|
|
217
|
+
/// file, and one for the root.
|
|
218
|
+
fn inodes_for(bytes: Option<usize>) -> Option<usize> {
|
|
219
|
+
bytes.map(|bytes| bytes.div_ceil(PAGE).saturating_add(1))
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const PAGE: usize = 4096;
|
|
223
|
+
|
|
224
|
+
impl Memory {
|
|
225
|
+
/// The most bytes a Memory's files hold unless given a limit of their
|
|
226
|
+
/// own: the size Docker gives a `--tmpfs` mount.
|
|
227
|
+
pub const DEFAULT_LIMIT: usize = 64 * 1024 * 1024;
|
|
228
|
+
|
|
229
|
+
pub fn new() -> Self {
|
|
230
|
+
let space = Space {
|
|
231
|
+
bytes: Budget::new(Some(Self::DEFAULT_LIMIT)),
|
|
232
|
+
inodes: Budget::new(inodes_for(Some(Self::DEFAULT_LIMIT))),
|
|
233
|
+
};
|
|
234
|
+
space.inodes.add(1);
|
|
235
|
+
Self {
|
|
236
|
+
root: Mutex::new(Directory {
|
|
237
|
+
ino: 1,
|
|
238
|
+
entries: BTreeMap::new(),
|
|
239
|
+
}),
|
|
240
|
+
space: Arc::new(space),
|
|
241
|
+
inodes_follow_limit: true,
|
|
242
|
+
dev: Metadata::unique_device(),
|
|
243
|
+
next_ino: AtomicU64::new(2),
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// A directory with an inode number no directory of this Memory had.
|
|
248
|
+
fn new_directory(&self) -> Node {
|
|
249
|
+
Node::Directory(Directory {
|
|
250
|
+
ino: self.next_ino.fetch_add(1, Ordering::Relaxed),
|
|
251
|
+
entries: BTreeMap::new(),
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/// This Memory with its files holding at most `limit` bytes together,
|
|
256
|
+
/// or without bound for `None`. Unless given inodes of their own, its
|
|
257
|
+
/// entries follow the limit: one for each 4 KiB it spans, and the
|
|
258
|
+
/// root. What it was seeded with counts, but is never refused.
|
|
259
|
+
pub fn with_limit(self, limit: Option<usize>) -> Self {
|
|
260
|
+
self.space.bytes.set_limit(limit);
|
|
261
|
+
if self.inodes_follow_limit {
|
|
262
|
+
self.space.inodes.set_limit(inodes_for(limit));
|
|
263
|
+
}
|
|
264
|
+
self
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// This Memory holding at most `inodes` entries, the root among them,
|
|
268
|
+
/// or without bound for `None`, as a tmpfs mount's `nr_inodes` does.
|
|
269
|
+
pub fn with_inodes(mut self, inodes: Option<usize>) -> Self {
|
|
270
|
+
self.space.inodes.set_limit(inodes);
|
|
271
|
+
self.inodes_follow_limit = false;
|
|
272
|
+
self
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// An empty Memory bounded as this one is.
|
|
276
|
+
pub(crate) fn to_empty(&self) -> Self {
|
|
277
|
+
let memory = Self::new().with_limit(self.space.bytes.limit());
|
|
278
|
+
if self.inodes_follow_limit {
|
|
279
|
+
memory
|
|
280
|
+
} else {
|
|
281
|
+
memory.with_inodes(self.space.inodes.limit())
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn root(&self) -> MutexGuard<'_, Directory> {
|
|
286
|
+
self.root
|
|
287
|
+
.lock()
|
|
288
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
impl Default for Memory {
|
|
293
|
+
fn default() -> Self {
|
|
294
|
+
Self::new()
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// Why a list of files cannot seed a Memory as given.
|
|
299
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
300
|
+
#[non_exhaustive]
|
|
301
|
+
pub enum SeedError {
|
|
302
|
+
/// A path naming the root, where no file can stand.
|
|
303
|
+
Root,
|
|
304
|
+
/// A file given twice.
|
|
305
|
+
Twice(String),
|
|
306
|
+
/// A path given as a file where another path needs a directory, or
|
|
307
|
+
/// the reverse.
|
|
308
|
+
FileAndDirectory(String),
|
|
309
|
+
/// A path naming `..`, which no runtime hands a Filesystem, so the
|
|
310
|
+
/// file would never be reached.
|
|
311
|
+
DotDot(String),
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
impl std::fmt::Display for SeedError {
|
|
315
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
316
|
+
match self {
|
|
317
|
+
Self::Root => write!(f, "a Memory seeds no file at its root"),
|
|
318
|
+
Self::Twice(path) => write!(f, "a Memory seeds {path} twice"),
|
|
319
|
+
Self::FileAndDirectory(path) => {
|
|
320
|
+
write!(f, "a Memory seeds {path} as both a file and a directory")
|
|
321
|
+
}
|
|
322
|
+
Self::DotDot(path) => write!(f, "a Memory seeds no path through `..`, got {path}"),
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
impl std::error::Error for SeedError {}
|
|
328
|
+
|
|
329
|
+
impl Memory {
|
|
330
|
+
/// A Memory seeded with files, each `(path, contents)`, its directories
|
|
331
|
+
/// created along the way, or why no Memory holds the list as given.
|
|
332
|
+
pub fn try_from_iter<P, C, I>(files: I) -> Result<Self, SeedError>
|
|
333
|
+
where
|
|
334
|
+
P: AsRef<str>,
|
|
335
|
+
C: Into<Vec<u8>>,
|
|
336
|
+
I: IntoIterator<Item = (P, C)>,
|
|
337
|
+
{
|
|
338
|
+
let memory = Self::new();
|
|
339
|
+
{
|
|
340
|
+
let mut root = memory.root();
|
|
341
|
+
for (path, contents) in files {
|
|
342
|
+
let path = normalize_seed(path.as_ref());
|
|
343
|
+
if path.is_empty() {
|
|
344
|
+
return Err(SeedError::Root);
|
|
345
|
+
}
|
|
346
|
+
if path.split('/').any(|name| name == "..") {
|
|
347
|
+
return Err(SeedError::DotDot(path));
|
|
348
|
+
}
|
|
349
|
+
let mut dir = &mut *root;
|
|
350
|
+
for above in ancestors(&path) {
|
|
351
|
+
let node = dir
|
|
352
|
+
.entries
|
|
353
|
+
.entry(name(above).to_string())
|
|
354
|
+
.or_insert_with(|| {
|
|
355
|
+
memory.space.inodes.add(1);
|
|
356
|
+
memory.new_directory()
|
|
357
|
+
});
|
|
358
|
+
dir = match node {
|
|
359
|
+
Node::Directory(inner) => inner,
|
|
360
|
+
Node::File(_) => {
|
|
361
|
+
return Err(SeedError::FileAndDirectory(above.to_string()))
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
let file_name = name(&path).to_string();
|
|
366
|
+
match dir.entries.get(&file_name) {
|
|
367
|
+
Some(Node::Directory(_)) => return Err(SeedError::FileAndDirectory(path)),
|
|
368
|
+
Some(Node::File(_)) => return Err(SeedError::Twice(path)),
|
|
369
|
+
None => {}
|
|
370
|
+
}
|
|
371
|
+
dir.entries.insert(
|
|
372
|
+
file_name,
|
|
373
|
+
Node::File(Content::seed(contents.into(), &memory.space)),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
Ok(memory)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/// A Memory seeded with files, each `(path, contents)`, its directories
|
|
382
|
+
/// created along the way.
|
|
383
|
+
///
|
|
384
|
+
/// # Panics
|
|
385
|
+
///
|
|
386
|
+
/// When `Memory::try_from_iter` refuses the list: a list written into the
|
|
387
|
+
/// Host App's own code that no Memory holds is a defect in that code.
|
|
388
|
+
impl<P: AsRef<str>, C: Into<Vec<u8>>> FromIterator<(P, C)> for Memory {
|
|
389
|
+
fn from_iter<I: IntoIterator<Item = (P, C)>>(files: I) -> Self {
|
|
390
|
+
Self::try_from_iter(files).unwrap_or_else(|error| panic!("{error}"))
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
impl Filesystem for Memory {
|
|
395
|
+
fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
|
|
396
|
+
let mut root = self.root();
|
|
397
|
+
let data = match root.find(path) {
|
|
398
|
+
Ok(Found::Directory(_)) => return Err(FilesystemError::IsADirectory),
|
|
399
|
+
Ok(Found::File(_)) if options.create_new => return Err(FilesystemError::AlreadyExists),
|
|
400
|
+
Ok(Found::File(data)) => {
|
|
401
|
+
if options.truncate {
|
|
402
|
+
lock(data).resize(0)?;
|
|
403
|
+
}
|
|
404
|
+
data.clone()
|
|
405
|
+
}
|
|
406
|
+
Err(FilesystemError::NotFound) if options.create || options.create_new => {
|
|
407
|
+
let (entries, name) = root.parent_mut(path)?;
|
|
408
|
+
let data = Content::create(&self.space)?;
|
|
409
|
+
entries.insert(name.to_string(), Node::File(data.clone()));
|
|
410
|
+
data
|
|
411
|
+
}
|
|
412
|
+
Err(error) => return Err(error),
|
|
413
|
+
};
|
|
414
|
+
Ok(Box::new(MemoryFile { data }))
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
418
|
+
match self.root().find(path)? {
|
|
419
|
+
Found::File(data) => Ok(Metadata::new(FileType::File, lock(data).bytes.len() as u64)),
|
|
420
|
+
Found::Directory(dir) => {
|
|
421
|
+
Ok(Metadata::new(FileType::Directory, 0).with_identity(self.dev, dir.ino))
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/// A Memory holds no links.
|
|
427
|
+
fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
428
|
+
self.metadata(path)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/// A Memory matches names byte for byte.
|
|
432
|
+
fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
|
|
433
|
+
Ok(path.to_string())
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/// A Memory holds no links to follow.
|
|
437
|
+
fn canonicalize_beneath(&self, _base: &str, path: &str) -> Result<String, FilesystemError> {
|
|
438
|
+
Ok(path.to_string())
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// A Memory holds no links a Policy needs kept from being followed.
|
|
442
|
+
fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
|
|
443
|
+
None
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/// A Memory never hands out a directory's identity twice.
|
|
447
|
+
fn hold_directory(&self, _path: &str) -> Result<Option<Hold>, FilesystemError> {
|
|
448
|
+
Ok(None)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
452
|
+
match self.root().find(path)? {
|
|
453
|
+
Found::Directory(dir) => Ok(dir
|
|
454
|
+
.entries
|
|
455
|
+
.iter()
|
|
456
|
+
.map(|(name, node)| DirEntry::new(name.as_str(), node.file_type()))
|
|
457
|
+
.collect()),
|
|
458
|
+
Found::File(_) => Err(FilesystemError::NotADirectory),
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
463
|
+
let mut root = self.root();
|
|
464
|
+
if root.find(path).is_ok() {
|
|
465
|
+
return Err(FilesystemError::AlreadyExists);
|
|
466
|
+
}
|
|
467
|
+
let (entries, name) = root.parent_mut(path)?;
|
|
468
|
+
self.space.inodes.take(1)?;
|
|
469
|
+
entries.insert(name.to_string(), self.new_directory());
|
|
470
|
+
Ok(())
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/// The root stays, as a mount point does.
|
|
474
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
475
|
+
let mut root = self.root();
|
|
476
|
+
if path.is_empty() {
|
|
477
|
+
return Err(if root.entries.is_empty() {
|
|
478
|
+
FilesystemError::ResourceBusy
|
|
479
|
+
} else {
|
|
480
|
+
FilesystemError::DirectoryNotEmpty
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
let (entries, name) = root.parent_mut(path)?;
|
|
484
|
+
match entries.get(name) {
|
|
485
|
+
Some(Node::File(_)) => Err(FilesystemError::NotADirectory),
|
|
486
|
+
Some(Node::Directory(dir)) if !dir.entries.is_empty() => {
|
|
487
|
+
Err(FilesystemError::DirectoryNotEmpty)
|
|
488
|
+
}
|
|
489
|
+
Some(Node::Directory(_)) => {
|
|
490
|
+
entries.remove(name);
|
|
491
|
+
self.space.inodes.give(1);
|
|
492
|
+
Ok(())
|
|
493
|
+
}
|
|
494
|
+
None => Err(FilesystemError::NotFound),
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
499
|
+
let mut root = self.root();
|
|
500
|
+
if path.is_empty() {
|
|
501
|
+
return Err(FilesystemError::IsADirectory);
|
|
502
|
+
}
|
|
503
|
+
let (entries, name) = root.parent_mut(path)?;
|
|
504
|
+
match entries.get(name) {
|
|
505
|
+
Some(Node::File(_)) => {
|
|
506
|
+
entries.remove(name);
|
|
507
|
+
Ok(())
|
|
508
|
+
}
|
|
509
|
+
Some(Node::Directory(_)) => Err(FilesystemError::IsADirectory),
|
|
510
|
+
None => Err(FilesystemError::NotFound),
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/// The entry moves under its new name with all it holds, so no name
|
|
515
|
+
/// beneath it changes.
|
|
516
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
517
|
+
let mut root = self.root();
|
|
518
|
+
let moving = root.find(from)?.file_type();
|
|
519
|
+
if from == to {
|
|
520
|
+
return Ok(());
|
|
521
|
+
}
|
|
522
|
+
rename.check(moving)?;
|
|
523
|
+
if is_beneath(to, from) {
|
|
524
|
+
return Err(beneath_itself(moving));
|
|
525
|
+
}
|
|
526
|
+
root.parent_mut(to)?;
|
|
527
|
+
match (moving, root.find(to)) {
|
|
528
|
+
(FileType::File, Ok(Found::Directory(_))) => return Err(FilesystemError::IsADirectory),
|
|
529
|
+
(FileType::Directory, Ok(Found::File(_))) => {
|
|
530
|
+
return Err(FilesystemError::NotADirectory)
|
|
531
|
+
}
|
|
532
|
+
(FileType::Directory, Ok(Found::Directory(dir))) if !dir.entries.is_empty() => {
|
|
533
|
+
return Err(FilesystemError::DirectoryNotEmpty)
|
|
534
|
+
}
|
|
535
|
+
_ => {}
|
|
536
|
+
}
|
|
537
|
+
let (entries, name) = root.parent_mut(from)?;
|
|
538
|
+
let node = entries.remove(name).ok_or(FilesystemError::NotFound)?;
|
|
539
|
+
let (entries, name) = root.parent_mut(to)?;
|
|
540
|
+
if let Some(Node::Directory(_)) = entries.insert(name.to_string(), node) {
|
|
541
|
+
self.space.inodes.give(1);
|
|
542
|
+
}
|
|
543
|
+
Ok(())
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/// Every Invocation shares a Memory.
|
|
547
|
+
fn for_invocation(&self) -> Option<Arc<dyn Filesystem>> {
|
|
548
|
+
None
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
fn lock(data: &Mutex<Content>) -> MutexGuard<'_, Content> {
|
|
553
|
+
data.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
fn write(data: &mut Content, buf: &[u8], start: usize) -> Result<usize, FilesystemError> {
|
|
557
|
+
if buf.is_empty() {
|
|
558
|
+
return Ok(0);
|
|
559
|
+
}
|
|
560
|
+
let end = start
|
|
561
|
+
.checked_add(buf.len())
|
|
562
|
+
.ok_or(FilesystemError::InvalidInput)?;
|
|
563
|
+
if data.bytes.len() < end {
|
|
564
|
+
data.resize(end)?;
|
|
565
|
+
}
|
|
566
|
+
data.bytes[start..end].copy_from_slice(buf);
|
|
567
|
+
Ok(buf.len())
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/// An open file of a Memory, sharing its bytes with every other handle on
|
|
571
|
+
/// the same path.
|
|
572
|
+
struct MemoryFile {
|
|
573
|
+
data: Arc<Mutex<Content>>,
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
impl File for MemoryFile {
|
|
577
|
+
fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
|
|
578
|
+
let data = &lock(&self.data).bytes;
|
|
579
|
+
let start = usize::try_from(offset)
|
|
580
|
+
.unwrap_or(usize::MAX)
|
|
581
|
+
.min(data.len());
|
|
582
|
+
let read = buf.len().min(data.len() - start);
|
|
583
|
+
buf[..read].copy_from_slice(&data[start..start + read]);
|
|
584
|
+
Ok(read)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
|
|
588
|
+
let start = usize::try_from(offset).map_err(|_| FilesystemError::InvalidInput)?;
|
|
589
|
+
write(&mut lock(&self.data), buf, start)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/// The end is read and written under one lock, so another handle's
|
|
593
|
+
/// append lands before or after this one, never over it.
|
|
594
|
+
fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
|
|
595
|
+
let mut data = lock(&self.data);
|
|
596
|
+
let start = data.bytes.len();
|
|
597
|
+
Ok((start as u64, write(&mut data, buf, start)?))
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
fn metadata(&self) -> Result<Metadata, FilesystemError> {
|
|
601
|
+
Ok(Metadata::new(
|
|
602
|
+
FileType::File,
|
|
603
|
+
lock(&self.data).bytes.len() as u64,
|
|
604
|
+
))
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
|
|
608
|
+
let len = usize::try_from(len).map_err(|_| FilesystemError::InvalidInput)?;
|
|
609
|
+
lock(&self.data).resize(len)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
#[cfg(test)]
|
|
614
|
+
mod tests {
|
|
615
|
+
use super::*;
|
|
616
|
+
|
|
617
|
+
fn read_all(fs: &dyn Filesystem, path: &str) -> Vec<u8> {
|
|
618
|
+
let mut file = fs
|
|
619
|
+
.open(
|
|
620
|
+
path,
|
|
621
|
+
&OpenOptions {
|
|
622
|
+
read: true,
|
|
623
|
+
..OpenOptions::default()
|
|
624
|
+
},
|
|
625
|
+
)
|
|
626
|
+
.expect("the file must open");
|
|
627
|
+
let mut buf = vec![0; 64];
|
|
628
|
+
let read = file.read_at(&mut buf, 0).expect("the file must read");
|
|
629
|
+
buf.truncate(read);
|
|
630
|
+
buf
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// @behavior FS-001
|
|
634
|
+
#[test]
|
|
635
|
+
fn a_memory_serves_the_files_it_was_seeded_with() {
|
|
636
|
+
let memory: Memory = [("/config/app.yml", "name: kobako")].into_iter().collect();
|
|
637
|
+
assert_eq!(
|
|
638
|
+
read_all(&memory, "config/app.yml"),
|
|
639
|
+
b"name: kobako",
|
|
640
|
+
"a file seeded into Memory through FromIterator must read back its content"
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// @behavior FS-067
|
|
645
|
+
#[test]
|
|
646
|
+
fn a_memory_tried_with_a_list_it_cannot_hold_answers_why() {
|
|
647
|
+
let refused = [
|
|
648
|
+
Memory::try_from_iter([("/", "x")]),
|
|
649
|
+
Memory::try_from_iter([("a", "x"), ("a/b", "y")]),
|
|
650
|
+
Memory::try_from_iter([("a", "x"), ("/a", "y")]),
|
|
651
|
+
]
|
|
652
|
+
.map(|tried| tried.map(drop));
|
|
653
|
+
assert_eq!(
|
|
654
|
+
refused,
|
|
655
|
+
[
|
|
656
|
+
Err(SeedError::Root),
|
|
657
|
+
Err(SeedError::FileAndDirectory("a".to_string())),
|
|
658
|
+
Err(SeedError::Twice("a".to_string())),
|
|
659
|
+
],
|
|
660
|
+
"a list naming the root, a file beneath a file, or a file twice through \
|
|
661
|
+
Memory::try_from_iter must answer the SeedError naming it"
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// A runtime never hands a Filesystem `..`, so a file seeded through one
|
|
666
|
+
// would be held where no call reaches it.
|
|
667
|
+
// @behavior FS-087
|
|
668
|
+
#[test]
|
|
669
|
+
fn a_memory_tried_with_a_path_through_dot_dot_answers_why() {
|
|
670
|
+
assert_eq!(
|
|
671
|
+
Memory::try_from_iter([("a/../b", "x")]).map(drop),
|
|
672
|
+
Err(SeedError::DotDot("a/../b".to_string())),
|
|
673
|
+
"a list naming a path through `..` through Memory::try_from_iter must answer \
|
|
674
|
+
SeedError::DotDot naming it"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// @behavior FS-062
|
|
679
|
+
#[test]
|
|
680
|
+
#[should_panic(expected = "a Memory seeds no file at its root")]
|
|
681
|
+
fn a_memory_seeded_at_its_root_panics() {
|
|
682
|
+
let _: Memory = [("/", "x")].into_iter().collect();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// @behavior FS-062
|
|
686
|
+
#[test]
|
|
687
|
+
#[should_panic(expected = "a Memory seeds a as both a file and a directory")]
|
|
688
|
+
fn a_memory_seeded_with_a_file_beneath_a_file_panics() {
|
|
689
|
+
let _: Memory = [("a", "x"), ("a/b", "y")].into_iter().collect();
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// @behavior FS-062
|
|
693
|
+
#[test]
|
|
694
|
+
#[should_panic(expected = "a Memory seeds a as both a file and a directory")]
|
|
695
|
+
fn a_memory_seeded_with_a_file_over_a_directory_panics() {
|
|
696
|
+
let _: Memory = [("a/b", "y"), ("a", "x")].into_iter().collect();
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// @behavior FS-062
|
|
700
|
+
#[test]
|
|
701
|
+
#[should_panic(expected = "a Memory seeds a twice")]
|
|
702
|
+
fn a_memory_seeded_with_one_path_twice_panics() {
|
|
703
|
+
let _: Memory = [("a", "x"), ("a", "y")].into_iter().collect();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// @behavior FS-002
|
|
707
|
+
#[test]
|
|
708
|
+
fn a_memory_keeps_what_was_written() {
|
|
709
|
+
let memory = Memory::new();
|
|
710
|
+
let mut file = memory
|
|
711
|
+
.open(
|
|
712
|
+
"notes.txt",
|
|
713
|
+
&OpenOptions {
|
|
714
|
+
write: true,
|
|
715
|
+
create: true,
|
|
716
|
+
..OpenOptions::default()
|
|
717
|
+
},
|
|
718
|
+
)
|
|
719
|
+
.expect("a new file must be created");
|
|
720
|
+
file.write_at(b"kept", 0).expect("the write must land");
|
|
721
|
+
drop(file);
|
|
722
|
+
assert_eq!(
|
|
723
|
+
read_all(&memory, "notes.txt"),
|
|
724
|
+
b"kept",
|
|
725
|
+
"a file written through Memory::open must read back from the same Memory"
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// @behavior FS-003
|
|
730
|
+
#[test]
|
|
731
|
+
fn every_invocation_shares_the_one_memory() {
|
|
732
|
+
assert!(
|
|
733
|
+
Memory::new().for_invocation().is_none(),
|
|
734
|
+
"Memory::for_invocation must hand every Invocation the same Memory"
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
#[test]
|
|
739
|
+
fn a_directory_with_entries_is_not_removed() {
|
|
740
|
+
let memory: Memory = [("dir/file", "x")].into_iter().collect();
|
|
741
|
+
assert_eq!(
|
|
742
|
+
memory.remove_dir("dir"),
|
|
743
|
+
Err(FilesystemError::DirectoryNotEmpty)
|
|
744
|
+
);
|
|
745
|
+
assert_eq!(memory.remove_file("dir/file"), Ok(()));
|
|
746
|
+
assert_eq!(memory.remove_dir("dir"), Ok(()));
|
|
747
|
+
assert_eq!(memory.metadata("dir"), Err(FilesystemError::NotFound));
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
#[test]
|
|
751
|
+
fn a_renamed_directory_carries_its_entries() {
|
|
752
|
+
let memory: Memory = [("old/inner/file", "x")].into_iter().collect();
|
|
753
|
+
memory
|
|
754
|
+
.rename("old", "new", Rename::Any)
|
|
755
|
+
.expect("the rename must succeed");
|
|
756
|
+
assert_eq!(read_all(&memory, "new/inner/file"), b"x");
|
|
757
|
+
assert_eq!(memory.metadata("old"), Err(FilesystemError::NotFound));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
fn writable(fs: &dyn Filesystem, path: &str) -> Box<dyn File> {
|
|
761
|
+
fs.open(
|
|
762
|
+
path,
|
|
763
|
+
&OpenOptions {
|
|
764
|
+
write: true,
|
|
765
|
+
create: true,
|
|
766
|
+
..OpenOptions::default()
|
|
767
|
+
},
|
|
768
|
+
)
|
|
769
|
+
.expect("the file must open")
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// @behavior FS-022
|
|
773
|
+
#[test]
|
|
774
|
+
fn a_write_past_a_memorys_limit_is_refused() {
|
|
775
|
+
let memory: Memory = [("file", "x")].into_iter().collect();
|
|
776
|
+
let memory = memory.with_limit(Some(4));
|
|
777
|
+
assert_eq!(
|
|
778
|
+
(
|
|
779
|
+
writable(&memory, "file").write_at(b"four", 1),
|
|
780
|
+
read_all(&memory, "file")
|
|
781
|
+
),
|
|
782
|
+
(Err(FilesystemError::StorageFull), b"x".to_vec()),
|
|
783
|
+
"a write growing a Memory's files past its limit through MemoryFile::write_at must \
|
|
784
|
+
answer StorageFull and leave the file as it was"
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// @behavior FS-096
|
|
789
|
+
#[test]
|
|
790
|
+
fn a_write_of_no_bytes_leaves_a_memory_file_as_it_was() {
|
|
791
|
+
let memory: Memory = [("file", "")].into_iter().collect();
|
|
792
|
+
assert_eq!(
|
|
793
|
+
(
|
|
794
|
+
writable(&memory, "file").write_at(b"", 1000),
|
|
795
|
+
read_all(&memory, "file")
|
|
796
|
+
),
|
|
797
|
+
(Ok(0), Vec::new()),
|
|
798
|
+
"an empty write past the end through MemoryFile::write_at must answer 0 and leave \
|
|
799
|
+
the file empty"
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// @behavior FS-023
|
|
804
|
+
#[test]
|
|
805
|
+
fn a_memory_holds_its_default_limit() {
|
|
806
|
+
let memory = Memory::new();
|
|
807
|
+
let mut file = writable(&memory, "file");
|
|
808
|
+
let mib_64 = 64 * 1024 * 1024;
|
|
809
|
+
assert_eq!(
|
|
810
|
+
(file.set_len(mib_64), file.set_len(mib_64 + 1)),
|
|
811
|
+
(Ok(()), Err(FilesystemError::StorageFull)),
|
|
812
|
+
"a Memory given no limit through Memory::new must hold 64 MiB and refuse a byte more"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// @behavior FS-024
|
|
817
|
+
#[test]
|
|
818
|
+
fn a_memory_without_a_limit_grows_past_the_default() {
|
|
819
|
+
let memory = Memory::new().with_limit(None);
|
|
820
|
+
assert_eq!(
|
|
821
|
+
writable(&memory, "file").set_len(Memory::DEFAULT_LIMIT as u64 + 1),
|
|
822
|
+
Ok(()),
|
|
823
|
+
"a Memory given no limit through Memory::with_limit(None) must grow past 64 MiB"
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// @behavior FS-025
|
|
828
|
+
#[test]
|
|
829
|
+
fn a_removed_file_returns_its_space() {
|
|
830
|
+
let memory = Memory::new().with_limit(Some(4));
|
|
831
|
+
writable(&memory, "first")
|
|
832
|
+
.write_at(b"four", 0)
|
|
833
|
+
.expect("the first file must fit");
|
|
834
|
+
memory.remove_file("first").expect("the file must remove");
|
|
835
|
+
assert_eq!(
|
|
836
|
+
writable(&memory, "second").write_at(b"four", 0),
|
|
837
|
+
Ok(4),
|
|
838
|
+
"the space a file removed through Memory::remove_file held must be free for the next"
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// A length past `isize::MAX` is one no allocator can hand out, so the
|
|
843
|
+
// refusal does not depend on how much the host has free.
|
|
844
|
+
// @behavior FS-026
|
|
845
|
+
#[test]
|
|
846
|
+
fn a_file_grown_past_what_can_be_allocated_is_refused() {
|
|
847
|
+
let memory: Memory = [("file", "x")].into_iter().collect();
|
|
848
|
+
let memory = memory.with_limit(None);
|
|
849
|
+
let mut file = writable(&memory, "file");
|
|
850
|
+
let unallocatable = isize::MAX as u64 + 1;
|
|
851
|
+
assert_eq!(
|
|
852
|
+
(
|
|
853
|
+
file.set_len(unallocatable),
|
|
854
|
+
file.write_at(b"x", unallocatable),
|
|
855
|
+
read_all(&memory, "file")
|
|
856
|
+
),
|
|
857
|
+
(
|
|
858
|
+
Err(FilesystemError::StorageFull),
|
|
859
|
+
Err(FilesystemError::StorageFull),
|
|
860
|
+
b"x".to_vec()
|
|
861
|
+
),
|
|
862
|
+
"a length no allocator can hand out through MemoryFile::set_len and write_at must \
|
|
863
|
+
answer StorageFull rather than abort"
|
|
864
|
+
);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
#[test]
|
|
868
|
+
fn a_path_beneath_a_file_is_not_a_directory() {
|
|
869
|
+
let memory: Memory = [("file", "x")].into_iter().collect();
|
|
870
|
+
assert_eq!(
|
|
871
|
+
memory.metadata("file/inner"),
|
|
872
|
+
Err(FilesystemError::NotADirectory)
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
fn create(fs: &dyn Filesystem, path: &str) -> Result<(), FilesystemError> {
|
|
877
|
+
fs.open(
|
|
878
|
+
path,
|
|
879
|
+
&OpenOptions {
|
|
880
|
+
write: true,
|
|
881
|
+
create: true,
|
|
882
|
+
..OpenOptions::default()
|
|
883
|
+
},
|
|
884
|
+
)
|
|
885
|
+
.map(drop)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Three inodes hold the root, one file, and one directory, so neither
|
|
889
|
+
// kind of entry gets past the count.
|
|
890
|
+
// @behavior FS-030
|
|
891
|
+
#[test]
|
|
892
|
+
fn a_memory_holds_at_most_its_inodes() {
|
|
893
|
+
let memory = Memory::new().with_inodes(Some(3));
|
|
894
|
+
assert_eq!(
|
|
895
|
+
(
|
|
896
|
+
create(&memory, "file"),
|
|
897
|
+
memory.create_dir("dir"),
|
|
898
|
+
create(&memory, "past"),
|
|
899
|
+
memory.create_dir("past_dir"),
|
|
900
|
+
),
|
|
901
|
+
(
|
|
902
|
+
Ok(()),
|
|
903
|
+
Ok(()),
|
|
904
|
+
Err(FilesystemError::StorageFull),
|
|
905
|
+
Err(FilesystemError::StorageFull)
|
|
906
|
+
),
|
|
907
|
+
"a Memory given inodes through Memory::with_inodes must refuse a file or directory \
|
|
908
|
+
past them, the root counted, as out of space"
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// @behavior FS-031
|
|
913
|
+
#[test]
|
|
914
|
+
fn a_memorys_inodes_follow_its_limit() {
|
|
915
|
+
let memory = Memory::new().with_limit(Some(2 * 4096));
|
|
916
|
+
assert_eq!(
|
|
917
|
+
(
|
|
918
|
+
create(&memory, "a"),
|
|
919
|
+
create(&memory, "b"),
|
|
920
|
+
create(&memory, "c")
|
|
921
|
+
),
|
|
922
|
+
(Ok(()), Ok(()), Err(FilesystemError::StorageFull)),
|
|
923
|
+
"a Memory given only a limit through Memory::with_limit must hold one entry for each \
|
|
924
|
+
4 KiB of it and the root"
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// The second handle opened before the first appended, so only an end
|
|
929
|
+
// read as the append lands puts its bytes after the first's.
|
|
930
|
+
// @behavior FS-036
|
|
931
|
+
#[test]
|
|
932
|
+
fn a_memory_appends_at_the_end_as_it_stands() {
|
|
933
|
+
let memory: Memory = [("log", "a")].into_iter().collect();
|
|
934
|
+
let mut first = writable(&memory, "log");
|
|
935
|
+
let mut second = writable(&memory, "log");
|
|
936
|
+
assert_eq!(
|
|
937
|
+
(
|
|
938
|
+
first.append(b"bb"),
|
|
939
|
+
second.append(b"ccc"),
|
|
940
|
+
read_all(&memory, "log")
|
|
941
|
+
),
|
|
942
|
+
(Ok((1, 2)), Ok((3, 3)), b"abbccc".to_vec()),
|
|
943
|
+
"MemoryFile::append through two handles on one file must land each at the end as it \
|
|
944
|
+
stands, answering where it landed"
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// @behavior FS-035
|
|
949
|
+
#[test]
|
|
950
|
+
fn a_memory_moves_only_what_a_rename_allows() {
|
|
951
|
+
let memory: Memory = [("dir/inner", "x"), ("file", "y")].into_iter().collect();
|
|
952
|
+
assert_eq!(
|
|
953
|
+
(
|
|
954
|
+
memory.rename("dir", "moved", Rename::File),
|
|
955
|
+
memory.rename("file", "moved", Rename::Directory),
|
|
956
|
+
memory.metadata("dir/inner").map(|m| m.file_type),
|
|
957
|
+
memory.metadata("file").map(|m| m.file_type),
|
|
958
|
+
),
|
|
959
|
+
(
|
|
960
|
+
Err(FilesystemError::CrossesDevices),
|
|
961
|
+
Err(FilesystemError::NotADirectory),
|
|
962
|
+
Ok(FileType::File),
|
|
963
|
+
Ok(FileType::File)
|
|
964
|
+
),
|
|
965
|
+
"Memory::rename must refuse a directory a file-only rename names as crossing \
|
|
966
|
+
devices, and a file a directory-only rename names as no directory, moving neither"
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// A directory replaced by a rename gives its inode back as a removed
|
|
971
|
+
// one does, so both leave room for the next.
|
|
972
|
+
// @behavior FS-034
|
|
973
|
+
#[test]
|
|
974
|
+
fn a_removed_directory_gives_its_inode_back() {
|
|
975
|
+
let memory = Memory::new().with_inodes(Some(3));
|
|
976
|
+
memory
|
|
977
|
+
.create_dir("a")
|
|
978
|
+
.expect("the first directory must fit");
|
|
979
|
+
memory
|
|
980
|
+
.create_dir("b")
|
|
981
|
+
.expect("the second directory must fit");
|
|
982
|
+
memory.remove_dir("a").expect("the directory must remove");
|
|
983
|
+
let after_removal = memory.create_dir("c");
|
|
984
|
+
memory
|
|
985
|
+
.rename("c", "b", Rename::Any)
|
|
986
|
+
.expect("a directory must replace an empty one");
|
|
987
|
+
assert_eq!(
|
|
988
|
+
(after_removal, memory.create_dir("d")),
|
|
989
|
+
(Ok(()), Ok(())),
|
|
990
|
+
"a directory removed through Memory::remove_dir, or replaced through Memory::rename, \
|
|
991
|
+
must give its inode back"
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// The removed file's handle is still open, so its inode is given back
|
|
996
|
+
// only when the handle goes, as Linux keeps an unlinked file's inode.
|
|
997
|
+
// @behavior FS-032
|
|
998
|
+
#[test]
|
|
999
|
+
fn a_removed_file_gives_its_inode_back_once_closed() {
|
|
1000
|
+
let memory = Memory::new().with_inodes(Some(2));
|
|
1001
|
+
let handle = writable(&memory, "first");
|
|
1002
|
+
memory.remove_file("first").expect("the file must remove");
|
|
1003
|
+
let while_open = create(&memory, "second");
|
|
1004
|
+
drop(handle);
|
|
1005
|
+
assert_eq!(
|
|
1006
|
+
(while_open, create(&memory, "second")),
|
|
1007
|
+
(Err(FilesystemError::StorageFull), Ok(())),
|
|
1008
|
+
"the inode of a file removed through Memory::remove_file must stay taken while a \
|
|
1009
|
+
handle holds it, and be free once it closes"
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// A host's `st_dev` never sets the top bit, so a device carrying it
|
|
1014
|
+
// cannot name a host's device, nor one another Filesystem took.
|
|
1015
|
+
// @behavior FS-085
|
|
1016
|
+
#[test]
|
|
1017
|
+
fn a_device_of_its_own_stands_apart_from_every_other_device() {
|
|
1018
|
+
let (first, second) = (Metadata::unique_device(), Metadata::unique_device());
|
|
1019
|
+
let memory = Memory::new().metadata("").expect("the root must stand").dev;
|
|
1020
|
+
assert!(
|
|
1021
|
+
[first, second, memory].iter().all(|dev| dev >> 63 == 1)
|
|
1022
|
+
&& first != second
|
|
1023
|
+
&& memory != first
|
|
1024
|
+
&& memory != second,
|
|
1025
|
+
"devices from Metadata::unique_device and a Memory's root must each set the top bit \
|
|
1026
|
+
and differ from one another"
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
fn names(dir: &Directory) -> Vec<String> {
|
|
1031
|
+
dir.entries
|
|
1032
|
+
.iter()
|
|
1033
|
+
.flat_map(|(name, node)| {
|
|
1034
|
+
let beneath = match node {
|
|
1035
|
+
Node::Directory(inner) => names(inner),
|
|
1036
|
+
Node::File(_) => Vec::new(),
|
|
1037
|
+
};
|
|
1038
|
+
std::iter::once(name.clone()).chain(beneath)
|
|
1039
|
+
})
|
|
1040
|
+
.collect()
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// Each round nests the file one level deeper, so a Memory holding
|
|
1044
|
+
// whole paths would hold names growing with the depth; held under its
|
|
1045
|
+
// own name, every entry stays the one letter it was given.
|
|
1046
|
+
// @behavior FS-094
|
|
1047
|
+
#[test]
|
|
1048
|
+
fn a_memory_holds_each_entry_under_its_own_name() {
|
|
1049
|
+
let memory: Memory = [("d/f", "x")].into_iter().collect();
|
|
1050
|
+
for _ in 0..64 {
|
|
1051
|
+
memory
|
|
1052
|
+
.create_dir("n")
|
|
1053
|
+
.expect("the new directory must be made");
|
|
1054
|
+
memory
|
|
1055
|
+
.rename("d", "n/d", Rename::Any)
|
|
1056
|
+
.expect("the directory must move into the new one");
|
|
1057
|
+
memory
|
|
1058
|
+
.rename("n", "d", Rename::Any)
|
|
1059
|
+
.expect("the new directory must take the old name");
|
|
1060
|
+
}
|
|
1061
|
+
let held = names(&memory.root());
|
|
1062
|
+
assert_eq!(
|
|
1063
|
+
(held, read_all(&memory, &format!("{}f", "d/".repeat(65)))),
|
|
1064
|
+
(
|
|
1065
|
+
std::iter::repeat_n("d".to_string(), 65)
|
|
1066
|
+
.chain(["f".to_string()])
|
|
1067
|
+
.collect::<Vec<_>>(),
|
|
1068
|
+
b"x".to_vec()
|
|
1069
|
+
),
|
|
1070
|
+
"a directory moved deeper 64 times through Memory::rename must leave every entry \
|
|
1071
|
+
holding only its own name, and the file reading where it now lies"
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// @behavior FS-070
|
|
1076
|
+
#[test]
|
|
1077
|
+
fn a_memory_gives_each_directory_it_makes_an_identity_of_its_own() {
|
|
1078
|
+
let memory = Memory::new();
|
|
1079
|
+
memory
|
|
1080
|
+
.create_dir("dir")
|
|
1081
|
+
.expect("the directory must be made");
|
|
1082
|
+
let identity = |path| {
|
|
1083
|
+
let metadata = memory.metadata(path).expect("the directory must stand");
|
|
1084
|
+
(metadata.dev, metadata.ino)
|
|
1085
|
+
};
|
|
1086
|
+
let before = identity("dir");
|
|
1087
|
+
memory
|
|
1088
|
+
.rename("dir", "moved", Rename::Any)
|
|
1089
|
+
.expect("the directory must move");
|
|
1090
|
+
memory
|
|
1091
|
+
.create_dir("dir")
|
|
1092
|
+
.expect("another directory must be made");
|
|
1093
|
+
assert_eq!(
|
|
1094
|
+
(identity("moved") == before, identity("dir") == before),
|
|
1095
|
+
(true, false),
|
|
1096
|
+
"a directory moved through Memory::rename must keep its identity, and one made at its \
|
|
1097
|
+
old path through Memory::create_dir must have another"
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
}
|