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,1985 @@
|
|
|
1
|
+
//! `Tmpfs`: a Filesystem held in memory, living as long as the Host
|
|
2
|
+
//! App's object does.
|
|
3
|
+
|
|
4
|
+
use std::any::Any;
|
|
5
|
+
use std::collections::BTreeMap;
|
|
6
|
+
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
|
|
7
|
+
use std::sync::{Arc, Mutex, MutexGuard};
|
|
8
|
+
use std::time::SystemTime;
|
|
9
|
+
|
|
10
|
+
use super::path::{ancestors, name, normalize_seed};
|
|
11
|
+
use super::{
|
|
12
|
+
DentryOperations, DirEntry, FileOperations, FileSystemType, FileType, Filesystem,
|
|
13
|
+
FilesystemError, Iattr, InodeOperations, Kiocb, Kstat, Lookup, OpenHow, RenameFlag, SetTime,
|
|
14
|
+
};
|
|
15
|
+
use crate::vfs::{dir_emit_dots, generic_file_llseek, generic_read_dir, DirContext, Whence};
|
|
16
|
+
use crate::vfs::{Dentry, Identity, Inode, NAME_MAX};
|
|
17
|
+
use whiteout::Whiteout;
|
|
18
|
+
|
|
19
|
+
mod tmpfile;
|
|
20
|
+
mod whiteout;
|
|
21
|
+
mod xattr;
|
|
22
|
+
|
|
23
|
+
/// Files and directories held in tmpfs. Every Invocation it is handed to
|
|
24
|
+
/// shares it, so what one writes the next reads. What its files hold
|
|
25
|
+
/// together, and how many entries it holds, are bounded as a tmpfs mount
|
|
26
|
+
/// bounds its size and its inodes, so a guest cannot grow it past what
|
|
27
|
+
/// the Host App allowed. A directory opened beneath it is a Tmpfs too,
|
|
28
|
+
/// following that directory wherever it moves, as a tmpfs descriptor
|
|
29
|
+
/// follows its inode.
|
|
30
|
+
pub struct Tmpfs {
|
|
31
|
+
tree: Arc<Tree>,
|
|
32
|
+
dir: Arc<Dir>,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// What every directory of one Tmpfs shares. No lock spans it: the VFS
|
|
36
|
+
/// holds each directory a call changes and serializes moves between
|
|
37
|
+
/// directories, as Linux's tmpfs relies on `i_rwsem` and
|
|
38
|
+
/// `s_vfs_rename_mutex`.
|
|
39
|
+
struct Tree {
|
|
40
|
+
space: Arc<Space>,
|
|
41
|
+
inodes_follow_limit: AtomicBool,
|
|
42
|
+
/// The device number this Tmpfs reports, its own as each tmpfs mount
|
|
43
|
+
/// has its own, so it shares an identity with no other Filesystem.
|
|
44
|
+
dev: u64,
|
|
45
|
+
/// The inode number the next directory takes, so each one made has an
|
|
46
|
+
/// identity no other directory of this Tmpfs had.
|
|
47
|
+
next_ino: AtomicU64,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[derive(Clone)]
|
|
51
|
+
enum Node {
|
|
52
|
+
File(Arc<Mutex<Content>>),
|
|
53
|
+
Directory(Arc<Dir>),
|
|
54
|
+
/// An Overlay's whiteout, as tmpfs holds a character device 0:0.
|
|
55
|
+
Whiteout {
|
|
56
|
+
_inode: Arc<Whiteout>,
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl Node {
|
|
61
|
+
/// Mark an entry no directory holds any more, which a handle may outlive, as
|
|
62
|
+
/// `shmem_unlink` and `shmem_rmdir` drop its last links.
|
|
63
|
+
fn drop_name(&self) {
|
|
64
|
+
match self {
|
|
65
|
+
Self::File(data) => lock(data).names = 0,
|
|
66
|
+
Self::Directory(dir) => dir.state().removed = true,
|
|
67
|
+
Self::Whiteout { .. } => {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// A name of it came or went, as `simple_rename_timestamp` and
|
|
72
|
+
/// `shmem_unlink` move an inode's `st_ctime`.
|
|
73
|
+
fn touch_changed(&self) {
|
|
74
|
+
let now = SystemTime::now();
|
|
75
|
+
match self {
|
|
76
|
+
Self::File(data) => lock(data).times.ctime = now,
|
|
77
|
+
Self::Directory(dir) => dir.state().times.ctime = now,
|
|
78
|
+
Self::Whiteout { .. } => {}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn file_type(&self) -> FileType {
|
|
83
|
+
match self {
|
|
84
|
+
Self::File(_) => FileType::RegFile,
|
|
85
|
+
Self::Directory(_) => FileType::Dir,
|
|
86
|
+
Self::Whiteout { .. } => FileType::Chrdev,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Its entry in a listing of a Tmpfs on `dev`, named as a stat of it is.
|
|
91
|
+
fn dir_entry(&self, name: &str, dev: u64) -> DirEntry {
|
|
92
|
+
let entry = DirEntry::new(name, self.file_type());
|
|
93
|
+
match self {
|
|
94
|
+
Self::File(content) => {
|
|
95
|
+
let content = lock(content);
|
|
96
|
+
entry.with_identity(content.dev, content.ino)
|
|
97
|
+
}
|
|
98
|
+
Self::Directory(dir) => entry.with_identity(dev, dir.ino),
|
|
99
|
+
Self::Whiteout { .. } => entry,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// A directory and its inode number. Each entry is held under its own
|
|
105
|
+
/// name in its directory, as a tmpfs holds one name to each entry, so the
|
|
106
|
+
/// names it holds stay bounded by its inodes however its directories move.
|
|
107
|
+
struct Dir {
|
|
108
|
+
ino: u64,
|
|
109
|
+
/// What it counts against for as long as anything holds it, as tmpfs frees
|
|
110
|
+
/// an inode when the last reference to it goes.
|
|
111
|
+
space: Arc<Space>,
|
|
112
|
+
state: Mutex<State>,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
struct State {
|
|
116
|
+
entries: BTreeMap<String, Node>,
|
|
117
|
+
/// Each name by its stable position in a listing, as `simple_offset_ctx`
|
|
118
|
+
/// keeps one, so a listing going on reads the directory as it stands without
|
|
119
|
+
/// holding what it began with.
|
|
120
|
+
offsets: BTreeMap<u64, String>,
|
|
121
|
+
offset_of: BTreeMap<String, u64>,
|
|
122
|
+
next_offset: u64,
|
|
123
|
+
/// How many of `entries` are directories, as `inc_nlink` keeps a
|
|
124
|
+
/// directory's count, so no stat reads every entry.
|
|
125
|
+
subdirectories: u64,
|
|
126
|
+
/// Whether no directory holds it any more, so it counts no name.
|
|
127
|
+
removed: bool,
|
|
128
|
+
/// The directory's extended attributes, as tmpfs keeps the `trusted.*`
|
|
129
|
+
/// ones an Overlay marks a directory opaque with.
|
|
130
|
+
xattrs: BTreeMap<String, Vec<u8>>,
|
|
131
|
+
times: Times,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
impl State {
|
|
135
|
+
/// Hold `node` under `name`, answering what stood there. The name takes the
|
|
136
|
+
/// next position in a listing, as `simple_offset_add` hands one out.
|
|
137
|
+
fn hold(&mut self, name: &str, node: Node) -> Option<Node> {
|
|
138
|
+
self.subdirectories += u64::from(matches!(node, Node::Directory(_)));
|
|
139
|
+
let replaced = self.entries.insert(name.to_string(), node);
|
|
140
|
+
self.subdirectories -= u64::from(matches!(replaced, Some(Node::Directory(_))));
|
|
141
|
+
self.forget_offset(name);
|
|
142
|
+
self.offsets.insert(self.next_offset, name.to_string());
|
|
143
|
+
self.offset_of.insert(name.to_string(), self.next_offset);
|
|
144
|
+
self.next_offset += 1;
|
|
145
|
+
replaced
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Leave `name` out of a listing from now on, as `simple_offset_remove`.
|
|
149
|
+
fn forget_offset(&mut self, name: &str) {
|
|
150
|
+
if let Some(offset) = self.offset_of.remove(name) {
|
|
151
|
+
self.offsets.remove(&offset);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// The first entry a listing at `pos` reaches, and the position it stands at.
|
|
156
|
+
fn entry_from(&self, pos: u64) -> Option<(u64, &str, &Node)> {
|
|
157
|
+
let (offset, name) = self.offsets.range(pos..).next()?;
|
|
158
|
+
Some((*offset, name, self.entries.get(name)?))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Hold `node` under `name`, a change to the directory's content.
|
|
162
|
+
fn insert(&mut self, name: &str, node: Node) {
|
|
163
|
+
node.touch_changed();
|
|
164
|
+
if let Some(replaced) = self.hold(name, node) {
|
|
165
|
+
replaced.drop_name();
|
|
166
|
+
replaced.touch_changed();
|
|
167
|
+
}
|
|
168
|
+
self.times.touch();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Hold `node`, which keeps a name elsewhere, in place of what stands at
|
|
172
|
+
/// `name`, which does too, a change to the directory's content.
|
|
173
|
+
fn exchange(&mut self, name: &str, node: Node) {
|
|
174
|
+
node.touch_changed();
|
|
175
|
+
self.hold(name, node);
|
|
176
|
+
self.times.touch();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Take the entry `name` away, to stand elsewhere, a change to the
|
|
180
|
+
/// directory's content.
|
|
181
|
+
fn take(&mut self, name: &str) {
|
|
182
|
+
self.entries.remove(name).inspect(|taken| {
|
|
183
|
+
self.subdirectories -= u64::from(matches!(taken, Node::Directory(_)));
|
|
184
|
+
});
|
|
185
|
+
self.forget_offset(name);
|
|
186
|
+
self.times.touch();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Take the entry `name` away for good, a change to the directory's content.
|
|
190
|
+
fn remove(&mut self, name: &str) {
|
|
191
|
+
if let Some(removed) = self.entries.get(name).cloned() {
|
|
192
|
+
removed.drop_name();
|
|
193
|
+
removed.touch_changed();
|
|
194
|
+
}
|
|
195
|
+
self.take(name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// An entry's three times, as a tmpfs mounted `noatime` keeps them: reading
|
|
200
|
+
/// touches nothing, a change to its content moves `st_mtime` and `st_ctime`
|
|
201
|
+
/// together, and a change to its names or attributes `st_ctime` alone.
|
|
202
|
+
#[derive(Clone, Copy)]
|
|
203
|
+
struct Times {
|
|
204
|
+
atime: SystemTime,
|
|
205
|
+
mtime: SystemTime,
|
|
206
|
+
ctime: SystemTime,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
impl Times {
|
|
210
|
+
fn now() -> Self {
|
|
211
|
+
let now = SystemTime::now();
|
|
212
|
+
Self {
|
|
213
|
+
atime: now,
|
|
214
|
+
mtime: now,
|
|
215
|
+
ctime: now,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Its content changed.
|
|
220
|
+
fn touch(&mut self) {
|
|
221
|
+
let now = SystemTime::now();
|
|
222
|
+
self.mtime = now;
|
|
223
|
+
self.ctime = now;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/// Set the times `attr` names, if it names any, as `shmem_setattr` takes them
|
|
227
|
+
/// from `setattr_copy`, all at the one moment the change lands, which counts
|
|
228
|
+
/// as a change to the entry.
|
|
229
|
+
fn apply(&mut self, attr: &Iattr) {
|
|
230
|
+
if attr.atime.is_none() && attr.mtime.is_none() {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if let Some(set) = attr.atime {
|
|
234
|
+
self.atime = attr.time_of(set);
|
|
235
|
+
}
|
|
236
|
+
if let Some(set) = attr.mtime {
|
|
237
|
+
self.mtime = attr.time_of(set);
|
|
238
|
+
}
|
|
239
|
+
self.ctime = attr.time_of(SetTime::Now);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
fn report(self, stat: Kstat) -> Kstat {
|
|
243
|
+
stat.with_times(Some(self.atime), Some(self.mtime), Some(self.ctime))
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
impl Dir {
|
|
248
|
+
fn new(ino: u64, space: Arc<Space>) -> Arc<Self> {
|
|
249
|
+
Arc::new(Self {
|
|
250
|
+
ino,
|
|
251
|
+
space,
|
|
252
|
+
state: Mutex::new(State {
|
|
253
|
+
entries: BTreeMap::new(),
|
|
254
|
+
offsets: BTreeMap::new(),
|
|
255
|
+
offset_of: BTreeMap::new(),
|
|
256
|
+
next_offset: 2,
|
|
257
|
+
subdirectories: 0,
|
|
258
|
+
removed: false,
|
|
259
|
+
xattrs: BTreeMap::new(),
|
|
260
|
+
times: Times::now(),
|
|
261
|
+
}),
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// What the directory reports: two names and one more for each
|
|
266
|
+
/// directory it holds, as a tmpfs counts `.` and each `..` beneath it,
|
|
267
|
+
/// and a size of `BOGO_DIRENT_SIZE` for each of its entries and those
|
|
268
|
+
/// two, as a tmpfs sizes a directory. One removed counts no name.
|
|
269
|
+
fn getattr(&self, dev: u64) -> Kstat {
|
|
270
|
+
let state = self.state();
|
|
271
|
+
let size = (2 + state.entries.len() as u64) * BOGO_DIRENT_SIZE;
|
|
272
|
+
let nlink = match state.removed {
|
|
273
|
+
true => 0,
|
|
274
|
+
false => 2 + state.subdirectories,
|
|
275
|
+
};
|
|
276
|
+
state.times.report(
|
|
277
|
+
Kstat::new(FileType::Dir, size)
|
|
278
|
+
.with_identity(dev, self.ino)
|
|
279
|
+
.with_nlink(nlink),
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
fn state(&self) -> MutexGuard<'_, State> {
|
|
284
|
+
self.state
|
|
285
|
+
.lock()
|
|
286
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// What stands at `name` in this directory.
|
|
290
|
+
fn get(&self, name: &str) -> Result<Node, FilesystemError> {
|
|
291
|
+
self.state()
|
|
292
|
+
.entries
|
|
293
|
+
.get(name)
|
|
294
|
+
.cloned()
|
|
295
|
+
.ok_or(FilesystemError::NotFound)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/// Hand `nested` every directory this one holds, leaving it holding
|
|
299
|
+
/// nothing.
|
|
300
|
+
fn take_directories(&mut self, nested: &mut Vec<Arc<Dir>>) {
|
|
301
|
+
let state = self
|
|
302
|
+
.state
|
|
303
|
+
.get_mut()
|
|
304
|
+
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
|
305
|
+
for (_, node) in std::mem::take(&mut state.entries) {
|
|
306
|
+
if let Node::Directory(dir) = node {
|
|
307
|
+
nested.push(dir);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// A guest can stack directories as deep as its inodes reach, so the last
|
|
314
|
+
/// reference to one lets go of everything beneath it in a loop, as Linux's
|
|
315
|
+
/// `dput` walks up in one, and no depth outruns the thread's stack.
|
|
316
|
+
impl Drop for Dir {
|
|
317
|
+
fn drop(&mut self) {
|
|
318
|
+
let mut nested = Vec::new();
|
|
319
|
+
self.take_directories(&mut nested);
|
|
320
|
+
while let Some(dir) = nested.pop() {
|
|
321
|
+
if let Some(mut dir) = Arc::into_inner(dir) {
|
|
322
|
+
dir.take_directories(&mut nested);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
self.space.inodes.give(1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// What a Tmpfs holds: the bytes of its files, and its entries, each
|
|
330
|
+
/// file and directory and the root counting one as an inode does.
|
|
331
|
+
struct Space {
|
|
332
|
+
bytes: Budget,
|
|
333
|
+
inodes: Budget,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/// How much of one thing is used, and the most that may be.
|
|
337
|
+
struct Budget {
|
|
338
|
+
limit: AtomicUsize,
|
|
339
|
+
used: AtomicUsize,
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
impl Budget {
|
|
343
|
+
fn new(limit: Option<usize>) -> Self {
|
|
344
|
+
Self {
|
|
345
|
+
limit: AtomicUsize::new(limit.unwrap_or(usize::MAX)),
|
|
346
|
+
used: AtomicUsize::new(0),
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
fn set_limit(&self, limit: Option<usize>) {
|
|
351
|
+
self.limit
|
|
352
|
+
.store(limit.unwrap_or(usize::MAX), Ordering::Relaxed);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// Count `amount` more, refusing when it would pass the limit.
|
|
356
|
+
fn take(&self, amount: usize) -> Result<(), FilesystemError> {
|
|
357
|
+
let limit = self.limit.load(Ordering::Relaxed);
|
|
358
|
+
self.used
|
|
359
|
+
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |used| {
|
|
360
|
+
used.checked_add(amount).filter(|&total| total <= limit)
|
|
361
|
+
})
|
|
362
|
+
.map(drop)
|
|
363
|
+
.map_err(|_| FilesystemError::StorageFull)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// Count `amount` more without refusing, for what the Host App seeds.
|
|
367
|
+
fn add(&self, amount: usize) {
|
|
368
|
+
self.used.fetch_add(amount, Ordering::Relaxed);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
fn give(&self, amount: usize) {
|
|
372
|
+
self.used.fetch_sub(amount, Ordering::Relaxed);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// One file's bytes, counted against its Tmpfs's space, with the file's
|
|
377
|
+
/// inode, for as long as they live, which outlasts the file's name while
|
|
378
|
+
/// a handle holds them.
|
|
379
|
+
struct Content {
|
|
380
|
+
bytes: Vec<u8>,
|
|
381
|
+
space: Arc<Space>,
|
|
382
|
+
times: Times,
|
|
383
|
+
dev: u64,
|
|
384
|
+
ino: u64,
|
|
385
|
+
/// One while a directory holds it, as a Tmpfs holds no hard links.
|
|
386
|
+
names: u64,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
impl Content {
|
|
390
|
+
/// A file the Host App seeds, counted but never refused.
|
|
391
|
+
fn seed(bytes: Vec<u8>, tree: &Tree) -> Arc<Mutex<Self>> {
|
|
392
|
+
tree.space.bytes.add(bytes.len());
|
|
393
|
+
tree.space.inodes.add(1);
|
|
394
|
+
Self::new(bytes, tree)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/// An empty file the guest creates, refused once no inode is left.
|
|
398
|
+
fn create(tree: &Tree) -> Result<Arc<Mutex<Self>>, FilesystemError> {
|
|
399
|
+
tree.space.inodes.take(1)?;
|
|
400
|
+
Ok(Self::new(Vec::new(), tree))
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
fn new(bytes: Vec<u8>, tree: &Tree) -> Arc<Mutex<Self>> {
|
|
404
|
+
Arc::new(Mutex::new(Self {
|
|
405
|
+
bytes,
|
|
406
|
+
space: tree.space.clone(),
|
|
407
|
+
times: Times::now(),
|
|
408
|
+
dev: tree.dev,
|
|
409
|
+
ino: tree.next_ino.fetch_add(1, Ordering::Relaxed),
|
|
410
|
+
names: 1,
|
|
411
|
+
}))
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
fn getattr(&self) -> Kstat {
|
|
415
|
+
self.times.report(
|
|
416
|
+
Kstat::new(FileType::RegFile, self.bytes.len() as u64)
|
|
417
|
+
.with_identity(self.dev, self.ino)
|
|
418
|
+
.with_nlink(self.names),
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// Resize to `len`, refusing a growth past the space left or past what
|
|
423
|
+
/// can be allocated, rather than aborting the host process.
|
|
424
|
+
fn resize(&mut self, len: usize) -> Result<(), FilesystemError> {
|
|
425
|
+
let growth = len.saturating_sub(self.bytes.len());
|
|
426
|
+
if growth > 0 {
|
|
427
|
+
self.space.bytes.take(growth)?;
|
|
428
|
+
if self.bytes.try_reserve_exact(growth).is_err() {
|
|
429
|
+
self.space.bytes.give(growth);
|
|
430
|
+
return Err(FilesystemError::StorageFull);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
self.space.bytes.give(self.bytes.len().saturating_sub(len));
|
|
434
|
+
self.bytes.resize(len, 0);
|
|
435
|
+
self.bytes.shrink_to(len);
|
|
436
|
+
Ok(())
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
impl Drop for Content {
|
|
441
|
+
fn drop(&mut self) {
|
|
442
|
+
self.space.bytes.give(self.bytes.len());
|
|
443
|
+
self.space.inodes.give(1);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/// The inodes a limit of `bytes` allows unless given its own: one for
|
|
448
|
+
/// each 4 KiB page the bytes span, as a tmpfs counts one page to each
|
|
449
|
+
/// file, and one for the root.
|
|
450
|
+
fn inodes_for(bytes: Option<usize>) -> Option<usize> {
|
|
451
|
+
bytes.map(|bytes| bytes.div_ceil(PAGE).saturating_add(1))
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const PAGE: usize = 4096;
|
|
455
|
+
|
|
456
|
+
/// The size a tmpfs counts for each name a directory holds.
|
|
457
|
+
const BOGO_DIRENT_SIZE: u64 = 20;
|
|
458
|
+
|
|
459
|
+
impl Tmpfs {
|
|
460
|
+
/// The most bytes a Tmpfs's files hold unless given a limit of their
|
|
461
|
+
/// own: the size Docker gives a `--tmpfs` mount.
|
|
462
|
+
pub const DEFAULT_SIZE: usize = 64 * 1024 * 1024;
|
|
463
|
+
|
|
464
|
+
pub fn new() -> Self {
|
|
465
|
+
let space = Space {
|
|
466
|
+
bytes: Budget::new(Some(Self::DEFAULT_SIZE)),
|
|
467
|
+
inodes: Budget::new(inodes_for(Some(Self::DEFAULT_SIZE))),
|
|
468
|
+
};
|
|
469
|
+
space.inodes.add(1);
|
|
470
|
+
let space = Arc::new(space);
|
|
471
|
+
Self {
|
|
472
|
+
tree: Arc::new(Tree {
|
|
473
|
+
space: space.clone(),
|
|
474
|
+
inodes_follow_limit: AtomicBool::new(true),
|
|
475
|
+
dev: Kstat::unique_device(),
|
|
476
|
+
next_ino: AtomicU64::new(2),
|
|
477
|
+
}),
|
|
478
|
+
dir: Dir::new(1, space),
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/// A directory with an inode number no directory of this Tmpfs had.
|
|
483
|
+
fn new_directory(&self) -> Arc<Dir> {
|
|
484
|
+
Dir::new(
|
|
485
|
+
self.tree.next_ino.fetch_add(1, Ordering::Relaxed),
|
|
486
|
+
self.tree.space.clone(),
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/// This Tmpfs with its files holding at most `size` bytes together,
|
|
491
|
+
/// or without bound for `None`, as a tmpfs mount's `size` does. Unless
|
|
492
|
+
/// given `nr_inodes` of its own, its entries follow the size: one for
|
|
493
|
+
/// each 4 KiB it spans, and the root. What it was seeded with counts,
|
|
494
|
+
/// but is never refused.
|
|
495
|
+
pub fn with_size(self, size: Option<usize>) -> Self {
|
|
496
|
+
self.tree.space.bytes.set_limit(size);
|
|
497
|
+
if self.tree.inodes_follow_limit.load(Ordering::Relaxed) {
|
|
498
|
+
self.tree.space.inodes.set_limit(inodes_for(size));
|
|
499
|
+
}
|
|
500
|
+
self
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/// This Tmpfs holding at most `nr_inodes` entries, the root among them,
|
|
504
|
+
/// or without bound for `None`, as a tmpfs mount's `nr_inodes` does.
|
|
505
|
+
pub fn with_nr_inodes(self, nr_inodes: Option<usize>) -> Self {
|
|
506
|
+
self.tree.space.inodes.set_limit(nr_inodes);
|
|
507
|
+
self.tree
|
|
508
|
+
.inodes_follow_limit
|
|
509
|
+
.store(false, Ordering::Relaxed);
|
|
510
|
+
self
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/// The directory `dir` of this Tmpfs, as a Tmpfs of its own.
|
|
514
|
+
fn beneath(&self, dir: Arc<Dir>) -> Self {
|
|
515
|
+
Self {
|
|
516
|
+
tree: self.tree.clone(),
|
|
517
|
+
dir,
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
impl Default for Tmpfs {
|
|
523
|
+
fn default() -> Self {
|
|
524
|
+
Self::new()
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/// Why a list of files cannot seed a Tmpfs as given.
|
|
529
|
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
530
|
+
#[non_exhaustive]
|
|
531
|
+
pub enum SeedError {
|
|
532
|
+
/// A path naming the root, where no file can stand.
|
|
533
|
+
Root,
|
|
534
|
+
/// A file given twice.
|
|
535
|
+
Twice(String),
|
|
536
|
+
/// A path given as a file where another path needs a directory, or
|
|
537
|
+
/// the reverse.
|
|
538
|
+
FileAndDirectory(String),
|
|
539
|
+
/// A path naming `..`, which no runtime hands a Filesystem, so the
|
|
540
|
+
/// file would never be reached.
|
|
541
|
+
DotDot(String),
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
impl std::fmt::Display for SeedError {
|
|
545
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
546
|
+
match self {
|
|
547
|
+
Self::Root => write!(f, "a Tmpfs seeds no file at its root"),
|
|
548
|
+
Self::Twice(path) => write!(f, "a Tmpfs seeds {path} twice"),
|
|
549
|
+
Self::FileAndDirectory(path) => {
|
|
550
|
+
write!(f, "a Tmpfs seeds {path} as both a file and a directory")
|
|
551
|
+
}
|
|
552
|
+
Self::DotDot(path) => write!(f, "a Tmpfs seeds no path through `..`, got {path}"),
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
impl std::error::Error for SeedError {}
|
|
558
|
+
|
|
559
|
+
impl Tmpfs {
|
|
560
|
+
/// A Tmpfs seeded with files, each `(path, contents)`, its directories
|
|
561
|
+
/// created along the way, or why no Tmpfs holds the list as given.
|
|
562
|
+
pub fn try_from_iter<P, C, I>(files: I) -> Result<Self, SeedError>
|
|
563
|
+
where
|
|
564
|
+
P: AsRef<str>,
|
|
565
|
+
C: Into<Vec<u8>>,
|
|
566
|
+
I: IntoIterator<Item = (P, C)>,
|
|
567
|
+
{
|
|
568
|
+
let tmpfs = Self::new();
|
|
569
|
+
for (path, contents) in files {
|
|
570
|
+
let path = normalize_seed(path.as_ref());
|
|
571
|
+
if path.is_empty() {
|
|
572
|
+
return Err(SeedError::Root);
|
|
573
|
+
}
|
|
574
|
+
if path.split('/').any(|name| name == "..") {
|
|
575
|
+
return Err(SeedError::DotDot(path));
|
|
576
|
+
}
|
|
577
|
+
let mut dir = tmpfs.dir.clone();
|
|
578
|
+
for above in ancestors(&path) {
|
|
579
|
+
let standing = dir.state().entries.get(name(above)).cloned();
|
|
580
|
+
let next = standing.unwrap_or_else(|| {
|
|
581
|
+
tmpfs.tree.space.inodes.add(1);
|
|
582
|
+
let made = Node::Directory(tmpfs.new_directory());
|
|
583
|
+
dir.state().hold(name(above), made.clone());
|
|
584
|
+
made
|
|
585
|
+
});
|
|
586
|
+
dir = match next {
|
|
587
|
+
Node::Directory(inner) => inner,
|
|
588
|
+
Node::File(_) | Node::Whiteout { .. } => {
|
|
589
|
+
return Err(SeedError::FileAndDirectory(above.to_string()))
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
let mut state = dir.state();
|
|
594
|
+
match state.entries.get(name(&path)) {
|
|
595
|
+
Some(Node::Directory(_)) => return Err(SeedError::FileAndDirectory(path)),
|
|
596
|
+
Some(Node::File(_) | Node::Whiteout { .. }) => return Err(SeedError::Twice(path)),
|
|
597
|
+
None => {}
|
|
598
|
+
}
|
|
599
|
+
state.hold(
|
|
600
|
+
name(&path),
|
|
601
|
+
Node::File(Content::seed(contents.into(), &tmpfs.tree)),
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
Ok(tmpfs)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/// A Tmpfs seeded with files, each `(path, contents)`, its directories
|
|
609
|
+
/// created along the way.
|
|
610
|
+
///
|
|
611
|
+
/// # Panics
|
|
612
|
+
///
|
|
613
|
+
/// When `Tmpfs::try_from_iter` refuses the list: a list written into the
|
|
614
|
+
/// Host App's own code that no Tmpfs holds is a defect in that code.
|
|
615
|
+
impl<P: AsRef<str>, C: Into<Vec<u8>>> FromIterator<(P, C)> for Tmpfs {
|
|
616
|
+
fn from_iter<I: IntoIterator<Item = (P, C)>>(files: I) -> Self {
|
|
617
|
+
Self::try_from_iter(files).unwrap_or_else(|error| panic!("{error}"))
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
impl Tmpfs {
|
|
622
|
+
/// What `node`, an entry of this Tmpfs, reports of itself. A whiteout is
|
|
623
|
+
/// a character device 0:0, as tmpfs makes one.
|
|
624
|
+
fn node_getattr(&self, node: &Node) -> Kstat {
|
|
625
|
+
match node {
|
|
626
|
+
Node::File(data) => lock(data).getattr(),
|
|
627
|
+
Node::Directory(dir) => dir.getattr(self.tree.dev),
|
|
628
|
+
Node::Whiteout { .. } => Kstat::new(FileType::Chrdev, 0),
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/// The inode answering for `node`, an entry of this Tmpfs.
|
|
633
|
+
fn inode_of(&self, node: Node) -> Arc<Inode> {
|
|
634
|
+
let stat = self.node_getattr(&node);
|
|
635
|
+
let ops: Arc<dyn InodeOperations> = match node {
|
|
636
|
+
Node::Directory(dir) => Arc::new(self.beneath(dir)),
|
|
637
|
+
Node::File(data) => Arc::new(TmpfsRegular { data }),
|
|
638
|
+
Node::Whiteout { .. } => Arc::new(TmpfsWhiteout),
|
|
639
|
+
};
|
|
640
|
+
Inode::new(&stat, ops)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/// Put `node` at the name `dentry` carries and record it there, as
|
|
644
|
+
/// `shmem_mknod` calls `d_make_persistent`.
|
|
645
|
+
fn make(&self, dentry: &Dentry, node: Node) -> Result<(), FilesystemError> {
|
|
646
|
+
self.dir.state().insert(&dentry.name(), node.clone());
|
|
647
|
+
dentry.add(Some(self.inode_of(node)));
|
|
648
|
+
Ok(())
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
impl FileSystemType for Tmpfs {
|
|
653
|
+
fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
|
|
654
|
+
sb.set_default_d_op(Arc::new(TmpfsDentry));
|
|
655
|
+
sb.set_dontcache();
|
|
656
|
+
sb.d_make_root(Inode::new(&self.dir.getattr(self.tree.dev), self));
|
|
657
|
+
Ok(())
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/// A directory of a Tmpfs answers for the names in it. A file answers
|
|
662
|
+
/// through `TmpfsRegular`, and a whiteout, a character device 0:0 with no
|
|
663
|
+
/// driver to open it, through `TmpfsWhiteout`.
|
|
664
|
+
impl InodeOperations for Tmpfs {
|
|
665
|
+
fn getattr(&self, _dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
666
|
+
Ok(self.dir.getattr(self.tree.dev))
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
fn setattr(&self, _dentry: &Dentry, attr: &Iattr) -> Result<(), FilesystemError> {
|
|
670
|
+
self.dir.state().times.apply(attr);
|
|
671
|
+
Ok(())
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/// A Tmpfs matches names byte for byte and holds no links, and refuses a
|
|
675
|
+
/// name past `NAME_MAX`, as `simple_lookup` does.
|
|
676
|
+
fn lookup(&self, dentry: &Dentry) -> Result<Lookup, FilesystemError> {
|
|
677
|
+
let name = dentry.name();
|
|
678
|
+
if name.len() > NAME_MAX {
|
|
679
|
+
return Err(FilesystemError::InvalidFilename);
|
|
680
|
+
}
|
|
681
|
+
let node = match self.dir.get(&name) {
|
|
682
|
+
Ok(node) => node,
|
|
683
|
+
Err(FilesystemError::NotFound) => return Ok(Lookup::Absent),
|
|
684
|
+
Err(err) => return Err(err),
|
|
685
|
+
};
|
|
686
|
+
Ok(Lookup::Found(self.inode_of(node)))
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
fn create(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
690
|
+
let data = Content::create(&self.tree)?;
|
|
691
|
+
self.make(dentry, Node::File(data))
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
fn open(
|
|
695
|
+
&self,
|
|
696
|
+
_dentry: &Dentry,
|
|
697
|
+
_how: &OpenHow,
|
|
698
|
+
) -> Result<Box<dyn FileOperations>, FilesystemError> {
|
|
699
|
+
Ok(Box::new(TmpfsDir {
|
|
700
|
+
dir: self.dir.clone(),
|
|
701
|
+
dev: self.tree.dev,
|
|
702
|
+
}))
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
fn mkdir(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
706
|
+
self.tree.space.inodes.take(1)?;
|
|
707
|
+
self.make(dentry, Node::Directory(self.new_directory()))
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/// The VFS found a directory at the name, as `shmem_rmdir` leaves it to,
|
|
711
|
+
/// so only whether it holds anything is left to answer.
|
|
712
|
+
fn rmdir(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
713
|
+
let name = dentry.name();
|
|
714
|
+
let mut state = self.dir.state();
|
|
715
|
+
if let Some(Node::Directory(removed)) = state.entries.get(&name) {
|
|
716
|
+
if !removed.state().entries.is_empty() {
|
|
717
|
+
return Err(FilesystemError::DirectoryNotEmpty);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
state.remove(&name);
|
|
721
|
+
Ok(())
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/// The VFS found no directory at the name, as `shmem_unlink` leaves it to.
|
|
725
|
+
fn unlink(&self, dentry: &Dentry) -> Result<(), FilesystemError> {
|
|
726
|
+
self.dir.state().remove(&dentry.name());
|
|
727
|
+
Ok(())
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
fn rename(
|
|
731
|
+
&self,
|
|
732
|
+
from: &Dentry,
|
|
733
|
+
to_dir: &dyn InodeOperations,
|
|
734
|
+
to: &Dentry,
|
|
735
|
+
flag: Option<RenameFlag>,
|
|
736
|
+
) -> Result<(), FilesystemError> {
|
|
737
|
+
let target = (to_dir as &dyn Any)
|
|
738
|
+
.downcast_ref::<Self>()
|
|
739
|
+
.ok_or(FilesystemError::CrossesDevices)?;
|
|
740
|
+
self.rename_at(&from.name(), target, &to.name(), flag)
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/// A directory of a Tmpfs, open.
|
|
745
|
+
struct TmpfsDir {
|
|
746
|
+
dir: Arc<Dir>,
|
|
747
|
+
dev: u64,
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
impl FileOperations for TmpfsDir {
|
|
751
|
+
fn llseek(
|
|
752
|
+
&mut self,
|
|
753
|
+
dentry: &Dentry,
|
|
754
|
+
pos: u64,
|
|
755
|
+
offset: i64,
|
|
756
|
+
whence: Whence,
|
|
757
|
+
) -> Result<u64, FilesystemError> {
|
|
758
|
+
generic_file_llseek(dentry, pos, offset, whence)
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
fn read_iter(&mut self, _iocb: &mut Kiocb, _buf: &mut [u8]) -> Result<usize, FilesystemError> {
|
|
762
|
+
generic_read_dir()
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/// Reads on from each name's stable position, as `offset_readdir` does, so
|
|
766
|
+
/// nothing is held between two calls.
|
|
767
|
+
fn iterate_shared(
|
|
768
|
+
&mut self,
|
|
769
|
+
dentry: &Dentry,
|
|
770
|
+
ctx: &mut DirContext<'_>,
|
|
771
|
+
) -> Result<(), FilesystemError> {
|
|
772
|
+
if !dir_emit_dots(dentry, ctx) {
|
|
773
|
+
return Ok(());
|
|
774
|
+
}
|
|
775
|
+
loop {
|
|
776
|
+
let found = {
|
|
777
|
+
let state = self.dir.state();
|
|
778
|
+
state
|
|
779
|
+
.entry_from(ctx.pos())
|
|
780
|
+
.map(|(offset, name, node)| (offset, node.dir_entry(name, self.dev)))
|
|
781
|
+
};
|
|
782
|
+
let Some((offset, entry)) = found else {
|
|
783
|
+
return Ok(());
|
|
784
|
+
};
|
|
785
|
+
ctx.set_pos(offset);
|
|
786
|
+
if !ctx.dir_emit(&entry) {
|
|
787
|
+
return Ok(());
|
|
788
|
+
}
|
|
789
|
+
ctx.set_pos(offset + 1);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/// A Tmpfs holds nothing to make durable, as tmpfs names `noop_fsync` for
|
|
794
|
+
/// its directories too.
|
|
795
|
+
fn fsync(&mut self, _datasync: bool) -> Result<(), FilesystemError> {
|
|
796
|
+
Ok(())
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/// A Tmpfs handed to more than one Filesystem instance changes through each,
|
|
801
|
+
/// behind the others' dentries, so a dentry recording an identity is valid
|
|
802
|
+
/// only while that entry stands at its name here, as NFS compares the handle
|
|
803
|
+
/// a fresh lookup finds. One recording none has nothing to compare. Moves
|
|
804
|
+
/// through two instances take no common lock, so together they are undefined.
|
|
805
|
+
pub(super) struct TmpfsDentry;
|
|
806
|
+
|
|
807
|
+
impl DentryOperations for TmpfsDentry {
|
|
808
|
+
fn revalidate(
|
|
809
|
+
&self,
|
|
810
|
+
dir: &dyn InodeOperations,
|
|
811
|
+
dentry: &Dentry,
|
|
812
|
+
) -> Result<bool, FilesystemError> {
|
|
813
|
+
let Some(dir) = (dir as &dyn Any).downcast_ref::<Tmpfs>() else {
|
|
814
|
+
return Ok(true);
|
|
815
|
+
};
|
|
816
|
+
let found = dir.dir.get(&dentry.name()).ok();
|
|
817
|
+
let Some(inode) = dentry.inode() else {
|
|
818
|
+
return Ok(found.is_none());
|
|
819
|
+
};
|
|
820
|
+
let Some(identity) = inode.identity() else {
|
|
821
|
+
return Ok(true);
|
|
822
|
+
};
|
|
823
|
+
Ok(found.is_some_and(|node| Identity::of(&dir.node_getattr(&node)) == Some(identity)))
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/// A file of a Tmpfs.
|
|
828
|
+
struct TmpfsRegular {
|
|
829
|
+
data: Arc<Mutex<Content>>,
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
impl InodeOperations for TmpfsRegular {
|
|
833
|
+
fn getattr(&self, _dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
834
|
+
Ok(lock(&self.data).getattr())
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
fn setattr(&self, _dentry: &Dentry, attr: &Iattr) -> Result<(), FilesystemError> {
|
|
838
|
+
let mut data = lock(&self.data);
|
|
839
|
+
if let Some(len) = attr.size {
|
|
840
|
+
let len = usize::try_from(len).map_err(|_| FilesystemError::InvalidInput)?;
|
|
841
|
+
data.resize(len)?;
|
|
842
|
+
}
|
|
843
|
+
data.times.apply(attr);
|
|
844
|
+
Ok(())
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
fn open(
|
|
848
|
+
&self,
|
|
849
|
+
_dentry: &Dentry,
|
|
850
|
+
_how: &OpenHow,
|
|
851
|
+
) -> Result<Box<dyn FileOperations>, FilesystemError> {
|
|
852
|
+
Ok(Box::new(TmpfsFile {
|
|
853
|
+
data: self.data.clone(),
|
|
854
|
+
}))
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/// A whiteout a Tmpfs holds for an Overlay above it.
|
|
859
|
+
struct TmpfsWhiteout;
|
|
860
|
+
|
|
861
|
+
impl InodeOperations for TmpfsWhiteout {
|
|
862
|
+
fn getattr(&self, _dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
863
|
+
Ok(Kstat::new(FileType::Chrdev, 0))
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
fn lock(data: &Mutex<Content>) -> MutexGuard<'_, Content> {
|
|
868
|
+
data.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
fn write(data: &mut Content, buf: &[u8], start: usize) -> Result<usize, FilesystemError> {
|
|
872
|
+
if buf.is_empty() {
|
|
873
|
+
return Ok(0);
|
|
874
|
+
}
|
|
875
|
+
let end = start
|
|
876
|
+
.checked_add(buf.len())
|
|
877
|
+
.ok_or(FilesystemError::InvalidInput)?;
|
|
878
|
+
if data.bytes.len() < end {
|
|
879
|
+
data.resize(end)?;
|
|
880
|
+
}
|
|
881
|
+
data.bytes[start..end].copy_from_slice(buf);
|
|
882
|
+
data.times.touch();
|
|
883
|
+
Ok(buf.len())
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/// An open file of a Tmpfs, sharing its bytes with every other handle on
|
|
887
|
+
/// the same path.
|
|
888
|
+
pub(crate) struct TmpfsFile {
|
|
889
|
+
data: Arc<Mutex<Content>>,
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
impl FileOperations for TmpfsFile {
|
|
893
|
+
fn llseek(
|
|
894
|
+
&mut self,
|
|
895
|
+
dentry: &Dentry,
|
|
896
|
+
pos: u64,
|
|
897
|
+
offset: i64,
|
|
898
|
+
whence: Whence,
|
|
899
|
+
) -> Result<u64, FilesystemError> {
|
|
900
|
+
generic_file_llseek(dentry, pos, offset, whence)
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
|
|
904
|
+
let data = &lock(&self.data).bytes;
|
|
905
|
+
let start = usize::try_from(iocb.pos)
|
|
906
|
+
.unwrap_or(usize::MAX)
|
|
907
|
+
.min(data.len());
|
|
908
|
+
let read = buf.len().min(data.len() - start);
|
|
909
|
+
buf[..read].copy_from_slice(&data[start..start + read]);
|
|
910
|
+
iocb.pos += read as u64;
|
|
911
|
+
Ok(read)
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/// The end is read and written under one lock, so another handle's
|
|
915
|
+
/// append lands before or after this one, never over it.
|
|
916
|
+
fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
|
|
917
|
+
let mut data = lock(&self.data);
|
|
918
|
+
let start = match iocb.append {
|
|
919
|
+
true => data.bytes.len(),
|
|
920
|
+
false => usize::try_from(iocb.pos).map_err(|_| FilesystemError::InvalidInput)?,
|
|
921
|
+
};
|
|
922
|
+
let written = write(&mut data, buf, start)?;
|
|
923
|
+
iocb.pos = (start + written) as u64;
|
|
924
|
+
drop(data);
|
|
925
|
+
crate::vfs::generic_write_sync(self, iocb, written)
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/// Nothing outlives the Tmpfs, so there is nothing to flush, as shmem's
|
|
929
|
+
/// `noop_fsync`.
|
|
930
|
+
fn fsync(&mut self, _datasync: bool) -> Result<(), FilesystemError> {
|
|
931
|
+
Ok(())
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/// Grow the file to where the room ends, as `shmem_fallocate` moves `i_size`
|
|
935
|
+
/// past it. A Tmpfs holds every byte up to its end, so the room is taken from
|
|
936
|
+
/// its size or refused as out of space.
|
|
937
|
+
fn fallocate(&mut self, offset: u64, len: u64) -> Result<(), FilesystemError> {
|
|
938
|
+
let end = usize::try_from(offset + len).map_err(|_| FilesystemError::StorageFull)?;
|
|
939
|
+
let mut data = lock(&self.data);
|
|
940
|
+
if data.bytes.len() < end {
|
|
941
|
+
data.resize(end)?;
|
|
942
|
+
}
|
|
943
|
+
data.times.touch();
|
|
944
|
+
Ok(())
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
#[cfg(test)]
|
|
949
|
+
mod tests {
|
|
950
|
+
use super::*;
|
|
951
|
+
use crate::vfs::kern_path::{kern_path, KernPath};
|
|
952
|
+
|
|
953
|
+
#[derive(Default)]
|
|
954
|
+
struct CountsLookups(std::sync::atomic::AtomicUsize);
|
|
955
|
+
|
|
956
|
+
impl crate::filesystem::wrap::Hook for CountsLookups {
|
|
957
|
+
fn lookup(
|
|
958
|
+
&self,
|
|
959
|
+
inner: &dyn InodeOperations,
|
|
960
|
+
dentry: &Dentry,
|
|
961
|
+
) -> Result<Lookup, FilesystemError> {
|
|
962
|
+
self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
963
|
+
inner.lookup(dentry)
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// @behavior FS-195
|
|
968
|
+
#[test]
|
|
969
|
+
fn a_listing_goes_on_from_a_position_each_name_keeps() {
|
|
970
|
+
use crate::vfs::{DirContext, File, Path};
|
|
971
|
+
let tmpfs: Tmpfs = [("a", ""), ("b", ""), ("c", "")].into_iter().collect();
|
|
972
|
+
let mount = crate::mount::Mount::new(
|
|
973
|
+
"/data",
|
|
974
|
+
Filesystem::new(Arc::new(tmpfs)).expect("the instance must stand"),
|
|
975
|
+
crate::mount::Access::ReadWrite,
|
|
976
|
+
)
|
|
977
|
+
.expect("/data is a guest path");
|
|
978
|
+
let cred = Arc::default();
|
|
979
|
+
let mut dir = File::root(mount.mnt(), &cred).expect("the root opens");
|
|
980
|
+
let list = |dir: &mut File, pos, room: usize| {
|
|
981
|
+
let mut seen = Vec::new();
|
|
982
|
+
let mut actor = |emit: &crate::vfs::DirEmit<'_>| {
|
|
983
|
+
let took = seen.len() < room;
|
|
984
|
+
if took {
|
|
985
|
+
seen.push(emit.name.to_string());
|
|
986
|
+
}
|
|
987
|
+
took
|
|
988
|
+
};
|
|
989
|
+
let pos: u64 = pos;
|
|
990
|
+
dir.llseek(pos as i64, crate::vfs::Whence::Set)
|
|
991
|
+
.expect("the directory seeks");
|
|
992
|
+
dir.iterate_dir(&mut DirContext::new(0, &mut actor))
|
|
993
|
+
.expect("the directory lists");
|
|
994
|
+
(seen, dir.pos())
|
|
995
|
+
};
|
|
996
|
+
let first = list(&mut dir, 2, 1);
|
|
997
|
+
let root = Path::root(mount.mnt());
|
|
998
|
+
crate::vfs::filename_unlinkat(&cred, &root, "a").expect("the name goes");
|
|
999
|
+
crate::vfs::filename_unlinkat(&cred, &root, "b").expect("the name goes");
|
|
1000
|
+
let how = crate::vfs::OpenHow {
|
|
1001
|
+
write: true,
|
|
1002
|
+
create: true,
|
|
1003
|
+
..crate::vfs::OpenHow::default()
|
|
1004
|
+
};
|
|
1005
|
+
crate::vfs::kern_path::filp_open(&cred, &root, "d", &how).expect("the name is made");
|
|
1006
|
+
let (first, next) = first;
|
|
1007
|
+
assert_eq!(
|
|
1008
|
+
(first, list(&mut dir, next, usize::MAX).0),
|
|
1009
|
+
(
|
|
1010
|
+
vec!["a".to_string()],
|
|
1011
|
+
vec!["c".to_string(), "d".to_string()]
|
|
1012
|
+
),
|
|
1013
|
+
"File::iterate_dir going on over a Tmpfs must repeat nothing, pass over what was \
|
|
1014
|
+
removed, and hold what was made since, as offset_readdir reads on from each name's \
|
|
1015
|
+
stable offset"
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// @behavior FS-192
|
|
1020
|
+
#[test]
|
|
1021
|
+
fn a_tmpfs_keeps_no_name_nothing_holds() {
|
|
1022
|
+
let counts = Arc::new(CountsLookups::default());
|
|
1023
|
+
let tmpfs: Tmpfs = [("file", "f")].into_iter().collect();
|
|
1024
|
+
let wrapped = crate::filesystem::wrap::Wrap::new(Arc::new(tmpfs), counts.clone());
|
|
1025
|
+
let mount = crate::mount::Mount::new(
|
|
1026
|
+
"/data",
|
|
1027
|
+
Filesystem::new(Arc::new(wrapped)).expect("the instance must stand"),
|
|
1028
|
+
crate::mount::Access::ReadWrite,
|
|
1029
|
+
)
|
|
1030
|
+
.expect("/data is a guest path");
|
|
1031
|
+
let root = crate::vfs::Path::root(mount.mnt());
|
|
1032
|
+
for _ in 0..2 {
|
|
1033
|
+
crate::vfs::vfs_fstatat(&Arc::default(), &root, "file", false)
|
|
1034
|
+
.expect("the file stands");
|
|
1035
|
+
}
|
|
1036
|
+
assert_eq!(
|
|
1037
|
+
counts.0.load(std::sync::atomic::Ordering::SeqCst),
|
|
1038
|
+
2,
|
|
1039
|
+
"vfs::vfs_fstatat twice of a Tmpfs's file must ask its lookup each time, as \
|
|
1040
|
+
shmem sets DCACHE_DONTCACHE"
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Each file leaves one name and takes another, so only keeping the count
|
|
1045
|
+
// through the swap reports both as still named.
|
|
1046
|
+
// @behavior FS-168
|
|
1047
|
+
#[test]
|
|
1048
|
+
fn an_exchange_leaves_both_files_named() {
|
|
1049
|
+
let filesystem = crate::filesystem::Filesystem::new(Arc::new(
|
|
1050
|
+
[("a", "first"), ("b", "second")]
|
|
1051
|
+
.into_iter()
|
|
1052
|
+
.collect::<Tmpfs>(),
|
|
1053
|
+
))
|
|
1054
|
+
.expect("the instance must stand");
|
|
1055
|
+
let dentries = filesystem.dentries();
|
|
1056
|
+
let root = filesystem.s_root();
|
|
1057
|
+
let at = |name| {
|
|
1058
|
+
dentries
|
|
1059
|
+
.alloc_parallel(&root, name)
|
|
1060
|
+
.expect("no other walk holds the name")
|
|
1061
|
+
};
|
|
1062
|
+
let (first, second) = (at("a"), at("b"));
|
|
1063
|
+
let dir = root.inode().expect("the root stands").ops().clone();
|
|
1064
|
+
dir.rename(&first, dir.as_ref(), &second, Some(RenameFlag::Exchange))
|
|
1065
|
+
.expect("the two must swap");
|
|
1066
|
+
let nlink = |name| {
|
|
1067
|
+
let dentry = at(name);
|
|
1068
|
+
dentry
|
|
1069
|
+
.lookup_inode(&filesystem, &root)
|
|
1070
|
+
.expect("the name must be found");
|
|
1071
|
+
dentry.getattr().map(|metadata| metadata.nlink)
|
|
1072
|
+
};
|
|
1073
|
+
assert_eq!(
|
|
1074
|
+
(nlink("a"), nlink("b")),
|
|
1075
|
+
(Ok(1), Ok(1)),
|
|
1076
|
+
"InodeOperations::rename of a Tmpfs asked to exchange two files must leave each with \
|
|
1077
|
+
one name"
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Linux's VFS sets no limit on one name, so this is the Tmpfs's own.
|
|
1082
|
+
// @behavior FS-154
|
|
1083
|
+
#[test]
|
|
1084
|
+
fn a_name_past_its_limit_is_too_long_for_a_tmpfs() {
|
|
1085
|
+
let fs = kern_path(Arc::new(Tmpfs::new()));
|
|
1086
|
+
let how = OpenHow {
|
|
1087
|
+
write: true,
|
|
1088
|
+
..OpenHow::default()
|
|
1089
|
+
};
|
|
1090
|
+
assert!(
|
|
1091
|
+
fs.create(&"x".repeat(NAME_MAX), &how).is_ok()
|
|
1092
|
+
&& matches!(
|
|
1093
|
+
fs.vfs_lstat(&"x".repeat(NAME_MAX + 1)),
|
|
1094
|
+
Err(FilesystemError::InvalidFilename)
|
|
1095
|
+
),
|
|
1096
|
+
"a name of NAME_MAX bytes must land in a Tmpfs, and one past it answer ENAMETOOLONG"
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// The space a Tmpfs counts is what bounds the host's memory, and a count
|
|
1101
|
+
// alone cannot tell, so only what the file still holds on to can.
|
|
1102
|
+
// @behavior FS-149
|
|
1103
|
+
#[test]
|
|
1104
|
+
fn a_file_cut_short_lets_go_of_what_it_held() {
|
|
1105
|
+
let tmpfs = Tmpfs::new();
|
|
1106
|
+
let content = Content::create(&tmpfs.tree).expect("the file must make");
|
|
1107
|
+
let mut data = lock(&content);
|
|
1108
|
+
data.resize(1 << 20).expect("the file must grow");
|
|
1109
|
+
data.resize(16).expect("the file must shrink");
|
|
1110
|
+
assert!(
|
|
1111
|
+
data.bytes.capacity() < 1 << 20,
|
|
1112
|
+
"Content::resize down to 16 bytes from 1 MiB must let go of the rest, holding {}",
|
|
1113
|
+
data.bytes.capacity()
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// @behavior FS-150
|
|
1118
|
+
#[test]
|
|
1119
|
+
fn a_file_past_its_limit_can_still_shrink() {
|
|
1120
|
+
let tmpfs = Tmpfs::try_from_iter([("file", vec![0; 100])])
|
|
1121
|
+
.expect("the seed must fit")
|
|
1122
|
+
.with_size(Some(10));
|
|
1123
|
+
let fs = kern_path(Arc::new(tmpfs));
|
|
1124
|
+
assert_eq!(
|
|
1125
|
+
fs.truncate("file", 5),
|
|
1126
|
+
Ok(()),
|
|
1127
|
+
"a truncate through Tmpfs of a file seeded past its limit must shrink it"
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/// `tmpfs` reached through the VFS.
|
|
1132
|
+
// Built directly, as a path cannot reach this deep and stacking chains
|
|
1133
|
+
// through the VFS takes a moving of each. A depth a recursive release
|
|
1134
|
+
// cannot survive on this stack is what tells it from a loop.
|
|
1135
|
+
// @behavior FS-145
|
|
1136
|
+
#[test]
|
|
1137
|
+
fn a_tmpfs_goes_however_deep_its_directories_stand() {
|
|
1138
|
+
let tmpfs = Tmpfs::new();
|
|
1139
|
+
let mut at = tmpfs.dir.clone();
|
|
1140
|
+
for _ in 0..200_000 {
|
|
1141
|
+
let below = tmpfs.new_directory();
|
|
1142
|
+
at.state()
|
|
1143
|
+
.entries
|
|
1144
|
+
.insert("a".to_string(), Node::Directory(below.clone()));
|
|
1145
|
+
at = below;
|
|
1146
|
+
}
|
|
1147
|
+
drop(at);
|
|
1148
|
+
let released = std::thread::Builder::new()
|
|
1149
|
+
.stack_size(64 * 1024)
|
|
1150
|
+
.spawn(move || drop(tmpfs))
|
|
1151
|
+
.expect("the thread must start")
|
|
1152
|
+
.join();
|
|
1153
|
+
assert!(
|
|
1154
|
+
released.is_ok(),
|
|
1155
|
+
"a Tmpfs 200000 directories deep, dropped on a 64 KiB stack, must go without \
|
|
1156
|
+
overflowing it"
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
fn at(tmpfs: Tmpfs) -> KernPath {
|
|
1161
|
+
kern_path(Arc::new(tmpfs))
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
fn read_all(fs: &KernPath, path: &str) -> Vec<u8> {
|
|
1165
|
+
let mut file = fs
|
|
1166
|
+
.open(
|
|
1167
|
+
path,
|
|
1168
|
+
&OpenHow {
|
|
1169
|
+
read: true,
|
|
1170
|
+
..OpenHow::default()
|
|
1171
|
+
},
|
|
1172
|
+
)
|
|
1173
|
+
.expect("the file must open");
|
|
1174
|
+
let mut buf = vec![0; 64];
|
|
1175
|
+
let read = file
|
|
1176
|
+
.read_iter(&mut Kiocb::at(0), &mut buf)
|
|
1177
|
+
.expect("the file must read");
|
|
1178
|
+
buf.truncate(read);
|
|
1179
|
+
buf
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// @behavior FS-001
|
|
1183
|
+
#[test]
|
|
1184
|
+
fn a_tmpfs_serves_the_files_it_was_seeded_with() {
|
|
1185
|
+
let tmpfs = at([("/config/app.yml", "name: kobako")].into_iter().collect());
|
|
1186
|
+
assert_eq!(
|
|
1187
|
+
read_all(&tmpfs, "config/app.yml"),
|
|
1188
|
+
b"name: kobako",
|
|
1189
|
+
"a file seeded into Tmpfs through FromIterator must read back its content"
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// @behavior FS-067
|
|
1194
|
+
#[test]
|
|
1195
|
+
fn a_tmpfs_tried_with_a_list_it_cannot_hold_answers_why() {
|
|
1196
|
+
let refused = [
|
|
1197
|
+
Tmpfs::try_from_iter([("/", "x")]),
|
|
1198
|
+
Tmpfs::try_from_iter([("a", "x"), ("a/b", "y")]),
|
|
1199
|
+
Tmpfs::try_from_iter([("a", "x"), ("/a", "y")]),
|
|
1200
|
+
]
|
|
1201
|
+
.map(|tried| tried.map(drop));
|
|
1202
|
+
assert_eq!(
|
|
1203
|
+
refused,
|
|
1204
|
+
[
|
|
1205
|
+
Err(SeedError::Root),
|
|
1206
|
+
Err(SeedError::FileAndDirectory("a".to_string())),
|
|
1207
|
+
Err(SeedError::Twice("a".to_string())),
|
|
1208
|
+
],
|
|
1209
|
+
"a list naming the root, a file beneath a file, or a file twice through \
|
|
1210
|
+
Tmpfs::try_from_iter must answer the SeedError naming it"
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// A runtime never hands a Filesystem `..`, so a file seeded through one
|
|
1215
|
+
// would be held where no call reaches it.
|
|
1216
|
+
// @behavior FS-087
|
|
1217
|
+
#[test]
|
|
1218
|
+
fn a_tmpfs_tried_with_a_path_through_dot_dot_answers_why() {
|
|
1219
|
+
assert_eq!(
|
|
1220
|
+
Tmpfs::try_from_iter([("a/../b", "x")]).map(drop),
|
|
1221
|
+
Err(SeedError::DotDot("a/../b".to_string())),
|
|
1222
|
+
"a list naming a path through `..` through Tmpfs::try_from_iter must answer \
|
|
1223
|
+
SeedError::DotDot naming it"
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// @behavior FS-062
|
|
1228
|
+
#[test]
|
|
1229
|
+
#[should_panic(expected = "a Tmpfs seeds no file at its root")]
|
|
1230
|
+
fn a_tmpfs_seeded_at_its_root_panics() {
|
|
1231
|
+
let _: Tmpfs = [("/", "x")].into_iter().collect();
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// @behavior FS-062
|
|
1235
|
+
#[test]
|
|
1236
|
+
#[should_panic(expected = "a Tmpfs seeds a as both a file and a directory")]
|
|
1237
|
+
fn a_tmpfs_seeded_with_a_file_beneath_a_file_panics() {
|
|
1238
|
+
let _: Tmpfs = [("a", "x"), ("a/b", "y")].into_iter().collect();
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// @behavior FS-062
|
|
1242
|
+
#[test]
|
|
1243
|
+
#[should_panic(expected = "a Tmpfs seeds a as both a file and a directory")]
|
|
1244
|
+
fn a_tmpfs_seeded_with_a_file_over_a_directory_panics() {
|
|
1245
|
+
let _: Tmpfs = [("a/b", "y"), ("a", "x")].into_iter().collect();
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// @behavior FS-062
|
|
1249
|
+
#[test]
|
|
1250
|
+
#[should_panic(expected = "a Tmpfs seeds a twice")]
|
|
1251
|
+
fn a_tmpfs_seeded_with_one_path_twice_panics() {
|
|
1252
|
+
let _: Tmpfs = [("a", "x"), ("a", "y")].into_iter().collect();
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// @behavior FS-002
|
|
1256
|
+
#[test]
|
|
1257
|
+
fn a_tmpfs_keeps_what_was_written() {
|
|
1258
|
+
let tmpfs = at(Tmpfs::new());
|
|
1259
|
+
let mut file = tmpfs
|
|
1260
|
+
.open_or_create(
|
|
1261
|
+
"notes.txt",
|
|
1262
|
+
&OpenHow {
|
|
1263
|
+
write: true,
|
|
1264
|
+
..OpenHow::default()
|
|
1265
|
+
},
|
|
1266
|
+
)
|
|
1267
|
+
.expect("a new file must be created");
|
|
1268
|
+
file.write_iter(&mut Kiocb::at(0), b"kept")
|
|
1269
|
+
.expect("the write must land");
|
|
1270
|
+
drop(file);
|
|
1271
|
+
assert_eq!(
|
|
1272
|
+
read_all(&tmpfs, "notes.txt"),
|
|
1273
|
+
b"kept",
|
|
1274
|
+
"a file written through Tmpfs::open must read back from the same Tmpfs"
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
#[test]
|
|
1279
|
+
fn a_directory_with_entries_is_not_removed() {
|
|
1280
|
+
let tmpfs = at([("dir/file", "x")].into_iter().collect());
|
|
1281
|
+
assert_eq!(
|
|
1282
|
+
tmpfs.vfs_rmdir("dir"),
|
|
1283
|
+
Err(FilesystemError::DirectoryNotEmpty)
|
|
1284
|
+
);
|
|
1285
|
+
assert_eq!(tmpfs.vfs_unlink("dir/file"), Ok(()));
|
|
1286
|
+
assert_eq!(tmpfs.vfs_rmdir("dir"), Ok(()));
|
|
1287
|
+
assert_eq!(tmpfs.vfs_lstat("dir"), Err(FilesystemError::NotFound));
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
#[test]
|
|
1291
|
+
fn a_renamed_directory_carries_its_entries() {
|
|
1292
|
+
let tmpfs = at([("old/inner/file", "x")].into_iter().collect());
|
|
1293
|
+
tmpfs
|
|
1294
|
+
.rename("old", &tmpfs, "new")
|
|
1295
|
+
.expect("the rename must succeed");
|
|
1296
|
+
assert_eq!(read_all(&tmpfs, "new/inner/file"), b"x");
|
|
1297
|
+
assert_eq!(tmpfs.vfs_lstat("old"), Err(FilesystemError::NotFound));
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
fn writable(fs: &KernPath, path: &str) -> Box<dyn FileOperations> {
|
|
1301
|
+
fs.open_or_create(
|
|
1302
|
+
path,
|
|
1303
|
+
&OpenHow {
|
|
1304
|
+
write: true,
|
|
1305
|
+
..OpenHow::default()
|
|
1306
|
+
},
|
|
1307
|
+
)
|
|
1308
|
+
.expect("the file must open")
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// @behavior FS-022
|
|
1312
|
+
#[test]
|
|
1313
|
+
fn a_write_past_a_tmpfs_size_is_refused() {
|
|
1314
|
+
let tmpfs: Tmpfs = [("file", "x")].into_iter().collect();
|
|
1315
|
+
let tmpfs = at(tmpfs.with_size(Some(4)));
|
|
1316
|
+
assert_eq!(
|
|
1317
|
+
(
|
|
1318
|
+
writable(&tmpfs, "file").write_iter(&mut Kiocb::at(1), b"four"),
|
|
1319
|
+
read_all(&tmpfs, "file")
|
|
1320
|
+
),
|
|
1321
|
+
(Err(FilesystemError::StorageFull), b"x".to_vec()),
|
|
1322
|
+
"a write growing a Tmpfs's files past its limit through TmpfsFile::write_iter must \
|
|
1323
|
+
answer StorageFull and leave the file as it was"
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// @behavior FS-096
|
|
1328
|
+
#[test]
|
|
1329
|
+
fn a_write_of_no_bytes_leaves_a_tmpfs_file_as_it_was() {
|
|
1330
|
+
let tmpfs = at([("file", "")].into_iter().collect());
|
|
1331
|
+
assert_eq!(
|
|
1332
|
+
(
|
|
1333
|
+
writable(&tmpfs, "file").write_iter(&mut Kiocb::at(1000), b""),
|
|
1334
|
+
read_all(&tmpfs, "file")
|
|
1335
|
+
),
|
|
1336
|
+
(Ok(0), Vec::new()),
|
|
1337
|
+
"an empty write past the end through TmpfsFile::write_iter must answer 0 and leave \
|
|
1338
|
+
the file empty"
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// @behavior FS-023
|
|
1343
|
+
#[test]
|
|
1344
|
+
fn a_tmpfs_holds_its_default_size() {
|
|
1345
|
+
let tmpfs = at(Tmpfs::new());
|
|
1346
|
+
let _file = writable(&tmpfs, "file");
|
|
1347
|
+
let mib_64 = 64 * 1024 * 1024;
|
|
1348
|
+
assert_eq!(
|
|
1349
|
+
(
|
|
1350
|
+
tmpfs.truncate("file", mib_64),
|
|
1351
|
+
tmpfs.truncate("file", mib_64 + 1)
|
|
1352
|
+
),
|
|
1353
|
+
(Ok(()), Err(FilesystemError::StorageFull)),
|
|
1354
|
+
"a Tmpfs given no limit through Tmpfs::new must hold 64 MiB and refuse a byte more"
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// @behavior FS-024
|
|
1359
|
+
#[test]
|
|
1360
|
+
fn a_tmpfs_without_a_size_grows_past_the_default() {
|
|
1361
|
+
let tmpfs = at(Tmpfs::new().with_size(None));
|
|
1362
|
+
let _file = writable(&tmpfs, "file");
|
|
1363
|
+
assert_eq!(
|
|
1364
|
+
tmpfs.truncate("file", Tmpfs::DEFAULT_SIZE as u64 + 1),
|
|
1365
|
+
Ok(()),
|
|
1366
|
+
"a Tmpfs given no limit through Tmpfs::with_size(None) must grow past 64 MiB"
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// @behavior FS-025
|
|
1371
|
+
#[test]
|
|
1372
|
+
fn a_removed_file_returns_its_space() {
|
|
1373
|
+
let tmpfs = at(Tmpfs::new().with_size(Some(4)));
|
|
1374
|
+
writable(&tmpfs, "first")
|
|
1375
|
+
.write_iter(&mut Kiocb::at(0), b"four")
|
|
1376
|
+
.expect("the first file must fit");
|
|
1377
|
+
tmpfs.vfs_unlink("first").expect("the file must remove");
|
|
1378
|
+
assert_eq!(
|
|
1379
|
+
writable(&tmpfs, "second").write_iter(&mut Kiocb::at(0), b"four"),
|
|
1380
|
+
Ok(4),
|
|
1381
|
+
"the space a file removed through vfs_unlink held must be free for the next"
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// A length past `isize::MAX` is one no allocator can hand out, so the
|
|
1386
|
+
// refusal does not depend on how much the host has free.
|
|
1387
|
+
// @behavior FS-026
|
|
1388
|
+
#[test]
|
|
1389
|
+
fn a_file_grown_past_what_can_be_allocated_is_refused() {
|
|
1390
|
+
let tmpfs: Tmpfs = [("file", "x")].into_iter().collect();
|
|
1391
|
+
let tmpfs = at(tmpfs.with_size(None));
|
|
1392
|
+
let mut file = writable(&tmpfs, "file");
|
|
1393
|
+
let unallocatable = isize::MAX as u64 + 1;
|
|
1394
|
+
assert_eq!(
|
|
1395
|
+
(
|
|
1396
|
+
tmpfs.truncate("file", unallocatable),
|
|
1397
|
+
file.write_iter(&mut Kiocb::at(unallocatable), b"x"),
|
|
1398
|
+
read_all(&tmpfs, "file")
|
|
1399
|
+
),
|
|
1400
|
+
(
|
|
1401
|
+
Err(FilesystemError::StorageFull),
|
|
1402
|
+
Err(FilesystemError::StorageFull),
|
|
1403
|
+
b"x".to_vec()
|
|
1404
|
+
),
|
|
1405
|
+
"a length no allocator can hand out through TmpfsRegular::setattr and TmpfsFile::write_iter must \
|
|
1406
|
+
answer StorageFull rather than abort"
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
#[test]
|
|
1411
|
+
fn a_path_beneath_a_file_is_not_a_directory() {
|
|
1412
|
+
let tmpfs = at([("file", "x")].into_iter().collect());
|
|
1413
|
+
assert_eq!(
|
|
1414
|
+
tmpfs.vfs_lstat("file/inner"),
|
|
1415
|
+
Err(FilesystemError::NotADirectory)
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
fn create(fs: &KernPath, path: &str) -> Result<(), FilesystemError> {
|
|
1420
|
+
fs.open_or_create(
|
|
1421
|
+
path,
|
|
1422
|
+
&OpenHow {
|
|
1423
|
+
write: true,
|
|
1424
|
+
..OpenHow::default()
|
|
1425
|
+
},
|
|
1426
|
+
)
|
|
1427
|
+
.map(drop)
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// Three inodes hold the root, one file, and one directory, so neither
|
|
1431
|
+
// kind of entry gets past the count.
|
|
1432
|
+
// @behavior FS-030
|
|
1433
|
+
#[test]
|
|
1434
|
+
fn a_tmpfs_holds_at_most_its_inodes() {
|
|
1435
|
+
let tmpfs = at(Tmpfs::new().with_nr_inodes(Some(3)));
|
|
1436
|
+
assert_eq!(
|
|
1437
|
+
(
|
|
1438
|
+
create(&tmpfs, "file"),
|
|
1439
|
+
tmpfs.vfs_mkdir("dir"),
|
|
1440
|
+
create(&tmpfs, "past"),
|
|
1441
|
+
tmpfs.vfs_mkdir("past_dir"),
|
|
1442
|
+
),
|
|
1443
|
+
(
|
|
1444
|
+
Ok(()),
|
|
1445
|
+
Ok(()),
|
|
1446
|
+
Err(FilesystemError::StorageFull),
|
|
1447
|
+
Err(FilesystemError::StorageFull)
|
|
1448
|
+
),
|
|
1449
|
+
"a Tmpfs given inodes through Tmpfs::with_nr_inodes must refuse a file or directory \
|
|
1450
|
+
past them, the root counted, as out of space"
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// @behavior FS-031
|
|
1455
|
+
#[test]
|
|
1456
|
+
fn a_tmpfs_inodes_follow_its_size() {
|
|
1457
|
+
let tmpfs = at(Tmpfs::new().with_size(Some(2 * 4096)));
|
|
1458
|
+
assert_eq!(
|
|
1459
|
+
(
|
|
1460
|
+
create(&tmpfs, "a"),
|
|
1461
|
+
create(&tmpfs, "b"),
|
|
1462
|
+
create(&tmpfs, "c")
|
|
1463
|
+
),
|
|
1464
|
+
(Ok(()), Ok(()), Err(FilesystemError::StorageFull)),
|
|
1465
|
+
"a Tmpfs given only a limit through Tmpfs::with_size must hold one entry for each \
|
|
1466
|
+
4 KiB of it and the root"
|
|
1467
|
+
);
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// The second handle opened before the first appended, so only an end
|
|
1471
|
+
// read as the append lands puts its bytes after the first's.
|
|
1472
|
+
// @behavior FS-036
|
|
1473
|
+
#[test]
|
|
1474
|
+
fn a_tmpfs_appends_at_the_end_as_it_stands() {
|
|
1475
|
+
let tmpfs = at([("log", "a")].into_iter().collect());
|
|
1476
|
+
let mut first = writable(&tmpfs, "log");
|
|
1477
|
+
let mut second = writable(&tmpfs, "log");
|
|
1478
|
+
let (mut from_first, mut from_second) =
|
|
1479
|
+
(Kiocb::at(0).with_append(), Kiocb::at(0).with_append());
|
|
1480
|
+
assert_eq!(
|
|
1481
|
+
(
|
|
1482
|
+
first.write_iter(&mut from_first, b"bb"),
|
|
1483
|
+
from_first.pos,
|
|
1484
|
+
second.write_iter(&mut from_second, b"ccc"),
|
|
1485
|
+
from_second.pos,
|
|
1486
|
+
read_all(&tmpfs, "log")
|
|
1487
|
+
),
|
|
1488
|
+
(Ok(2), 3, Ok(3), 6, b"abbccc".to_vec()),
|
|
1489
|
+
"TmpfsFile::append through two handles on one file must land each at the end as it \
|
|
1490
|
+
stands, answering where it landed"
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// The VFS refuses a move onto an entry of another kind, as
|
|
1495
|
+
// `may_delete_dentry` does, and the Tmpfs a directory still holding
|
|
1496
|
+
// entries, as `shmem_rename2` does, so the two keep each move here
|
|
1497
|
+
// from replacing its target.
|
|
1498
|
+
// @behavior FS-133
|
|
1499
|
+
#[test]
|
|
1500
|
+
fn a_tmpfs_replaces_only_what_rename_lets_a_move_replace() {
|
|
1501
|
+
let tmpfs = at([("file", "f"), ("full/inner", "i")].into_iter().collect());
|
|
1502
|
+
tmpfs
|
|
1503
|
+
.vfs_mkdir("empty")
|
|
1504
|
+
.expect("the directory must be made");
|
|
1505
|
+
let moves = [
|
|
1506
|
+
("file", "empty"),
|
|
1507
|
+
("file", "full"),
|
|
1508
|
+
("empty", "file"),
|
|
1509
|
+
("empty", "full"),
|
|
1510
|
+
]
|
|
1511
|
+
.map(|(from, to)| tmpfs.rename(from, &tmpfs, to).err());
|
|
1512
|
+
let mut standing: Vec<String> = tmpfs
|
|
1513
|
+
.iterate_dir()
|
|
1514
|
+
.expect("the root must list")
|
|
1515
|
+
.into_iter()
|
|
1516
|
+
.map(|entry| entry.name)
|
|
1517
|
+
.collect();
|
|
1518
|
+
standing.sort();
|
|
1519
|
+
assert_eq!(
|
|
1520
|
+
(moves, standing),
|
|
1521
|
+
(
|
|
1522
|
+
[
|
|
1523
|
+
Some(FilesystemError::IsADirectory),
|
|
1524
|
+
Some(FilesystemError::IsADirectory),
|
|
1525
|
+
Some(FilesystemError::NotADirectory),
|
|
1526
|
+
Some(FilesystemError::DirectoryNotEmpty),
|
|
1527
|
+
],
|
|
1528
|
+
vec!["empty".to_string(), "file".to_string(), "full".to_string()]
|
|
1529
|
+
),
|
|
1530
|
+
"Tmpfs::rename of a file onto a directory, and of a directory onto a file or a \
|
|
1531
|
+
directory holding entries, must answer EISDIR, ENOTDIR, and ENOTEMPTY, moving nothing"
|
|
1532
|
+
);
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
// A directory replaced by a rename gives its inode back as a removed
|
|
1536
|
+
// one does, so both leave room for the next.
|
|
1537
|
+
// @behavior FS-034
|
|
1538
|
+
#[test]
|
|
1539
|
+
fn a_removed_directory_gives_its_inode_back() {
|
|
1540
|
+
let tmpfs = at(Tmpfs::new().with_nr_inodes(Some(3)));
|
|
1541
|
+
tmpfs.vfs_mkdir("a").expect("the first directory must fit");
|
|
1542
|
+
tmpfs.vfs_mkdir("b").expect("the second directory must fit");
|
|
1543
|
+
tmpfs.vfs_rmdir("a").expect("the directory must remove");
|
|
1544
|
+
let after_removal = tmpfs.vfs_mkdir("c");
|
|
1545
|
+
tmpfs
|
|
1546
|
+
.rename("c", &tmpfs, "b")
|
|
1547
|
+
.expect("a directory must replace an empty one");
|
|
1548
|
+
assert_eq!(
|
|
1549
|
+
(after_removal, tmpfs.vfs_mkdir("d")),
|
|
1550
|
+
(Ok(()), Ok(())),
|
|
1551
|
+
"a directory removed through vfs_rmdir, or replaced through Tmpfs::rename, \
|
|
1552
|
+
must give its inode back"
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// The removed file's handle is still open, so its inode is given back
|
|
1557
|
+
// only when the handle goes, as Linux keeps an unlinked file's inode.
|
|
1558
|
+
// @behavior FS-032
|
|
1559
|
+
#[test]
|
|
1560
|
+
fn a_removed_file_gives_its_inode_back_once_closed() {
|
|
1561
|
+
let tmpfs = at(Tmpfs::new().with_nr_inodes(Some(2)));
|
|
1562
|
+
let handle = writable(&tmpfs, "first");
|
|
1563
|
+
tmpfs.vfs_unlink("first").expect("the file must remove");
|
|
1564
|
+
let while_open = create(&tmpfs, "second");
|
|
1565
|
+
drop(handle);
|
|
1566
|
+
assert_eq!(
|
|
1567
|
+
(while_open, create(&tmpfs, "second")),
|
|
1568
|
+
(Err(FilesystemError::StorageFull), Ok(())),
|
|
1569
|
+
"the inode of a file removed through vfs_unlink must stay taken while a \
|
|
1570
|
+
handle holds it, and be free once it closes"
|
|
1571
|
+
);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
// A host's `st_dev` never sets the top bit, so a device carrying it
|
|
1575
|
+
// cannot name a host's device, nor one another Filesystem took.
|
|
1576
|
+
// @behavior FS-085
|
|
1577
|
+
#[test]
|
|
1578
|
+
fn a_device_of_its_own_stands_apart_from_every_other_device() {
|
|
1579
|
+
let (first, second) = (Kstat::unique_device(), Kstat::unique_device());
|
|
1580
|
+
let tmpfs = at(Tmpfs::new())
|
|
1581
|
+
.vfs_getattr()
|
|
1582
|
+
.expect("the root must stand")
|
|
1583
|
+
.dev;
|
|
1584
|
+
assert!(
|
|
1585
|
+
[first, second, tmpfs].iter().all(|dev| dev >> 63 == 1)
|
|
1586
|
+
&& first != second
|
|
1587
|
+
&& tmpfs != first
|
|
1588
|
+
&& tmpfs != second,
|
|
1589
|
+
"devices from Kstat::unique_device and a Tmpfs's root must each set the top bit \
|
|
1590
|
+
and differ from one another"
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
fn names(dir: &Dir) -> Vec<String> {
|
|
1595
|
+
dir.state()
|
|
1596
|
+
.entries
|
|
1597
|
+
.iter()
|
|
1598
|
+
.flat_map(|(name, node)| {
|
|
1599
|
+
let beneath = match node {
|
|
1600
|
+
Node::Directory(inner) => names(inner),
|
|
1601
|
+
Node::File(_) | Node::Whiteout { .. } => Vec::new(),
|
|
1602
|
+
};
|
|
1603
|
+
std::iter::once(name.clone()).chain(beneath)
|
|
1604
|
+
})
|
|
1605
|
+
.collect()
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// Each round nests the file one level deeper, so a Tmpfs holding
|
|
1609
|
+
// whole paths would hold names growing with the depth; held under its
|
|
1610
|
+
// own name, every entry stays the one letter it was given.
|
|
1611
|
+
// @behavior FS-094
|
|
1612
|
+
#[test]
|
|
1613
|
+
fn a_tmpfs_holds_each_entry_under_its_own_name() {
|
|
1614
|
+
let held: Arc<Tmpfs> = Arc::new([("d/f", "x")].into_iter().collect());
|
|
1615
|
+
let tmpfs = kern_path(held.clone());
|
|
1616
|
+
for _ in 0..64 {
|
|
1617
|
+
tmpfs
|
|
1618
|
+
.vfs_mkdir("n")
|
|
1619
|
+
.expect("the new directory must be made");
|
|
1620
|
+
tmpfs
|
|
1621
|
+
.rename("d", &tmpfs, "n/d")
|
|
1622
|
+
.expect("the directory must move into the new one");
|
|
1623
|
+
tmpfs
|
|
1624
|
+
.rename("n", &tmpfs, "d")
|
|
1625
|
+
.expect("the new directory must take the old name");
|
|
1626
|
+
}
|
|
1627
|
+
let held = names(&held.dir);
|
|
1628
|
+
assert_eq!(
|
|
1629
|
+
(held, read_all(&tmpfs, &format!("{}f", "d/".repeat(65)))),
|
|
1630
|
+
(
|
|
1631
|
+
std::iter::repeat_n("d".to_string(), 65)
|
|
1632
|
+
.chain(["f".to_string()])
|
|
1633
|
+
.collect::<Vec<_>>(),
|
|
1634
|
+
b"x".to_vec()
|
|
1635
|
+
),
|
|
1636
|
+
"a directory moved deeper 64 times through Tmpfs::rename must leave every entry \
|
|
1637
|
+
holding only its own name, and the file reading where it now lies"
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// @behavior FS-105
|
|
1642
|
+
#[test]
|
|
1643
|
+
fn a_directory_opened_from_a_tmpfs_follows_it_when_it_moves() {
|
|
1644
|
+
let tmpfs = at([("dir/.keep", "")].into_iter().collect());
|
|
1645
|
+
let dir = tmpfs.open_dir("dir").expect("the directory must open");
|
|
1646
|
+
tmpfs
|
|
1647
|
+
.rename("dir", &tmpfs, "moved")
|
|
1648
|
+
.expect("the directory must move");
|
|
1649
|
+
create(&dir, "made").expect("the file must be made");
|
|
1650
|
+
assert_eq!(
|
|
1651
|
+
(
|
|
1652
|
+
tmpfs.vfs_lstat("moved/made").map(|m| m.file_type),
|
|
1653
|
+
tmpfs.vfs_lstat("dir/made").map(|m| m.file_type)
|
|
1654
|
+
),
|
|
1655
|
+
(Ok(FileType::RegFile), Err(FilesystemError::NotFound)),
|
|
1656
|
+
"a file made through a directory Tmpfs::open_dir opened before Tmpfs::rename moved \
|
|
1657
|
+
it must stand beneath the directory's new path"
|
|
1658
|
+
);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// @behavior FS-106
|
|
1662
|
+
#[test]
|
|
1663
|
+
fn a_tmpfs_directory_removed_while_opened_makes_nothing_more() {
|
|
1664
|
+
let tmpfs = at(Tmpfs::new());
|
|
1665
|
+
tmpfs.vfs_mkdir("dir").expect("the directory must be made");
|
|
1666
|
+
let dir = tmpfs.open_dir("dir").expect("the directory must open");
|
|
1667
|
+
tmpfs.vfs_rmdir("dir").expect("the directory must remove");
|
|
1668
|
+
assert_eq!(
|
|
1669
|
+
(
|
|
1670
|
+
create(&dir, "made"),
|
|
1671
|
+
dir.vfs_mkdir("inner"),
|
|
1672
|
+
dir.iterate_dir().map(|entries| entries.len()),
|
|
1673
|
+
dir.vfs_lstat("").map(|m| m.file_type)
|
|
1674
|
+
),
|
|
1675
|
+
(
|
|
1676
|
+
Err(FilesystemError::NotFound),
|
|
1677
|
+
Err(FilesystemError::NotFound),
|
|
1678
|
+
Ok(0),
|
|
1679
|
+
Ok(FileType::Dir)
|
|
1680
|
+
),
|
|
1681
|
+
"a directory Tmpfs::open_dir opened, once vfs_rmdir removed it, must make \
|
|
1682
|
+
nothing, list nothing, and still stat as a directory"
|
|
1683
|
+
);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// @behavior FS-107
|
|
1687
|
+
#[test]
|
|
1688
|
+
fn a_tmpfs_directory_never_moves_beneath_itself() {
|
|
1689
|
+
let tmpfs = at(Tmpfs::new());
|
|
1690
|
+
tmpfs.vfs_mkdir("a").expect("the directory must be made");
|
|
1691
|
+
tmpfs
|
|
1692
|
+
.vfs_mkdir("a/b")
|
|
1693
|
+
.expect("the inner directory must be made");
|
|
1694
|
+
let inner = tmpfs.open_dir("a/b").expect("the directory must open");
|
|
1695
|
+
assert_eq!(
|
|
1696
|
+
(
|
|
1697
|
+
tmpfs.rename("a", &inner, "a"),
|
|
1698
|
+
tmpfs.vfs_lstat("a/b").map(|m| m.file_type)
|
|
1699
|
+
),
|
|
1700
|
+
(Err(FilesystemError::InvalidInput), Ok(FileType::Dir)),
|
|
1701
|
+
"Tmpfs::rename of a directory into one opened beneath it must answer InvalidInput \
|
|
1702
|
+
and move nothing"
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// The content stands as it was, so only `st_ctime` tells the name moved, as
|
|
1707
|
+
// `simple_rename_timestamp` moves it alone. The wait passes the clock's
|
|
1708
|
+
// resolution, so the two times cannot pass for one.
|
|
1709
|
+
// @behavior FS-177
|
|
1710
|
+
#[test]
|
|
1711
|
+
fn a_moved_file_moves_only_its_changed_time() {
|
|
1712
|
+
let tmpfs = at([("file", "x")].into_iter().collect());
|
|
1713
|
+
let before = tmpfs.vfs_lstat("file").expect("the file must stand");
|
|
1714
|
+
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
1715
|
+
tmpfs
|
|
1716
|
+
.rename("file", &tmpfs, "moved")
|
|
1717
|
+
.expect("the move must land");
|
|
1718
|
+
let after = tmpfs.vfs_lstat("moved").expect("the file must stand");
|
|
1719
|
+
assert!(
|
|
1720
|
+
after.ctime > before.ctime && after.mtime == before.mtime,
|
|
1721
|
+
"Tmpfs::rename of a file must move its changed time and leave its modified time"
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// Linux's `notify_change` reads `current_time` once and `shmem_setattr` sets
|
|
1726
|
+
// every time from it, so a clock read for each would leave them apart. A
|
|
1727
|
+
// moment long past tells the one handed from one read here.
|
|
1728
|
+
// @behavior FS-199
|
|
1729
|
+
#[test]
|
|
1730
|
+
fn times_set_to_now_read_the_one_moment_the_change_lands() {
|
|
1731
|
+
let tmpfs = at([("file", "x")].into_iter().collect());
|
|
1732
|
+
let landed = SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1);
|
|
1733
|
+
let now = Some(SetTime::Now);
|
|
1734
|
+
tmpfs
|
|
1735
|
+
.setattr(
|
|
1736
|
+
"file",
|
|
1737
|
+
&Iattr {
|
|
1738
|
+
ctime: Some(landed),
|
|
1739
|
+
..Iattr::times(now, now)
|
|
1740
|
+
},
|
|
1741
|
+
)
|
|
1742
|
+
.expect("the times must be set");
|
|
1743
|
+
let set = tmpfs.vfs_lstat("file").expect("the file must stand");
|
|
1744
|
+
assert_eq!(
|
|
1745
|
+
(set.atime, set.mtime, set.ctime),
|
|
1746
|
+
(Some(landed), Some(landed), Some(landed)),
|
|
1747
|
+
"Tmpfs::setattr setting both times to now must set them and its changed time to the \
|
|
1748
|
+
moment the change lands"
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
// tmpfs frees an inode when the last reference goes, so a directory removed
|
|
1753
|
+
// while it is open still counts, and only its close lets another be made.
|
|
1754
|
+
// @behavior FS-173
|
|
1755
|
+
#[test]
|
|
1756
|
+
fn a_removed_directory_something_holds_keeps_its_inode() {
|
|
1757
|
+
let mount = crate::mount::Mount::new(
|
|
1758
|
+
"/data",
|
|
1759
|
+
Filesystem::new(Arc::new(Tmpfs::new().with_nr_inodes(Some(2))))
|
|
1760
|
+
.expect("the instance must stand"),
|
|
1761
|
+
crate::mount::Access::ReadWrite,
|
|
1762
|
+
)
|
|
1763
|
+
.expect("/data is a guest path");
|
|
1764
|
+
let root = crate::vfs::Path::root(mount.mnt());
|
|
1765
|
+
let cred = Arc::default();
|
|
1766
|
+
crate::vfs::filename_mkdirat(&cred, &root, "held").expect("the directory must be made");
|
|
1767
|
+
let how = crate::vfs::OpenHow {
|
|
1768
|
+
read: true,
|
|
1769
|
+
..Default::default()
|
|
1770
|
+
};
|
|
1771
|
+
let Ok(open) = crate::vfs::kern_path::filp_open(&cred, &root, "held", &how) else {
|
|
1772
|
+
panic!("the directory must open");
|
|
1773
|
+
};
|
|
1774
|
+
crate::vfs::filename_rmdir(&cred, &root, "held").expect("the directory must be removed");
|
|
1775
|
+
let while_open = crate::vfs::filename_mkdirat(&cred, &root, "next");
|
|
1776
|
+
drop(open);
|
|
1777
|
+
assert_eq!(
|
|
1778
|
+
(
|
|
1779
|
+
while_open,
|
|
1780
|
+
crate::vfs::filename_mkdirat(&cred, &root, "next")
|
|
1781
|
+
),
|
|
1782
|
+
(Err(FilesystemError::StorageFull), Ok(())),
|
|
1783
|
+
"vfs::filename_mkdirat in a Tmpfs out of inodes must answer ENOSPC while a removed \
|
|
1784
|
+
directory stays open, and succeed once it closes"
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
// @behavior FS-109
|
|
1789
|
+
#[test]
|
|
1790
|
+
fn a_rename_into_a_directory_of_another_tmpfs_crosses_devices() {
|
|
1791
|
+
let mount = |tmpfs: Tmpfs| {
|
|
1792
|
+
crate::mount::Mount::new(
|
|
1793
|
+
"/data",
|
|
1794
|
+
Filesystem::new(Arc::new(tmpfs)).expect("the instance must stand"),
|
|
1795
|
+
crate::mount::Access::ReadWrite,
|
|
1796
|
+
)
|
|
1797
|
+
.expect("/data is a guest path")
|
|
1798
|
+
};
|
|
1799
|
+
let (from, to) = (
|
|
1800
|
+
mount([("file", "x")].into_iter().collect()),
|
|
1801
|
+
mount(Tmpfs::new()),
|
|
1802
|
+
);
|
|
1803
|
+
let root = |mount: &crate::mount::Mount| crate::vfs::Path::root(mount.mnt());
|
|
1804
|
+
let cred = Arc::default();
|
|
1805
|
+
assert_eq!(
|
|
1806
|
+
(
|
|
1807
|
+
crate::vfs::filename_renameat2(&cred, &root(&from), "file", &root(&to), "file"),
|
|
1808
|
+
crate::vfs::vfs_fstatat(&cred, &root(&from), "file", false).map(|found| found.size),
|
|
1809
|
+
),
|
|
1810
|
+
(Err(FilesystemError::CrossesDevices), Ok(1)),
|
|
1811
|
+
"vfs::filename_renameat2 into a directory of another Tmpfs must answer EXDEV and \
|
|
1812
|
+
leave the file"
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// The write waits past the clock's resolution, so a time left where
|
|
1817
|
+
// the file was made cannot pass for one the write moved.
|
|
1818
|
+
// @behavior FS-115
|
|
1819
|
+
#[test]
|
|
1820
|
+
fn a_tmpfs_keeps_a_files_times_as_a_tmpfs_mounted_noatime_does() {
|
|
1821
|
+
let tmpfs = at(Tmpfs::new());
|
|
1822
|
+
let how = OpenHow {
|
|
1823
|
+
read: true,
|
|
1824
|
+
write: true,
|
|
1825
|
+
..OpenHow::default()
|
|
1826
|
+
};
|
|
1827
|
+
let mut file = tmpfs
|
|
1828
|
+
.open_or_create("file", &how)
|
|
1829
|
+
.expect("the file must be made");
|
|
1830
|
+
file.write_iter(&mut Kiocb::at(0), b"ab")
|
|
1831
|
+
.expect("the file must take a write");
|
|
1832
|
+
let made = tmpfs.vfs_lstat("file").expect("the file must report");
|
|
1833
|
+
file.read_iter(&mut Kiocb::at(0), &mut [0; 1])
|
|
1834
|
+
.expect("the file must read");
|
|
1835
|
+
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
1836
|
+
file.write_iter(&mut Kiocb::at(0), b"c")
|
|
1837
|
+
.expect("the file must take a write within its length");
|
|
1838
|
+
let written = tmpfs.vfs_lstat("file").expect("the file must stand");
|
|
1839
|
+
assert_eq!(
|
|
1840
|
+
(
|
|
1841
|
+
written.atime == made.atime,
|
|
1842
|
+
written.mtime > made.mtime,
|
|
1843
|
+
written.ctime == written.mtime,
|
|
1844
|
+
),
|
|
1845
|
+
(true, true, true),
|
|
1846
|
+
"vfs_lstat of a file read and then written must keep its access time from \
|
|
1847
|
+
when it was made, and move its modification and change times to the write"
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
// @behavior FS-116
|
|
1852
|
+
#[test]
|
|
1853
|
+
fn a_tmpfs_counts_a_directorys_names_as_a_tmpfs_does() {
|
|
1854
|
+
let tmpfs = at([("dir/file", "x")].into_iter().collect());
|
|
1855
|
+
tmpfs.vfs_mkdir("dir/a").expect("a directory must be made");
|
|
1856
|
+
tmpfs.vfs_mkdir("dir/b").expect("a directory must be made");
|
|
1857
|
+
let nlink = |path| tmpfs.vfs_lstat(path).expect("the entry must stand").nlink;
|
|
1858
|
+
assert_eq!(
|
|
1859
|
+
(nlink("dir"), nlink("dir/file")),
|
|
1860
|
+
(4, 1),
|
|
1861
|
+
"vfs_lstat of a directory holding a file and two directories must count four \
|
|
1862
|
+
names, and of the file one"
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// A Tmpfs is the one Filesystem answering this itself, so only its own count
|
|
1867
|
+
// can leave a removed directory reporting names it no longer has.
|
|
1868
|
+
// @behavior FS-178
|
|
1869
|
+
#[test]
|
|
1870
|
+
fn a_removed_directory_something_holds_counts_no_name() {
|
|
1871
|
+
let tmpfs = at(Tmpfs::new());
|
|
1872
|
+
for path in ["gone", "replaced", "moved", "other"] {
|
|
1873
|
+
tmpfs.vfs_mkdir(path).expect("a directory must be made");
|
|
1874
|
+
}
|
|
1875
|
+
let held = |path| tmpfs.open_dir(path).expect("the directory must open");
|
|
1876
|
+
let (gone, replaced, moved) = (held("gone"), held("replaced"), held("moved"));
|
|
1877
|
+
tmpfs.vfs_rmdir("gone").expect("the directory must go");
|
|
1878
|
+
tmpfs
|
|
1879
|
+
.rename("other", &tmpfs, "replaced")
|
|
1880
|
+
.expect("the move must replace the directory");
|
|
1881
|
+
tmpfs
|
|
1882
|
+
.rename("moved", &tmpfs, "there")
|
|
1883
|
+
.expect("the move must land");
|
|
1884
|
+
let nlink = |dir: &KernPath| dir.vfs_getattr().expect("the directory must answer").nlink;
|
|
1885
|
+
assert_eq!(
|
|
1886
|
+
(nlink(&gone), nlink(&replaced), nlink(&moved)),
|
|
1887
|
+
(0, 0, 2),
|
|
1888
|
+
"vfs_getattr of a held Tmpfs directory removed, or replaced by a move, must count no \
|
|
1889
|
+
name, and of one that moved must count its two"
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1893
|
+
// @behavior FS-137
|
|
1894
|
+
#[test]
|
|
1895
|
+
fn a_tmpfs_sizes_a_directory_as_a_tmpfs_does() {
|
|
1896
|
+
let tmpfs = at([("dir/file", "x")].into_iter().collect());
|
|
1897
|
+
tmpfs.vfs_mkdir("dir/a").expect("a directory must be made");
|
|
1898
|
+
tmpfs.vfs_mkdir("empty").expect("a directory must be made");
|
|
1899
|
+
let len = |path| {
|
|
1900
|
+
tmpfs
|
|
1901
|
+
.vfs_lstat(path)
|
|
1902
|
+
.expect("the directory must stand")
|
|
1903
|
+
.size
|
|
1904
|
+
};
|
|
1905
|
+
assert_eq!(
|
|
1906
|
+
(len("dir"), len("empty")),
|
|
1907
|
+
(80, 40),
|
|
1908
|
+
"vfs_lstat of a directory holding two entries must report a size of 80, and of \
|
|
1909
|
+
an empty one 40"
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
// @behavior FS-070
|
|
1914
|
+
#[test]
|
|
1915
|
+
fn a_tmpfs_gives_each_directory_it_makes_an_identity_of_its_own() {
|
|
1916
|
+
let tmpfs = at(Tmpfs::new());
|
|
1917
|
+
tmpfs.vfs_mkdir("dir").expect("the directory must be made");
|
|
1918
|
+
let identity = |path| {
|
|
1919
|
+
let metadata = tmpfs.vfs_lstat(path).expect("the directory must stand");
|
|
1920
|
+
(metadata.dev, metadata.ino)
|
|
1921
|
+
};
|
|
1922
|
+
let before = identity("dir");
|
|
1923
|
+
tmpfs
|
|
1924
|
+
.rename("dir", &tmpfs, "moved")
|
|
1925
|
+
.expect("the directory must move");
|
|
1926
|
+
tmpfs
|
|
1927
|
+
.vfs_mkdir("dir")
|
|
1928
|
+
.expect("another directory must be made");
|
|
1929
|
+
assert_eq!(
|
|
1930
|
+
(identity("moved") == before, identity("dir") == before),
|
|
1931
|
+
(true, false),
|
|
1932
|
+
"a directory moved through Tmpfs::rename must keep its identity, and one made at its \
|
|
1933
|
+
old path through vfs_mkdir must have another"
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
mod stress {
|
|
1938
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
1939
|
+
|
|
1940
|
+
use super::*;
|
|
1941
|
+
|
|
1942
|
+
// A move leaves its old directory before it reaches the new one, so only
|
|
1943
|
+
// a stat landing between the two shows a move counted as a removal.
|
|
1944
|
+
// @behavior FS-201
|
|
1945
|
+
#[test]
|
|
1946
|
+
#[ignore = "a stress witness; run by rake crates:stress"]
|
|
1947
|
+
fn a_moved_file_keeps_its_names_through_the_move() {
|
|
1948
|
+
let mount = crate::mount::Mount::new(
|
|
1949
|
+
"/data",
|
|
1950
|
+
Filesystem::new(Arc::new([("a", "x")].into_iter().collect::<Tmpfs>()))
|
|
1951
|
+
.expect("the instance must stand"),
|
|
1952
|
+
crate::mount::Access::ReadWrite,
|
|
1953
|
+
)
|
|
1954
|
+
.expect("/data is a guest path");
|
|
1955
|
+
let root = crate::vfs::Path::root(mount.mnt());
|
|
1956
|
+
let how = crate::vfs::OpenHow {
|
|
1957
|
+
read: true,
|
|
1958
|
+
..Default::default()
|
|
1959
|
+
};
|
|
1960
|
+
let held = crate::vfs::kern_path::filp_open(&Arc::default(), &root, "a", &how)
|
|
1961
|
+
.unwrap_or_else(|_| panic!("the file must open"));
|
|
1962
|
+
let stop = AtomicBool::new(false);
|
|
1963
|
+
let nameless = std::thread::scope(|scope| {
|
|
1964
|
+
scope.spawn(|| {
|
|
1965
|
+
while !stop.load(Ordering::SeqCst) {
|
|
1966
|
+
for (from, to) in [("a", "b"), ("b", "a")] {
|
|
1967
|
+
crate::vfs::filename_renameat2(&Arc::default(), &root, from, &root, to)
|
|
1968
|
+
.unwrap_or_else(|_| panic!("the move must land"));
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
let nameless = (0..200_000)
|
|
1973
|
+
.filter(|_| held.getattr().is_ok_and(|found| found.nlink == 0))
|
|
1974
|
+
.count();
|
|
1975
|
+
stop.store(true, Ordering::SeqCst);
|
|
1976
|
+
nameless
|
|
1977
|
+
});
|
|
1978
|
+
assert_eq!(
|
|
1979
|
+
nameless, 0,
|
|
1980
|
+
"File::getattr of a Tmpfs file another thread keeps moving must never report it \
|
|
1981
|
+
with no name"
|
|
1982
|
+
);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|