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