kobako 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +170 -0
  4. data/Cargo.lock +111 -82
  5. data/README.md +154 -36
  6. data/ROADMAP.md +5 -0
  7. data/SECURITY.md +1 -1
  8. data/crates/kobako-runtime/CHANGELOG.md +86 -0
  9. data/crates/kobako-runtime/Cargo.toml +27 -6
  10. data/crates/kobako-runtime/README.md +13 -8
  11. data/crates/kobako-runtime/src/dispatch.rs +8 -0
  12. data/crates/kobako-runtime/src/filesystem/directory/spelling.rs +194 -0
  13. data/crates/kobako-runtime/src/filesystem/directory.rs +1082 -0
  14. data/crates/kobako-runtime/src/filesystem/memory.rs +1100 -0
  15. data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +88 -0
  16. data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +808 -0
  17. data/crates/kobako-runtime/src/filesystem/overlay.rs +1955 -0
  18. data/crates/kobako-runtime/src/filesystem/path.rs +60 -0
  19. data/crates/kobako-runtime/src/filesystem/temporary.rs +167 -0
  20. data/crates/kobako-runtime/src/filesystem.rs +392 -0
  21. data/crates/kobako-runtime/src/grant.rs +20 -0
  22. data/crates/kobako-runtime/src/lib.rs +11 -1
  23. data/crates/kobako-runtime/src/mount.rs +178 -0
  24. data/crates/kobako-runtime/src/profile.rs +4 -4
  25. data/crates/kobako-runtime/src/runtime.rs +12 -0
  26. data/crates/kobako-runtime/src/snapshot.rs +20 -5
  27. data/crates/kobako-runtime/src/yielder.rs +11 -6
  28. data/crates/kobako-transport/CHANGELOG.md +17 -0
  29. data/crates/kobako-transport/Cargo.toml +1 -1
  30. data/crates/kobako-transport/README.md +1 -1
  31. data/crates/kobako-transport/src/abi.rs +1 -1
  32. data/crates/kobako-transport/src/envelope/fault.rs +2 -1
  33. data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
  34. data/crates/kobako-transport/src/envelope/reply.rs +32 -14
  35. data/crates/kobako-transport/src/envelope/request.rs +18 -8
  36. data/crates/kobako-wasmtime/CHANGELOG.md +119 -0
  37. data/crates/kobako-wasmtime/Cargo.toml +21 -16
  38. data/crates/kobako-wasmtime/README.md +5 -3
  39. data/crates/kobako-wasmtime/src/abi.rs +66 -32
  40. data/crates/kobako-wasmtime/src/ambient.rs +63 -4
  41. data/crates/kobako-wasmtime/src/cache.rs +185 -74
  42. data/crates/kobako-wasmtime/src/config.rs +58 -1
  43. data/crates/kobako-wasmtime/src/descriptors.rs +167 -0
  44. data/crates/kobako-wasmtime/src/dispatch.rs +47 -29
  45. data/crates/kobako-wasmtime/src/driver.rs +46 -44
  46. data/crates/kobako-wasmtime/src/guest_mem.rs +13 -7
  47. data/crates/kobako-wasmtime/src/imports.rs +46 -0
  48. data/crates/kobako-wasmtime/src/instance_pre.rs +17 -45
  49. data/crates/kobako-wasmtime/src/invocation.rs +35 -46
  50. data/crates/kobako-wasmtime/src/io.rs +5 -202
  51. data/crates/kobako-wasmtime/src/lib.rs +4 -0
  52. data/crates/kobako-wasmtime/src/preview1/args.rs +60 -0
  53. data/crates/kobako-wasmtime/src/preview1/beneath.rs +188 -0
  54. data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +609 -0
  55. data/crates/kobako-wasmtime/src/preview1/differential.rs +372 -0
  56. data/crates/kobako-wasmtime/src/preview1/errno.rs +267 -0
  57. data/crates/kobako-wasmtime/src/preview1/fd.rs +672 -0
  58. data/crates/kobako-wasmtime/src/preview1/memory.rs +205 -0
  59. data/crates/kobako-wasmtime/src/preview1/mount_view.rs +2499 -0
  60. data/crates/kobako-wasmtime/src/preview1/path.rs +421 -0
  61. data/crates/kobako-wasmtime/src/preview1/poll.rs +333 -0
  62. data/crates/kobako-wasmtime/src/preview1/sock.rs +30 -0
  63. data/crates/kobako-wasmtime/src/preview1.rs +461 -0
  64. data/crates/kobako-wasmtime/src/trap.rs +39 -19
  65. data/crates/kobako-wasmtime/src/wasi.rs +317 -0
  66. data/crates/kobako-wasmtime/tests/compile_posture.rs +43 -0
  67. data/crates/kobako-wasmtime/tests/guest_file.rs +1054 -0
  68. data/crates/kobako-wasmtime/tests/preamble_flags.rs +3 -2
  69. data/crates/kobako-wasmtime/tests/snippet_compile.rs +4 -3
  70. data/crates/kobako-wasmtime/tests/support/mod.rs +9 -13
  71. data/crates/kobako-wasmtime/tests/unanswered_call.rs +86 -0
  72. data/crates/kobako-wasmtime/tests/unsupported_call.rs +1 -0
  73. data/data/kobako.wasm +0 -0
  74. data/ext/kobako/Cargo.toml +4 -2
  75. data/ext/kobako/src/runtime/bridge.rs +154 -81
  76. data/ext/kobako/src/runtime/escape.rs +53 -0
  77. data/ext/kobako/src/runtime/filesystem/grants.rs +91 -0
  78. data/ext/kobako/src/runtime/filesystem/ruby.rs +400 -0
  79. data/ext/kobako/src/runtime/filesystem.rs +236 -0
  80. data/ext/kobako/src/runtime/gvl.rs +7 -1
  81. data/ext/kobako/src/runtime/pinned_value.rs +31 -0
  82. data/ext/kobako/src/runtime/snapshot.rs +127 -0
  83. data/ext/kobako/src/runtime.rs +120 -199
  84. data/lib/kobako/catalog/flags.rb +2 -14
  85. data/lib/kobako/catalog/grants.rb +30 -0
  86. data/lib/kobako/catalog/mount.rb +27 -0
  87. data/lib/kobako/catalog/services.rb +11 -0
  88. data/lib/kobako/catalog/snippets.rb +5 -9
  89. data/lib/kobako/catalog.rb +11 -0
  90. data/lib/kobako/codec/decoder.rb +1 -0
  91. data/lib/kobako/codec/ext_types.rb +1 -1
  92. data/lib/kobako/codec/state.rb +19 -3
  93. data/lib/kobako/codec/value_budget.rb +86 -0
  94. data/lib/kobako/codec.rb +7 -0
  95. data/lib/kobako/context.rb +38 -11
  96. data/lib/kobako/errors.rb +15 -14
  97. data/lib/kobako/filesystem.rb +38 -0
  98. data/lib/kobako/pool.rb +2 -2
  99. data/lib/kobako/sandbox.rb +40 -12
  100. data/lib/kobako/sandbox_options.rb +39 -12
  101. data/lib/kobako/snippet/{binary.rb → bytecode.rb} +1 -1
  102. data/lib/kobako/snippet.rb +1 -1
  103. data/lib/kobako/transport/block_failure.rb +35 -0
  104. data/lib/kobako/transport/dispatcher.rb +30 -26
  105. data/lib/kobako/transport/exposure.rb +5 -7
  106. data/lib/kobako/transport/reflection.rb +79 -23
  107. data/lib/kobako/transport/yielder.rb +8 -30
  108. data/lib/kobako/transport.rb +1 -0
  109. data/lib/kobako/version.rb +1 -1
  110. data/lib/kobako.rb +1 -0
  111. data/sig/kobako/catalog/flags.rbs +0 -4
  112. data/sig/kobako/catalog/grants.rbs +13 -0
  113. data/sig/kobako/catalog/mount.rbs +28 -0
  114. data/sig/kobako/catalog/services.rbs +4 -0
  115. data/sig/kobako/catalog/snippets.rbs +1 -1
  116. data/sig/kobako/catalog.rbs +1 -0
  117. data/sig/kobako/codec/state.rbs +7 -1
  118. data/sig/kobako/codec/value_budget.rbs +23 -0
  119. data/sig/kobako/codec.rbs +2 -0
  120. data/sig/kobako/context.rbs +14 -6
  121. data/sig/kobako/errors.rbs +4 -4
  122. data/sig/kobako/filesystem.rbs +88 -0
  123. data/sig/kobako/guest_yielder.rbs +9 -0
  124. data/sig/kobako/pool.rbs +1 -1
  125. data/sig/kobako/runtime.rbs +21 -49
  126. data/sig/kobako/sandbox.rbs +7 -1
  127. data/sig/kobako/sandbox_options.rbs +24 -2
  128. data/sig/kobako/snapshot.rbs +20 -0
  129. data/sig/kobako/snippet/bytecode.rbs +12 -0
  130. data/sig/kobako/transport/block_failure.rbs +13 -0
  131. data/sig/kobako/transport/dispatcher.rbs +3 -3
  132. data/sig/kobako/transport/exposure.rbs +1 -1
  133. data/sig/kobako/transport/reflection.rbs +14 -1
  134. data/sig/kobako/transport/yielder.rbs +3 -7
  135. data/sig/kobako/transport.rbs +2 -2
  136. metadata +60 -7
  137. data/sig/kobako/snippet/binary.rbs +0 -12
@@ -0,0 +1,808 @@
1
+ //! The Layers of an Overlay and the rules that make them one view: which
2
+ //! Layer shows a path, how a change lands in a writable Layer, and the
3
+ //! markers that hide what a read-only Layer still holds.
4
+ //!
5
+ //! Each change that touches more than one entry writes its marker or its
6
+ //! replacement before it takes the old one away, as overlayfs builds a
7
+ //! copy before it swaps it in, so a change that fails part way leaves the
8
+ //! view as it was or as it was meant to become.
9
+
10
+ use std::collections::HashSet;
11
+ use std::sync::Arc;
12
+
13
+ use super::super::path::{ancestors, beneath_itself, is_beneath, join, name, parent};
14
+ use super::super::{
15
+ Access, DirEntry, File, FileType, Filesystem, FilesystemError, Hold, Metadata, OpenOptions,
16
+ Rename,
17
+ };
18
+
19
+ /// The prefix of a whiteout, and of every name reserved for a marker.
20
+ const WHITEOUT: &str = ".wh.";
21
+ /// The marker hiding every lower Layer's entries of its directory.
22
+ const OPAQUE: &str = ".wh..wh..opq";
23
+
24
+ /// How a Layer is asked what a path names: following a link there, or not.
25
+ type Lookup = fn(&(dyn Filesystem + 'static), &str) -> Result<Metadata, FilesystemError>;
26
+
27
+ #[derive(Clone)]
28
+ struct Layer {
29
+ filesystem: Arc<dyn Filesystem>,
30
+ access: Access,
31
+ }
32
+
33
+ impl Layer {
34
+ fn writable(&self) -> bool {
35
+ self.access == Access::ReadWrite
36
+ }
37
+
38
+ fn find(&self, path: &str) -> Result<Option<Metadata>, FilesystemError> {
39
+ self.find_by(path, <dyn Filesystem>::metadata)
40
+ }
41
+
42
+ /// What this Layer holds at `path` as one of the Layers merged beneath
43
+ /// a directory shown: a file standing above `path` here ends the merge
44
+ /// as a file at `path` does, rather than failing it.
45
+ fn find_below(&self, path: &str) -> Result<Option<Metadata>, FilesystemError> {
46
+ match self.find(path) {
47
+ Err(FilesystemError::NotADirectory) => Ok(Some(Metadata::new(FileType::File, 0))),
48
+ found => found,
49
+ }
50
+ }
51
+
52
+ fn find_by(&self, path: &str, lookup: Lookup) -> Result<Option<Metadata>, FilesystemError> {
53
+ match lookup(self.filesystem.as_ref(), path) {
54
+ Ok(metadata) => Ok(Some(metadata)),
55
+ Err(FilesystemError::NotFound) => Ok(None),
56
+ Err(err) => Err(err),
57
+ }
58
+ }
59
+
60
+ /// Whether this Layer has an entry at `path`, a link pointing at
61
+ /// nothing among them. A Layer that cannot answer fails the call, so a
62
+ /// marker it could not read never lets a lower entry show.
63
+ fn holds(&self, path: &str) -> Result<bool, FilesystemError> {
64
+ match self.filesystem.symlink_metadata(path) {
65
+ Ok(_) => Ok(true),
66
+ Err(FilesystemError::NotFound | FilesystemError::NotADirectory) => Ok(false),
67
+ Err(err) => Err(err),
68
+ }
69
+ }
70
+
71
+ /// Whether this Layer hides `path` in every Layer below it: a whiteout
72
+ /// of it or of a directory above it, an opaque marker in one of those
73
+ /// directories or in `path` itself, or a file or link standing at it
74
+ /// or above it, as overlayfs merges only directories. A marker lives
75
+ /// in a directory, so the walk ends at the first directory above
76
+ /// `path` this Layer lacks.
77
+ fn hides(&self, path: &str) -> Result<bool, FilesystemError> {
78
+ if self.holds(&join(path, OPAQUE))? {
79
+ return Ok(true);
80
+ }
81
+ for name in ancestors(path).chain(std::iter::once(path)) {
82
+ if self.holds(&whiteout(name))? || self.holds(&join(parent(name), OPAQUE))? {
83
+ return Ok(true);
84
+ }
85
+ match self.filesystem.symlink_metadata(name) {
86
+ Ok(metadata) if metadata.file_type != FileType::Directory => return Ok(true),
87
+ Ok(_) => {}
88
+ Err(FilesystemError::NotFound | FilesystemError::NotADirectory) if name != path => {
89
+ return Ok(false)
90
+ }
91
+ Err(FilesystemError::NotFound | FilesystemError::NotADirectory) => {}
92
+ Err(err) => return Err(err),
93
+ }
94
+ }
95
+ Ok(false)
96
+ }
97
+ }
98
+
99
+ /// The whiteout hiding `path`, beside it in its directory.
100
+ fn whiteout(path: &str) -> String {
101
+ join(parent(path), &format!("{WHITEOUT}{}", name(path)))
102
+ }
103
+
104
+ fn copy_whole(source: &mut dyn File, target: &mut dyn File) -> Result<(), FilesystemError> {
105
+ let mut buf = vec![0; 64 * 1024];
106
+ let mut offset = 0;
107
+ loop {
108
+ let read = source.read_at(&mut buf, offset)?;
109
+ if read == 0 {
110
+ return Ok(());
111
+ }
112
+ let mut written = 0;
113
+ while written < read {
114
+ match target.write_at(&buf[written..read], offset + written as u64)? {
115
+ 0 => {
116
+ return Err(FilesystemError::Other(
117
+ "a copy up's write wrote nothing".to_string(),
118
+ ))
119
+ }
120
+ wrote => written += wrote,
121
+ }
122
+ }
123
+ offset += read as u64;
124
+ }
125
+ }
126
+
127
+ /// Take every marker out of a writable Layer's copy of the directory
128
+ /// `path`, which the view shows empty, for `act` to change it; any other
129
+ /// entry there means it is not. When `act` fails, or a marker will not
130
+ /// go, the markers taken out are put back, so the view stays as it was.
131
+ fn without_markers(
132
+ filesystem: &dyn Filesystem,
133
+ path: &str,
134
+ act: impl FnOnce() -> Result<(), FilesystemError>,
135
+ ) -> Result<(), FilesystemError> {
136
+ let markers: Vec<String> = filesystem
137
+ .read_dir(path)?
138
+ .into_iter()
139
+ .map(|entry| entry.name)
140
+ .collect();
141
+ if markers.iter().any(|name| !name.starts_with(WHITEOUT)) {
142
+ return Err(FilesystemError::DirectoryNotEmpty);
143
+ }
144
+ let mut cleared = Vec::new();
145
+ let changed = markers
146
+ .iter()
147
+ .try_for_each(|name| {
148
+ let marker = join(path, name);
149
+ filesystem.remove_file(&marker)?;
150
+ cleared.push(marker);
151
+ Ok(())
152
+ })
153
+ .and_then(|()| act());
154
+ if changed.is_err() {
155
+ for marker in cleared {
156
+ let _ = put_marker(filesystem, &marker);
157
+ }
158
+ }
159
+ changed
160
+ }
161
+
162
+ /// An empty file at `path`, as every marker is.
163
+ fn put_marker(filesystem: &dyn Filesystem, path: &str) -> Result<(), FilesystemError> {
164
+ filesystem
165
+ .open(
166
+ path,
167
+ &OpenOptions {
168
+ write: true,
169
+ create: true,
170
+ ..OpenOptions::default()
171
+ },
172
+ )?
173
+ .close()
174
+ }
175
+
176
+ /// Refuse a path naming an entry reserved for the Overlay's markers, in
177
+ /// any case, since a Layer that folds case would hold `.WH.name` as one.
178
+ fn refuse_reserved(path: &str) -> Result<(), FilesystemError> {
179
+ let marker = |name: &str| {
180
+ name.get(..WHITEOUT.len())
181
+ .is_some_and(|start| start.eq_ignore_ascii_case(WHITEOUT))
182
+ };
183
+ if path.split('/').any(marker) {
184
+ return Err(FilesystemError::InvalidInput);
185
+ }
186
+ Ok(())
187
+ }
188
+
189
+ /// Layers stacked into one view, the first added on top.
190
+ #[derive(Clone, Default)]
191
+ pub(super) struct Stack {
192
+ layers: Vec<Layer>,
193
+ }
194
+
195
+ impl Stack {
196
+ pub(super) fn push(&mut self, filesystem: Arc<dyn Filesystem>, access: Access) {
197
+ self.layers.push(Layer { filesystem, access });
198
+ }
199
+
200
+ /// The Layers a new Invocation starts from, or `None` when every Layer
201
+ /// is shared.
202
+ pub(super) fn for_invocation(&self) -> Option<Self> {
203
+ self.with_each_layer(|filesystem| filesystem.for_invocation())
204
+ }
205
+
206
+ /// The Layers a Mount with a Policy reaches, or `None` when no Layer
207
+ /// holds a link.
208
+ pub(super) fn for_policy(&self) -> Option<Self> {
209
+ self.with_each_layer(|filesystem| filesystem.for_policy())
210
+ }
211
+
212
+ /// The Layers with each Filesystem `next` answers in its place, or
213
+ /// `None` when it answers none.
214
+ fn with_each_layer(
215
+ &self,
216
+ next: impl Fn(&Arc<dyn Filesystem>) -> Option<Arc<dyn Filesystem>>,
217
+ ) -> Option<Self> {
218
+ let next: Vec<_> = self
219
+ .layers
220
+ .iter()
221
+ .map(|layer| next(&layer.filesystem))
222
+ .collect();
223
+ if next.iter().all(Option::is_none) {
224
+ return None;
225
+ }
226
+ let layers = self
227
+ .layers
228
+ .iter()
229
+ .zip(next)
230
+ .map(|(layer, next)| Layer {
231
+ filesystem: next.unwrap_or_else(|| layer.filesystem.clone()),
232
+ access: layer.access,
233
+ })
234
+ .collect();
235
+ Some(Self { layers })
236
+ }
237
+
238
+ /// The topmost Layer holding `path` and what it holds there, a link
239
+ /// followed within that Layer, or `None` when no Layer shows it or the
240
+ /// link shown points at nothing.
241
+ fn locate(&self, path: &str) -> Result<Option<(usize, Metadata)>, FilesystemError> {
242
+ match self.locate_by(path, <dyn Filesystem>::symlink_metadata)? {
243
+ Some((index, metadata)) if metadata.file_type == FileType::Symlink => Ok(self.layers
244
+ [index]
245
+ .find(path)?
246
+ .map(|metadata| (index, metadata))),
247
+ found => Ok(found),
248
+ }
249
+ }
250
+
251
+ fn locate_by(
252
+ &self,
253
+ path: &str,
254
+ lookup: Lookup,
255
+ ) -> Result<Option<(usize, Metadata)>, FilesystemError> {
256
+ for (index, layer) in self.layers.iter().enumerate() {
257
+ if let Some(metadata) = layer.find_by(path, lookup)? {
258
+ return Ok(Some((index, metadata)));
259
+ }
260
+ if layer.hides(path)? {
261
+ break;
262
+ }
263
+ }
264
+ Ok(None)
265
+ }
266
+
267
+ /// Every Layer still showing `path`, topmost first, and the topmost
268
+ /// Layer below them whose entry there a file or link above shadows.
269
+ fn holders(&self, path: &str) -> Result<(Vec<usize>, Option<usize>), FilesystemError> {
270
+ let mut holders = Vec::new();
271
+ for (index, layer) in self.layers.iter().enumerate() {
272
+ let holds = layer.holds(path)?;
273
+ if holds {
274
+ holders.push(index);
275
+ }
276
+ if layer.hides(path)? {
277
+ let shadowed = if holds {
278
+ self.beneath(index, path)?
279
+ } else {
280
+ None
281
+ };
282
+ return Ok((holders, shadowed));
283
+ }
284
+ }
285
+ Ok((holders, None))
286
+ }
287
+
288
+ /// The topmost Layer below `above` holding `path` in sight.
289
+ fn beneath(&self, above: usize, path: &str) -> Result<Option<usize>, FilesystemError> {
290
+ for (index, layer) in self.layers.iter().enumerate().skip(above + 1) {
291
+ if layer.holds(path)? {
292
+ return Ok(Some(index));
293
+ }
294
+ if layer.hides(path)? {
295
+ return Ok(None);
296
+ }
297
+ }
298
+ Ok(None)
299
+ }
300
+
301
+ /// The topmost writable Layer, where every change that is not in place
302
+ /// lands.
303
+ fn upper(&self) -> Result<usize, FilesystemError> {
304
+ self.layers
305
+ .iter()
306
+ .position(Layer::writable)
307
+ .ok_or(FilesystemError::ReadOnlyFilesystem)
308
+ }
309
+
310
+ /// Refuse as read-only a change landing at `path` in Layer `into` that
311
+ /// a read-only Layer above it would cover, holding or hiding `path`, so
312
+ /// no change succeeds out of sight.
313
+ fn refuse_covered(&self, path: &str, into: usize) -> Result<(), FilesystemError> {
314
+ for layer in &self.layers[..into] {
315
+ if layer.holds(path)? || layer.hides(path)? {
316
+ return Err(FilesystemError::ReadOnlyFilesystem);
317
+ }
318
+ }
319
+ Ok(())
320
+ }
321
+
322
+ /// Create in Layer `into` each directory above `path` that so far shows
323
+ /// only from another Layer. A Layer holds no link it was not handed, so
324
+ /// a change reaching through a link another Layer holds is refused as
325
+ /// crossing devices rather than landing beside where the link points.
326
+ fn copy_up_directories(&self, path: &str, into: usize) -> Result<(), FilesystemError> {
327
+ let target = &self.layers[into];
328
+ for dir in ancestors(path) {
329
+ if target.holds(dir)? {
330
+ continue;
331
+ }
332
+ match self.locate_by(dir, <dyn Filesystem>::symlink_metadata)? {
333
+ Some((_, metadata)) => match metadata.file_type {
334
+ FileType::Directory => target.filesystem.create_dir(dir)?,
335
+ FileType::Symlink => return Err(FilesystemError::CrossesDevices),
336
+ _ => return Err(FilesystemError::NotADirectory),
337
+ },
338
+ None => return Err(FilesystemError::NotFound),
339
+ }
340
+ }
341
+ Ok(())
342
+ }
343
+
344
+ /// Copy the file at `path` from Layer `from` into Layer `into` whole.
345
+ /// Layer `into` held nothing there, so a copy that fails part way is
346
+ /// taken away again rather than left to hide the whole file. A link
347
+ /// is not copied up, as no Layer is handed one to hold.
348
+ fn copy_up_file(&self, path: &str, from: usize, into: usize) -> Result<(), FilesystemError> {
349
+ if self.layers[from]
350
+ .filesystem
351
+ .symlink_metadata(path)?
352
+ .file_type
353
+ == FileType::Symlink
354
+ {
355
+ return Err(FilesystemError::CrossesDevices);
356
+ }
357
+ self.copy_up_directories(path, into)?;
358
+ let mut source = self.layers[from].filesystem.open(
359
+ path,
360
+ &OpenOptions {
361
+ read: true,
362
+ ..OpenOptions::default()
363
+ },
364
+ )?;
365
+ let target = &self.layers[into].filesystem;
366
+ let mut copy = target.open(
367
+ path,
368
+ &OpenOptions {
369
+ write: true,
370
+ create: true,
371
+ truncate: true,
372
+ ..OpenOptions::default()
373
+ },
374
+ )?;
375
+ let copied = copy_whole(source.as_mut(), copy.as_mut()).and(copy.close());
376
+ if copied.is_err() {
377
+ let _ = target.remove_file(path);
378
+ }
379
+ copied
380
+ }
381
+
382
+ /// Clear the whiteout of `path` in Layer `into` once an entry has
383
+ /// landed there. A whiteout left beside the entry hides only what lies
384
+ /// below, which the entry replaced, so failing to clear it fails
385
+ /// nothing.
386
+ fn unhide(&self, path: &str, into: usize) {
387
+ let _ = self.layers[into].filesystem.remove_file(&whiteout(path));
388
+ }
389
+
390
+ fn mark(&self, path: &str, into: usize) -> Result<(), FilesystemError> {
391
+ self.copy_up_directories(path, into)?;
392
+ put_marker(self.layers[into].filesystem.as_ref(), path)
393
+ }
394
+
395
+ /// The Layer a whiteout over the Layer at `shown` lands in, or `None`
396
+ /// when nothing below needs one; refused as read-only when no writable
397
+ /// Layer sits above it.
398
+ fn whiteout_layer(&self, shown: Option<usize>) -> Result<Option<usize>, FilesystemError> {
399
+ match shown {
400
+ Some(shown) => match self.upper()? {
401
+ upper if upper < shown => Ok(Some(upper)),
402
+ _ => Err(FilesystemError::ReadOnlyFilesystem),
403
+ },
404
+ None => Ok(None),
405
+ }
406
+ }
407
+
408
+ /// Take `path` out of the view: a whiteout over any Layer that keeps it
409
+ /// first, then out of every other Layer holding it. A read-only Layer
410
+ /// keeps it, and so does a writable one `keeps` answers for, so no
411
+ /// Layer changes before each is known to go; `remove` takes it out of
412
+ /// one Layer.
413
+ fn withdraw(
414
+ &self,
415
+ path: &str,
416
+ keeps: impl Fn(&dyn Filesystem) -> Result<bool, FilesystemError>,
417
+ remove: impl Fn(&dyn Filesystem, &str) -> Result<(), FilesystemError>,
418
+ ) -> Result<(), FilesystemError> {
419
+ let (holders, shadowed) = self.holders(path)?;
420
+ let mut kept = Vec::new();
421
+ for &index in &holders {
422
+ let layer = &self.layers[index];
423
+ if !layer.writable() || keeps(layer.filesystem.as_ref())? {
424
+ kept.push(index);
425
+ }
426
+ }
427
+ let shown = kept.first().copied().into_iter().chain(shadowed).min();
428
+ if let Some(upper) = self.whiteout_layer(shown)? {
429
+ self.mark(&whiteout(path), upper)?;
430
+ }
431
+ for &index in &holders {
432
+ if !kept.contains(&index) {
433
+ remove(self.layers[index].filesystem.as_ref(), path)?;
434
+ }
435
+ }
436
+ Ok(())
437
+ }
438
+
439
+ pub(super) fn open(
440
+ &self,
441
+ path: &str,
442
+ options: &OpenOptions,
443
+ ) -> Result<Box<dyn File>, FilesystemError> {
444
+ refuse_reserved(path)?;
445
+ let changes = options.write || options.truncate;
446
+ match self.locate(path)? {
447
+ Some((
448
+ _,
449
+ Metadata {
450
+ file_type: FileType::Directory,
451
+ ..
452
+ },
453
+ )) => Err(FilesystemError::IsADirectory),
454
+ Some(_) if options.create_new => Err(FilesystemError::AlreadyExists),
455
+ Some((index, _)) if !changes || self.layers[index].writable() => {
456
+ self.layers[index].filesystem.open(path, options)
457
+ }
458
+ Some((index, _)) => {
459
+ let into = self.layers[..index]
460
+ .iter()
461
+ .rposition(Layer::writable)
462
+ .ok_or(FilesystemError::ReadOnlyFilesystem)?;
463
+ self.copy_up_file(path, index, into)?;
464
+ self.layers[into].filesystem.open(path, options)
465
+ }
466
+ None if options.create || options.create_new => self.create_file(path, options),
467
+ None => Err(FilesystemError::NotFound),
468
+ }
469
+ }
470
+
471
+ /// A link pointing at nothing still names an entry, so an exclusive
472
+ /// create finds it there; any other create reaches through it, which
473
+ /// only the Layer holding the link may answer. The new file shadows
474
+ /// whatever the whiteout hid before the whiteout goes, so nothing
475
+ /// shows through in between.
476
+ fn create_file(
477
+ &self,
478
+ path: &str,
479
+ options: &OpenOptions,
480
+ ) -> Result<Box<dyn File>, FilesystemError> {
481
+ match self.locate_by(path, <dyn Filesystem>::symlink_metadata)? {
482
+ Some(_) if options.create_new => Err(FilesystemError::AlreadyExists),
483
+ Some((index, _)) if self.layers[index].writable() => {
484
+ self.layers[index].filesystem.open(path, options)
485
+ }
486
+ Some(_) => Err(FilesystemError::CrossesDevices),
487
+ None => {
488
+ let upper = self.upper()?;
489
+ self.refuse_covered(path, upper)?;
490
+ self.copy_up_directories(path, upper)?;
491
+ let file = self.layers[upper].filesystem.open(path, options)?;
492
+ self.unhide(path, upper);
493
+ Ok(file)
494
+ }
495
+ }
496
+ }
497
+
498
+ pub(super) fn metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
499
+ refuse_reserved(path)?;
500
+ let (index, metadata) = self.locate(path)?.ok_or(FilesystemError::NotFound)?;
501
+ self.merge_identity(path, index, metadata)
502
+ }
503
+
504
+ /// What the Layer showing `path` holds there, a link it holds
505
+ /// reported as one even where it points at nothing.
506
+ pub(super) fn symlink_metadata(&self, path: &str) -> Result<Metadata, FilesystemError> {
507
+ refuse_reserved(path)?;
508
+ let (index, metadata) = self
509
+ .locate_by(path, <dyn Filesystem>::symlink_metadata)?
510
+ .ok_or(FilesystemError::NotFound)?;
511
+ self.merge_identity(path, index, metadata)
512
+ }
513
+
514
+ /// `path` spelled as the topmost Layer showing it stores it; a name
515
+ /// no Layer shows keeps its spelling, beneath its directory's.
516
+ pub(super) fn stored_path(&self, path: &str) -> Result<String, FilesystemError> {
517
+ if path.is_empty() {
518
+ return Ok(String::new());
519
+ }
520
+ match self.locate_by(path, <dyn Filesystem>::symlink_metadata) {
521
+ Ok(Some((index, _))) => self.layers[index].filesystem.stored_path(path),
522
+ Ok(None) | Err(FilesystemError::NotFound | FilesystemError::NotADirectory) => {
523
+ Ok(join(&self.stored_path(parent(path))?, name(path)))
524
+ }
525
+ Err(err) => Err(err),
526
+ }
527
+ }
528
+
529
+ /// `path` beneath `base` with its links followed by the Layer showing
530
+ /// the first of them, as a link is followed within the Layer holding
531
+ /// it; a path the merged view shows no link on answers as given.
532
+ pub(super) fn canonicalize_beneath(
533
+ &self,
534
+ base: &str,
535
+ path: &str,
536
+ ) -> Result<String, FilesystemError> {
537
+ if path.is_empty() {
538
+ return Ok(String::new());
539
+ }
540
+ let full = join(base, path);
541
+ let start = full.len() - path.len();
542
+ let ends = full[start..]
543
+ .match_indices('/')
544
+ .map(|(end, _)| start + end)
545
+ .chain([full.len()]);
546
+ for end in ends {
547
+ match self.locate_by(&full[..end], <dyn Filesystem>::symlink_metadata) {
548
+ Ok(Some((index, metadata))) if metadata.file_type == FileType::Symlink => {
549
+ return self.layers[index]
550
+ .filesystem
551
+ .canonicalize_beneath(base, path)
552
+ }
553
+ Ok(Some(_)) => {}
554
+ Ok(None) | Err(FilesystemError::NotFound | FilesystemError::NotADirectory) => break,
555
+ Err(err) => return Err(err),
556
+ }
557
+ }
558
+ Ok(path.to_string())
559
+ }
560
+
561
+ /// `metadata`, found at `path` in the Layer at `index`, as the merged
562
+ /// view reports it: a directory takes the identity of the lowest Layer
563
+ /// still showing it, so copying it up into a writable Layer leaves the
564
+ /// identity as it was.
565
+ fn merge_identity(
566
+ &self,
567
+ path: &str,
568
+ index: usize,
569
+ metadata: Metadata,
570
+ ) -> Result<Metadata, FilesystemError> {
571
+ if metadata.file_type != FileType::Directory {
572
+ return Ok(metadata);
573
+ }
574
+ let (_, lowest) = self.lowest_directory(path, index, metadata)?;
575
+ Ok(metadata.with_identity(lowest.dev, lowest.ino))
576
+ }
577
+
578
+ /// The lowest Layer at or below `index` still showing the directory
579
+ /// `path`, found there as `metadata`, and what it holds there.
580
+ fn lowest_directory(
581
+ &self,
582
+ path: &str,
583
+ index: usize,
584
+ metadata: Metadata,
585
+ ) -> Result<(usize, Metadata), FilesystemError> {
586
+ let mut lowest = (index, metadata);
587
+ for (at, layer) in self.layers.iter().enumerate().skip(index) {
588
+ match layer.find_below(path)? {
589
+ Some(found) if found.file_type == FileType::Directory => lowest = (at, found),
590
+ Some(_) => break,
591
+ None => {}
592
+ }
593
+ if layer.hides(path)? {
594
+ break;
595
+ }
596
+ }
597
+ Ok(lowest)
598
+ }
599
+
600
+ /// The Hold of the Layer whose directory gives `path` its identity.
601
+ pub(super) fn hold_directory(&self, path: &str) -> Result<Option<Hold>, FilesystemError> {
602
+ let (index, metadata) = self.locate(path)?.ok_or(FilesystemError::NotFound)?;
603
+ if metadata.file_type != FileType::Directory {
604
+ return Err(FilesystemError::NotADirectory);
605
+ }
606
+ let (lowest, _) = self.lowest_directory(path, index, metadata)?;
607
+ self.layers[lowest].filesystem.hold_directory(path)
608
+ }
609
+
610
+ pub(super) fn read_dir(&self, path: &str) -> Result<Vec<DirEntry>, FilesystemError> {
611
+ if self.metadata(path)?.file_type != FileType::Directory {
612
+ return Err(FilesystemError::NotADirectory);
613
+ }
614
+ let mut entries: Vec<DirEntry> = Vec::new();
615
+ let mut taken: HashSet<String> = HashSet::new();
616
+ for layer in &self.layers {
617
+ match layer.find_below(path)? {
618
+ Some(Metadata {
619
+ file_type: FileType::Directory,
620
+ ..
621
+ }) => {
622
+ let listed = layer.filesystem.read_dir(path)?;
623
+ for entry in &listed {
624
+ if !entry.name.starts_with(WHITEOUT) && taken.insert(entry.name.clone()) {
625
+ entries.push(entry.clone());
626
+ }
627
+ }
628
+ taken.extend(
629
+ listed
630
+ .iter()
631
+ .filter_map(|entry| entry.name.strip_prefix(WHITEOUT))
632
+ .map(str::to_string),
633
+ );
634
+ if listed.iter().any(|entry| entry.name == OPAQUE) {
635
+ break;
636
+ }
637
+ }
638
+ Some(_) => break,
639
+ None => {}
640
+ }
641
+ if layer.hides(path)? {
642
+ break;
643
+ }
644
+ }
645
+ Ok(entries)
646
+ }
647
+
648
+ /// A directory made where a whiteout hid one is marked opaque before
649
+ /// the whiteout goes, so the lower Layers' entries never show in it.
650
+ pub(super) fn create_dir(&self, path: &str) -> Result<(), FilesystemError> {
651
+ refuse_reserved(path)?;
652
+ if self
653
+ .locate_by(path, <dyn Filesystem>::symlink_metadata)?
654
+ .is_some()
655
+ {
656
+ return Err(FilesystemError::AlreadyExists);
657
+ }
658
+ let upper = self.upper()?;
659
+ self.refuse_covered(path, upper)?;
660
+ self.copy_up_directories(path, upper)?;
661
+ let layer = &self.layers[upper];
662
+ let hid = layer.holds(&whiteout(path))?;
663
+ layer.filesystem.create_dir(path)?;
664
+ if hid {
665
+ if let Err(err) = self.mark(&join(path, OPAQUE), upper) {
666
+ let _ = layer.filesystem.remove_dir(path);
667
+ return Err(err);
668
+ }
669
+ self.unhide(path, upper);
670
+ }
671
+ Ok(())
672
+ }
673
+
674
+ /// A writable Layer's copy of the directory may hold only markers; any
675
+ /// other entry there means the view is not what was found empty. A
676
+ /// copy still holding what a Layer above hides stays, beneath the
677
+ /// whiteout.
678
+ pub(super) fn remove_dir(&self, path: &str) -> Result<(), FilesystemError> {
679
+ if self.symlink_metadata(path)?.file_type != FileType::Directory {
680
+ return Err(FilesystemError::NotADirectory);
681
+ }
682
+ if !self.read_dir(path)?.is_empty() {
683
+ return Err(FilesystemError::DirectoryNotEmpty);
684
+ }
685
+ self.withdraw(
686
+ path,
687
+ |filesystem| {
688
+ Ok(filesystem
689
+ .read_dir(path)?
690
+ .iter()
691
+ .any(|entry| !entry.name.starts_with(WHITEOUT)))
692
+ },
693
+ |filesystem, path| without_markers(filesystem, path, || filesystem.remove_dir(path)),
694
+ )
695
+ }
696
+
697
+ /// A link is removed as itself, wherever it points.
698
+ pub(super) fn remove_file(&self, path: &str) -> Result<(), FilesystemError> {
699
+ if self.symlink_metadata(path)?.file_type == FileType::Directory {
700
+ return Err(FilesystemError::IsADirectory);
701
+ }
702
+ self.withdraw(
703
+ path,
704
+ |_| Ok(false),
705
+ |filesystem, path| filesystem.remove_file(path),
706
+ )
707
+ }
708
+
709
+ /// A directory moves only when the topmost writable Layer alone holds
710
+ /// it; moving one out of another would mean copying up everything
711
+ /// beneath it, so it is refused as crossing devices, as overlayfs
712
+ /// refuses it without `redirect_dir`; so is a link or another kind a
713
+ /// lower Layer holds, which no copy up can make. What the view holds
714
+ /// at `to` is checked as rename(2) checks it, and a directory moved
715
+ /// over one a lower Layer holds is marked opaque before it moves, so
716
+ /// it never lands without the marker. The source is whited out before
717
+ /// it moves, so it never shows from a read-only Layer once moved. What
718
+ /// a lower writable Layer holds is refused as crossing devices, as
719
+ /// unionfs-fuse refuses a rename across writable branches, since moving
720
+ /// it would change two Layers that could fail apart.
721
+ pub(super) fn rename(
722
+ &self,
723
+ from: &str,
724
+ to: &str,
725
+ rename: Rename,
726
+ ) -> Result<(), FilesystemError> {
727
+ refuse_reserved(to)?;
728
+ let moving = self.symlink_metadata(from)?;
729
+ if from == to {
730
+ return Ok(());
731
+ }
732
+ rename.check(moving.file_type)?;
733
+ if is_beneath(to, from) {
734
+ return Err(beneath_itself(moving.file_type));
735
+ }
736
+ let replaced = self
737
+ .locate_by(to, <dyn Filesystem>::symlink_metadata)?
738
+ .map(|(_, metadata)| metadata.file_type);
739
+ // Only a directory replaces a directory, and only an empty one.
740
+ match (moving.file_type == FileType::Directory, replaced) {
741
+ (false, Some(FileType::Directory)) => return Err(FilesystemError::IsADirectory),
742
+ (true, Some(FileType::Directory)) if !self.read_dir(to)?.is_empty() => {
743
+ return Err(FilesystemError::DirectoryNotEmpty)
744
+ }
745
+ (true, Some(found)) if found != FileType::Directory => {
746
+ return Err(FilesystemError::NotADirectory)
747
+ }
748
+ _ => {}
749
+ }
750
+ let upper = self.upper()?;
751
+ self.refuse_covered(to, upper)?;
752
+ let mut replaces_below = false;
753
+ if moving.file_type == FileType::Directory {
754
+ for (index, layer) in self.layers.iter().enumerate() {
755
+ replaces_below |= index != upper && layer.holds(to)?;
756
+ }
757
+ }
758
+ let (holders, shadowed) = self.holders(from)?;
759
+ if holders
760
+ .iter()
761
+ .any(|&index| index != upper && self.layers[index].writable())
762
+ {
763
+ return Err(FilesystemError::CrossesDevices);
764
+ }
765
+ let read_only = holders
766
+ .iter()
767
+ .copied()
768
+ .find(|&index| !self.layers[index].writable());
769
+ let shown_below = self.whiteout_layer(read_only.into_iter().chain(shadowed).min())?;
770
+ match moving.file_type {
771
+ FileType::Directory if holders != [upper] => {
772
+ return Err(FilesystemError::CrossesDevices)
773
+ }
774
+ FileType::File if holders.first() != Some(&upper) => {
775
+ let shown = holders.first().ok_or(FilesystemError::NotFound)?;
776
+ self.copy_up_file(from, *shown, upper)?
777
+ }
778
+ FileType::Symlink | FileType::Unknown if holders.first() != Some(&upper) => {
779
+ return Err(FilesystemError::CrossesDevices)
780
+ }
781
+ _ => {}
782
+ }
783
+ self.copy_up_directories(to, upper)?;
784
+ if let Some(upper) = shown_below {
785
+ self.mark(&whiteout(from), upper)?;
786
+ }
787
+ let filesystem = self.layers[upper].filesystem.as_ref();
788
+ let opaque = join(from, OPAQUE);
789
+ let marks = replaces_below && !self.layers[upper].holds(&opaque)?;
790
+ if marks {
791
+ put_marker(filesystem, &opaque)?;
792
+ }
793
+ let moved = || filesystem.rename(from, to, rename);
794
+ let moved = if replaced == Some(FileType::Directory) && self.layers[upper].holds(to)? {
795
+ without_markers(filesystem, to, moved)
796
+ } else {
797
+ moved()
798
+ };
799
+ if let Err(err) = moved {
800
+ if marks {
801
+ let _ = filesystem.remove_file(&opaque);
802
+ }
803
+ return Err(err);
804
+ }
805
+ self.unhide(to, upper);
806
+ Ok(())
807
+ }
808
+ }