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,1955 @@
|
|
|
1
|
+
//! `Overlay`: Layers stacked into one Filesystem, each read-only or
|
|
2
|
+
//! writable on its own.
|
|
3
|
+
//!
|
|
4
|
+
//! The rules follow the union filesystems that allow writable Layers
|
|
5
|
+
//! below the top, aufs among them. A removal hides what a read-only Layer
|
|
6
|
+
//! still shows with a whiteout, and a directory made again where one was
|
|
7
|
+
//! removed is marked opaque. Both markers take the OCI image layer
|
|
8
|
+
//! format's names and live in the topmost writable Layer, so they last as
|
|
9
|
+
//! long as it does. Every call holds the Overlay's lock, so a change
|
|
10
|
+
//! across several Layers is never seen half made.
|
|
11
|
+
|
|
12
|
+
mod exclusive;
|
|
13
|
+
mod stack;
|
|
14
|
+
|
|
15
|
+
use std::sync::Arc;
|
|
16
|
+
|
|
17
|
+
pub use exclusive::Blocking;
|
|
18
|
+
|
|
19
|
+
use super::{
|
|
20
|
+
Access, DirEntry, File, Filesystem, FilesystemError, Hold, Metadata, OpenOptions, Rename,
|
|
21
|
+
};
|
|
22
|
+
use exclusive::{Exclusive, Inline};
|
|
23
|
+
use stack::Stack;
|
|
24
|
+
|
|
25
|
+
/// Layers stacked into one Filesystem, the first one added on top. Its
|
|
26
|
+
/// clones share its lock; two Overlays sharing a writable Layer are not
|
|
27
|
+
/// serialized against each other, as overlayfs leaves two mounts sharing
|
|
28
|
+
/// an upper directory undefined.
|
|
29
|
+
#[derive(Clone)]
|
|
30
|
+
pub struct Overlay {
|
|
31
|
+
stack: Stack,
|
|
32
|
+
lock: Arc<Exclusive>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl Default for Overlay {
|
|
36
|
+
fn default() -> Self {
|
|
37
|
+
Self::new()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl Overlay {
|
|
42
|
+
/// An Overlay with no Layer yet.
|
|
43
|
+
pub fn new() -> Self {
|
|
44
|
+
Self {
|
|
45
|
+
stack: Stack::default(),
|
|
46
|
+
lock: Arc::new(Exclusive::new(Arc::new(Inline))),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// This Overlay with `filesystem` as a new Layer beneath the ones
|
|
51
|
+
/// already added, allowing `access` there. It takes a lock of its own,
|
|
52
|
+
/// so an Overlay holding the one it was built from as a Layer does not
|
|
53
|
+
/// wait on itself.
|
|
54
|
+
pub fn layer(mut self, filesystem: Arc<dyn Filesystem>, access: Access) -> Self {
|
|
55
|
+
self.stack.push(filesystem, access);
|
|
56
|
+
self.lock = Arc::new(Exclusive::new(self.lock.blocking()));
|
|
57
|
+
self
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// This Overlay waiting for its lock through `blocking`.
|
|
61
|
+
pub fn with_blocking(self, blocking: Arc<dyn Blocking>) -> Self {
|
|
62
|
+
Self {
|
|
63
|
+
lock: Arc::new(Exclusive::new(blocking)),
|
|
64
|
+
..self
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl Filesystem for Overlay {
|
|
70
|
+
fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
|
|
71
|
+
let _held = self.lock.hold()?;
|
|
72
|
+
self.stack.open(path, options)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
76
|
+
let _held = self.lock.hold()?;
|
|
77
|
+
self.stack.metadata(path)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
81
|
+
let _held = self.lock.hold()?;
|
|
82
|
+
self.stack.symlink_metadata(path)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
|
|
86
|
+
let _held = self.lock.hold()?;
|
|
87
|
+
self.stack.stored_path(path)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn canonicalize_beneath(&self, base: &str, path: &str) -> Result<String, FilesystemError> {
|
|
91
|
+
let _held = self.lock.hold()?;
|
|
92
|
+
self.stack.canonicalize_beneath(base, path)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn hold_directory(&self, path: &str) -> Result<Option<Hold>, FilesystemError> {
|
|
96
|
+
let _held = self.lock.hold()?;
|
|
97
|
+
self.stack.hold_directory(path)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
101
|
+
let _held = self.lock.hold()?;
|
|
102
|
+
self.stack.read_dir(path)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
106
|
+
let _held = self.lock.hold()?;
|
|
107
|
+
self.stack.create_dir(path)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
111
|
+
let _held = self.lock.hold()?;
|
|
112
|
+
self.stack.remove_dir(path)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
116
|
+
let _held = self.lock.hold()?;
|
|
117
|
+
self.stack.remove_file(path)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
121
|
+
let _held = self.lock.hold()?;
|
|
122
|
+
self.stack.rename(from, to, rename)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// A fresh Overlay when any Layer starts each Invocation fresh, over
|
|
126
|
+
/// each Layer's own next Filesystem. It keeps this Overlay's lock,
|
|
127
|
+
/// since the Layers it shares are the same.
|
|
128
|
+
fn for_invocation(&self) -> Option<Arc<dyn Filesystem>> {
|
|
129
|
+
let stack = self.stack.for_invocation()?;
|
|
130
|
+
Some(Arc::new(Self {
|
|
131
|
+
stack,
|
|
132
|
+
lock: self.lock.clone(),
|
|
133
|
+
}))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// An Overlay over each Layer's own Filesystem for a Policy, keeping
|
|
137
|
+
/// this Overlay's lock, since the Layers it shares are the same.
|
|
138
|
+
fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
|
|
139
|
+
let stack = self.stack.for_policy()?;
|
|
140
|
+
Some(Arc::new(Self {
|
|
141
|
+
stack,
|
|
142
|
+
lock: self.lock.clone(),
|
|
143
|
+
}))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[cfg(test)]
|
|
148
|
+
mod tests {
|
|
149
|
+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
150
|
+
use std::sync::Mutex;
|
|
151
|
+
use std::thread;
|
|
152
|
+
use std::time::{Duration, Instant};
|
|
153
|
+
|
|
154
|
+
use super::super::memory::Memory;
|
|
155
|
+
use super::super::temporary::Temporary;
|
|
156
|
+
use super::super::{FileType, Hold};
|
|
157
|
+
use super::*;
|
|
158
|
+
|
|
159
|
+
/// The answer of a test Filesystem holding no links.
|
|
160
|
+
macro_rules! holds_no_links {
|
|
161
|
+
() => {
|
|
162
|
+
fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
163
|
+
self.metadata(path)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn canonicalize_beneath(
|
|
167
|
+
&self,
|
|
168
|
+
_base: &str,
|
|
169
|
+
path: &str,
|
|
170
|
+
) -> Result<String, FilesystemError> {
|
|
171
|
+
Ok(path.to_string())
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
|
|
175
|
+
None
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// The answer of a test Filesystem matching names byte for byte.
|
|
181
|
+
macro_rules! matches_names_exactly {
|
|
182
|
+
() => {
|
|
183
|
+
fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
|
|
184
|
+
Ok(path.to_string())
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// The answer of a test Filesystem whose identities never repeat.
|
|
190
|
+
macro_rules! never_repeats_identities {
|
|
191
|
+
() => {
|
|
192
|
+
fn hold_directory(&self, _path: &str) -> Result<Option<Hold>, FilesystemError> {
|
|
193
|
+
Ok(None)
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// The answer of a test Filesystem every Invocation shares.
|
|
199
|
+
macro_rules! is_shared {
|
|
200
|
+
() => {
|
|
201
|
+
fn for_invocation(&self) -> Option<Arc<dyn Filesystem>> {
|
|
202
|
+
None
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn read(fs: &dyn Filesystem, path: &str) -> Vec<u8> {
|
|
208
|
+
let mut file = fs
|
|
209
|
+
.open(
|
|
210
|
+
path,
|
|
211
|
+
&OpenOptions {
|
|
212
|
+
read: true,
|
|
213
|
+
..OpenOptions::default()
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
.expect("the file must open");
|
|
217
|
+
let mut buf = vec![0; 64];
|
|
218
|
+
let read = file.read_at(&mut buf, 0).expect("the file must read");
|
|
219
|
+
buf.truncate(read);
|
|
220
|
+
buf
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fn write(fs: &dyn Filesystem, path: &str, contents: &[u8]) -> Result<(), FilesystemError> {
|
|
224
|
+
fs.open(
|
|
225
|
+
path,
|
|
226
|
+
&OpenOptions {
|
|
227
|
+
write: true,
|
|
228
|
+
create: true,
|
|
229
|
+
truncate: true,
|
|
230
|
+
..OpenOptions::default()
|
|
231
|
+
},
|
|
232
|
+
)?
|
|
233
|
+
.write_at(contents, 0)
|
|
234
|
+
.map(drop)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
fn overwrite(fs: &dyn Filesystem, path: &str, contents: &[u8]) {
|
|
238
|
+
write(fs, path, contents).expect("the write must land");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn names(fs: &dyn Filesystem, path: &str) -> Vec<String> {
|
|
242
|
+
let mut names: Vec<String> = fs
|
|
243
|
+
.read_dir(path)
|
|
244
|
+
.expect("the directory must list")
|
|
245
|
+
.into_iter()
|
|
246
|
+
.map(|entry| entry.name)
|
|
247
|
+
.collect();
|
|
248
|
+
names.sort();
|
|
249
|
+
names
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn memory_with(path: &str, contents: &str) -> Arc<Memory> {
|
|
253
|
+
Arc::new([(path, contents)].into_iter().collect())
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// A writable `upper` over a read-only Memory holding `dir/file` =
|
|
257
|
+
/// "lower".
|
|
258
|
+
fn overlay_on(upper: Arc<dyn Filesystem>) -> (Overlay, Arc<Memory>) {
|
|
259
|
+
let lower = memory_with("dir/file", "lower");
|
|
260
|
+
let overlay = Overlay::new()
|
|
261
|
+
.layer(upper, Access::ReadWrite)
|
|
262
|
+
.layer(lower.clone(), Access::Read);
|
|
263
|
+
(overlay, lower)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// A host directory holding `target`, `dir/inner`, and the links `link`
|
|
267
|
+
/// to `target`, `dirlink` to `dir`, and `dangling` to nothing, removed
|
|
268
|
+
/// when it drops.
|
|
269
|
+
#[cfg(unix)]
|
|
270
|
+
struct LinkTree(std::path::PathBuf);
|
|
271
|
+
|
|
272
|
+
#[cfg(unix)]
|
|
273
|
+
impl LinkTree {
|
|
274
|
+
fn new(name: &str) -> Self {
|
|
275
|
+
let path =
|
|
276
|
+
std::env::temp_dir().join(format!("kobako-overlay-{name}-{}", std::process::id()));
|
|
277
|
+
let _ = std::fs::remove_dir_all(&path);
|
|
278
|
+
std::fs::create_dir_all(path.join("dir")).expect("the host directory must be created");
|
|
279
|
+
std::fs::write(path.join("target"), "lower").expect("the host file must be written");
|
|
280
|
+
std::fs::write(path.join("dir/inner"), "lower").expect("the host file must be written");
|
|
281
|
+
for (target, link) in [
|
|
282
|
+
("target", "link"),
|
|
283
|
+
("dir", "dirlink"),
|
|
284
|
+
("missing", "dangling"),
|
|
285
|
+
] {
|
|
286
|
+
std::os::unix::fs::symlink(target, path.join(link)).expect("the link must be made");
|
|
287
|
+
}
|
|
288
|
+
Self(path)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/// A writable Memory over the host directory as a read-only Layer.
|
|
292
|
+
fn overlay(&self) -> Overlay {
|
|
293
|
+
let lower =
|
|
294
|
+
super::super::Directory::new(&self.0).expect("the host directory must open");
|
|
295
|
+
Overlay::new()
|
|
296
|
+
.layer(Arc::new(Memory::new()), Access::ReadWrite)
|
|
297
|
+
.layer(Arc::new(lower), Access::Read)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
#[cfg(unix)]
|
|
302
|
+
impl Drop for LinkTree {
|
|
303
|
+
fn drop(&mut self) {
|
|
304
|
+
let _ = std::fs::remove_dir_all(&self.0);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// The link points at a file in the same Layer, so only a lookup that
|
|
309
|
+
// reaches the Layer's own `symlink_metadata` can answer a link.
|
|
310
|
+
// The links stand in the Directory Layer beneath a Memory one, so only
|
|
311
|
+
// the merged view handing them to the Layer holding them resolves them.
|
|
312
|
+
// @behavior FS-098
|
|
313
|
+
#[cfg(unix)]
|
|
314
|
+
#[test]
|
|
315
|
+
fn an_overlay_follows_a_link_beneath_a_directory_within_the_layer_holding_it() {
|
|
316
|
+
let host = LinkTree::new("beneath");
|
|
317
|
+
std::fs::create_dir_all(host.0.join("dir/sub"))
|
|
318
|
+
.expect("the host directory must be created");
|
|
319
|
+
std::fs::write(host.0.join("dir/sub/y"), "inner").expect("the inner file must be written");
|
|
320
|
+
std::os::unix::fs::symlink("sub", host.0.join("dir/in")).expect("the link must be made");
|
|
321
|
+
std::os::unix::fs::symlink("../target", host.0.join("dir/up"))
|
|
322
|
+
.expect("the link must be made");
|
|
323
|
+
let overlay = host.overlay();
|
|
324
|
+
assert_eq!(
|
|
325
|
+
(
|
|
326
|
+
overlay.canonicalize_beneath("dir", "in/y"),
|
|
327
|
+
overlay.canonicalize_beneath("dir", "up"),
|
|
328
|
+
),
|
|
329
|
+
(Ok("sub/y".to_string()), Err(FilesystemError::NotPermitted)),
|
|
330
|
+
"a link a Directory Layer holds within `dir`, resolved through \
|
|
331
|
+
Overlay::canonicalize_beneath, must resolve to where it leads, and one leading out \
|
|
332
|
+
of `dir` must not be permitted"
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// The link stands in the Directory Layer beneath a Memory one, so only
|
|
337
|
+
// the Overlay handing each Layer's own Filesystem for a Policy refuses it.
|
|
338
|
+
// @behavior FS-100
|
|
339
|
+
#[cfg(unix)]
|
|
340
|
+
#[test]
|
|
341
|
+
fn an_overlay_for_a_policy_follows_no_link_in_any_layer() {
|
|
342
|
+
let host = LinkTree::new("for-policy");
|
|
343
|
+
let overlay = host.overlay();
|
|
344
|
+
let reading = OpenOptions {
|
|
345
|
+
read: true,
|
|
346
|
+
..OpenOptions::default()
|
|
347
|
+
};
|
|
348
|
+
let for_policy = overlay
|
|
349
|
+
.for_policy()
|
|
350
|
+
.expect("an Overlay over a Directory has a Filesystem for a Policy");
|
|
351
|
+
assert_eq!(
|
|
352
|
+
(
|
|
353
|
+
overlay.open("dirlink/inner", &reading).map(drop),
|
|
354
|
+
for_policy.open("dirlink/inner", &reading).map(drop),
|
|
355
|
+
),
|
|
356
|
+
(Ok(()), Err(FilesystemError::FilesystemLoop)),
|
|
357
|
+
"a path through a link a Directory Layer holds, opened through an Overlay's \
|
|
358
|
+
for_policy, must be a link loop, where the Overlay itself reaches it"
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// @behavior FS-049
|
|
363
|
+
#[cfg(unix)]
|
|
364
|
+
#[test]
|
|
365
|
+
fn an_overlay_reports_a_link_its_layer_holds_as_a_link() {
|
|
366
|
+
let host = LinkTree::new("lstat");
|
|
367
|
+
let overlay = host.overlay();
|
|
368
|
+
let file_type = |path| {
|
|
369
|
+
overlay
|
|
370
|
+
.symlink_metadata(path)
|
|
371
|
+
.map(|metadata| metadata.file_type)
|
|
372
|
+
};
|
|
373
|
+
assert_eq!(
|
|
374
|
+
(file_type("link"), file_type("dangling")),
|
|
375
|
+
(Ok(FileType::Symlink), Ok(FileType::Symlink)),
|
|
376
|
+
"a link a Directory Layer holds, pointing at a file or at nothing, looked up through \
|
|
377
|
+
Overlay::symlink_metadata must be reported as a link"
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// A link to a directory follows to one, and a dangling link follows to
|
|
382
|
+
// nothing, so only judging each link itself lets both go.
|
|
383
|
+
// @behavior FS-050
|
|
384
|
+
#[cfg(unix)]
|
|
385
|
+
#[test]
|
|
386
|
+
fn an_overlay_removes_a_link_its_read_only_layer_holds_as_itself() {
|
|
387
|
+
let host = LinkTree::new("unlink");
|
|
388
|
+
let overlay = host.overlay();
|
|
389
|
+
let removed = (
|
|
390
|
+
overlay.remove_dir("dirlink"),
|
|
391
|
+
overlay.remove_file("dirlink"),
|
|
392
|
+
overlay.remove_file("dangling"),
|
|
393
|
+
);
|
|
394
|
+
let gone = |path| overlay.symlink_metadata(path).err();
|
|
395
|
+
assert_eq!(
|
|
396
|
+
(
|
|
397
|
+
removed,
|
|
398
|
+
gone("dirlink"),
|
|
399
|
+
gone("dangling"),
|
|
400
|
+
host.0.join("dirlink").is_symlink()
|
|
401
|
+
),
|
|
402
|
+
(
|
|
403
|
+
(Err(FilesystemError::NotADirectory), Ok(()), Ok(())),
|
|
404
|
+
Some(FilesystemError::NotFound),
|
|
405
|
+
Some(FilesystemError::NotFound),
|
|
406
|
+
true
|
|
407
|
+
),
|
|
408
|
+
"a link a read-only Directory Layer holds, removed through Overlay::remove_dir and \
|
|
409
|
+
Overlay::remove_file, must be refused as no directory and then removed as itself, \
|
|
410
|
+
leaving the Layer as it was"
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// A write would copy the FIFO up, which opens it, and a rename would
|
|
415
|
+
// move what no Layer above can hold, so only refusals keep either
|
|
416
|
+
// from waiting on a peer or landing nothing.
|
|
417
|
+
// @behavior FS-092
|
|
418
|
+
#[cfg(unix)]
|
|
419
|
+
#[test]
|
|
420
|
+
fn an_overlay_neither_copies_up_nor_moves_a_fifo_below_it() {
|
|
421
|
+
let host = LinkTree::new("fifo");
|
|
422
|
+
let made = std::process::Command::new("mkfifo")
|
|
423
|
+
.arg(host.0.join("pipe"))
|
|
424
|
+
.status()
|
|
425
|
+
.expect("mkfifo must run");
|
|
426
|
+
assert!(made.success(), "the FIFO must be made");
|
|
427
|
+
let overlay = host.overlay();
|
|
428
|
+
assert_eq!(
|
|
429
|
+
(
|
|
430
|
+
write(&overlay, "pipe", b"upper"),
|
|
431
|
+
overlay.rename("pipe", "moved", Rename::File)
|
|
432
|
+
),
|
|
433
|
+
(
|
|
434
|
+
Err(FilesystemError::PermissionDenied),
|
|
435
|
+
Err(FilesystemError::CrossesDevices)
|
|
436
|
+
),
|
|
437
|
+
"a FIFO a read-only Directory Layer holds, written or renamed through an Overlay, must \
|
|
438
|
+
be refused as permission denied and as crossing devices"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// A FIFO is no directory, so each rename meets a kind it cannot
|
|
443
|
+
// replace; were it taken for neither, the directory made above would
|
|
444
|
+
// hide the FIFO, and the FIFO's rename would answer for its Layer.
|
|
445
|
+
// @behavior FS-095
|
|
446
|
+
#[cfg(unix)]
|
|
447
|
+
#[test]
|
|
448
|
+
fn an_overlay_replaces_neither_a_fifo_nor_a_directory_with_the_other() {
|
|
449
|
+
let host = LinkTree::new("fifo-kinds");
|
|
450
|
+
let made = std::process::Command::new("mkfifo")
|
|
451
|
+
.arg(host.0.join("pipe"))
|
|
452
|
+
.status()
|
|
453
|
+
.expect("mkfifo must run");
|
|
454
|
+
assert!(made.success(), "the FIFO must be made");
|
|
455
|
+
let overlay = host.overlay();
|
|
456
|
+
overlay
|
|
457
|
+
.create_dir("made")
|
|
458
|
+
.expect("a directory must be made in the writable Layer");
|
|
459
|
+
let file_type = |path| overlay.metadata(path).map(|metadata| metadata.file_type);
|
|
460
|
+
assert_eq!(
|
|
461
|
+
(
|
|
462
|
+
overlay.rename("pipe", "dir", Rename::Any),
|
|
463
|
+
overlay.rename("made", "pipe", Rename::Any),
|
|
464
|
+
file_type("pipe"),
|
|
465
|
+
file_type("made"),
|
|
466
|
+
),
|
|
467
|
+
(
|
|
468
|
+
Err(FilesystemError::IsADirectory),
|
|
469
|
+
Err(FilesystemError::NotADirectory),
|
|
470
|
+
Ok(FileType::Unknown),
|
|
471
|
+
Ok(FileType::Directory)
|
|
472
|
+
),
|
|
473
|
+
"a FIFO renamed onto a directory through an Overlay must be refused as a directory, \
|
|
474
|
+
and a directory renamed onto a FIFO as no directory, leaving both in place"
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Each change would land in the writable Memory, which holds no link,
|
|
479
|
+
// so only refusing it keeps the write from parting with its target.
|
|
480
|
+
// @behavior FS-051
|
|
481
|
+
#[cfg(unix)]
|
|
482
|
+
#[test]
|
|
483
|
+
fn an_overlay_refuses_a_change_through_a_link_its_read_only_layer_holds() {
|
|
484
|
+
let host = LinkTree::new("through");
|
|
485
|
+
let overlay = host.overlay();
|
|
486
|
+
let creates = OpenOptions {
|
|
487
|
+
write: true,
|
|
488
|
+
create: true,
|
|
489
|
+
..OpenOptions::default()
|
|
490
|
+
};
|
|
491
|
+
let refused = [
|
|
492
|
+
write(&overlay, "link", b"upper"),
|
|
493
|
+
overlay.open("dirlink/new", &creates).map(drop),
|
|
494
|
+
overlay.open("dangling", &creates).map(drop),
|
|
495
|
+
overlay.rename("link", "moved", Rename::Any),
|
|
496
|
+
];
|
|
497
|
+
assert_eq!(
|
|
498
|
+
(refused, read(&overlay, "target")),
|
|
499
|
+
(
|
|
500
|
+
[
|
|
501
|
+
Err(FilesystemError::CrossesDevices),
|
|
502
|
+
Err(FilesystemError::CrossesDevices),
|
|
503
|
+
Err(FilesystemError::CrossesDevices),
|
|
504
|
+
Err(FilesystemError::CrossesDevices)
|
|
505
|
+
],
|
|
506
|
+
b"lower".to_vec()
|
|
507
|
+
),
|
|
508
|
+
"a write, a create, and a rename reaching through a link a read-only Directory Layer \
|
|
509
|
+
holds, through an Overlay, must be refused as crossing devices and leave the target \
|
|
510
|
+
as it was"
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// The lower Memory holds a readable `dangling`, so only the upper
|
|
515
|
+
// link standing at that path can keep the read from reaching it.
|
|
516
|
+
// @behavior FS-053
|
|
517
|
+
#[cfg(unix)]
|
|
518
|
+
#[test]
|
|
519
|
+
fn a_link_a_layer_holds_hides_what_the_layers_below_hold_there() {
|
|
520
|
+
let host = LinkTree::new("shadow");
|
|
521
|
+
let upper = super::super::Directory::new(&host.0).expect("the host directory must open");
|
|
522
|
+
let overlay = Overlay::new()
|
|
523
|
+
.layer(Arc::new(upper), Access::ReadWrite)
|
|
524
|
+
.layer(memory_with("dangling", "lower"), Access::Read);
|
|
525
|
+
assert_eq!(
|
|
526
|
+
overlay
|
|
527
|
+
.open(
|
|
528
|
+
"dangling",
|
|
529
|
+
&OpenOptions {
|
|
530
|
+
read: true,
|
|
531
|
+
..OpenOptions::default()
|
|
532
|
+
}
|
|
533
|
+
)
|
|
534
|
+
.map(drop),
|
|
535
|
+
Err(FilesystemError::NotFound),
|
|
536
|
+
"a lower file beneath a dangling link a higher Directory Layer holds at its path, \
|
|
537
|
+
opened through Overlay::open, must not be found"
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// The lower Memory holds `dirlink/x` beside where the upper link points,
|
|
542
|
+
// so only the link hiding everything beneath it keeps `x` out.
|
|
543
|
+
// @behavior FS-057
|
|
544
|
+
#[cfg(unix)]
|
|
545
|
+
#[test]
|
|
546
|
+
fn a_link_a_layer_holds_hides_what_the_layers_below_hold_beneath_it() {
|
|
547
|
+
let host = LinkTree::new("shadow-beneath");
|
|
548
|
+
let upper = super::super::Directory::new(&host.0).expect("the host directory must open");
|
|
549
|
+
let overlay = Overlay::new()
|
|
550
|
+
.layer(Arc::new(upper), Access::ReadWrite)
|
|
551
|
+
.layer(memory_with("dirlink/x", "lower"), Access::Read);
|
|
552
|
+
let found = overlay
|
|
553
|
+
.metadata("dirlink/x")
|
|
554
|
+
.map(|metadata| metadata.file_type);
|
|
555
|
+
assert_eq!(
|
|
556
|
+
(names(&overlay, "dirlink"), found),
|
|
557
|
+
(vec!["inner".to_string()], Err(FilesystemError::NotFound)),
|
|
558
|
+
"a directory a higher Directory Layer holds as a link, listed and looked into through \
|
|
559
|
+
an Overlay, must show only where the link points and none of a lower Layer's entries"
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// The lower directory sits in a writable Layer, so only the whiteout,
|
|
564
|
+
// not a removal that cannot take a directory, keeps the view whole.
|
|
565
|
+
// @behavior FS-058
|
|
566
|
+
#[test]
|
|
567
|
+
fn removing_a_file_over_a_lower_directory_leaves_it_hidden() {
|
|
568
|
+
let lower = memory_with("f/g", "lower");
|
|
569
|
+
let overlay = Overlay::new()
|
|
570
|
+
.layer(memory_with("f", "upper"), Access::ReadWrite)
|
|
571
|
+
.layer(lower.clone(), Access::ReadWrite);
|
|
572
|
+
let removed = overlay.remove_file("f");
|
|
573
|
+
assert_eq!(
|
|
574
|
+
(
|
|
575
|
+
removed,
|
|
576
|
+
overlay.symlink_metadata("f").err(),
|
|
577
|
+
lower.metadata("f/g").is_ok()
|
|
578
|
+
),
|
|
579
|
+
(Ok(()), Some(FilesystemError::NotFound), true),
|
|
580
|
+
"a file a higher Layer holds over a lower Layer's directory, removed through \
|
|
581
|
+
Overlay::remove_file, must leave the path gone from the view and the lower \
|
|
582
|
+
directory as it was"
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// The writable Layer's `dir` holds only the whiteout hiding `file`, so
|
|
587
|
+
// only judging the view, not that copy, lets the move go.
|
|
588
|
+
// @behavior FS-059
|
|
589
|
+
#[test]
|
|
590
|
+
fn a_directory_moves_onto_one_holding_only_markers() {
|
|
591
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
592
|
+
overlay
|
|
593
|
+
.remove_file("dir/file")
|
|
594
|
+
.expect("the file must be removed");
|
|
595
|
+
overlay
|
|
596
|
+
.create_dir("new")
|
|
597
|
+
.expect("the directory must be made");
|
|
598
|
+
overwrite(&overlay, "new/made", b"made");
|
|
599
|
+
let moved = overlay.rename("new", "dir", Rename::Directory);
|
|
600
|
+
assert_eq!(
|
|
601
|
+
(moved, names(&overlay, "dir")),
|
|
602
|
+
(Ok(()), vec!["made".to_string()]),
|
|
603
|
+
"a directory renamed through Overlay::rename onto one the view shows empty, whose \
|
|
604
|
+
writable copy holds only a whiteout, must take its place and show only its own entries"
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// The directory made again is marked opaque in the writable Layer, so
|
|
609
|
+
// only that mark tells it apart from one the read-only Layer still holds.
|
|
610
|
+
// @behavior FS-060
|
|
611
|
+
#[test]
|
|
612
|
+
fn a_directory_made_again_moves_as_the_writable_layers_own() {
|
|
613
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
614
|
+
overlay
|
|
615
|
+
.remove_file("dir/file")
|
|
616
|
+
.expect("the file must be removed");
|
|
617
|
+
overlay
|
|
618
|
+
.remove_dir("dir")
|
|
619
|
+
.expect("the directory must be removed");
|
|
620
|
+
overlay
|
|
621
|
+
.create_dir("dir")
|
|
622
|
+
.expect("the directory must be made again");
|
|
623
|
+
let moved = overlay.rename("dir", "moved", Rename::Directory);
|
|
624
|
+
assert_eq!(
|
|
625
|
+
(
|
|
626
|
+
moved,
|
|
627
|
+
overlay.metadata("dir").err(),
|
|
628
|
+
overlay.read_dir("moved").map(|entries| entries.len())
|
|
629
|
+
),
|
|
630
|
+
(Ok(()), Some(FilesystemError::NotFound), Ok(0)),
|
|
631
|
+
"a directory made again through an Overlay where a read-only Layer's one was removed, \
|
|
632
|
+
renamed through Overlay::rename, must move and leave nothing of the lower one behind"
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// @behavior FS-061
|
|
637
|
+
#[test]
|
|
638
|
+
fn an_overlay_refuses_a_file_moved_beneath_itself_as_no_directory() {
|
|
639
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
640
|
+
assert_eq!(
|
|
641
|
+
overlay.rename("dir/file", "dir/file/x", Rename::Any),
|
|
642
|
+
Err(FilesystemError::NotADirectory),
|
|
643
|
+
"a file renamed through Overlay::rename to a path beneath itself must be refused as \
|
|
644
|
+
no directory, as rename(2) refuses it"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// The link points at nothing, so only the link itself can be what an
|
|
649
|
+
// exclusive create or a new directory finds there.
|
|
650
|
+
// @behavior FS-052
|
|
651
|
+
#[cfg(unix)]
|
|
652
|
+
#[test]
|
|
653
|
+
fn an_overlay_finds_a_dangling_link_where_a_new_entry_would_go() {
|
|
654
|
+
let host = LinkTree::new("dangling");
|
|
655
|
+
let overlay = host.overlay();
|
|
656
|
+
let exclusive = OpenOptions {
|
|
657
|
+
write: true,
|
|
658
|
+
create_new: true,
|
|
659
|
+
..OpenOptions::default()
|
|
660
|
+
};
|
|
661
|
+
assert_eq!(
|
|
662
|
+
(
|
|
663
|
+
overlay.open("dangling", &exclusive).map(drop),
|
|
664
|
+
overlay.create_dir("dangling")
|
|
665
|
+
),
|
|
666
|
+
(
|
|
667
|
+
Err(FilesystemError::AlreadyExists),
|
|
668
|
+
Err(FilesystemError::AlreadyExists)
|
|
669
|
+
),
|
|
670
|
+
"an exclusive create and a create_dir at a dangling link a read-only Directory Layer \
|
|
671
|
+
holds, through an Overlay, must find it already there"
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// @behavior FS-008
|
|
676
|
+
#[test]
|
|
677
|
+
fn an_overlay_reads_through_to_a_lower_layer() {
|
|
678
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
679
|
+
assert_eq!(
|
|
680
|
+
read(&overlay, "dir/file"),
|
|
681
|
+
b"lower",
|
|
682
|
+
"a lower file read through Overlay::open must read back the lower content"
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// @behavior FS-009
|
|
687
|
+
#[test]
|
|
688
|
+
fn a_write_to_a_read_only_layers_file_lands_in_the_nearest_writable_layer_above_it() {
|
|
689
|
+
let upper = Arc::new(Memory::new());
|
|
690
|
+
let (overlay, _) = overlay_on(upper.clone());
|
|
691
|
+
overwrite(&overlay, "dir/file", b"upper");
|
|
692
|
+
assert_eq!(
|
|
693
|
+
read(upper.as_ref(), "dir/file"),
|
|
694
|
+
b"upper",
|
|
695
|
+
"a read-only Layer's file written through Overlay::open must land in the writable Layer above it"
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// @behavior FS-010
|
|
700
|
+
#[test]
|
|
701
|
+
fn an_overlay_leaves_a_read_only_layer_as_it_was() {
|
|
702
|
+
let (overlay, lower) = overlay_on(Arc::new(Memory::new()));
|
|
703
|
+
overwrite(&overlay, "dir/file", b"upper");
|
|
704
|
+
overlay
|
|
705
|
+
.remove_file("dir/file")
|
|
706
|
+
.expect("the file must be removed");
|
|
707
|
+
assert_eq!(
|
|
708
|
+
read(lower.as_ref(), "dir/file"),
|
|
709
|
+
b"lower",
|
|
710
|
+
"a read-only Layer's file written and removed through an Overlay must stay as it was"
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// @behavior FS-011
|
|
715
|
+
#[test]
|
|
716
|
+
fn a_file_removed_through_an_overlay_is_gone_from_its_view() {
|
|
717
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
718
|
+
overlay
|
|
719
|
+
.remove_file("dir/file")
|
|
720
|
+
.expect("the file must be removed");
|
|
721
|
+
assert_eq!(
|
|
722
|
+
overlay.metadata("dir/file"),
|
|
723
|
+
Err(FilesystemError::NotFound),
|
|
724
|
+
"a lower file removed through Overlay::remove_file must no longer be found there"
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// @behavior FS-012
|
|
729
|
+
#[test]
|
|
730
|
+
fn an_overlay_over_a_temporary_starts_every_invocation_from_its_lower_layers() {
|
|
731
|
+
let (overlay, _) = overlay_on(Arc::new(Temporary::new()));
|
|
732
|
+
overwrite(&overlay, "dir/file", b"upper");
|
|
733
|
+
let next = overlay
|
|
734
|
+
.for_invocation()
|
|
735
|
+
.expect("an Overlay over a Temporary must start each Invocation afresh");
|
|
736
|
+
assert_eq!(
|
|
737
|
+
read(next.as_ref(), "dir/file"),
|
|
738
|
+
b"lower",
|
|
739
|
+
"Overlay::for_invocation over a Temporary must hand the next Invocation the lower content"
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// @behavior FS-013
|
|
744
|
+
#[test]
|
|
745
|
+
fn an_upper_layer_hides_the_same_path_in_a_lower_one() {
|
|
746
|
+
let overlay = Overlay::new()
|
|
747
|
+
.layer(memory_with("note", "upper"), Access::Read)
|
|
748
|
+
.layer(memory_with("note", "lower"), Access::Read);
|
|
749
|
+
assert_eq!(
|
|
750
|
+
read(&overlay, "note"),
|
|
751
|
+
b"upper",
|
|
752
|
+
"a path both Layers hold read through Overlay::open must read the upper Layer's content"
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// @behavior FS-014
|
|
757
|
+
#[test]
|
|
758
|
+
fn a_write_to_a_writable_layers_file_changes_it_in_place() {
|
|
759
|
+
let upper = Arc::new(Memory::new());
|
|
760
|
+
let lower = memory_with("note", "lower");
|
|
761
|
+
let overlay = Overlay::new()
|
|
762
|
+
.layer(upper.clone(), Access::ReadWrite)
|
|
763
|
+
.layer(lower.clone(), Access::ReadWrite);
|
|
764
|
+
overwrite(&overlay, "note", b"changed");
|
|
765
|
+
assert_eq!(
|
|
766
|
+
(read(lower.as_ref(), "note"), upper.metadata("note")),
|
|
767
|
+
(b"changed".to_vec(), Err(FilesystemError::NotFound)),
|
|
768
|
+
"a writable lower Layer's file written through Overlay::open must change in that Layer alone"
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// The lower Layer is writable, so only the refusal itself keeps the
|
|
773
|
+
// move from landing in two Layers.
|
|
774
|
+
// @behavior FS-088
|
|
775
|
+
#[test]
|
|
776
|
+
fn an_overlay_refuses_to_move_a_file_a_lower_writable_layer_holds() {
|
|
777
|
+
let upper = Arc::new(Memory::new());
|
|
778
|
+
let lower = memory_with("note", "lower");
|
|
779
|
+
let overlay = Overlay::new()
|
|
780
|
+
.layer(upper.clone(), Access::ReadWrite)
|
|
781
|
+
.layer(lower.clone(), Access::ReadWrite);
|
|
782
|
+
assert_eq!(
|
|
783
|
+
(
|
|
784
|
+
overlay.rename("note", "moved", Rename::File),
|
|
785
|
+
read(lower.as_ref(), "note"),
|
|
786
|
+
upper.read_dir("").map(|entries| entries.len())
|
|
787
|
+
),
|
|
788
|
+
(Err(FilesystemError::CrossesDevices), b"lower".to_vec(), Ok(0)),
|
|
789
|
+
"a file a lower writable Layer holds, renamed through Overlay::rename, must be refused \
|
|
790
|
+
as crossing devices with nothing moved or copied"
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// The writable Layer keeps the whiteout the removal left, so only a
|
|
795
|
+
// call that answers by what landed rather than by the clean-up after
|
|
796
|
+
// it succeeds.
|
|
797
|
+
// @behavior FS-089
|
|
798
|
+
#[test]
|
|
799
|
+
fn an_overlay_change_that_lands_succeeds_though_its_whiteout_stays() {
|
|
800
|
+
let (overlay, _) = overlay_on(Arc::new(KeepsWhiteouts(Memory::new())));
|
|
801
|
+
overlay
|
|
802
|
+
.remove_file("dir/file")
|
|
803
|
+
.expect("the file must be removed");
|
|
804
|
+
assert_eq!(
|
|
805
|
+
write(&overlay, "dir/file", b"again").map(|()| read(&overlay, "dir/file")),
|
|
806
|
+
Ok(b"again".to_vec()),
|
|
807
|
+
"a file made again through Overlay::open where it was removed, while the writable \
|
|
808
|
+
Layer refuses to remove the whiteout, must succeed and show the new file"
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// @behavior FS-015
|
|
813
|
+
#[test]
|
|
814
|
+
fn a_new_file_lands_in_the_topmost_writable_layer() {
|
|
815
|
+
let first = Arc::new(Memory::new());
|
|
816
|
+
let second = Arc::new(Memory::new());
|
|
817
|
+
let overlay = Overlay::new()
|
|
818
|
+
.layer(Arc::new(Memory::new()), Access::Read)
|
|
819
|
+
.layer(first.clone(), Access::ReadWrite)
|
|
820
|
+
.layer(second.clone(), Access::ReadWrite);
|
|
821
|
+
overwrite(&overlay, "new", b"made");
|
|
822
|
+
assert_eq!(
|
|
823
|
+
(read(first.as_ref(), "new"), second.metadata("new")),
|
|
824
|
+
(b"made".to_vec(), Err(FilesystemError::NotFound)),
|
|
825
|
+
"a file created through Overlay::open must land in the topmost writable Layer"
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// @behavior FS-016
|
|
830
|
+
#[test]
|
|
831
|
+
fn a_removal_leaves_a_whiteout_in_the_topmost_writable_layer() {
|
|
832
|
+
let upper = Arc::new(Memory::new());
|
|
833
|
+
let (overlay, _) = overlay_on(upper.clone());
|
|
834
|
+
overlay
|
|
835
|
+
.remove_file("dir/file")
|
|
836
|
+
.expect("the file must be removed");
|
|
837
|
+
assert_eq!(
|
|
838
|
+
upper.metadata("dir/.wh.file").map(|metadata| metadata.file_type),
|
|
839
|
+
Ok(FileType::File),
|
|
840
|
+
"a read-only Layer's file removed through Overlay::remove_file must leave .wh.file beside it in the writable Layer"
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// @behavior FS-017
|
|
845
|
+
#[test]
|
|
846
|
+
fn a_whiteout_lasts_as_long_as_its_writable_layer() {
|
|
847
|
+
let upper = Arc::new(Memory::new());
|
|
848
|
+
let (overlay, lower) = overlay_on(upper.clone());
|
|
849
|
+
overlay
|
|
850
|
+
.remove_file("dir/file")
|
|
851
|
+
.expect("the file must be removed");
|
|
852
|
+
let rebuilt = Overlay::new()
|
|
853
|
+
.layer(upper, Access::ReadWrite)
|
|
854
|
+
.layer(lower, Access::Read);
|
|
855
|
+
assert_eq!(
|
|
856
|
+
rebuilt.metadata("dir/file"),
|
|
857
|
+
Err(FilesystemError::NotFound),
|
|
858
|
+
"a lower file removed through an Overlay over a Memory must stay gone from a new Overlay over the same Layers"
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// @behavior FS-018
|
|
863
|
+
#[test]
|
|
864
|
+
fn a_directory_made_again_where_one_was_removed_shows_nothing_from_below() {
|
|
865
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
866
|
+
overlay
|
|
867
|
+
.remove_file("dir/file")
|
|
868
|
+
.expect("the file must be removed");
|
|
869
|
+
overlay
|
|
870
|
+
.remove_dir("dir")
|
|
871
|
+
.expect("the directory must be removed");
|
|
872
|
+
overlay
|
|
873
|
+
.create_dir("dir")
|
|
874
|
+
.expect("the directory must be made");
|
|
875
|
+
assert_eq!(
|
|
876
|
+
names(&overlay, "dir"),
|
|
877
|
+
Vec::<String>::new(),
|
|
878
|
+
"a directory made again through Overlay::create_dir must list nothing from the read-only Layer"
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// @behavior FS-019
|
|
883
|
+
#[test]
|
|
884
|
+
fn an_overlay_lists_none_of_its_markers() {
|
|
885
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
886
|
+
overwrite(&overlay, "dir/new", b"made");
|
|
887
|
+
overlay
|
|
888
|
+
.remove_file("dir/file")
|
|
889
|
+
.expect("the file must be removed");
|
|
890
|
+
assert_eq!(
|
|
891
|
+
names(&overlay, "dir"),
|
|
892
|
+
["new"],
|
|
893
|
+
"a directory holding a whiteout listed through Overlay::read_dir must show no .wh. entry"
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// @behavior FS-020
|
|
898
|
+
#[test]
|
|
899
|
+
fn an_overlay_refuses_a_name_reserved_for_its_markers() {
|
|
900
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
901
|
+
assert_eq!(
|
|
902
|
+
write(&overlay, "dir/.wh.note", b"x"),
|
|
903
|
+
Err(FilesystemError::InvalidInput),
|
|
904
|
+
"a file named .wh.note created through Overlay::open must be refused"
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// A Layer folding case would hold `.WH.note` as the marker hiding `note`.
|
|
909
|
+
// @behavior FS-056
|
|
910
|
+
#[test]
|
|
911
|
+
fn an_overlay_refuses_its_reserved_name_in_any_case() {
|
|
912
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
913
|
+
assert_eq!(
|
|
914
|
+
write(&overlay, "dir/.WH.note", b"x"),
|
|
915
|
+
Err(FilesystemError::InvalidInput),
|
|
916
|
+
"a file named .WH.note created through Overlay::open must be refused"
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// @behavior FS-021
|
|
921
|
+
#[test]
|
|
922
|
+
fn an_overlay_with_no_writable_layer_refuses_every_change_as_read_only() {
|
|
923
|
+
let overlay = Overlay::new().layer(memory_with("dir/file", "lower"), Access::Read);
|
|
924
|
+
assert_eq!(
|
|
925
|
+
write(&overlay, "dir/file", b"x"),
|
|
926
|
+
Err(FilesystemError::ReadOnlyFilesystem),
|
|
927
|
+
"a file written through an Overlay of read-only Layers must be refused as read-only"
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
#[test]
|
|
932
|
+
fn an_overlay_lists_every_layer_without_what_it_hid() {
|
|
933
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
934
|
+
overwrite(&overlay, "dir/new", b"made");
|
|
935
|
+
assert_eq!(names(&overlay, "dir"), ["file", "new"]);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
#[test]
|
|
939
|
+
fn a_lower_file_renamed_through_an_overlay_moves_into_the_writable_layer() {
|
|
940
|
+
let upper = Arc::new(Memory::new());
|
|
941
|
+
let (overlay, lower) = overlay_on(upper.clone());
|
|
942
|
+
overlay
|
|
943
|
+
.rename("dir/file", "dir/moved", Rename::Any)
|
|
944
|
+
.expect("the file must move");
|
|
945
|
+
assert_eq!(
|
|
946
|
+
(
|
|
947
|
+
read(&overlay, "dir/moved"),
|
|
948
|
+
overlay.metadata("dir/file"),
|
|
949
|
+
read(lower.as_ref(), "dir/file"),
|
|
950
|
+
),
|
|
951
|
+
(
|
|
952
|
+
b"lower".to_vec(),
|
|
953
|
+
Err(FilesystemError::NotFound),
|
|
954
|
+
b"lower".to_vec()
|
|
955
|
+
),
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/// A Memory that, the first time it lists a directory, starts another
|
|
960
|
+
/// call through `overlay` on a thread of its own and gives it a while
|
|
961
|
+
/// to finish, recording whether it did before this call returned.
|
|
962
|
+
struct Interleaves {
|
|
963
|
+
memory: Memory,
|
|
964
|
+
overlay: Mutex<Option<Overlay>>,
|
|
965
|
+
finished_inside: Mutex<Option<bool>>,
|
|
966
|
+
other: Mutex<Option<thread::JoinHandle<()>>>,
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
impl Filesystem for Interleaves {
|
|
970
|
+
holds_no_links!();
|
|
971
|
+
never_repeats_identities!();
|
|
972
|
+
is_shared!();
|
|
973
|
+
matches_names_exactly!();
|
|
974
|
+
|
|
975
|
+
fn open(
|
|
976
|
+
&self,
|
|
977
|
+
path: &str,
|
|
978
|
+
options: &OpenOptions,
|
|
979
|
+
) -> Result<Box<dyn File>, FilesystemError> {
|
|
980
|
+
self.memory.open(path, options)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
984
|
+
self.memory.metadata(path)
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
988
|
+
if let Some(overlay) = self.overlay.lock().unwrap().take() {
|
|
989
|
+
let finished = Arc::new(AtomicUsize::new(0));
|
|
990
|
+
let marker = finished.clone();
|
|
991
|
+
let handle = thread::spawn(move || {
|
|
992
|
+
let _ = write(&overlay, "dir/new", b"made");
|
|
993
|
+
marker.store(1, Ordering::SeqCst);
|
|
994
|
+
});
|
|
995
|
+
let deadline = Instant::now() + Duration::from_millis(200);
|
|
996
|
+
while finished.load(Ordering::SeqCst) == 0 && Instant::now() < deadline {
|
|
997
|
+
thread::sleep(Duration::from_millis(5));
|
|
998
|
+
}
|
|
999
|
+
*self.finished_inside.lock().unwrap() = Some(finished.load(Ordering::SeqCst) == 1);
|
|
1000
|
+
*self.other.lock().unwrap() = Some(handle);
|
|
1001
|
+
}
|
|
1002
|
+
self.memory.read_dir(path)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1006
|
+
self.memory.create_dir(path)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1010
|
+
self.memory.remove_dir(path)
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1014
|
+
self.memory.remove_file(path)
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
1018
|
+
self.memory.rename(from, to, rename)
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// The other call starts inside this one, between its emptiness check
|
|
1023
|
+
// and its removal, so only the Overlay's lock keeps the file it
|
|
1024
|
+
// creates from being removed with the directory.
|
|
1025
|
+
// @behavior FS-037
|
|
1026
|
+
#[test]
|
|
1027
|
+
fn a_change_through_an_overlay_waits_for_the_one_in_progress() {
|
|
1028
|
+
let upper = Arc::new(Interleaves {
|
|
1029
|
+
memory: [("dir/.keep", "")].into_iter().collect(),
|
|
1030
|
+
overlay: Mutex::new(None),
|
|
1031
|
+
finished_inside: Mutex::new(None),
|
|
1032
|
+
other: Mutex::new(None),
|
|
1033
|
+
});
|
|
1034
|
+
upper
|
|
1035
|
+
.memory
|
|
1036
|
+
.remove_file("dir/.keep")
|
|
1037
|
+
.expect("the seed must go");
|
|
1038
|
+
let overlay = Overlay::new().layer(upper.clone(), Access::ReadWrite);
|
|
1039
|
+
*upper.overlay.lock().unwrap() = Some(overlay.clone());
|
|
1040
|
+
let removed = overlay.remove_dir("dir");
|
|
1041
|
+
upper
|
|
1042
|
+
.other
|
|
1043
|
+
.lock()
|
|
1044
|
+
.unwrap()
|
|
1045
|
+
.take()
|
|
1046
|
+
.expect("the other call must have started")
|
|
1047
|
+
.join()
|
|
1048
|
+
.expect("the other call must not panic");
|
|
1049
|
+
assert_eq!(
|
|
1050
|
+
(removed, *upper.finished_inside.lock().unwrap(), overlay.metadata("dir/new").is_ok()),
|
|
1051
|
+
(Ok(()), Some(false), false),
|
|
1052
|
+
"a change through Overlay::open started while Overlay::remove_dir runs must wait for it, \
|
|
1053
|
+
failing rather than landing in the directory being removed"
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// @behavior FS-038
|
|
1058
|
+
#[test]
|
|
1059
|
+
fn a_call_through_an_overlay_from_inside_one_answers_a_deadlock() {
|
|
1060
|
+
struct Reenters {
|
|
1061
|
+
memory: Memory,
|
|
1062
|
+
overlay: Mutex<Option<Overlay>>,
|
|
1063
|
+
answer: Mutex<Option<Result<Metadata, FilesystemError>>>,
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
impl Filesystem for Reenters {
|
|
1067
|
+
holds_no_links!();
|
|
1068
|
+
never_repeats_identities!();
|
|
1069
|
+
is_shared!();
|
|
1070
|
+
matches_names_exactly!();
|
|
1071
|
+
|
|
1072
|
+
fn open(
|
|
1073
|
+
&self,
|
|
1074
|
+
path: &str,
|
|
1075
|
+
options: &OpenOptions,
|
|
1076
|
+
) -> Result<Box<dyn File>, FilesystemError> {
|
|
1077
|
+
self.memory.open(path, options)
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1081
|
+
if let Some(overlay) = self.overlay.lock().unwrap().take() {
|
|
1082
|
+
*self.answer.lock().unwrap() = Some(overlay.metadata(path));
|
|
1083
|
+
}
|
|
1084
|
+
self.memory.metadata(path)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1088
|
+
self.memory.read_dir(path)
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1092
|
+
self.memory.create_dir(path)
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1096
|
+
self.memory.remove_dir(path)
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1100
|
+
self.memory.remove_file(path)
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
1104
|
+
self.memory.rename(from, to, rename)
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
let layer = Arc::new(Reenters {
|
|
1109
|
+
memory: [("note", "x")].into_iter().collect(),
|
|
1110
|
+
overlay: Mutex::new(None),
|
|
1111
|
+
answer: Mutex::new(None),
|
|
1112
|
+
});
|
|
1113
|
+
let overlay = Overlay::new().layer(layer.clone(), Access::Read);
|
|
1114
|
+
*layer.overlay.lock().unwrap() = Some(overlay.clone());
|
|
1115
|
+
overlay
|
|
1116
|
+
.metadata("note")
|
|
1117
|
+
.expect("the outer call must answer");
|
|
1118
|
+
assert_eq!(
|
|
1119
|
+
layer.answer.lock().unwrap().take(),
|
|
1120
|
+
Some(Err(FilesystemError::Deadlock)),
|
|
1121
|
+
"a call through an Overlay made from inside one of its own calls on the same thread \
|
|
1122
|
+
must answer Deadlock rather than wait forever"
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/// A writable Memory seeded with `files` and room for `inodes`, over a
|
|
1127
|
+
/// read-only one holding `dir/file` = "lower".
|
|
1128
|
+
fn full_over_lower(files: &[(&str, &str)], inodes: usize) -> (Overlay, Arc<Memory>) {
|
|
1129
|
+
let upper = Memory::from_iter(files.iter().copied()).with_inodes(Some(inodes));
|
|
1130
|
+
let upper = Arc::new(upper);
|
|
1131
|
+
let overlay = Overlay::new()
|
|
1132
|
+
.layer(upper.clone(), Access::ReadWrite)
|
|
1133
|
+
.layer(memory_with("dir/file", "lower"), Access::Read);
|
|
1134
|
+
(overlay, upper)
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// The upper copy is all the room there is, so the whiteout cannot be
|
|
1138
|
+
// made; taking the copy away first would show the lower file again.
|
|
1139
|
+
// @behavior FS-040
|
|
1140
|
+
#[test]
|
|
1141
|
+
fn a_removal_that_cannot_leave_its_whiteout_removes_nothing() {
|
|
1142
|
+
let (overlay, _) = full_over_lower(&[("dir/file", "upper")], 3);
|
|
1143
|
+
assert_eq!(
|
|
1144
|
+
(overlay.remove_file("dir/file"), read(&overlay, "dir/file")),
|
|
1145
|
+
(Err(FilesystemError::StorageFull), b"upper".to_vec()),
|
|
1146
|
+
"a file removed through Overlay::remove_file whose whiteout finds no room must stay as \
|
|
1147
|
+
it was"
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// @behavior FS-041
|
|
1152
|
+
#[test]
|
|
1153
|
+
fn a_directory_that_cannot_be_marked_opaque_is_not_made() {
|
|
1154
|
+
let (overlay, upper) = full_over_lower(&[(".wh.dir", "")], 3);
|
|
1155
|
+
assert_eq!(
|
|
1156
|
+
(
|
|
1157
|
+
overlay.create_dir("dir"),
|
|
1158
|
+
overlay.metadata("dir"),
|
|
1159
|
+
upper.metadata("dir")
|
|
1160
|
+
),
|
|
1161
|
+
(
|
|
1162
|
+
Err(FilesystemError::StorageFull),
|
|
1163
|
+
Err(FilesystemError::NotFound),
|
|
1164
|
+
Err(FilesystemError::NotFound)
|
|
1165
|
+
),
|
|
1166
|
+
"a directory made through Overlay::create_dir where one was removed, with no room for \
|
|
1167
|
+
its opaque marker, must not be made, nor its whiteout lifted"
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// The copy up takes the last room, so the whiteout over the source
|
|
1172
|
+
// cannot be made; moving first would leave the file in two places.
|
|
1173
|
+
// @behavior FS-042
|
|
1174
|
+
#[test]
|
|
1175
|
+
fn a_rename_that_cannot_leave_its_whiteout_moves_nothing() {
|
|
1176
|
+
let (overlay, _) = full_over_lower(&[], 3);
|
|
1177
|
+
assert_eq!(
|
|
1178
|
+
(
|
|
1179
|
+
overlay.rename("dir/file", "dir/moved", Rename::Any),
|
|
1180
|
+
overlay.metadata("dir/moved"),
|
|
1181
|
+
read(&overlay, "dir/file")
|
|
1182
|
+
),
|
|
1183
|
+
(
|
|
1184
|
+
Err(FilesystemError::StorageFull),
|
|
1185
|
+
Err(FilesystemError::NotFound),
|
|
1186
|
+
b"lower".to_vec()
|
|
1187
|
+
),
|
|
1188
|
+
"a file renamed through Overlay::rename whose whiteout finds no room must stay where \
|
|
1189
|
+
it was"
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// The lower writable copy still holds the file the upper whiteout
|
|
1194
|
+
// hides, so it cannot be removed; removing the upper copy first would
|
|
1195
|
+
// show the file again.
|
|
1196
|
+
// @behavior FS-074
|
|
1197
|
+
#[test]
|
|
1198
|
+
fn a_directory_a_lower_writable_layer_still_fills_is_removed_behind_a_whiteout() {
|
|
1199
|
+
let lower = memory_with("dir/file", "lower");
|
|
1200
|
+
let overlay = Overlay::new()
|
|
1201
|
+
.layer(memory_with("dir/.wh.file", ""), Access::ReadWrite)
|
|
1202
|
+
.layer(lower.clone(), Access::ReadWrite);
|
|
1203
|
+
assert_eq!(
|
|
1204
|
+
(
|
|
1205
|
+
overlay.remove_dir("dir"),
|
|
1206
|
+
overlay.metadata("dir").err(),
|
|
1207
|
+
read(lower.as_ref(), "dir/file")
|
|
1208
|
+
),
|
|
1209
|
+
(Ok(()), Some(FilesystemError::NotFound), b"lower".to_vec()),
|
|
1210
|
+
"a directory the view shows empty, removed through Overlay::remove_dir while a lower \
|
|
1211
|
+
writable Layer still holds a whited-out file in it, must leave the view and stay \
|
|
1212
|
+
below unseen"
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// The marker is the one entry past the room the upper Layer has, so
|
|
1217
|
+
// marking after the move would leave the directory moved.
|
|
1218
|
+
// @behavior FS-075
|
|
1219
|
+
#[test]
|
|
1220
|
+
fn a_rename_that_cannot_mark_its_directory_opaque_moves_nothing() {
|
|
1221
|
+
let upper = Arc::new(Memory::new().with_inodes(Some(2)));
|
|
1222
|
+
upper
|
|
1223
|
+
.create_dir("moving")
|
|
1224
|
+
.expect("the directory must be made");
|
|
1225
|
+
let lower = Arc::new(Memory::new());
|
|
1226
|
+
lower.create_dir("dir").expect("the directory must be made");
|
|
1227
|
+
let overlay = Overlay::new()
|
|
1228
|
+
.layer(upper.clone(), Access::ReadWrite)
|
|
1229
|
+
.layer(lower, Access::Read);
|
|
1230
|
+
assert_eq!(
|
|
1231
|
+
(
|
|
1232
|
+
overlay.rename("moving", "dir", Rename::Directory),
|
|
1233
|
+
upper.metadata("moving").map(|metadata| metadata.file_type),
|
|
1234
|
+
upper.metadata("dir").err()
|
|
1235
|
+
),
|
|
1236
|
+
(
|
|
1237
|
+
Err(FilesystemError::StorageFull),
|
|
1238
|
+
Ok(FileType::Directory),
|
|
1239
|
+
Some(FilesystemError::NotFound)
|
|
1240
|
+
),
|
|
1241
|
+
"a directory renamed through Overlay::rename over one a lower Layer holds, with no \
|
|
1242
|
+
room for its opaque marker, must stay where it was"
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// The writable Layer sits between two read-only ones, so a rename
|
|
1247
|
+
// landing in it beneath the top Layer's `to` would vanish from view.
|
|
1248
|
+
// @behavior FS-076
|
|
1249
|
+
#[test]
|
|
1250
|
+
fn a_rename_a_read_only_layer_above_would_cover_is_refused() {
|
|
1251
|
+
let overlay = Overlay::new()
|
|
1252
|
+
.layer(memory_with("to", "top"), Access::Read)
|
|
1253
|
+
.layer(Arc::new(Memory::new()), Access::ReadWrite)
|
|
1254
|
+
.layer(memory_with("from", "moved"), Access::Read);
|
|
1255
|
+
assert_eq!(
|
|
1256
|
+
(
|
|
1257
|
+
overlay.rename("from", "to", Rename::Any),
|
|
1258
|
+
read(&overlay, "from"),
|
|
1259
|
+
read(&overlay, "to")
|
|
1260
|
+
),
|
|
1261
|
+
(
|
|
1262
|
+
Err(FilesystemError::ReadOnlyFilesystem),
|
|
1263
|
+
b"moved".to_vec(),
|
|
1264
|
+
b"top".to_vec()
|
|
1265
|
+
),
|
|
1266
|
+
"a file renamed through Overlay::rename onto a name a read-only Layer above the \
|
|
1267
|
+
writable one holds must be refused as read-only and move nothing"
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// @behavior FS-077
|
|
1272
|
+
#[test]
|
|
1273
|
+
fn a_name_a_read_only_layer_above_hides_is_not_made() {
|
|
1274
|
+
let upper = Arc::new(Memory::new());
|
|
1275
|
+
let overlay = Overlay::new()
|
|
1276
|
+
.layer(
|
|
1277
|
+
Arc::new(
|
|
1278
|
+
[(".wh.file", ""), (".wh.dir", "")]
|
|
1279
|
+
.into_iter()
|
|
1280
|
+
.collect::<Memory>(),
|
|
1281
|
+
),
|
|
1282
|
+
Access::Read,
|
|
1283
|
+
)
|
|
1284
|
+
.layer(upper.clone(), Access::ReadWrite);
|
|
1285
|
+
assert_eq!(
|
|
1286
|
+
(
|
|
1287
|
+
write(&overlay, "file", b"new"),
|
|
1288
|
+
overlay.create_dir("dir"),
|
|
1289
|
+
upper.metadata("file").err(),
|
|
1290
|
+
upper.metadata("dir").err()
|
|
1291
|
+
),
|
|
1292
|
+
(
|
|
1293
|
+
Err(FilesystemError::ReadOnlyFilesystem),
|
|
1294
|
+
Err(FilesystemError::ReadOnlyFilesystem),
|
|
1295
|
+
Some(FilesystemError::NotFound),
|
|
1296
|
+
Some(FilesystemError::NotFound)
|
|
1297
|
+
),
|
|
1298
|
+
"a file and a directory made through an Overlay where a read-only Layer above the \
|
|
1299
|
+
writable one holds their whiteouts must be refused as read-only and not made"
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// @behavior FS-043
|
|
1304
|
+
#[test]
|
|
1305
|
+
fn a_directory_a_lower_layer_holds_does_not_move_through_an_overlay() {
|
|
1306
|
+
let (overlay, _) = overlay_on(Arc::new(Memory::new()));
|
|
1307
|
+
assert_eq!(
|
|
1308
|
+
(
|
|
1309
|
+
overlay.rename("dir", "moved", Rename::Any),
|
|
1310
|
+
overlay.metadata("moved")
|
|
1311
|
+
),
|
|
1312
|
+
(
|
|
1313
|
+
Err(FilesystemError::CrossesDevices),
|
|
1314
|
+
Err(FilesystemError::NotFound)
|
|
1315
|
+
),
|
|
1316
|
+
"a directory a read-only Layer holds renamed through Overlay::rename must be refused \
|
|
1317
|
+
as crossing devices, as overlayfs refuses it"
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// The directory shows only from the read-only Layer, so only a check of
|
|
1322
|
+
// the whole view finds it in the way.
|
|
1323
|
+
// @behavior FS-044
|
|
1324
|
+
#[test]
|
|
1325
|
+
fn a_rename_onto_what_an_overlay_shows_is_checked_against_the_view() {
|
|
1326
|
+
let upper = Arc::new(Memory::new());
|
|
1327
|
+
let (overlay, _) = overlay_on(upper.clone());
|
|
1328
|
+
overwrite(&overlay, "note", b"x");
|
|
1329
|
+
overlay
|
|
1330
|
+
.create_dir("empty")
|
|
1331
|
+
.expect("the directory must be made");
|
|
1332
|
+
assert_eq!(
|
|
1333
|
+
(
|
|
1334
|
+
overlay.rename("note", "dir", Rename::Any),
|
|
1335
|
+
overlay.rename("empty", "dir", Rename::Any),
|
|
1336
|
+
read(&overlay, "dir/file")
|
|
1337
|
+
),
|
|
1338
|
+
(
|
|
1339
|
+
Err(FilesystemError::IsADirectory),
|
|
1340
|
+
Err(FilesystemError::DirectoryNotEmpty),
|
|
1341
|
+
b"lower".to_vec()
|
|
1342
|
+
),
|
|
1343
|
+
"a rename through Overlay::rename onto a directory only a read-only Layer shows must \
|
|
1344
|
+
be refused as rename(2) refuses it, leaving that directory as it was"
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// A directory moved over an empty one a lower Layer holds must not take
|
|
1349
|
+
// up what that Layer later shows there.
|
|
1350
|
+
// @behavior FS-045
|
|
1351
|
+
#[test]
|
|
1352
|
+
fn a_directory_moved_over_a_lower_one_is_marked_opaque() {
|
|
1353
|
+
let upper = Arc::new(Memory::new());
|
|
1354
|
+
let lower: Arc<Memory> = Arc::new([("dir/.keep", "")].into_iter().collect());
|
|
1355
|
+
lower.remove_file("dir/.keep").expect("the seed must go");
|
|
1356
|
+
let overlay = Overlay::new()
|
|
1357
|
+
.layer(upper.clone(), Access::ReadWrite)
|
|
1358
|
+
.layer(lower.clone(), Access::Read);
|
|
1359
|
+
overlay
|
|
1360
|
+
.create_dir("made")
|
|
1361
|
+
.expect("the directory must be made");
|
|
1362
|
+
overlay
|
|
1363
|
+
.rename("made", "dir", Rename::Any)
|
|
1364
|
+
.expect("the directory must move over the empty one");
|
|
1365
|
+
assert_eq!(
|
|
1366
|
+
upper
|
|
1367
|
+
.metadata("dir/.wh..wh..opq")
|
|
1368
|
+
.map(|metadata| metadata.file_type),
|
|
1369
|
+
Ok(FileType::File),
|
|
1370
|
+
"a directory moved through Overlay::rename over one a read-only Layer holds must be \
|
|
1371
|
+
marked opaque in the writable Layer"
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// The writable Layer has room for the copy's entry but for less than
|
|
1376
|
+
// the lower file's bytes, so the copy fails part way.
|
|
1377
|
+
// @behavior FS-046
|
|
1378
|
+
#[test]
|
|
1379
|
+
fn a_copy_up_that_fails_part_way_leaves_nothing_behind() {
|
|
1380
|
+
let upper = Arc::new(Memory::new().with_limit(Some(2)).with_inodes(Some(8)));
|
|
1381
|
+
let (overlay, _) = overlay_on(upper.clone());
|
|
1382
|
+
assert_eq!(
|
|
1383
|
+
(
|
|
1384
|
+
write(&overlay, "dir/file", b"x"),
|
|
1385
|
+
upper.metadata("dir/file"),
|
|
1386
|
+
read(&overlay, "dir/file")
|
|
1387
|
+
),
|
|
1388
|
+
(
|
|
1389
|
+
Err(FilesystemError::StorageFull),
|
|
1390
|
+
Err(FilesystemError::NotFound),
|
|
1391
|
+
b"lower".to_vec()
|
|
1392
|
+
),
|
|
1393
|
+
"a write through Overlay::open whose copy up runs out of room must leave no partial \
|
|
1394
|
+
copy hiding the lower file"
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
/// A Memory whose files write at most three bytes per call and, when
|
|
1399
|
+
/// `refuses_close`, refuse their close.
|
|
1400
|
+
struct Trickles {
|
|
1401
|
+
memory: Memory,
|
|
1402
|
+
refuses_close: bool,
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
struct TrickleFile {
|
|
1406
|
+
file: Box<dyn File>,
|
|
1407
|
+
refuses_close: bool,
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
impl File for TrickleFile {
|
|
1411
|
+
fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
|
|
1412
|
+
self.file.read_at(buf, offset)
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
|
|
1416
|
+
self.file.write_at(&buf[..buf.len().min(3)], offset)
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
|
|
1420
|
+
self.file.append(&buf[..buf.len().min(3)])
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
fn metadata(&self) -> Result<Metadata, FilesystemError> {
|
|
1424
|
+
self.file.metadata()
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
|
|
1428
|
+
self.file.set_len(len)
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
fn close(self: Box<Self>) -> Result<(), FilesystemError> {
|
|
1432
|
+
if self.refuses_close {
|
|
1433
|
+
return Err(FilesystemError::PermissionDenied);
|
|
1434
|
+
}
|
|
1435
|
+
self.file.close()
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
impl Filesystem for Trickles {
|
|
1440
|
+
holds_no_links!();
|
|
1441
|
+
never_repeats_identities!();
|
|
1442
|
+
is_shared!();
|
|
1443
|
+
matches_names_exactly!();
|
|
1444
|
+
|
|
1445
|
+
fn open(
|
|
1446
|
+
&self,
|
|
1447
|
+
path: &str,
|
|
1448
|
+
options: &OpenOptions,
|
|
1449
|
+
) -> Result<Box<dyn File>, FilesystemError> {
|
|
1450
|
+
Ok(Box::new(TrickleFile {
|
|
1451
|
+
file: self.memory.open(path, options)?,
|
|
1452
|
+
refuses_close: self.refuses_close,
|
|
1453
|
+
}))
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1457
|
+
self.memory.metadata(path)
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1461
|
+
self.memory.read_dir(path)
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1465
|
+
self.memory.create_dir(path)
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1469
|
+
self.memory.remove_dir(path)
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1473
|
+
self.memory.remove_file(path)
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
1477
|
+
self.memory.rename(from, to, rename)
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/// A Memory refusing to move or remove a directory.
|
|
1482
|
+
struct HoldsDirectories(Memory);
|
|
1483
|
+
|
|
1484
|
+
impl Filesystem for HoldsDirectories {
|
|
1485
|
+
holds_no_links!();
|
|
1486
|
+
never_repeats_identities!();
|
|
1487
|
+
is_shared!();
|
|
1488
|
+
matches_names_exactly!();
|
|
1489
|
+
|
|
1490
|
+
fn open(
|
|
1491
|
+
&self,
|
|
1492
|
+
path: &str,
|
|
1493
|
+
options: &OpenOptions,
|
|
1494
|
+
) -> Result<Box<dyn File>, FilesystemError> {
|
|
1495
|
+
self.0.open(path, options)
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1499
|
+
self.0.metadata(path)
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1503
|
+
self.0.read_dir(path)
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1507
|
+
self.0.create_dir(path)
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
fn remove_dir(&self, _path: &str) -> Result<(), FilesystemError> {
|
|
1511
|
+
Err(FilesystemError::PermissionDenied)
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1515
|
+
self.0.remove_file(path)
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
fn rename(&self, _from: &str, _to: &str, _rename: Rename) -> Result<(), FilesystemError> {
|
|
1519
|
+
Err(FilesystemError::PermissionDenied)
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/// A Memory refusing to remove a whiteout.
|
|
1524
|
+
struct KeepsWhiteouts(Memory);
|
|
1525
|
+
|
|
1526
|
+
impl Filesystem for KeepsWhiteouts {
|
|
1527
|
+
holds_no_links!();
|
|
1528
|
+
never_repeats_identities!();
|
|
1529
|
+
is_shared!();
|
|
1530
|
+
matches_names_exactly!();
|
|
1531
|
+
|
|
1532
|
+
fn open(
|
|
1533
|
+
&self,
|
|
1534
|
+
path: &str,
|
|
1535
|
+
options: &OpenOptions,
|
|
1536
|
+
) -> Result<Box<dyn File>, FilesystemError> {
|
|
1537
|
+
self.0.open(path, options)
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1541
|
+
self.0.metadata(path)
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1545
|
+
self.0.read_dir(path)
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1549
|
+
self.0.create_dir(path)
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1553
|
+
self.0.remove_dir(path)
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1557
|
+
match path.rsplit('/').next() {
|
|
1558
|
+
Some(name) if name.starts_with(".wh.") => Err(FilesystemError::PermissionDenied),
|
|
1559
|
+
_ => self.0.remove_file(path),
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
1564
|
+
self.0.rename(from, to, rename)
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/// A Memory answering a lookup of any marker name with an I/O failure.
|
|
1569
|
+
struct ReadsNoMarker(Memory);
|
|
1570
|
+
|
|
1571
|
+
impl Filesystem for ReadsNoMarker {
|
|
1572
|
+
holds_no_links!();
|
|
1573
|
+
never_repeats_identities!();
|
|
1574
|
+
is_shared!();
|
|
1575
|
+
matches_names_exactly!();
|
|
1576
|
+
|
|
1577
|
+
fn open(
|
|
1578
|
+
&self,
|
|
1579
|
+
path: &str,
|
|
1580
|
+
options: &OpenOptions,
|
|
1581
|
+
) -> Result<Box<dyn File>, FilesystemError> {
|
|
1582
|
+
self.0.open(path, options)
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1586
|
+
if path
|
|
1587
|
+
.rsplit('/')
|
|
1588
|
+
.next()
|
|
1589
|
+
.is_some_and(|name| name.starts_with(".wh."))
|
|
1590
|
+
{
|
|
1591
|
+
return Err(FilesystemError::Other(
|
|
1592
|
+
"the marker cannot be read".to_string(),
|
|
1593
|
+
));
|
|
1594
|
+
}
|
|
1595
|
+
self.0.metadata(path)
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1599
|
+
self.0.read_dir(path)
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1603
|
+
self.0.create_dir(path)
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1607
|
+
self.0.remove_dir(path)
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1611
|
+
self.0.remove_file(path)
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
1615
|
+
self.0.rename(from, to, rename)
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/// A Memory whose `file` is gone after its first lookup, as a host
|
|
1620
|
+
/// directory another process changes mid-call.
|
|
1621
|
+
struct Vanishes {
|
|
1622
|
+
memory: Memory,
|
|
1623
|
+
seen: AtomicBool,
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
impl Filesystem for Vanishes {
|
|
1627
|
+
holds_no_links!();
|
|
1628
|
+
never_repeats_identities!();
|
|
1629
|
+
is_shared!();
|
|
1630
|
+
matches_names_exactly!();
|
|
1631
|
+
|
|
1632
|
+
fn open(
|
|
1633
|
+
&self,
|
|
1634
|
+
path: &str,
|
|
1635
|
+
options: &OpenOptions,
|
|
1636
|
+
) -> Result<Box<dyn File>, FilesystemError> {
|
|
1637
|
+
self.memory.open(path, options)
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1641
|
+
if path == "file" && self.seen.swap(true, Ordering::SeqCst) {
|
|
1642
|
+
return Err(FilesystemError::NotFound);
|
|
1643
|
+
}
|
|
1644
|
+
self.memory.metadata(path)
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1648
|
+
self.memory.read_dir(path)
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1652
|
+
self.memory.create_dir(path)
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1656
|
+
self.memory.remove_dir(path)
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1660
|
+
self.memory.remove_file(path)
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
1664
|
+
self.memory.rename(from, to, rename)
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
/// A Memory storing names in lower case and reaching each by any
|
|
1669
|
+
/// case, as a host that folds case does.
|
|
1670
|
+
struct FoldsCase(Memory);
|
|
1671
|
+
|
|
1672
|
+
impl Filesystem for FoldsCase {
|
|
1673
|
+
holds_no_links!();
|
|
1674
|
+
never_repeats_identities!();
|
|
1675
|
+
is_shared!();
|
|
1676
|
+
|
|
1677
|
+
fn open(
|
|
1678
|
+
&self,
|
|
1679
|
+
path: &str,
|
|
1680
|
+
options: &OpenOptions,
|
|
1681
|
+
) -> Result<Box<dyn File>, FilesystemError> {
|
|
1682
|
+
self.0.open(&path.to_lowercase(), options)
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
1686
|
+
self.0.metadata(&path.to_lowercase())
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
|
|
1690
|
+
Ok(path.to_lowercase())
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
1694
|
+
self.0.read_dir(&path.to_lowercase())
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1698
|
+
self.0.create_dir(&path.to_lowercase())
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1702
|
+
self.0.remove_dir(&path.to_lowercase())
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
1706
|
+
self.0.remove_file(&path.to_lowercase())
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
1710
|
+
self.0
|
|
1711
|
+
.rename(&from.to_lowercase(), &to.to_lowercase(), rename)
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// @behavior FS-083
|
|
1716
|
+
#[test]
|
|
1717
|
+
fn an_overlay_spells_a_path_as_the_layer_showing_it_stores_it() {
|
|
1718
|
+
let overlay = Overlay::new()
|
|
1719
|
+
.layer(Arc::new(Memory::new()), Access::ReadWrite)
|
|
1720
|
+
.layer(
|
|
1721
|
+
Arc::new(FoldsCase([("private/secret", "x")].into_iter().collect())),
|
|
1722
|
+
Access::Read,
|
|
1723
|
+
);
|
|
1724
|
+
assert_eq!(
|
|
1725
|
+
(
|
|
1726
|
+
overlay.stored_path("PRIVATE/SECRET"),
|
|
1727
|
+
overlay.stored_path("PRIVATE/New")
|
|
1728
|
+
),
|
|
1729
|
+
(
|
|
1730
|
+
Ok("private/secret".to_string()),
|
|
1731
|
+
Ok("private/New".to_string())
|
|
1732
|
+
),
|
|
1733
|
+
"a path asked of Overlay::stored_path in another case, shown by a Layer folding case, \
|
|
1734
|
+
must come back as that Layer stores each name, a new name kept as given"
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// A whiteout the Layer cannot read would otherwise count as absent and
|
|
1739
|
+
// let the lower file show.
|
|
1740
|
+
// @behavior FS-079
|
|
1741
|
+
#[test]
|
|
1742
|
+
fn a_layer_that_cannot_read_its_markers_fails_the_lookup() {
|
|
1743
|
+
let top: Memory = [(".wh.file", "")].into_iter().collect();
|
|
1744
|
+
let overlay = Overlay::new()
|
|
1745
|
+
.layer(Arc::new(ReadsNoMarker(top)), Access::Read)
|
|
1746
|
+
.layer(memory_with("file", "hidden"), Access::Read);
|
|
1747
|
+
assert!(
|
|
1748
|
+
matches!(overlay.metadata("file"), Err(FilesystemError::Other(_))),
|
|
1749
|
+
"a file looked up through Overlay::metadata, beneath a Layer that cannot read the \
|
|
1750
|
+
whiteout over it, must fail the lookup rather than show the file"
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// @behavior FS-080
|
|
1755
|
+
#[test]
|
|
1756
|
+
fn a_file_gone_during_a_rename_answers_not_found() {
|
|
1757
|
+
let lower = Vanishes {
|
|
1758
|
+
memory: [("file", "lower")].into_iter().collect(),
|
|
1759
|
+
seen: AtomicBool::new(false),
|
|
1760
|
+
};
|
|
1761
|
+
let overlay = Overlay::new()
|
|
1762
|
+
.layer(Arc::new(Memory::new()), Access::ReadWrite)
|
|
1763
|
+
.layer(Arc::new(lower), Access::Read);
|
|
1764
|
+
assert_eq!(
|
|
1765
|
+
overlay.rename("file", "moved", Rename::Any),
|
|
1766
|
+
Err(FilesystemError::NotFound),
|
|
1767
|
+
"a file renamed through Overlay::rename that its Layer loses during the call must \
|
|
1768
|
+
answer not found"
|
|
1769
|
+
);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// The upper Layer's `dir/sub` stands where the lower Layer holds a
|
|
1773
|
+
// file `dir`, so the merge below it meets a file on the way.
|
|
1774
|
+
// @behavior FS-078
|
|
1775
|
+
#[test]
|
|
1776
|
+
fn a_directory_over_a_lower_file_on_its_way_reads_as_a_directory() {
|
|
1777
|
+
let overlay = Overlay::new()
|
|
1778
|
+
.layer(memory_with("dir/sub/file", "upper"), Access::ReadWrite)
|
|
1779
|
+
.layer(memory_with("dir", "lower"), Access::Read);
|
|
1780
|
+
assert_eq!(
|
|
1781
|
+
(
|
|
1782
|
+
overlay.metadata("dir/sub").map(|metadata| metadata.file_type),
|
|
1783
|
+
read(&overlay, "dir/sub/file")
|
|
1784
|
+
),
|
|
1785
|
+
(Ok(FileType::Directory), b"upper".to_vec()),
|
|
1786
|
+
"a directory looked up through Overlay::metadata above a lower Layer holding a file on \
|
|
1787
|
+
its path must read as a directory, as its files open"
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
// The writable Layer refuses each change only after the Overlay has
|
|
1792
|
+
// cleared the whiteout from `dir`, so only putting it back keeps the
|
|
1793
|
+
// read-only Layer's `dir/file` hidden.
|
|
1794
|
+
// @behavior FS-063
|
|
1795
|
+
#[test]
|
|
1796
|
+
fn a_refused_change_leaves_what_a_whiteout_hid_hidden() {
|
|
1797
|
+
let (overlay, _) = overlay_on(Arc::new(HoldsDirectories(Memory::new())));
|
|
1798
|
+
overlay
|
|
1799
|
+
.remove_file("dir/file")
|
|
1800
|
+
.expect("the file must be removed");
|
|
1801
|
+
overlay
|
|
1802
|
+
.create_dir("new")
|
|
1803
|
+
.expect("the directory must be made");
|
|
1804
|
+
let refused = (
|
|
1805
|
+
overlay.rename("new", "dir", Rename::Directory),
|
|
1806
|
+
overlay.remove_dir("dir"),
|
|
1807
|
+
);
|
|
1808
|
+
assert_eq!(
|
|
1809
|
+
(refused, overlay.metadata("dir/file").err()),
|
|
1810
|
+
(
|
|
1811
|
+
(
|
|
1812
|
+
Err(FilesystemError::PermissionDenied),
|
|
1813
|
+
Err(FilesystemError::PermissionDenied)
|
|
1814
|
+
),
|
|
1815
|
+
Some(FilesystemError::NotFound)
|
|
1816
|
+
),
|
|
1817
|
+
"a rename onto and a removal of a directory whose writable copy holds only a whiteout, \
|
|
1818
|
+
refused by the Layer through an Overlay, must leave the whited-out file hidden"
|
|
1819
|
+
);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
fn trickles(refuses_close: bool) -> Arc<Trickles> {
|
|
1823
|
+
Arc::new(Trickles {
|
|
1824
|
+
memory: Memory::new(),
|
|
1825
|
+
refuses_close,
|
|
1826
|
+
})
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
// @behavior FS-054
|
|
1830
|
+
#[test]
|
|
1831
|
+
fn a_copy_up_through_a_layer_writing_in_parts_copies_the_whole_file() {
|
|
1832
|
+
let upper = trickles(false);
|
|
1833
|
+
let (overlay, _) = overlay_on(upper.clone());
|
|
1834
|
+
overlay
|
|
1835
|
+
.open(
|
|
1836
|
+
"dir/file",
|
|
1837
|
+
&OpenOptions {
|
|
1838
|
+
write: true,
|
|
1839
|
+
..OpenOptions::default()
|
|
1840
|
+
},
|
|
1841
|
+
)
|
|
1842
|
+
.expect("the open must copy the lower file up");
|
|
1843
|
+
assert_eq!(
|
|
1844
|
+
read(&upper.memory, "dir/file"),
|
|
1845
|
+
b"lower".to_vec(),
|
|
1846
|
+
"a write open through Overlay::open must copy the whole lower file up into a Layer \
|
|
1847
|
+
whose writes land a few bytes at a time"
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// @behavior FS-055
|
|
1852
|
+
#[test]
|
|
1853
|
+
fn a_copy_up_whose_close_a_layer_refuses_leaves_nothing_behind() {
|
|
1854
|
+
let upper = trickles(true);
|
|
1855
|
+
let (overlay, _) = overlay_on(upper.clone());
|
|
1856
|
+
assert_eq!(
|
|
1857
|
+
(
|
|
1858
|
+
write(&overlay, "dir/file", b"x"),
|
|
1859
|
+
upper.metadata("dir/file"),
|
|
1860
|
+
read(&overlay, "dir/file")
|
|
1861
|
+
),
|
|
1862
|
+
(
|
|
1863
|
+
Err(FilesystemError::PermissionDenied),
|
|
1864
|
+
Err(FilesystemError::NotFound),
|
|
1865
|
+
b"lower".to_vec()
|
|
1866
|
+
),
|
|
1867
|
+
"a write through Overlay::open whose copy up the writable Layer refuses to close must \
|
|
1868
|
+
leave no copy hiding the lower file"
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/// Counts each wait its Overlay's lock makes through it.
|
|
1873
|
+
struct CountsWaits(AtomicUsize);
|
|
1874
|
+
|
|
1875
|
+
impl Blocking for CountsWaits {
|
|
1876
|
+
fn wait(&self, wait: &mut dyn FnMut()) {
|
|
1877
|
+
self.0.fetch_add(1, Ordering::SeqCst);
|
|
1878
|
+
wait();
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// @behavior FS-039
|
|
1883
|
+
#[test]
|
|
1884
|
+
fn a_contended_overlay_waits_through_its_blocking() {
|
|
1885
|
+
let counts = Arc::new(CountsWaits(AtomicUsize::new(0)));
|
|
1886
|
+
let overlay = Overlay::new()
|
|
1887
|
+
.layer(memory_with("note", "x"), Access::Read)
|
|
1888
|
+
.with_blocking(counts.clone());
|
|
1889
|
+
let held = overlay.lock.hold().expect("the lock must be free");
|
|
1890
|
+
let other = overlay.clone();
|
|
1891
|
+
let waiter = thread::spawn(move || other.metadata("note").map(|m| m.file_type));
|
|
1892
|
+
while counts.0.load(Ordering::SeqCst) == 0 {
|
|
1893
|
+
thread::sleep(Duration::from_millis(1));
|
|
1894
|
+
}
|
|
1895
|
+
drop(held);
|
|
1896
|
+
assert_eq!(
|
|
1897
|
+
(
|
|
1898
|
+
waiter.join().expect("the waiter must not panic"),
|
|
1899
|
+
counts.0.load(Ordering::SeqCst)
|
|
1900
|
+
),
|
|
1901
|
+
(Ok(FileType::File), 1),
|
|
1902
|
+
"a call through an Overlay whose lock another thread holds must wait through the \
|
|
1903
|
+
Blocking given by Overlay::with_blocking, then answer"
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
// Copying up makes the directory in the writable Layer, whose own
|
|
1908
|
+
// identity differs, so only reporting the lower Layer's keeps it.
|
|
1909
|
+
// @behavior FS-072
|
|
1910
|
+
#[test]
|
|
1911
|
+
fn an_overlay_keeps_a_directorys_identity_when_a_write_copies_it_up() {
|
|
1912
|
+
let lower: Arc<dyn Filesystem> =
|
|
1913
|
+
Arc::new([("dir/a.txt", "a")].into_iter().collect::<Memory>());
|
|
1914
|
+
let overlay = Overlay::new()
|
|
1915
|
+
.layer(Arc::new(Memory::new()), Access::ReadWrite)
|
|
1916
|
+
.layer(lower, Access::Read);
|
|
1917
|
+
let identity = |overlay: &Overlay| {
|
|
1918
|
+
let metadata = overlay.metadata("dir").expect("the directory must stand");
|
|
1919
|
+
(metadata.dev, metadata.ino)
|
|
1920
|
+
};
|
|
1921
|
+
let before = identity(&overlay);
|
|
1922
|
+
overwrite(&overlay, "dir/b.txt", b"b");
|
|
1923
|
+
assert_eq!(
|
|
1924
|
+
(identity(&overlay) == before, before.1 != 0),
|
|
1925
|
+
(true, true),
|
|
1926
|
+
"a directory of a read-only Layer copied up by a write through an Overlay's open must \
|
|
1927
|
+
keep the identity Overlay::metadata reported before"
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// @behavior FS-073
|
|
1932
|
+
#[test]
|
|
1933
|
+
fn an_overlay_names_a_directory_alike_whether_a_link_at_its_end_is_followed() {
|
|
1934
|
+
let layer = |name: &str| -> Arc<dyn Filesystem> {
|
|
1935
|
+
Arc::new(
|
|
1936
|
+
[(format!("dir/{name}"), "x")]
|
|
1937
|
+
.into_iter()
|
|
1938
|
+
.collect::<Memory>(),
|
|
1939
|
+
)
|
|
1940
|
+
};
|
|
1941
|
+
let overlay = Overlay::new()
|
|
1942
|
+
.layer(layer("upper"), Access::ReadWrite)
|
|
1943
|
+
.layer(layer("lower"), Access::Read);
|
|
1944
|
+
let identity = |metadata: Result<Metadata, FilesystemError>| {
|
|
1945
|
+
let metadata = metadata.expect("the directory must stand");
|
|
1946
|
+
(metadata.dev, metadata.ino)
|
|
1947
|
+
};
|
|
1948
|
+
assert_eq!(
|
|
1949
|
+
identity(overlay.symlink_metadata("dir")),
|
|
1950
|
+
identity(overlay.metadata("dir")),
|
|
1951
|
+
"a directory two Layers hold, looked up through Overlay::symlink_metadata, must \
|
|
1952
|
+
report the identity Overlay::metadata does"
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
}
|