kobako 0.28.0 → 0.30.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 +377 -0
- data/Cargo.lock +181 -154
- data/README.md +99 -459
- data/ROADMAP.md +7 -1
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +354 -0
- data/crates/kobako-runtime/Cargo.toml +38 -7
- data/crates/kobako-runtime/README.md +17 -8
- data/crates/kobako-runtime/src/dispatch.rs +9 -1
- data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
- data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
- data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
- data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
- data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
- data/crates/kobako-runtime/src/endpoint.rs +166 -0
- data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
- data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
- data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
- data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
- data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
- data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
- data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
- data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
- data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
- data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
- data/crates/kobako-runtime/src/filesystem.rs +26 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +16 -1
- data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
- data/crates/kobako-runtime/src/locking.rs +218 -0
- data/crates/kobako-runtime/src/mount.rs +105 -0
- data/crates/kobako-runtime/src/profile.rs +140 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
- data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
- data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
- data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
- data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
- data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
- data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
- data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
- data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
- data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
- data/crates/kobako-runtime/src/vfs/path.rs +64 -0
- data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
- data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
- data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
- data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
- data/crates/kobako-runtime/src/vfs.rs +60 -0
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
- data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
- data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
- data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
- data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
- data/crates/kobako-transport/CHANGELOG.md +23 -0
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
- data/crates/kobako-transport/src/envelope/call.rs +5 -5
- data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
- data/crates/kobako-transport/src/envelope/fault.rs +4 -3
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
- data/crates/kobako-transport/src/envelope/reply.rs +34 -16
- data/crates/kobako-transport/src/envelope/request.rs +19 -9
- data/crates/kobako-transport/src/envelope/run.rs +1 -1
- data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
- data/crates/kobako-transport/src/envelope.rs +2 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
- data/crates/kobako-wasmtime/Cargo.toml +25 -19
- data/crates/kobako-wasmtime/README.md +13 -7
- data/crates/kobako-wasmtime/src/abi.rs +75 -49
- data/crates/kobako-wasmtime/src/ambient.rs +64 -6
- data/crates/kobako-wasmtime/src/cache.rs +188 -77
- data/crates/kobako-wasmtime/src/config.rs +77 -5
- data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
- data/crates/kobako-wasmtime/src/driver.rs +70 -135
- data/crates/kobako-wasmtime/src/exports.rs +5 -16
- data/crates/kobako-wasmtime/src/guest.rs +38 -0
- data/crates/kobako-wasmtime/src/imports.rs +52 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
- data/crates/kobako-wasmtime/src/invocation.rs +43 -74
- data/crates/kobako-wasmtime/src/lib.rs +5 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
- data/crates/kobako-wasmtime/src/preview1.rs +512 -0
- data/crates/kobako-wasmtime/src/trap.rs +45 -25
- data/crates/kobako-wasmtime/src/wasi.rs +333 -0
- data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
- data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
- data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
- data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -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/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +67 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
- data/ext/kobako/src/runtime/filesystem.rs +208 -0
- data/ext/kobako/src/runtime/gvl.rs +49 -8
- 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 +125 -199
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +19 -0
- data/lib/kobako/catalog/services.rb +13 -2
- data/lib/kobako/catalog/snippets.rb +11 -15
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +10 -9
- data/lib/kobako/codec/ext_types.rb +4 -6
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +40 -13
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +69 -0
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +6 -6
- data/lib/kobako/sandbox.rb +55 -27
- data/lib/kobako/sandbox_options.rb +39 -13
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
- data/lib/kobako/snippet/source.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 +33 -29
- data/lib/kobako/transport/exposure.rb +7 -9
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +9 -31
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +35 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +24 -0
- data/sig/kobako/catalog/services.rbs +5 -1
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/decoder.rbs +1 -1
- data/sig/kobako/codec/handle_walk.rbs +1 -1
- data/sig/kobako/codec/nesting.rbs +2 -2
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +3 -2
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +93 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +3 -3
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +25 -3
- 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 +4 -4
- data/sig/kobako/transport/exposure.rbs +2 -2
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +3 -3
- metadata +116 -15
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
- data/crates/kobako-wasmtime/src/io.rs +0 -286
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -0,0 +1,2665 @@
|
|
|
1
|
+
//! `Bind`: a Filesystem over a host directory, from which nothing
|
|
2
|
+
//! outside it is reachable.
|
|
3
|
+
//!
|
|
4
|
+
//! The runtime hands a Bind one name at a time and follows every link
|
|
5
|
+
//! itself, so the VFS keeps the guest inside. Each name still resolves
|
|
6
|
+
//! through `cap_std::fs::Dir`, or the host's kernel where it confines a
|
|
7
|
+
//! path itself, which refuses a `..` or a link leading out of the
|
|
8
|
+
//! directory whatever a caller hands over, and answers alike on every
|
|
9
|
+
//! host, Windows among them.
|
|
10
|
+
|
|
11
|
+
mod fid;
|
|
12
|
+
mod lookup;
|
|
13
|
+
mod walk;
|
|
14
|
+
|
|
15
|
+
use std::any::Any;
|
|
16
|
+
use std::io::{self, Read, Seek, SeekFrom, Write};
|
|
17
|
+
use std::path::Path;
|
|
18
|
+
use std::sync::{Arc, Mutex, PoisonError, Weak};
|
|
19
|
+
|
|
20
|
+
use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt, SystemTimeSpec};
|
|
21
|
+
use cap_std::ambient_authority;
|
|
22
|
+
use cap_std::fs::{Dir, OpenOptions as CapOpenOptions};
|
|
23
|
+
|
|
24
|
+
use super::rdir::Rdir;
|
|
25
|
+
use super::{
|
|
26
|
+
DentryOperations, DirEntry, FileOperations, FileSystemType, FileType, Filesystem,
|
|
27
|
+
FilesystemError, Finish, Iattr, InodeOperations, Kiocb, Kstat, Lookup, OpenHow, RenameFlag,
|
|
28
|
+
SetTime,
|
|
29
|
+
};
|
|
30
|
+
use crate::vfs::{
|
|
31
|
+
generic_file_llseek, generic_read_dir, Dentry, DirContext, Identity, Inode, Whence,
|
|
32
|
+
};
|
|
33
|
+
use fid::{FidCache, OpenDirFid};
|
|
34
|
+
|
|
35
|
+
/// A host directory and everything beneath it. What a guest writes lands
|
|
36
|
+
/// on the host and lasts as long as the directory does.
|
|
37
|
+
///
|
|
38
|
+
/// Every entry beneath it answers through a Bind too, finding what a call is
|
|
39
|
+
/// about through the dentry it is handed, as Linux's 9p does. A directory is
|
|
40
|
+
/// opened on the host when first asked, as 9p walks to a dentry's fid only
|
|
41
|
+
/// when a call needs one. One a guest holds open holds a host descriptor; the
|
|
42
|
+
/// others a walk asked about hold one only up to a bound.
|
|
43
|
+
#[derive(Clone)]
|
|
44
|
+
pub struct Bind {
|
|
45
|
+
/// The host directory the Bind was made at.
|
|
46
|
+
root: Arc<Dir>,
|
|
47
|
+
fids: Arc<FidCache>,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
impl Bind {
|
|
51
|
+
/// Open the host directory at `path`, the one place this Filesystem
|
|
52
|
+
/// reaches outside itself. One the host lets no one read is refused, so its
|
|
53
|
+
/// root answers an open to read as any directory beneath it does, on every
|
|
54
|
+
/// host alike.
|
|
55
|
+
pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
|
|
56
|
+
let root = Dir::open_ambient_dir(path, ambient_authority())?;
|
|
57
|
+
may_read_dir(&root).map_err(|_| io::Error::from(io::ErrorKind::PermissionDenied))?;
|
|
58
|
+
Ok(Self {
|
|
59
|
+
root: Arc::new(root),
|
|
60
|
+
fids: Arc::default(),
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// The host directory kept for `dentry`, if one is.
|
|
65
|
+
fn find_fid(&self, dentry: &Dentry) -> Option<Arc<Dir>> {
|
|
66
|
+
if dentry.is_root() {
|
|
67
|
+
return Some(self.root.clone());
|
|
68
|
+
}
|
|
69
|
+
OpenDirFid::get(dentry).or_else(|| self.fids.get(dentry))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// The host directory `dentry` names, as 9p's `v9fs_fid_lookup` finds a
|
|
73
|
+
/// fid: the one kept for it, or the names beneath the nearest directory
|
|
74
|
+
/// kept, read from the dentries as `build_path_from_dentry` reads them.
|
|
75
|
+
/// 9p holds the rename lock through a walk; one here compares what it found
|
|
76
|
+
/// with what the dentry recorded instead, as NFS compares a file handle.
|
|
77
|
+
fn fid_lookup(&self, dentry: &Dentry) -> Result<Arc<Dir>, FilesystemError> {
|
|
78
|
+
if let Some(held) = self.find_fid(dentry) {
|
|
79
|
+
return Ok(held);
|
|
80
|
+
}
|
|
81
|
+
let mut names = vec![dentry.name()];
|
|
82
|
+
let mut at = dentry.parent();
|
|
83
|
+
let above = loop {
|
|
84
|
+
if let Some(held) = self.find_fid(&at) {
|
|
85
|
+
break held;
|
|
86
|
+
}
|
|
87
|
+
names.push(at.name());
|
|
88
|
+
at = at.parent();
|
|
89
|
+
};
|
|
90
|
+
names.reverse();
|
|
91
|
+
let recorded = dentry.inode().and_then(|inode| inode.identity());
|
|
92
|
+
let stands = |dir: &Dir| {
|
|
93
|
+
recorded.is_none()
|
|
94
|
+
|| dir
|
|
95
|
+
.dir_metadata()
|
|
96
|
+
.is_ok_and(|found| Identity::of(&to_kstat(found)) == recorded)
|
|
97
|
+
};
|
|
98
|
+
let dir = walk::walk(&above, &names)?;
|
|
99
|
+
if !stands(&dir) {
|
|
100
|
+
return Err(FilesystemError::NotFound);
|
|
101
|
+
}
|
|
102
|
+
let dir = Arc::new(dir);
|
|
103
|
+
self.fids.put(dentry, &dir);
|
|
104
|
+
Ok(dir)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Answer `call` with the directory above `dentry` and the name it
|
|
108
|
+
/// carries as the host spells it.
|
|
109
|
+
fn at<T>(
|
|
110
|
+
&self,
|
|
111
|
+
dentry: &Dentry,
|
|
112
|
+
call: impl FnOnce(&Dir, &str) -> Result<T, FilesystemError>,
|
|
113
|
+
) -> Result<T, FilesystemError> {
|
|
114
|
+
let (parent, name) = dentry.parent_and_name();
|
|
115
|
+
call(&*self.fid_lookup(&parent)?, to_host_name(&name)?)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Open the file `dentry` names and keep its descriptor on the inode, so
|
|
119
|
+
/// the entry answers from it however its name changes, as 9p's
|
|
120
|
+
/// `v9fs_open_fid_add` keeps an open fid. What this made is recorded
|
|
121
|
+
/// first, as `->atomic_open` calls `d_instantiate`.
|
|
122
|
+
fn open_file(
|
|
123
|
+
&self,
|
|
124
|
+
dentry: &Dentry,
|
|
125
|
+
how: &OpenHow,
|
|
126
|
+
create: bool,
|
|
127
|
+
) -> Result<Box<dyn FileOperations>, FilesystemError> {
|
|
128
|
+
let file = self.at(dentry, |dir, name| open_in(dir, name, how, create))?;
|
|
129
|
+
let opened = to_kstat(cap_std::fs::Metadata::from_file(&file).map_err(to_error)?);
|
|
130
|
+
match dentry.inode().map(|inode| inode.identity()) {
|
|
131
|
+
None => dentry.add(Some(Inode::new(
|
|
132
|
+
&opened,
|
|
133
|
+
Arc::new(BindRegular(self.clone())),
|
|
134
|
+
))),
|
|
135
|
+
// A host path may by now hold another entry than the one judged,
|
|
136
|
+
// which only what opened can tell, as NFS's open compares the file
|
|
137
|
+
// handle it gets back.
|
|
138
|
+
Some(Some(identity)) if Identity::of(&opened) != Some(identity) => {
|
|
139
|
+
return Err(FilesystemError::NotFound)
|
|
140
|
+
}
|
|
141
|
+
Some(_) => {}
|
|
142
|
+
}
|
|
143
|
+
let file = Arc::new(file);
|
|
144
|
+
let inode = dentry.inode().ok_or(FilesystemError::NotFound)?;
|
|
145
|
+
let kept = inode
|
|
146
|
+
.private()
|
|
147
|
+
.get_or_init(|| Arc::new(OpenFids::default()))
|
|
148
|
+
.clone();
|
|
149
|
+
if let Ok(fids) = kept.downcast::<OpenFids>() {
|
|
150
|
+
let mut fids = fids.0.lock().unwrap_or_else(PoisonError::into_inner);
|
|
151
|
+
fids.retain(|(fid, _)| fid.strong_count() > 0);
|
|
152
|
+
fids.push((Arc::downgrade(&file), how.write || how.truncate));
|
|
153
|
+
}
|
|
154
|
+
Ok(Box::new(BindFile { file }))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// Whether the entry `dentry` recorded still stands at its name in the
|
|
158
|
+
/// directory above, as a walk asks each name once the one above it stood.
|
|
159
|
+
/// A host may reuse an inode number for what it made in its place, so the
|
|
160
|
+
/// kind must match too, as NFS compares a file handle's attributes.
|
|
161
|
+
fn stands_as_recorded(&self, dentry: &Dentry) -> Result<bool, FilesystemError> {
|
|
162
|
+
let inode = dentry.inode();
|
|
163
|
+
if inode
|
|
164
|
+
.as_ref()
|
|
165
|
+
.is_some_and(|inode| inode.identity().is_none())
|
|
166
|
+
{
|
|
167
|
+
return Ok(true);
|
|
168
|
+
}
|
|
169
|
+
let found = self.at(dentry, |dir, name| {
|
|
170
|
+
dir.symlink_metadata(name).map_err(to_error)
|
|
171
|
+
});
|
|
172
|
+
match (found, inode) {
|
|
173
|
+
(Ok(found), Some(inode)) => {
|
|
174
|
+
let found = to_kstat(found);
|
|
175
|
+
Ok(Identity::of(&found) == inode.identity() && found.file_type == inode.kind())
|
|
176
|
+
}
|
|
177
|
+
(Ok(_), None) => Ok(false),
|
|
178
|
+
(Err(FilesystemError::NotFound), None) => Ok(true),
|
|
179
|
+
(
|
|
180
|
+
Err(
|
|
181
|
+
FilesystemError::NotFound
|
|
182
|
+
| FilesystemError::NotADirectory
|
|
183
|
+
| FilesystemError::FilesystemLoop,
|
|
184
|
+
),
|
|
185
|
+
_,
|
|
186
|
+
) => Ok(false),
|
|
187
|
+
(Err(err), _) => Err(err),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// The names `dir` holds, each with the kind and the identity a listing gives.
|
|
193
|
+
fn entries_of(dir: &Dir) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
194
|
+
#[cfg(unix)]
|
|
195
|
+
let dev = {
|
|
196
|
+
use cap_std::fs::MetadataExt;
|
|
197
|
+
dir.dir_metadata().map_err(to_error)?.dev()
|
|
198
|
+
};
|
|
199
|
+
dir.entries()
|
|
200
|
+
.map_err(to_error)?
|
|
201
|
+
.map(|entry| {
|
|
202
|
+
let entry = entry.map_err(to_error)?;
|
|
203
|
+
let name = entry
|
|
204
|
+
.file_name()
|
|
205
|
+
.into_string()
|
|
206
|
+
.map_err(|_| FilesystemError::IllegalSequence)?;
|
|
207
|
+
let listed = DirEntry::new(name, to_file_type(entry.file_type().map_err(to_error)?));
|
|
208
|
+
#[cfg(unix)]
|
|
209
|
+
let listed = {
|
|
210
|
+
use std::os::unix::fs::DirEntryExt;
|
|
211
|
+
listed.with_identity(dev, entry.ino())
|
|
212
|
+
};
|
|
213
|
+
Ok(listed)
|
|
214
|
+
})
|
|
215
|
+
.collect()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// The directory `name` in `dir`, a link there refused as a loop, as
|
|
219
|
+
/// Linux's `RESOLVE_NO_SYMLINKS` refuses one, where the host names it no
|
|
220
|
+
/// directory.
|
|
221
|
+
fn open_dir_nofollow(dir: &Dir, name: &str) -> Result<Dir, FilesystemError> {
|
|
222
|
+
dir.open_dir_nofollow(name)
|
|
223
|
+
.map_err(|err| nofollow_error(dir, name, err))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Why an open of `name` in `dir` that follows no link failed, a link there
|
|
227
|
+
/// named a loop, as Linux's `O_NOFOLLOW` answers `ELOOP`, where the host
|
|
228
|
+
/// names it otherwise.
|
|
229
|
+
fn nofollow_error(dir: &Dir, name: &str, err: io::Error) -> FilesystemError {
|
|
230
|
+
match dir.symlink_metadata(name) {
|
|
231
|
+
Ok(metadata) if metadata.file_type().is_symlink() => FilesystemError::FilesystemLoop,
|
|
232
|
+
_ => to_error(err),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// `name` as `cap_std` takes it. Windows reads some names as other names or as
|
|
237
|
+
/// devices, so there one is refused rather than reached as an entry a Mount's
|
|
238
|
+
/// Policy never saw.
|
|
239
|
+
fn to_host_name(name: &str) -> Result<&str, FilesystemError> {
|
|
240
|
+
if cfg!(windows) && is_windows_alias(name) {
|
|
241
|
+
return Err(FilesystemError::InvalidInput);
|
|
242
|
+
}
|
|
243
|
+
Ok(name)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// Whether Windows reads `name` as something else: a `\` as a separator, a `:`
|
|
247
|
+
/// as a stream of a file, a trailing `.` or space as nothing, a wildcard as a
|
|
248
|
+
/// pattern, or a device's name, whatever extension follows, as the device.
|
|
249
|
+
fn is_windows_alias(name: &str) -> bool {
|
|
250
|
+
const DEVICES: [&str; 33] = [
|
|
251
|
+
"CON", "PRN", "AUX", "NUL", "CONIN$", "CONOUT$", "CLOCK$", "COM0", "COM1", "COM2", "COM3",
|
|
252
|
+
"COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "COM¹", "COM²", "COM³", "LPT0", "LPT1",
|
|
253
|
+
"LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "LPT¹", "LPT²", "LPT³",
|
|
254
|
+
];
|
|
255
|
+
let reserved = |c: char| c.is_ascii_control() || "\\:*?\"<>|".contains(c);
|
|
256
|
+
let stem = name.split('.').next().unwrap_or(name).trim_end();
|
|
257
|
+
name.contains(reserved)
|
|
258
|
+
|| name.ends_with(['.', ' '])
|
|
259
|
+
|| DEVICES.contains(&stem.to_uppercase().as_str())
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// The FilesystemError a host failure names. cap-std's own refusal of a path
|
|
263
|
+
/// leading out of the directory carries no OS code. A Bind hands it one name and
|
|
264
|
+
/// no `..`, so that refusal only says a move carried the name out of the
|
|
265
|
+
/// directory as it was opened, as `complete_walk` answers EXDEV, and nothing
|
|
266
|
+
/// stands at it now. A link loop stays unnamed until std's `FilesystemLoop` is
|
|
267
|
+
/// stable.
|
|
268
|
+
fn to_error(err: io::Error) -> FilesystemError {
|
|
269
|
+
match err.kind() {
|
|
270
|
+
io::ErrorKind::PermissionDenied if err.raw_os_error().is_none() => {
|
|
271
|
+
FilesystemError::NotFound
|
|
272
|
+
}
|
|
273
|
+
io::ErrorKind::NotFound => FilesystemError::NotFound,
|
|
274
|
+
io::ErrorKind::AlreadyExists => FilesystemError::AlreadyExists,
|
|
275
|
+
io::ErrorKind::NotADirectory => FilesystemError::NotADirectory,
|
|
276
|
+
io::ErrorKind::IsADirectory => FilesystemError::IsADirectory,
|
|
277
|
+
io::ErrorKind::DirectoryNotEmpty => FilesystemError::DirectoryNotEmpty,
|
|
278
|
+
io::ErrorKind::PermissionDenied => FilesystemError::PermissionDenied,
|
|
279
|
+
io::ErrorKind::ReadOnlyFilesystem => FilesystemError::ReadOnlyFilesystem,
|
|
280
|
+
io::ErrorKind::InvalidInput => FilesystemError::InvalidInput,
|
|
281
|
+
io::ErrorKind::Unsupported => FilesystemError::Unsupported,
|
|
282
|
+
io::ErrorKind::StorageFull => FilesystemError::StorageFull,
|
|
283
|
+
io::ErrorKind::FileTooLarge => FilesystemError::FileTooLarge,
|
|
284
|
+
io::ErrorKind::CrossesDevices => FilesystemError::CrossesDevices,
|
|
285
|
+
io::ErrorKind::Deadlock => FilesystemError::Deadlock,
|
|
286
|
+
io::ErrorKind::ResourceBusy => FilesystemError::ResourceBusy,
|
|
287
|
+
io::ErrorKind::InvalidFilename => FilesystemError::InvalidFilename,
|
|
288
|
+
io::ErrorKind::NotSeekable => FilesystemError::NotSeekable,
|
|
289
|
+
io::ErrorKind::Interrupted => FilesystemError::Interrupted,
|
|
290
|
+
io::ErrorKind::WouldBlock => FilesystemError::WouldBlock,
|
|
291
|
+
io::ErrorKind::OutOfMemory => FilesystemError::OutOfMemory,
|
|
292
|
+
_ => FilesystemError::Other(err.to_string()),
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
fn to_file_type(host: cap_std::fs::FileType) -> FileType {
|
|
297
|
+
if host.is_symlink() {
|
|
298
|
+
FileType::Symlink
|
|
299
|
+
} else if host.is_dir() {
|
|
300
|
+
FileType::Dir
|
|
301
|
+
} else if host.is_file() {
|
|
302
|
+
FileType::RegFile
|
|
303
|
+
} else {
|
|
304
|
+
to_special_file_type(host)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// What a host entry neither a file, a directory, nor a link is, as `st_mode`
|
|
309
|
+
/// names it.
|
|
310
|
+
#[cfg(unix)]
|
|
311
|
+
fn to_special_file_type(host: cap_std::fs::FileType) -> FileType {
|
|
312
|
+
use cap_std::fs::FileTypeExt;
|
|
313
|
+
if host.is_fifo() {
|
|
314
|
+
FileType::Fifo
|
|
315
|
+
} else if host.is_socket() {
|
|
316
|
+
FileType::Sock
|
|
317
|
+
} else if host.is_block_device() {
|
|
318
|
+
FileType::Blkdev
|
|
319
|
+
} else if host.is_char_device() {
|
|
320
|
+
FileType::Chrdev
|
|
321
|
+
} else {
|
|
322
|
+
FileType::Unknown
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/// Elsewhere a Bind names no kind.
|
|
327
|
+
#[cfg(not(unix))]
|
|
328
|
+
fn to_special_file_type(_host: cap_std::fs::FileType) -> FileType {
|
|
329
|
+
FileType::Unknown
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// What a host entry is, with the size the host reports. The host names
|
|
333
|
+
/// its identity, link count, and times on unix; elsewhere a Bind reports
|
|
334
|
+
/// none.
|
|
335
|
+
fn to_kstat(metadata: cap_std::fs::Metadata) -> Kstat {
|
|
336
|
+
let file_type = to_file_type(metadata.file_type());
|
|
337
|
+
let size = metadata.len();
|
|
338
|
+
#[cfg(unix)]
|
|
339
|
+
{
|
|
340
|
+
use cap_std::fs::MetadataExt;
|
|
341
|
+
Kstat::new(file_type, size)
|
|
342
|
+
.with_identity(metadata.dev(), metadata.ino())
|
|
343
|
+
.with_nlink(metadata.nlink())
|
|
344
|
+
.with_times(
|
|
345
|
+
unix_time(metadata.atime(), metadata.atime_nsec()),
|
|
346
|
+
unix_time(metadata.mtime(), metadata.mtime_nsec()),
|
|
347
|
+
unix_time(metadata.ctime(), metadata.ctime_nsec()),
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
#[cfg(not(unix))]
|
|
351
|
+
Kstat::new(file_type, size)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/// The time `seconds` and `nanoseconds` after the Unix epoch, as `stat`
|
|
355
|
+
/// reports one; `None` before the epoch, which no guest's clock reads.
|
|
356
|
+
#[cfg(unix)]
|
|
357
|
+
fn unix_time(seconds: i64, nanoseconds: i64) -> Option<std::time::SystemTime> {
|
|
358
|
+
let seconds = u64::try_from(seconds).ok()?;
|
|
359
|
+
let nanoseconds = u32::try_from(nanoseconds).ok()?;
|
|
360
|
+
std::time::UNIX_EPOCH.checked_add(std::time::Duration::new(seconds, nanoseconds))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// `time` as the host takes it, so a time of now is the host's own.
|
|
364
|
+
fn to_host_time(time: Option<SetTime>) -> Option<SystemTimeSpec> {
|
|
365
|
+
time.map(|time| match time {
|
|
366
|
+
SetTime::Now => SystemTimeSpec::SymbolicNow,
|
|
367
|
+
SetTime::At(at) => SystemTimeSpec::Absolute(cap_std::time::SystemTime::from_std(at)),
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/// The descriptors open on one file, and which were opened to write, as 9p
|
|
372
|
+
/// keeps open fids on `i_private`.
|
|
373
|
+
#[derive(Default)]
|
|
374
|
+
struct OpenFids(Mutex<Vec<(Weak<std::fs::File>, bool)>>);
|
|
375
|
+
|
|
376
|
+
/// A descriptor open on the file `dentry` records, one open to write where
|
|
377
|
+
/// `writable` asks, as 9p's `v9fs_fid_find_inode` finds one.
|
|
378
|
+
fn v9fs_fid_find_inode(dentry: &Dentry, writable: bool) -> Option<Arc<std::fs::File>> {
|
|
379
|
+
let inode = dentry.inode()?;
|
|
380
|
+
let fids = inode.private().get()?.clone().downcast::<OpenFids>().ok()?;
|
|
381
|
+
let fids = fids.0.lock().unwrap_or_else(PoisonError::into_inner);
|
|
382
|
+
fids.iter()
|
|
383
|
+
.filter(|(_, opened_to_write)| *opened_to_write || !writable)
|
|
384
|
+
.find_map(|(fid, _)| fid.upgrade())
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// Open `name` in `dir` as `how` asks, made first where `create` says,
|
|
388
|
+
/// refusing one that already stands.
|
|
389
|
+
fn open_in(
|
|
390
|
+
dir: &Dir,
|
|
391
|
+
name: &str,
|
|
392
|
+
how: &OpenHow,
|
|
393
|
+
create: bool,
|
|
394
|
+
) -> Result<std::fs::File, FilesystemError> {
|
|
395
|
+
let writes = how.write || how.truncate || create;
|
|
396
|
+
let mut open = CapOpenOptions::new();
|
|
397
|
+
// The host refuses one that creates or truncates without writing; the
|
|
398
|
+
// runtime decides what is written, so such opens write here.
|
|
399
|
+
open.read(how.read).write(writes).create_new(create);
|
|
400
|
+
// Only a file or a directory opens: a FIFO would wait for a peer
|
|
401
|
+
// and a device would act on being opened. The host could swap an
|
|
402
|
+
// entry in between, so what opened is checked again before its
|
|
403
|
+
// first use, the open itself never waiting for a peer, nor following
|
|
404
|
+
// a link swapped in, since the runtime follows a link itself.
|
|
405
|
+
open.follow(FollowSymlinks::No);
|
|
406
|
+
#[cfg(unix)]
|
|
407
|
+
{
|
|
408
|
+
use cap_std::fs::OpenOptionsExt;
|
|
409
|
+
use rustix::fs::OFlags;
|
|
410
|
+
open.custom_flags((OFlags::NONBLOCK | OFlags::NOCTTY).bits() as i32);
|
|
411
|
+
}
|
|
412
|
+
let file = dir
|
|
413
|
+
.open_with(name, &open)
|
|
414
|
+
.map_err(|err| nofollow_error(dir, name, err))?;
|
|
415
|
+
match to_file_type(file.metadata().map_err(to_error)?.file_type()) {
|
|
416
|
+
FileType::RegFile => {}
|
|
417
|
+
FileType::Dir => return Err(FilesystemError::IsADirectory),
|
|
418
|
+
_ => return Err(FilesystemError::PermissionDenied),
|
|
419
|
+
}
|
|
420
|
+
#[cfg(unix)]
|
|
421
|
+
{
|
|
422
|
+
use rustix::fs::{fcntl_getfl, fcntl_setfl, OFlags};
|
|
423
|
+
let flags = fcntl_getfl(&file)
|
|
424
|
+
.map_err(io::Error::from)
|
|
425
|
+
.map_err(to_error)?;
|
|
426
|
+
fcntl_setfl(&file, flags - OFlags::NONBLOCK)
|
|
427
|
+
.map_err(io::Error::from)
|
|
428
|
+
.map_err(to_error)?;
|
|
429
|
+
}
|
|
430
|
+
Ok(file.into_std())
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/// The host may move or remake what a dentry names behind the runtime's
|
|
434
|
+
/// back, so a dentry recording an identity is valid only while the same
|
|
435
|
+
/// entry stands at the path it stands for, as NFS compares the handle a
|
|
436
|
+
/// fresh lookup finds. One recording none has nothing to compare, so nothing
|
|
437
|
+
/// keeps it once its holder lets go.
|
|
438
|
+
impl FileSystemType for Bind {
|
|
439
|
+
/// Its root reports the host directory it holds, as 9p's `fill_super` asks of
|
|
440
|
+
/// the attached fid, and a host that cannot answer makes no instance.
|
|
441
|
+
fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
|
|
442
|
+
sb.set_default_d_op(self.clone());
|
|
443
|
+
let root = to_kstat(self.root.dir_metadata().map_err(to_error)?);
|
|
444
|
+
sb.d_make_root(Inode::new(&root, self));
|
|
445
|
+
Ok(())
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
impl DentryOperations for Bind {
|
|
450
|
+
fn revalidate(
|
|
451
|
+
&self,
|
|
452
|
+
_dir: &dyn InodeOperations,
|
|
453
|
+
dentry: &Dentry,
|
|
454
|
+
) -> Result<bool, FilesystemError> {
|
|
455
|
+
self.stands_as_recorded(dentry)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
fn delete(&self, dentry: &Dentry) -> bool {
|
|
459
|
+
dentry
|
|
460
|
+
.inode()
|
|
461
|
+
.is_some_and(|inode| inode.identity().is_none())
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/// A file of a Bind, or an entry a host holds that is none of the others, as
|
|
466
|
+
/// `v9fs_file_inode_operations_dotl` answers for one.
|
|
467
|
+
struct BindRegular(Bind);
|
|
468
|
+
|
|
469
|
+
/// A link of a Bind, as `v9fs_symlink_inode_operations_dotl` answers for one.
|
|
470
|
+
struct BindLink(Bind);
|
|
471
|
+
|
|
472
|
+
/// What an entry standing at its name in the directory above reports.
|
|
473
|
+
fn stat_at(bind: &Bind, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
474
|
+
bind.at(dentry, |dir, name| {
|
|
475
|
+
Ok(to_kstat(dir.symlink_metadata(name).map_err(to_error)?))
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
impl InodeOperations for BindRegular {
|
|
480
|
+
/// A file open anywhere answers from the descriptor that holds it, even
|
|
481
|
+
/// once its name is gone.
|
|
482
|
+
fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
483
|
+
match v9fs_fid_find_inode(dentry, false) {
|
|
484
|
+
Some(file) => Ok(to_kstat(
|
|
485
|
+
cap_std::fs::Metadata::from_file(&file).map_err(to_error)?,
|
|
486
|
+
)),
|
|
487
|
+
None => stat_at(&self.0, dentry),
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
fn setattr(&self, dentry: &Dentry, attr: &Iattr) -> Result<(), FilesystemError> {
|
|
492
|
+
if let Some(len) = attr.size {
|
|
493
|
+
// A truncate made through an open takes that open's descriptor, as
|
|
494
|
+
// 9p takes `ia_file`'s fid; one made by name, one open to write
|
|
495
|
+
// elsewhere.
|
|
496
|
+
let through = attr
|
|
497
|
+
.file
|
|
498
|
+
.and_then(|file| (file as &dyn Any).downcast_ref::<BindFile>())
|
|
499
|
+
.map(|opened| opened.file.clone());
|
|
500
|
+
return match through.or_else(|| v9fs_fid_find_inode(dentry, true)) {
|
|
501
|
+
Some(file) => file.set_len(len).map_err(to_error),
|
|
502
|
+
None => Err(FilesystemError::InvalidInput),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
// A file open anywhere takes them through what holds it, whatever stands
|
|
506
|
+
// at its name by now.
|
|
507
|
+
if let Some(file) = v9fs_fid_find_inode(dentry, false) {
|
|
508
|
+
let mut times = std::fs::FileTimes::new();
|
|
509
|
+
if let Some(atime) = attr.atime {
|
|
510
|
+
times = times.set_accessed(attr.time_of(atime));
|
|
511
|
+
}
|
|
512
|
+
if let Some(mtime) = attr.mtime {
|
|
513
|
+
times = times.set_modified(attr.time_of(mtime));
|
|
514
|
+
}
|
|
515
|
+
return file.set_times(times).map_err(to_error);
|
|
516
|
+
}
|
|
517
|
+
let (atime, mtime) = (to_host_time(attr.atime), to_host_time(attr.mtime));
|
|
518
|
+
self.0.at(dentry, |dir, name| {
|
|
519
|
+
dir.set_symlink_times(name, atime, mtime).map_err(to_error)
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
fn open(
|
|
524
|
+
&self,
|
|
525
|
+
dentry: &Dentry,
|
|
526
|
+
how: &OpenHow,
|
|
527
|
+
) -> Result<Box<dyn FileOperations>, FilesystemError> {
|
|
528
|
+
self.0.open_file(dentry, how, false)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
impl InodeOperations for BindLink {
|
|
533
|
+
fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
534
|
+
stat_at(&self.0, dentry)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/// Where it points as the host holds it, even outside this directory,
|
|
538
|
+
/// as `->get_link` answers, since the walk judges what a link leads to. A
|
|
539
|
+
/// target that is no valid UTF-8 answers `IllegalSequence`, as a guest path
|
|
540
|
+
/// is one.
|
|
541
|
+
fn get_link(&self, dentry: &Dentry) -> Result<String, FilesystemError> {
|
|
542
|
+
self.0.at(dentry, |dir, name| {
|
|
543
|
+
dir.read_link_contents(name)
|
|
544
|
+
.map_err(to_error)?
|
|
545
|
+
.into_os_string()
|
|
546
|
+
.into_string()
|
|
547
|
+
.map_err(|_| FilesystemError::IllegalSequence)
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
impl InodeOperations for Bind {
|
|
553
|
+
/// A directory answers from its own descriptor, which follows it wherever
|
|
554
|
+
/// it moves and keeps its inode taken while it is open.
|
|
555
|
+
fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
556
|
+
Ok(to_kstat(
|
|
557
|
+
self.fid_lookup(dentry)?.dir_metadata().map_err(to_error)?,
|
|
558
|
+
))
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/// A directory takes the times through the descriptor that holds it, as it
|
|
562
|
+
/// answers for itself.
|
|
563
|
+
fn setattr(&self, dentry: &Dentry, attr: &Iattr) -> Result<(), FilesystemError> {
|
|
564
|
+
let (atime, mtime) = (to_host_time(attr.atime), to_host_time(attr.mtime));
|
|
565
|
+
self.fid_lookup(dentry)?
|
|
566
|
+
.set_times(".", atime, mtime)
|
|
567
|
+
.map_err(to_error)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/// What stands at the name `dentry` carries, spelled as the host spells
|
|
571
|
+
/// it, as a lookup hands `d_splice_alias` the inode it found, so
|
|
572
|
+
/// `revalidate` can tell a dentry the host replaced behind the
|
|
573
|
+
/// runtime's back. That costs one host stat a name.
|
|
574
|
+
fn lookup(&self, dentry: &Dentry) -> Result<Lookup, FilesystemError> {
|
|
575
|
+
self.at(dentry, |dir, name| {
|
|
576
|
+
let stored_name = lookup::lookup(dir, name)?;
|
|
577
|
+
match dir.symlink_metadata(&stored_name).map_err(to_error) {
|
|
578
|
+
Ok(found) => {
|
|
579
|
+
let found = to_kstat(found);
|
|
580
|
+
let ops: Arc<dyn InodeOperations> = match found.file_type {
|
|
581
|
+
FileType::Dir => Arc::new(self.clone()),
|
|
582
|
+
FileType::Symlink => Arc::new(BindLink(self.clone())),
|
|
583
|
+
_ => Arc::new(BindRegular(self.clone())),
|
|
584
|
+
};
|
|
585
|
+
let inode = Inode::new(&found, ops);
|
|
586
|
+
match stored_name == name {
|
|
587
|
+
true => Ok(Lookup::Found(inode)),
|
|
588
|
+
false => Ok(Lookup::Spelled {
|
|
589
|
+
name: stored_name,
|
|
590
|
+
inode,
|
|
591
|
+
}),
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
Err(FilesystemError::NotFound) => Ok(Lookup::Absent),
|
|
595
|
+
Err(err) => Err(err),
|
|
596
|
+
}
|
|
597
|
+
})
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
fn open(
|
|
601
|
+
&self,
|
|
602
|
+
dentry: &Dentry,
|
|
603
|
+
how: &OpenHow,
|
|
604
|
+
) -> Result<Box<dyn FileOperations>, FilesystemError> {
|
|
605
|
+
let fid = self.fid_lookup(dentry)?;
|
|
606
|
+
if how.read {
|
|
607
|
+
may_read_dir(&fid)?;
|
|
608
|
+
}
|
|
609
|
+
OpenDirFid::set(dentry, &fid);
|
|
610
|
+
Ok(Box::new(BindDir(fid, Rdir::default())))
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/// A name still in lookup is looked up first, and any name is left to the
|
|
614
|
+
/// VFS unless the open may make it and nothing stands there, as 9p's
|
|
615
|
+
/// `v9fs_vfs_atomic_open_dotl` asks its server before it creates.
|
|
616
|
+
fn atomic_open(&self, dentry: &Dentry, how: &OpenHow) -> Result<Finish, FilesystemError> {
|
|
617
|
+
if dentry.in_lookup() {
|
|
618
|
+
let found = self.lookup(dentry)?;
|
|
619
|
+
if !how.create || !matches!(found, Lookup::Absent) {
|
|
620
|
+
return Ok(Finish::Lookup(found));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if !how.create {
|
|
624
|
+
return Ok(Finish::NoOpen { created: false });
|
|
625
|
+
}
|
|
626
|
+
let file = self.open_file(dentry, how, true)?;
|
|
627
|
+
Ok(Finish::Open {
|
|
628
|
+
file,
|
|
629
|
+
created: true,
|
|
630
|
+
})
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
fn mkdir(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
634
|
+
self.at(dentry, |dir, name| dir.create_dir(name).map_err(to_error))
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
fn rmdir(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
638
|
+
self.at(dentry, |dir, name| dir.remove_dir(name).map_err(to_error))
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
fn unlink(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
642
|
+
self.at(dentry, |dir, name| dir.remove_file(name).map_err(to_error))
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/// A host directory takes no lock the runtime's check of the source
|
|
646
|
+
/// holds, so another process of the host's changing it in between is
|
|
647
|
+
/// the host's to rule out. A link moves as itself, as `rename(2)`
|
|
648
|
+
/// moves one.
|
|
649
|
+
fn rename(
|
|
650
|
+
&self,
|
|
651
|
+
from: &Dentry,
|
|
652
|
+
to_dir: &dyn InodeOperations,
|
|
653
|
+
to: &Dentry,
|
|
654
|
+
flag: Option<RenameFlag>,
|
|
655
|
+
) -> Result<(), FilesystemError> {
|
|
656
|
+
if flag.is_some() {
|
|
657
|
+
return Err(FilesystemError::InvalidInput);
|
|
658
|
+
}
|
|
659
|
+
let target = (to_dir as &dyn Any)
|
|
660
|
+
.downcast_ref::<Self>()
|
|
661
|
+
.ok_or(FilesystemError::CrossesDevices)?;
|
|
662
|
+
self.at(from, |from_dir, from_name| {
|
|
663
|
+
target.at(to, |to_dir, to_name| {
|
|
664
|
+
from_dir
|
|
665
|
+
.rename(from_name, to_dir, to_name)
|
|
666
|
+
.map_err(to_error)
|
|
667
|
+
})
|
|
668
|
+
})
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/// A host directory, open on the descriptor it holds, as 9p's `v9fs_dir_readdir`
|
|
673
|
+
/// reads the fid in `private_data`, so it follows the directory wherever it
|
|
674
|
+
/// moves.
|
|
675
|
+
struct BindDir(Arc<Dir>, Rdir);
|
|
676
|
+
|
|
677
|
+
impl FileOperations for BindDir {
|
|
678
|
+
fn llseek(
|
|
679
|
+
&mut self,
|
|
680
|
+
dentry: &Dentry,
|
|
681
|
+
pos: u64,
|
|
682
|
+
offset: i64,
|
|
683
|
+
whence: Whence,
|
|
684
|
+
) -> Result<u64, FilesystemError> {
|
|
685
|
+
generic_file_llseek(dentry, pos, offset, whence)
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
fn read_iter(&mut self, _iocb: &mut Kiocb, _buf: &mut [u8]) -> Result<usize, FilesystemError> {
|
|
689
|
+
generic_read_dir()
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/// What a listing began with stays on the open directory until it ends, as
|
|
693
|
+
/// 9p's `fid->rdir` holds what the server sent.
|
|
694
|
+
fn iterate_shared(
|
|
695
|
+
&mut self,
|
|
696
|
+
dentry: &Dentry,
|
|
697
|
+
ctx: &mut DirContext<'_>,
|
|
698
|
+
) -> Result<(), FilesystemError> {
|
|
699
|
+
let dir = self.0.clone();
|
|
700
|
+
self.1.iterate(dentry, ctx, || entries_of(&dir))
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/// Make the host directory's names durable. Windows syncs no directory, so
|
|
704
|
+
/// there the host answers for them itself.
|
|
705
|
+
fn fsync(&mut self, _datasync: bool) -> Result<(), FilesystemError> {
|
|
706
|
+
sync_dir(&self.0)
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/// `dir` opened to read. cap-std holds a directory for its path alone on Linux,
|
|
711
|
+
/// which reads and syncs nothing, so the host judges reading it only here.
|
|
712
|
+
#[cfg(unix)]
|
|
713
|
+
fn open_to_read(dir: &Dir) -> Result<rustix::fd::OwnedFd, FilesystemError> {
|
|
714
|
+
use rustix::fs::{openat, Mode, OFlags};
|
|
715
|
+
|
|
716
|
+
let flags = OFlags::RDONLY | OFlags::DIRECTORY | OFlags::CLOEXEC;
|
|
717
|
+
openat(dir, ".", flags, Mode::empty()).map_err(|err| to_error(io::Error::from(err)))
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
#[cfg(unix)]
|
|
721
|
+
fn sync_dir(dir: &Dir) -> Result<(), FilesystemError> {
|
|
722
|
+
rustix::fs::fsync(open_to_read(dir)?).map_err(|err| to_error(io::Error::from(err)))
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
#[cfg(not(unix))]
|
|
726
|
+
fn sync_dir(_dir: &Dir) -> Result<(), FilesystemError> {
|
|
727
|
+
Ok(())
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/// Refuse to open `dir` to read unless the host lets it be read, as `may_open`
|
|
731
|
+
/// asks `inode_permission`. A file holds it for what it was opened for, so a
|
|
732
|
+
/// directory reached only for its path answers no such question.
|
|
733
|
+
#[cfg(unix)]
|
|
734
|
+
fn may_read_dir(dir: &Dir) -> Result<(), FilesystemError> {
|
|
735
|
+
open_to_read(dir).map(drop)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
#[cfg(not(unix))]
|
|
739
|
+
fn may_read_dir(_dir: &Dir) -> Result<(), FilesystemError> {
|
|
740
|
+
Ok(())
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/// An open host file. The runtime names every offset, so each access seeks
|
|
744
|
+
/// first.
|
|
745
|
+
struct BindFile {
|
|
746
|
+
file: Arc<std::fs::File>,
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
impl BindFile {
|
|
750
|
+
/// Windows has no append to switch one handle to for a single write,
|
|
751
|
+
/// so an append that could land over another writer's is refused.
|
|
752
|
+
#[cfg(not(unix))]
|
|
753
|
+
fn append(&mut self, _iocb: &mut Kiocb, _buf: &[u8]) -> Result<usize, FilesystemError> {
|
|
754
|
+
Err(FilesystemError::Unsupported)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/// The host places the write at the file's end as `O_APPEND` does, so
|
|
758
|
+
/// another writer's append lands before or after it, never over it.
|
|
759
|
+
#[cfg(unix)]
|
|
760
|
+
fn append(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
|
|
761
|
+
use rustix::fs::{fcntl_getfl, fcntl_setfl, OFlags};
|
|
762
|
+
|
|
763
|
+
let mut file = &*self.file;
|
|
764
|
+
let flags = fcntl_getfl(file)
|
|
765
|
+
.map_err(io::Error::from)
|
|
766
|
+
.map_err(to_error)?;
|
|
767
|
+
fcntl_setfl(file, flags | OFlags::APPEND)
|
|
768
|
+
.map_err(io::Error::from)
|
|
769
|
+
.map_err(to_error)?;
|
|
770
|
+
let written = file.write(buf);
|
|
771
|
+
let end = file.stream_position();
|
|
772
|
+
fcntl_setfl(file, flags)
|
|
773
|
+
.map_err(io::Error::from)
|
|
774
|
+
.map_err(to_error)?;
|
|
775
|
+
let written = written.map_err(to_error)?;
|
|
776
|
+
iocb.pos = end.map_err(to_error)?;
|
|
777
|
+
Ok(written)
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
impl FileOperations for BindFile {
|
|
782
|
+
fn llseek(
|
|
783
|
+
&mut self,
|
|
784
|
+
dentry: &Dentry,
|
|
785
|
+
pos: u64,
|
|
786
|
+
offset: i64,
|
|
787
|
+
whence: Whence,
|
|
788
|
+
) -> Result<u64, FilesystemError> {
|
|
789
|
+
generic_file_llseek(dentry, pos, offset, whence)
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
|
|
793
|
+
let mut file = &*self.file;
|
|
794
|
+
file.seek(SeekFrom::Start(iocb.pos)).map_err(to_error)?;
|
|
795
|
+
let read = file.read(buf).map_err(to_error)?;
|
|
796
|
+
iocb.pos += read as u64;
|
|
797
|
+
Ok(read)
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
|
|
801
|
+
let written = match iocb.append {
|
|
802
|
+
true => self.append(iocb, buf)?,
|
|
803
|
+
false => {
|
|
804
|
+
let mut file = &*self.file;
|
|
805
|
+
file.seek(SeekFrom::Start(iocb.pos)).map_err(to_error)?;
|
|
806
|
+
let written = file.write(buf).map_err(to_error)?;
|
|
807
|
+
iocb.pos += written as u64;
|
|
808
|
+
written
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
crate::vfs::generic_write_sync(self, iocb, written)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
fn fsync(&mut self, datasync: bool) -> Result<(), FilesystemError> {
|
|
815
|
+
match datasync {
|
|
816
|
+
true => self.file.sync_data(),
|
|
817
|
+
false => self.file.sync_all(),
|
|
818
|
+
}
|
|
819
|
+
.map_err(to_error)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/// Reserve the room on the host, as `v9fs_file_fallocate` asks its server. Only
|
|
823
|
+
/// Linux grows a file in one step without shortening one a peer just grew, so
|
|
824
|
+
/// elsewhere this refuses, as an append that cannot land in one step does.
|
|
825
|
+
#[cfg(target_os = "linux")]
|
|
826
|
+
fn fallocate(&mut self, offset: u64, len: u64) -> Result<(), FilesystemError> {
|
|
827
|
+
let flags = rustix::fs::FallocateFlags::empty();
|
|
828
|
+
rustix::fs::fallocate(&*self.file, flags, offset, len)
|
|
829
|
+
.map_err(|err| to_error(io::Error::from(err)))
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
#[cfg(test)]
|
|
834
|
+
mod tests {
|
|
835
|
+
use std::fs;
|
|
836
|
+
use std::path::PathBuf;
|
|
837
|
+
|
|
838
|
+
use super::*;
|
|
839
|
+
use crate::vfs::kern_path::{kern_path, KernPath};
|
|
840
|
+
|
|
841
|
+
/// A Bind reports times only where the host names them.
|
|
842
|
+
#[cfg(unix)]
|
|
843
|
+
mod set_times {
|
|
844
|
+
use super::*;
|
|
845
|
+
use crate::filesystem::{Overlay, Tmpfs};
|
|
846
|
+
use crate::mount::{Access, Mount};
|
|
847
|
+
use crate::vfs::kern_path::filp_open;
|
|
848
|
+
use crate::vfs::{do_utimes_fd, do_utimes_path, vfs_fstatat, OpenHow, Path as VfsPath};
|
|
849
|
+
|
|
850
|
+
/// One second past the Unix epoch, which no clock is at.
|
|
851
|
+
fn moment() -> std::time::SystemTime {
|
|
852
|
+
std::time::UNIX_EPOCH + std::time::Duration::from_secs(1)
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
fn set_modified() -> Iattr<'static> {
|
|
856
|
+
Iattr::times(None, Some(SetTime::At(moment())))
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/// The root of `filesystem` mounted to change.
|
|
860
|
+
fn root_of(filesystem: Arc<dyn FileSystemType>) -> VfsPath {
|
|
861
|
+
let mount = Mount::new(
|
|
862
|
+
"/data",
|
|
863
|
+
Filesystem::new(filesystem).expect("the instance must stand"),
|
|
864
|
+
Access::ReadWrite,
|
|
865
|
+
)
|
|
866
|
+
.expect("/data is a guest path");
|
|
867
|
+
VfsPath::root(mount.mnt())
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/// An Overlay of `lower` beneath an empty upper.
|
|
871
|
+
fn overlay_of(lower: Bind) -> Arc<Overlay> {
|
|
872
|
+
Arc::new(
|
|
873
|
+
Overlay::new(
|
|
874
|
+
vec![Filesystem::new(Arc::new(lower)).expect("the instance must stand")],
|
|
875
|
+
Some(Filesystem::new(Arc::new(Tmpfs::new())).expect("the instance must stand")),
|
|
876
|
+
)
|
|
877
|
+
.unwrap_or_else(|_| panic!("the Overlay must stack")),
|
|
878
|
+
)
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/// What a stat of `path` beneath `root` reports as its modified time, once
|
|
882
|
+
/// it was set.
|
|
883
|
+
fn set_and_read_modified(root: &VfsPath, path: &str) -> Option<std::time::SystemTime> {
|
|
884
|
+
do_utimes_path(&Arc::default(), root, path, false, &set_modified())
|
|
885
|
+
.expect("the time must be set");
|
|
886
|
+
vfs_fstatat(&Arc::default(), root, path, false)
|
|
887
|
+
.ok()
|
|
888
|
+
.and_then(|stat| stat.mtime)
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/// When the host last changed what `path` holds.
|
|
892
|
+
fn host_modified(path: &std::path::Path) -> Option<std::time::SystemTime> {
|
|
893
|
+
fs::metadata(path).and_then(|found| found.modified()).ok()
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/// A host directory holding `dir/file.txt`.
|
|
897
|
+
fn host_with_a_file(name: &str) -> HostDir {
|
|
898
|
+
let host = HostDir::new(name);
|
|
899
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
900
|
+
fs::write(host.0.join("dir/file.txt"), "x").expect("the host file must be written");
|
|
901
|
+
host
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
fn bind_of(host: &HostDir) -> Bind {
|
|
905
|
+
Bind::new(&host.0).expect("the host directory must open")
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// @behavior FS-185
|
|
909
|
+
#[test]
|
|
910
|
+
fn a_built_in_filesystem_keeps_the_times_a_caller_sets() {
|
|
911
|
+
let host = host_with_a_file("set-times");
|
|
912
|
+
let tmpfs: Tmpfs = [("dir/file.txt", "x")].into_iter().collect();
|
|
913
|
+
let roots = [
|
|
914
|
+
root_of(Arc::new(tmpfs)),
|
|
915
|
+
root_of(Arc::new(bind_of(&host))),
|
|
916
|
+
root_of(overlay_of(bind_of(&host))),
|
|
917
|
+
];
|
|
918
|
+
let answers = roots.map(|root| {
|
|
919
|
+
(
|
|
920
|
+
set_and_read_modified(&root, "dir/file.txt"),
|
|
921
|
+
set_and_read_modified(&root, "dir"),
|
|
922
|
+
)
|
|
923
|
+
});
|
|
924
|
+
let set = Some(moment());
|
|
925
|
+
assert_eq!(
|
|
926
|
+
(answers, host_modified(&host.0.join("dir/file.txt"))),
|
|
927
|
+
([(set, set); 3], set),
|
|
928
|
+
"vfs::do_utimes_pathon a file and a directory of a Tmpfs, a Bind, and an Overlay must \
|
|
929
|
+
keep the time, a Bind's on the host"
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// @behavior FS-186
|
|
934
|
+
#[test]
|
|
935
|
+
fn a_directory_time_set_through_an_overlay_leaves_its_lower_layer() {
|
|
936
|
+
let host = host_with_a_file("overlay-dir-time");
|
|
937
|
+
let before = host_modified(&host.0.join("dir"));
|
|
938
|
+
let seen = set_and_read_modified(&root_of(overlay_of(bind_of(&host))), "dir");
|
|
939
|
+
assert_eq!(
|
|
940
|
+
(seen, host_modified(&host.0.join("dir")) == before),
|
|
941
|
+
(Some(moment()), true),
|
|
942
|
+
"vfs::do_utimes_pathon a directory through an Overlay must show the time and leave the \
|
|
943
|
+
lower Layer's directory as it was"
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// A guest sees nothing of a link but that it is one, so its times are no more
|
|
948
|
+
// its to set than where it points is to read.
|
|
949
|
+
// @behavior FS-187
|
|
950
|
+
#[test]
|
|
951
|
+
fn a_link_keeps_its_own_times() {
|
|
952
|
+
let host = host_with_a_file("link-time");
|
|
953
|
+
std::os::unix::fs::symlink("dir", host.0.join("link")).expect("the link must be made");
|
|
954
|
+
assert_eq!(
|
|
955
|
+
do_utimes_path(
|
|
956
|
+
&Arc::default(),
|
|
957
|
+
&root_of(Arc::new(bind_of(&host))),
|
|
958
|
+
"link",
|
|
959
|
+
false,
|
|
960
|
+
&set_modified(),
|
|
961
|
+
),
|
|
962
|
+
Err(FilesystemError::Unsupported),
|
|
963
|
+
"vfs::do_utimes_pathfollowing no link of a Bind's link must answer ENOTSUP"
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Another file takes the name, so a time reaching it by name would be seen.
|
|
968
|
+
// @behavior FS-188
|
|
969
|
+
#[test]
|
|
970
|
+
fn a_time_set_through_a_descriptor_reaches_the_file_it_holds() {
|
|
971
|
+
let host = host_with_a_file("held-time");
|
|
972
|
+
let root = root_of(Arc::new(bind_of(&host)));
|
|
973
|
+
let how = OpenHow {
|
|
974
|
+
read: true,
|
|
975
|
+
..OpenHow::default()
|
|
976
|
+
};
|
|
977
|
+
let held = filp_open(&Arc::default(), &root, "dir/file.txt", &how)
|
|
978
|
+
.unwrap_or_else(|_| panic!("the file must open"));
|
|
979
|
+
let file = host.0.join("dir/file.txt");
|
|
980
|
+
fs::remove_file(&file).expect("the host removes the file");
|
|
981
|
+
fs::write(&file, "y").expect("the host makes another");
|
|
982
|
+
let other_before = host_modified(&file);
|
|
983
|
+
let answer = do_utimes_fd(&Arc::default(), held.path(), &set_modified());
|
|
984
|
+
let held_time = held.getattr().ok().and_then(|stat| stat.mtime);
|
|
985
|
+
assert_eq!(
|
|
986
|
+
(answer, held_time, host_modified(&file) == other_before),
|
|
987
|
+
(Ok(()), Some(moment()), true),
|
|
988
|
+
"vfs::do_utimes_fd through a Bind's descriptor must set the file it holds, and leave \
|
|
989
|
+
what took its name"
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/// The host directory at `path` as a Bind, reached through the
|
|
995
|
+
/// VFS.
|
|
996
|
+
fn bind_host(path: impl AsRef<Path>) -> KernPath {
|
|
997
|
+
kern_path(Arc::new(
|
|
998
|
+
Bind::new(path).expect("the host directory must open"),
|
|
999
|
+
))
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
/// A fresh host directory of the test's own, removed when it drops.
|
|
1003
|
+
struct HostDir(PathBuf);
|
|
1004
|
+
|
|
1005
|
+
impl HostDir {
|
|
1006
|
+
fn new(name: &str) -> Self {
|
|
1007
|
+
let path = std::env::temp_dir()
|
|
1008
|
+
.join(format!("kobako-directory-{name}-{}", std::process::id()));
|
|
1009
|
+
let _ = fs::remove_dir_all(&path);
|
|
1010
|
+
fs::create_dir_all(&path).expect("the host directory must be created");
|
|
1011
|
+
Self(path)
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
impl Drop for HostDir {
|
|
1016
|
+
fn drop(&mut self) {
|
|
1017
|
+
let _ = fs::remove_dir_all(&self.0);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
/// A Bind over `depth` nested directories each named `d`, with the file
|
|
1022
|
+
/// `f` at the bottom, and the path to that file.
|
|
1023
|
+
#[cfg(unix)]
|
|
1024
|
+
fn nested_tree(host: &HostDir, depth: usize) -> (Arc<Bind>, String) {
|
|
1025
|
+
let path = vec!["d"; depth].join("/");
|
|
1026
|
+
fs::create_dir_all(host.0.join(&path)).expect("the host tree must be made");
|
|
1027
|
+
fs::write(host.0.join(&path).join("f"), "x").expect("the host file must be written");
|
|
1028
|
+
let bind = Arc::new(Bind::new(&host.0).expect("the host directory must open"));
|
|
1029
|
+
(bind, format!("{path}/f"))
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Counted on the Bind, as a count of the process's descriptors would see
|
|
1033
|
+
// every other test's. Past the bound the walk still lands, so nothing a
|
|
1034
|
+
// guest reaches depends on what was kept.
|
|
1035
|
+
// @behavior FS-151
|
|
1036
|
+
#[cfg(unix)]
|
|
1037
|
+
#[test]
|
|
1038
|
+
fn a_walk_keeps_a_bounded_number_of_host_directories_open() {
|
|
1039
|
+
use crate::vfs::{self, OpenHow, Path as VfsPath};
|
|
1040
|
+
let host = HostDir::new("bounded-fids");
|
|
1041
|
+
let (bind, file) = nested_tree(&host, 200);
|
|
1042
|
+
let mount = crate::mount::Mount::new(
|
|
1043
|
+
"/data",
|
|
1044
|
+
crate::filesystem::Filesystem::new(bind.clone()).expect("the instance must stand"),
|
|
1045
|
+
crate::mount::Access::Read,
|
|
1046
|
+
)
|
|
1047
|
+
.expect("/data is a guest path");
|
|
1048
|
+
let root = VfsPath::root(mount.mnt());
|
|
1049
|
+
let read = OpenHow {
|
|
1050
|
+
read: true,
|
|
1051
|
+
..OpenHow::default()
|
|
1052
|
+
};
|
|
1053
|
+
let opened = vfs::kern_path::filp_open(&Arc::default(), &root, &file, &read);
|
|
1054
|
+
assert!(
|
|
1055
|
+
opened.is_ok() && bind.fids.len() <= 64,
|
|
1056
|
+
"vfs::do_file_open 200 directories beneath a Bind must land and keep at most 64 of \
|
|
1057
|
+
them open, keeping {}",
|
|
1058
|
+
bind.fids.len()
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// @behavior FS-196
|
|
1063
|
+
#[test]
|
|
1064
|
+
fn a_directory_of_each_built_in_filesystem_answers_its_read_as_a_directory() {
|
|
1065
|
+
use crate::vfs::File;
|
|
1066
|
+
let host = HostDir::new("reads-directory");
|
|
1067
|
+
let read = |fs: Arc<dyn FileSystemType>| {
|
|
1068
|
+
let mount = crate::mount::Mount::new(
|
|
1069
|
+
"/data",
|
|
1070
|
+
Filesystem::new(fs).expect("the instance must stand"),
|
|
1071
|
+
crate::mount::Access::Read,
|
|
1072
|
+
)
|
|
1073
|
+
.expect("/data is a guest path");
|
|
1074
|
+
let mut dir = File::root(mount.mnt(), &Arc::default()).expect("the root opens");
|
|
1075
|
+
(dir.read(&mut [0; 1]).err(), dir.pread(&mut [0; 1], 0).err())
|
|
1076
|
+
};
|
|
1077
|
+
let overlay = crate::filesystem::Overlay::new(
|
|
1078
|
+
vec![
|
|
1079
|
+
Filesystem::new(Arc::new(crate::filesystem::Tmpfs::new()))
|
|
1080
|
+
.expect("the instance must stand"),
|
|
1081
|
+
Filesystem::new(Arc::new(crate::filesystem::Tmpfs::new()))
|
|
1082
|
+
.expect("the instance must stand"),
|
|
1083
|
+
],
|
|
1084
|
+
None,
|
|
1085
|
+
)
|
|
1086
|
+
.expect("the Overlay must stack");
|
|
1087
|
+
let refused = Some(FilesystemError::IsADirectory);
|
|
1088
|
+
for answer in [
|
|
1089
|
+
read(Arc::new(crate::filesystem::Tmpfs::new())),
|
|
1090
|
+
read(Arc::new(
|
|
1091
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1092
|
+
)),
|
|
1093
|
+
read(Arc::new(overlay)),
|
|
1094
|
+
] {
|
|
1095
|
+
assert_eq!(
|
|
1096
|
+
answer,
|
|
1097
|
+
(refused.clone(), refused.clone()),
|
|
1098
|
+
"File::read and pread of a directory of each built-in Filesystem must answer \
|
|
1099
|
+
EISDIR, as generic_read_dir does"
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// Nothing holds `x` between the two stats, so what answers the second is the
|
|
1105
|
+
// negative dentry the first left, asked whether it still holds.
|
|
1106
|
+
// @behavior FS-193
|
|
1107
|
+
#[test]
|
|
1108
|
+
fn a_name_kept_as_absent_shows_what_the_host_made_there() {
|
|
1109
|
+
use crate::vfs::{self, Path as VfsPath};
|
|
1110
|
+
let host = HostDir::new("kept-absent");
|
|
1111
|
+
let mount = crate::mount::Mount::new(
|
|
1112
|
+
"/data",
|
|
1113
|
+
crate::filesystem::Filesystem::new(Arc::new(
|
|
1114
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1115
|
+
))
|
|
1116
|
+
.expect("the instance must stand"),
|
|
1117
|
+
crate::mount::Access::ReadWrite,
|
|
1118
|
+
)
|
|
1119
|
+
.expect("/data is a guest path");
|
|
1120
|
+
let root = VfsPath::root(mount.mnt());
|
|
1121
|
+
let cred = Arc::default();
|
|
1122
|
+
let stat = || vfs::vfs_fstatat(&cred, &root, "x", false).err();
|
|
1123
|
+
let before = stat();
|
|
1124
|
+
fs::write(host.0.join("x"), "x").expect("the host file must be written");
|
|
1125
|
+
assert_eq!(
|
|
1126
|
+
(before, stat()),
|
|
1127
|
+
(Some(FilesystemError::NotFound), None),
|
|
1128
|
+
"vfs::vfs_fstatat of a name kept as absent, once the host made a file there, must \
|
|
1129
|
+
find the file"
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
// The Bind records nothing it makes, so what stands at the name is known only
|
|
1134
|
+
// to a lookup, which the absent dentry the walk kept must not answer for.
|
|
1135
|
+
// @behavior FS-194
|
|
1136
|
+
#[test]
|
|
1137
|
+
fn a_directory_made_where_a_name_was_kept_as_absent_is_found() {
|
|
1138
|
+
use crate::vfs::{self, Path as VfsPath};
|
|
1139
|
+
let host = HostDir::new("made-at-absent");
|
|
1140
|
+
let mount = crate::mount::Mount::new(
|
|
1141
|
+
"/data",
|
|
1142
|
+
crate::filesystem::Filesystem::new(Arc::new(
|
|
1143
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1144
|
+
))
|
|
1145
|
+
.expect("the instance must stand"),
|
|
1146
|
+
crate::mount::Access::ReadWrite,
|
|
1147
|
+
)
|
|
1148
|
+
.expect("/data is a guest path");
|
|
1149
|
+
let root = VfsPath::root(mount.mnt());
|
|
1150
|
+
let cred = Arc::default();
|
|
1151
|
+
let before = vfs::vfs_fstatat(&cred, &root, "made", false).err();
|
|
1152
|
+
vfs::filename_mkdirat(&cred, &root, "made").expect("the directory must be made");
|
|
1153
|
+
let after = vfs::vfs_fstatat(&cred, &root, "made", false).map(|made| made.file_type);
|
|
1154
|
+
assert_eq!(
|
|
1155
|
+
(before, after),
|
|
1156
|
+
(Some(FilesystemError::NotFound), Ok(FileType::Dir)),
|
|
1157
|
+
"vfs::filename_mkdirat at a name kept as absent must leave the next walk finding \
|
|
1158
|
+
the directory, as d_drop leaves a dentry nothing recorded on"
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// Nothing holds `x` between the two opens, so only a walk asking whether
|
|
1163
|
+
// the name it kept still stands finds the directory gone from beneath the
|
|
1164
|
+
// Mount, rather than following what it kept out through the moved one.
|
|
1165
|
+
// @behavior FS-161
|
|
1166
|
+
#[cfg(unix)]
|
|
1167
|
+
#[test]
|
|
1168
|
+
fn a_name_kept_for_the_next_walk_reaches_nothing_the_host_moved_out() {
|
|
1169
|
+
use crate::vfs::{self, OpenHow, Path as VfsPath};
|
|
1170
|
+
let host = HostDir::new("kept-name");
|
|
1171
|
+
fs::create_dir_all(host.0.join("m/x")).expect("the host tree must be made");
|
|
1172
|
+
fs::create_dir(host.0.join("outside")).expect("the host tree must be made");
|
|
1173
|
+
fs::write(host.0.join("m/x/secret"), "s").expect("the host file must be written");
|
|
1174
|
+
let mount = crate::mount::Mount::new(
|
|
1175
|
+
"/data",
|
|
1176
|
+
crate::filesystem::Filesystem::new(Arc::new(
|
|
1177
|
+
Bind::new(host.0.join("m")).expect("the host directory must open"),
|
|
1178
|
+
))
|
|
1179
|
+
.expect("the instance must stand"),
|
|
1180
|
+
crate::mount::Access::Read,
|
|
1181
|
+
)
|
|
1182
|
+
.expect("/data is a guest path");
|
|
1183
|
+
let root = VfsPath::root(mount.mnt());
|
|
1184
|
+
let cred = crate::security::restrict_self(
|
|
1185
|
+
&crate::security::prepare_creds_within(&[], None),
|
|
1186
|
+
[&mount],
|
|
1187
|
+
);
|
|
1188
|
+
let read = OpenHow {
|
|
1189
|
+
read: true,
|
|
1190
|
+
..OpenHow::default()
|
|
1191
|
+
};
|
|
1192
|
+
let open = || vfs::kern_path::filp_open(&cred, &root, "x/secret", &read).err();
|
|
1193
|
+
let before = open();
|
|
1194
|
+
fs::rename(host.0.join("m/x"), host.0.join("outside/x")).expect("the host must move it");
|
|
1195
|
+
assert_eq!(
|
|
1196
|
+
(before, open()),
|
|
1197
|
+
(None, Some(FilesystemError::NotFound)),
|
|
1198
|
+
"vfs::do_file_open through a name kept since the last walk, once the host moved it \
|
|
1199
|
+
out of the Mount, must answer ENOENT"
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Another writer moves the name the Policy judges, then moves a refused
|
|
1204
|
+
// file onto where it went, between the judgement and the open, so only
|
|
1205
|
+
// telling what opened from what was judged keeps the open from reaching it.
|
|
1206
|
+
// @behavior FS-156
|
|
1207
|
+
#[cfg(unix)]
|
|
1208
|
+
#[test]
|
|
1209
|
+
fn an_open_reaches_only_the_entry_it_judged() {
|
|
1210
|
+
use crate::vfs::{self, OpenHow, Path as VfsPath};
|
|
1211
|
+
|
|
1212
|
+
/// Runs what it holds once, as a Policy is asked, then allows it.
|
|
1213
|
+
struct RunsWhenAsked(std::sync::Mutex<Option<Box<dyn FnOnce() + Send>>>);
|
|
1214
|
+
|
|
1215
|
+
impl crate::mount::Policy for RunsWhenAsked {
|
|
1216
|
+
fn allows(&self, _path: &str, _access: crate::mount::Access) -> bool {
|
|
1217
|
+
let run = self.0.lock().expect("never poisoned").take();
|
|
1218
|
+
if let Some(run) = run {
|
|
1219
|
+
run();
|
|
1220
|
+
}
|
|
1221
|
+
true
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
let host = HostDir::new("open-identity");
|
|
1226
|
+
for dir in ["public", "archive"] {
|
|
1227
|
+
fs::create_dir(host.0.join(dir)).expect("the host tree must be made");
|
|
1228
|
+
}
|
|
1229
|
+
fs::write(host.0.join("public/x"), "judged").expect("the host file must be written");
|
|
1230
|
+
fs::write(host.0.join("archive/secret"), "refused").expect("the host file must be written");
|
|
1231
|
+
let filesystem = crate::filesystem::Filesystem::new(Arc::new(
|
|
1232
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1233
|
+
))
|
|
1234
|
+
.expect("the instance must stand");
|
|
1235
|
+
let writer =
|
|
1236
|
+
crate::mount::Mount::new("/data", filesystem.clone(), crate::mount::Access::ReadWrite)
|
|
1237
|
+
.expect("/data is a guest path");
|
|
1238
|
+
let writer = VfsPath::root(writer.mnt());
|
|
1239
|
+
let swap = Box::new(move || {
|
|
1240
|
+
let cred = Arc::default();
|
|
1241
|
+
vfs::filename_renameat2(&cred, &writer, "public/x", &writer, "archive/x")
|
|
1242
|
+
.expect("the name must move");
|
|
1243
|
+
vfs::filename_renameat2(&cred, &writer, "archive/secret", &writer, "archive/x")
|
|
1244
|
+
.expect("the refused file must move onto it");
|
|
1245
|
+
});
|
|
1246
|
+
let mount = crate::mount::Mount::new("/data", filesystem, crate::mount::Access::Read)
|
|
1247
|
+
.expect("/data is a guest path")
|
|
1248
|
+
.with_policy(Arc::new(RunsWhenAsked(std::sync::Mutex::new(Some(swap)))));
|
|
1249
|
+
let read = OpenHow {
|
|
1250
|
+
read: true,
|
|
1251
|
+
..OpenHow::default()
|
|
1252
|
+
};
|
|
1253
|
+
let opened = vfs::kern_path::filp_open(
|
|
1254
|
+
&crate::security::restrict_self(
|
|
1255
|
+
&crate::security::prepare_creds_within(&[], None),
|
|
1256
|
+
[&mount],
|
|
1257
|
+
),
|
|
1258
|
+
&VfsPath::root(mount.mnt()),
|
|
1259
|
+
"public/x",
|
|
1260
|
+
&read,
|
|
1261
|
+
);
|
|
1262
|
+
assert_eq!(
|
|
1263
|
+
opened.err(),
|
|
1264
|
+
Some(FilesystemError::NotFound),
|
|
1265
|
+
"vfs::do_file_open of a file another writer replaced after the Policy judged it must \
|
|
1266
|
+
answer ENOENT rather than open what stands there now"
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Other walks push what the descriptor walked to out of the bound, so only
|
|
1271
|
+
// a descriptor keeping its own directory still reaches it once the host
|
|
1272
|
+
// moved it.
|
|
1273
|
+
// @behavior FS-152
|
|
1274
|
+
#[cfg(unix)]
|
|
1275
|
+
#[test]
|
|
1276
|
+
fn a_directory_held_open_keeps_its_host_directory_past_the_bound() {
|
|
1277
|
+
use crate::vfs::{self, OpenHow, Path as VfsPath};
|
|
1278
|
+
let host = HostDir::new("held-past-bound");
|
|
1279
|
+
fs::create_dir_all(host.0.join("held")).expect("the host tree must be made");
|
|
1280
|
+
fs::write(host.0.join("held/f"), "x").expect("the host file must be written");
|
|
1281
|
+
let (bind, file) = nested_tree(&host, 100);
|
|
1282
|
+
let mount = crate::mount::Mount::new(
|
|
1283
|
+
"/data",
|
|
1284
|
+
crate::filesystem::Filesystem::new(bind).expect("the instance must stand"),
|
|
1285
|
+
crate::mount::Access::Read,
|
|
1286
|
+
)
|
|
1287
|
+
.expect("/data is a guest path");
|
|
1288
|
+
let root = VfsPath::root(mount.mnt());
|
|
1289
|
+
let cred = Arc::default();
|
|
1290
|
+
let directory = OpenHow {
|
|
1291
|
+
read: true,
|
|
1292
|
+
directory: true,
|
|
1293
|
+
..OpenHow::default()
|
|
1294
|
+
};
|
|
1295
|
+
let Ok(held) = vfs::kern_path::filp_open(&cred, &root, "held", &directory) else {
|
|
1296
|
+
panic!("the directory must open");
|
|
1297
|
+
};
|
|
1298
|
+
let read = OpenHow {
|
|
1299
|
+
read: true,
|
|
1300
|
+
..OpenHow::default()
|
|
1301
|
+
};
|
|
1302
|
+
assert!(
|
|
1303
|
+
vfs::kern_path::filp_open(&cred, &root, &file, &read).is_ok(),
|
|
1304
|
+
"the deep file must open"
|
|
1305
|
+
);
|
|
1306
|
+
fs::rename(host.0.join("held"), host.0.join("moved")).expect("the host must move it");
|
|
1307
|
+
assert!(
|
|
1308
|
+
vfs::vfs_fstatat(&cred, held.path(), "f", false).is_ok(),
|
|
1309
|
+
"vfs::vfs_fstatat through a directory held open beneath a Bind, once a walk passed \
|
|
1310
|
+
the bound and the host moved it, must still reach the directory it opened"
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// The dentry the descriptor holds names the moved directory, so only a
|
|
1315
|
+
// Bind answering it stale keeps the removal of the new one from
|
|
1316
|
+
// killing it.
|
|
1317
|
+
// @behavior FS-135
|
|
1318
|
+
#[cfg(unix)]
|
|
1319
|
+
#[test]
|
|
1320
|
+
fn a_directory_remade_by_the_host_leaves_the_moved_ones_descriptor_alive() {
|
|
1321
|
+
use crate::mount::Mount;
|
|
1322
|
+
use crate::vfs::{self, OpenHow};
|
|
1323
|
+
let host = HostDir::new("remade");
|
|
1324
|
+
fs::create_dir_all(host.0.join("a")).expect("the host directory must be made");
|
|
1325
|
+
let filesystem = crate::filesystem::Filesystem::new(Arc::new(
|
|
1326
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1327
|
+
))
|
|
1328
|
+
.expect("the instance must stand");
|
|
1329
|
+
let mount = Mount::new("/data", filesystem, crate::mount::Access::ReadWrite)
|
|
1330
|
+
.expect("/data is a guest path");
|
|
1331
|
+
let root = vfs::Path::root(mount.mnt());
|
|
1332
|
+
let how = OpenHow {
|
|
1333
|
+
read: true,
|
|
1334
|
+
directory: true,
|
|
1335
|
+
..OpenHow::default()
|
|
1336
|
+
};
|
|
1337
|
+
let Ok(opened) = vfs::kern_path::filp_open(&Arc::default(), &root, "a", &how) else {
|
|
1338
|
+
panic!("the directory must open");
|
|
1339
|
+
};
|
|
1340
|
+
let held = opened.path();
|
|
1341
|
+
fs::rename(host.0.join("a"), host.0.join("moved")).expect("the host must move it");
|
|
1342
|
+
fs::create_dir(host.0.join("a")).expect("the host must remake it");
|
|
1343
|
+
vfs::filename_rmdir(&Arc::default(), &root, "a").expect("the new directory must go");
|
|
1344
|
+
assert_eq!(
|
|
1345
|
+
(
|
|
1346
|
+
vfs::filename_mkdirat(&Arc::default(),held, "made"),
|
|
1347
|
+
host.0.join("moved/made").is_dir()
|
|
1348
|
+
),
|
|
1349
|
+
(Ok(()), true),
|
|
1350
|
+
"vfs::filename_mkdirat through a Bind directory the host moved away, once vfs::filename_rmdir \
|
|
1351
|
+
removed the one remade at its path, must make the directory in the moved one"
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/// A Policy refusing everything beneath `/data/refused`.
|
|
1356
|
+
#[cfg(unix)]
|
|
1357
|
+
struct RefusesRefused;
|
|
1358
|
+
|
|
1359
|
+
#[cfg(unix)]
|
|
1360
|
+
impl crate::mount::Policy for RefusesRefused {
|
|
1361
|
+
fn allows(&self, path: &str, _access: crate::mount::Access) -> bool {
|
|
1362
|
+
!path.starts_with("/data/refused")
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// The guest's descriptor of `a/d` keeps `a`'s dentry, and the host
|
|
1367
|
+
// directory a walk opened for it, so only a dentry the Bind answers
|
|
1368
|
+
// stale once the host moved `a` keeps a later walk through `a` from
|
|
1369
|
+
// reaching the moved directory while the Policy judges `a`.
|
|
1370
|
+
// @behavior FS-136
|
|
1371
|
+
#[cfg(unix)]
|
|
1372
|
+
#[test]
|
|
1373
|
+
fn a_walk_through_a_held_directory_the_host_moved_reaches_nothing_the_policy_refuses() {
|
|
1374
|
+
use crate::mount::Mount;
|
|
1375
|
+
use crate::vfs::{self, OpenHow};
|
|
1376
|
+
let host = HostDir::new("held-ancestor");
|
|
1377
|
+
fs::create_dir_all(host.0.join("a/d")).expect("the host tree must be made");
|
|
1378
|
+
fs::create_dir_all(host.0.join("refused")).expect("the host tree must be made");
|
|
1379
|
+
fs::write(host.0.join("a/d/secret"), "s").expect("the host file must be written");
|
|
1380
|
+
let filesystem = crate::filesystem::Filesystem::new(Arc::new(
|
|
1381
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1382
|
+
))
|
|
1383
|
+
.expect("the instance must stand");
|
|
1384
|
+
let mount = Mount::new("/data", filesystem, crate::mount::Access::Read)
|
|
1385
|
+
.expect("/data is a guest path")
|
|
1386
|
+
.with_policy(Arc::new(RefusesRefused));
|
|
1387
|
+
let root = vfs::Path::root(mount.mnt());
|
|
1388
|
+
let cred = crate::security::restrict_self(
|
|
1389
|
+
&crate::security::prepare_creds_within(&[], None),
|
|
1390
|
+
[&mount],
|
|
1391
|
+
);
|
|
1392
|
+
let directory = OpenHow {
|
|
1393
|
+
read: true,
|
|
1394
|
+
directory: true,
|
|
1395
|
+
..OpenHow::default()
|
|
1396
|
+
};
|
|
1397
|
+
let Ok(_held) = vfs::kern_path::filp_open(&cred, &root, "a/d", &directory) else {
|
|
1398
|
+
panic!("a/d must open");
|
|
1399
|
+
};
|
|
1400
|
+
fs::rename(host.0.join("a"), host.0.join("refused/a")).expect("the host must move a");
|
|
1401
|
+
fs::create_dir(host.0.join("a")).expect("the host must remake a");
|
|
1402
|
+
let read = OpenHow {
|
|
1403
|
+
read: true,
|
|
1404
|
+
..OpenHow::default()
|
|
1405
|
+
};
|
|
1406
|
+
assert_eq!(
|
|
1407
|
+
vfs::kern_path::filp_open(&cred, &root, "a/d/secret", &read).err(),
|
|
1408
|
+
Some(FilesystemError::NotFound),
|
|
1409
|
+
"vfs::do_file_open beneath a Bind Mount with a Policy, through a directory the host moved \
|
|
1410
|
+
beneath a refused path while a descriptor beneath it was held, must answer NotFound"
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
/// The roots of a Tmpfs, a Bind, and an Overlay, each holding a five byte
|
|
1415
|
+
/// `f`, by name.
|
|
1416
|
+
#[cfg(unix)]
|
|
1417
|
+
fn each_holding_five_bytes(host: &HostDir) -> [(&'static str, crate::vfs::Path); 3] {
|
|
1418
|
+
use crate::filesystem::Filesystem;
|
|
1419
|
+
use crate::filesystem::{Overlay, Tmpfs};
|
|
1420
|
+
use crate::mount::{Access, Mount};
|
|
1421
|
+
fs::write(host.0.join("f"), "hello").expect("the host file must be written");
|
|
1422
|
+
let tmpfs = || [("f", "hello")].into_iter().collect::<Tmpfs>();
|
|
1423
|
+
let overlay = Overlay::new(
|
|
1424
|
+
vec![Filesystem::new(Arc::new(tmpfs())).expect("the instance must stand")],
|
|
1425
|
+
Some(Filesystem::new(Arc::new(Tmpfs::new())).expect("the instance must stand")),
|
|
1426
|
+
)
|
|
1427
|
+
.expect("the Overlay must stack");
|
|
1428
|
+
let root = |filesystem: Arc<dyn FileSystemType>| {
|
|
1429
|
+
let mount = Mount::new(
|
|
1430
|
+
"/data",
|
|
1431
|
+
Filesystem::new(filesystem).expect("the instance must stand"),
|
|
1432
|
+
Access::ReadWrite,
|
|
1433
|
+
)
|
|
1434
|
+
.expect("/data is a guest path");
|
|
1435
|
+
crate::vfs::Path::root(mount.mnt())
|
|
1436
|
+
};
|
|
1437
|
+
[
|
|
1438
|
+
("Tmpfs", root(Arc::new(tmpfs()))),
|
|
1439
|
+
(
|
|
1440
|
+
"Bind",
|
|
1441
|
+
root(Arc::new(
|
|
1442
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1443
|
+
)),
|
|
1444
|
+
),
|
|
1445
|
+
("Overlay", root(Arc::new(overlay))),
|
|
1446
|
+
]
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// @behavior FS-141
|
|
1450
|
+
#[cfg(unix)]
|
|
1451
|
+
#[test]
|
|
1452
|
+
fn a_file_removed_while_open_still_reports_itself() {
|
|
1453
|
+
use crate::vfs::{self, OpenHow};
|
|
1454
|
+
let host = HostDir::new("removed-open");
|
|
1455
|
+
let read = OpenHow {
|
|
1456
|
+
read: true,
|
|
1457
|
+
..OpenHow::default()
|
|
1458
|
+
};
|
|
1459
|
+
for (name, root) in each_holding_five_bytes(&host) {
|
|
1460
|
+
let Ok(mut file) = vfs::kern_path::filp_open(&Arc::default(), &root, "f", &read) else {
|
|
1461
|
+
panic!("{name}'s f must open");
|
|
1462
|
+
};
|
|
1463
|
+
vfs::filename_unlinkat(&Arc::default(), &root, "f").expect("the name must go");
|
|
1464
|
+
let mut buf = [0; 5];
|
|
1465
|
+
assert_eq!(
|
|
1466
|
+
(
|
|
1467
|
+
file.getattr().map(|found| found.size).ok(),
|
|
1468
|
+
file.pread(&mut buf, 0),
|
|
1469
|
+
&buf
|
|
1470
|
+
),
|
|
1471
|
+
(Some(5), Ok(5), b"hello"),
|
|
1472
|
+
"File::getattr and File::pread of a file whose name \
|
|
1473
|
+
vfs::filename_unlinkat removed on a {name} must answer what it holds"
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// @behavior FS-142
|
|
1479
|
+
#[cfg(unix)]
|
|
1480
|
+
#[test]
|
|
1481
|
+
fn a_file_removed_while_open_can_still_be_sized() {
|
|
1482
|
+
use crate::vfs::{self, OpenHow};
|
|
1483
|
+
let host = HostDir::new("removed-sized");
|
|
1484
|
+
let write = OpenHow {
|
|
1485
|
+
write: true,
|
|
1486
|
+
..OpenHow::default()
|
|
1487
|
+
};
|
|
1488
|
+
for (name, root) in each_holding_five_bytes(&host) {
|
|
1489
|
+
let Ok(mut file) = vfs::kern_path::filp_open(&Arc::default(), &root, "f", &write)
|
|
1490
|
+
else {
|
|
1491
|
+
panic!("{name}'s f must open");
|
|
1492
|
+
};
|
|
1493
|
+
vfs::filename_unlinkat(&Arc::default(), &root, "f").expect("the name must go");
|
|
1494
|
+
assert_eq!(
|
|
1495
|
+
(
|
|
1496
|
+
file.truncate(2),
|
|
1497
|
+
file.getattr().map(|found| found.size).ok()
|
|
1498
|
+
),
|
|
1499
|
+
(Ok(()), Some(2)),
|
|
1500
|
+
"File::truncate of a file whose name vfs::filename_unlinkat removed on a {name} \
|
|
1501
|
+
must size what it holds"
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// A dentry the host replaced is dropped by its identity, so the path
|
|
1507
|
+
// reaches the new file while the open one keeps its own descriptor.
|
|
1508
|
+
// @behavior FS-143
|
|
1509
|
+
#[cfg(unix)]
|
|
1510
|
+
#[test]
|
|
1511
|
+
fn a_file_replaced_while_open_answers_apart_from_its_path() {
|
|
1512
|
+
use crate::vfs::{self, OpenHow};
|
|
1513
|
+
let host = HostDir::new("replaced-open");
|
|
1514
|
+
let [_, (_, root), _] = each_holding_five_bytes(&host);
|
|
1515
|
+
let read = OpenHow {
|
|
1516
|
+
read: true,
|
|
1517
|
+
..OpenHow::default()
|
|
1518
|
+
};
|
|
1519
|
+
let Ok(file) = vfs::kern_path::filp_open(&Arc::default(), &root, "f", &read) else {
|
|
1520
|
+
panic!("f must open");
|
|
1521
|
+
};
|
|
1522
|
+
fs::rename(host.0.join("f"), host.0.join("aside")).expect("the host must move f");
|
|
1523
|
+
fs::write(host.0.join("f"), "longer!").expect("the host must remake f");
|
|
1524
|
+
assert_eq!(
|
|
1525
|
+
(
|
|
1526
|
+
vfs::vfs_fstatat(&Arc::default(), &root, "f", true)
|
|
1527
|
+
.map(|found| found.size)
|
|
1528
|
+
.ok(),
|
|
1529
|
+
file.getattr().map(|found| found.size).ok()
|
|
1530
|
+
),
|
|
1531
|
+
(Some(7), Some(5)),
|
|
1532
|
+
"vfs::vfs_fstatat of a path the host replaced under an open File must answer the new \
|
|
1533
|
+
file, and File::getattr the one it holds"
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// The guest's descriptor of `a` keeps the Overlay's dentry and the
|
|
1538
|
+
// Bind's, whose host directory the host then moved aside; only the
|
|
1539
|
+
// Overlay asking the Bind's dentry drops its own for the `a` remade there.
|
|
1540
|
+
// @behavior FS-139
|
|
1541
|
+
#[cfg(unix)]
|
|
1542
|
+
#[test]
|
|
1543
|
+
fn an_overlay_takes_afresh_a_name_its_bind_layer_no_longer_holds() {
|
|
1544
|
+
use crate::filesystem::Filesystem;
|
|
1545
|
+
use crate::filesystem::{Overlay, Tmpfs};
|
|
1546
|
+
use crate::mount::{Access, Mount};
|
|
1547
|
+
use crate::vfs::{self, OpenHow};
|
|
1548
|
+
let host = HostDir::new("overlay-replaced");
|
|
1549
|
+
fs::create_dir(host.0.join("a")).expect("the host directory must be made");
|
|
1550
|
+
let lower = Filesystem::new(Arc::new(
|
|
1551
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1552
|
+
))
|
|
1553
|
+
.expect("the instance must stand");
|
|
1554
|
+
let upper = Filesystem::new(Arc::new(Tmpfs::new())).expect("the instance must stand");
|
|
1555
|
+
let overlay = Overlay::new(vec![lower], Some(upper)).expect("the Overlay must stack");
|
|
1556
|
+
let mount = Mount::new(
|
|
1557
|
+
"/data",
|
|
1558
|
+
Filesystem::new(Arc::new(overlay)).expect("the instance must stand"),
|
|
1559
|
+
Access::Read,
|
|
1560
|
+
)
|
|
1561
|
+
.expect("/data is a guest path");
|
|
1562
|
+
let root = vfs::Path::root(mount.mnt());
|
|
1563
|
+
let directory = OpenHow {
|
|
1564
|
+
read: true,
|
|
1565
|
+
directory: true,
|
|
1566
|
+
..OpenHow::default()
|
|
1567
|
+
};
|
|
1568
|
+
let Ok(_held) = vfs::kern_path::filp_open(&Arc::default(), &root, "a", &directory) else {
|
|
1569
|
+
panic!("a must open");
|
|
1570
|
+
};
|
|
1571
|
+
fs::rename(host.0.join("a"), host.0.join("aside")).expect("the host must move a");
|
|
1572
|
+
fs::create_dir(host.0.join("a")).expect("the host must remake a");
|
|
1573
|
+
fs::write(host.0.join("a/f"), "new").expect("the host file must be written");
|
|
1574
|
+
let read = OpenHow {
|
|
1575
|
+
read: true,
|
|
1576
|
+
..OpenHow::default()
|
|
1577
|
+
};
|
|
1578
|
+
let Ok(mut file) = vfs::kern_path::filp_open(&Arc::default(), &root, "a/f", &read) else {
|
|
1579
|
+
panic!("a/f must open through the directory the host remade");
|
|
1580
|
+
};
|
|
1581
|
+
let mut buf = [0; 3];
|
|
1582
|
+
assert_eq!(
|
|
1583
|
+
(file.pread(&mut buf, 0), &buf),
|
|
1584
|
+
(Ok(3), b"new"),
|
|
1585
|
+
"vfs::do_file_open through an Overlay over a Bind whose host replaced a directory \
|
|
1586
|
+
a descriptor holds must read the file standing there now"
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
// The host took the directory after the name was found, so only the open
|
|
1591
|
+
// asking the host answers that it is gone, rather than a descriptor that
|
|
1592
|
+
// fails at its first use.
|
|
1593
|
+
// @behavior FS-166
|
|
1594
|
+
#[test]
|
|
1595
|
+
fn a_directory_that_cannot_open_answers_why() {
|
|
1596
|
+
let host = HostDir::new("gone-before-open");
|
|
1597
|
+
fs::create_dir_all(host.0.join("a")).expect("the host directory must be made");
|
|
1598
|
+
let filesystem = crate::filesystem::Filesystem::new(Arc::new(
|
|
1599
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1600
|
+
))
|
|
1601
|
+
.expect("the instance must stand");
|
|
1602
|
+
let dentries = filesystem.dentries();
|
|
1603
|
+
let root = filesystem.s_root();
|
|
1604
|
+
let dir = dentries
|
|
1605
|
+
.alloc_parallel(&root, "a")
|
|
1606
|
+
.expect("no other walk holds the name");
|
|
1607
|
+
dir.lookup_inode(&filesystem, &root)
|
|
1608
|
+
.expect("the name must be found");
|
|
1609
|
+
fs::remove_dir(host.0.join("a")).expect("the host must remove it");
|
|
1610
|
+
let answer = dir
|
|
1611
|
+
.inode_ops()
|
|
1612
|
+
.expect("the name records its directory")
|
|
1613
|
+
.open(&dir, &OpenHow::default());
|
|
1614
|
+
assert_eq!(
|
|
1615
|
+
answer.err(),
|
|
1616
|
+
Some(FilesystemError::NotFound),
|
|
1617
|
+
"InodeOperations::open of a Bind's directory the host removed must answer ENOENT"
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
// A walk finds what stands at the names now, so only comparing it with what
|
|
1622
|
+
// the dentry recorded keeps another directory from answering for it.
|
|
1623
|
+
// @behavior FS-167
|
|
1624
|
+
#[cfg(unix)]
|
|
1625
|
+
#[test]
|
|
1626
|
+
fn a_directory_the_host_replaced_is_not_walked_to_in_its_place() {
|
|
1627
|
+
let host = HostDir::new("replaced-before-walk");
|
|
1628
|
+
fs::create_dir_all(host.0.join("a")).expect("the host directory must be made");
|
|
1629
|
+
let filesystem = crate::filesystem::Filesystem::new(Arc::new(
|
|
1630
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1631
|
+
))
|
|
1632
|
+
.expect("the instance must stand");
|
|
1633
|
+
let dentries = filesystem.dentries();
|
|
1634
|
+
let root = filesystem.s_root();
|
|
1635
|
+
let dir = dentries
|
|
1636
|
+
.alloc_parallel(&root, "a")
|
|
1637
|
+
.expect("no other walk holds the name");
|
|
1638
|
+
dir.lookup_inode(&filesystem, &root)
|
|
1639
|
+
.expect("the name must be found");
|
|
1640
|
+
fs::rename(host.0.join("a"), host.0.join("moved")).expect("the host must move it");
|
|
1641
|
+
fs::create_dir(host.0.join("a")).expect("the host must make another");
|
|
1642
|
+
assert_eq!(
|
|
1643
|
+
dir.getattr().err(),
|
|
1644
|
+
Some(FilesystemError::NotFound),
|
|
1645
|
+
"InodeOperations::getattr of a Bind directory the host replaced must answer ENOENT \
|
|
1646
|
+
rather than what stands there now"
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
// A host's own rename takes no such flag, so only the refusal keeps a
|
|
1651
|
+
// call asking for one from moving the file as a plain rename would.
|
|
1652
|
+
// @behavior FS-163
|
|
1653
|
+
#[test]
|
|
1654
|
+
fn a_filesystem_taking_no_rename_flag_refuses_one() {
|
|
1655
|
+
let host = HostDir::new("rename-flag");
|
|
1656
|
+
fs::write(host.0.join("a"), "a").expect("the host file must be written");
|
|
1657
|
+
let filesystem = crate::filesystem::Filesystem::new(Arc::new(
|
|
1658
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1659
|
+
))
|
|
1660
|
+
.expect("the instance must stand");
|
|
1661
|
+
let dentries = filesystem.dentries();
|
|
1662
|
+
let root = filesystem.s_root();
|
|
1663
|
+
let dir = root.inode().expect("the root stands").ops().clone();
|
|
1664
|
+
let at = |name| {
|
|
1665
|
+
dentries
|
|
1666
|
+
.alloc_parallel(&root, name)
|
|
1667
|
+
.expect("no other walk holds the name")
|
|
1668
|
+
};
|
|
1669
|
+
let answer = dir.rename(
|
|
1670
|
+
&at("a"),
|
|
1671
|
+
dir.as_ref(),
|
|
1672
|
+
&at("b"),
|
|
1673
|
+
Some(RenameFlag::NoReplace),
|
|
1674
|
+
);
|
|
1675
|
+
assert_eq!(
|
|
1676
|
+
(answer, host.0.join("a").exists()),
|
|
1677
|
+
(Err(FilesystemError::InvalidInput), true),
|
|
1678
|
+
"InodeOperations::rename of a Bind asked for a flag must answer EINVAL and move \
|
|
1679
|
+
nothing"
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// A directory a Bind has named but not opened yet is walked to by the
|
|
1684
|
+
// names its dentry stands at when first asked, so a move the dentry
|
|
1685
|
+
// followed meanwhile leads to it, as 9p's walk reads them then.
|
|
1686
|
+
#[test]
|
|
1687
|
+
fn a_directory_named_before_a_move_opens_where_its_dentry_went() {
|
|
1688
|
+
let host = HostDir::new("named-then-moved");
|
|
1689
|
+
fs::create_dir_all(host.0.join("a")).expect("the host directory must be made");
|
|
1690
|
+
fs::write(host.0.join("a/note"), "n").expect("the host file must be written");
|
|
1691
|
+
let filesystem = crate::filesystem::Filesystem::new(Arc::new(
|
|
1692
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
1693
|
+
))
|
|
1694
|
+
.expect("the instance must stand");
|
|
1695
|
+
let dentries = filesystem.dentries();
|
|
1696
|
+
let root = filesystem.s_root();
|
|
1697
|
+
let at = |dir: &Arc<Dentry>, name| {
|
|
1698
|
+
dentries
|
|
1699
|
+
.alloc_parallel(dir, name)
|
|
1700
|
+
.expect("no other walk holds the name")
|
|
1701
|
+
};
|
|
1702
|
+
let a = at(&root, "a");
|
|
1703
|
+
fs::rename(host.0.join("a"), host.0.join("x")).expect("the host must move it");
|
|
1704
|
+
dentries.move_dentry(&a, &at(&root, "x"));
|
|
1705
|
+
let note = at(&a, "note");
|
|
1706
|
+
assert!(
|
|
1707
|
+
matches!(
|
|
1708
|
+
root.inode().expect("the root stands").ops().lookup(¬e),
|
|
1709
|
+
Ok(Lookup::Found(inode)) if inode.kind() == FileType::RegFile
|
|
1710
|
+
),
|
|
1711
|
+
"a name looked up through Bind in a directory not yet opened, once its dentry moved \
|
|
1712
|
+
with it, must be found where the dentry stands"
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
/// A host directory of the test's own holding the FIFO `pipe`.
|
|
1717
|
+
#[cfg(unix)]
|
|
1718
|
+
fn make_fifo_host(name: &str) -> HostDir {
|
|
1719
|
+
let host = HostDir::new(name);
|
|
1720
|
+
let made = std::process::Command::new("mkfifo")
|
|
1721
|
+
.arg(host.0.join("pipe"))
|
|
1722
|
+
.status()
|
|
1723
|
+
.expect("mkfifo must run");
|
|
1724
|
+
assert!(made.success(), "the FIFO must be made");
|
|
1725
|
+
host
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
// Opening the FIFO with no writer would wait for one, so only an open
|
|
1729
|
+
// refused before it waits answers within the deadline; `/dev/null`
|
|
1730
|
+
// is a device every unix host holds.
|
|
1731
|
+
// @behavior FS-090
|
|
1732
|
+
#[cfg(unix)]
|
|
1733
|
+
#[test]
|
|
1734
|
+
fn a_directory_opens_only_a_file_or_a_directory() {
|
|
1735
|
+
let host = make_fifo_host("fifo-open");
|
|
1736
|
+
let fifo = bind_host(&host.0);
|
|
1737
|
+
let devices = bind_host("/dev");
|
|
1738
|
+
let (sender, answered) = std::sync::mpsc::channel();
|
|
1739
|
+
std::thread::spawn(move || {
|
|
1740
|
+
let read = OpenHow {
|
|
1741
|
+
read: true,
|
|
1742
|
+
..OpenHow::default()
|
|
1743
|
+
};
|
|
1744
|
+
let _ = sender.send([
|
|
1745
|
+
fifo.open("pipe", &read).err(),
|
|
1746
|
+
devices.open("null", &read).err(),
|
|
1747
|
+
]);
|
|
1748
|
+
});
|
|
1749
|
+
assert_eq!(
|
|
1750
|
+
answered.recv_timeout(crate::sched::testing::MAX_WAIT),
|
|
1751
|
+
Ok([
|
|
1752
|
+
Some(FilesystemError::PermissionDenied),
|
|
1753
|
+
Some(FilesystemError::PermissionDenied)
|
|
1754
|
+
]),
|
|
1755
|
+
"a FIFO and a device opened through Bind::open must be refused as permission \
|
|
1756
|
+
denied without waiting"
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// @behavior FS-091
|
|
1761
|
+
#[cfg(unix)]
|
|
1762
|
+
#[test]
|
|
1763
|
+
fn a_directory_reports_a_fifo_as_one() {
|
|
1764
|
+
let host = make_fifo_host("fifo-type");
|
|
1765
|
+
let directory = bind_host(&host.0);
|
|
1766
|
+
let listed: Vec<_> = directory
|
|
1767
|
+
.iterate_dir()
|
|
1768
|
+
.expect("the directory must list")
|
|
1769
|
+
.into_iter()
|
|
1770
|
+
.map(|entry| entry.file_type)
|
|
1771
|
+
.collect();
|
|
1772
|
+
assert_eq!(
|
|
1773
|
+
(
|
|
1774
|
+
directory.vfs_lstat("pipe").map(|stat| stat.file_type),
|
|
1775
|
+
listed
|
|
1776
|
+
),
|
|
1777
|
+
(Ok(FileType::Fifo), vec![FileType::Fifo]),
|
|
1778
|
+
"a FIFO beneath a Bind, looked up and listed through the VFS, must answer a FIFO"
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// `/dev/null` stands on every unix host, and a socket needs only its file
|
|
1783
|
+
// to stand here, so neither is opened.
|
|
1784
|
+
// @behavior FS-144
|
|
1785
|
+
#[cfg(unix)]
|
|
1786
|
+
#[test]
|
|
1787
|
+
fn a_directory_reports_the_kind_its_host_gives_a_special_file() {
|
|
1788
|
+
let host = HostDir::new("special");
|
|
1789
|
+
let _socket = std::os::unix::net::UnixListener::bind(host.0.join("sock"))
|
|
1790
|
+
.expect("the socket must be bound");
|
|
1791
|
+
let kind = |directory: KernPath, name| directory.vfs_lstat(name).map(|stat| stat.file_type);
|
|
1792
|
+
assert_eq!(
|
|
1793
|
+
(
|
|
1794
|
+
kind(bind_host(&host.0), "sock"),
|
|
1795
|
+
kind(bind_host("/dev"), "null"),
|
|
1796
|
+
),
|
|
1797
|
+
(Ok(FileType::Sock), Ok(FileType::Chrdev)),
|
|
1798
|
+
"vfs_lstat of a socket and of /dev/null must answer the kind the host \
|
|
1799
|
+
gives each"
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
// @behavior FS-005
|
|
1804
|
+
#[test]
|
|
1805
|
+
fn a_directory_serves_the_files_of_its_host_directory() {
|
|
1806
|
+
let host = HostDir::new("serves");
|
|
1807
|
+
fs::write(host.0.join("data.csv"), "a,b").expect("the host file must be written");
|
|
1808
|
+
|
|
1809
|
+
let directory = bind_host(&host.0);
|
|
1810
|
+
let mut file = directory
|
|
1811
|
+
.open(
|
|
1812
|
+
"data.csv",
|
|
1813
|
+
&OpenHow {
|
|
1814
|
+
read: true,
|
|
1815
|
+
..OpenHow::default()
|
|
1816
|
+
},
|
|
1817
|
+
)
|
|
1818
|
+
.expect("the host file must open");
|
|
1819
|
+
let mut buf = [0; 16];
|
|
1820
|
+
let read = file
|
|
1821
|
+
.read_iter(&mut Kiocb::at(0), &mut buf)
|
|
1822
|
+
.expect("the host file must read");
|
|
1823
|
+
assert_eq!(
|
|
1824
|
+
&buf[..read],
|
|
1825
|
+
b"a,b",
|
|
1826
|
+
"a host file read through a Bind's open must read back its content"
|
|
1827
|
+
);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// @behavior FS-006
|
|
1831
|
+
#[test]
|
|
1832
|
+
fn a_directory_writes_into_its_host_directory() {
|
|
1833
|
+
let host = HostDir::new("writes");
|
|
1834
|
+
let directory = bind_host(&host.0);
|
|
1835
|
+
directory
|
|
1836
|
+
.open_or_create(
|
|
1837
|
+
"out.txt",
|
|
1838
|
+
&OpenHow {
|
|
1839
|
+
write: true,
|
|
1840
|
+
..OpenHow::default()
|
|
1841
|
+
},
|
|
1842
|
+
)
|
|
1843
|
+
.expect("a new file must be created")
|
|
1844
|
+
.write_iter(&mut Kiocb::at(0), b"written")
|
|
1845
|
+
.expect("the write must land");
|
|
1846
|
+
assert_eq!(
|
|
1847
|
+
fs::read(host.0.join("out.txt")).expect("the host file must exist"),
|
|
1848
|
+
b"written",
|
|
1849
|
+
"a file created through a Bind's open must land in the host directory"
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// The file is opened to write, so only the refusal keeps the append
|
|
1854
|
+
// from taking the two steps another writer could land between.
|
|
1855
|
+
// @behavior FS-093
|
|
1856
|
+
#[cfg(windows)]
|
|
1857
|
+
#[test]
|
|
1858
|
+
fn a_directory_on_windows_refuses_to_append() {
|
|
1859
|
+
let host = HostDir::new("append");
|
|
1860
|
+
fs::write(host.0.join("f"), "x").expect("the host file must be written");
|
|
1861
|
+
let directory = bind_host(&host.0);
|
|
1862
|
+
let mut file = directory
|
|
1863
|
+
.open(
|
|
1864
|
+
"f",
|
|
1865
|
+
&OpenHow {
|
|
1866
|
+
write: true,
|
|
1867
|
+
..OpenHow::default()
|
|
1868
|
+
},
|
|
1869
|
+
)
|
|
1870
|
+
.expect("the host file must open");
|
|
1871
|
+
assert_eq!(
|
|
1872
|
+
(
|
|
1873
|
+
file.write_iter(&mut Kiocb::at(0).with_append(), b"y").err(),
|
|
1874
|
+
fs::read(host.0.join("f")).expect("the host file must read")
|
|
1875
|
+
),
|
|
1876
|
+
(Some(FilesystemError::Unsupported), b"x".to_vec()),
|
|
1877
|
+
"an append through a Bind file on Windows must be refused as unsupported and \
|
|
1878
|
+
leave the file as it was"
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
// The host file exists beneath `d`, so only the refusal can keep the
|
|
1883
|
+
// open from reaching it.
|
|
1884
|
+
// @behavior FS-029
|
|
1885
|
+
#[cfg(windows)]
|
|
1886
|
+
#[test]
|
|
1887
|
+
fn a_directory_on_windows_refuses_a_name_holding_a_backslash() {
|
|
1888
|
+
let host = HostDir::new("backslash");
|
|
1889
|
+
fs::create_dir_all(host.0.join("d")).expect("the host directory must be created");
|
|
1890
|
+
fs::write(host.0.join("d").join("f"), "x").expect("the host file must be written");
|
|
1891
|
+
let directory = bind_host(&host.0);
|
|
1892
|
+
assert!(
|
|
1893
|
+
matches!(
|
|
1894
|
+
directory.open(r"d\f", &OpenHow::default()),
|
|
1895
|
+
Err(FilesystemError::InvalidInput)
|
|
1896
|
+
),
|
|
1897
|
+
"a path holding a backslash opened through a Bind on Windows must be refused as \
|
|
1898
|
+
invalid input"
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
// Judged on every host, as only Windows's own CI reaches the refusal, and
|
|
1903
|
+
// what the host would do with each name is what no other host can show.
|
|
1904
|
+
// @behavior FS-153
|
|
1905
|
+
#[test]
|
|
1906
|
+
fn a_name_windows_reads_as_another_is_told_apart() {
|
|
1907
|
+
let aliases = [
|
|
1908
|
+
"secret.",
|
|
1909
|
+
"secret ",
|
|
1910
|
+
"file:stream",
|
|
1911
|
+
r"d\f",
|
|
1912
|
+
"con",
|
|
1913
|
+
"NUL.txt",
|
|
1914
|
+
"conin$",
|
|
1915
|
+
"com1 .log",
|
|
1916
|
+
"a?",
|
|
1917
|
+
"a*",
|
|
1918
|
+
"a<",
|
|
1919
|
+
"a|",
|
|
1920
|
+
"a\"",
|
|
1921
|
+
"a\u{1}",
|
|
1922
|
+
];
|
|
1923
|
+
let plain = ["secret", "file.txt", "console", "com10", ".hidden", "a.b.c"];
|
|
1924
|
+
assert_eq!(
|
|
1925
|
+
(
|
|
1926
|
+
aliases
|
|
1927
|
+
.iter()
|
|
1928
|
+
.filter(|name| !is_windows_alias(name))
|
|
1929
|
+
.count(),
|
|
1930
|
+
plain.iter().filter(|name| is_windows_alias(name)).count(),
|
|
1931
|
+
),
|
|
1932
|
+
(0, 0),
|
|
1933
|
+
"is_windows_alias must name every spelling Windows reads as another, and no plain name"
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// @behavior FS-153
|
|
1938
|
+
#[cfg(windows)]
|
|
1939
|
+
#[test]
|
|
1940
|
+
fn a_directory_on_windows_refuses_a_name_it_reads_as_another() {
|
|
1941
|
+
let host = HostDir::new("aliases");
|
|
1942
|
+
fs::write(host.0.join("secret"), "x").expect("the host file must be written");
|
|
1943
|
+
let directory = bind_host(&host.0);
|
|
1944
|
+
assert!(
|
|
1945
|
+
["secret.", "secret:stream", "CON"]
|
|
1946
|
+
.iter()
|
|
1947
|
+
.all(|name| matches!(
|
|
1948
|
+
directory.open(name, &OpenHow::default()),
|
|
1949
|
+
Err(FilesystemError::InvalidInput)
|
|
1950
|
+
)),
|
|
1951
|
+
"a name Windows reads as another, opened through a Bind on Windows, must be refused \
|
|
1952
|
+
as invalid input"
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
// The host answers ENAMETOOLONG, which only a mapping naming it keeps
|
|
1957
|
+
// from reaching the guest as an EIO.
|
|
1958
|
+
// @behavior FS-065
|
|
1959
|
+
#[test]
|
|
1960
|
+
fn a_directory_answers_a_host_failure_as_the_error_naming_it() {
|
|
1961
|
+
let host = HostDir::new("names");
|
|
1962
|
+
let directory = bind_host(&host.0);
|
|
1963
|
+
let created = directory.open_or_create(
|
|
1964
|
+
&"x".repeat(300),
|
|
1965
|
+
&OpenHow {
|
|
1966
|
+
write: true,
|
|
1967
|
+
..OpenHow::default()
|
|
1968
|
+
},
|
|
1969
|
+
);
|
|
1970
|
+
assert_eq!(
|
|
1971
|
+
created.map(drop),
|
|
1972
|
+
Err(FilesystemError::InvalidFilename),
|
|
1973
|
+
"a name past the host's limit created through Bind::open must fail as \
|
|
1974
|
+
InvalidFilename, not as an unnamed failure"
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// Which offset a host refuses depends on its filesystem: tmpfs, XFS and
|
|
1979
|
+
// overlay keep files up to `i64::MAX`. The host's own EFBIG, 27 on Linux
|
|
1980
|
+
// and macOS alike, is what the Bind must name.
|
|
1981
|
+
// @behavior FS-068
|
|
1982
|
+
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
|
1983
|
+
#[test]
|
|
1984
|
+
fn a_directory_answers_a_host_file_too_large_as_too_large() {
|
|
1985
|
+
assert_eq!(
|
|
1986
|
+
to_error(io::Error::from_raw_os_error(27)),
|
|
1987
|
+
FilesystemError::FileTooLarge,
|
|
1988
|
+
"a host EFBIG through Bind's error mapping must fail as FileTooLarge, \
|
|
1989
|
+
not as an unnamed failure"
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
// A Bind's root is the directory cap-std opened for its path alone on Linux,
|
|
1994
|
+
// so a sync of the descriptor it holds would answer EBADF only there.
|
|
1995
|
+
// @behavior FS-171
|
|
1996
|
+
#[test]
|
|
1997
|
+
fn a_binds_root_syncs() {
|
|
1998
|
+
let host = HostDir::new("sync-root");
|
|
1999
|
+
let mount = crate::mount::Mount::new(
|
|
2000
|
+
"/data",
|
|
2001
|
+
Filesystem::new(Arc::new(
|
|
2002
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
2003
|
+
))
|
|
2004
|
+
.expect("the instance must stand"),
|
|
2005
|
+
crate::mount::Access::Read,
|
|
2006
|
+
)
|
|
2007
|
+
.expect("/data is a guest path");
|
|
2008
|
+
let Ok(mut root) = crate::vfs::File::root(mount.mnt(), &Arc::default()) else {
|
|
2009
|
+
panic!("the root must open");
|
|
2010
|
+
};
|
|
2011
|
+
assert_eq!(
|
|
2012
|
+
root.fsync(false),
|
|
2013
|
+
Ok(()),
|
|
2014
|
+
"File::fsync of a Bind's root must succeed, as Linux syncs a directory"
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
// A rename moves a name out from under `openat2` only within a race, which
|
|
2019
|
+
// the Linux baseline meets at times and no test can stage, so the answer the
|
|
2020
|
+
// refusal reaches is held here.
|
|
2021
|
+
// @behavior FS-191
|
|
2022
|
+
#[test]
|
|
2023
|
+
fn a_name_carried_out_as_it_is_opened_answers_not_found() {
|
|
2024
|
+
let refused = io::Error::new(io::ErrorKind::PermissionDenied, "a path led outside");
|
|
2025
|
+
assert_eq!(
|
|
2026
|
+
to_error(refused),
|
|
2027
|
+
FilesystemError::NotFound,
|
|
2028
|
+
"cap-std's refusal of a single name leading out of a Bind's directory must answer \
|
|
2029
|
+
ENOENT, as a move carried it away"
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// Eviction leaves a walk to reach a directory from one far above it, so only
|
|
2034
|
+
// a path past what the host takes in one call shows it goes on by name.
|
|
2035
|
+
// @behavior FS-190
|
|
2036
|
+
#[cfg(unix)]
|
|
2037
|
+
#[test]
|
|
2038
|
+
fn a_directory_past_the_hosts_path_limit_is_reached() {
|
|
2039
|
+
let host = HostDir::new("past-path-max");
|
|
2040
|
+
let top = Dir::open_ambient_dir(&host.0, ambient_authority()).expect("the host directory");
|
|
2041
|
+
let names = vec!["n".repeat(200); 30];
|
|
2042
|
+
let deepest = names
|
|
2043
|
+
.iter()
|
|
2044
|
+
.fold(top.try_clone().expect("a second handle"), |at, name| {
|
|
2045
|
+
at.create_dir(name)
|
|
2046
|
+
.expect("the host directory must be made");
|
|
2047
|
+
at.open_dir(name).expect("the host directory must open")
|
|
2048
|
+
});
|
|
2049
|
+
deepest
|
|
2050
|
+
.write("key", "x")
|
|
2051
|
+
.expect("the host file must be written");
|
|
2052
|
+
let reached = walk::walk(&top, &names).map(|dir| dir.exists("key"));
|
|
2053
|
+
assert_eq!(
|
|
2054
|
+
reached.ok(),
|
|
2055
|
+
Some(true),
|
|
2056
|
+
"a Bind's walk to a directory 6000 bytes beneath the one it holds must reach it, past \
|
|
2057
|
+
what the host takes in one call"
|
|
2058
|
+
);
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// Root reads anything, so a host letting this one be read has nothing to
|
|
2062
|
+
// show. Linux reaches a directory for its path alone, so only asking to read
|
|
2063
|
+
// it there makes the host answer; macOS answers on any open.
|
|
2064
|
+
// @behavior FS-189
|
|
2065
|
+
#[cfg(unix)]
|
|
2066
|
+
#[test]
|
|
2067
|
+
fn a_directory_the_host_lets_no_one_read_refuses_an_open_to_read() {
|
|
2068
|
+
use std::os::unix::fs::PermissionsExt;
|
|
2069
|
+
|
|
2070
|
+
let host = HostDir::new("unreadable");
|
|
2071
|
+
let dir = host.0.join("sealed");
|
|
2072
|
+
fs::create_dir(&dir).expect("the host directory must be made");
|
|
2073
|
+
let mode = |mode| fs::Permissions::from_mode(mode);
|
|
2074
|
+
fs::set_permissions(&dir, mode(0o000)).expect("the host directory's mode must be set");
|
|
2075
|
+
let enforced = fs::read_dir(&dir).is_err();
|
|
2076
|
+
let mount = crate::mount::Mount::new(
|
|
2077
|
+
"/data",
|
|
2078
|
+
Filesystem::new(Arc::new(
|
|
2079
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
2080
|
+
))
|
|
2081
|
+
.expect("the instance must stand"),
|
|
2082
|
+
crate::mount::Access::Read,
|
|
2083
|
+
)
|
|
2084
|
+
.expect("/data is a guest path");
|
|
2085
|
+
let root = crate::vfs::Path::root(mount.mnt());
|
|
2086
|
+
let how = crate::vfs::OpenHow {
|
|
2087
|
+
read: true,
|
|
2088
|
+
directory: true,
|
|
2089
|
+
..Default::default()
|
|
2090
|
+
};
|
|
2091
|
+
let answer = crate::vfs::kern_path::filp_open(&Arc::default(), &root, "sealed", &how).err();
|
|
2092
|
+
fs::set_permissions(&dir, mode(0o755)).expect("the host directory's mode must be restored");
|
|
2093
|
+
if enforced {
|
|
2094
|
+
assert_eq!(
|
|
2095
|
+
answer,
|
|
2096
|
+
Some(FilesystemError::PermissionDenied),
|
|
2097
|
+
"vfs::do_file_open to read a host directory no one may read must answer EACCES"
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Linux holds a directory for its path alone, so only asking the host to
|
|
2103
|
+
// read it refuses one no one may read; macOS refuses it on any open. Root
|
|
2104
|
+
// reads anything, so only another user sees the refusal.
|
|
2105
|
+
// @behavior FS-200
|
|
2106
|
+
#[cfg(unix)]
|
|
2107
|
+
#[test]
|
|
2108
|
+
fn a_bind_of_a_directory_the_host_lets_no_one_read_is_refused() {
|
|
2109
|
+
use std::os::unix::fs::PermissionsExt;
|
|
2110
|
+
|
|
2111
|
+
let host = HostDir::new("unreadable-root");
|
|
2112
|
+
let mode = |mode| fs::Permissions::from_mode(mode);
|
|
2113
|
+
fs::set_permissions(&host.0, mode(0o100)).expect("the host directory's mode must be set");
|
|
2114
|
+
let enforced = fs::read_dir(&host.0).is_err();
|
|
2115
|
+
let answer = Bind::new(&host.0).err().map(|err| err.kind());
|
|
2116
|
+
fs::set_permissions(&host.0, mode(0o755))
|
|
2117
|
+
.expect("the host directory's mode must be restored");
|
|
2118
|
+
if enforced {
|
|
2119
|
+
assert_eq!(
|
|
2120
|
+
answer,
|
|
2121
|
+
Some(io::ErrorKind::PermissionDenied),
|
|
2122
|
+
"Bind::new of a host directory no one may read must be refused as permission denied"
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// macOS has no open for a directory's path alone. Root reads anything, so
|
|
2128
|
+
// only a walk run as another user sees the difference.
|
|
2129
|
+
// @behavior FS-172
|
|
2130
|
+
#[cfg(target_os = "linux")]
|
|
2131
|
+
#[test]
|
|
2132
|
+
fn a_directory_a_host_lets_be_searched_reports_itself() {
|
|
2133
|
+
use std::os::unix::fs::PermissionsExt;
|
|
2134
|
+
|
|
2135
|
+
let host = HostDir::new("search-only");
|
|
2136
|
+
fs::create_dir(host.0.join("searched")).expect("the host directory must be made");
|
|
2137
|
+
let mode = |mode| fs::Permissions::from_mode(mode);
|
|
2138
|
+
fs::set_permissions(host.0.join("searched"), mode(0o111))
|
|
2139
|
+
.expect("the host directory's mode must be set");
|
|
2140
|
+
let mount = crate::mount::Mount::new(
|
|
2141
|
+
"/data",
|
|
2142
|
+
Filesystem::new(Arc::new(
|
|
2143
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
2144
|
+
))
|
|
2145
|
+
.expect("the instance must stand"),
|
|
2146
|
+
crate::mount::Access::Read,
|
|
2147
|
+
)
|
|
2148
|
+
.expect("/data is a guest path");
|
|
2149
|
+
let root = crate::vfs::Path::root(mount.mnt());
|
|
2150
|
+
let found = crate::vfs::vfs_fstatat(&Arc::default(), &root, "searched", false);
|
|
2151
|
+
fs::set_permissions(host.0.join("searched"), mode(0o755))
|
|
2152
|
+
.expect("the host directory's mode must be restored");
|
|
2153
|
+
assert_eq!(
|
|
2154
|
+
found.map(|metadata| metadata.file_type),
|
|
2155
|
+
Ok(FileType::Dir),
|
|
2156
|
+
"vfs::vfs_fstatat of a host directory only searchable must report it, as Linux's stat \
|
|
2157
|
+
needs no read permission on it"
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
|
|
2161
|
+
// A host that folds case answers `A` with the entry stored as `a`, so the
|
|
2162
|
+
// VFS finds one entry on both sides; only a host that folds witnesses it.
|
|
2163
|
+
// @behavior FS-205
|
|
2164
|
+
#[test]
|
|
2165
|
+
fn a_rename_to_another_spelling_of_one_name_changes_nothing() {
|
|
2166
|
+
let host = HostDir::new("case-only-rename");
|
|
2167
|
+
fs::write(host.0.join("a"), "x").expect("the host file must be written");
|
|
2168
|
+
if !host.0.join("A").exists() {
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
let mount = crate::mount::Mount::new(
|
|
2172
|
+
"/data",
|
|
2173
|
+
Filesystem::new(Arc::new(
|
|
2174
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
2175
|
+
))
|
|
2176
|
+
.expect("the instance must stand"),
|
|
2177
|
+
crate::mount::Access::ReadWrite,
|
|
2178
|
+
)
|
|
2179
|
+
.expect("/data is a guest path");
|
|
2180
|
+
let root = crate::vfs::Path::root(mount.mnt());
|
|
2181
|
+
let renamed = crate::vfs::filename_renameat2(&Arc::default(), &root, "a", &root, "A");
|
|
2182
|
+
let stored: Vec<_> = fs::read_dir(&host.0)
|
|
2183
|
+
.expect("the host directory must list")
|
|
2184
|
+
.map(|entry| entry.expect("the entry must read").file_name())
|
|
2185
|
+
.collect();
|
|
2186
|
+
// Windows reports no inode, so nothing ties `A` to the stored `a` and
|
|
2187
|
+
// the spelling is refused rather than followed.
|
|
2188
|
+
let answered = match cfg!(windows) {
|
|
2189
|
+
true => Err(FilesystemError::PermissionDenied),
|
|
2190
|
+
false => Ok(()),
|
|
2191
|
+
};
|
|
2192
|
+
assert_eq!(
|
|
2193
|
+
(renamed, stored),
|
|
2194
|
+
(answered, vec![std::ffi::OsString::from("a")]),
|
|
2195
|
+
"vfs::filename_renameat2 of `a` to `A` on a Bind of a host that folds case must \
|
|
2196
|
+
leave `a` spelled as it was, as vfs_rename finds one entry, refusing it only \
|
|
2197
|
+
where the host ties no spelling to the entry"
|
|
2198
|
+
);
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// Linux walks a directory it may only search, so a stat through one
|
|
2202
|
+
// succeeds there; the Bind reads the directory to learn the name's
|
|
2203
|
+
// spelling, which the mode refuses. Root reads anything, so only another
|
|
2204
|
+
// user sees the refusal.
|
|
2205
|
+
// @behavior FS-202
|
|
2206
|
+
#[cfg(unix)]
|
|
2207
|
+
#[test]
|
|
2208
|
+
fn a_name_beneath_a_directory_the_host_lets_only_be_searched_is_refused() {
|
|
2209
|
+
use std::os::unix::fs::PermissionsExt;
|
|
2210
|
+
|
|
2211
|
+
let host = HostDir::new("search-only-name");
|
|
2212
|
+
fs::create_dir(host.0.join("searched")).expect("the host directory must be made");
|
|
2213
|
+
fs::write(host.0.join("searched/x"), "x").expect("the host file must be written");
|
|
2214
|
+
let mode = |mode| fs::Permissions::from_mode(mode);
|
|
2215
|
+
fs::set_permissions(host.0.join("searched"), mode(0o111))
|
|
2216
|
+
.expect("the host directory's mode must be set");
|
|
2217
|
+
let enforced = fs::read_dir(host.0.join("searched")).is_err();
|
|
2218
|
+
let mount = crate::mount::Mount::new(
|
|
2219
|
+
"/data",
|
|
2220
|
+
Filesystem::new(Arc::new(
|
|
2221
|
+
Bind::new(&host.0).expect("the host directory must open"),
|
|
2222
|
+
))
|
|
2223
|
+
.expect("the instance must stand"),
|
|
2224
|
+
crate::mount::Access::Read,
|
|
2225
|
+
)
|
|
2226
|
+
.expect("/data is a guest path");
|
|
2227
|
+
let root = crate::vfs::Path::root(mount.mnt());
|
|
2228
|
+
let found = crate::vfs::vfs_fstatat(&Arc::default(), &root, "searched/x", false);
|
|
2229
|
+
fs::set_permissions(host.0.join("searched"), mode(0o755))
|
|
2230
|
+
.expect("the host directory's mode must be restored");
|
|
2231
|
+
if enforced {
|
|
2232
|
+
assert_eq!(
|
|
2233
|
+
found.err(),
|
|
2234
|
+
Some(FilesystemError::PermissionDenied),
|
|
2235
|
+
"vfs::vfs_fstatat of a file beneath a host directory only searchable must \
|
|
2236
|
+
answer EACCES, as the Bind reads the directory to spell the name"
|
|
2237
|
+
);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// Only Linux keeps a name that is not UTF-8; macOS refuses to make one.
|
|
2242
|
+
// @behavior FS-066
|
|
2243
|
+
#[cfg(target_os = "linux")]
|
|
2244
|
+
#[test]
|
|
2245
|
+
fn a_directory_refuses_to_list_a_host_name_that_is_not_utf8() {
|
|
2246
|
+
use std::os::unix::ffi::OsStrExt;
|
|
2247
|
+
|
|
2248
|
+
let host = HostDir::new("ilseq");
|
|
2249
|
+
fs::write(host.0.join(std::ffi::OsStr::from_bytes(b"\xff")), "")
|
|
2250
|
+
.expect("the host file must be written");
|
|
2251
|
+
let directory = bind_host(&host.0);
|
|
2252
|
+
assert_eq!(
|
|
2253
|
+
directory.iterate_dir().map(drop),
|
|
2254
|
+
Err(FilesystemError::IllegalSequence),
|
|
2255
|
+
"a host name that is not UTF-8 listed through iterate_dir must fail as \
|
|
2256
|
+
IllegalSequence, not as an unnamed failure"
|
|
2257
|
+
);
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// The host file only reads, so a host open asking to write, as one that
|
|
2261
|
+
// creates must, would be refused where nothing is to be created.
|
|
2262
|
+
// @behavior FS-064
|
|
2263
|
+
#[cfg(unix)]
|
|
2264
|
+
#[test]
|
|
2265
|
+
fn a_directory_opens_a_file_already_there_without_writing_to_create_it() {
|
|
2266
|
+
use std::os::unix::fs::PermissionsExt;
|
|
2267
|
+
let host = HostDir::new("create-existing");
|
|
2268
|
+
fs::write(host.0.join("note"), "x").expect("the host file must be written");
|
|
2269
|
+
fs::set_permissions(host.0.join("note"), fs::Permissions::from_mode(0o444))
|
|
2270
|
+
.expect("the host file must become read-only");
|
|
2271
|
+
let directory = bind_host(&host.0);
|
|
2272
|
+
let creates = OpenHow {
|
|
2273
|
+
read: true,
|
|
2274
|
+
..OpenHow::default()
|
|
2275
|
+
};
|
|
2276
|
+
assert_eq!(
|
|
2277
|
+
(
|
|
2278
|
+
directory.open_or_create("note", &creates).map(drop),
|
|
2279
|
+
directory.open_or_create("new", &creates).map(drop),
|
|
2280
|
+
host.0.join("new").exists()
|
|
2281
|
+
),
|
|
2282
|
+
(Ok(()), Ok(()), true),
|
|
2283
|
+
"a Bind::open that may create, of a read-only host file already there, must open \
|
|
2284
|
+
it, and of a missing one must create it"
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
// The link's target is a readable file, so only the refusal of the
|
|
2289
|
+
// link itself keeps the open from reaching it.
|
|
2290
|
+
// @behavior FS-047
|
|
2291
|
+
#[cfg(unix)]
|
|
2292
|
+
#[test]
|
|
2293
|
+
fn a_directory_opens_no_link() {
|
|
2294
|
+
let host = HostDir::new("nofollow");
|
|
2295
|
+
fs::write(host.0.join("target"), "x").expect("the host file must be written");
|
|
2296
|
+
std::os::unix::fs::symlink("target", host.0.join("link")).expect("the link must be made");
|
|
2297
|
+
let opened = bind_host(&host.0)
|
|
2298
|
+
.open(
|
|
2299
|
+
"link",
|
|
2300
|
+
&OpenHow {
|
|
2301
|
+
read: true,
|
|
2302
|
+
..OpenHow::default()
|
|
2303
|
+
},
|
|
2304
|
+
)
|
|
2305
|
+
.map(drop);
|
|
2306
|
+
assert_eq!(
|
|
2307
|
+
opened,
|
|
2308
|
+
Err(FilesystemError::FilesystemLoop),
|
|
2309
|
+
"a host link opened through a Bind must be refused as a link, its target unreached"
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// The VFS refuses a link it found before asking the Bind, so only the
|
|
2314
|
+
// Bind's own open sees a link the host put in place after the lookup.
|
|
2315
|
+
// @behavior FS-047
|
|
2316
|
+
#[cfg(unix)]
|
|
2317
|
+
#[test]
|
|
2318
|
+
fn a_directory_refuses_a_link_put_in_place_of_what_it_opens() {
|
|
2319
|
+
let host = HostDir::new("nofollow-swapped");
|
|
2320
|
+
fs::write(host.0.join("target"), "x").expect("the host file must be written");
|
|
2321
|
+
std::os::unix::fs::symlink("target", host.0.join("link")).expect("the link must be made");
|
|
2322
|
+
let dir = Dir::open_ambient_dir(&host.0, ambient_authority()).expect("the host directory");
|
|
2323
|
+
let read = OpenHow {
|
|
2324
|
+
read: true,
|
|
2325
|
+
..OpenHow::default()
|
|
2326
|
+
};
|
|
2327
|
+
assert_eq!(
|
|
2328
|
+
open_in(&dir, "link", &read, false).map(drop),
|
|
2329
|
+
Err(FilesystemError::FilesystemLoop),
|
|
2330
|
+
"a host link standing where a Bind opens a file must be refused as a link, its \
|
|
2331
|
+
target unreached"
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
// The target's five bytes would show as the link's size if a lookup
|
|
2336
|
+
// that follows none still followed it.
|
|
2337
|
+
// @behavior FS-048
|
|
2338
|
+
#[cfg(unix)]
|
|
2339
|
+
#[test]
|
|
2340
|
+
fn a_directory_reports_a_link_a_lookup_does_not_follow_as_a_link() {
|
|
2341
|
+
let host = HostDir::new("lstat");
|
|
2342
|
+
fs::write(host.0.join("target"), "hello").expect("the host file must be written");
|
|
2343
|
+
std::os::unix::fs::symlink("target", host.0.join("link")).expect("the link must be made");
|
|
2344
|
+
let directory = bind_host(&host.0);
|
|
2345
|
+
let listed = directory
|
|
2346
|
+
.iterate_dir()
|
|
2347
|
+
.expect("the host directory must list")
|
|
2348
|
+
.into_iter()
|
|
2349
|
+
.find(|entry| entry.name == "link")
|
|
2350
|
+
.map(|entry| entry.file_type);
|
|
2351
|
+
assert_eq!(
|
|
2352
|
+
(
|
|
2353
|
+
directory.vfs_lstat("link").map(|stat| stat.file_type),
|
|
2354
|
+
listed
|
|
2355
|
+
),
|
|
2356
|
+
(Ok(FileType::Symlink), Some(FileType::Symlink)),
|
|
2357
|
+
"a host link beneath a Bind, looked up through vfs_lstat and listed through \
|
|
2358
|
+
iterate_dir, must report a link"
|
|
2359
|
+
);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// A directory's size is the host's to choose, so the host's own stat
|
|
2363
|
+
// is the only witness of it.
|
|
2364
|
+
// @behavior FS-138
|
|
2365
|
+
#[test]
|
|
2366
|
+
fn a_directory_reports_the_size_its_host_reports() {
|
|
2367
|
+
let host = HostDir::new("dir-size");
|
|
2368
|
+
fs::create_dir(host.0.join("sub")).expect("the host directory must be created");
|
|
2369
|
+
fs::write(host.0.join("sub/file"), "hello").expect("the host file must be written");
|
|
2370
|
+
let directory = bind_host(&host.0);
|
|
2371
|
+
assert_eq!(
|
|
2372
|
+
directory.vfs_lstat("sub").map(|stat| stat.size),
|
|
2373
|
+
Ok(fs::metadata(host.0.join("sub"))
|
|
2374
|
+
.expect("the host directory must stand")
|
|
2375
|
+
.len()),
|
|
2376
|
+
"a host directory beneath a Bind looked up through vfs_lstat must report the size \
|
|
2377
|
+
its host reports"
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// @behavior FS-028
|
|
2382
|
+
#[test]
|
|
2383
|
+
fn a_directory_refuses_to_remove_a_directory_as_a_file() {
|
|
2384
|
+
let host = HostDir::new("unlink-dir");
|
|
2385
|
+
fs::create_dir(host.0.join("sub")).expect("the host directory must be created");
|
|
2386
|
+
let directory = bind_host(&host.0);
|
|
2387
|
+
assert_eq!(
|
|
2388
|
+
directory.vfs_unlink("sub"),
|
|
2389
|
+
Err(FilesystemError::IsADirectory),
|
|
2390
|
+
"a host directory beneath a Bind removed through vfs_unlink must answer EISDIR"
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
/// A host directory holding `dir/sub/y` and `secret`, with `dir/in`
|
|
2395
|
+
/// linking to `sub` and `dir/up` to `../secret`.
|
|
2396
|
+
#[cfg(unix)]
|
|
2397
|
+
fn make_links(name: &str) -> HostDir {
|
|
2398
|
+
let host = HostDir::new(name);
|
|
2399
|
+
fs::create_dir_all(host.0.join("dir/sub")).expect("the host directory must be created");
|
|
2400
|
+
fs::write(host.0.join("dir/sub/y"), "inner").expect("the inner file must be written");
|
|
2401
|
+
fs::write(host.0.join("secret"), "outer").expect("the outer file must be written");
|
|
2402
|
+
std::os::unix::fs::symlink("sub", host.0.join("dir/in")).expect("the link must be made");
|
|
2403
|
+
std::os::unix::fs::symlink("../secret", host.0.join("dir/up"))
|
|
2404
|
+
.expect("the link must be made");
|
|
2405
|
+
host
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// The link leads to a directory inside the host directory, so only
|
|
2409
|
+
// refusing the link itself keeps it from being opened.
|
|
2410
|
+
// @behavior FS-099
|
|
2411
|
+
#[cfg(unix)]
|
|
2412
|
+
#[test]
|
|
2413
|
+
fn a_directory_opens_no_link_as_a_directory_where_asked_not_to_follow() {
|
|
2414
|
+
let host = make_links("open-dir-nofollow");
|
|
2415
|
+
let directory = bind_host(&host.0);
|
|
2416
|
+
let dir = directory.open_dir("dir").expect("the directory must open");
|
|
2417
|
+
assert_eq!(
|
|
2418
|
+
dir.open_dir("in").map(drop),
|
|
2419
|
+
Err(FilesystemError::FilesystemLoop),
|
|
2420
|
+
"a link to a directory opened through Bind::open_dir must be a link loop"
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// @behavior FS-114
|
|
2425
|
+
#[cfg(unix)]
|
|
2426
|
+
#[test]
|
|
2427
|
+
fn a_directory_reads_where_a_link_points() {
|
|
2428
|
+
let host = make_links("read-link");
|
|
2429
|
+
let directory = bind_host(&host.0);
|
|
2430
|
+
let dir = directory.open_dir("dir").expect("the directory must open");
|
|
2431
|
+
assert_eq!(
|
|
2432
|
+
(dir.vfs_readlink("up"), dir.vfs_readlink("sub")),
|
|
2433
|
+
(
|
|
2434
|
+
Ok("../secret".to_string()),
|
|
2435
|
+
Err(FilesystemError::InvalidInput)
|
|
2436
|
+
),
|
|
2437
|
+
"vfs_readlink beneath a Bind must answer where a link points as stored, and refuse \
|
|
2438
|
+
a directory with EINVAL"
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
// @behavior FS-007
|
|
2443
|
+
#[cfg(unix)]
|
|
2444
|
+
#[test]
|
|
2445
|
+
fn a_directory_reaches_nothing_outside_its_host_directory() {
|
|
2446
|
+
let outside = HostDir::new("outside");
|
|
2447
|
+
fs::write(outside.0.join("secret"), "host").expect("the outside file must be written");
|
|
2448
|
+
let host = HostDir::new("confined");
|
|
2449
|
+
std::os::unix::fs::symlink(outside.0.join("secret"), host.0.join("escape"))
|
|
2450
|
+
.expect("the link must be created");
|
|
2451
|
+
|
|
2452
|
+
let directory = bind_host(&host.0);
|
|
2453
|
+
assert!(
|
|
2454
|
+
directory
|
|
2455
|
+
.open(
|
|
2456
|
+
"escape",
|
|
2457
|
+
&OpenHow {
|
|
2458
|
+
read: true,
|
|
2459
|
+
..OpenHow::default()
|
|
2460
|
+
}
|
|
2461
|
+
)
|
|
2462
|
+
.is_err(),
|
|
2463
|
+
"a link out of the host directory opened through a Bind's open must be refused"
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// @behavior FS-071
|
|
2468
|
+
#[cfg(unix)]
|
|
2469
|
+
#[test]
|
|
2470
|
+
fn a_directory_reports_the_identity_its_host_gives_a_path() {
|
|
2471
|
+
use std::os::unix::fs::MetadataExt;
|
|
2472
|
+
|
|
2473
|
+
let host = HostDir::new("identity");
|
|
2474
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
2475
|
+
let expected = fs::metadata(host.0.join("dir")).expect("the host must stat it");
|
|
2476
|
+
let directory = bind_host(&host.0);
|
|
2477
|
+
let reported = directory
|
|
2478
|
+
.vfs_lstat("dir")
|
|
2479
|
+
.expect("the directory must stand");
|
|
2480
|
+
assert_eq!(
|
|
2481
|
+
(reported.dev, reported.ino),
|
|
2482
|
+
(expected.dev(), expected.ino()),
|
|
2483
|
+
"a host directory beneath a Bind looked up through vfs_lstat must carry the device \
|
|
2484
|
+
and inode numbers the host reports"
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// Two handles racing one-byte appends: a two-step append (read the
|
|
2489
|
+
// length, then write there) loses about half the bytes to the other
|
|
2490
|
+
// handle's writes, so only an append the host places keeps them all.
|
|
2491
|
+
// @behavior FS-069
|
|
2492
|
+
#[cfg(unix)]
|
|
2493
|
+
#[test]
|
|
2494
|
+
fn a_directory_appends_at_its_host_files_end_in_one_step() {
|
|
2495
|
+
let host = HostDir::new("append");
|
|
2496
|
+
fs::write(host.0.join("log.txt"), "").expect("the host file must be written");
|
|
2497
|
+
let directory = bind_host(&host.0);
|
|
2498
|
+
|
|
2499
|
+
let writers: Vec<_> = [b"a", b"b"]
|
|
2500
|
+
.into_iter()
|
|
2501
|
+
.map(|byte| {
|
|
2502
|
+
let directory = directory.clone();
|
|
2503
|
+
std::thread::spawn(move || {
|
|
2504
|
+
let how = OpenHow {
|
|
2505
|
+
write: true,
|
|
2506
|
+
..OpenHow::default()
|
|
2507
|
+
};
|
|
2508
|
+
let mut file = directory
|
|
2509
|
+
.open("log.txt", &how)
|
|
2510
|
+
.expect("the host file must open to write");
|
|
2511
|
+
for _ in 0..10_000 {
|
|
2512
|
+
file.write_iter(&mut Kiocb::at(0).with_append(), byte)
|
|
2513
|
+
.expect("the append must succeed");
|
|
2514
|
+
}
|
|
2515
|
+
})
|
|
2516
|
+
})
|
|
2517
|
+
.collect();
|
|
2518
|
+
for writer in writers {
|
|
2519
|
+
writer.join().expect("the writer must finish");
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
let written = fs::read(host.0.join("log.txt")).expect("the host file must be read");
|
|
2523
|
+
assert_eq!(
|
|
2524
|
+
(
|
|
2525
|
+
written.iter().filter(|&&b| b == b'a').count(),
|
|
2526
|
+
written.iter().filter(|&&b| b == b'b').count()
|
|
2527
|
+
),
|
|
2528
|
+
(10_000, 10_000),
|
|
2529
|
+
"two handles appending to one host file through a Bind's FileOperations::write_iter must each \
|
|
2530
|
+
keep every byte"
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
// ext4 hands a removed directory's inode number to the next one made,
|
|
2535
|
+
// so the number staying unused while open is what the opened directory
|
|
2536
|
+
// keeps. A host that never reuses numbers cannot show that, but every
|
|
2537
|
+
// Linux host lists the removed directory among the process's
|
|
2538
|
+
// descriptors while it is open.
|
|
2539
|
+
// @behavior FS-086
|
|
2540
|
+
#[cfg(target_os = "linux")]
|
|
2541
|
+
#[test]
|
|
2542
|
+
fn a_directory_opened_keeps_its_inode_number_from_the_next_one_made() {
|
|
2543
|
+
use std::os::unix::fs::MetadataExt;
|
|
2544
|
+
|
|
2545
|
+
let host = HostDir::new("held");
|
|
2546
|
+
let ino = |path: &str| {
|
|
2547
|
+
fs::metadata(host.0.join(path))
|
|
2548
|
+
.expect("it must stand")
|
|
2549
|
+
.ino()
|
|
2550
|
+
};
|
|
2551
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
2552
|
+
let first = ino("dir");
|
|
2553
|
+
fs::remove_dir(host.0.join("dir")).expect("the host directory must go");
|
|
2554
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
2555
|
+
let reuses = ino("dir") == first;
|
|
2556
|
+
let first = ino("dir");
|
|
2557
|
+
let directory = bind_host(&host.0);
|
|
2558
|
+
let opened = directory.open_dir("dir").expect("the directory must open");
|
|
2559
|
+
fs::remove_dir(host.0.join("dir")).expect("the host directory must go");
|
|
2560
|
+
let removed = format!("{} (deleted)", host.0.join("dir").display());
|
|
2561
|
+
let held_open = fs::read_dir("/proc/self/fd")
|
|
2562
|
+
.expect("the process's descriptors must list")
|
|
2563
|
+
.filter_map(|entry| fs::read_link(entry.ok()?.path()).ok())
|
|
2564
|
+
.any(|target| target.to_string_lossy() == removed);
|
|
2565
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
2566
|
+
let made = ino("dir");
|
|
2567
|
+
drop(opened);
|
|
2568
|
+
assert!(
|
|
2569
|
+
held_open,
|
|
2570
|
+
"a directory Bind::open_dir opened must stay open once removed"
|
|
2571
|
+
);
|
|
2572
|
+
if !reuses {
|
|
2573
|
+
eprintln!("the host does not reuse inode numbers; nothing more to witness");
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
assert_ne!(
|
|
2577
|
+
made, first,
|
|
2578
|
+
"a directory made where Bind::open_dir holds a removed one open must take \
|
|
2579
|
+
another inode number on a host reusing them"
|
|
2580
|
+
);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// Windows refuses to move a directory a handle holds open, so the host
|
|
2584
|
+
// cannot make the move this witnesses there.
|
|
2585
|
+
// @behavior FS-108
|
|
2586
|
+
#[cfg(unix)]
|
|
2587
|
+
#[test]
|
|
2588
|
+
fn a_directory_opened_from_a_directory_follows_it_when_the_host_moves_it() {
|
|
2589
|
+
let host = HostDir::new("opened-moves");
|
|
2590
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
2591
|
+
let directory = bind_host(&host.0);
|
|
2592
|
+
let dir = directory
|
|
2593
|
+
.open_dir("dir")
|
|
2594
|
+
.expect("the host directory must open");
|
|
2595
|
+
fs::rename(host.0.join("dir"), host.0.join("moved")).expect("the host must move it");
|
|
2596
|
+
dir.open_or_create(
|
|
2597
|
+
"made",
|
|
2598
|
+
&OpenHow {
|
|
2599
|
+
write: true,
|
|
2600
|
+
..OpenHow::default()
|
|
2601
|
+
},
|
|
2602
|
+
)
|
|
2603
|
+
.expect("the file must be made");
|
|
2604
|
+
assert_eq!(
|
|
2605
|
+
(
|
|
2606
|
+
host.0.join("moved/made").is_file(),
|
|
2607
|
+
host.0.join("dir").exists()
|
|
2608
|
+
),
|
|
2609
|
+
(true, false),
|
|
2610
|
+
"a file made through a directory Bind::open_dir opened before the host moved it \
|
|
2611
|
+
must stand beneath the directory's new host path"
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
// @behavior FS-081
|
|
2616
|
+
#[test]
|
|
2617
|
+
fn a_directory_spells_names_it_matches_exactly_as_given() {
|
|
2618
|
+
let host = HostDir::new("spelled");
|
|
2619
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
2620
|
+
fs::write(host.0.join("dir/file"), "x").expect("the host file must be written");
|
|
2621
|
+
let directory = bind_host(&host.0);
|
|
2622
|
+
let dir = directory
|
|
2623
|
+
.open_dir("dir")
|
|
2624
|
+
.expect("the host directory must open");
|
|
2625
|
+
assert_eq!(
|
|
2626
|
+
(dir.lookup("file"), dir.lookup("new")),
|
|
2627
|
+
(Ok("file".to_string()), Ok("new".to_string())),
|
|
2628
|
+
"a name asked of Bind::lookup in the spelling its host stores, and one it \
|
|
2629
|
+
does not hold, must come back as given"
|
|
2630
|
+
);
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// macOS volumes fold case and Unicode form unless formatted
|
|
2634
|
+
// case-sensitive, where every spelling is already the stored one.
|
|
2635
|
+
// @behavior FS-082
|
|
2636
|
+
#[cfg(target_os = "macos")]
|
|
2637
|
+
#[test]
|
|
2638
|
+
fn a_directory_spells_a_path_as_a_folding_host_stores_it() {
|
|
2639
|
+
let host = HostDir::new("folded");
|
|
2640
|
+
fs::create_dir(host.0.join("private")).expect("the host directory must be made");
|
|
2641
|
+
fs::write(host.0.join("private/caf\u{e9}"), "x").expect("the host file must be written");
|
|
2642
|
+
if !host.0.join("PRIVATE").exists() {
|
|
2643
|
+
eprintln!("the host volume matches names exactly; nothing folds to witness");
|
|
2644
|
+
return;
|
|
2645
|
+
}
|
|
2646
|
+
let directory = bind_host(&host.0);
|
|
2647
|
+
let private = directory
|
|
2648
|
+
.open_dir("PRIVATE")
|
|
2649
|
+
.expect("the host directory must open in any case");
|
|
2650
|
+
assert_eq!(
|
|
2651
|
+
(
|
|
2652
|
+
directory.lookup("Private"),
|
|
2653
|
+
private.lookup("CAFE\u{301}"),
|
|
2654
|
+
private.lookup("missing")
|
|
2655
|
+
),
|
|
2656
|
+
(
|
|
2657
|
+
Ok("private".to_string()),
|
|
2658
|
+
Ok("caf\u{e9}".to_string()),
|
|
2659
|
+
Ok("missing".to_string())
|
|
2660
|
+
),
|
|
2661
|
+
"a name asked of Bind::lookup in another case and Unicode form on a host \
|
|
2662
|
+
folding both must come back as the host stores it, one it does not hold as given"
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
}
|