kobako 0.29.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 (201) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +217 -10
  4. data/Cargo.lock +139 -141
  5. data/README.md +96 -574
  6. data/ROADMAP.md +2 -1
  7. data/crates/kobako-runtime/CHANGELOG.md +269 -1
  8. data/crates/kobako-runtime/Cargo.toml +25 -15
  9. data/crates/kobako-runtime/README.md +7 -3
  10. data/crates/kobako-runtime/src/dispatch.rs +1 -1
  11. data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
  12. data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
  13. data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
  14. data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
  15. data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
  16. data/crates/kobako-runtime/src/endpoint.rs +166 -0
  17. data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
  18. data/crates/kobako-runtime/src/filesystem/{directory/spelling.rs → bind/lookup.rs} +91 -49
  19. data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
  20. data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
  21. data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
  22. data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
  23. data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
  24. data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
  25. data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
  26. data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
  27. data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
  28. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +227 -734
  29. data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
  30. data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
  31. data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
  32. data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
  33. data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
  34. data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
  35. data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
  36. data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
  37. data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
  38. data/crates/kobako-runtime/src/filesystem.rs +21 -387
  39. data/crates/kobako-runtime/src/hashtable.rs +44 -0
  40. data/crates/kobako-runtime/src/lib.rs +11 -6
  41. data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
  42. data/crates/kobako-runtime/src/locking.rs +218 -0
  43. data/crates/kobako-runtime/src/mount.rs +31 -104
  44. data/crates/kobako-runtime/src/profile.rs +137 -1
  45. data/crates/kobako-runtime/src/sched.rs +210 -0
  46. data/crates/kobako-runtime/src/security.rs +696 -0
  47. data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
  48. data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
  49. data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
  50. data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
  51. data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
  52. data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
  53. data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
  54. data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
  55. data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
  56. data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
  57. data/crates/kobako-runtime/src/vfs/path.rs +64 -0
  58. data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
  59. data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
  60. data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
  61. data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
  62. data/crates/kobako-runtime/src/vfs.rs +60 -0
  63. data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
  64. data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
  65. data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
  66. data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
  67. data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
  68. data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
  69. data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
  70. data/crates/kobako-transport/CHANGELOG.md +8 -2
  71. data/crates/kobako-transport/Cargo.toml +3 -3
  72. data/crates/kobako-transport/README.md +6 -5
  73. data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
  74. data/crates/kobako-transport/src/envelope/call.rs +5 -5
  75. data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
  76. data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
  77. data/crates/kobako-transport/src/envelope/fault.rs +2 -2
  78. data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
  79. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  80. data/crates/kobako-transport/src/envelope/reply.rs +2 -2
  81. data/crates/kobako-transport/src/envelope/request.rs +2 -2
  82. data/crates/kobako-transport/src/envelope/run.rs +1 -1
  83. data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
  84. data/crates/kobako-transport/src/envelope.rs +2 -2
  85. data/crates/kobako-wasmtime/CHANGELOG.md +154 -8
  86. data/crates/kobako-wasmtime/Cargo.toml +15 -14
  87. data/crates/kobako-wasmtime/README.md +9 -5
  88. data/crates/kobako-wasmtime/src/abi.rs +12 -20
  89. data/crates/kobako-wasmtime/src/ambient.rs +1 -2
  90. data/crates/kobako-wasmtime/src/cache.rs +5 -5
  91. data/crates/kobako-wasmtime/src/config.rs +22 -7
  92. data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
  93. data/crates/kobako-wasmtime/src/driver.rs +29 -96
  94. data/crates/kobako-wasmtime/src/exports.rs +5 -16
  95. data/crates/kobako-wasmtime/src/guest.rs +38 -0
  96. data/crates/kobako-wasmtime/src/imports.rs +14 -8
  97. data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
  98. data/crates/kobako-wasmtime/src/invocation.rs +26 -46
  99. data/crates/kobako-wasmtime/src/lib.rs +1 -4
  100. data/crates/kobako-wasmtime/src/limiter.rs +6 -230
  101. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
  102. data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
  103. data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
  104. data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
  105. data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
  106. data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
  107. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
  108. data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
  109. data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
  110. data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
  111. data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
  112. data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
  113. data/crates/kobako-wasmtime/src/preview1.rs +464 -413
  114. data/crates/kobako-wasmtime/src/trap.rs +9 -9
  115. data/crates/kobako-wasmtime/src/wasi.rs +53 -37
  116. data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
  117. data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
  118. data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
  119. data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
  120. data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
  121. data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -1
  122. data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
  123. data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
  124. data/data/kobako.wasm +0 -0
  125. data/ext/kobako/Cargo.toml +1 -1
  126. data/ext/kobako/src/runtime/bridge.rs +1 -1
  127. data/ext/kobako/src/runtime/errors.rs +1 -1
  128. data/ext/kobako/src/runtime/escape.rs +28 -14
  129. data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
  130. data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
  131. data/ext/kobako/src/runtime/filesystem.rs +109 -137
  132. data/ext/kobako/src/runtime/gvl.rs +43 -8
  133. data/ext/kobako/src/runtime/snapshot.rs +1 -1
  134. data/ext/kobako/src/runtime.rs +6 -1
  135. data/lib/kobako/catalog/extensions.rb +5 -5
  136. data/lib/kobako/catalog/flags.rb +2 -2
  137. data/lib/kobako/catalog/handles.rb +6 -6
  138. data/lib/kobako/catalog/mount.rb +2 -10
  139. data/lib/kobako/catalog/services.rb +2 -2
  140. data/lib/kobako/catalog/snippets.rb +7 -7
  141. data/lib/kobako/catalog.rb +1 -1
  142. data/lib/kobako/codec/decoder.rb +10 -10
  143. data/lib/kobako/codec/ext_types.rb +3 -5
  144. data/lib/kobako/codec/handle_walk.rb +4 -6
  145. data/lib/kobako/codec/nesting.rb +7 -7
  146. data/lib/kobako/codec/utils.rb +1 -1
  147. data/lib/kobako/codec/value_budget.rb +3 -3
  148. data/lib/kobako/context.rb +5 -5
  149. data/lib/kobako/filesystem.rb +44 -13
  150. data/lib/kobako/payload/arguments.rb +2 -2
  151. data/lib/kobako/pool.rb +4 -4
  152. data/lib/kobako/sandbox.rb +28 -28
  153. data/lib/kobako/sandbox_options.rb +2 -3
  154. data/lib/kobako/snippet/bytecode.rb +1 -1
  155. data/lib/kobako/snippet/source.rb +1 -1
  156. data/lib/kobako/transport/dispatcher.rb +5 -5
  157. data/lib/kobako/transport/exposure.rb +2 -2
  158. data/lib/kobako/transport/reflection.rb +7 -7
  159. data/lib/kobako/transport/run.rb +5 -5
  160. data/lib/kobako/transport/yielder.rb +1 -1
  161. data/lib/kobako/version.rb +1 -1
  162. data/lib/kobako.rb +34 -0
  163. data/release-please-config.json +1 -0
  164. data/sig/kobako/catalog/extensions.rbs +3 -3
  165. data/sig/kobako/catalog/handles.rbs +3 -3
  166. data/sig/kobako/catalog/mount.rbs +0 -4
  167. data/sig/kobako/catalog/services.rbs +1 -1
  168. data/sig/kobako/catalog/snippets.rbs +3 -3
  169. data/sig/kobako/catalog.rbs +1 -1
  170. data/sig/kobako/codec/decoder.rbs +1 -1
  171. data/sig/kobako/codec/handle_walk.rbs +1 -1
  172. data/sig/kobako/codec/nesting.rbs +2 -2
  173. data/sig/kobako/codec/utils.rbs +1 -1
  174. data/sig/kobako/codec/value_budget.rbs +1 -1
  175. data/sig/kobako/codec.rbs +1 -2
  176. data/sig/kobako/context.rbs +1 -1
  177. data/sig/kobako/filesystem.rbs +19 -14
  178. data/sig/kobako/guest_yielder.rbs +1 -1
  179. data/sig/kobako/payload/arguments.rbs +1 -1
  180. data/sig/kobako/pool.rbs +2 -2
  181. data/sig/kobako/runtime.rbs +1 -1
  182. data/sig/kobako/sandbox.rbs +3 -5
  183. data/sig/kobako/sandbox_options.rbs +1 -1
  184. data/sig/kobako/transport/dispatcher.rbs +2 -2
  185. data/sig/kobako/transport/exposure.rbs +1 -1
  186. data/sig/kobako/transport/reflection.rbs +2 -2
  187. data/sig/kobako/transport/run.rbs +3 -3
  188. data/sig/kobako/transport.rbs +1 -1
  189. metadata +67 -19
  190. data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
  191. data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
  192. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
  193. data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
  194. data/crates/kobako-wasmtime/src/capture.rs +0 -96
  195. data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
  196. data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
  197. data/crates/kobako-wasmtime/src/io.rs +0 -89
  198. data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
  199. data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
  200. /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
  201. /data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +0 -0
@@ -1,1100 +0,0 @@
1
- //! `Memory`: a Filesystem held in memory, living as long as the Host
2
- //! App's object does.
3
-
4
- use std::collections::BTreeMap;
5
- use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
6
- use std::sync::{Arc, Mutex, MutexGuard};
7
-
8
- use super::path::{
9
- ancestors, beneath_itself, components, is_beneath, name, normalize_seed, parent,
10
- };
11
- use super::{
12
- DirEntry, File, FileType, Filesystem, FilesystemError, Hold, Metadata, OpenOptions, Rename,
13
- };
14
-
15
- /// Files and directories held in memory. Every Invocation it is handed to
16
- /// shares it, so what one writes the next reads. What its files hold
17
- /// together, and how many entries it holds, are bounded as a tmpfs mount
18
- /// bounds its size and its inodes, so a guest cannot grow it past what
19
- /// the Host App allowed.
20
- pub struct Memory {
21
- /// Each entry is held under its own name in its directory, as a tmpfs
22
- /// holds one name to each entry, so the names it holds stay bounded
23
- /// by its inodes however its directories move.
24
- root: Mutex<Directory>,
25
- space: Arc<Space>,
26
- inodes_follow_limit: bool,
27
- /// The device number this Memory reports, its own as each tmpfs mount
28
- /// has its own, so it shares an identity with no other Filesystem.
29
- dev: u64,
30
- /// The inode number the next directory takes, so each one made has an
31
- /// identity no other directory of this Memory had.
32
- next_ino: AtomicU64,
33
- }
34
-
35
- enum Node {
36
- File(Arc<Mutex<Content>>),
37
- Directory(Directory),
38
- }
39
-
40
- impl Node {
41
- fn file_type(&self) -> FileType {
42
- match self {
43
- Self::File(_) => FileType::File,
44
- Self::Directory(_) => FileType::Directory,
45
- }
46
- }
47
- }
48
-
49
- /// A directory, its inode number, and its entries by name.
50
- struct Directory {
51
- ino: u64,
52
- entries: BTreeMap<String, Node>,
53
- }
54
-
55
- /// What a path names beneath a directory, the directory itself for the
56
- /// empty path.
57
- enum Found<'a> {
58
- File(&'a Arc<Mutex<Content>>),
59
- Directory(&'a Directory),
60
- }
61
-
62
- impl Found<'_> {
63
- fn file_type(&self) -> FileType {
64
- match self {
65
- Self::File(_) => FileType::File,
66
- Self::Directory(_) => FileType::Directory,
67
- }
68
- }
69
- }
70
-
71
- impl Directory {
72
- /// What `path` names beneath this directory, or why it names nothing:
73
- /// a file standing where a directory above it should be, or simply no
74
- /// entry.
75
- fn find(&self, path: &str) -> Result<Found<'_>, FilesystemError> {
76
- let mut found = Found::Directory(self);
77
- for name in components(path) {
78
- found = match found {
79
- Found::Directory(dir) => match dir.entries.get(name) {
80
- Some(Node::File(data)) => Found::File(data),
81
- Some(Node::Directory(dir)) => Found::Directory(dir),
82
- None => return Err(FilesystemError::NotFound),
83
- },
84
- Found::File(_) => return Err(FilesystemError::NotADirectory),
85
- };
86
- }
87
- Ok(found)
88
- }
89
-
90
- /// The directory `path` names beneath this one.
91
- fn directory_mut(&mut self, path: &str) -> Result<&mut Directory, FilesystemError> {
92
- let mut dir = self;
93
- for name in components(path) {
94
- dir = match dir.entries.get_mut(name) {
95
- Some(Node::Directory(inner)) => inner,
96
- Some(Node::File(_)) => return Err(FilesystemError::NotADirectory),
97
- None => return Err(FilesystemError::NotFound),
98
- };
99
- }
100
- Ok(dir)
101
- }
102
-
103
- /// The entries of the directory a new entry at `path` goes into, which
104
- /// must already be one, and the entry's name there.
105
- fn parent_mut<'a>(
106
- &mut self,
107
- path: &'a str,
108
- ) -> Result<(&mut BTreeMap<String, Node>, &'a str), FilesystemError> {
109
- Ok((&mut self.directory_mut(parent(path))?.entries, name(path)))
110
- }
111
- }
112
-
113
- /// What a Memory holds: the bytes of its files, and its entries, each
114
- /// file and directory and the root counting one as an inode does.
115
- struct Space {
116
- bytes: Budget,
117
- inodes: Budget,
118
- }
119
-
120
- /// How much of one thing is used, and the most that may be.
121
- struct Budget {
122
- limit: AtomicUsize,
123
- used: AtomicUsize,
124
- }
125
-
126
- impl Budget {
127
- fn new(limit: Option<usize>) -> Self {
128
- Self {
129
- limit: AtomicUsize::new(limit.unwrap_or(usize::MAX)),
130
- used: AtomicUsize::new(0),
131
- }
132
- }
133
-
134
- fn limit(&self) -> Option<usize> {
135
- Some(self.limit.load(Ordering::Relaxed)).filter(|&limit| limit != usize::MAX)
136
- }
137
-
138
- fn set_limit(&self, limit: Option<usize>) {
139
- self.limit
140
- .store(limit.unwrap_or(usize::MAX), Ordering::Relaxed);
141
- }
142
-
143
- /// Count `amount` more, refusing when it would pass the limit.
144
- fn take(&self, amount: usize) -> Result<(), FilesystemError> {
145
- let limit = self.limit.load(Ordering::Relaxed);
146
- self.used
147
- .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |used| {
148
- used.checked_add(amount).filter(|&total| total <= limit)
149
- })
150
- .map(drop)
151
- .map_err(|_| FilesystemError::StorageFull)
152
- }
153
-
154
- /// Count `amount` more without refusing, for what the Host App seeds.
155
- fn add(&self, amount: usize) {
156
- self.used.fetch_add(amount, Ordering::Relaxed);
157
- }
158
-
159
- fn give(&self, amount: usize) {
160
- self.used.fetch_sub(amount, Ordering::Relaxed);
161
- }
162
- }
163
-
164
- /// One file's bytes, counted against its Memory's space, with the file's
165
- /// inode, for as long as they live, which outlasts the file's name while
166
- /// a handle holds them.
167
- struct Content {
168
- bytes: Vec<u8>,
169
- space: Arc<Space>,
170
- }
171
-
172
- impl Content {
173
- /// A file the Host App seeds, counted but never refused.
174
- fn seed(bytes: Vec<u8>, space: &Arc<Space>) -> Arc<Mutex<Self>> {
175
- space.bytes.add(bytes.len());
176
- space.inodes.add(1);
177
- Self::new(bytes, space)
178
- }
179
-
180
- /// An empty file the guest creates, refused once no inode is left.
181
- fn create(space: &Arc<Space>) -> Result<Arc<Mutex<Self>>, FilesystemError> {
182
- space.inodes.take(1)?;
183
- Ok(Self::new(Vec::new(), space))
184
- }
185
-
186
- fn new(bytes: Vec<u8>, space: &Arc<Space>) -> Arc<Mutex<Self>> {
187
- Arc::new(Mutex::new(Self {
188
- bytes,
189
- space: space.clone(),
190
- }))
191
- }
192
-
193
- /// Resize to `len`, refusing a growth past the space left or past what
194
- /// can be allocated, rather than aborting the host process.
195
- fn resize(&mut self, len: usize) -> Result<(), FilesystemError> {
196
- let growth = len.saturating_sub(self.bytes.len());
197
- self.space.bytes.take(growth)?;
198
- if self.bytes.try_reserve_exact(growth).is_err() {
199
- self.space.bytes.give(growth);
200
- return Err(FilesystemError::StorageFull);
201
- }
202
- self.space.bytes.give(self.bytes.len().saturating_sub(len));
203
- self.bytes.resize(len, 0);
204
- Ok(())
205
- }
206
- }
207
-
208
- impl Drop for Content {
209
- fn drop(&mut self) {
210
- self.space.bytes.give(self.bytes.len());
211
- self.space.inodes.give(1);
212
- }
213
- }
214
-
215
- /// The inodes a limit of `bytes` allows unless given its own: one for
216
- /// each 4 KiB page the bytes span, as a tmpfs counts one page to each
217
- /// file, and one for the root.
218
- fn inodes_for(bytes: Option<usize>) -> Option<usize> {
219
- bytes.map(|bytes| bytes.div_ceil(PAGE).saturating_add(1))
220
- }
221
-
222
- const PAGE: usize = 4096;
223
-
224
- impl Memory {
225
- /// The most bytes a Memory's files hold unless given a limit of their
226
- /// own: the size Docker gives a `--tmpfs` mount.
227
- pub const DEFAULT_LIMIT: usize = 64 * 1024 * 1024;
228
-
229
- pub fn new() -> Self {
230
- let space = Space {
231
- bytes: Budget::new(Some(Self::DEFAULT_LIMIT)),
232
- inodes: Budget::new(inodes_for(Some(Self::DEFAULT_LIMIT))),
233
- };
234
- space.inodes.add(1);
235
- Self {
236
- root: Mutex::new(Directory {
237
- ino: 1,
238
- entries: BTreeMap::new(),
239
- }),
240
- space: Arc::new(space),
241
- inodes_follow_limit: true,
242
- dev: Metadata::unique_device(),
243
- next_ino: AtomicU64::new(2),
244
- }
245
- }
246
-
247
- /// A directory with an inode number no directory of this Memory had.
248
- fn new_directory(&self) -> Node {
249
- Node::Directory(Directory {
250
- ino: self.next_ino.fetch_add(1, Ordering::Relaxed),
251
- entries: BTreeMap::new(),
252
- })
253
- }
254
-
255
- /// This Memory with its files holding at most `limit` bytes together,
256
- /// or without bound for `None`. Unless given inodes of their own, its
257
- /// entries follow the limit: one for each 4 KiB it spans, and the
258
- /// root. What it was seeded with counts, but is never refused.
259
- pub fn with_limit(self, limit: Option<usize>) -> Self {
260
- self.space.bytes.set_limit(limit);
261
- if self.inodes_follow_limit {
262
- self.space.inodes.set_limit(inodes_for(limit));
263
- }
264
- self
265
- }
266
-
267
- /// This Memory holding at most `inodes` entries, the root among them,
268
- /// or without bound for `None`, as a tmpfs mount's `nr_inodes` does.
269
- pub fn with_inodes(mut self, inodes: Option<usize>) -> Self {
270
- self.space.inodes.set_limit(inodes);
271
- self.inodes_follow_limit = false;
272
- self
273
- }
274
-
275
- /// An empty Memory bounded as this one is.
276
- pub(crate) fn to_empty(&self) -> Self {
277
- let memory = Self::new().with_limit(self.space.bytes.limit());
278
- if self.inodes_follow_limit {
279
- memory
280
- } else {
281
- memory.with_inodes(self.space.inodes.limit())
282
- }
283
- }
284
-
285
- fn root(&self) -> MutexGuard<'_, Directory> {
286
- self.root
287
- .lock()
288
- .unwrap_or_else(|poisoned| poisoned.into_inner())
289
- }
290
- }
291
-
292
- impl Default for Memory {
293
- fn default() -> Self {
294
- Self::new()
295
- }
296
- }
297
-
298
- /// Why a list of files cannot seed a Memory as given.
299
- #[derive(Clone, Debug, PartialEq, Eq)]
300
- #[non_exhaustive]
301
- pub enum SeedError {
302
- /// A path naming the root, where no file can stand.
303
- Root,
304
- /// A file given twice.
305
- Twice(String),
306
- /// A path given as a file where another path needs a directory, or
307
- /// the reverse.
308
- FileAndDirectory(String),
309
- /// A path naming `..`, which no runtime hands a Filesystem, so the
310
- /// file would never be reached.
311
- DotDot(String),
312
- }
313
-
314
- impl std::fmt::Display for SeedError {
315
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
316
- match self {
317
- Self::Root => write!(f, "a Memory seeds no file at its root"),
318
- Self::Twice(path) => write!(f, "a Memory seeds {path} twice"),
319
- Self::FileAndDirectory(path) => {
320
- write!(f, "a Memory seeds {path} as both a file and a directory")
321
- }
322
- Self::DotDot(path) => write!(f, "a Memory seeds no path through `..`, got {path}"),
323
- }
324
- }
325
- }
326
-
327
- impl std::error::Error for SeedError {}
328
-
329
- impl Memory {
330
- /// A Memory seeded with files, each `(path, contents)`, its directories
331
- /// created along the way, or why no Memory holds the list as given.
332
- pub fn try_from_iter<P, C, I>(files: I) -> Result<Self, SeedError>
333
- where
334
- P: AsRef<str>,
335
- C: Into<Vec<u8>>,
336
- I: IntoIterator<Item = (P, C)>,
337
- {
338
- let memory = Self::new();
339
- {
340
- let mut root = memory.root();
341
- for (path, contents) in files {
342
- let path = normalize_seed(path.as_ref());
343
- if path.is_empty() {
344
- return Err(SeedError::Root);
345
- }
346
- if path.split('/').any(|name| name == "..") {
347
- return Err(SeedError::DotDot(path));
348
- }
349
- let mut dir = &mut *root;
350
- for above in ancestors(&path) {
351
- let node = dir
352
- .entries
353
- .entry(name(above).to_string())
354
- .or_insert_with(|| {
355
- memory.space.inodes.add(1);
356
- memory.new_directory()
357
- });
358
- dir = match node {
359
- Node::Directory(inner) => inner,
360
- Node::File(_) => {
361
- return Err(SeedError::FileAndDirectory(above.to_string()))
362
- }
363
- };
364
- }
365
- let file_name = name(&path).to_string();
366
- match dir.entries.get(&file_name) {
367
- Some(Node::Directory(_)) => return Err(SeedError::FileAndDirectory(path)),
368
- Some(Node::File(_)) => return Err(SeedError::Twice(path)),
369
- None => {}
370
- }
371
- dir.entries.insert(
372
- file_name,
373
- Node::File(Content::seed(contents.into(), &memory.space)),
374
- );
375
- }
376
- }
377
- Ok(memory)
378
- }
379
- }
380
-
381
- /// A Memory seeded with files, each `(path, contents)`, its directories
382
- /// created along the way.
383
- ///
384
- /// # Panics
385
- ///
386
- /// When `Memory::try_from_iter` refuses the list: a list written into the
387
- /// Host App's own code that no Memory holds is a defect in that code.
388
- impl<P: AsRef<str>, C: Into<Vec<u8>>> FromIterator<(P, C)> for Memory {
389
- fn from_iter<I: IntoIterator<Item = (P, C)>>(files: I) -> Self {
390
- Self::try_from_iter(files).unwrap_or_else(|error| panic!("{error}"))
391
- }
392
- }
393
-
394
- impl Filesystem for Memory {
395
- fn open(&self, path: &str, options: &OpenOptions) -> Result<Box<dyn File>, FilesystemError> {
396
- let mut root = self.root();
397
- let data = match root.find(path) {
398
- Ok(Found::Directory(_)) => return Err(FilesystemError::IsADirectory),
399
- Ok(Found::File(_)) if options.create_new => return Err(FilesystemError::AlreadyExists),
400
- Ok(Found::File(data)) => {
401
- if options.truncate {
402
- lock(data).resize(0)?;
403
- }
404
- data.clone()
405
- }
406
- Err(FilesystemError::NotFound) if options.create || options.create_new => {
407
- let (entries, name) = root.parent_mut(path)?;
408
- let data = Content::create(&self.space)?;
409
- entries.insert(name.to_string(), Node::File(data.clone()));
410
- data
411
- }
412
- Err(error) => return Err(error),
413
- };
414
- Ok(Box::new(MemoryFile { data }))
415
- }
416
-
417
- fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
418
- match self.root().find(path)? {
419
- Found::File(data) => Ok(Metadata::new(FileType::File, lock(data).bytes.len() as u64)),
420
- Found::Directory(dir) => {
421
- Ok(Metadata::new(FileType::Directory, 0).with_identity(self.dev, dir.ino))
422
- }
423
- }
424
- }
425
-
426
- /// A Memory holds no links.
427
- fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
428
- self.metadata(path)
429
- }
430
-
431
- /// A Memory matches names byte for byte.
432
- fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
433
- Ok(path.to_string())
434
- }
435
-
436
- /// A Memory holds no links to follow.
437
- fn canonicalize_beneath(&self, _base: &str, path: &str) -> Result<String, FilesystemError> {
438
- Ok(path.to_string())
439
- }
440
-
441
- /// A Memory holds no links a Policy needs kept from being followed.
442
- fn for_policy(&self) -> Option<Arc<dyn Filesystem>> {
443
- None
444
- }
445
-
446
- /// A Memory never hands out a directory's identity twice.
447
- fn hold_directory(&self, _path: &str) -> Result<Option<Hold>, FilesystemError> {
448
- Ok(None)
449
- }
450
-
451
- fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
452
- match self.root().find(path)? {
453
- Found::Directory(dir) => Ok(dir
454
- .entries
455
- .iter()
456
- .map(|(name, node)| DirEntry::new(name.as_str(), node.file_type()))
457
- .collect()),
458
- Found::File(_) => Err(FilesystemError::NotADirectory),
459
- }
460
- }
461
-
462
- fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
463
- let mut root = self.root();
464
- if root.find(path).is_ok() {
465
- return Err(FilesystemError::AlreadyExists);
466
- }
467
- let (entries, name) = root.parent_mut(path)?;
468
- self.space.inodes.take(1)?;
469
- entries.insert(name.to_string(), self.new_directory());
470
- Ok(())
471
- }
472
-
473
- /// The root stays, as a mount point does.
474
- fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
475
- let mut root = self.root();
476
- if path.is_empty() {
477
- return Err(if root.entries.is_empty() {
478
- FilesystemError::ResourceBusy
479
- } else {
480
- FilesystemError::DirectoryNotEmpty
481
- });
482
- }
483
- let (entries, name) = root.parent_mut(path)?;
484
- match entries.get(name) {
485
- Some(Node::File(_)) => Err(FilesystemError::NotADirectory),
486
- Some(Node::Directory(dir)) if !dir.entries.is_empty() => {
487
- Err(FilesystemError::DirectoryNotEmpty)
488
- }
489
- Some(Node::Directory(_)) => {
490
- entries.remove(name);
491
- self.space.inodes.give(1);
492
- Ok(())
493
- }
494
- None => Err(FilesystemError::NotFound),
495
- }
496
- }
497
-
498
- fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
499
- let mut root = self.root();
500
- if path.is_empty() {
501
- return Err(FilesystemError::IsADirectory);
502
- }
503
- let (entries, name) = root.parent_mut(path)?;
504
- match entries.get(name) {
505
- Some(Node::File(_)) => {
506
- entries.remove(name);
507
- Ok(())
508
- }
509
- Some(Node::Directory(_)) => Err(FilesystemError::IsADirectory),
510
- None => Err(FilesystemError::NotFound),
511
- }
512
- }
513
-
514
- /// The entry moves under its new name with all it holds, so no name
515
- /// beneath it changes.
516
- fn rename(&self, from: &str, to: &str, rename: Rename) -> Result<(), FilesystemError> {
517
- let mut root = self.root();
518
- let moving = root.find(from)?.file_type();
519
- if from == to {
520
- return Ok(());
521
- }
522
- rename.check(moving)?;
523
- if is_beneath(to, from) {
524
- return Err(beneath_itself(moving));
525
- }
526
- root.parent_mut(to)?;
527
- match (moving, root.find(to)) {
528
- (FileType::File, Ok(Found::Directory(_))) => return Err(FilesystemError::IsADirectory),
529
- (FileType::Directory, Ok(Found::File(_))) => {
530
- return Err(FilesystemError::NotADirectory)
531
- }
532
- (FileType::Directory, Ok(Found::Directory(dir))) if !dir.entries.is_empty() => {
533
- return Err(FilesystemError::DirectoryNotEmpty)
534
- }
535
- _ => {}
536
- }
537
- let (entries, name) = root.parent_mut(from)?;
538
- let node = entries.remove(name).ok_or(FilesystemError::NotFound)?;
539
- let (entries, name) = root.parent_mut(to)?;
540
- if let Some(Node::Directory(_)) = entries.insert(name.to_string(), node) {
541
- self.space.inodes.give(1);
542
- }
543
- Ok(())
544
- }
545
-
546
- /// Every Invocation shares a Memory.
547
- fn for_invocation(&self) -> Option<Arc<dyn Filesystem>> {
548
- None
549
- }
550
- }
551
-
552
- fn lock(data: &Mutex<Content>) -> MutexGuard<'_, Content> {
553
- data.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
554
- }
555
-
556
- fn write(data: &mut Content, buf: &[u8], start: usize) -> Result<usize, FilesystemError> {
557
- if buf.is_empty() {
558
- return Ok(0);
559
- }
560
- let end = start
561
- .checked_add(buf.len())
562
- .ok_or(FilesystemError::InvalidInput)?;
563
- if data.bytes.len() < end {
564
- data.resize(end)?;
565
- }
566
- data.bytes[start..end].copy_from_slice(buf);
567
- Ok(buf.len())
568
- }
569
-
570
- /// An open file of a Memory, sharing its bytes with every other handle on
571
- /// the same path.
572
- struct MemoryFile {
573
- data: Arc<Mutex<Content>>,
574
- }
575
-
576
- impl File for MemoryFile {
577
- fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, FilesystemError> {
578
- let data = &lock(&self.data).bytes;
579
- let start = usize::try_from(offset)
580
- .unwrap_or(usize::MAX)
581
- .min(data.len());
582
- let read = buf.len().min(data.len() - start);
583
- buf[..read].copy_from_slice(&data[start..start + read]);
584
- Ok(read)
585
- }
586
-
587
- fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, FilesystemError> {
588
- let start = usize::try_from(offset).map_err(|_| FilesystemError::InvalidInput)?;
589
- write(&mut lock(&self.data), buf, start)
590
- }
591
-
592
- /// The end is read and written under one lock, so another handle's
593
- /// append lands before or after this one, never over it.
594
- fn append(&mut self, buf: &[u8]) -> Result<(u64, usize), FilesystemError> {
595
- let mut data = lock(&self.data);
596
- let start = data.bytes.len();
597
- Ok((start as u64, write(&mut data, buf, start)?))
598
- }
599
-
600
- fn metadata(&self) -> Result<Metadata, FilesystemError> {
601
- Ok(Metadata::new(
602
- FileType::File,
603
- lock(&self.data).bytes.len() as u64,
604
- ))
605
- }
606
-
607
- fn set_len(&mut self, len: u64) -> Result<(), FilesystemError> {
608
- let len = usize::try_from(len).map_err(|_| FilesystemError::InvalidInput)?;
609
- lock(&self.data).resize(len)
610
- }
611
- }
612
-
613
- #[cfg(test)]
614
- mod tests {
615
- use super::*;
616
-
617
- fn read_all(fs: &dyn Filesystem, path: &str) -> Vec<u8> {
618
- let mut file = fs
619
- .open(
620
- path,
621
- &OpenOptions {
622
- read: true,
623
- ..OpenOptions::default()
624
- },
625
- )
626
- .expect("the file must open");
627
- let mut buf = vec![0; 64];
628
- let read = file.read_at(&mut buf, 0).expect("the file must read");
629
- buf.truncate(read);
630
- buf
631
- }
632
-
633
- // @behavior FS-001
634
- #[test]
635
- fn a_memory_serves_the_files_it_was_seeded_with() {
636
- let memory: Memory = [("/config/app.yml", "name: kobako")].into_iter().collect();
637
- assert_eq!(
638
- read_all(&memory, "config/app.yml"),
639
- b"name: kobako",
640
- "a file seeded into Memory through FromIterator must read back its content"
641
- );
642
- }
643
-
644
- // @behavior FS-067
645
- #[test]
646
- fn a_memory_tried_with_a_list_it_cannot_hold_answers_why() {
647
- let refused = [
648
- Memory::try_from_iter([("/", "x")]),
649
- Memory::try_from_iter([("a", "x"), ("a/b", "y")]),
650
- Memory::try_from_iter([("a", "x"), ("/a", "y")]),
651
- ]
652
- .map(|tried| tried.map(drop));
653
- assert_eq!(
654
- refused,
655
- [
656
- Err(SeedError::Root),
657
- Err(SeedError::FileAndDirectory("a".to_string())),
658
- Err(SeedError::Twice("a".to_string())),
659
- ],
660
- "a list naming the root, a file beneath a file, or a file twice through \
661
- Memory::try_from_iter must answer the SeedError naming it"
662
- );
663
- }
664
-
665
- // A runtime never hands a Filesystem `..`, so a file seeded through one
666
- // would be held where no call reaches it.
667
- // @behavior FS-087
668
- #[test]
669
- fn a_memory_tried_with_a_path_through_dot_dot_answers_why() {
670
- assert_eq!(
671
- Memory::try_from_iter([("a/../b", "x")]).map(drop),
672
- Err(SeedError::DotDot("a/../b".to_string())),
673
- "a list naming a path through `..` through Memory::try_from_iter must answer \
674
- SeedError::DotDot naming it"
675
- );
676
- }
677
-
678
- // @behavior FS-062
679
- #[test]
680
- #[should_panic(expected = "a Memory seeds no file at its root")]
681
- fn a_memory_seeded_at_its_root_panics() {
682
- let _: Memory = [("/", "x")].into_iter().collect();
683
- }
684
-
685
- // @behavior FS-062
686
- #[test]
687
- #[should_panic(expected = "a Memory seeds a as both a file and a directory")]
688
- fn a_memory_seeded_with_a_file_beneath_a_file_panics() {
689
- let _: Memory = [("a", "x"), ("a/b", "y")].into_iter().collect();
690
- }
691
-
692
- // @behavior FS-062
693
- #[test]
694
- #[should_panic(expected = "a Memory seeds a as both a file and a directory")]
695
- fn a_memory_seeded_with_a_file_over_a_directory_panics() {
696
- let _: Memory = [("a/b", "y"), ("a", "x")].into_iter().collect();
697
- }
698
-
699
- // @behavior FS-062
700
- #[test]
701
- #[should_panic(expected = "a Memory seeds a twice")]
702
- fn a_memory_seeded_with_one_path_twice_panics() {
703
- let _: Memory = [("a", "x"), ("a", "y")].into_iter().collect();
704
- }
705
-
706
- // @behavior FS-002
707
- #[test]
708
- fn a_memory_keeps_what_was_written() {
709
- let memory = Memory::new();
710
- let mut file = memory
711
- .open(
712
- "notes.txt",
713
- &OpenOptions {
714
- write: true,
715
- create: true,
716
- ..OpenOptions::default()
717
- },
718
- )
719
- .expect("a new file must be created");
720
- file.write_at(b"kept", 0).expect("the write must land");
721
- drop(file);
722
- assert_eq!(
723
- read_all(&memory, "notes.txt"),
724
- b"kept",
725
- "a file written through Memory::open must read back from the same Memory"
726
- );
727
- }
728
-
729
- // @behavior FS-003
730
- #[test]
731
- fn every_invocation_shares_the_one_memory() {
732
- assert!(
733
- Memory::new().for_invocation().is_none(),
734
- "Memory::for_invocation must hand every Invocation the same Memory"
735
- );
736
- }
737
-
738
- #[test]
739
- fn a_directory_with_entries_is_not_removed() {
740
- let memory: Memory = [("dir/file", "x")].into_iter().collect();
741
- assert_eq!(
742
- memory.remove_dir("dir"),
743
- Err(FilesystemError::DirectoryNotEmpty)
744
- );
745
- assert_eq!(memory.remove_file("dir/file"), Ok(()));
746
- assert_eq!(memory.remove_dir("dir"), Ok(()));
747
- assert_eq!(memory.metadata("dir"), Err(FilesystemError::NotFound));
748
- }
749
-
750
- #[test]
751
- fn a_renamed_directory_carries_its_entries() {
752
- let memory: Memory = [("old/inner/file", "x")].into_iter().collect();
753
- memory
754
- .rename("old", "new", Rename::Any)
755
- .expect("the rename must succeed");
756
- assert_eq!(read_all(&memory, "new/inner/file"), b"x");
757
- assert_eq!(memory.metadata("old"), Err(FilesystemError::NotFound));
758
- }
759
-
760
- fn writable(fs: &dyn Filesystem, path: &str) -> Box<dyn File> {
761
- fs.open(
762
- path,
763
- &OpenOptions {
764
- write: true,
765
- create: true,
766
- ..OpenOptions::default()
767
- },
768
- )
769
- .expect("the file must open")
770
- }
771
-
772
- // @behavior FS-022
773
- #[test]
774
- fn a_write_past_a_memorys_limit_is_refused() {
775
- let memory: Memory = [("file", "x")].into_iter().collect();
776
- let memory = memory.with_limit(Some(4));
777
- assert_eq!(
778
- (
779
- writable(&memory, "file").write_at(b"four", 1),
780
- read_all(&memory, "file")
781
- ),
782
- (Err(FilesystemError::StorageFull), b"x".to_vec()),
783
- "a write growing a Memory's files past its limit through MemoryFile::write_at must \
784
- answer StorageFull and leave the file as it was"
785
- );
786
- }
787
-
788
- // @behavior FS-096
789
- #[test]
790
- fn a_write_of_no_bytes_leaves_a_memory_file_as_it_was() {
791
- let memory: Memory = [("file", "")].into_iter().collect();
792
- assert_eq!(
793
- (
794
- writable(&memory, "file").write_at(b"", 1000),
795
- read_all(&memory, "file")
796
- ),
797
- (Ok(0), Vec::new()),
798
- "an empty write past the end through MemoryFile::write_at must answer 0 and leave \
799
- the file empty"
800
- );
801
- }
802
-
803
- // @behavior FS-023
804
- #[test]
805
- fn a_memory_holds_its_default_limit() {
806
- let memory = Memory::new();
807
- let mut file = writable(&memory, "file");
808
- let mib_64 = 64 * 1024 * 1024;
809
- assert_eq!(
810
- (file.set_len(mib_64), file.set_len(mib_64 + 1)),
811
- (Ok(()), Err(FilesystemError::StorageFull)),
812
- "a Memory given no limit through Memory::new must hold 64 MiB and refuse a byte more"
813
- );
814
- }
815
-
816
- // @behavior FS-024
817
- #[test]
818
- fn a_memory_without_a_limit_grows_past_the_default() {
819
- let memory = Memory::new().with_limit(None);
820
- assert_eq!(
821
- writable(&memory, "file").set_len(Memory::DEFAULT_LIMIT as u64 + 1),
822
- Ok(()),
823
- "a Memory given no limit through Memory::with_limit(None) must grow past 64 MiB"
824
- );
825
- }
826
-
827
- // @behavior FS-025
828
- #[test]
829
- fn a_removed_file_returns_its_space() {
830
- let memory = Memory::new().with_limit(Some(4));
831
- writable(&memory, "first")
832
- .write_at(b"four", 0)
833
- .expect("the first file must fit");
834
- memory.remove_file("first").expect("the file must remove");
835
- assert_eq!(
836
- writable(&memory, "second").write_at(b"four", 0),
837
- Ok(4),
838
- "the space a file removed through Memory::remove_file held must be free for the next"
839
- );
840
- }
841
-
842
- // A length past `isize::MAX` is one no allocator can hand out, so the
843
- // refusal does not depend on how much the host has free.
844
- // @behavior FS-026
845
- #[test]
846
- fn a_file_grown_past_what_can_be_allocated_is_refused() {
847
- let memory: Memory = [("file", "x")].into_iter().collect();
848
- let memory = memory.with_limit(None);
849
- let mut file = writable(&memory, "file");
850
- let unallocatable = isize::MAX as u64 + 1;
851
- assert_eq!(
852
- (
853
- file.set_len(unallocatable),
854
- file.write_at(b"x", unallocatable),
855
- read_all(&memory, "file")
856
- ),
857
- (
858
- Err(FilesystemError::StorageFull),
859
- Err(FilesystemError::StorageFull),
860
- b"x".to_vec()
861
- ),
862
- "a length no allocator can hand out through MemoryFile::set_len and write_at must \
863
- answer StorageFull rather than abort"
864
- );
865
- }
866
-
867
- #[test]
868
- fn a_path_beneath_a_file_is_not_a_directory() {
869
- let memory: Memory = [("file", "x")].into_iter().collect();
870
- assert_eq!(
871
- memory.metadata("file/inner"),
872
- Err(FilesystemError::NotADirectory)
873
- );
874
- }
875
-
876
- fn create(fs: &dyn Filesystem, path: &str) -> Result<(), FilesystemError> {
877
- fs.open(
878
- path,
879
- &OpenOptions {
880
- write: true,
881
- create: true,
882
- ..OpenOptions::default()
883
- },
884
- )
885
- .map(drop)
886
- }
887
-
888
- // Three inodes hold the root, one file, and one directory, so neither
889
- // kind of entry gets past the count.
890
- // @behavior FS-030
891
- #[test]
892
- fn a_memory_holds_at_most_its_inodes() {
893
- let memory = Memory::new().with_inodes(Some(3));
894
- assert_eq!(
895
- (
896
- create(&memory, "file"),
897
- memory.create_dir("dir"),
898
- create(&memory, "past"),
899
- memory.create_dir("past_dir"),
900
- ),
901
- (
902
- Ok(()),
903
- Ok(()),
904
- Err(FilesystemError::StorageFull),
905
- Err(FilesystemError::StorageFull)
906
- ),
907
- "a Memory given inodes through Memory::with_inodes must refuse a file or directory \
908
- past them, the root counted, as out of space"
909
- );
910
- }
911
-
912
- // @behavior FS-031
913
- #[test]
914
- fn a_memorys_inodes_follow_its_limit() {
915
- let memory = Memory::new().with_limit(Some(2 * 4096));
916
- assert_eq!(
917
- (
918
- create(&memory, "a"),
919
- create(&memory, "b"),
920
- create(&memory, "c")
921
- ),
922
- (Ok(()), Ok(()), Err(FilesystemError::StorageFull)),
923
- "a Memory given only a limit through Memory::with_limit must hold one entry for each \
924
- 4 KiB of it and the root"
925
- );
926
- }
927
-
928
- // The second handle opened before the first appended, so only an end
929
- // read as the append lands puts its bytes after the first's.
930
- // @behavior FS-036
931
- #[test]
932
- fn a_memory_appends_at_the_end_as_it_stands() {
933
- let memory: Memory = [("log", "a")].into_iter().collect();
934
- let mut first = writable(&memory, "log");
935
- let mut second = writable(&memory, "log");
936
- assert_eq!(
937
- (
938
- first.append(b"bb"),
939
- second.append(b"ccc"),
940
- read_all(&memory, "log")
941
- ),
942
- (Ok((1, 2)), Ok((3, 3)), b"abbccc".to_vec()),
943
- "MemoryFile::append through two handles on one file must land each at the end as it \
944
- stands, answering where it landed"
945
- );
946
- }
947
-
948
- // @behavior FS-035
949
- #[test]
950
- fn a_memory_moves_only_what_a_rename_allows() {
951
- let memory: Memory = [("dir/inner", "x"), ("file", "y")].into_iter().collect();
952
- assert_eq!(
953
- (
954
- memory.rename("dir", "moved", Rename::File),
955
- memory.rename("file", "moved", Rename::Directory),
956
- memory.metadata("dir/inner").map(|m| m.file_type),
957
- memory.metadata("file").map(|m| m.file_type),
958
- ),
959
- (
960
- Err(FilesystemError::CrossesDevices),
961
- Err(FilesystemError::NotADirectory),
962
- Ok(FileType::File),
963
- Ok(FileType::File)
964
- ),
965
- "Memory::rename must refuse a directory a file-only rename names as crossing \
966
- devices, and a file a directory-only rename names as no directory, moving neither"
967
- );
968
- }
969
-
970
- // A directory replaced by a rename gives its inode back as a removed
971
- // one does, so both leave room for the next.
972
- // @behavior FS-034
973
- #[test]
974
- fn a_removed_directory_gives_its_inode_back() {
975
- let memory = Memory::new().with_inodes(Some(3));
976
- memory
977
- .create_dir("a")
978
- .expect("the first directory must fit");
979
- memory
980
- .create_dir("b")
981
- .expect("the second directory must fit");
982
- memory.remove_dir("a").expect("the directory must remove");
983
- let after_removal = memory.create_dir("c");
984
- memory
985
- .rename("c", "b", Rename::Any)
986
- .expect("a directory must replace an empty one");
987
- assert_eq!(
988
- (after_removal, memory.create_dir("d")),
989
- (Ok(()), Ok(())),
990
- "a directory removed through Memory::remove_dir, or replaced through Memory::rename, \
991
- must give its inode back"
992
- );
993
- }
994
-
995
- // The removed file's handle is still open, so its inode is given back
996
- // only when the handle goes, as Linux keeps an unlinked file's inode.
997
- // @behavior FS-032
998
- #[test]
999
- fn a_removed_file_gives_its_inode_back_once_closed() {
1000
- let memory = Memory::new().with_inodes(Some(2));
1001
- let handle = writable(&memory, "first");
1002
- memory.remove_file("first").expect("the file must remove");
1003
- let while_open = create(&memory, "second");
1004
- drop(handle);
1005
- assert_eq!(
1006
- (while_open, create(&memory, "second")),
1007
- (Err(FilesystemError::StorageFull), Ok(())),
1008
- "the inode of a file removed through Memory::remove_file must stay taken while a \
1009
- handle holds it, and be free once it closes"
1010
- );
1011
- }
1012
-
1013
- // A host's `st_dev` never sets the top bit, so a device carrying it
1014
- // cannot name a host's device, nor one another Filesystem took.
1015
- // @behavior FS-085
1016
- #[test]
1017
- fn a_device_of_its_own_stands_apart_from_every_other_device() {
1018
- let (first, second) = (Metadata::unique_device(), Metadata::unique_device());
1019
- let memory = Memory::new().metadata("").expect("the root must stand").dev;
1020
- assert!(
1021
- [first, second, memory].iter().all(|dev| dev >> 63 == 1)
1022
- && first != second
1023
- && memory != first
1024
- && memory != second,
1025
- "devices from Metadata::unique_device and a Memory's root must each set the top bit \
1026
- and differ from one another"
1027
- );
1028
- }
1029
-
1030
- fn names(dir: &Directory) -> Vec<String> {
1031
- dir.entries
1032
- .iter()
1033
- .flat_map(|(name, node)| {
1034
- let beneath = match node {
1035
- Node::Directory(inner) => names(inner),
1036
- Node::File(_) => Vec::new(),
1037
- };
1038
- std::iter::once(name.clone()).chain(beneath)
1039
- })
1040
- .collect()
1041
- }
1042
-
1043
- // Each round nests the file one level deeper, so a Memory holding
1044
- // whole paths would hold names growing with the depth; held under its
1045
- // own name, every entry stays the one letter it was given.
1046
- // @behavior FS-094
1047
- #[test]
1048
- fn a_memory_holds_each_entry_under_its_own_name() {
1049
- let memory: Memory = [("d/f", "x")].into_iter().collect();
1050
- for _ in 0..64 {
1051
- memory
1052
- .create_dir("n")
1053
- .expect("the new directory must be made");
1054
- memory
1055
- .rename("d", "n/d", Rename::Any)
1056
- .expect("the directory must move into the new one");
1057
- memory
1058
- .rename("n", "d", Rename::Any)
1059
- .expect("the new directory must take the old name");
1060
- }
1061
- let held = names(&memory.root());
1062
- assert_eq!(
1063
- (held, read_all(&memory, &format!("{}f", "d/".repeat(65)))),
1064
- (
1065
- std::iter::repeat_n("d".to_string(), 65)
1066
- .chain(["f".to_string()])
1067
- .collect::<Vec<_>>(),
1068
- b"x".to_vec()
1069
- ),
1070
- "a directory moved deeper 64 times through Memory::rename must leave every entry \
1071
- holding only its own name, and the file reading where it now lies"
1072
- );
1073
- }
1074
-
1075
- // @behavior FS-070
1076
- #[test]
1077
- fn a_memory_gives_each_directory_it_makes_an_identity_of_its_own() {
1078
- let memory = Memory::new();
1079
- memory
1080
- .create_dir("dir")
1081
- .expect("the directory must be made");
1082
- let identity = |path| {
1083
- let metadata = memory.metadata(path).expect("the directory must stand");
1084
- (metadata.dev, metadata.ino)
1085
- };
1086
- let before = identity("dir");
1087
- memory
1088
- .rename("dir", "moved", Rename::Any)
1089
- .expect("the directory must move");
1090
- memory
1091
- .create_dir("dir")
1092
- .expect("another directory must be made");
1093
- assert_eq!(
1094
- (identity("moved") == before, identity("dir") == before),
1095
- (true, false),
1096
- "a directory moved through Memory::rename must keep its identity, and one made at its \
1097
- old path through Memory::create_dir must have another"
1098
- );
1099
- }
1100
- }