microsandbox-rb 0.17.0 → 0.17.2
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/CHANGELOG.md +229 -8
- data/Cargo.lock +45 -46
- data/DESIGN.md +90 -72
- data/README.md +178 -669
- data/SECURITY.md +65 -0
- data/docs/configuration.md +154 -0
- data/docs/errors.md +74 -0
- data/docs/execution.md +108 -0
- data/docs/filesystem.md +45 -0
- data/docs/images.md +65 -0
- data/docs/lifecycle.md +141 -0
- data/docs/observability.md +99 -0
- data/docs/releasing.md +117 -0
- data/docs/runtime.md +154 -0
- data/docs/surface.md +75 -0
- data/ext/microsandbox/Cargo.toml +9 -5
- data/ext/microsandbox/src/agent.rs +13 -11
- data/ext/microsandbox/src/backend.rs +1 -1
- data/ext/microsandbox/src/exec.rs +12 -8
- data/ext/microsandbox/src/fs_stream.rs +19 -7
- data/ext/microsandbox/src/host_path.rs +108 -0
- data/ext/microsandbox/src/image.rs +2 -2
- data/ext/microsandbox/src/lib.rs +3 -2
- data/ext/microsandbox/src/runtime.rs +189 -13
- data/ext/microsandbox/src/sandbox.rs +149 -102
- data/ext/microsandbox/src/snapshot.rs +17 -11
- data/ext/microsandbox/src/ssh.rs +22 -16
- data/ext/microsandbox/src/stream.rs +6 -2
- data/ext/microsandbox/src/volume.rs +50 -17
- data/lib/microsandbox/fs.rb +4 -8
- data/lib/microsandbox/image.rb +2 -2
- data/lib/microsandbox/log_entry.rb +1 -1
- data/lib/microsandbox/metrics.rb +1 -1
- data/lib/microsandbox/outbound_proxy.rb +8 -0
- data/lib/microsandbox/sandbox.rb +128 -13
- data/lib/microsandbox/snapshot.rb +1 -1
- data/lib/microsandbox/ssh.rb +2 -10
- data/lib/microsandbox/streams.rb +8 -5
- data/lib/microsandbox/version.rb +1 -1
- data/lib/microsandbox/volume.rb +31 -14
- data/lib/microsandbox-rb.rb +7 -0
- data/lib/microsandbox.rb +88 -31
- data/sig/microsandbox.rbs +24 -0
- metadata +14 -1
|
@@ -110,7 +110,7 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
|
|
|
110
110
|
b = b.resumable();
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
let snap = block_on(b.create())
|
|
113
|
+
let snap = block_on(b.create())?.map_err(error::to_ruby)?;
|
|
114
114
|
Ok(snapshot_to_hash(&snap))
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -119,14 +119,14 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
|
|
|
119
119
|
/// Hash — the only way to inspect an artifact addressed by path (`get`/`list`
|
|
120
120
|
/// read the local index, which path-addressed artifacts are absent from).
|
|
121
121
|
fn open(path_or_name: String) -> Result<RHash, Error> {
|
|
122
|
-
let snap = block_on(Snapshot::open(&path_or_name))
|
|
122
|
+
let snap = block_on(Snapshot::open(&path_or_name))?.map_err(error::to_ruby)?;
|
|
123
123
|
Ok(snapshot_to_hash(&snap))
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/// Walk `dir` and parse each subdirectory's `snapshot.json` without touching
|
|
127
127
|
/// the local index — for enumerating external/un-imported snapshot collections.
|
|
128
128
|
fn list_dir(dir: String) -> Result<RArray, Error> {
|
|
129
|
-
let snaps = block_on(Snapshot::list_dir(Path::new(&dir)))
|
|
129
|
+
let snaps = block_on(Snapshot::list_dir(Path::new(&dir)))?.map_err(error::to_ruby)?;
|
|
130
130
|
let arr = ruby().ary_new();
|
|
131
131
|
for snap in &snaps {
|
|
132
132
|
arr.push(snapshot_to_hash(snap))?;
|
|
@@ -145,19 +145,19 @@ fn reindex(dir: Option<String>) -> Result<u64, Error> {
|
|
|
145
145
|
.map(|l| l.snapshots_dir())
|
|
146
146
|
.unwrap_or_else(|| PathBuf::from(".")),
|
|
147
147
|
};
|
|
148
|
-
let n = block_on(Snapshot::reindex(&dir))
|
|
148
|
+
let n = block_on(Snapshot::reindex(&dir))?.map_err(error::to_ruby)?;
|
|
149
149
|
Ok(n as u64)
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
/// Metadata for one snapshot by name or digest.
|
|
153
153
|
fn get(name_or_digest: String) -> Result<RHash, Error> {
|
|
154
|
-
let handle = block_on(Snapshot::get(&name_or_digest))
|
|
154
|
+
let handle = block_on(Snapshot::get(&name_or_digest))?.map_err(error::to_ruby)?;
|
|
155
155
|
Ok(handle_to_hash(&handle))
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
/// All snapshots as metadata hashes.
|
|
159
159
|
fn list() -> Result<RArray, Error> {
|
|
160
|
-
let handles = block_on(Snapshot::list())
|
|
160
|
+
let handles = block_on(Snapshot::list())?.map_err(error::to_ruby)?;
|
|
161
161
|
let arr = ruby().ary_new();
|
|
162
162
|
for h in &handles {
|
|
163
163
|
arr.push(handle_to_hash(h))?;
|
|
@@ -167,7 +167,7 @@ fn list() -> Result<RArray, Error> {
|
|
|
167
167
|
|
|
168
168
|
/// Remove a snapshot artifact by name or path.
|
|
169
169
|
fn remove(name_or_path: String, force: bool) -> Result<(), Error> {
|
|
170
|
-
block_on(Snapshot::remove(&name_or_path, force))
|
|
170
|
+
block_on(Snapshot::remove(&name_or_path, force))?.map_err(error::to_ruby)
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
/// Verify a snapshot's recorded upper-layer integrity. Returns
|
|
@@ -176,8 +176,14 @@ fn remove(name_or_path: String, force: bool) -> Result<(), Error> {
|
|
|
176
176
|
/// "verified" (mismatches raise SnapshotIntegrityError), snapshots without
|
|
177
177
|
/// recorded integrity report "not_recorded" with no algorithm/digest keys.
|
|
178
178
|
fn verify(name_or_path: String) -> Result<RHash, Error> {
|
|
179
|
-
|
|
180
|
-
|
|
179
|
+
// Open AND verify in ONE `block_on`, so the opened snapshot never sits in
|
|
180
|
+
// this frame between two calls (`block_on` returns `Err` when an interrupt
|
|
181
|
+
// is delivered before it starts; one call leaves no such in-between state).
|
|
182
|
+
let report = block_on(async move {
|
|
183
|
+
let snap = Snapshot::open(&name_or_path).await?;
|
|
184
|
+
snap.verify().await
|
|
185
|
+
})?
|
|
186
|
+
.map_err(error::to_ruby)?;
|
|
181
187
|
Ok(verify_report_to_hash(&report))
|
|
182
188
|
}
|
|
183
189
|
|
|
@@ -193,7 +199,7 @@ fn save(name_or_path: String, out_path: String, opts: RHash) -> Result<(), Error
|
|
|
193
199
|
&name_or_path,
|
|
194
200
|
std::path::Path::new(&out_path),
|
|
195
201
|
save_opts,
|
|
196
|
-
))
|
|
202
|
+
))?
|
|
197
203
|
.map_err(error::to_ruby)
|
|
198
204
|
}
|
|
199
205
|
|
|
@@ -204,7 +210,7 @@ fn load(archive_path: String, dest: Option<String>) -> Result<RHash, Error> {
|
|
|
204
210
|
let handle = block_on(Snapshot::load(
|
|
205
211
|
std::path::Path::new(&archive_path),
|
|
206
212
|
dest_path.as_deref(),
|
|
207
|
-
))
|
|
213
|
+
))?
|
|
208
214
|
.map_err(error::to_ruby)?;
|
|
209
215
|
Ok(handle_to_hash(&handle))
|
|
210
216
|
}
|
data/ext/microsandbox/src/ssh.rs
CHANGED
|
@@ -52,7 +52,7 @@ impl SshClient {
|
|
|
52
52
|
let guard = inner.lock().await;
|
|
53
53
|
let client = guard.as_ref().ok_or_else(consumed)?;
|
|
54
54
|
client.exec_with(command, |b| b.tty(tty)).await
|
|
55
|
-
})
|
|
55
|
+
})?;
|
|
56
56
|
Ok(ssh_output_to_hash(result.map_err(error::to_ruby)?))
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -74,7 +74,7 @@ impl SshClient {
|
|
|
74
74
|
b
|
|
75
75
|
})
|
|
76
76
|
.await
|
|
77
|
-
})
|
|
77
|
+
})?;
|
|
78
78
|
result.map_err(error::to_ruby)
|
|
79
79
|
}
|
|
80
80
|
|
|
@@ -85,7 +85,7 @@ impl SshClient {
|
|
|
85
85
|
let guard = inner.lock().await;
|
|
86
86
|
let client = guard.as_ref().ok_or_else(consumed)?;
|
|
87
87
|
client.sftp().await
|
|
88
|
-
})
|
|
88
|
+
})?;
|
|
89
89
|
Ok(SftpClient::from_core(result.map_err(error::to_ruby)?))
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -101,7 +101,7 @@ impl SshClient {
|
|
|
101
101
|
Some(c) => c.close().await,
|
|
102
102
|
None => Ok(()),
|
|
103
103
|
}
|
|
104
|
-
})
|
|
104
|
+
})?;
|
|
105
105
|
result.map_err(error::to_ruby)
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -144,7 +144,7 @@ impl SftpClient {
|
|
|
144
144
|
let guard = inner.lock().await;
|
|
145
145
|
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
146
146
|
sftp.read(path).await.map_err(sftp_str)
|
|
147
|
-
})
|
|
147
|
+
})?;
|
|
148
148
|
Ok(ruby().str_from_slice(&result.map_err(error::base_error)?))
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -158,7 +158,7 @@ impl SftpClient {
|
|
|
158
158
|
file.write_all(&bytes).await.map_err(sftp_str)?;
|
|
159
159
|
file.shutdown().await.map_err(sftp_str)?;
|
|
160
160
|
Ok(())
|
|
161
|
-
})
|
|
161
|
+
})?;
|
|
162
162
|
result.map_err(error::base_error)
|
|
163
163
|
}
|
|
164
164
|
|
|
@@ -168,7 +168,7 @@ impl SftpClient {
|
|
|
168
168
|
let guard = inner.lock().await;
|
|
169
169
|
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
170
170
|
sftp.create_dir(path).await.map_err(sftp_str)
|
|
171
|
-
})
|
|
171
|
+
})?;
|
|
172
172
|
result.map_err(error::base_error)
|
|
173
173
|
}
|
|
174
174
|
|
|
@@ -178,7 +178,7 @@ impl SftpClient {
|
|
|
178
178
|
let guard = inner.lock().await;
|
|
179
179
|
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
180
180
|
sftp.remove_file(path).await.map_err(sftp_str)
|
|
181
|
-
})
|
|
181
|
+
})?;
|
|
182
182
|
result.map_err(error::base_error)
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -188,7 +188,7 @@ impl SftpClient {
|
|
|
188
188
|
let guard = inner.lock().await;
|
|
189
189
|
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
190
190
|
sftp.remove_dir(path).await.map_err(sftp_str)
|
|
191
|
-
})
|
|
191
|
+
})?;
|
|
192
192
|
result.map_err(error::base_error)
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -198,7 +198,7 @@ impl SftpClient {
|
|
|
198
198
|
let guard = inner.lock().await;
|
|
199
199
|
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
200
200
|
sftp.rename(old_path, new_path).await.map_err(sftp_str)
|
|
201
|
-
})
|
|
201
|
+
})?;
|
|
202
202
|
result.map_err(error::base_error)
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -208,7 +208,7 @@ impl SftpClient {
|
|
|
208
208
|
let guard = inner.lock().await;
|
|
209
209
|
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
210
210
|
sftp.symlink(target, link_path).await.map_err(sftp_str)
|
|
211
|
-
})
|
|
211
|
+
})?;
|
|
212
212
|
result.map_err(error::base_error)
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -218,7 +218,7 @@ impl SftpClient {
|
|
|
218
218
|
let guard = inner.lock().await;
|
|
219
219
|
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
220
220
|
sftp.canonicalize(path).await.map_err(sftp_str)
|
|
221
|
-
})
|
|
221
|
+
})?;
|
|
222
222
|
result.map_err(error::base_error)
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -228,7 +228,7 @@ impl SftpClient {
|
|
|
228
228
|
let guard = inner.lock().await;
|
|
229
229
|
let sftp = guard.as_ref().ok_or_else(|| sftp_str("session closed"))?;
|
|
230
230
|
sftp.read_link(path).await.map_err(sftp_str)
|
|
231
|
-
})
|
|
231
|
+
})?;
|
|
232
232
|
result.map_err(error::base_error)
|
|
233
233
|
}
|
|
234
234
|
|
|
@@ -243,7 +243,7 @@ impl SftpClient {
|
|
|
243
243
|
Some(s) => s.close().await.map_err(sftp_str),
|
|
244
244
|
None => Ok(()),
|
|
245
245
|
}
|
|
246
|
-
})
|
|
246
|
+
})?;
|
|
247
247
|
result.map_err(error::base_error)
|
|
248
248
|
}
|
|
249
249
|
}
|
|
@@ -276,7 +276,7 @@ impl SshServer {
|
|
|
276
276
|
Some(s) => s.serve_connection(SshStdioStream::new()).await,
|
|
277
277
|
None => Err(consumed()),
|
|
278
278
|
}
|
|
279
|
-
})
|
|
279
|
+
})?;
|
|
280
280
|
result.map_err(error::to_ruby)
|
|
281
281
|
}
|
|
282
282
|
|
|
@@ -285,19 +285,23 @@ impl SshServer {
|
|
|
285
285
|
let inner = Arc::clone(&self.inner);
|
|
286
286
|
block_on(async move {
|
|
287
287
|
inner.lock().await.take();
|
|
288
|
-
})
|
|
288
|
+
})?;
|
|
289
289
|
Ok(())
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
294
294
|
let client = native.define_class("SshClient", ruby.class_object())?;
|
|
295
|
+
// Resolve the wrapper class now, not on first use (see `preload_class`).
|
|
296
|
+
crate::runtime::preload_class::<SshClient>(ruby);
|
|
295
297
|
client.define_method("exec", method!(SshClient::exec, 2))?;
|
|
296
298
|
client.define_method("attach", method!(SshClient::attach, 2))?;
|
|
297
299
|
client.define_method("sftp", method!(SshClient::sftp, 0))?;
|
|
298
300
|
client.define_method("close", method!(SshClient::close, 0))?;
|
|
299
301
|
|
|
300
302
|
let sftp = native.define_class("SftpClient", ruby.class_object())?;
|
|
303
|
+
// Resolve the wrapper class now, not on first use (see `preload_class`).
|
|
304
|
+
crate::runtime::preload_class::<SftpClient>(ruby);
|
|
301
305
|
sftp.define_method("read", method!(SftpClient::read, 1))?;
|
|
302
306
|
sftp.define_method("write", method!(SftpClient::write, 2))?;
|
|
303
307
|
sftp.define_method("mkdir", method!(SftpClient::mkdir, 1))?;
|
|
@@ -310,6 +314,8 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
310
314
|
sftp.define_method("close", method!(SftpClient::close, 0))?;
|
|
311
315
|
|
|
312
316
|
let server = native.define_class("SshServer", ruby.class_object())?;
|
|
317
|
+
// Resolve the wrapper class now, not on first use (see `preload_class`).
|
|
318
|
+
crate::runtime::preload_class::<SshServer>(ruby);
|
|
313
319
|
server.define_method("serve_connection", method!(SshServer::serve_connection, 0))?;
|
|
314
320
|
server.define_method("close", method!(SshServer::close, 0))?;
|
|
315
321
|
|
|
@@ -41,7 +41,7 @@ impl LogStream {
|
|
|
41
41
|
fn recv(&self) -> Result<Option<RHash>, Error> {
|
|
42
42
|
use futures::StreamExt;
|
|
43
43
|
let inner = Arc::clone(&self.inner);
|
|
44
|
-
match block_on(async move { inner.lock().await.next().await }) {
|
|
44
|
+
match block_on(async move { inner.lock().await.next().await })? {
|
|
45
45
|
Some(Ok(entry)) => Ok(Some(log_entry_to_hash(&entry))),
|
|
46
46
|
Some(Err(e)) => Err(error::to_ruby(e)),
|
|
47
47
|
None => Ok(None),
|
|
@@ -67,7 +67,7 @@ impl MetricsStream {
|
|
|
67
67
|
fn recv(&self) -> Result<Option<RHash>, Error> {
|
|
68
68
|
use futures::StreamExt;
|
|
69
69
|
let inner = Arc::clone(&self.inner);
|
|
70
|
-
match block_on(async move { inner.lock().await.next().await }) {
|
|
70
|
+
match block_on(async move { inner.lock().await.next().await })? {
|
|
71
71
|
Some(Ok(metrics)) => Ok(Some(metrics_to_hash(&metrics))),
|
|
72
72
|
Some(Err(e)) => Err(error::to_ruby(e)),
|
|
73
73
|
None => Ok(None),
|
|
@@ -77,9 +77,13 @@ impl MetricsStream {
|
|
|
77
77
|
|
|
78
78
|
pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
79
79
|
let logs = native.define_class("LogStream", ruby.class_object())?;
|
|
80
|
+
// Resolve the wrapper class now, not on first use (see `preload_class`).
|
|
81
|
+
crate::runtime::preload_class::<LogStream>(ruby);
|
|
80
82
|
logs.define_method("recv", method!(LogStream::recv, 0))?;
|
|
81
83
|
|
|
82
84
|
let metrics = native.define_class("MetricsStream", ruby.class_object())?;
|
|
85
|
+
// Resolve the wrapper class now, not on first use (see `preload_class`).
|
|
86
|
+
crate::runtime::preload_class::<MetricsStream>(ruby);
|
|
83
87
|
metrics.define_method("recv", method!(MetricsStream::recv, 0))?;
|
|
84
88
|
|
|
85
89
|
Ok(())
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
//! Mirrors `sdk/python/src/volume.rs`. Static async CRUD over the volume store;
|
|
4
4
|
//! results are returned as plain Ruby Hashes/Arrays.
|
|
5
5
|
|
|
6
|
+
use std::path::{Component, Path};
|
|
6
7
|
use std::sync::Arc;
|
|
7
8
|
|
|
8
9
|
use magnus::{function, method, prelude::*, Error, RArray, RHash, RModule, RString, Ruby};
|
|
9
10
|
use microsandbox::volume::VolumeHandle;
|
|
10
|
-
use microsandbox::Backend;
|
|
11
|
+
use microsandbox::{Backend, MicrosandboxError};
|
|
11
12
|
|
|
12
13
|
use crate::conv;
|
|
13
14
|
use crate::error;
|
|
@@ -67,7 +68,7 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
|
|
|
67
68
|
builder = builder.label(k, v);
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
let vol = block_on(builder.create())
|
|
71
|
+
let vol = block_on(builder.create())?.map_err(error::to_ruby)?;
|
|
71
72
|
let hash = ruby().hash_new();
|
|
72
73
|
hash.aset("name", vol.name().to_string())?;
|
|
73
74
|
// `path()` is fallible as of v0.5.8 (returns `Unsupported` for cloud
|
|
@@ -80,19 +81,19 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
fn get(name: String) -> Result<RHash, Error> {
|
|
83
|
-
let handle = block_on(microsandbox::Volume::get(&name))
|
|
84
|
+
let handle = block_on(microsandbox::Volume::get(&name))?.map_err(error::to_ruby)?;
|
|
84
85
|
Ok(handle_to_hash(&handle))
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
/// The backend's default volume (v0.6.9). Cloud-backend only — the local
|
|
88
89
|
/// backend rejects it (Unsupported) to avoid accidental host access.
|
|
89
90
|
fn get_default() -> Result<RHash, Error> {
|
|
90
|
-
let handle = block_on(microsandbox::Volume::get_default())
|
|
91
|
+
let handle = block_on(microsandbox::Volume::get_default())?.map_err(error::to_ruby)?;
|
|
91
92
|
Ok(handle_to_hash(&handle))
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
fn list() -> Result<RArray, Error> {
|
|
95
|
-
let handles = block_on(microsandbox::Volume::list())
|
|
96
|
+
let handles = block_on(microsandbox::Volume::list())?.map_err(error::to_ruby)?;
|
|
96
97
|
let arr = ruby().ary_new();
|
|
97
98
|
for h in handles.iter() {
|
|
98
99
|
arr.push(handle_to_hash(h))?;
|
|
@@ -101,7 +102,24 @@ fn list() -> Result<RArray, Error> {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
fn remove(name: String) -> Result<(), Error> {
|
|
104
|
-
block_on(microsandbox::Volume::remove(&name))
|
|
105
|
+
block_on(microsandbox::Volume::remove(&name))?.map_err(error::to_ruby)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Volume paths are relative to the volume root and never need a `..` segment,
|
|
109
|
+
/// so one is refused before the path reaches the core's resolver (which keeps
|
|
110
|
+
/// its own containment check). Enforced here rather than only in the Ruby
|
|
111
|
+
/// wrapper because `Microsandbox::Native::VolumeFs` is callable directly.
|
|
112
|
+
fn contained(path: &str) -> Result<&str, Error> {
|
|
113
|
+
if Path::new(path)
|
|
114
|
+
.components()
|
|
115
|
+
.any(|part| matches!(part, Component::ParentDir))
|
|
116
|
+
{
|
|
117
|
+
return Err(error::to_ruby(MicrosandboxError::SandboxFsOps(
|
|
118
|
+
"path traversal outside volume root (`..` segments are not allowed in volume paths)"
|
|
119
|
+
.into(),
|
|
120
|
+
)));
|
|
121
|
+
}
|
|
122
|
+
Ok(path)
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
/// A host-side filesystem view over a named volume — read/write its contents
|
|
@@ -131,26 +149,30 @@ impl VolumeFs {
|
|
|
131
149
|
}
|
|
132
150
|
|
|
133
151
|
fn read(&self, path: String) -> Result<RString, Error> {
|
|
152
|
+
let path = contained(&path)?;
|
|
134
153
|
let fs = self.fs();
|
|
135
|
-
let bytes = block_on(fs.read(
|
|
154
|
+
let bytes = block_on(fs.read(path))?.map_err(error::to_ruby)?;
|
|
136
155
|
Ok(ruby().str_from_slice(bytes.as_ref()))
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
fn read_text(&self, path: String) -> Result<String, Error> {
|
|
159
|
+
let path = contained(&path)?;
|
|
140
160
|
let fs = self.fs();
|
|
141
|
-
block_on(fs.read_to_string(
|
|
161
|
+
block_on(fs.read_to_string(path))?.map_err(error::to_ruby)
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
fn write(&self, path: String, data: RString) -> Result<(), Error> {
|
|
145
165
|
// Copy the bytes out while the GVL is held (GC.compact could move them).
|
|
146
166
|
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
167
|
+
let path = contained(&path)?;
|
|
147
168
|
let fs = self.fs();
|
|
148
|
-
block_on(fs.write(
|
|
169
|
+
block_on(fs.write(path, &bytes))?.map_err(error::to_ruby)
|
|
149
170
|
}
|
|
150
171
|
|
|
151
172
|
fn list(&self, path: String) -> Result<RArray, Error> {
|
|
173
|
+
let path = contained(&path)?;
|
|
152
174
|
let fs = self.fs();
|
|
153
|
-
let entries = block_on(fs.list(
|
|
175
|
+
let entries = block_on(fs.list(path))?.map_err(error::to_ruby)?;
|
|
154
176
|
let arr = ruby().ary_new();
|
|
155
177
|
for entry in &entries {
|
|
156
178
|
arr.push(fs_entry_to_hash(entry))?;
|
|
@@ -159,38 +181,47 @@ impl VolumeFs {
|
|
|
159
181
|
}
|
|
160
182
|
|
|
161
183
|
fn mkdir(&self, path: String) -> Result<(), Error> {
|
|
184
|
+
let path = contained(&path)?;
|
|
162
185
|
let fs = self.fs();
|
|
163
|
-
block_on(fs.mkdir(
|
|
186
|
+
block_on(fs.mkdir(path))?.map_err(error::to_ruby)
|
|
164
187
|
}
|
|
165
188
|
|
|
166
189
|
fn remove_file(&self, path: String) -> Result<(), Error> {
|
|
190
|
+
let path = contained(&path)?;
|
|
167
191
|
let fs = self.fs();
|
|
168
|
-
block_on(fs.remove(
|
|
192
|
+
block_on(fs.remove(path))?.map_err(error::to_ruby)
|
|
169
193
|
}
|
|
170
194
|
|
|
171
195
|
fn remove_dir(&self, path: String) -> Result<(), Error> {
|
|
196
|
+
let path = contained(&path)?;
|
|
172
197
|
let fs = self.fs();
|
|
173
|
-
block_on(fs.remove_dir(
|
|
198
|
+
block_on(fs.remove_dir(path))?.map_err(error::to_ruby)
|
|
174
199
|
}
|
|
175
200
|
|
|
176
201
|
fn exists(&self, path: String) -> Result<bool, Error> {
|
|
202
|
+
let path = contained(&path)?;
|
|
177
203
|
let fs = self.fs();
|
|
178
|
-
block_on(fs.exists(
|
|
204
|
+
block_on(fs.exists(path))?.map_err(error::to_ruby)
|
|
179
205
|
}
|
|
180
206
|
|
|
181
207
|
fn copy(&self, from: String, to: String) -> Result<(), Error> {
|
|
208
|
+
let from = contained(&from)?;
|
|
209
|
+
let to = contained(&to)?;
|
|
182
210
|
let fs = self.fs();
|
|
183
|
-
block_on(fs.copy(
|
|
211
|
+
block_on(fs.copy(from, to))?.map_err(error::to_ruby)
|
|
184
212
|
}
|
|
185
213
|
|
|
186
214
|
fn rename(&self, from: String, to: String) -> Result<(), Error> {
|
|
215
|
+
let from = contained(&from)?;
|
|
216
|
+
let to = contained(&to)?;
|
|
187
217
|
let fs = self.fs();
|
|
188
|
-
block_on(fs.rename(
|
|
218
|
+
block_on(fs.rename(from, to))?.map_err(error::to_ruby)
|
|
189
219
|
}
|
|
190
220
|
|
|
191
221
|
fn stat(&self, path: String) -> Result<RHash, Error> {
|
|
222
|
+
let path = contained(&path)?;
|
|
192
223
|
let fs = self.fs();
|
|
193
|
-
let meta = block_on(fs.stat(
|
|
224
|
+
let meta = block_on(fs.stat(path))?.map_err(error::to_ruby)?;
|
|
194
225
|
Ok(fs_metadata_to_hash(&meta))
|
|
195
226
|
}
|
|
196
227
|
}
|
|
@@ -210,6 +241,8 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
|
|
|
210
241
|
class.define_singleton_method("fs", function!(fs, 1))?;
|
|
211
242
|
|
|
212
243
|
let vfs = native.define_class("VolumeFs", ruby.class_object())?;
|
|
244
|
+
// Resolve the wrapper class now, not on first use (see `preload_class`).
|
|
245
|
+
crate::runtime::preload_class::<VolumeFs>(ruby);
|
|
213
246
|
vfs.define_method("read", method!(VolumeFs::read, 1))?;
|
|
214
247
|
vfs.define_method("read_text", method!(VolumeFs::read_text, 1))?;
|
|
215
248
|
vfs.define_method("write", method!(VolumeFs::write, 2))?;
|
data/lib/microsandbox/fs.rb
CHANGED
|
@@ -27,7 +27,7 @@ module Microsandbox
|
|
|
27
27
|
|
|
28
28
|
# @return [Time, nil] last-modified time, if known
|
|
29
29
|
def modified
|
|
30
|
-
|
|
30
|
+
Microsandbox.time_from_ms(@modified_ms)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def file? = @type == :file
|
|
@@ -64,12 +64,12 @@ module Microsandbox
|
|
|
64
64
|
|
|
65
65
|
# @return [Time, nil] last-modified time, if known
|
|
66
66
|
def modified
|
|
67
|
-
|
|
67
|
+
Microsandbox.time_from_ms(@modified_ms)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# @return [Time, nil] creation time, if known
|
|
71
71
|
def created
|
|
72
|
-
|
|
72
|
+
Microsandbox.time_from_ms(@created_ms)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
75
|
def inspect
|
|
@@ -188,11 +188,7 @@ module Microsandbox
|
|
|
188
188
|
sink = FsWriteSink.new(@native.fs_write_stream(path.to_s))
|
|
189
189
|
return sink unless block_given?
|
|
190
190
|
|
|
191
|
-
|
|
192
|
-
yield sink
|
|
193
|
-
ensure
|
|
194
|
-
sink.close
|
|
195
|
-
end
|
|
191
|
+
Microsandbox.close_after(sink) { yield sink }
|
|
196
192
|
end
|
|
197
193
|
end
|
|
198
194
|
|
data/lib/microsandbox/image.rb
CHANGED
|
@@ -18,12 +18,12 @@ module Microsandbox
|
|
|
18
18
|
|
|
19
19
|
# @return [Time, nil]
|
|
20
20
|
def created_at
|
|
21
|
-
|
|
21
|
+
Microsandbox.time_from_ms(@created_at_ms)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# @return [Time, nil]
|
|
25
25
|
def last_used_at
|
|
26
|
-
|
|
26
|
+
Microsandbox.time_from_ms(@last_used_at_ms)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def inspect
|
data/lib/microsandbox/metrics.rb
CHANGED
|
@@ -246,6 +246,14 @@ module Microsandbox
|
|
|
246
246
|
if address.empty?
|
|
247
247
|
raise ArgumentError, "proxy address must be a non-empty \"IP:port\" String (got \"\")"
|
|
248
248
|
end
|
|
249
|
+
# A URL-style address (`socks5://user:pass@host:1080`) is never valid, and
|
|
250
|
+
# the core would quote it back in its error. Refuse it by shape, without
|
|
251
|
+
# echoing it, so the embedded credentials stay out of logs.
|
|
252
|
+
if address.match?(%r{@|://|\s})
|
|
253
|
+
raise ArgumentError,
|
|
254
|
+
"proxy address must be \"IP:port\" with no scheme, credentials or whitespace " \
|
|
255
|
+
"(pass SOCKS5 credentials as username:/password:)"
|
|
256
|
+
end
|
|
249
257
|
# Same rules as the Python SDK's OutboundProxy.__post_init__; the address
|
|
250
258
|
# itself is parsed by the core (which reports e.g. "invalid SOCKS5 proxy
|
|
251
259
|
# address" as an InvalidConfigError at create time).
|