kobako 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +217 -10
- data/Cargo.lock +139 -141
- data/README.md +96 -574
- data/ROADMAP.md +2 -1
- data/crates/kobako-runtime/CHANGELOG.md +269 -1
- data/crates/kobako-runtime/Cargo.toml +25 -15
- data/crates/kobako-runtime/README.md +7 -3
- data/crates/kobako-runtime/src/dispatch.rs +1 -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/{directory/spelling.rs → bind/lookup.rs} +91 -49
- 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 +227 -734
- data/crates/kobako-runtime/src/filesystem/overlay.rs +2649 -1135
- data/crates/kobako-runtime/src/filesystem/path.rs +2 -41
- 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 +21 -387
- data/crates/kobako-runtime/src/hashtable.rs +44 -0
- data/crates/kobako-runtime/src/lib.rs +11 -6
- 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 +31 -104
- data/crates/kobako-runtime/src/profile.rs +137 -1
- data/crates/kobako-runtime/src/sched.rs +210 -0
- data/crates/kobako-runtime/src/security.rs +696 -0
- 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/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 +8 -2
- data/crates/kobako-transport/Cargo.toml +3 -3
- data/crates/kobako-transport/README.md +6 -5
- 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 +2 -2
- data/crates/kobako-transport/src/envelope/outcome.rs +3 -3
- data/crates/kobako-transport/src/envelope/preamble.rs +12 -12
- data/crates/kobako-transport/src/envelope/reply.rs +2 -2
- data/crates/kobako-transport/src/envelope/request.rs +2 -2
- 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 +154 -8
- data/crates/kobako-wasmtime/Cargo.toml +15 -14
- data/crates/kobako-wasmtime/README.md +9 -5
- data/crates/kobako-wasmtime/src/abi.rs +12 -20
- data/crates/kobako-wasmtime/src/ambient.rs +1 -2
- data/crates/kobako-wasmtime/src/cache.rs +5 -5
- data/crates/kobako-wasmtime/src/config.rs +22 -7
- data/crates/kobako-wasmtime/src/descriptors.rs +133 -100
- data/crates/kobako-wasmtime/src/driver.rs +29 -96
- 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 +14 -8
- data/crates/kobako-wasmtime/src/instance_pre.rs +3 -3
- data/crates/kobako-wasmtime/src/invocation.rs +26 -46
- data/crates/kobako-wasmtime/src/lib.rs +1 -4
- data/crates/kobako-wasmtime/src/limiter.rs +6 -230
- data/crates/kobako-wasmtime/src/preview1/differential/mount.rs +69 -46
- data/crates/kobako-wasmtime/src/preview1/differential.rs +121 -7
- data/crates/kobako-wasmtime/src/preview1/errno.rs +74 -206
- data/crates/kobako-wasmtime/src/preview1/fd.rs +1248 -500
- data/crates/kobako-wasmtime/src/preview1/memory.rs +117 -150
- data/crates/kobako-wasmtime/src/preview1/mount_view/wrap.rs +211 -0
- data/crates/kobako-wasmtime/src/preview1/mount_view.rs +1939 -672
- data/crates/kobako-wasmtime/src/preview1/path.rs +230 -349
- data/crates/kobako-wasmtime/src/preview1/poll.rs +148 -100
- data/crates/kobako-wasmtime/src/preview1/sock.rs +6 -23
- 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 +464 -413
- data/crates/kobako-wasmtime/src/trap.rs +9 -9
- data/crates/kobako-wasmtime/src/wasi.rs +53 -37
- data/crates/kobako-wasmtime/tests/{guest_file.rs → integration/guest_file.rs} +232 -201
- data/crates/kobako-wasmtime/tests/integration/main.rs +29 -0
- data/crates/kobako-wasmtime/tests/{preamble_flags.rs → integration/preamble_flags.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{snippet_compile.rs → integration/snippet_compile.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{unanswered_call.rs → integration/unanswered_call.rs} +1 -1
- data/crates/kobako-wasmtime/tests/{unsupported_call.rs → integration/unsupported_call.rs} +1 -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 +1 -1
- data/ext/kobako/src/runtime/bridge.rs +1 -1
- data/ext/kobako/src/runtime/errors.rs +1 -1
- data/ext/kobako/src/runtime/escape.rs +28 -14
- data/ext/kobako/src/runtime/filesystem/grants.rs +21 -5
- data/ext/kobako/src/runtime/filesystem/ruby.rs +464 -156
- data/ext/kobako/src/runtime/filesystem.rs +109 -137
- data/ext/kobako/src/runtime/gvl.rs +43 -8
- data/ext/kobako/src/runtime/snapshot.rs +1 -1
- data/ext/kobako/src/runtime.rs +6 -1
- data/lib/kobako/catalog/extensions.rb +5 -5
- data/lib/kobako/catalog/flags.rb +2 -2
- data/lib/kobako/catalog/handles.rb +6 -6
- data/lib/kobako/catalog/mount.rb +2 -10
- data/lib/kobako/catalog/services.rb +2 -2
- data/lib/kobako/catalog/snippets.rb +7 -7
- data/lib/kobako/catalog.rb +1 -1
- data/lib/kobako/codec/decoder.rb +10 -10
- data/lib/kobako/codec/ext_types.rb +3 -5
- data/lib/kobako/codec/handle_walk.rb +4 -6
- data/lib/kobako/codec/nesting.rb +7 -7
- data/lib/kobako/codec/utils.rb +1 -1
- data/lib/kobako/codec/value_budget.rb +3 -3
- data/lib/kobako/context.rb +5 -5
- data/lib/kobako/filesystem.rb +44 -13
- data/lib/kobako/payload/arguments.rb +2 -2
- data/lib/kobako/pool.rb +4 -4
- data/lib/kobako/sandbox.rb +28 -28
- data/lib/kobako/sandbox_options.rb +2 -3
- data/lib/kobako/snippet/bytecode.rb +1 -1
- data/lib/kobako/snippet/source.rb +1 -1
- data/lib/kobako/transport/dispatcher.rb +5 -5
- data/lib/kobako/transport/exposure.rb +2 -2
- data/lib/kobako/transport/reflection.rb +7 -7
- data/lib/kobako/transport/run.rb +5 -5
- data/lib/kobako/transport/yielder.rb +1 -1
- data/lib/kobako/version.rb +1 -1
- data/lib/kobako.rb +34 -0
- data/release-please-config.json +1 -0
- data/sig/kobako/catalog/extensions.rbs +3 -3
- data/sig/kobako/catalog/handles.rbs +3 -3
- data/sig/kobako/catalog/mount.rbs +0 -4
- data/sig/kobako/catalog/services.rbs +1 -1
- data/sig/kobako/catalog/snippets.rbs +3 -3
- data/sig/kobako/catalog.rbs +1 -1
- 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/utils.rbs +1 -1
- data/sig/kobako/codec/value_budget.rbs +1 -1
- data/sig/kobako/codec.rbs +1 -2
- data/sig/kobako/context.rbs +1 -1
- data/sig/kobako/filesystem.rbs +19 -14
- data/sig/kobako/guest_yielder.rbs +1 -1
- data/sig/kobako/payload/arguments.rbs +1 -1
- data/sig/kobako/pool.rbs +2 -2
- data/sig/kobako/runtime.rbs +1 -1
- data/sig/kobako/sandbox.rbs +3 -5
- data/sig/kobako/sandbox_options.rbs +1 -1
- data/sig/kobako/transport/dispatcher.rbs +2 -2
- data/sig/kobako/transport/exposure.rbs +1 -1
- data/sig/kobako/transport/reflection.rbs +2 -2
- data/sig/kobako/transport/run.rbs +3 -3
- data/sig/kobako/transport.rbs +1 -1
- metadata +67 -19
- data/crates/kobako-runtime/src/filesystem/directory.rs +0 -1082
- data/crates/kobako-runtime/src/filesystem/memory.rs +0 -1100
- data/crates/kobako-runtime/src/filesystem/overlay/exclusive.rs +0 -88
- data/crates/kobako-runtime/src/filesystem/temporary.rs +0 -167
- data/crates/kobako-wasmtime/src/capture.rs +0 -96
- data/crates/kobako-wasmtime/src/dispatch.rs +0 -125
- data/crates/kobako-wasmtime/src/guest_mem.rs +0 -271
- data/crates/kobako-wasmtime/src/io.rs +0 -89
- data/crates/kobako-wasmtime/src/preview1/args.rs +0 -60
- data/crates/kobako-wasmtime/src/preview1/beneath.rs +0 -188
- /data/crates/kobako-wasmtime/tests/{compile_posture.rs → integration/compile_posture.rs} +0 -0
- /data/crates/kobako-wasmtime/tests/{support → integration/support}/mod.rs +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
//! The Ruby face of the Filesystem: `Kobako::Filesystem::
|
|
2
|
-
//! `
|
|
3
|
-
//!
|
|
1
|
+
//! The Ruby face of the Filesystem: `Kobako::Filesystem::Tmpfs`,
|
|
2
|
+
//! `Bind`, and `Overlay`, each standing for the `kobako_runtime`
|
|
3
|
+
//! Filesystem of that kind.
|
|
4
4
|
//!
|
|
5
5
|
//! A kind kobako ships runs in Rust; Ruby only picks it and the access.
|
|
6
|
-
//! Ruby is asked again while the guest runs through a mount's block
|
|
7
|
-
//!
|
|
8
|
-
//! Ruby, once per call.
|
|
6
|
+
//! Ruby is asked again while the guest runs, through a mount's block and
|
|
7
|
+
//! through a Filesystem written in Ruby.
|
|
9
8
|
|
|
10
9
|
mod grants;
|
|
11
10
|
mod ruby;
|
|
@@ -14,38 +13,36 @@ use std::sync::Arc;
|
|
|
14
13
|
|
|
15
14
|
use magnus::value::{Opaque, ReprValue};
|
|
16
15
|
use magnus::{
|
|
17
|
-
function, gc,
|
|
18
|
-
|
|
19
|
-
Value,
|
|
16
|
+
function, gc, prelude::*, scan_args::get_kwargs, scan_args::scan_args, DataTypeFunctions,
|
|
17
|
+
Error as MagnusError, RArray, RHash, RModule, RString, Ruby, Symbol, TypedData, Value,
|
|
20
18
|
};
|
|
21
19
|
|
|
22
20
|
use ruby::RubyFilesystem;
|
|
23
21
|
|
|
24
|
-
use kobako_runtime::filesystem::{
|
|
25
|
-
Access, Blocking, Directory, Filesystem, Memory, Overlay, Temporary,
|
|
26
|
-
};
|
|
22
|
+
use kobako_runtime::filesystem::{Bind, Filesystem, FilesystemError, Overlay, Tmpfs};
|
|
27
23
|
|
|
28
24
|
pub(super) use grants::{to_grants, validate_mount};
|
|
29
25
|
|
|
26
|
+
/// Each kind kobako ships is one Filesystem instance, made once with its
|
|
27
|
+
/// Ruby object, so every Mount of that object shares it as a bind mount
|
|
28
|
+
/// shares a super block.
|
|
30
29
|
macro_rules! filesystem_class {
|
|
31
30
|
($name:ident, $class:literal) => {
|
|
32
31
|
#[magnus::wrap(class = $class, free_immediately, size)]
|
|
33
|
-
pub(super) struct $name(Arc<
|
|
32
|
+
pub(super) struct $name(Arc<Filesystem>);
|
|
34
33
|
};
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
filesystem_class!(
|
|
38
|
-
filesystem_class!(
|
|
39
|
-
filesystem_class!(DirectoryFs, "Kobako::Filesystem::Directory");
|
|
36
|
+
filesystem_class!(TmpfsFs, "Kobako::Filesystem::Tmpfs");
|
|
37
|
+
filesystem_class!(BindFs, "Kobako::Filesystem::Bind");
|
|
40
38
|
|
|
41
|
-
/// The Overlay
|
|
42
|
-
/// new one and leave this one as it was. It marks the Ruby object of
|
|
39
|
+
/// The Overlay's one Filesystem instance. It marks the Ruby object of
|
|
43
40
|
/// every Layer, since a Filesystem written in Ruby has nothing else
|
|
44
41
|
/// keeping it alive.
|
|
45
42
|
#[derive(magnus::TypedData)]
|
|
46
43
|
#[magnus(class = "Kobako::Filesystem::Overlay", free_immediately, size, mark)]
|
|
47
44
|
pub(super) struct OverlayFs {
|
|
48
|
-
|
|
45
|
+
filesystem: Arc<Filesystem>,
|
|
49
46
|
layers: Vec<Opaque<Value>>,
|
|
50
47
|
}
|
|
51
48
|
|
|
@@ -57,40 +54,36 @@ impl DataTypeFunctions for OverlayFs {
|
|
|
57
54
|
|
|
58
55
|
pub(super) fn register(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
59
56
|
let module = kobako.define_module("Filesystem")?;
|
|
60
|
-
let
|
|
61
|
-
|
|
62
|
-
let
|
|
63
|
-
|
|
64
|
-
let directory = module.define_class("Directory", ruby.class_object())?;
|
|
65
|
-
directory.define_singleton_method("new", function!(directory_new, 1))?;
|
|
57
|
+
let tmpfs = module.define_class("Tmpfs", ruby.class_object())?;
|
|
58
|
+
tmpfs.define_singleton_method("new", function!(tmpfs_new, -1))?;
|
|
59
|
+
let bind = module.define_class("Bind", ruby.class_object())?;
|
|
60
|
+
bind.define_singleton_method("new", function!(bind_new, 1))?;
|
|
66
61
|
let overlay = module.define_class("Overlay", ruby.class_object())?;
|
|
67
|
-
overlay.define_singleton_method("new", function!(overlay_new,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
TemporaryFs::class(ruby);
|
|
71
|
-
DirectoryFs::class(ruby);
|
|
62
|
+
overlay.define_singleton_method("new", function!(overlay_new, -1))?;
|
|
63
|
+
TmpfsFs::class(ruby);
|
|
64
|
+
BindFs::class(ruby);
|
|
72
65
|
OverlayFs::class(ruby);
|
|
73
66
|
Ok(())
|
|
74
67
|
}
|
|
75
68
|
|
|
76
|
-
/// `
|
|
69
|
+
/// `Tmpfs.new(files = {}, size: 64 MiB, nr_inodes:)`: each
|
|
77
70
|
/// `path => contents` pair becomes a file, its directories created along
|
|
78
71
|
/// the way. Ruby hands a bare `"path" => contents` over as keywords, so
|
|
79
|
-
/// every String keyword is a file too. Without `
|
|
80
|
-
/// follow the
|
|
81
|
-
fn
|
|
72
|
+
/// every String keyword is a file too. Without `nr_inodes:` the entries
|
|
73
|
+
/// follow the size.
|
|
74
|
+
fn tmpfs_new(args: &[Value]) -> Result<TmpfsFs, MagnusError> {
|
|
82
75
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
83
76
|
let args = scan_args::<(), (Option<RHash>,), (), (), RHash, ()>(args)?;
|
|
84
77
|
let mut files = Vec::new();
|
|
85
|
-
let mut
|
|
86
|
-
let mut
|
|
87
|
-
let
|
|
88
|
-
let
|
|
78
|
+
let mut size = Some(Tmpfs::DEFAULT_SIZE);
|
|
79
|
+
let mut nr_inodes = None;
|
|
80
|
+
let size_key = ruby.to_symbol("size");
|
|
81
|
+
let nr_inodes_key = ruby.to_symbol("nr_inodes");
|
|
89
82
|
for hash in args.optional.0.into_iter().chain([args.keywords]) {
|
|
90
83
|
hash.foreach(|key: Value, value: Value| {
|
|
91
84
|
match Symbol::from_value(key) {
|
|
92
|
-
Some(key) if key.equal(
|
|
93
|
-
Some(key) if key.equal(
|
|
85
|
+
Some(key) if key.equal(size_key)? => size = to_count(value)?,
|
|
86
|
+
Some(key) if key.equal(nr_inodes_key)? => nr_inodes = Some(to_count(value)?),
|
|
94
87
|
Some(key) => {
|
|
95
88
|
return Err(MagnusError::new(
|
|
96
89
|
ruby.exception_arg_error(),
|
|
@@ -105,125 +98,104 @@ fn memory_new(args: &[Value]) -> Result<MemoryFs, MagnusError> {
|
|
|
105
98
|
Ok(magnus::r_hash::ForEach::Continue)
|
|
106
99
|
})?;
|
|
107
100
|
}
|
|
108
|
-
// A list no
|
|
109
|
-
let
|
|
101
|
+
// A list no Tmpfs holds is an `ArgumentError` the Host App can rescue.
|
|
102
|
+
let tmpfs = Tmpfs::try_from_iter(files)
|
|
110
103
|
.map_err(|error| MagnusError::new(ruby.exception_arg_error(), error.to_string()))?
|
|
111
|
-
.
|
|
112
|
-
|
|
113
|
-
Some(
|
|
114
|
-
None =>
|
|
115
|
-
}))
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/// `Temporary.new(limit: 64 MiB, inodes:)`.
|
|
119
|
-
fn temporary_new(args: &[Value]) -> Result<TemporaryFs, MagnusError> {
|
|
120
|
-
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
121
|
-
let kwargs = get_kwargs::<_, (), (Option<Value>, Option<Value>), ()>(
|
|
122
|
-
args.keywords,
|
|
123
|
-
&[],
|
|
124
|
-
&["limit", "inodes"],
|
|
125
|
-
)?;
|
|
126
|
-
let (limit, inodes) = kwargs.optional;
|
|
127
|
-
let limit = match limit {
|
|
128
|
-
Some(limit) => to_limit(limit)?,
|
|
129
|
-
None => Some(Memory::DEFAULT_LIMIT),
|
|
130
|
-
};
|
|
131
|
-
let temporary = Temporary::new().with_limit(limit);
|
|
132
|
-
Ok(TemporaryFs(Arc::new(match inodes {
|
|
133
|
-
Some(inodes) => temporary.with_inodes(to_limit(inodes)?),
|
|
134
|
-
None => temporary,
|
|
135
|
-
})))
|
|
104
|
+
.with_size(size);
|
|
105
|
+
let filesystem = Filesystem::new(Arc::new(match nr_inodes {
|
|
106
|
+
Some(nr_inodes) => tmpfs.with_nr_inodes(nr_inodes),
|
|
107
|
+
None => tmpfs,
|
|
108
|
+
}));
|
|
109
|
+
filesystem.map(TmpfsFs).map_err(cannot_stand)
|
|
136
110
|
}
|
|
137
111
|
|
|
138
|
-
/// A
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
fn directory_new(path: String) -> Result<DirectoryFs, MagnusError> {
|
|
112
|
+
/// A Filesystem whose instance cannot stand, an `ArgumentError` the Host App
|
|
113
|
+
/// can rescue.
|
|
114
|
+
fn cannot_stand(error: FilesystemError) -> MagnusError {
|
|
144
115
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
145
|
-
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
ruby.exception_arg_error(),
|
|
150
|
-
format!("cannot open host directory {path}: {err}"),
|
|
151
|
-
)
|
|
152
|
-
})
|
|
116
|
+
MagnusError::new(
|
|
117
|
+
ruby.exception_arg_error(),
|
|
118
|
+
format!("cannot make a Filesystem: {error:?}"),
|
|
119
|
+
)
|
|
153
120
|
}
|
|
154
121
|
|
|
155
|
-
/// `
|
|
156
|
-
fn
|
|
157
|
-
|
|
158
|
-
overlay: Overlay::new().with_blocking(Arc::new(GvlFreeWait)),
|
|
159
|
-
layers: Vec::new(),
|
|
160
|
-
}
|
|
122
|
+
/// A `size:` or `nr_inodes:` count, or `nil` for none.
|
|
123
|
+
fn to_count(count: Value) -> Result<Option<usize>, MagnusError> {
|
|
124
|
+
Option::<usize>::try_convert(count)
|
|
161
125
|
}
|
|
162
126
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
127
|
+
fn bind_new(path: String) -> Result<BindFs, MagnusError> {
|
|
128
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
129
|
+
let directory = Bind::new(&path).map_err(|err| {
|
|
130
|
+
MagnusError::new(
|
|
131
|
+
ruby.exception_arg_error(),
|
|
132
|
+
format!("cannot open host directory {path}: {err}"),
|
|
133
|
+
)
|
|
134
|
+
})?;
|
|
135
|
+
Filesystem::new(Arc::new(directory))
|
|
136
|
+
.map(BindFs)
|
|
137
|
+
.map_err(cannot_stand)
|
|
171
138
|
}
|
|
172
139
|
|
|
173
|
-
/// `
|
|
174
|
-
///
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let
|
|
178
|
-
let (
|
|
179
|
-
let
|
|
180
|
-
|
|
181
|
-
|
|
140
|
+
/// `Overlay.new(lower:, upper: nil)`: the Filesystems in `lower`, topmost
|
|
141
|
+
/// first, under `upper`, a Tmpfs. Without `upper` it takes two lower
|
|
142
|
+
/// Layers at least.
|
|
143
|
+
fn overlay_new(args: &[Value]) -> Result<OverlayFs, MagnusError> {
|
|
144
|
+
let ruby = Ruby::get().expect("Ruby thread");
|
|
145
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
146
|
+
let kwargs =
|
|
147
|
+
get_kwargs::<_, (RArray,), (Option<Value>,), ()>(args.keywords, &["lower"], &["upper"])?;
|
|
148
|
+
let lower = (0..kwargs.required.0.len())
|
|
149
|
+
.map(|index| kwargs.required.0.entry::<Value>(index as isize))
|
|
150
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
151
|
+
let upper = kwargs.optional.0.filter(|upper| !upper.is_nil());
|
|
152
|
+
let upper_fs = match upper {
|
|
153
|
+
Some(upper) => match <&TmpfsFs>::try_convert(upper) {
|
|
154
|
+
Ok(tmpfs) => Some(tmpfs.0.clone()),
|
|
155
|
+
Err(_) => {
|
|
156
|
+
return Err(MagnusError::new(
|
|
157
|
+
ruby.exception_type_error(),
|
|
158
|
+
format!(
|
|
159
|
+
"upper must be a Kobako::Filesystem::Tmpfs, got {}",
|
|
160
|
+
upper.class()
|
|
161
|
+
),
|
|
162
|
+
))
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
None => None,
|
|
182
166
|
};
|
|
183
|
-
let
|
|
184
|
-
|
|
167
|
+
let lower_fs = lower
|
|
168
|
+
.iter()
|
|
169
|
+
.map(|layer| to_filesystem(*layer))
|
|
170
|
+
.collect::<Result<Vec<_>, _>>()?;
|
|
171
|
+
let overlay = Overlay::new(lower_fs, upper_fs).map_err(|_| {
|
|
172
|
+
MagnusError::new(
|
|
173
|
+
ruby.exception_arg_error(),
|
|
174
|
+
"an Overlay needs a lower Layer under its upper, or two lower Layers without one, \
|
|
175
|
+
each Filesystem at most once",
|
|
176
|
+
)
|
|
177
|
+
})?;
|
|
185
178
|
Ok(OverlayFs {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
.clone()
|
|
189
|
-
.layer(to_filesystem(filesystem)?, access),
|
|
190
|
-
layers,
|
|
179
|
+
filesystem: Filesystem::new(Arc::new(overlay)).map_err(cannot_stand)?,
|
|
180
|
+
layers: lower.into_iter().chain(upper).map(Opaque::from).collect(),
|
|
191
181
|
})
|
|
192
182
|
}
|
|
193
183
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
Some("read") => Ok(Access::Read),
|
|
200
|
-
Some("read_write") => Ok(Access::ReadWrite),
|
|
201
|
-
_ => Err(MagnusError::new(
|
|
202
|
-
Ruby::get().expect("Ruby thread").exception_arg_error(),
|
|
203
|
-
format!(
|
|
204
|
-
"access must be :read or :read_write, got {}",
|
|
205
|
-
access.inspect()
|
|
206
|
-
),
|
|
207
|
-
)),
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/// The Filesystem a Ruby handle of any kobako kind stands for.
|
|
212
|
-
fn to_filesystem(value: Value) -> Result<Arc<dyn Filesystem>, MagnusError> {
|
|
213
|
-
if let Ok(fs) = <&MemoryFs>::try_convert(value) {
|
|
214
|
-
return Ok(fs.0.clone());
|
|
215
|
-
}
|
|
216
|
-
if let Ok(fs) = <&TemporaryFs>::try_convert(value) {
|
|
184
|
+
/// The Filesystem instance a Ruby handle of any kobako kind stands for. A
|
|
185
|
+
/// Filesystem written in Ruby has no instance of its own to share, so each
|
|
186
|
+
/// call makes one.
|
|
187
|
+
fn to_filesystem(value: Value) -> Result<Arc<Filesystem>, MagnusError> {
|
|
188
|
+
if let Ok(fs) = <&TmpfsFs>::try_convert(value) {
|
|
217
189
|
return Ok(fs.0.clone());
|
|
218
190
|
}
|
|
219
|
-
if let Ok(fs) = <&
|
|
191
|
+
if let Ok(fs) = <&BindFs>::try_convert(value) {
|
|
220
192
|
return Ok(fs.0.clone());
|
|
221
193
|
}
|
|
222
194
|
if let Ok(fs) = <&OverlayFs>::try_convert(value) {
|
|
223
|
-
return Ok(
|
|
195
|
+
return Ok(fs.filesystem.clone());
|
|
224
196
|
}
|
|
225
197
|
if let Some(fs) = RubyFilesystem::from_value(value) {
|
|
226
|
-
return
|
|
198
|
+
return Filesystem::new(Arc::new(fs)).map_err(cannot_stand);
|
|
227
199
|
}
|
|
228
200
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
229
201
|
Err(MagnusError::new(
|
|
@@ -6,16 +6,24 @@
|
|
|
6
6
|
//! callback, which is ordinary Ruby and must hold the lock. A thread-local flag
|
|
7
7
|
//! stops a nested dispatch from re-acquiring a second time, and both directions
|
|
8
8
|
//! run their closure through a `catch_unwind` trampoline because a panic must
|
|
9
|
-
//! never cross the C frame of `
|
|
9
|
+
//! never cross the C frame of `rb_thread_call_without_gvl2` /
|
|
10
10
|
//! `rb_thread_call_with_gvl` (undefined behaviour). The flag is per-thread,
|
|
11
11
|
//! matching the one-thread-one-Sandbox contract, and is restored through a drop
|
|
12
12
|
//! guard so an unwinding panic cannot leave it corrupt for the next invocation.
|
|
13
|
+
//!
|
|
14
|
+
//! A release never raises where it returns, as `rb_thread_call_without_gvl`
|
|
15
|
+
//! would, since that jumps over every Rust frame of the run and what they hold:
|
|
16
|
+
//! a lock's guard, this flag. An interrupt waits for the run to end instead.
|
|
13
17
|
|
|
14
18
|
use std::cell::Cell;
|
|
15
19
|
use std::os::raw::c_void;
|
|
16
20
|
use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
|
|
17
21
|
use std::ptr;
|
|
18
22
|
|
|
23
|
+
use kobako_runtime::sched::{Blocking, Task};
|
|
24
|
+
|
|
25
|
+
use super::escape;
|
|
26
|
+
|
|
19
27
|
thread_local! {
|
|
20
28
|
// True while this thread sits inside a region whose GVL we released and
|
|
21
29
|
// have not yet re-acquired. Drives `reenter`'s one-shot re-acquire.
|
|
@@ -30,17 +38,39 @@ where
|
|
|
30
38
|
return f();
|
|
31
39
|
}
|
|
32
40
|
let _guard = ReleasedGuard::set(true);
|
|
41
|
+
// An interrupt the thread receives meanwhile stops each wait for a lock the
|
|
42
|
+
// run meets; the interrupt itself still waits for the run to end.
|
|
43
|
+
let task = Task::current();
|
|
44
|
+
let task_ptr = ptr::addr_of!(task).cast_mut().cast::<c_void>();
|
|
33
45
|
call_via(f, |func, data| unsafe {
|
|
34
|
-
rb_sys::
|
|
46
|
+
rb_sys::rb_thread_call_without_gvl2(Some(func), data, Some(interrupt_task), task_ptr)
|
|
35
47
|
})
|
|
36
48
|
}
|
|
37
49
|
|
|
50
|
+
/// Have the thread's run stop waiting for locks, called by Ruby without the GVL
|
|
51
|
+
/// as the region's unblocking function.
|
|
52
|
+
unsafe extern "C" fn interrupt_task(task: *mut c_void) {
|
|
53
|
+
let task = unsafe { &*task.cast::<Task>() };
|
|
54
|
+
task.interrupt();
|
|
55
|
+
}
|
|
56
|
+
|
|
38
57
|
/// Whether this thread holds the GVL: outside any released region, or
|
|
39
58
|
/// inside a dispatch that re-acquired it.
|
|
40
|
-
|
|
59
|
+
fn holds() -> bool {
|
|
41
60
|
!GVL_RELEASED.with(|g| g.get())
|
|
42
61
|
}
|
|
43
62
|
|
|
63
|
+
/// A thread waits for a lock without the GVL, so a holder asking a
|
|
64
|
+
/// Filesystem written in Ruby, or a mount's block, can take the GVL and
|
|
65
|
+
/// finish.
|
|
66
|
+
pub(crate) struct GvlFreeWait;
|
|
67
|
+
|
|
68
|
+
impl Blocking for GvlFreeWait {
|
|
69
|
+
fn wait(&self, wait: &mut dyn FnMut()) {
|
|
70
|
+
region(holds(), wait);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
44
74
|
/// Runs inline when the GVL is already held — hold mode, or a nested
|
|
45
75
|
/// dispatch whose outer frame re-acquired it.
|
|
46
76
|
pub(crate) fn reenter<F, R>(f: F) -> R
|
|
@@ -95,7 +125,7 @@ where
|
|
|
95
125
|
|
|
96
126
|
fn call_via<F, R>(
|
|
97
127
|
f: F,
|
|
98
|
-
invoke: impl
|
|
128
|
+
mut invoke: impl FnMut(unsafe extern "C" fn(*mut c_void) -> *mut c_void, *mut c_void) -> *mut c_void,
|
|
99
129
|
) -> R
|
|
100
130
|
where
|
|
101
131
|
F: FnOnce() -> R,
|
|
@@ -104,9 +134,14 @@ where
|
|
|
104
134
|
f: Some(f),
|
|
105
135
|
result: None,
|
|
106
136
|
};
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
137
|
+
loop {
|
|
138
|
+
invoke(trampoline::<F, R>, ptr::addr_of_mut!(ctx).cast::<c_void>());
|
|
139
|
+
match ctx.result.take() {
|
|
140
|
+
Some(Ok(value)) => return value,
|
|
141
|
+
Some(Err(payload)) => resume_unwind(payload),
|
|
142
|
+
// `rb_thread_call_without_gvl2` runs nothing while an interrupt
|
|
143
|
+
// is pending, so that one is taken off the thread first.
|
|
144
|
+
None => escape::hold_interrupts(),
|
|
145
|
+
}
|
|
111
146
|
}
|
|
112
147
|
}
|
|
@@ -46,7 +46,7 @@ impl Snapshot {
|
|
|
46
46
|
Self(snapshot, verb)
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/// Already split off the
|
|
49
|
+
/// Already split off the Envelope, so the Ruby side maps a failure
|
|
50
50
|
/// onto its error taxonomy without decoding a payload byte: the ok
|
|
51
51
|
/// arm's payload, or a Panic's fields beside an empty payload. A trap
|
|
52
52
|
/// has no Outcome; `#trap_error` is the discriminator there.
|
data/ext/kobako/src/runtime.rs
CHANGED
|
@@ -111,13 +111,17 @@ pub fn init(ruby: &Ruby, kobako: RModule) -> Result<(), MagnusError> {
|
|
|
111
111
|
// directly into those classes through the constructors and mappers
|
|
112
112
|
// in `runtime/errors.rs` — no intermediate hierarchy is registered.
|
|
113
113
|
|
|
114
|
+
// This extension is the only frontend in its process, so the first
|
|
115
|
+
// registration is the one that stands.
|
|
116
|
+
let _ = kobako_runtime::sched::register_blocking(Box::new(gvl::GvlFreeWait));
|
|
117
|
+
|
|
114
118
|
let runtime = kobako.define_class("Runtime", ruby.class_object())?;
|
|
115
119
|
runtime.define_singleton_method("from_path", function!(Runtime::from_path, 2))?;
|
|
116
120
|
runtime.define_method("eval", method!(Runtime::eval, 3))?;
|
|
117
121
|
runtime.define_method("run", method!(Runtime::run, 4))?;
|
|
118
122
|
runtime.define_method("profile", method!(Runtime::profile, 0))?;
|
|
119
123
|
runtime.define_method("compile", method!(Runtime::compile, 1))?;
|
|
120
|
-
runtime.define_singleton_method("validate_mount
|
|
124
|
+
runtime.define_singleton_method("validate_mount", function!(filesystem::validate_mount, 3))?;
|
|
121
125
|
// The guest re-enters for a block yield through a frame-scoped
|
|
122
126
|
// `Kobako::GuestYielder` the dispatcher hands the Proc, not a method on
|
|
123
127
|
// Runtime.
|
|
@@ -331,6 +335,7 @@ impl Runtime {
|
|
|
331
335
|
let ruby = Ruby::get().expect("Ruby thread");
|
|
332
336
|
let preamble = to_preamble(&ruby, preamble)?;
|
|
333
337
|
let bytecodes = gvl::region(self.release_gvl, || self.driver.compile(&preamble));
|
|
338
|
+
escape::resume()?;
|
|
334
339
|
Ok(bytecodes.map(|bytecodes| {
|
|
335
340
|
ruby.ary_from_iter(
|
|
336
341
|
bytecodes
|
|
@@ -16,10 +16,10 @@ module Kobako
|
|
|
16
16
|
# The Extension readers are duck-typed, so a malformed +name+ or
|
|
17
17
|
# +backend.path+ surfaces through the registry it routes to.
|
|
18
18
|
def install(extension, snippets:, services:)
|
|
19
|
-
validate
|
|
19
|
+
validate(extension)
|
|
20
20
|
snippets.register(code: extension.source, name: extension.name)
|
|
21
21
|
backend = extension.backend
|
|
22
|
-
services.bind(backend.path,
|
|
22
|
+
services.bind(backend.path, object_of(backend)) if backend
|
|
23
23
|
@entries << extension
|
|
24
24
|
self
|
|
25
25
|
end
|
|
@@ -27,7 +27,7 @@ module Kobako
|
|
|
27
27
|
# The asserted flag flips only on success, so a check that failed
|
|
28
28
|
# re-runs on the next invocation rather than silently passing a broken
|
|
29
29
|
# Sandbox.
|
|
30
|
-
def assert_dependencies
|
|
30
|
+
def assert_dependencies
|
|
31
31
|
return self if @asserted
|
|
32
32
|
|
|
33
33
|
extension, dependency = unmet_dependency
|
|
@@ -52,7 +52,7 @@ module Kobako
|
|
|
52
52
|
|
|
53
53
|
private
|
|
54
54
|
|
|
55
|
-
def
|
|
55
|
+
def object_of(backend) = backend.object.nil? ? Kobako::Unresolved : backend.object
|
|
56
56
|
|
|
57
57
|
def resolve_backend(extension, resolved, by_provider)
|
|
58
58
|
backend = extension.backend
|
|
@@ -64,7 +64,7 @@ module Kobako
|
|
|
64
64
|
resolved[backend.path.to_s] = by_provider.fetch(provider) { by_provider[provider] = provider.call }
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
def validate
|
|
67
|
+
def validate(extension)
|
|
68
68
|
source = extension.source
|
|
69
69
|
raise ArgumentError, "Extension #source must be a String, got #{source.class}" unless source.is_a?(String)
|
|
70
70
|
|
data/lib/kobako/catalog/flags.rb
CHANGED
|
@@ -18,10 +18,10 @@ module Kobako
|
|
|
18
18
|
end
|
|
19
19
|
raise ArgumentError, "flag value must be a String, got #{value.class}" unless value.is_a?(String)
|
|
20
20
|
|
|
21
|
-
key = Catalog.to_utf8
|
|
21
|
+
key = Catalog.to_utf8(name.to_s, "flag name")
|
|
22
22
|
raise ArgumentError, "flag #{key.inspect} already set" if @entries.key?(key)
|
|
23
23
|
|
|
24
|
-
@entries[key] = [Catalog.to_utf8
|
|
24
|
+
@entries[key] = [Catalog.to_utf8(value, "flag value"), !advisory]
|
|
25
25
|
nil
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -23,8 +23,8 @@ module Kobako
|
|
|
23
23
|
# call made through this Handle is authorized against the reference
|
|
24
24
|
# the guest was given.
|
|
25
25
|
def alloc(object)
|
|
26
|
-
reject_unwrappable
|
|
27
|
-
ensure_capacity
|
|
26
|
+
reject_unwrappable(object)
|
|
27
|
+
ensure_capacity
|
|
28
28
|
id = @next_id
|
|
29
29
|
@entries[id] = Kobako::Transport::Exposure.of(object, @surfaces)
|
|
30
30
|
@next_id = id + 1
|
|
@@ -36,7 +36,7 @@ module Kobako
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def exposure(id)
|
|
39
|
-
require_bound
|
|
39
|
+
require_bound(id)
|
|
40
40
|
@entries[id]
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -61,7 +61,7 @@ module Kobako
|
|
|
61
61
|
# a surface the floor reads as ordinary Service behaviour. Raising here
|
|
62
62
|
# keeps the rule at the single mint point, so it holds on both the
|
|
63
63
|
# Service-return and the +#run+ host→guest auto-wrap paths.
|
|
64
|
-
def reject_unwrappable
|
|
64
|
+
def reject_unwrappable(object)
|
|
65
65
|
case object
|
|
66
66
|
when Binding, Method, UnboundMethod, Module, Delegator
|
|
67
67
|
# Delegator < BasicObject exposes no static +#class+, so name the
|
|
@@ -71,7 +71,7 @@ module Kobako
|
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
-
def ensure_capacity
|
|
74
|
+
def ensure_capacity
|
|
75
75
|
cap = Kobako::Handle::MAX_ID
|
|
76
76
|
return unless @next_id > cap
|
|
77
77
|
|
|
@@ -80,7 +80,7 @@ module Kobako
|
|
|
80
80
|
"in a single invocation (limit #{cap})"
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
def require_bound
|
|
83
|
+
def require_bound(id)
|
|
84
84
|
return if @entries.key?(id)
|
|
85
85
|
|
|
86
86
|
raise SandboxError, "unknown Handle id: #{id.inspect}"
|
data/lib/kobako/catalog/mount.rb
CHANGED
|
@@ -7,21 +7,13 @@ module Kobako
|
|
|
7
7
|
def key = at
|
|
8
8
|
|
|
9
9
|
# Why +other+ cannot be mounted beside this Mount, or +nil+: a path
|
|
10
|
-
# means one Mount,
|
|
10
|
+
# means one Mount, as Docker refuses a duplicate mount point.
|
|
11
11
|
def conflict(other)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"path #{at.inspect} nests with the mounted path #{other.at.inspect}" if nested?(other.at)
|
|
12
|
+
"path #{at.inspect} is already mounted" if other.at == at
|
|
15
13
|
end
|
|
16
14
|
|
|
17
15
|
# The Mount as the native side reads it.
|
|
18
16
|
def to_native = [at, access, filesystem, policy]
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def nested?(other)
|
|
23
|
-
[at, other].include?("/") || at.start_with?("#{other}/") || other.start_with?("#{at}/")
|
|
24
|
-
end
|
|
25
17
|
end
|
|
26
18
|
end
|
|
27
19
|
end
|
|
@@ -16,7 +16,7 @@ module Kobako
|
|
|
16
16
|
|
|
17
17
|
# The binding records +object+'s Exposure as it stands now.
|
|
18
18
|
def bind(path, object)
|
|
19
|
-
path_str = validate_path
|
|
19
|
+
path_str = validate_path(path)
|
|
20
20
|
if reserved?(path_str)
|
|
21
21
|
raise ArgumentError,
|
|
22
22
|
"Service path #{path_str} is in the reserved #{RESERVED_NAMESPACE} namespace"
|
|
@@ -46,7 +46,7 @@ module Kobako
|
|
|
46
46
|
|
|
47
47
|
private
|
|
48
48
|
|
|
49
|
-
def validate_path
|
|
49
|
+
def validate_path(path)
|
|
50
50
|
path_str = path.to_s
|
|
51
51
|
segments = path_str.split("::", -1)
|
|
52
52
|
return path_str if !segments.empty? && segments.all? { |seg| CONSTANT_NAME.match?(seg) }
|
|
@@ -40,17 +40,17 @@ module Kobako
|
|
|
40
40
|
if binary
|
|
41
41
|
raise ArgumentError, "cannot combine binary: with code: / name:" if code || name
|
|
42
42
|
|
|
43
|
-
register_binary
|
|
43
|
+
register_binary(binary)
|
|
44
44
|
else
|
|
45
|
-
register_source
|
|
45
|
+
register_source(code, name)
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
private
|
|
50
50
|
|
|
51
|
-
def register_source
|
|
52
|
-
code, name = ensure_source_args
|
|
53
|
-
body = Catalog.to_utf8
|
|
51
|
+
def register_source(code, name)
|
|
52
|
+
code, name = ensure_source_args(code, name)
|
|
53
|
+
body = Catalog.to_utf8(code, "code")
|
|
54
54
|
|
|
55
55
|
name_sym = normalize_name(name)
|
|
56
56
|
if @entries.any? { |e| e.is_a?(Snippet::Source) && e.name == name_sym }
|
|
@@ -63,7 +63,7 @@ module Kobako
|
|
|
63
63
|
|
|
64
64
|
# The +code:+ type check runs first so an explicit +code: nil+ reads
|
|
65
65
|
# as a type error rather than a missing keyword.
|
|
66
|
-
def ensure_source_args
|
|
66
|
+
def ensure_source_args(code, name)
|
|
67
67
|
raise ArgumentError, "missing keyword: code: + name:, or binary:" if code.nil? && name.nil?
|
|
68
68
|
raise ArgumentError, "code must be a String, got #{code.class}" unless code.is_a?(String)
|
|
69
69
|
raise ArgumentError, "missing keyword: name:" if name.nil?
|
|
@@ -73,7 +73,7 @@ module Kobako
|
|
|
73
73
|
|
|
74
74
|
# Kept as bytes whatever encoding the caller tagged them with, since
|
|
75
75
|
# the guest reads them as bytecode rather than text.
|
|
76
|
-
def register_binary
|
|
76
|
+
def register_binary(bytes)
|
|
77
77
|
raise ArgumentError, "binary must be a String, got #{bytes.class}" unless bytes.is_a?(String)
|
|
78
78
|
|
|
79
79
|
@entries << Snippet::Bytecode.new(body: bytes.dup.force_encoding(Encoding::ASCII_8BIT))
|
data/lib/kobako/catalog.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Kobako
|
|
|
13
13
|
# The native side carries a registered text only as UTF-8, so text read
|
|
14
14
|
# as bytes is re-tagged, and bytes that are not UTF-8 are refused at
|
|
15
15
|
# registration rather than failing every invocation.
|
|
16
|
-
def self.to_utf8
|
|
16
|
+
def self.to_utf8(text, what)
|
|
17
17
|
utf8 = text.dup.force_encoding(Encoding::UTF_8)
|
|
18
18
|
raise ArgumentError, "#{what} must be UTF-8 text" unless utf8.valid_encoding?
|
|
19
19
|
|