kobako 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +241 -0
- data/Cargo.lock +132 -126
- data/README.md +240 -147
- data/ROADMAP.md +28 -22
- data/SECURITY.md +2 -2
- data/crates/kobako-runtime/CHANGELOG.md +117 -0
- data/crates/kobako-runtime/Cargo.toml +27 -6
- data/crates/kobako-runtime/README.md +16 -10
- data/crates/kobako-runtime/src/dispatch.rs +8 -0
- data/crates/kobako-runtime/src/error.rs +8 -9
- data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
- data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
- data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
- data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
- data/crates/kobako-runtime/src/filesystem.rs +392 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/lib.rs +12 -2
- data/crates/kobako-runtime/src/mount.rs +178 -0
- data/crates/kobako-runtime/src/profile.rs +4 -4
- data/crates/kobako-runtime/src/runtime.rs +43 -17
- data/crates/kobako-runtime/src/snapshot.rs +33 -15
- data/crates/kobako-runtime/src/yielder.rs +14 -6
- data/crates/kobako-transport/CHANGELOG.md +45 -0
- data/crates/kobako-transport/Cargo.toml +1 -1
- data/crates/kobako-transport/README.md +21 -12
- data/crates/kobako-transport/src/abi.rs +11 -14
- data/crates/kobako-transport/src/envelope/bytes.rs +18 -6
- data/crates/kobako-transport/src/envelope/compile_reply.rs +139 -0
- data/crates/kobako-transport/src/envelope/fault.rs +2 -1
- data/crates/kobako-transport/src/envelope/outcome.rs +17 -16
- data/crates/kobako-transport/src/envelope/preamble.rs +513 -0
- data/crates/kobako-transport/src/envelope/reply.rs +103 -61
- data/crates/kobako-transport/src/envelope/request.rs +305 -0
- data/crates/kobako-transport/src/envelope/run.rs +1 -2
- data/crates/kobako-transport/src/envelope/tag.rs +49 -0
- data/crates/kobako-transport/src/envelope/unsupported.rs +105 -0
- data/crates/kobako-transport/src/envelope.rs +19 -12
- data/crates/kobako-transport/src/lib.rs +0 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +163 -0
- data/crates/kobako-wasmtime/Cargo.toml +23 -16
- data/crates/kobako-wasmtime/README.md +6 -4
- data/crates/kobako-wasmtime/src/abi.rs +67 -36
- data/crates/kobako-wasmtime/src/ambient.rs +64 -5
- data/crates/kobako-wasmtime/src/cache.rs +204 -127
- data/crates/kobako-wasmtime/src/capture.rs +4 -11
- data/crates/kobako-wasmtime/src/config.rs +69 -15
- data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
- data/crates/kobako-wasmtime/src/dispatch.rs +95 -87
- data/crates/kobako-wasmtime/src/driver.rs +174 -125
- data/crates/kobako-wasmtime/src/exports.rs +88 -34
- data/crates/kobako-wasmtime/src/guest_mem.rs +151 -138
- data/crates/kobako-wasmtime/src/imports.rs +46 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +19 -58
- data/crates/kobako-wasmtime/src/invocation.rs +84 -412
- data/crates/kobako-wasmtime/src/io.rs +89 -0
- data/crates/kobako-wasmtime/src/lib.rs +7 -24
- data/crates/kobako-wasmtime/src/limiter.rs +253 -0
- data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
- data/crates/kobako-wasmtime/src/preview1.rs +461 -0
- data/crates/kobako-wasmtime/src/trap.rs +66 -59
- data/crates/kobako-wasmtime/src/wasi.rs +317 -0
- data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
- data/crates/kobako-wasmtime/tests/preamble_flags.rs +70 -0
- data/crates/kobako-wasmtime/tests/snippet_compile.rs +95 -0
- data/crates/kobako-wasmtime/tests/support/mod.rs +30 -0
- data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/unsupported_call.rs +51 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +177 -113
- data/ext/kobako/src/runtime/errors.rs +26 -32
- data/ext/kobako/src/runtime/escape.rs +53 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
- data/ext/kobako/src/runtime/filesystem.rs +236 -0
- data/ext/kobako/src/runtime/gvl.rs +11 -13
- 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 +190 -312
- data/lib/kobako/capture.rb +1 -4
- data/lib/kobako/catalog/extensions.rb +21 -59
- data/lib/kobako/catalog/flags.rb +29 -0
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +7 -54
- data/lib/kobako/catalog/mount.rb +27 -0
- data/lib/kobako/catalog/services.rb +17 -60
- data/lib/kobako/catalog/snippets.rb +41 -69
- data/lib/kobako/catalog.rb +13 -10
- data/lib/kobako/codec/decoder.rb +10 -28
- data/lib/kobako/codec/encoder.rb +3 -22
- data/lib/kobako/codec/error.rb +6 -27
- data/lib/kobako/codec/ext_types.rb +72 -94
- data/lib/kobako/codec/handle_walk.rb +65 -130
- data/lib/kobako/codec/nesting.rb +3 -8
- data/lib/kobako/codec/state.rb +21 -11
- data/lib/kobako/codec/utils.rb +6 -34
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +10 -22
- data/lib/kobako/constant_name.rb +8 -0
- data/lib/kobako/context.rb +83 -117
- data/lib/kobako/errors.rb +57 -132
- data/lib/kobako/execution.rb +16 -21
- data/lib/kobako/extension.rb +23 -40
- data/lib/kobako/filesystem.rb +38 -0
- data/lib/kobako/handle.rb +7 -12
- data/lib/kobako/outcome.rb +51 -80
- data/lib/kobako/payload/arguments.rb +1 -16
- data/lib/kobako/payload.rb +4 -12
- data/lib/kobako/pool.rb +25 -55
- data/lib/kobako/runtime.rb +7 -21
- data/lib/kobako/sandbox.rb +128 -137
- data/lib/kobako/sandbox_options.rb +71 -90
- data/lib/kobako/snippet/bytecode.rb +11 -0
- data/lib/kobako/snippet/source.rb +1 -16
- data/lib/kobako/snippet.rb +2 -10
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/call.rb +1 -14
- data/lib/kobako/transport/dispatcher.rb +127 -206
- data/lib/kobako/transport/error.rb +6 -20
- data/lib/kobako/transport/exposure.rb +10 -17
- data/lib/kobako/transport/reflection.rb +108 -63
- data/lib/kobako/transport/run.rb +17 -71
- data/lib/kobako/transport/yielder.rb +20 -94
- data/lib/kobako/transport.rb +4 -15
- data/lib/kobako/unresolved.rb +4 -11
- data/lib/kobako/usage.rb +2 -6
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +1 -0
- data/sig/kobako/catalog/extensions.rbs +2 -2
- data/sig/kobako/catalog/flags.rbs +13 -0
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/mount.rbs +28 -0
- data/sig/kobako/catalog/services.rbs +3 -5
- data/sig/kobako/catalog/snippets.rbs +5 -5
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/ext_types.rbs +10 -8
- data/sig/kobako/codec/handle_walk.rbs +9 -7
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +2 -2
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +4 -2
- data/sig/kobako/constant_name.rbs +3 -0
- data/sig/kobako/context.rbs +15 -8
- data/sig/kobako/errors.rbs +10 -4
- data/sig/kobako/filesystem.rbs +88 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/outcome.rbs +6 -8
- data/sig/kobako/pool.rbs +1 -1
- data/sig/kobako/runtime.rbs +21 -45
- data/sig/kobako/sandbox.rbs +11 -1
- data/sig/kobako/sandbox_options.rbs +25 -7
- 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 +15 -19
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +19 -4
- data/sig/kobako/transport/run.rbs +1 -3
- data/sig/kobako/transport/yielder.rbs +5 -9
- data/sig/kobako/transport.rbs +9 -8
- metadata +75 -10
- data/crates/kobako-transport/src/envelope/invocation_frames.rs +0 -241
- data/crates/kobako-wasmtime/src/frames.rs +0 -273
- data/lib/kobako/snippet/binary.rb +0 -26
- data/rust-toolchain.toml +0 -15
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
//! `Directory`: a Filesystem over a host directory, from which nothing
|
|
2
|
+
//! outside it is reachable.
|
|
3
|
+
//!
|
|
4
|
+
//! Every path resolves through `cap_std::fs::Dir`, which refuses a `..`
|
|
5
|
+
//! or a symbolic link that would lead out of the directory, so the
|
|
6
|
+
//! confinement holds whatever path a caller hands over.
|
|
7
|
+
|
|
8
|
+
mod spelling;
|
|
9
|
+
|
|
10
|
+
use std::io::{self, Read, Seek, SeekFrom, Write};
|
|
11
|
+
use std::path::{Component, Path};
|
|
12
|
+
use std::sync::Arc;
|
|
13
|
+
|
|
14
|
+
use cap_fs_ext::{DirExt, FollowSymlinks, OpenOptionsFollowExt};
|
|
15
|
+
use cap_std::ambient_authority;
|
|
16
|
+
use cap_std::fs::{Dir, OpenOptions as CapOpenOptions};
|
|
17
|
+
|
|
18
|
+
use super::path::{components, name, parent};
|
|
19
|
+
use super::{
|
|
20
|
+
DirEntry, File, FileType, Filesystem, FilesystemError, Hold, Metadata, OpenOptions, Rename,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/// A host directory and everything beneath it. What a guest writes lands
|
|
24
|
+
/// on the host and lasts as long as the directory does.
|
|
25
|
+
pub struct Directory {
|
|
26
|
+
dir: Arc<Dir>,
|
|
27
|
+
/// Whether a path may pass through a link, which a Directory for a
|
|
28
|
+
/// Policy follows nowhere on it.
|
|
29
|
+
follows_links: bool,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Directory {
|
|
33
|
+
/// Open the host directory at `path`, the one place this Filesystem
|
|
34
|
+
/// reaches outside itself.
|
|
35
|
+
pub fn new(path: impl AsRef<Path>) -> io::Result<Self> {
|
|
36
|
+
Ok(Self {
|
|
37
|
+
dir: Arc::new(Dir::open_ambient_dir(path, ambient_authority())?),
|
|
38
|
+
follows_links: true,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Answer `call` with the directory holding `path`'s last name and
|
|
43
|
+
/// that name. A Directory for a Policy reaches that directory one name
|
|
44
|
+
/// at a time from the one it already holds, following no link, so a
|
|
45
|
+
/// link swapped in on the way is a loop rather than a path elsewhere.
|
|
46
|
+
fn at<T>(
|
|
47
|
+
&self,
|
|
48
|
+
path: &str,
|
|
49
|
+
call: impl FnOnce(&Dir, &str) -> Result<T, FilesystemError>,
|
|
50
|
+
) -> Result<T, FilesystemError> {
|
|
51
|
+
if self.follows_links {
|
|
52
|
+
return call(&self.dir, to_host_path(path)?);
|
|
53
|
+
}
|
|
54
|
+
let mut held: Option<Dir> = None;
|
|
55
|
+
for component in components(parent(path)) {
|
|
56
|
+
let from = held.as_ref().unwrap_or(&self.dir);
|
|
57
|
+
held = Some(open_dir_nofollow(from, to_host_path(component)?)?);
|
|
58
|
+
}
|
|
59
|
+
call(
|
|
60
|
+
held.as_ref().unwrap_or(&self.dir),
|
|
61
|
+
to_host_path(name(path))?,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// The directory `name` in `dir`, a link there followed only where
|
|
66
|
+
/// this Directory follows links.
|
|
67
|
+
fn open_dir_in(&self, dir: &Dir, name: &str) -> Result<Dir, FilesystemError> {
|
|
68
|
+
if self.follows_links {
|
|
69
|
+
dir.open_dir(name).map_err(to_error)
|
|
70
|
+
} else {
|
|
71
|
+
open_dir_nofollow(dir, name)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// The directory `name` in `dir`, a link there refused as a loop, as
|
|
77
|
+
/// Linux's `RESOLVE_NO_SYMLINKS` refuses one, where the host names it no
|
|
78
|
+
/// directory.
|
|
79
|
+
fn open_dir_nofollow(dir: &Dir, name: &str) -> Result<Dir, FilesystemError> {
|
|
80
|
+
dir.open_dir_nofollow(name)
|
|
81
|
+
.map_err(|err| match dir.symlink_metadata(name) {
|
|
82
|
+
Ok(metadata) if metadata.file_type().is_symlink() => FilesystemError::FilesystemLoop,
|
|
83
|
+
_ => to_error(err),
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// `path` as `cap_std` takes it, the root being `.`. Windows reads `\` as
|
|
88
|
+
/// a separator too, so there a name holding one is refused rather than
|
|
89
|
+
/// reached as names a Mount's Policy never saw.
|
|
90
|
+
fn to_host_path(path: &str) -> Result<&str, FilesystemError> {
|
|
91
|
+
if cfg!(windows) && path.contains('\\') {
|
|
92
|
+
return Err(FilesystemError::InvalidInput);
|
|
93
|
+
}
|
|
94
|
+
Ok(if path.is_empty() { "." } else { path })
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// The FilesystemError a host failure names. cap-std's own refusal of a
|
|
98
|
+
/// path leading out of the directory carries no OS code, and answers as
|
|
99
|
+
/// not permitted, as wasmtime-wasi reads it. A link loop stays unnamed
|
|
100
|
+
/// until std's `FilesystemLoop` is stable.
|
|
101
|
+
fn to_error(err: io::Error) -> FilesystemError {
|
|
102
|
+
match err.kind() {
|
|
103
|
+
io::ErrorKind::PermissionDenied if err.raw_os_error().is_none() => {
|
|
104
|
+
FilesystemError::NotPermitted
|
|
105
|
+
}
|
|
106
|
+
io::ErrorKind::NotFound => FilesystemError::NotFound,
|
|
107
|
+
io::ErrorKind::AlreadyExists => FilesystemError::AlreadyExists,
|
|
108
|
+
io::ErrorKind::NotADirectory => FilesystemError::NotADirectory,
|
|
109
|
+
io::ErrorKind::IsADirectory => FilesystemError::IsADirectory,
|
|
110
|
+
io::ErrorKind::DirectoryNotEmpty => FilesystemError::DirectoryNotEmpty,
|
|
111
|
+
io::ErrorKind::PermissionDenied => FilesystemError::PermissionDenied,
|
|
112
|
+
io::ErrorKind::ReadOnlyFilesystem => FilesystemError::ReadOnlyFilesystem,
|
|
113
|
+
io::ErrorKind::InvalidInput => FilesystemError::InvalidInput,
|
|
114
|
+
io::ErrorKind::Unsupported => FilesystemError::Unsupported,
|
|
115
|
+
io::ErrorKind::StorageFull => FilesystemError::StorageFull,
|
|
116
|
+
io::ErrorKind::FileTooLarge => FilesystemError::FileTooLarge,
|
|
117
|
+
io::ErrorKind::CrossesDevices => FilesystemError::CrossesDevices,
|
|
118
|
+
io::ErrorKind::Deadlock => FilesystemError::Deadlock,
|
|
119
|
+
io::ErrorKind::ResourceBusy => FilesystemError::ResourceBusy,
|
|
120
|
+
io::ErrorKind::InvalidFilename => FilesystemError::InvalidFilename,
|
|
121
|
+
io::ErrorKind::NotSeekable => FilesystemError::NotSeekable,
|
|
122
|
+
io::ErrorKind::Interrupted => FilesystemError::Interrupted,
|
|
123
|
+
io::ErrorKind::OutOfMemory => FilesystemError::OutOfMemory,
|
|
124
|
+
_ => FilesystemError::Other(err.to_string()),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn to_file_type(host: cap_std::fs::FileType) -> FileType {
|
|
129
|
+
if host.is_symlink() {
|
|
130
|
+
FileType::Symlink
|
|
131
|
+
} else if host.is_dir() {
|
|
132
|
+
FileType::Directory
|
|
133
|
+
} else if host.is_file() {
|
|
134
|
+
FileType::File
|
|
135
|
+
} else {
|
|
136
|
+
FileType::Unknown
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// What a host entry is, a file's size alone counted: a link reports no
|
|
141
|
+
/// size, so nothing of where it points reaches the guest. The host names
|
|
142
|
+
/// its identity on unix; elsewhere a Directory reports none.
|
|
143
|
+
fn to_metadata(metadata: cap_std::fs::Metadata) -> Metadata {
|
|
144
|
+
let file_type = to_file_type(metadata.file_type());
|
|
145
|
+
let len = if file_type == FileType::File {
|
|
146
|
+
metadata.len()
|
|
147
|
+
} else {
|
|
148
|
+
0
|
|
149
|
+
};
|
|
150
|
+
#[cfg(unix)]
|
|
151
|
+
{
|
|
152
|
+
use cap_std::fs::MetadataExt;
|
|
153
|
+
Metadata::new(file_type, len).with_identity(metadata.dev(), metadata.ino())
|
|
154
|
+
}
|
|
155
|
+
#[cfg(not(unix))]
|
|
156
|
+
Metadata::new(file_type, len)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Open `name` in `dir` as `options` asks.
|
|
160
|
+
fn open_in(dir: &Dir, name: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
|
|
161
|
+
// The host creates only for a writer, so a file already there is
|
|
162
|
+
// opened as asked rather than for writing it was never given.
|
|
163
|
+
if options.create && !(options.create_new || options.write || options.truncate) {
|
|
164
|
+
let existing = OpenOptions {
|
|
165
|
+
create: false,
|
|
166
|
+
..*options
|
|
167
|
+
};
|
|
168
|
+
match open_in(dir, name, &existing) {
|
|
169
|
+
Err(FilesystemError::NotFound) => {}
|
|
170
|
+
opened => return opened,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let creates = options.create || options.create_new || options.truncate;
|
|
174
|
+
let writes = options.write || creates;
|
|
175
|
+
let mut open = CapOpenOptions::new();
|
|
176
|
+
// The host refuses an open for neither reading nor writing, and one
|
|
177
|
+
// that creates or truncates without writing; the runtime decides
|
|
178
|
+
// what is read and written, so such opens read or write here.
|
|
179
|
+
open.read(options.read || !writes)
|
|
180
|
+
.write(writes)
|
|
181
|
+
.create(options.create)
|
|
182
|
+
.create_new(options.create_new)
|
|
183
|
+
.truncate(options.truncate);
|
|
184
|
+
// Only a file or a directory opens: a FIFO would wait for a peer
|
|
185
|
+
// and a device would act on being opened. The host could swap an
|
|
186
|
+
// entry in between, so what opened is checked again before its
|
|
187
|
+
// first use, the open itself never waiting for a peer, nor following
|
|
188
|
+
// a link swapped in where none was asked to be followed.
|
|
189
|
+
let found = if options.nofollow {
|
|
190
|
+
open.follow(FollowSymlinks::No);
|
|
191
|
+
dir.symlink_metadata(name)
|
|
192
|
+
} else {
|
|
193
|
+
dir.metadata(name)
|
|
194
|
+
};
|
|
195
|
+
match found.map(|metadata| to_file_type(metadata.file_type())) {
|
|
196
|
+
Ok(FileType::Symlink) => return Err(FilesystemError::FilesystemLoop),
|
|
197
|
+
Ok(FileType::Unknown) => return Err(FilesystemError::PermissionDenied),
|
|
198
|
+
_ => {}
|
|
199
|
+
}
|
|
200
|
+
#[cfg(unix)]
|
|
201
|
+
{
|
|
202
|
+
use cap_std::fs::OpenOptionsExt;
|
|
203
|
+
use rustix::fs::OFlags;
|
|
204
|
+
open.custom_flags((OFlags::NONBLOCK | OFlags::NOCTTY).bits() as i32);
|
|
205
|
+
}
|
|
206
|
+
let file = dir.open_with(name, &open).map_err(to_error)?;
|
|
207
|
+
match to_file_type(file.metadata().map_err(to_error)?.file_type()) {
|
|
208
|
+
FileType::File => {}
|
|
209
|
+
FileType::Directory => return Err(FilesystemError::IsADirectory),
|
|
210
|
+
_ => return Err(FilesystemError::PermissionDenied),
|
|
211
|
+
}
|
|
212
|
+
#[cfg(unix)]
|
|
213
|
+
{
|
|
214
|
+
use rustix::fs::{fcntl_getfl, fcntl_setfl, OFlags};
|
|
215
|
+
let flags = fcntl_getfl(&file)
|
|
216
|
+
.map_err(io::Error::from)
|
|
217
|
+
.map_err(to_error)?;
|
|
218
|
+
fcntl_setfl(&file, flags - OFlags::NONBLOCK)
|
|
219
|
+
.map_err(io::Error::from)
|
|
220
|
+
.map_err(to_error)?;
|
|
221
|
+
}
|
|
222
|
+
Ok(Box::new(DirectoryFile {
|
|
223
|
+
file: file.into_std(),
|
|
224
|
+
}))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
impl Filesystem for Directory {
|
|
228
|
+
/// A Directory for a Policy follows no link at the end either.
|
|
229
|
+
fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
|
|
230
|
+
let options = OpenOptions {
|
|
231
|
+
nofollow: options.nofollow || !self.follows_links,
|
|
232
|
+
..*options
|
|
233
|
+
};
|
|
234
|
+
self.at(path, |dir, name| open_in(dir, name, &options))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// A Directory for a Policy answers a link at the end as a loop rather
|
|
238
|
+
/// than follow it.
|
|
239
|
+
fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
240
|
+
if !self.follows_links {
|
|
241
|
+
let metadata = self.symlink_metadata(path)?;
|
|
242
|
+
if metadata.file_type == FileType::Symlink {
|
|
243
|
+
return Err(FilesystemError::FilesystemLoop);
|
|
244
|
+
}
|
|
245
|
+
return Ok(metadata);
|
|
246
|
+
}
|
|
247
|
+
self.at(path, |dir, name| {
|
|
248
|
+
Ok(to_metadata(dir.metadata(name).map_err(to_error)?))
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
|
|
253
|
+
self.at(path, |dir, name| {
|
|
254
|
+
Ok(to_metadata(dir.symlink_metadata(name).map_err(to_error)?))
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// The host directory's own descriptor, so its inode stays taken.
|
|
259
|
+
/// Elsewhere a Directory reports no identity, so it holds nothing.
|
|
260
|
+
fn hold_directory(&self, path: &str) -> Result<Option<Hold>, FilesystemError> {
|
|
261
|
+
if cfg!(not(unix)) {
|
|
262
|
+
return Ok(None);
|
|
263
|
+
}
|
|
264
|
+
let dir = self.at(path, |dir, name| self.open_dir_in(dir, name))?;
|
|
265
|
+
Ok(Some(Hold::new(dir)))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
|
|
269
|
+
let mut parent = Some(self.dir.try_clone().map_err(to_error)?);
|
|
270
|
+
let mut stored = Vec::new();
|
|
271
|
+
let mut names = components(path).peekable();
|
|
272
|
+
while let Some(name) = names.next() {
|
|
273
|
+
let found = match &parent {
|
|
274
|
+
Some(dir) => spelling::stored_name(dir, to_host_path(name)?).map_err(to_error)?,
|
|
275
|
+
None => None,
|
|
276
|
+
};
|
|
277
|
+
// The last name is spelled from its parent alone, so a link
|
|
278
|
+
// standing there is never followed to spell it.
|
|
279
|
+
parent = match (&parent, &found) {
|
|
280
|
+
(Some(dir), Some(found)) if names.peek().is_some() => {
|
|
281
|
+
match self.open_dir_in(dir, found) {
|
|
282
|
+
Ok(next) => Some(next),
|
|
283
|
+
Err(FilesystemError::NotFound | FilesystemError::NotADirectory) => None,
|
|
284
|
+
Err(err) => return Err(err),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
_ => None,
|
|
288
|
+
};
|
|
289
|
+
stored.push(found.unwrap_or_else(|| name.to_string()));
|
|
290
|
+
}
|
|
291
|
+
Ok(stored.join("/"))
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/// A Directory for a Policy follows no link, so a path resolves as
|
|
295
|
+
/// given.
|
|
296
|
+
fn canonicalize_beneath(&self, base: &str, path: &str) -> Result<String, FilesystemError> {
|
|
297
|
+
if !self.follows_links {
|
|
298
|
+
return Ok(path.to_string());
|
|
299
|
+
}
|
|
300
|
+
let beneath = self.dir.open_dir(to_host_path(base)?).map_err(to_error)?;
|
|
301
|
+
let found = beneath
|
|
302
|
+
.canonicalize(to_host_path(path)?)
|
|
303
|
+
.map_err(to_error)?;
|
|
304
|
+
found
|
|
305
|
+
.components()
|
|
306
|
+
.filter_map(|component| match component {
|
|
307
|
+
Component::Normal(name) => Some(
|
|
308
|
+
name.to_str()
|
|
309
|
+
.map(str::to_string)
|
|
310
|
+
.ok_or(FilesystemError::IllegalSequence),
|
|
311
|
+
),
|
|
312
|
+
_ => None,
|
|
313
|
+
})
|
|
314
|
+
.collect::<Result<Vec<_>, _>>()
|
|
315
|
+
.map(|names| names.join("/"))
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
|
|
319
|
+
self.at(path, |dir, name| self.open_dir_in(dir, name))?
|
|
320
|
+
.entries()
|
|
321
|
+
.map_err(to_error)?
|
|
322
|
+
.map(|entry| {
|
|
323
|
+
let entry = entry.map_err(to_error)?;
|
|
324
|
+
let name = entry
|
|
325
|
+
.file_name()
|
|
326
|
+
.into_string()
|
|
327
|
+
.map_err(|_| FilesystemError::IllegalSequence)?;
|
|
328
|
+
Ok(DirEntry {
|
|
329
|
+
name,
|
|
330
|
+
file_type: to_file_type(entry.file_type().map_err(to_error)?),
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
.collect()
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
337
|
+
self.at(path, |dir, name| dir.create_dir(name).map_err(to_error))
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
|
|
341
|
+
self.at(path, |dir, name| dir.remove_dir(name).map_err(to_error))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// A directory is refused as one on every host, where the host itself
|
|
345
|
+
/// answers EISDIR on Linux but EPERM on macOS.
|
|
346
|
+
fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
|
|
347
|
+
self.at(path, |dir, name| {
|
|
348
|
+
if dir.symlink_metadata(name).is_ok_and(|m| m.is_dir()) {
|
|
349
|
+
return Err(FilesystemError::IsADirectory);
|
|
350
|
+
}
|
|
351
|
+
dir.remove_file(name).map_err(to_error)
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// A host directory takes no lock a Filesystem could check the source
|
|
356
|
+
/// under, so another process of the host's changing it in between is
|
|
357
|
+
/// the host's to rule out. A link moves as itself, as `rename(2)`
|
|
358
|
+
/// moves one.
|
|
359
|
+
fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
|
|
360
|
+
if from != to {
|
|
361
|
+
rename.check(self.symlink_metadata(from)?.file_type)?;
|
|
362
|
+
}
|
|
363
|
+
self.at(from, |from_dir, from_name| {
|
|
364
|
+
self.at(to, |to_dir, to_name| {
|
|
365
|
+
from_dir
|
|
366
|
+
.rename(from_name, to_dir, to_name)
|
|
367
|
+
.map_err(to_error)
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/// Every Invocation shares the host directory.
|
|
373
|
+
fn for_invocation(&self) -> Option<Arc<dyn Filesystem>> {
|
|
374
|
+
None
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// The same host directory, reached one name at a time.
|
|
378
|
+
fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
|
|
379
|
+
Some(Arc::new(Self {
|
|
380
|
+
dir: self.dir.clone(),
|
|
381
|
+
follows_links: false,
|
|
382
|
+
}))
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/// An open host file. The runtime names every offset, so each access seeks
|
|
387
|
+
/// first.
|
|
388
|
+
struct DirectoryFile {
|
|
389
|
+
file: std::fs::File,
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
impl File for DirectoryFile {
|
|
393
|
+
fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
|
|
394
|
+
self.file.seek(SeekFrom::Start(offset)).map_err(to_error)?;
|
|
395
|
+
self.file.read(buf).map_err(to_error)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
|
|
399
|
+
self.file.seek(SeekFrom::Start(offset)).map_err(to_error)?;
|
|
400
|
+
self.file.write(buf).map_err(to_error)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/// Windows has no append to switch one handle to for a single write,
|
|
404
|
+
/// so an append that could land over another writer's is refused.
|
|
405
|
+
#[cfg(not(unix))]
|
|
406
|
+
fn append(&mut self, _buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
|
|
407
|
+
Err(FilesystemError::Unsupported)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/// The host places the write at the file's end as `O_APPEND` does, so
|
|
411
|
+
/// another writer's append lands before or after it, never over it.
|
|
412
|
+
#[cfg(unix)]
|
|
413
|
+
fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
|
|
414
|
+
use rustix::fs::{fcntl_getfl, fcntl_setfl, OFlags};
|
|
415
|
+
|
|
416
|
+
let flags = fcntl_getfl(&self.file)
|
|
417
|
+
.map_err(io::Error::from)
|
|
418
|
+
.map_err(to_error)?;
|
|
419
|
+
fcntl_setfl(&self.file, flags | OFlags::APPEND)
|
|
420
|
+
.map_err(io::Error::from)
|
|
421
|
+
.map_err(to_error)?;
|
|
422
|
+
let written = self.file.write(buf);
|
|
423
|
+
let end = self.file.stream_position();
|
|
424
|
+
fcntl_setfl(&self.file, flags)
|
|
425
|
+
.map_err(io::Error::from)
|
|
426
|
+
.map_err(to_error)?;
|
|
427
|
+
let written = written.map_err(to_error)?;
|
|
428
|
+
Ok((end.map_err(to_error)? - written as u64, written))
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
fn metadata(&self) -> Result<Metadata, FilesystemError> {
|
|
432
|
+
let metadata = self.file.metadata().map_err(to_error)?;
|
|
433
|
+
Ok(Metadata::new(FileType::File, metadata.len()))
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
|
|
437
|
+
self.file.set_len(len).map_err(to_error)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
fn sync(&mut self) -> Result<(), FilesystemError> {
|
|
441
|
+
self.file.sync_all().map_err(to_error)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[cfg(test)]
|
|
446
|
+
mod tests {
|
|
447
|
+
use std::fs;
|
|
448
|
+
use std::path::PathBuf;
|
|
449
|
+
|
|
450
|
+
use super::*;
|
|
451
|
+
|
|
452
|
+
/// A fresh host directory of the test's own, removed when it drops.
|
|
453
|
+
struct HostDir(PathBuf);
|
|
454
|
+
|
|
455
|
+
impl HostDir {
|
|
456
|
+
fn new(name: &str) -> Self {
|
|
457
|
+
let path = std::env::temp_dir()
|
|
458
|
+
.join(format!("kobako-directory-{name}-{}", std::process::id()));
|
|
459
|
+
let _ = fs::remove_dir_all(&path);
|
|
460
|
+
fs::create_dir_all(&path).expect("the host directory must be created");
|
|
461
|
+
Self(path)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
impl Drop for HostDir {
|
|
466
|
+
fn drop(&mut self) {
|
|
467
|
+
let _ = fs::remove_dir_all(&self.0);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/// A host directory of the test's own holding the FIFO `pipe`.
|
|
472
|
+
#[cfg(unix)]
|
|
473
|
+
fn make_fifo_host(name: &str) -> HostDir {
|
|
474
|
+
let host = HostDir::new(name);
|
|
475
|
+
let made = std::process::Command::new("mkfifo")
|
|
476
|
+
.arg(host.0.join("pipe"))
|
|
477
|
+
.status()
|
|
478
|
+
.expect("mkfifo must run");
|
|
479
|
+
assert!(made.success(), "the FIFO must be made");
|
|
480
|
+
host
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Opening the FIFO with no writer would wait for one, so only an open
|
|
484
|
+
// refused before it waits answers within the deadline; `/dev/null`
|
|
485
|
+
// is a device every unix host holds.
|
|
486
|
+
// @behavior FS-090
|
|
487
|
+
#[cfg(unix)]
|
|
488
|
+
#[test]
|
|
489
|
+
fn a_directory_opens_only_a_file_or_a_directory() {
|
|
490
|
+
let host = make_fifo_host("fifo-open");
|
|
491
|
+
let fifo = Directory::new(&host.0).expect("the host directory must open");
|
|
492
|
+
let devices = Directory::new("/dev").expect("the device directory must open");
|
|
493
|
+
let (sender, answered) = std::sync::mpsc::channel();
|
|
494
|
+
std::thread::spawn(move || {
|
|
495
|
+
let read = OpenOptions {
|
|
496
|
+
read: true,
|
|
497
|
+
..OpenOptions::default()
|
|
498
|
+
};
|
|
499
|
+
let _ = sender.send([
|
|
500
|
+
fifo.open("pipe", &read).err(),
|
|
501
|
+
devices.open("null", &read).err(),
|
|
502
|
+
]);
|
|
503
|
+
});
|
|
504
|
+
assert_eq!(
|
|
505
|
+
answered.recv_timeout(std::time::Duration::from_secs(5)),
|
|
506
|
+
Ok([
|
|
507
|
+
Some(FilesystemError::PermissionDenied),
|
|
508
|
+
Some(FilesystemError::PermissionDenied)
|
|
509
|
+
]),
|
|
510
|
+
"a FIFO and a device opened through Directory::open must be refused as permission \
|
|
511
|
+
denied without waiting"
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// @behavior FS-091
|
|
516
|
+
#[cfg(unix)]
|
|
517
|
+
#[test]
|
|
518
|
+
fn a_directory_reports_a_fifo_as_of_an_unknown_type() {
|
|
519
|
+
let host = make_fifo_host("fifo-type");
|
|
520
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
521
|
+
let listed: Vec<_> = directory
|
|
522
|
+
.read_dir("")
|
|
523
|
+
.expect("the directory must list")
|
|
524
|
+
.into_iter()
|
|
525
|
+
.map(|entry| entry.file_type)
|
|
526
|
+
.collect();
|
|
527
|
+
assert_eq!(
|
|
528
|
+
(
|
|
529
|
+
directory
|
|
530
|
+
.metadata("pipe")
|
|
531
|
+
.map(|metadata| metadata.file_type),
|
|
532
|
+
directory
|
|
533
|
+
.symlink_metadata("pipe")
|
|
534
|
+
.map(|metadata| metadata.file_type),
|
|
535
|
+
listed
|
|
536
|
+
),
|
|
537
|
+
(
|
|
538
|
+
Ok(FileType::Unknown),
|
|
539
|
+
Ok(FileType::Unknown),
|
|
540
|
+
vec![FileType::Unknown]
|
|
541
|
+
),
|
|
542
|
+
"a FIFO looked up through Directory::metadata and symlink_metadata, and listed \
|
|
543
|
+
through read_dir, must answer an unknown type"
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// @behavior FS-005
|
|
548
|
+
#[test]
|
|
549
|
+
fn a_directory_serves_the_files_of_its_host_directory() {
|
|
550
|
+
let host = HostDir::new("serves");
|
|
551
|
+
fs::write(host.0.join("data.csv"), "a,b").expect("the host file must be written");
|
|
552
|
+
|
|
553
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
554
|
+
let mut file = directory
|
|
555
|
+
.open(
|
|
556
|
+
"data.csv",
|
|
557
|
+
&OpenOptions {
|
|
558
|
+
read: true,
|
|
559
|
+
..OpenOptions::default()
|
|
560
|
+
},
|
|
561
|
+
)
|
|
562
|
+
.expect("the host file must open");
|
|
563
|
+
let mut buf = [0; 16];
|
|
564
|
+
let read = file.read_at(&mut buf, 0).expect("the host file must read");
|
|
565
|
+
assert_eq!(
|
|
566
|
+
&buf[..read],
|
|
567
|
+
b"a,b",
|
|
568
|
+
"a host file read through a Directory's open must read back its content"
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
#[test]
|
|
573
|
+
fn an_open_for_neither_reading_nor_writing_still_opens() {
|
|
574
|
+
let host = HostDir::new("lookup");
|
|
575
|
+
fs::write(host.0.join("seen"), "x").expect("the host file must be written");
|
|
576
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
577
|
+
assert!(directory.open("seen", &OpenOptions::default()).is_ok());
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// @behavior FS-006
|
|
581
|
+
#[test]
|
|
582
|
+
fn a_directory_writes_into_its_host_directory() {
|
|
583
|
+
let host = HostDir::new("writes");
|
|
584
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
585
|
+
directory
|
|
586
|
+
.open(
|
|
587
|
+
"out.txt",
|
|
588
|
+
&OpenOptions {
|
|
589
|
+
write: true,
|
|
590
|
+
create: true,
|
|
591
|
+
..OpenOptions::default()
|
|
592
|
+
},
|
|
593
|
+
)
|
|
594
|
+
.expect("a new file must be created")
|
|
595
|
+
.write_at(b"written", 0)
|
|
596
|
+
.expect("the write must land");
|
|
597
|
+
assert_eq!(
|
|
598
|
+
fs::read(host.0.join("out.txt")).expect("the host file must exist"),
|
|
599
|
+
b"written",
|
|
600
|
+
"a file created through a Directory's open must land in the host directory"
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// The file is opened to write, so only the refusal keeps the append
|
|
605
|
+
// from taking the two steps another writer could land between.
|
|
606
|
+
// @behavior FS-093
|
|
607
|
+
#[cfg(windows)]
|
|
608
|
+
#[test]
|
|
609
|
+
fn a_directory_on_windows_refuses_to_append() {
|
|
610
|
+
let host = HostDir::new("append");
|
|
611
|
+
fs::write(host.0.join("f"), "x").expect("the host file must be written");
|
|
612
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
613
|
+
let mut file = directory
|
|
614
|
+
.open(
|
|
615
|
+
"f",
|
|
616
|
+
&OpenOptions {
|
|
617
|
+
write: true,
|
|
618
|
+
..OpenOptions::default()
|
|
619
|
+
},
|
|
620
|
+
)
|
|
621
|
+
.expect("the host file must open");
|
|
622
|
+
assert_eq!(
|
|
623
|
+
(
|
|
624
|
+
file.append(b"y").err(),
|
|
625
|
+
fs::read(host.0.join("f")).expect("the host file must read")
|
|
626
|
+
),
|
|
627
|
+
(Some(FilesystemError::Unsupported), b"x".to_vec()),
|
|
628
|
+
"an append through a Directory file on Windows must be refused as unsupported and \
|
|
629
|
+
leave the file as it was"
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// The host file exists beneath `d`, so only the refusal can keep the
|
|
634
|
+
// open from reaching it.
|
|
635
|
+
// @behavior FS-029
|
|
636
|
+
#[cfg(windows)]
|
|
637
|
+
#[test]
|
|
638
|
+
fn a_directory_on_windows_refuses_a_name_holding_a_backslash() {
|
|
639
|
+
let host = HostDir::new("backslash");
|
|
640
|
+
fs::create_dir_all(host.0.join("d")).expect("the host directory must be created");
|
|
641
|
+
fs::write(host.0.join("d").join("f"), "x").expect("the host file must be written");
|
|
642
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
643
|
+
assert!(
|
|
644
|
+
matches!(
|
|
645
|
+
directory.open(r"d\f", &OpenOptions::default()),
|
|
646
|
+
Err(FilesystemError::InvalidInput)
|
|
647
|
+
),
|
|
648
|
+
"a path holding a backslash opened through a Directory on Windows must be refused as \
|
|
649
|
+
invalid input"
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// The host answers ENAMETOOLONG, which only a mapping naming it keeps
|
|
654
|
+
// from reaching the guest as an EIO.
|
|
655
|
+
// @behavior FS-065
|
|
656
|
+
#[test]
|
|
657
|
+
fn a_directory_answers_a_host_failure_as_the_error_naming_it() {
|
|
658
|
+
let host = HostDir::new("names");
|
|
659
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
660
|
+
let created = directory.open(
|
|
661
|
+
&"x".repeat(300),
|
|
662
|
+
&OpenOptions {
|
|
663
|
+
write: true,
|
|
664
|
+
create: true,
|
|
665
|
+
..OpenOptions::default()
|
|
666
|
+
},
|
|
667
|
+
);
|
|
668
|
+
assert_eq!(
|
|
669
|
+
created.map(drop),
|
|
670
|
+
Err(FilesystemError::InvalidFilename),
|
|
671
|
+
"a name past the host's limit created through Directory::open must fail as \
|
|
672
|
+
InvalidFilename, not as an unnamed failure"
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Which offset a host refuses depends on its filesystem: tmpfs, XFS and
|
|
677
|
+
// overlay keep files up to `i64::MAX`. The host's own EFBIG, 27 on Linux
|
|
678
|
+
// and macOS alike, is what the Directory must name.
|
|
679
|
+
// @behavior FS-068
|
|
680
|
+
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
|
681
|
+
#[test]
|
|
682
|
+
fn a_directory_answers_a_host_file_too_large_as_too_large() {
|
|
683
|
+
assert_eq!(
|
|
684
|
+
to_error(io::Error::from_raw_os_error(27)),
|
|
685
|
+
FilesystemError::FileTooLarge,
|
|
686
|
+
"a host EFBIG through Directory's error mapping must fail as FileTooLarge, \
|
|
687
|
+
not as an unnamed failure"
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Only Linux keeps a name that is not UTF-8; macOS refuses to make one.
|
|
692
|
+
// @behavior FS-066
|
|
693
|
+
#[cfg(target_os = "linux")]
|
|
694
|
+
#[test]
|
|
695
|
+
fn a_directory_refuses_to_list_a_host_name_that_is_not_utf8() {
|
|
696
|
+
use std::os::unix::ffi::OsStrExt;
|
|
697
|
+
|
|
698
|
+
let host = HostDir::new("ilseq");
|
|
699
|
+
fs::write(host.0.join(std::ffi::OsStr::from_bytes(b"\xff")), "")
|
|
700
|
+
.expect("the host file must be written");
|
|
701
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
702
|
+
assert_eq!(
|
|
703
|
+
directory.read_dir("").map(drop),
|
|
704
|
+
Err(FilesystemError::IllegalSequence),
|
|
705
|
+
"a host name that is not UTF-8 listed through Directory::read_dir must fail as \
|
|
706
|
+
IllegalSequence, not as an unnamed failure"
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// The host file only reads, so a host open asking to write, as one that
|
|
711
|
+
// creates must, would be refused where nothing is to be created.
|
|
712
|
+
// @behavior FS-064
|
|
713
|
+
#[cfg(unix)]
|
|
714
|
+
#[test]
|
|
715
|
+
fn a_directory_opens_a_file_already_there_without_writing_to_create_it() {
|
|
716
|
+
use std::os::unix::fs::PermissionsExt;
|
|
717
|
+
let host = HostDir::new("create-existing");
|
|
718
|
+
fs::write(host.0.join("note"), "x").expect("the host file must be written");
|
|
719
|
+
fs::set_permissions(host.0.join("note"), fs::Permissions::from_mode(0o444))
|
|
720
|
+
.expect("the host file must become read-only");
|
|
721
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
722
|
+
let creates = OpenOptions {
|
|
723
|
+
read: true,
|
|
724
|
+
create: true,
|
|
725
|
+
..OpenOptions::default()
|
|
726
|
+
};
|
|
727
|
+
assert_eq!(
|
|
728
|
+
(
|
|
729
|
+
directory.open("note", &creates).map(drop),
|
|
730
|
+
directory.open("new", &creates).map(drop),
|
|
731
|
+
host.0.join("new").exists()
|
|
732
|
+
),
|
|
733
|
+
(Ok(()), Ok(()), true),
|
|
734
|
+
"a Directory::open that may create, of a read-only host file already there, must open \
|
|
735
|
+
it, and of a missing one must create it"
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// The link's target is a readable file, so only the refusal of the
|
|
740
|
+
// link itself keeps the open from reaching it.
|
|
741
|
+
// @behavior FS-047
|
|
742
|
+
#[cfg(unix)]
|
|
743
|
+
#[test]
|
|
744
|
+
fn a_directory_opens_no_link_an_open_asks_not_to_follow() {
|
|
745
|
+
let host = HostDir::new("nofollow");
|
|
746
|
+
fs::write(host.0.join("target"), "x").expect("the host file must be written");
|
|
747
|
+
std::os::unix::fs::symlink("target", host.0.join("link")).expect("the link must be made");
|
|
748
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
749
|
+
let open = |nofollow| {
|
|
750
|
+
directory
|
|
751
|
+
.open(
|
|
752
|
+
"link",
|
|
753
|
+
&OpenOptions {
|
|
754
|
+
read: true,
|
|
755
|
+
nofollow,
|
|
756
|
+
..OpenOptions::default()
|
|
757
|
+
},
|
|
758
|
+
)
|
|
759
|
+
.map(drop)
|
|
760
|
+
};
|
|
761
|
+
assert_eq!(
|
|
762
|
+
(open(true), open(false)),
|
|
763
|
+
(Err(FilesystemError::FilesystemLoop), Ok(())),
|
|
764
|
+
"a host link opened through a Directory must be refused when the open asks to follow \
|
|
765
|
+
none, and reached when it does not"
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// The target's five bytes would show as the link's size if a lookup
|
|
770
|
+
// that follows none still followed it.
|
|
771
|
+
// @behavior FS-048
|
|
772
|
+
#[cfg(unix)]
|
|
773
|
+
#[test]
|
|
774
|
+
fn a_directory_reports_a_link_a_lookup_does_not_follow_as_a_link() {
|
|
775
|
+
let host = HostDir::new("lstat");
|
|
776
|
+
fs::write(host.0.join("target"), "hello").expect("the host file must be written");
|
|
777
|
+
std::os::unix::fs::symlink("target", host.0.join("link")).expect("the link must be made");
|
|
778
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
779
|
+
let listed = directory
|
|
780
|
+
.read_dir("")
|
|
781
|
+
.expect("the host directory must list")
|
|
782
|
+
.into_iter()
|
|
783
|
+
.find(|entry| entry.name == "link")
|
|
784
|
+
.map(|entry| entry.file_type);
|
|
785
|
+
let shape = |metadata: Metadata| (metadata.file_type, metadata.len);
|
|
786
|
+
assert_eq!(
|
|
787
|
+
(
|
|
788
|
+
directory.symlink_metadata("link").map(shape),
|
|
789
|
+
listed,
|
|
790
|
+
directory.metadata("link").map(shape)
|
|
791
|
+
),
|
|
792
|
+
(
|
|
793
|
+
Ok((FileType::Symlink, 0)),
|
|
794
|
+
Some(FileType::Symlink),
|
|
795
|
+
Ok((FileType::File, 5))
|
|
796
|
+
),
|
|
797
|
+
"a host link looked up through Directory::symlink_metadata and listed through \
|
|
798
|
+
Directory::read_dir must report a link with no size, and Directory::metadata must \
|
|
799
|
+
report the file it points to"
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// @behavior FS-028
|
|
804
|
+
#[test]
|
|
805
|
+
fn a_directory_refuses_to_remove_a_directory_as_a_file() {
|
|
806
|
+
let host = HostDir::new("unlink-dir");
|
|
807
|
+
fs::create_dir(host.0.join("sub")).expect("the host directory must be created");
|
|
808
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
809
|
+
assert_eq!(
|
|
810
|
+
directory.remove_file("sub"),
|
|
811
|
+
Err(FilesystemError::IsADirectory),
|
|
812
|
+
"a host directory removed through Directory::remove_file must answer IsADirectory"
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Both links stay inside the host directory, so only resolving beneath
|
|
817
|
+
// `dir` rather than beneath the root can refuse the outer one.
|
|
818
|
+
// @behavior FS-097
|
|
819
|
+
#[cfg(unix)]
|
|
820
|
+
#[test]
|
|
821
|
+
fn a_directory_follows_a_link_only_within_the_directory_it_resolves_beneath() {
|
|
822
|
+
let host = HostDir::new("beneath");
|
|
823
|
+
fs::create_dir_all(host.0.join("dir/sub")).expect("the host directory must be created");
|
|
824
|
+
fs::write(host.0.join("dir/sub/y"), "inner").expect("the inner file must be written");
|
|
825
|
+
fs::write(host.0.join("secret"), "outer").expect("the outer file must be written");
|
|
826
|
+
std::os::unix::fs::symlink("sub", host.0.join("dir/in")).expect("the link must be made");
|
|
827
|
+
std::os::unix::fs::symlink("../secret", host.0.join("dir/up"))
|
|
828
|
+
.expect("the link must be made");
|
|
829
|
+
|
|
830
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
831
|
+
assert_eq!(
|
|
832
|
+
(
|
|
833
|
+
directory.canonicalize_beneath("dir", "in/y"),
|
|
834
|
+
directory.canonicalize_beneath("dir", "up"),
|
|
835
|
+
),
|
|
836
|
+
(Ok("sub/y".to_string()), Err(FilesystemError::NotPermitted)),
|
|
837
|
+
"a link staying within `dir`, resolved through Directory::canonicalize_beneath, must \
|
|
838
|
+
resolve to where it leads, and one leading out of `dir` must not be permitted"
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// The link leads to a directory inside the host directory, so only a
|
|
843
|
+
// Directory following no link refuses what a plain one reaches.
|
|
844
|
+
// @behavior FS-099
|
|
845
|
+
#[cfg(unix)]
|
|
846
|
+
#[test]
|
|
847
|
+
fn a_directory_for_a_policy_follows_no_link_on_a_path() {
|
|
848
|
+
let host = HostDir::new("for-policy");
|
|
849
|
+
fs::create_dir_all(host.0.join("target")).expect("the host directory must be created");
|
|
850
|
+
fs::write(host.0.join("target/x"), "inner").expect("the inner file must be written");
|
|
851
|
+
std::os::unix::fs::symlink("target", host.0.join("link")).expect("the link must be made");
|
|
852
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
853
|
+
let reading = OpenOptions {
|
|
854
|
+
read: true,
|
|
855
|
+
..OpenOptions::default()
|
|
856
|
+
};
|
|
857
|
+
let answers = |filesystem: &dyn Filesystem| {
|
|
858
|
+
(
|
|
859
|
+
filesystem.open("link/x", &reading).map(drop),
|
|
860
|
+
filesystem.read_dir("link").map(drop),
|
|
861
|
+
)
|
|
862
|
+
};
|
|
863
|
+
let for_policy = directory
|
|
864
|
+
.for_policy()
|
|
865
|
+
.expect("a Directory has a Filesystem for a Policy");
|
|
866
|
+
assert_eq!(
|
|
867
|
+
(answers(&directory), answers(for_policy.as_ref())),
|
|
868
|
+
(
|
|
869
|
+
(Ok(()), Ok(())),
|
|
870
|
+
(
|
|
871
|
+
Err(FilesystemError::FilesystemLoop),
|
|
872
|
+
Err(FilesystemError::FilesystemLoop)
|
|
873
|
+
),
|
|
874
|
+
),
|
|
875
|
+
"a path through a link opened, and the link listed, through a Directory's \
|
|
876
|
+
for_policy must each be a link loop, where the Directory itself reaches them"
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// @behavior FS-007
|
|
881
|
+
#[cfg(unix)]
|
|
882
|
+
#[test]
|
|
883
|
+
fn a_directory_reaches_nothing_outside_its_host_directory() {
|
|
884
|
+
let outside = HostDir::new("outside");
|
|
885
|
+
fs::write(outside.0.join("secret"), "host").expect("the outside file must be written");
|
|
886
|
+
let host = HostDir::new("confined");
|
|
887
|
+
std::os::unix::fs::symlink(outside.0.join("secret"), host.0.join("escape"))
|
|
888
|
+
.expect("the link must be created");
|
|
889
|
+
fs::write(host.0.join("inside"), "guest").expect("the inside file must be written");
|
|
890
|
+
std::os::unix::fs::symlink("inside", host.0.join("alias"))
|
|
891
|
+
.expect("the inside link must be created");
|
|
892
|
+
|
|
893
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
894
|
+
assert!(
|
|
895
|
+
directory
|
|
896
|
+
.open(
|
|
897
|
+
"alias",
|
|
898
|
+
&OpenOptions {
|
|
899
|
+
read: true,
|
|
900
|
+
..OpenOptions::default()
|
|
901
|
+
}
|
|
902
|
+
)
|
|
903
|
+
.is_ok(),
|
|
904
|
+
"a link that stays inside the host directory must still open through a Directory's open"
|
|
905
|
+
);
|
|
906
|
+
assert!(
|
|
907
|
+
directory
|
|
908
|
+
.open(
|
|
909
|
+
"escape",
|
|
910
|
+
&OpenOptions {
|
|
911
|
+
read: true,
|
|
912
|
+
..OpenOptions::default()
|
|
913
|
+
}
|
|
914
|
+
)
|
|
915
|
+
.is_err(),
|
|
916
|
+
"a link out of the host directory opened through a Directory's open must be refused"
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// @behavior FS-071
|
|
921
|
+
#[cfg(unix)]
|
|
922
|
+
#[test]
|
|
923
|
+
fn a_directory_reports_the_identity_its_host_gives_a_path() {
|
|
924
|
+
use std::os::unix::fs::MetadataExt;
|
|
925
|
+
|
|
926
|
+
let host = HostDir::new("identity");
|
|
927
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
928
|
+
let expected = fs::metadata(host.0.join("dir")).expect("the host must stat it");
|
|
929
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
930
|
+
let reported = directory.metadata("dir").expect("the directory must stand");
|
|
931
|
+
assert_eq!(
|
|
932
|
+
(reported.dev, reported.ino),
|
|
933
|
+
(expected.dev(), expected.ino()),
|
|
934
|
+
"a host directory's metadata through Directory::metadata must carry the device and \
|
|
935
|
+
inode numbers the host reports"
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Two handles racing one-byte appends: a two-step append (read the
|
|
940
|
+
// length, then write there) loses about half the bytes to the other
|
|
941
|
+
// handle's writes, so only an append the host places keeps them all.
|
|
942
|
+
// @behavior FS-069
|
|
943
|
+
#[cfg(unix)]
|
|
944
|
+
#[test]
|
|
945
|
+
fn a_directory_appends_at_its_host_files_end_in_one_step() {
|
|
946
|
+
use std::sync::Arc;
|
|
947
|
+
|
|
948
|
+
let host = HostDir::new("append");
|
|
949
|
+
fs::write(host.0.join("log.txt"), "").expect("the host file must be written");
|
|
950
|
+
let directory = Arc::new(Directory::new(&host.0).expect("the host directory must open"));
|
|
951
|
+
|
|
952
|
+
let writers: Vec<_> = [b"a", b"b"]
|
|
953
|
+
.into_iter()
|
|
954
|
+
.map(|byte| {
|
|
955
|
+
let directory = Arc::clone(&directory);
|
|
956
|
+
std::thread::spawn(move || {
|
|
957
|
+
let options = OpenOptions {
|
|
958
|
+
write: true,
|
|
959
|
+
..OpenOptions::default()
|
|
960
|
+
};
|
|
961
|
+
let mut file = directory
|
|
962
|
+
.open("log.txt", &options)
|
|
963
|
+
.expect("the host file must open to write");
|
|
964
|
+
for _ in 0..10_000 {
|
|
965
|
+
file.append(byte).expect("the append must succeed");
|
|
966
|
+
}
|
|
967
|
+
})
|
|
968
|
+
})
|
|
969
|
+
.collect();
|
|
970
|
+
for writer in writers {
|
|
971
|
+
writer.join().expect("the writer must finish");
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
let written = fs::read(host.0.join("log.txt")).expect("the host file must be read");
|
|
975
|
+
assert_eq!(
|
|
976
|
+
(
|
|
977
|
+
written.iter().filter(|&&b| b == b'a').count(),
|
|
978
|
+
written.iter().filter(|&&b| b == b'b').count()
|
|
979
|
+
),
|
|
980
|
+
(10_000, 10_000),
|
|
981
|
+
"two handles appending to one host file through a Directory's File::append must each \
|
|
982
|
+
keep every byte"
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// ext4 hands a removed directory's inode number to the next one made,
|
|
987
|
+
// so the number staying unused while held is what the Hold keeps. A
|
|
988
|
+
// host that never reuses numbers cannot show that, but every Linux
|
|
989
|
+
// host lists the removed directory among the process's descriptors
|
|
990
|
+
// while it is held.
|
|
991
|
+
// @behavior FS-086
|
|
992
|
+
#[cfg(target_os = "linux")]
|
|
993
|
+
#[test]
|
|
994
|
+
fn a_directory_held_keeps_its_inode_number_from_the_next_one_made() {
|
|
995
|
+
use std::os::unix::fs::MetadataExt;
|
|
996
|
+
|
|
997
|
+
let host = HostDir::new("held");
|
|
998
|
+
let ino = |path: &str| {
|
|
999
|
+
fs::metadata(host.0.join(path))
|
|
1000
|
+
.expect("it must stand")
|
|
1001
|
+
.ino()
|
|
1002
|
+
};
|
|
1003
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
1004
|
+
let first = ino("dir");
|
|
1005
|
+
fs::remove_dir(host.0.join("dir")).expect("the host directory must go");
|
|
1006
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
1007
|
+
let reuses = ino("dir") == first;
|
|
1008
|
+
let first = ino("dir");
|
|
1009
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
1010
|
+
let hold = directory
|
|
1011
|
+
.hold_directory("dir")
|
|
1012
|
+
.expect("the directory must be held");
|
|
1013
|
+
fs::remove_dir(host.0.join("dir")).expect("the host directory must go");
|
|
1014
|
+
let removed = format!("{} (deleted)", host.0.join("dir").display());
|
|
1015
|
+
let held_open = fs::read_dir("/proc/self/fd")
|
|
1016
|
+
.expect("the process's descriptors must list")
|
|
1017
|
+
.filter_map(|entry| fs::read_link(entry.ok()?.path()).ok())
|
|
1018
|
+
.any(|target| target.to_string_lossy() == removed);
|
|
1019
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
1020
|
+
let made = ino("dir");
|
|
1021
|
+
drop(hold);
|
|
1022
|
+
assert!(
|
|
1023
|
+
held_open,
|
|
1024
|
+
"a directory Directory::hold_directory holds must stay open once removed"
|
|
1025
|
+
);
|
|
1026
|
+
if !reuses {
|
|
1027
|
+
eprintln!("the host does not reuse inode numbers; nothing more to witness");
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
assert_ne!(
|
|
1031
|
+
made, first,
|
|
1032
|
+
"a directory made where Directory::hold_directory holds a removed one must take \
|
|
1033
|
+
another inode number on a host reusing them"
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// @behavior FS-081
|
|
1038
|
+
#[test]
|
|
1039
|
+
fn a_directory_spells_names_it_matches_exactly_as_given() {
|
|
1040
|
+
let host = HostDir::new("spelled");
|
|
1041
|
+
fs::create_dir(host.0.join("dir")).expect("the host directory must be made");
|
|
1042
|
+
fs::write(host.0.join("dir/file"), "x").expect("the host file must be written");
|
|
1043
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
1044
|
+
assert_eq!(
|
|
1045
|
+
(
|
|
1046
|
+
directory.stored_path("dir/file"),
|
|
1047
|
+
directory.stored_path("dir/new/name")
|
|
1048
|
+
),
|
|
1049
|
+
(Ok("dir/file".to_string()), Ok("dir/new/name".to_string())),
|
|
1050
|
+
"a path asked of Directory::stored_path in the spelling its host stores, and one \
|
|
1051
|
+
whose tail is missing, must come back as given"
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// macOS volumes fold case and Unicode form unless formatted
|
|
1056
|
+
// case-sensitive, where every spelling is already the stored one.
|
|
1057
|
+
// @behavior FS-082
|
|
1058
|
+
#[cfg(target_os = "macos")]
|
|
1059
|
+
#[test]
|
|
1060
|
+
fn a_directory_spells_a_path_as_a_folding_host_stores_it() {
|
|
1061
|
+
let host = HostDir::new("folded");
|
|
1062
|
+
fs::create_dir(host.0.join("private")).expect("the host directory must be made");
|
|
1063
|
+
fs::write(host.0.join("private/caf\u{e9}"), "x").expect("the host file must be written");
|
|
1064
|
+
if !host.0.join("PRIVATE").exists() {
|
|
1065
|
+
eprintln!("the host volume matches names exactly; nothing folds to witness");
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
let directory = Directory::new(&host.0).expect("the host directory must open");
|
|
1069
|
+
assert_eq!(
|
|
1070
|
+
(
|
|
1071
|
+
directory.stored_path("PRIVATE/CAFE\u{301}"),
|
|
1072
|
+
directory.stored_path("Private/missing")
|
|
1073
|
+
),
|
|
1074
|
+
(
|
|
1075
|
+
Ok("private/caf\u{e9}".to_string()),
|
|
1076
|
+
Ok("private/missing".to_string())
|
|
1077
|
+
),
|
|
1078
|
+
"a path asked of Directory::stored_path in another case and Unicode form on a host \
|
|
1079
|
+
folding both must come back as the host stores each name it holds"
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
}
|