kobako 0.28.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +377 -0
- data/Cargo.lock +181 -154
- data/README.md +99 -459
- data/ROADMAP.md +7 -1
- data/SECURITY.md +1 -1
- data/crates/kobako-runtime/CHANGELOG.md +354 -0
- data/crates/kobako-runtime/Cargo.toml +38 -7
- data/crates/kobako-runtime/README.md +17 -8
- data/crates/kobako-runtime/src/dispatch.rs +9 -1
- data/crates/kobako-runtime/src/endpoint/buffer.rs +192 -0
- data/crates/kobako-runtime/src/endpoint/capture.rs +115 -0
- data/crates/kobako-runtime/src/endpoint/dispatch.rs +253 -0
- data/crates/kobako-runtime/src/endpoint/invoke.rs +194 -0
- data/crates/kobako-runtime/src/endpoint/memory_cap.rs +238 -0
- data/crates/kobako-runtime/src/endpoint.rs +166 -0
- data/crates/kobako-runtime/src/filesystem/bind/fid.rs +82 -0
- data/crates/kobako-runtime/src/filesystem/bind/lookup.rs +236 -0
- data/crates/kobako-runtime/src/filesystem/bind/walk.rs +249 -0
- data/crates/kobako-runtime/src/filesystem/bind.rs +2665 -0
- data/crates/kobako-runtime/src/filesystem/overlay/file.rs +89 -0
- data/crates/kobako-runtime/src/filesystem/overlay/inode.rs +106 -0
- data/crates/kobako-runtime/src/filesystem/overlay/path.rs +285 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/copy_up.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/dir.rs +263 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/namei.rs +135 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack/readdir.rs +57 -0
- data/crates/kobako-runtime/src/filesystem/overlay/stack.rs +301 -0
- data/crates/kobako-runtime/src/filesystem/overlay.rs +3469 -0
- data/crates/kobako-runtime/src/filesystem/path.rs +21 -0
- data/crates/kobako-runtime/src/filesystem/pipe.rs +221 -0
- data/crates/kobako-runtime/src/filesystem/rdir.rs +101 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/tmpfile.rs +32 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/whiteout.rs +85 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs/xattr.rs +22 -0
- data/crates/kobako-runtime/src/filesystem/tmpfs.rs +1985 -0
- data/crates/kobako-runtime/src/filesystem/wrap.rs +224 -0
- data/crates/kobako-runtime/src/filesystem.rs +26 -0
- data/crates/kobako-runtime/src/grant.rs +20 -0
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +16 -1
- data/crates/kobako-runtime/src/locking/seqlock.rs +66 -0
- data/crates/kobako-runtime/src/locking.rs +218 -0
- data/crates/kobako-runtime/src/mount.rs +105 -0
- data/crates/kobako-runtime/src/profile.rs +140 -4
- data/crates/kobako-runtime/src/runtime.rs +12 -0
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- data/crates/kobako-runtime/src/snapshot.rs +20 -5
- data/crates/kobako-runtime/src/vfs/attr.rs +162 -0
- data/crates/kobako-runtime/src/vfs/cred.rs +334 -0
- data/crates/kobako-runtime/src/vfs/dentry.rs +1412 -0
- data/crates/kobako-runtime/src/vfs/errno.rs +153 -0
- data/crates/kobako-runtime/src/vfs/file.rs +1915 -0
- data/crates/kobako-runtime/src/vfs/inode.rs +429 -0
- data/crates/kobako-runtime/src/vfs/kern_path.rs +217 -0
- data/crates/kobako-runtime/src/vfs/mount.rs +59 -0
- data/crates/kobako-runtime/src/vfs/namei.rs +5708 -0
- data/crates/kobako-runtime/src/vfs/openat2.rs +116 -0
- data/crates/kobako-runtime/src/vfs/path.rs +64 -0
- data/crates/kobako-runtime/src/vfs/readdir.rs +163 -0
- data/crates/kobako-runtime/src/vfs/stat.rs +157 -0
- data/crates/kobako-runtime/src/vfs/super_block.rs +122 -0
- data/crates/kobako-runtime/src/vfs/utimes.rs +289 -0
- data/crates/kobako-runtime/src/vfs.rs +60 -0
- data/crates/kobako-runtime/src/yielder.rs +11 -6
- data/crates/kobako-runtime/tests/linux_baseline/calls.rs +171 -0
- data/crates/kobako-runtime/tests/linux_baseline/concurrent.rs +276 -0
- data/crates/kobako-runtime/tests/linux_baseline/kobako.rs +323 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux-baseline-expected.txt +16 -0
- data/crates/kobako-runtime/tests/linux_baseline/linux.rs +449 -0
- data/crates/kobako-runtime/tests/linux_baseline/main.rs +611 -0
- data/crates/kobako-runtime/tests/linux_baseline/permissions.rs +236 -0
- data/crates/kobako-transport/CHANGELOG.md +23 -0
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- data/crates/kobako-transport/src/abi.rs +1 -1
- data/crates/kobako-transport/src/envelope/bytes.rs +33 -25
- data/crates/kobako-transport/src/envelope/call.rs +5 -5
- data/crates/kobako-transport/src/envelope/compile_reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/error_record.rs +3 -3
- data/crates/kobako-transport/src/envelope/fault.rs +4 -3
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +24 -24
- data/crates/kobako-transport/src/envelope/reply.rs +34 -16
- data/crates/kobako-transport/src/envelope/request.rs +19 -9
- data/crates/kobako-transport/src/envelope/run.rs +1 -1
- data/crates/kobako-transport/src/envelope/unsupported.rs +2 -2
- data/crates/kobako-transport/src/envelope.rs +2 -2
- data/crates/kobako-wasmtime/CHANGELOG.md +265 -0
- data/crates/kobako-wasmtime/Cargo.toml +25 -19
- data/crates/kobako-wasmtime/README.md +13 -7
- data/crates/kobako-wasmtime/src/abi.rs +75 -49
- data/crates/kobako-wasmtime/src/ambient.rs +64 -6
- data/crates/kobako-wasmtime/src/cache.rs +188 -77
- data/crates/kobako-wasmtime/src/config.rs +77 -5
- data/crates/kobako-wasmtime/src/descriptors.rs +200 -0
- data/crates/kobako-wasmtime/src/driver.rs +70 -135
- data/crates/kobako-wasmtime/src/exports.rs +5 -16
- data/crates/kobako-wasmtime/src/guest.rs +38 -0
- data/crates/kobako-wasmtime/src/imports.rs +52 -0
- data/crates/kobako-wasmtime/src/instance_pre.rs +18 -46
- data/crates/kobako-wasmtime/src/invocation.rs +43 -74
- data/crates/kobako-wasmtime/src/lib.rs +5 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +632 -0
- data/crates/kobako-wasmtime/src/preview1/differential.rs +486 -0
- data/crates/kobako-wasmtime/src/preview1/errno.rs +135 -0
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1420 -0
- data/crates/kobako-wasmtime/src/preview1/memory.rs +172 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +3766 -0
- data/crates/kobako-wasmtime/src/preview1/path.rs +302 -0
- data/crates/kobako-wasmtime/src/preview1/poll.rs +381 -0
- data/crates/kobako-wasmtime/src/preview1/sock.rs +13 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite-expected.txt +6 -0
- data/crates/kobako-wasmtime/src/preview1/testsuite.rs +157 -0
- data/crates/kobako-wasmtime/src/preview1.rs +512 -0
- data/crates/kobako-wasmtime/src/trap.rs +45 -25
- data/crates/kobako-wasmtime/src/wasi.rs +333 -0
- data/crates/kobako-wasmtime/tests/integration/compile_posture.rs +43 -0
- data/crates/kobako-wasmtime/tests/integration/guest_file.rs +1085 -0
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +4 -3
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +5 -4
- data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +9 -13
- data/crates/kobako-wasmtime/tests/integration/unanswered_call.rs +86 -0
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +2 -1
- data/crates/kobako-wasmtime/witx/typenames.witx +750 -0
- data/crates/kobako-wasmtime/witx/wasi_snapshot_preview1.witx +441 -0
- data/data/kobako.wasm +0 -0
- data/ext/kobako/Cargo.toml +4 -2
- data/ext/kobako/src/runtime/bridge.rs +154 -81
- data/ext/kobako/src/runtime/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +67 -0
- data/ext/kobako/src/runtime/filesystem/grants.rs +107 -0
- data/ext/kobako/src/runtime/filesystem/ruby.rs +708 -0
- data/ext/kobako/src/runtime/filesystem.rs +208 -0
- data/ext/kobako/src/runtime/gvl.rs +49 -8
- data/ext/kobako/src/runtime/pinned_value.rs +31 -0
- data/ext/kobako/src/runtime/snapshot.rs +127 -0
- data/ext/kobako/src/runtime.rs +125 -199
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -14
- data/lib/kobako/catalog/grants.rb +30 -0
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +19 -0
- data/lib/kobako/catalog/services.rb +13 -2
- data/lib/kobako/catalog/snippets.rb +11 -15
- data/lib/kobako/catalog.rb +11 -0
- data/lib/kobako/codec/decoder.rb +10 -9
- data/lib/kobako/codec/ext_types.rb +4 -6
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/state.rb +19 -3
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +86 -0
- data/lib/kobako/codec.rb +7 -0
- data/lib/kobako/context.rb +40 -13
- data/lib/kobako/errors.rb +15 -14
- data/lib/kobako/filesystem.rb +69 -0
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +6 -6
- data/lib/kobako/sandbox.rb +55 -27
- data/lib/kobako/sandbox_options.rb +39 -13
- data/lib/kobako/snippet/{binary.rb → bytecode.rb} +2 -2
- data/lib/kobako/snippet/source.rb +1 -1
- data/lib/kobako/snippet.rb +1 -1
- data/lib/kobako/transport/block_failure.rb +35 -0
- data/lib/kobako/transport/dispatcher.rb +33 -29
- data/lib/kobako/transport/exposure.rb +7 -9
- data/lib/kobako/transport/reflection.rb +79 -23
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +9 -31
- data/lib/kobako/transport.rb +1 -0
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +35 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/flags.rbs +0 -4
- data/sig/kobako/catalog/grants.rbs +13 -0
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +24 -0
- data/sig/kobako/catalog/services.rbs +5 -1
- data/sig/kobako/catalog/snippets.rbs +4 -4
- data/sig/kobako/catalog.rbs +1 -0
- data/sig/kobako/codec/decoder.rbs +1 -1
- data/sig/kobako/codec/handle_walk.rbs +1 -1
- data/sig/kobako/codec/nesting.rbs +2 -2
- data/sig/kobako/codec/state.rbs +7 -1
- data/sig/kobako/codec/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +23 -0
- data/sig/kobako/codec.rbs +3 -2
- data/sig/kobako/context.rbs +14 -6
- data/sig/kobako/errors.rbs +4 -4
- data/sig/kobako/filesystem.rbs +93 -0
- data/sig/kobako/guest_yielder.rbs +9 -0
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +3 -3
- data/sig/kobako/runtime.rbs +21 -49
- data/sig/kobako/sandbox.rbs +9 -5
- data/sig/kobako/sandbox_options.rbs +25 -3
- data/sig/kobako/snapshot.rbs +20 -0
- data/sig/kobako/snippet/bytecode.rbs +12 -0
- data/sig/kobako/transport/block_failure.rbs +13 -0
- data/sig/kobako/transport/dispatcher.rbs +4 -4
- data/sig/kobako/transport/exposure.rbs +2 -2
- data/sig/kobako/transport/reflection.rbs +14 -1
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport/yielder.rbs +3 -7
- data/sig/kobako/transport.rbs +3 -3
- metadata +116 -15
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -107
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -265
- data/crates/kobako-wasmtime/src/io.rs +0 -286
- data/sig/kobako/snippet/binary.rbs +0 -12
|
@@ -0,0 +1,1085 @@
|
|
|
1
|
+
//! The bundled guest's `File` over a Mount: what a script reads and
|
|
2
|
+
//! writes beneath a mounted path, and how it fails where nothing is
|
|
3
|
+
//! mounted, nothing is there, or the Mount only reads.
|
|
4
|
+
|
|
5
|
+
use crate::support;
|
|
6
|
+
|
|
7
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
8
|
+
use std::sync::Arc;
|
|
9
|
+
|
|
10
|
+
use kobako_runtime::filesystem::{
|
|
11
|
+
FileOperations, FileSystemType, FileType, Filesystem, FilesystemError, Finish, InodeOperations,
|
|
12
|
+
Kiocb, Kstat, Lookup, OpenHow, Tmpfs,
|
|
13
|
+
};
|
|
14
|
+
use kobako_runtime::grant::Grant;
|
|
15
|
+
use kobako_runtime::mount::{Access, Mount};
|
|
16
|
+
use kobako_runtime::security::prepare_creds_within;
|
|
17
|
+
use kobako_runtime::vfs::Inode;
|
|
18
|
+
use kobako_runtime::vfs::{self, Dentry, Path};
|
|
19
|
+
use kobako_runtime::{Completion, Entry, Runtime};
|
|
20
|
+
use kobako_transport::envelope::{Flag, Outcome, Preamble};
|
|
21
|
+
|
|
22
|
+
/// What one script left: its printed output, or the class of the
|
|
23
|
+
/// exception it ended with. `None` when the Guest Binary is not built.
|
|
24
|
+
fn run(source: &str, grants: &[Grant], flags: Vec<Flag>) -> Option<Result<String, String>> {
|
|
25
|
+
let driver = support::bundled_driver()?;
|
|
26
|
+
let mut preamble = Preamble::default();
|
|
27
|
+
preamble.flags = flags;
|
|
28
|
+
let snapshot = driver
|
|
29
|
+
.invoke(
|
|
30
|
+
Entry::Eval {
|
|
31
|
+
source: source.as_bytes(),
|
|
32
|
+
},
|
|
33
|
+
&preamble,
|
|
34
|
+
None,
|
|
35
|
+
grants,
|
|
36
|
+
)
|
|
37
|
+
.expect("the invocation must start");
|
|
38
|
+
let Completion::Outcome(outcome) = snapshot.completion else {
|
|
39
|
+
panic!("the invocation must answer rather than trap");
|
|
40
|
+
};
|
|
41
|
+
Some(match outcome {
|
|
42
|
+
Outcome::Ok(_) => Ok(String::from_utf8(snapshot.stdout.bytes).expect("stdout is UTF-8")),
|
|
43
|
+
Outcome::Panic(panic) => Err(panic.error.name),
|
|
44
|
+
Outcome::Unsupported(_) => panic!("the bundled guest serves eval"),
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn mount(filesystem: Arc<dyn FileSystemType>, access: Access) -> Grant {
|
|
49
|
+
Grant::Mount(
|
|
50
|
+
Mount::new(
|
|
51
|
+
"/data",
|
|
52
|
+
Filesystem::new(filesystem).expect("the instance must stand"),
|
|
53
|
+
access,
|
|
54
|
+
)
|
|
55
|
+
.expect("/data is a guest path"),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
fn note_tmpfs() -> Arc<Tmpfs> {
|
|
60
|
+
Arc::new([("note.txt", "hello\nworld\n")].into_iter().collect())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// The root of `filesystem` as the host reaches it through the VFS.
|
|
64
|
+
fn host<F: FileSystemType>(filesystem: &Arc<F>) -> Path {
|
|
65
|
+
let filesystem = Filesystem::new(filesystem.clone()).expect("the instance must stand");
|
|
66
|
+
let mount = Mount::new("/data", filesystem, Access::Read).expect("/data is a guest path");
|
|
67
|
+
Path::root(mount.mnt())
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn contents<F: FileSystemType>(filesystem: &Arc<F>, path: &str) -> Vec<u8> {
|
|
71
|
+
let mut how = OpenHow::default();
|
|
72
|
+
how.read = true;
|
|
73
|
+
let op = vfs::build_open_flags(&how).expect("a read means an open");
|
|
74
|
+
let Ok(mut file) = vfs::do_file_open(
|
|
75
|
+
&prepare_creds_within(&[], None),
|
|
76
|
+
&host(filesystem),
|
|
77
|
+
path,
|
|
78
|
+
&op,
|
|
79
|
+
) else {
|
|
80
|
+
panic!("the file must exist");
|
|
81
|
+
};
|
|
82
|
+
let mut buf = vec![0; 64];
|
|
83
|
+
let read = file.pread(&mut buf, 0).expect("the file must read");
|
|
84
|
+
buf.truncate(read);
|
|
85
|
+
buf
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// @behavior FL-001
|
|
89
|
+
#[test]
|
|
90
|
+
fn a_script_reads_a_file_beneath_a_mounted_path() {
|
|
91
|
+
let Some(answer) = run(
|
|
92
|
+
r#"print File.read("/data/note.txt")"#,
|
|
93
|
+
&[mount(note_tmpfs(), Access::Read)],
|
|
94
|
+
Vec::new(),
|
|
95
|
+
) else {
|
|
96
|
+
return;
|
|
97
|
+
};
|
|
98
|
+
assert_eq!(
|
|
99
|
+
answer,
|
|
100
|
+
Ok("hello\nworld\n".to_string()),
|
|
101
|
+
"File.read beneath a Mount through the bundled guest must read the file"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// @behavior FL-002
|
|
106
|
+
#[test]
|
|
107
|
+
fn a_script_writes_a_file_beneath_a_read_write_mount() {
|
|
108
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
109
|
+
let Some(answer) = run(
|
|
110
|
+
r#"print File.open("/data/out.txt", "w") { |f| f.write("kept"); :done }"#,
|
|
111
|
+
&[mount(tmpfs.clone(), Access::ReadWrite)],
|
|
112
|
+
Vec::new(),
|
|
113
|
+
) else {
|
|
114
|
+
return;
|
|
115
|
+
};
|
|
116
|
+
assert_eq!(
|
|
117
|
+
answer,
|
|
118
|
+
Ok("done".to_string()),
|
|
119
|
+
"File.open must answer its block's value"
|
|
120
|
+
);
|
|
121
|
+
assert_eq!(
|
|
122
|
+
contents(&tmpfs, "out.txt"),
|
|
123
|
+
b"kept",
|
|
124
|
+
"a file written in File.open's block through the bundled guest must land in the Tmpfs"
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// @behavior FL-003
|
|
129
|
+
#[test]
|
|
130
|
+
fn a_script_reads_a_file_line_by_line() {
|
|
131
|
+
let Some(answer) = run(
|
|
132
|
+
r#"File.open("/data/note.txt") { |f| f.each_line { |line| print line.upcase } }; nil"#,
|
|
133
|
+
&[mount(note_tmpfs(), Access::Read)],
|
|
134
|
+
Vec::new(),
|
|
135
|
+
) else {
|
|
136
|
+
return;
|
|
137
|
+
};
|
|
138
|
+
assert_eq!(
|
|
139
|
+
answer,
|
|
140
|
+
Ok("HELLO\nWORLD\n".to_string()),
|
|
141
|
+
"IO#each_line on a File through the bundled guest must yield each line"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// @behavior FL-004
|
|
146
|
+
#[test]
|
|
147
|
+
fn opening_a_file_with_nothing_mounted_is_unimplemented() {
|
|
148
|
+
let Some(answer) = run(r#"File.open("/data/note.txt")"#, &[], Vec::new()) else {
|
|
149
|
+
return;
|
|
150
|
+
};
|
|
151
|
+
assert_eq!(
|
|
152
|
+
answer,
|
|
153
|
+
Err("NotImplementedError".to_string()),
|
|
154
|
+
"File.open with no Mount through the bundled guest must raise \
|
|
155
|
+
NotImplementedError"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// @behavior FL-005
|
|
160
|
+
#[test]
|
|
161
|
+
fn opening_a_missing_file_raises_enoent() {
|
|
162
|
+
let Some(answer) = run(
|
|
163
|
+
r#"File.read("/data/missing.txt")"#,
|
|
164
|
+
&[mount(note_tmpfs(), Access::Read)],
|
|
165
|
+
Vec::new(),
|
|
166
|
+
) else {
|
|
167
|
+
return;
|
|
168
|
+
};
|
|
169
|
+
assert_eq!(
|
|
170
|
+
answer,
|
|
171
|
+
Err("Errno::ENOENT".to_string()),
|
|
172
|
+
"File.read of a path a Mount does not hold through the bundled guest must raise \
|
|
173
|
+
Errno::ENOENT"
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// @behavior FL-006
|
|
178
|
+
#[test]
|
|
179
|
+
fn writing_beneath_a_read_only_mount_raises_erofs() {
|
|
180
|
+
let Some(answer) = run(
|
|
181
|
+
r#"File.write("/data/note.txt", "changed")"#,
|
|
182
|
+
&[mount(note_tmpfs(), Access::Read)],
|
|
183
|
+
Vec::new(),
|
|
184
|
+
) else {
|
|
185
|
+
return;
|
|
186
|
+
};
|
|
187
|
+
assert_eq!(
|
|
188
|
+
answer,
|
|
189
|
+
Err("Errno::EROFS".to_string()),
|
|
190
|
+
"File.write beneath a read-only Mount through the bundled guest must raise Errno::EROFS"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// The write goes through an open file, so `IO#write` itself must raise,
|
|
195
|
+
// not only `File.write`.
|
|
196
|
+
// @behavior FL-012
|
|
197
|
+
#[test]
|
|
198
|
+
fn a_write_past_a_mounts_space_raises_enospc() {
|
|
199
|
+
let full = Arc::new(Tmpfs::new().with_size(Some(4)));
|
|
200
|
+
let Some(answer) = run(
|
|
201
|
+
r#"File.open("/data/note.txt", "w") { |file| file.write("five!") }"#,
|
|
202
|
+
&[mount(full, Access::ReadWrite)],
|
|
203
|
+
Vec::new(),
|
|
204
|
+
) else {
|
|
205
|
+
return;
|
|
206
|
+
};
|
|
207
|
+
assert_eq!(
|
|
208
|
+
answer,
|
|
209
|
+
Err("Errno::ENOSPC".to_string()),
|
|
210
|
+
"IO#write past a Tmpfs's limit through the bundled guest must raise Errno::ENOSPC"
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// A name at NAME_MAX still lands, so the refusal is the limit and not
|
|
215
|
+
// the name's length alone.
|
|
216
|
+
// @behavior FL-013
|
|
217
|
+
#[test]
|
|
218
|
+
fn a_name_or_path_past_linuxs_limits_raises_enametoolong() {
|
|
219
|
+
let script = r#"
|
|
220
|
+
answers = [255, 256].map do |length|
|
|
221
|
+
begin
|
|
222
|
+
File.write("/data/" + "x" * length, "")
|
|
223
|
+
:written
|
|
224
|
+
rescue Errno::ENAMETOOLONG
|
|
225
|
+
:too_long
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
begin
|
|
229
|
+
File.read("/data/" + "d/" * 2048)
|
|
230
|
+
rescue Errno::ENAMETOOLONG
|
|
231
|
+
answers << :too_long
|
|
232
|
+
end
|
|
233
|
+
print answers.inspect
|
|
234
|
+
"#;
|
|
235
|
+
let Some(answer) = run(
|
|
236
|
+
script,
|
|
237
|
+
&[mount(Arc::new(Tmpfs::new()), Access::ReadWrite)],
|
|
238
|
+
Vec::new(),
|
|
239
|
+
) else {
|
|
240
|
+
return;
|
|
241
|
+
};
|
|
242
|
+
assert_eq!(
|
|
243
|
+
answer,
|
|
244
|
+
Ok("[:written, :too_long, :too_long]".to_string()),
|
|
245
|
+
"a name past 255 bytes or a path past 4095 through the bundled guest's File must raise \
|
|
246
|
+
Errno::ENAMETOOLONG"
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// EBUSY once had no class in the guest, so the script saw an IOError.
|
|
251
|
+
// @behavior FL-014
|
|
252
|
+
#[test]
|
|
253
|
+
fn a_code_the_engine_answers_raises_its_own_errno() {
|
|
254
|
+
let Some(answer) = run(
|
|
255
|
+
r#"File.rename("/data", "/data/moved")"#,
|
|
256
|
+
&[mount(note_tmpfs(), Access::ReadWrite)],
|
|
257
|
+
Vec::new(),
|
|
258
|
+
) else {
|
|
259
|
+
return;
|
|
260
|
+
};
|
|
261
|
+
assert_eq!(
|
|
262
|
+
answer,
|
|
263
|
+
Err("Errno::EBUSY".to_string()),
|
|
264
|
+
"File.rename of a Mount's root through the bundled guest must raise Errno::EBUSY"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// @behavior FL-007
|
|
269
|
+
#[test]
|
|
270
|
+
fn a_file_is_an_io() {
|
|
271
|
+
let Some(answer) = run(r#"print(File.superclass == IO)"#, &[], Vec::new()) else {
|
|
272
|
+
return;
|
|
273
|
+
};
|
|
274
|
+
assert_eq!(
|
|
275
|
+
answer,
|
|
276
|
+
Ok("true".to_string()),
|
|
277
|
+
"File in the bundled guest must descend from IO"
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// @behavior FL-008
|
|
282
|
+
#[test]
|
|
283
|
+
fn the_disable_file_flag_removes_file() {
|
|
284
|
+
let flag = Flag {
|
|
285
|
+
name: "disable_file".into(),
|
|
286
|
+
value: String::new(),
|
|
287
|
+
critical: true,
|
|
288
|
+
};
|
|
289
|
+
let Some(answer) = run(r#"print Object.const_defined?(:File)"#, &[], vec![flag]) else {
|
|
290
|
+
return;
|
|
291
|
+
};
|
|
292
|
+
assert_eq!(
|
|
293
|
+
answer,
|
|
294
|
+
Ok("false".to_string()),
|
|
295
|
+
"the disable_file flag through the bundled guest must leave no File constant"
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// @behavior FL-009
|
|
300
|
+
#[test]
|
|
301
|
+
fn an_io_may_stand_for_a_file_the_guest_opened() {
|
|
302
|
+
let Some(answer) = run(
|
|
303
|
+
r#"File.open("/data/note.txt") { |f| print IO.new(f.fileno, "r").read }; nil"#,
|
|
304
|
+
&[mount(note_tmpfs(), Access::Read)],
|
|
305
|
+
Vec::new(),
|
|
306
|
+
) else {
|
|
307
|
+
return;
|
|
308
|
+
};
|
|
309
|
+
assert_eq!(
|
|
310
|
+
answer,
|
|
311
|
+
Ok("hello\nworld\n".to_string()),
|
|
312
|
+
"IO.new over a file the guest opened through the bundled guest must read that file"
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// The file keeping its content shows `wx` refused before truncating,
|
|
317
|
+
// which a plain `w` would have done.
|
|
318
|
+
// @behavior FL-024
|
|
319
|
+
#[test]
|
|
320
|
+
fn an_exclusive_create_refuses_a_file_already_there() {
|
|
321
|
+
let tmpfs = note_tmpfs();
|
|
322
|
+
let Some(answer) = run(
|
|
323
|
+
r#"File.open("/data/note.txt", "wx") { |f| f.write("lost") }"#,
|
|
324
|
+
&[mount(tmpfs.clone(), Access::ReadWrite)],
|
|
325
|
+
Vec::new(),
|
|
326
|
+
) else {
|
|
327
|
+
return;
|
|
328
|
+
};
|
|
329
|
+
assert_eq!(
|
|
330
|
+
(answer, contents(&tmpfs, "note.txt")),
|
|
331
|
+
(Err("Errno::EEXIST".to_string()), b"hello\nworld\n".to_vec()),
|
|
332
|
+
"File.open with wx of a file already there through the bundled guest must raise \
|
|
333
|
+
Errno::EEXIST and leave the file as it was"
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// @behavior FL-025
|
|
338
|
+
#[test]
|
|
339
|
+
fn an_exclusive_create_makes_a_file_nothing_is_at() {
|
|
340
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
341
|
+
let Some(answer) = run(
|
|
342
|
+
r#"File.open("/data/lock", "w+x") { |f| f.write("mine") }; nil"#,
|
|
343
|
+
&[mount(tmpfs.clone(), Access::ReadWrite)],
|
|
344
|
+
Vec::new(),
|
|
345
|
+
) else {
|
|
346
|
+
return;
|
|
347
|
+
};
|
|
348
|
+
assert_eq!(
|
|
349
|
+
(answer, contents(&tmpfs, "lock")),
|
|
350
|
+
(Ok(String::new()), b"mine".to_vec()),
|
|
351
|
+
"File.open with w+x of a path nothing is at through the bundled guest must create the \
|
|
352
|
+
file with what was written"
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// `bw` puts a modifier before the access, and `wbt` names both newline
|
|
357
|
+
// modes, so only a reader keeping CRuby's order and exclusion refuses them.
|
|
358
|
+
// @behavior FL-026
|
|
359
|
+
#[test]
|
|
360
|
+
fn an_access_mode_cruby_refuses_raises_argument_error() {
|
|
361
|
+
let script = r#"
|
|
362
|
+
answers = %w[xw rx r+x ax bw wbt].map do |mode|
|
|
363
|
+
begin
|
|
364
|
+
File.open("/data/note.txt", mode) { }
|
|
365
|
+
"opened"
|
|
366
|
+
rescue ArgumentError => e
|
|
367
|
+
e.message
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
print answers.join(",")
|
|
371
|
+
"#;
|
|
372
|
+
let Some(answer) = run(
|
|
373
|
+
script,
|
|
374
|
+
&[mount(note_tmpfs(), Access::ReadWrite)],
|
|
375
|
+
Vec::new(),
|
|
376
|
+
) else {
|
|
377
|
+
return;
|
|
378
|
+
};
|
|
379
|
+
assert_eq!(
|
|
380
|
+
answer,
|
|
381
|
+
Ok(
|
|
382
|
+
"invalid access mode xw,invalid access mode rx,invalid access mode r+x,\
|
|
383
|
+
invalid access mode ax,invalid access mode bw,invalid access mode wbt"
|
|
384
|
+
.to_string()
|
|
385
|
+
),
|
|
386
|
+
"File.open with an access mode CRuby refuses through the bundled guest must raise \
|
|
387
|
+
ArgumentError naming the mode"
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// @behavior FL-027
|
|
392
|
+
#[test]
|
|
393
|
+
fn an_integer_mode_opens_by_its_file_constants_flags() {
|
|
394
|
+
let tmpfs = note_tmpfs();
|
|
395
|
+
let Some(answer) = run(
|
|
396
|
+
r#"File.open("/data/note.txt", File::WRONLY | File::CREAT | File::EXCL) { }"#,
|
|
397
|
+
&[mount(tmpfs.clone(), Access::ReadWrite)],
|
|
398
|
+
Vec::new(),
|
|
399
|
+
) else {
|
|
400
|
+
return;
|
|
401
|
+
};
|
|
402
|
+
assert_eq!(
|
|
403
|
+
(answer, contents(&tmpfs, "note.txt")),
|
|
404
|
+
(Err("Errno::EEXIST".to_string()), b"hello\nworld\n".to_vec()),
|
|
405
|
+
"File.open with WRONLY | CREAT | EXCL of a file already there through the bundled guest \
|
|
406
|
+
must raise Errno::EEXIST and leave the file as it was"
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// EXCL alone asks no create, so the open finds nothing rather than
|
|
411
|
+
// making the file an exclusive create would.
|
|
412
|
+
// @behavior FL-028
|
|
413
|
+
#[test]
|
|
414
|
+
fn an_exclusive_open_without_create_makes_no_file() {
|
|
415
|
+
let tmpfs = Arc::new(Tmpfs::new());
|
|
416
|
+
let Some(answer) = run(
|
|
417
|
+
r#"File.open("/data/lock", File::RDONLY | File::EXCL) { }"#,
|
|
418
|
+
&[mount(tmpfs.clone(), Access::ReadWrite)],
|
|
419
|
+
Vec::new(),
|
|
420
|
+
) else {
|
|
421
|
+
return;
|
|
422
|
+
};
|
|
423
|
+
assert_eq!(
|
|
424
|
+
(
|
|
425
|
+
answer,
|
|
426
|
+
vfs::vfs_fstatat(
|
|
427
|
+
&prepare_creds_within(&[], None),
|
|
428
|
+
&host(&tmpfs),
|
|
429
|
+
"lock",
|
|
430
|
+
false
|
|
431
|
+
)
|
|
432
|
+
.is_err()
|
|
433
|
+
),
|
|
434
|
+
(Err("Errno::ENOENT".to_string()), true),
|
|
435
|
+
"File.open with RDONLY | EXCL of a path nothing is at through the bundled guest must \
|
|
436
|
+
raise Errno::ENOENT and make no file"
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// 0o4000 is CRuby's `NONBLOCK` on Linux, a flag kobako does not honor, so
|
|
441
|
+
// ignoring it would open differently than CRuby. `WRONLY | RDWR` names
|
|
442
|
+
// no access mode on Linux.
|
|
443
|
+
// @behavior FL-029
|
|
444
|
+
#[test]
|
|
445
|
+
fn an_integer_mode_with_a_flag_file_constants_lacks_raises_einval() {
|
|
446
|
+
let script = r#"
|
|
447
|
+
answers = [File::RDONLY | 0o4000, File::WRONLY | File::RDWR].map do |mode|
|
|
448
|
+
begin
|
|
449
|
+
File.open("/data/note.txt", mode) { }
|
|
450
|
+
"opened"
|
|
451
|
+
rescue => e
|
|
452
|
+
e.class.to_s
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
print answers.join(",")
|
|
456
|
+
"#;
|
|
457
|
+
let Some(answer) = run(
|
|
458
|
+
script,
|
|
459
|
+
&[mount(note_tmpfs(), Access::ReadWrite)],
|
|
460
|
+
Vec::new(),
|
|
461
|
+
) else {
|
|
462
|
+
return;
|
|
463
|
+
};
|
|
464
|
+
assert_eq!(
|
|
465
|
+
answer,
|
|
466
|
+
Ok("Errno::EINVAL,Errno::EINVAL".to_string()),
|
|
467
|
+
"File.open with a mode bit no File::Constants flag names, or with WRONLY beside RDWR, \
|
|
468
|
+
through the bundled guest must raise Errno::EINVAL"
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// CRuby's `RDONLY` is zero, so beside `WRONLY` it asks nothing more.
|
|
473
|
+
// @behavior FL-031
|
|
474
|
+
#[test]
|
|
475
|
+
fn rdonly_beside_wronly_opens_to_write_only() {
|
|
476
|
+
let tmpfs = note_tmpfs();
|
|
477
|
+
let script = r#"
|
|
478
|
+
File.open("/data/note.txt", File::RDONLY | File::WRONLY) do |file|
|
|
479
|
+
file.write("J")
|
|
480
|
+
begin
|
|
481
|
+
file.read
|
|
482
|
+
rescue IOError => e
|
|
483
|
+
print e.class
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
"#;
|
|
487
|
+
let Some(answer) = run(
|
|
488
|
+
script,
|
|
489
|
+
&[mount(tmpfs.clone(), Access::ReadWrite)],
|
|
490
|
+
Vec::new(),
|
|
491
|
+
) else {
|
|
492
|
+
return;
|
|
493
|
+
};
|
|
494
|
+
assert_eq!(
|
|
495
|
+
(answer, contents(&tmpfs, "note.txt")),
|
|
496
|
+
(Ok("IOError".to_string()), b"Jello\nworld\n".to_vec()),
|
|
497
|
+
"File.open with RDONLY | WRONLY through the bundled guest must write and raise IOError \
|
|
498
|
+
on a read"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Neither open asks to write, so only a mode that reaches the host as it
|
|
503
|
+
// stands, rather than one refused before it leaves the guest, changes
|
|
504
|
+
// the Tmpfs.
|
|
505
|
+
// @behavior FL-032
|
|
506
|
+
#[test]
|
|
507
|
+
fn an_integer_mode_may_create_or_truncate_without_asking_to_write() {
|
|
508
|
+
let tmpfs = note_tmpfs();
|
|
509
|
+
let script = r#"
|
|
510
|
+
File.open("/data/lock", File::CREAT | File::EXCL) { }
|
|
511
|
+
File.open("/data/note.txt", File::RDONLY | File::TRUNC) { }
|
|
512
|
+
"#;
|
|
513
|
+
let Some(answer) = run(
|
|
514
|
+
script,
|
|
515
|
+
&[mount(tmpfs.clone(), Access::ReadWrite)],
|
|
516
|
+
Vec::new(),
|
|
517
|
+
) else {
|
|
518
|
+
return;
|
|
519
|
+
};
|
|
520
|
+
assert_eq!(
|
|
521
|
+
(
|
|
522
|
+
answer,
|
|
523
|
+
contents(&tmpfs, "lock"),
|
|
524
|
+
contents(&tmpfs, "note.txt")
|
|
525
|
+
),
|
|
526
|
+
(Ok(String::new()), Vec::new(), Vec::new()),
|
|
527
|
+
"File.open with CREAT | EXCL of a new path and with RDONLY | TRUNC of a file through the \
|
|
528
|
+
bundled guest must make the new file empty and empty the file"
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// @behavior FL-033
|
|
533
|
+
#[test]
|
|
534
|
+
fn a_path_holding_a_nul_byte_raises_argument_error() {
|
|
535
|
+
let script = r#"
|
|
536
|
+
path = "/data/note.txt\0x"
|
|
537
|
+
calls = [-> { File.open(path) { } }, -> { File.read(path) }, -> { File.write(path, "x") },
|
|
538
|
+
-> { File.exist?(path) }, -> { File.stat(path) }, -> { Dir.exist?(path) },
|
|
539
|
+
-> { Dir.children(path) }]
|
|
540
|
+
print calls.map { |call| begin; call.call; "none"; rescue Exception => e; e.class.to_s; end }.join(",")
|
|
541
|
+
"#;
|
|
542
|
+
let Some(answer) = run(
|
|
543
|
+
script,
|
|
544
|
+
&[mount(note_tmpfs(), Access::ReadWrite)],
|
|
545
|
+
Vec::new(),
|
|
546
|
+
) else {
|
|
547
|
+
return;
|
|
548
|
+
};
|
|
549
|
+
assert_eq!(
|
|
550
|
+
answer,
|
|
551
|
+
Ok("ArgumentError,ArgumentError,ArgumentError,ArgumentError,ArgumentError,ArgumentError,\
|
|
552
|
+
ArgumentError"
|
|
553
|
+
.to_string()),
|
|
554
|
+
"File.open, File.read, File.write, File.exist?, File.stat, Dir.exist?, and Dir.children of a \
|
|
555
|
+
path holding a NUL byte through the bundled guest must each raise ArgumentError"
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// The values are CRuby's on Linux, so a script built from numbers it saw
|
|
560
|
+
// there opens the same way.
|
|
561
|
+
// @behavior FL-030
|
|
562
|
+
#[test]
|
|
563
|
+
fn file_constants_names_the_flags_an_open_honors() {
|
|
564
|
+
let script = r##"
|
|
565
|
+
names = File::Constants.constants.sort
|
|
566
|
+
print names.map { |name| "#{name}=#{File::Constants.const_get(name)}" }.join(","),
|
|
567
|
+
" ", IO.include?(File::Constants)
|
|
568
|
+
"##;
|
|
569
|
+
let Some(answer) = run(script, &[], Vec::new()) else {
|
|
570
|
+
return;
|
|
571
|
+
};
|
|
572
|
+
assert_eq!(
|
|
573
|
+
answer,
|
|
574
|
+
Ok("APPEND=1024,CREAT=64,EXCL=128,RDONLY=0,RDWR=2,TRUNC=512,WRONLY=1 true".to_string()),
|
|
575
|
+
"File::Constants in the bundled guest must name the seven flags an open honors at \
|
|
576
|
+
CRuby's Linux values, and IO must include it"
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/// A Tmpfs whose every open file is handed through `wrap` first.
|
|
581
|
+
struct WrapsFiles<W> {
|
|
582
|
+
inner: Arc<dyn InodeOperations>,
|
|
583
|
+
wrap: Arc<W>,
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
impl<W> WrapsFiles<W> {
|
|
587
|
+
fn over_note(contents: &str, wrap: W) -> Arc<Self> {
|
|
588
|
+
let tmpfs: Tmpfs = [("note.txt", contents)].into_iter().collect();
|
|
589
|
+
Arc::new(Self {
|
|
590
|
+
inner: Arc::new(tmpfs),
|
|
591
|
+
wrap: Arc::new(wrap),
|
|
592
|
+
})
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
impl<W, F> WrapsFiles<W>
|
|
597
|
+
where
|
|
598
|
+
W: Fn(Box<dyn FileOperations>) -> F + Send + Sync + 'static,
|
|
599
|
+
F: FileOperations + 'static,
|
|
600
|
+
{
|
|
601
|
+
/// `found` with the entry it names handing its files through `wrap` too.
|
|
602
|
+
fn wrap_found(&self, found: Lookup) -> Lookup {
|
|
603
|
+
match found {
|
|
604
|
+
Lookup::Found(inode) => Lookup::Found(inode.with_ops(Arc::new(Self {
|
|
605
|
+
inner: inode.ops().clone(),
|
|
606
|
+
wrap: self.wrap.clone(),
|
|
607
|
+
}))),
|
|
608
|
+
other => other,
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
impl<W, F> FileSystemType for WrapsFiles<W>
|
|
614
|
+
where
|
|
615
|
+
W: Fn(Box<dyn FileOperations>) -> F + Send + Sync + 'static,
|
|
616
|
+
F: FileOperations + 'static,
|
|
617
|
+
{
|
|
618
|
+
fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
|
|
619
|
+
sb.d_make_root(Inode::new(&Kstat::new(FileType::Dir, 0), self));
|
|
620
|
+
Ok(())
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
impl<W, F> InodeOperations for WrapsFiles<W>
|
|
625
|
+
where
|
|
626
|
+
W: Fn(Box<dyn FileOperations>) -> F + Send + Sync + 'static,
|
|
627
|
+
F: FileOperations + 'static,
|
|
628
|
+
{
|
|
629
|
+
fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
630
|
+
self.inner.getattr(dentry)
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
fn setattr(
|
|
634
|
+
&self,
|
|
635
|
+
dentry: &Dentry,
|
|
636
|
+
attr: &kobako_runtime::filesystem::Iattr,
|
|
637
|
+
) -> Result<(), FilesystemError> {
|
|
638
|
+
self.inner.setattr(dentry, attr)
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
fn lookup(&self, dentry: &Dentry) -> Result<Lookup, FilesystemError> {
|
|
642
|
+
Ok(self.wrap_found(self.inner.lookup(dentry)?))
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
fn open(
|
|
646
|
+
&self,
|
|
647
|
+
dentry: &Dentry,
|
|
648
|
+
how: &OpenHow,
|
|
649
|
+
) -> Result<Box<dyn FileOperations>, FilesystemError> {
|
|
650
|
+
let opened = self.inner.open(dentry, how)?;
|
|
651
|
+
if dentry
|
|
652
|
+
.inode()
|
|
653
|
+
.is_none_or(|inode| inode.kind() == kobako_runtime::filesystem::FileType::Dir)
|
|
654
|
+
{
|
|
655
|
+
return Ok(opened);
|
|
656
|
+
}
|
|
657
|
+
Ok(Box::new((self.wrap)(opened)))
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/// Opens what it made, as a Filesystem ending in `finish_open` does, so the
|
|
661
|
+
/// new file is wrapped too; what it only looked up is handed back wrapped.
|
|
662
|
+
fn atomic_open(&self, dentry: &Dentry, how: &OpenHow) -> Result<Finish, FilesystemError> {
|
|
663
|
+
let (made, created) = match self.inner.atomic_open(dentry, how)? {
|
|
664
|
+
Finish::Open { file, created } => (file, created),
|
|
665
|
+
Finish::NoOpen { created } => (self.inner.open(dentry, how)?, created),
|
|
666
|
+
Finish::Lookup(found) => return Ok(Finish::Lookup(self.wrap_found(found))),
|
|
667
|
+
Finish::Spelled { file, name, inode } => {
|
|
668
|
+
let Lookup::Found(inode) = self.wrap_found(Lookup::Found(inode)) else {
|
|
669
|
+
unreachable!("a found entry stays found once wrapped");
|
|
670
|
+
};
|
|
671
|
+
let file = Box::new((self.wrap)(file));
|
|
672
|
+
return Ok(Finish::Spelled { file, name, inode });
|
|
673
|
+
}
|
|
674
|
+
_ => unreachable!("Finish has no other end"),
|
|
675
|
+
};
|
|
676
|
+
Ok(Finish::Open {
|
|
677
|
+
file: Box::new((self.wrap)(made)),
|
|
678
|
+
created,
|
|
679
|
+
})
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
struct CountedFile {
|
|
684
|
+
file: Box<dyn FileOperations>,
|
|
685
|
+
reads: Arc<AtomicUsize>,
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
impl FileOperations for CountedFile {
|
|
689
|
+
fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
|
|
690
|
+
self.reads.fetch_add(1, Ordering::Relaxed);
|
|
691
|
+
self.file.read_iter(iocb, buf)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
|
|
695
|
+
self.file.write_iter(iocb, buf)
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/// Each read reaching the Filesystem crosses the wasm boundary, and one a
|
|
700
|
+
/// Host App writes in Ruby re-enters Ruby too, so a read of a length the
|
|
701
|
+
/// file holds asks once.
|
|
702
|
+
#[test]
|
|
703
|
+
fn a_read_of_a_length_asks_the_filesystem_once() {
|
|
704
|
+
let reads = Arc::new(AtomicUsize::new(0));
|
|
705
|
+
let counted = reads.clone();
|
|
706
|
+
let filesystem = WrapsFiles::over_note(&"x".repeat(6400), move |file| CountedFile {
|
|
707
|
+
file,
|
|
708
|
+
reads: counted.clone(),
|
|
709
|
+
});
|
|
710
|
+
let Some(answer) = run(
|
|
711
|
+
r#"File.open("/data/note.txt") { |f| print f.read(64).size }; nil"#,
|
|
712
|
+
&[mount(filesystem, Access::Read)],
|
|
713
|
+
Vec::new(),
|
|
714
|
+
) else {
|
|
715
|
+
return;
|
|
716
|
+
};
|
|
717
|
+
assert_eq!(answer, Ok("64".to_string()));
|
|
718
|
+
assert_eq!(
|
|
719
|
+
reads.load(Ordering::Relaxed),
|
|
720
|
+
1,
|
|
721
|
+
"File#read(64) of a longer file through the bundled guest must ask its Filesystem for one read"
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/// An open file whose Filesystem refuses to release it, as a database
|
|
726
|
+
/// refusing the commit a close stands for.
|
|
727
|
+
struct RefusesClose(Box<dyn FileOperations>);
|
|
728
|
+
|
|
729
|
+
impl FileOperations for RefusesClose {
|
|
730
|
+
fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
|
|
731
|
+
self.0.read_iter(iocb, buf)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
|
|
735
|
+
self.0.write_iter(iocb, buf)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
fn flush(&mut self) -> Result<(), FilesystemError> {
|
|
739
|
+
Err(FilesystemError::PermissionDenied)
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/// An open file whose every read fails in a way no error code names.
|
|
744
|
+
struct FailsReads(Box<dyn FileOperations>);
|
|
745
|
+
|
|
746
|
+
impl FileOperations for FailsReads {
|
|
747
|
+
fn read_iter(&mut self, _iocb: &mut Kiocb, _buf: &mut [u8]) -> Result<usize, FilesystemError> {
|
|
748
|
+
Err(FilesystemError::Other("the disk went away".to_string()))
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
|
|
752
|
+
self.0.write_iter(iocb, buf)
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// @behavior FL-011
|
|
757
|
+
#[test]
|
|
758
|
+
fn a_failure_no_error_code_names_raises_eio() {
|
|
759
|
+
let filesystem = WrapsFiles::over_note("hello", FailsReads);
|
|
760
|
+
let Some(answer) = run(
|
|
761
|
+
r#"begin; File.read("/data/note.txt"); rescue Errno::EIO; print "failed"; end; nil"#,
|
|
762
|
+
&[mount(filesystem, Access::Read)],
|
|
763
|
+
Vec::new(),
|
|
764
|
+
) else {
|
|
765
|
+
return;
|
|
766
|
+
};
|
|
767
|
+
assert_eq!(
|
|
768
|
+
answer,
|
|
769
|
+
Ok("failed".to_string()),
|
|
770
|
+
"File.read of a file whose Filesystem fails with no named code through the bundled guest must raise Errno::EIO"
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// @behavior FL-010
|
|
775
|
+
#[test]
|
|
776
|
+
fn a_close_the_filesystem_refuses_fails_the_scripts_close() {
|
|
777
|
+
let filesystem = WrapsFiles::over_note("hello", RefusesClose);
|
|
778
|
+
let Some(answer) = run(
|
|
779
|
+
r#"f = File.open("/data/note.txt"); begin; f.close; rescue Errno::EACCES; print "refused"; end; print f.closed?; nil"#,
|
|
780
|
+
&[mount(filesystem, Access::Read)],
|
|
781
|
+
Vec::new(),
|
|
782
|
+
) else {
|
|
783
|
+
return;
|
|
784
|
+
};
|
|
785
|
+
assert_eq!(
|
|
786
|
+
answer,
|
|
787
|
+
Ok("refusedtrue".to_string()),
|
|
788
|
+
"File#close of a file whose Filesystem refuses its close through the bundled guest must raise Errno::EACCES and leave the file closed"
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/// An open file taking at most three bytes per write, as a Filesystem a
|
|
793
|
+
/// Host App writes in Ruby may.
|
|
794
|
+
struct WritesInParts(Box<dyn FileOperations>);
|
|
795
|
+
|
|
796
|
+
impl FileOperations for WritesInParts {
|
|
797
|
+
fn read_iter(&mut self, iocb: &mut Kiocb, buf: &mut [u8]) -> Result<usize, FilesystemError> {
|
|
798
|
+
self.0.read_iter(iocb, buf)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
fn write_iter(&mut self, iocb: &mut Kiocb, buf: &[u8]) -> Result<usize, FilesystemError> {
|
|
802
|
+
self.0.write_iter(iocb, &buf[..buf.len().min(3)])
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// @behavior FL-016
|
|
807
|
+
#[test]
|
|
808
|
+
fn an_errno_a_script_raises_reads_as_crubys() {
|
|
809
|
+
let script = r#"
|
|
810
|
+
out = []
|
|
811
|
+
begin; raise Errno::ENOENT, "foo"; rescue => e; out << e.message; end
|
|
812
|
+
out << Errno::ENOENT.new.message
|
|
813
|
+
e = SystemCallError.new("failed", 2)
|
|
814
|
+
out << e.class.to_s << e.errno
|
|
815
|
+
print out.inspect
|
|
816
|
+
"#;
|
|
817
|
+
let Some(answer) = run(script, &[mount(note_tmpfs(), Access::Read)], Vec::new()) else {
|
|
818
|
+
return;
|
|
819
|
+
};
|
|
820
|
+
assert_eq!(
|
|
821
|
+
answer,
|
|
822
|
+
Ok(r#"["No such file or directory - foo", "No such file or directory", "Errno::ENOENT", 2]"#
|
|
823
|
+
.to_string()),
|
|
824
|
+
"Errno::ENOENT raised with a message, built bare, and SystemCallError.new with a known \
|
|
825
|
+
number through the bundled guest must read as CRuby's"
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// No Errno class names 9999, so only the number the error was built with
|
|
830
|
+
// can answer `errno`.
|
|
831
|
+
// @behavior FL-019
|
|
832
|
+
#[test]
|
|
833
|
+
fn a_system_call_error_keeps_a_number_no_class_names() {
|
|
834
|
+
let Some(answer) = run(
|
|
835
|
+
r#"e = SystemCallError.new(9999); print [e.class.to_s, e.message, e.errno].inspect"#,
|
|
836
|
+
&[mount(note_tmpfs(), Access::Read)],
|
|
837
|
+
Vec::new(),
|
|
838
|
+
) else {
|
|
839
|
+
return;
|
|
840
|
+
};
|
|
841
|
+
assert_eq!(
|
|
842
|
+
answer,
|
|
843
|
+
Ok(r#"["SystemCallError", "Unknown error: 9999", 9999]"#.to_string()),
|
|
844
|
+
"SystemCallError.new with a number no Errno class names through the bundled guest must \
|
|
845
|
+
keep that number as its errno, as CRuby does"
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// @behavior FL-022
|
|
850
|
+
#[test]
|
|
851
|
+
fn a_read_length_past_64_bits_raises_range_error() {
|
|
852
|
+
let script = r#"
|
|
853
|
+
begin
|
|
854
|
+
File.open("/data/note.txt") { |f| f.read(2**70) }
|
|
855
|
+
rescue RangeError => e
|
|
856
|
+
print e.class.to_s
|
|
857
|
+
end
|
|
858
|
+
"#;
|
|
859
|
+
let Some(answer) = run(script, &[mount(note_tmpfs(), Access::Read)], Vec::new()) else {
|
|
860
|
+
return;
|
|
861
|
+
};
|
|
862
|
+
assert_eq!(
|
|
863
|
+
answer,
|
|
864
|
+
Ok("RangeError".to_string()),
|
|
865
|
+
"File#read with a length of 2**70 through the bundled guest must raise RangeError, as \
|
|
866
|
+
CRuby does"
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// The C errno is an `int`, so 2**40 is already past it although it is no
|
|
871
|
+
// big integer.
|
|
872
|
+
// @behavior FL-023
|
|
873
|
+
#[test]
|
|
874
|
+
fn a_system_call_error_number_past_the_errno_range_raises_range_error() {
|
|
875
|
+
let script = r#"
|
|
876
|
+
out = [2**40, 2**70].map do |number|
|
|
877
|
+
SystemCallError.new("failed", number)
|
|
878
|
+
rescue RangeError => e
|
|
879
|
+
e.class.to_s
|
|
880
|
+
end
|
|
881
|
+
print out.inspect
|
|
882
|
+
"#;
|
|
883
|
+
let Some(answer) = run(script, &[mount(note_tmpfs(), Access::Read)], Vec::new()) else {
|
|
884
|
+
return;
|
|
885
|
+
};
|
|
886
|
+
assert_eq!(
|
|
887
|
+
answer,
|
|
888
|
+
Ok(r#"["RangeError", "RangeError"]"#.to_string()),
|
|
889
|
+
"SystemCallError.new with a message and 2**40 or 2**70 through the bundled guest must \
|
|
890
|
+
raise RangeError, as CRuby does"
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// @behavior FL-017
|
|
895
|
+
#[test]
|
|
896
|
+
fn a_read_or_write_a_stream_was_not_opened_for_raises_ioerror() {
|
|
897
|
+
let script = r#"
|
|
898
|
+
out = []
|
|
899
|
+
[-> { File.open("/data/note.txt", "r") { |f| f.write("x") } },
|
|
900
|
+
-> { File.open("/data/note.txt", "w") { |f| f.read } },
|
|
901
|
+
-> { $stdout.read }].each do |call|
|
|
902
|
+
begin; call.call; rescue IOError => e; out << e.message; end
|
|
903
|
+
end
|
|
904
|
+
print out.inspect
|
|
905
|
+
"#;
|
|
906
|
+
let Some(answer) = run(
|
|
907
|
+
script,
|
|
908
|
+
&[mount(note_tmpfs(), Access::ReadWrite)],
|
|
909
|
+
Vec::new(),
|
|
910
|
+
) else {
|
|
911
|
+
return;
|
|
912
|
+
};
|
|
913
|
+
assert_eq!(
|
|
914
|
+
answer,
|
|
915
|
+
Ok(
|
|
916
|
+
r#"["not opened for writing", "not opened for reading", "not opened for reading"]"#
|
|
917
|
+
.to_string()
|
|
918
|
+
),
|
|
919
|
+
"a write to a file opened with r, a read of one opened with w, and $stdout.read through \
|
|
920
|
+
the bundled guest must raise IOError as CRuby does"
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// @behavior FL-018
|
|
925
|
+
#[test]
|
|
926
|
+
fn a_path_that_is_no_valid_utf8_raises_eilseq() {
|
|
927
|
+
let Some(answer) = run(
|
|
928
|
+
r#"File.read("/data/\xff")"#,
|
|
929
|
+
&[mount(note_tmpfs(), Access::Read)],
|
|
930
|
+
Vec::new(),
|
|
931
|
+
) else {
|
|
932
|
+
return;
|
|
933
|
+
};
|
|
934
|
+
assert_eq!(
|
|
935
|
+
answer,
|
|
936
|
+
Err("Errno::EILSEQ".to_string()),
|
|
937
|
+
"File.read of a path holding the byte 0xff through the bundled guest must raise Errno::EILSEQ"
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// @behavior FL-015
|
|
942
|
+
#[test]
|
|
943
|
+
fn a_write_lands_whole_when_the_filesystem_takes_it_in_parts() {
|
|
944
|
+
let filesystem = WrapsFiles::over_note("", WritesInParts);
|
|
945
|
+
let Some(answer) = run(
|
|
946
|
+
r#"File.open("/data/note.txt", "w") { |f| print f.write("hello world") }; nil"#,
|
|
947
|
+
&[mount(filesystem.clone(), Access::ReadWrite)],
|
|
948
|
+
Vec::new(),
|
|
949
|
+
) else {
|
|
950
|
+
return;
|
|
951
|
+
};
|
|
952
|
+
assert_eq!(
|
|
953
|
+
(answer, contents(&filesystem,"note.txt")),
|
|
954
|
+
(Ok("11".to_string()), b"hello world".to_vec()),
|
|
955
|
+
"IO#write to a file whose Filesystem takes three bytes per write through the bundled guest \
|
|
956
|
+
must answer every byte and land them all"
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/// An open file reporting a length it does not hold, so a script meets a
|
|
961
|
+
/// file of several GiB without the test allocating one.
|
|
962
|
+
struct ReportsLen(Arc<dyn InodeOperations>, u64);
|
|
963
|
+
|
|
964
|
+
impl FileSystemType for ReportsLen {
|
|
965
|
+
fn fill_super(self: Arc<Self>, sb: &mut Filesystem) -> Result<(), FilesystemError> {
|
|
966
|
+
sb.d_make_root(Inode::new(&Kstat::new(FileType::Dir, 0), self));
|
|
967
|
+
Ok(())
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
impl InodeOperations for ReportsLen {
|
|
972
|
+
fn getattr(&self, dentry: &Dentry) -> Result<Kstat, FilesystemError> {
|
|
973
|
+
let mut stat = self.0.getattr(dentry)?;
|
|
974
|
+
if stat.file_type == FileType::RegFile {
|
|
975
|
+
stat.size = self.1;
|
|
976
|
+
}
|
|
977
|
+
Ok(stat)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
fn lookup(&self, dentry: &Dentry) -> Result<Lookup, FilesystemError> {
|
|
981
|
+
Ok(match self.0.lookup(dentry)? {
|
|
982
|
+
Lookup::Found(inode) => {
|
|
983
|
+
Lookup::Found(inode.with_ops(Arc::new(Self(inode.ops().clone(), self.1))))
|
|
984
|
+
}
|
|
985
|
+
other => other,
|
|
986
|
+
})
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
fn open(
|
|
990
|
+
&self,
|
|
991
|
+
dentry: &Dentry,
|
|
992
|
+
how: &OpenHow,
|
|
993
|
+
) -> Result<Box<dyn FileOperations>, FilesystemError> {
|
|
994
|
+
self.0.open(dentry, how)
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
fn three_gib_note() -> Grant {
|
|
999
|
+
let tmpfs: Tmpfs = [("note.txt", "hello")].into_iter().collect();
|
|
1000
|
+
mount(Arc::new(ReportsLen(Arc::new(tmpfs), 3 << 30)), Access::Read)
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// @behavior FL-020
|
|
1004
|
+
#[test]
|
|
1005
|
+
fn a_file_past_2_gib_reports_its_whole_size() {
|
|
1006
|
+
let Some(answer) = run(
|
|
1007
|
+
r#"File.open("/data/note.txt") { |f| print f.size }"#,
|
|
1008
|
+
&[three_gib_note()],
|
|
1009
|
+
Vec::new(),
|
|
1010
|
+
) else {
|
|
1011
|
+
return;
|
|
1012
|
+
};
|
|
1013
|
+
assert_eq!(
|
|
1014
|
+
answer,
|
|
1015
|
+
Ok("3221225472".to_string()),
|
|
1016
|
+
"File#size of a 3 GiB file through the bundled guest must answer 3221225472, not a \
|
|
1017
|
+
32-bit ceiling"
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// @behavior FL-021
|
|
1022
|
+
#[test]
|
|
1023
|
+
fn a_position_past_2_gib_is_reported_whole() {
|
|
1024
|
+
let Some(answer) = run(
|
|
1025
|
+
r#"File.open("/data/note.txt") { |f| f.seek(2147483649); print f.pos }"#,
|
|
1026
|
+
&[three_gib_note()],
|
|
1027
|
+
Vec::new(),
|
|
1028
|
+
) else {
|
|
1029
|
+
return;
|
|
1030
|
+
};
|
|
1031
|
+
assert_eq!(
|
|
1032
|
+
answer,
|
|
1033
|
+
Ok("2147483649".to_string()),
|
|
1034
|
+
"IO#pos after seeking a 3 GiB file to 2 GiB and one byte through the bundled guest \
|
|
1035
|
+
must answer 2147483649"
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// @behavior FL-036
|
|
1040
|
+
#[test]
|
|
1041
|
+
fn setting_a_files_position_moves_where_the_next_read_starts() {
|
|
1042
|
+
let Some(answer) = run(
|
|
1043
|
+
r#"File.open("/data/note.txt") { |f| f.pos = 2; print f.read }"#,
|
|
1044
|
+
&[mount(note_tmpfs(), Access::Read)],
|
|
1045
|
+
Vec::new(),
|
|
1046
|
+
) else {
|
|
1047
|
+
return;
|
|
1048
|
+
};
|
|
1049
|
+
assert_eq!(
|
|
1050
|
+
answer,
|
|
1051
|
+
Ok("llo\nworld\n".to_string()),
|
|
1052
|
+
"IO#pos= 2 over a file of hello and world through the bundled guest must leave the \
|
|
1053
|
+
next read at llo"
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// @behavior FL-037
|
|
1058
|
+
// Each seek is refused alone, so one that moved or answered another class
|
|
1059
|
+
// would print its own name in place of `Errno::EINVAL`.
|
|
1060
|
+
#[test]
|
|
1061
|
+
fn a_seek_before_the_start_or_by_an_unknown_whence_raises_einval() {
|
|
1062
|
+
let Some(answer) = run(
|
|
1063
|
+
r#"File.open("/data/note.txt") do |f|
|
|
1064
|
+
[-> { f.pos = -1 }, -> { f.seek(-1) }, -> { f.seek(1, 9) }].each do |seek|
|
|
1065
|
+
begin
|
|
1066
|
+
seek.call
|
|
1067
|
+
print "moved "
|
|
1068
|
+
rescue => e
|
|
1069
|
+
print e.class, " "
|
|
1070
|
+
end
|
|
1071
|
+
end
|
|
1072
|
+
end
|
|
1073
|
+
nil"#,
|
|
1074
|
+
&[mount(note_tmpfs(), Access::Read)],
|
|
1075
|
+
Vec::new(),
|
|
1076
|
+
) else {
|
|
1077
|
+
return;
|
|
1078
|
+
};
|
|
1079
|
+
assert_eq!(
|
|
1080
|
+
answer,
|
|
1081
|
+
Ok("Errno::EINVAL Errno::EINVAL Errno::EINVAL ".to_string()),
|
|
1082
|
+
"IO#pos= -1, IO#seek(-1), and IO#seek(1, 9) through the bundled guest must each raise \
|
|
1083
|
+
Errno::EINVAL"
|
|
1084
|
+
);
|
|
1085
|
+
}
|