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.
Files changed (218) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +377 -0
  4. data/Cargo.lock +181 -154
  5. data/README.md +99 -459
  6. data/ROADMAP.md +7 -1
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +354 -0
  9. data/crates/kobako-runtime/Cargo.toml +38 -7
  10. data/crates/kobako-runtime/README.md +17 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +9 -1
  12. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  13. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  14. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  15. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  16. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  17. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  18. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  19. data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
  20. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  21. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  29. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
  30. data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
  31. data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
  32. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  33. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  37. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  38. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  39. data/crates/kobako-runtime/src/filesystem.rs +26 -0
  40. data/crates/kobako-runtime/src/grant.rs +20 -0
  41. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  42. data/crates/kobako-runtime/src/lib.rs +16 -1
  43. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  44. data/crates/kobako-runtime/src/locking.rs +218 -0
  45. data/crates/kobako-runtime/src/mount.rs +105 -0
  46. data/crates/kobako-runtime/src/profile.rs +140 -4
  47. data/crates/kobako-runtime/src/runtime.rs +12 -0
  48. data/crates/kobako-runtime/src/sched.rs +210 -0
  49. data/crates/kobako-runtime/src/security.rs +696 -0
  50. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  51. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  52. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  53. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  54. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  55. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  56. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  57. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  58. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  59. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  60. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  61. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  62. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  63. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  64. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  65. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  66. data/crates/kobako-runtime/src/vfs.rs +60 -0
  67. data/crates/kobako-runtime/src/yielder.rs +11 -6
  68. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  70. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  71. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  72. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  73. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  74. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  75. data/crates/kobako-transport/CHANGELOG.md +23 -0
  76. data/crates/kobako-transport/Cargo.toml +3 -3
  77. data/crates/kobako-transport/README.md +6 -5
  78. data/crates/kobako-transport/src/abi.rs +1 -1
  79. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  80. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  81. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  83. data/crates/kobako-transport/src/envelope/fault.rs +4 -3
  84. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  85. data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
  86. data/crates/kobako-transport/src/envelope/reply.rs +34 -16
  87. data/crates/kobako-transport/src/envelope/request.rs +19 -9
  88. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  89. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  90. data/crates/kobako-transport/src/envelope.rs +2 -2
  91. data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
  92. data/crates/kobako-wasmtime/Cargo.toml +25 -19
  93. data/crates/kobako-wasmtime/README.md +13 -7
  94. data/crates/kobako-wasmtime/src/abi.rs +75 -49
  95. data/crates/kobako-wasmtime/src/ambient.rs +64 -6
  96. data/crates/kobako-wasmtime/src/cache.rs +188 -77
  97. data/crates/kobako-wasmtime/src/config.rs +77 -5
  98. data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
  99. data/crates/kobako-wasmtime/src/driver.rs +70 -135
  100. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  101. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  102. data/crates/kobako-wasmtime/src/imports.rs +52 -0
  103. data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
  104. data/crates/kobako-wasmtime/src/invocation.rs +43 -74
  105. data/crates/kobako-wasmtime/src/lib.rs +5 -4
  106. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  107. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
  108. data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
  109. data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
  110. data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
  111. data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
  112. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  113. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
  114. data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
  115. data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
  116. data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
  117. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  118. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  119. data/crates/kobako-wasmtime/src/preview1.rs +512 -0
  120. data/crates/kobako-wasmtime/src/trap.rs +45 -25
  121. data/crates/kobako-wasmtime/src/wasi.rs +333 -0
  122. data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
  123. data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
  124. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  125. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
  126. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
  127. data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
  128. data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
  129. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
  130. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  131. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  132. data/data/kobako.wasm +0 -0
  133. data/ext/kobako/Cargo.toml +4 -2
  134. data/ext/kobako/src/runtime/bridge.rs +154 -81
  135. data/ext/kobako/src/runtime/errors.rs +1 -1
  136. data/ext/kobako/src/runtime/escape.rs +67 -0
  137. data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
  138. data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
  139. data/ext/kobako/src/runtime/filesystem.rs +208 -0
  140. data/ext/kobako/src/runtime/gvl.rs +49 -8
  141. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  142. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  143. data/ext/kobako/src/runtime.rs +125 -199
  144. data/lib/kobako/catalog/extensions.rb +5 -5
  145. data/lib/kobako/catalog/flags.rb +2 -14
  146. data/lib/kobako/catalog/grants.rb +30 -0
  147. data/lib/kobako/catalog/handles.rb +6 -6
  148. data/lib/kobako/catalog/mount.rb +19 -0
  149. data/lib/kobako/catalog/services.rb +13 -2
  150. data/lib/kobako/catalog/snippets.rb +11 -15
  151. data/lib/kobako/catalog.rb +11 -0
  152. data/lib/kobako/codec/decoder.rb +10 -9
  153. data/lib/kobako/codec/ext_types.rb +4 -6
  154. data/lib/kobako/codec/handle_walk.rb +4 -6
  155. data/lib/kobako/codec/nesting.rb +7 -7
  156. data/lib/kobako/codec/state.rb +19 -3
  157. data/lib/kobako/codec/utils.rb +1 -1
  158. data/lib/kobako/codec/value_budget.rb +86 -0
  159. data/lib/kobako/codec.rb +7 -0
  160. data/lib/kobako/context.rb +40 -13
  161. data/lib/kobako/errors.rb +15 -14
  162. data/lib/kobako/filesystem.rb +69 -0
  163. data/lib/kobako/payload/arguments.rb +2 -2
  164. data/lib/kobako/pool.rb +6 -6
  165. data/lib/kobako/sandbox.rb +55 -27
  166. data/lib/kobako/sandbox_options.rb +39 -13
  167. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
  168. data/lib/kobako/snippet/source.rb +1 -1
  169. data/lib/kobako/snippet.rb +1 -1
  170. data/lib/kobako/transport/block_failure.rb +35 -0
  171. data/lib/kobako/transport/dispatcher.rb +33 -29
  172. data/lib/kobako/transport/exposure.rb +7 -9
  173. data/lib/kobako/transport/reflection.rb +79 -23
  174. data/lib/kobako/transport/run.rb +5 -5
  175. data/lib/kobako/transport/yielder.rb +9 -31
  176. data/lib/kobako/transport.rb +1 -0
  177. data/lib/kobako/version.rb +1 -1
  178. data/lib/kobako.rb +35 -0
  179. data/release-please-config.json +1 -0
  180. data/sig/kobako/catalog/extensions.rbs +3 -3
  181. data/sig/kobako/catalog/flags.rbs +0 -4
  182. data/sig/kobako/catalog/grants.rbs +13 -0
  183. data/sig/kobako/catalog/handles.rbs +3 -3
  184. data/sig/kobako/catalog/mount.rbs +24 -0
  185. data/sig/kobako/catalog/services.rbs +5 -1
  186. data/sig/kobako/catalog/snippets.rbs +4 -4
  187. data/sig/kobako/catalog.rbs +1 -0
  188. data/sig/kobako/codec/decoder.rbs +1 -1
  189. data/sig/kobako/codec/handle_walk.rbs +1 -1
  190. data/sig/kobako/codec/nesting.rbs +2 -2
  191. data/sig/kobako/codec/state.rbs +7 -1
  192. data/sig/kobako/codec/utils.rbs +1 -1
  193. data/sig/kobako/codec/value_budget.rbs +23 -0
  194. data/sig/kobako/codec.rbs +3 -2
  195. data/sig/kobako/context.rbs +14 -6
  196. data/sig/kobako/errors.rbs +4 -4
  197. data/sig/kobako/filesystem.rbs +93 -0
  198. data/sig/kobako/guest_yielder.rbs +9 -0
  199. data/sig/kobako/payload/arguments.rbs +1 -1
  200. data/sig/kobako/pool.rbs +3 -3
  201. data/sig/kobako/runtime.rbs +21 -49
  202. data/sig/kobako/sandbox.rbs +9 -5
  203. data/sig/kobako/sandbox_options.rbs +25 -3
  204. data/sig/kobako/snapshot.rbs +20 -0
  205. data/sig/kobako/snippet/bytecode.rbs +12 -0
  206. data/sig/kobako/transport/block_failure.rbs +13 -0
  207. data/sig/kobako/transport/dispatcher.rbs +4 -4
  208. data/sig/kobako/transport/exposure.rbs +2 -2
  209. data/sig/kobako/transport/reflection.rbs +14 -1
  210. data/sig/kobako/transport/run.rbs +3 -3
  211. data/sig/kobako/transport/yielder.rbs +3 -7
  212. data/sig/kobako/transport.rbs +3 -3
  213. metadata +116 -15
  214. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  215. data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
  216. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
  217. data/crates/kobako-wasmtime/src/io.rs +0 -286
  218. 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
+ }