microsandbox-rb 0.5.9 → 0.5.11

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.
@@ -89,9 +89,11 @@ impl Sandbox {
89
89
  for (host, guest) in conv::opt_port_map(opts, "ports")? {
90
90
  b = b.port(host, guest);
91
91
  }
92
- // volumes: normalized by the Ruby layer to [guest, kind, source] triples.
92
+ // volumes: normalized by the Ruby layer to [guest, kind, source] triples,
93
+ // or [guest, kind, source, options] quads where options is a comma-joined
94
+ // list (ro/readonly, rw, noexec, nosuid, nodev).
93
95
  for spec in conv::opt::<Vec<Vec<String>>>(opts, "volumes")?.unwrap_or_default() {
94
- if spec.len() != 3 {
96
+ if spec.len() < 3 || spec.len() > 4 {
95
97
  return Err(error::base_error("invalid volume mount spec"));
96
98
  }
97
99
  let (guest, kind, source) = (spec[0].clone(), spec[1].clone(), spec[2].clone());
@@ -103,12 +105,43 @@ impl Sandbox {
103
105
  )))
104
106
  }
105
107
  }
108
+ // Validate options up front (the volume closure cannot return an error).
109
+ let mount_opts: Vec<String> = spec
110
+ .get(3)
111
+ .map(|s| {
112
+ s.split(',')
113
+ .map(|o| o.trim().to_string())
114
+ .filter(|o| !o.is_empty())
115
+ .collect()
116
+ })
117
+ .unwrap_or_default();
118
+ for opt in &mount_opts {
119
+ match opt.as_str() {
120
+ "ro" | "readonly" | "rw" | "noexec" | "nosuid" | "nodev" => {}
121
+ other => {
122
+ return Err(error::base_error(format!(
123
+ "unknown volume mount option {other:?} \
124
+ (expected ro/rw/noexec/nosuid/nodev)"
125
+ )))
126
+ }
127
+ }
128
+ }
106
129
  b = b.volume(guest, move |m| {
107
- if kind == "named" {
130
+ let mut m = if kind == "named" {
108
131
  m.named(source)
109
132
  } else {
110
133
  m.bind(source)
134
+ };
135
+ for opt in &mount_opts {
136
+ m = match opt.as_str() {
137
+ "ro" | "readonly" => m.readonly(),
138
+ "noexec" => m.noexec(),
139
+ "nosuid" => m.nosuid(),
140
+ "nodev" => m.nodev(),
141
+ _ => m, // "rw" — default; already validated above
142
+ };
111
143
  }
144
+ m
112
145
  });
113
146
  }
114
147
  // patches: rootfs modifications applied before boot. The Ruby layer
@@ -209,27 +242,36 @@ impl Sandbox {
209
242
  Ok(Sandbox::from_inner(inner))
210
243
  }
211
244
 
212
- /// Lightweight metadata for a sandbox by name (running or not).
213
- fn get(name: String) -> Result<RHash, Error> {
245
+ /// A controllable handle for a sandbox by name (running or not). Carries
246
+ /// metadata accessors and the full lifecycle surface (see `SbHandle`).
247
+ fn get(name: String) -> Result<SbHandle, Error> {
214
248
  let handle = block_on(microsandbox::Sandbox::get(&name)).map_err(error::to_ruby)?;
215
- Ok(handle_to_hash(&handle))
249
+ Ok(SbHandle::from_inner(handle))
216
250
  }
217
251
 
218
- /// All sandboxes as metadata hashes.
252
+ /// All sandboxes as controllable handles.
219
253
  fn list() -> Result<RArray, Error> {
220
254
  let handles = block_on(microsandbox::Sandbox::list()).map_err(error::to_ruby)?;
221
- rhash_array(handles.iter().map(handle_to_hash))
255
+ let arr = ruby().ary_new();
256
+ for h in handles {
257
+ arr.push(SbHandle::from_inner(h))?;
258
+ }
259
+ Ok(arr)
222
260
  }
223
261
 
224
- /// Sandboxes filtered by required `key=value` labels (AND-matched). `opts`
225
- /// carries a string→string `labels` map.
262
+ /// Sandboxes filtered by required `key=value` labels (AND-matched), as
263
+ /// controllable handles. `opts` carries a string→string `labels` map.
226
264
  fn list_with(opts: RHash) -> Result<RArray, Error> {
227
265
  let mut filter = SandboxFilter::new();
228
266
  for (k, v) in conv::opt_string_map(opts, "labels")? {
229
267
  filter = filter.label(k, v);
230
268
  }
231
269
  let handles = block_on(microsandbox::Sandbox::list_with(filter)).map_err(error::to_ruby)?;
232
- rhash_array(handles.iter().map(handle_to_hash))
270
+ let arr = ruby().ary_new();
271
+ for h in handles {
272
+ arr.push(SbHandle::from_inner(h))?;
273
+ }
274
+ Ok(arr)
233
275
  }
234
276
 
235
277
  /// Remove a (stopped) sandbox by name.
@@ -286,44 +328,46 @@ impl Sandbox {
286
328
  Ok(ExecHandle::from_core(handle))
287
329
  }
288
330
 
289
- /// Graceful stop (+ wait). `timeout` is optional seconds.
290
- fn stop(&self, timeout: Option<f64>) -> Result<(), Error> {
291
- match timeout {
292
- Some(secs) => block_on(self.inner.stop_with_timeout(Duration::from_secs_f64(secs))),
293
- None => block_on(self.inner.stop()),
294
- }
331
+ /// Graceful stop. Mirrors the official SDKs: the live handle routes through
332
+ /// a freshly fetched `SandboxHandle::stop` (SIGTERM→SIGKILL escalation with
333
+ /// a 10s default). Fine-grained control — a custom timeout or fire-and-
334
+ /// return `request_*` — lives on `SandboxHandle`, obtained via `Sandbox.get`.
335
+ fn stop(&self) -> Result<(), Error> {
336
+ let name = self.inner.name().to_string();
337
+ block_on(async move {
338
+ let handle = microsandbox::sandbox::Sandbox::get(&name).await?;
339
+ handle.stop().await
340
+ })
295
341
  .map_err(error::to_ruby)
296
342
  }
297
343
 
298
- /// Force kill (SIGKILL). `timeout` is optional seconds.
299
- fn kill(&self, timeout: Option<f64>) -> Result<(), Error> {
300
- match timeout {
301
- Some(secs) => block_on(self.inner.kill_with_timeout(Duration::from_secs_f64(secs))),
302
- None => block_on(self.inner.kill()),
303
- }
304
- .map_err(error::to_ruby)
344
+ /// Graceful stop, then wait for the process to exit. Returns an exit-status
345
+ /// Hash (`exit_code`, `success`). Local backend only.
346
+ fn stop_and_wait(&self) -> Result<RHash, Error> {
347
+ let status = block_on(self.inner.stop_and_wait()).map_err(error::to_ruby)?;
348
+ Ok(exit_status_to_hash(status))
305
349
  }
306
350
 
307
- /// Send the graceful-shutdown request and return without waiting.
308
- fn request_stop(&self) -> Result<(), Error> {
309
- block_on(self.inner.request_stop()).map_err(error::to_ruby)
351
+ /// Force kill (SIGKILL).
352
+ fn kill(&self) -> Result<(), Error> {
353
+ block_on(self.inner.kill()).map_err(error::to_ruby)
310
354
  }
311
355
 
312
- /// Send the force-kill request and return without waiting.
313
- fn request_kill(&self) -> Result<(), Error> {
314
- block_on(self.inner.request_kill()).map_err(error::to_ruby)
356
+ /// Trigger a graceful drain (SIGUSR1 on local).
357
+ fn drain(&self) -> Result<(), Error> {
358
+ block_on(self.inner.drain()).map_err(error::to_ruby)
315
359
  }
316
360
 
317
- /// Send the drain request and return without waiting.
318
- fn request_drain(&self) -> Result<(), Error> {
319
- block_on(self.inner.request_drain()).map_err(error::to_ruby)
361
+ /// Wait for the process to exit. Returns an exit-status Hash. Local only.
362
+ fn wait(&self) -> Result<RHash, Error> {
363
+ let status = block_on(self.inner.wait()).map_err(error::to_ruby)?;
364
+ Ok(exit_status_to_hash(status))
320
365
  }
321
366
 
322
- /// Block until the sandbox is observed in a terminal state; returns a
323
- /// stop-result Hash (name, status, exit_code, signal, observed_at_ms, source).
324
- fn wait_until_stopped(&self) -> Result<RHash, Error> {
325
- let result = block_on(self.inner.wait_until_stopped()).map_err(error::to_ruby)?;
326
- Ok(stop_result_to_hash(&result))
367
+ /// Live status fetched from the backend (a round-trip per call).
368
+ fn status(&self) -> Result<String, Error> {
369
+ let status = block_on(self.inner.status()).map_err(error::to_ruby)?;
370
+ Ok(sandbox_status_str(status).to_string())
327
371
  }
328
372
 
329
373
  /// Whether this handle owns the sandbox process lifecycle (a synchronous,
@@ -1064,6 +1108,7 @@ struct ExecOpts {
1064
1108
  timeout: Option<Duration>,
1065
1109
  tty: bool,
1066
1110
  stdin: Option<Vec<u8>>,
1111
+ stdin_pipe: bool,
1067
1112
  rlimits: Vec<(RlimitResource, u64, u64)>,
1068
1113
  }
1069
1114
 
@@ -1078,6 +1123,7 @@ impl ExecOpts {
1078
1123
  timeout: conv::opt_f64(opts, "timeout")?.map(Duration::from_secs_f64),
1079
1124
  tty: conv::opt_bool(opts, "tty")?,
1080
1125
  stdin,
1126
+ stdin_pipe: conv::opt_bool(opts, "stdin_pipe")?,
1081
1127
  rlimits: parse_rlimits(opts)?,
1082
1128
  })
1083
1129
  }
@@ -1104,7 +1150,13 @@ impl ExecOpts {
1104
1150
  if self.tty {
1105
1151
  b = b.tty(true);
1106
1152
  }
1107
- if let Some(stdin) = self.stdin {
1153
+ // Pipe mode opens a writable stdin sink (lifted out by ExecHandle via
1154
+ // `take_stdin`); bytes mode feeds a fixed buffer and closes. The core's
1155
+ // `StdinMode` is a single enum, so the two are mutually exclusive — pipe
1156
+ // wins if a caller somehow sets both.
1157
+ if self.stdin_pipe {
1158
+ b = b.stdin_pipe();
1159
+ } else if let Some(stdin) = self.stdin {
1108
1160
  b = b.stdin_bytes(stdin);
1109
1161
  }
1110
1162
  for (resource, soft, hard) in self.rlimits {
@@ -1195,7 +1247,13 @@ pub(crate) fn metrics_to_hash(m: &SandboxMetrics) -> RHash {
1195
1247
  }
1196
1248
 
1197
1249
  fn sandbox_status_str(status: SandboxStatus) -> &'static str {
1250
+ // Lowercased `Debug` names, matching the official SDKs' `format!("{:?}")`.
1251
+ // `Created`/`Starting` are new in v0.5.8 (cloud-only today). The match is
1252
+ // intentionally exhaustive — no wildcard — so a future upstream variant
1253
+ // surfaces as a compile error rather than a silent fallback.
1198
1254
  match status {
1255
+ SandboxStatus::Created => "created",
1256
+ SandboxStatus::Starting => "starting",
1199
1257
  SandboxStatus::Running => "running",
1200
1258
  SandboxStatus::Draining => "draining",
1201
1259
  SandboxStatus::Paused => "paused",
@@ -1204,6 +1262,15 @@ fn sandbox_status_str(status: SandboxStatus) -> &'static str {
1204
1262
  }
1205
1263
  }
1206
1264
 
1265
+ /// A `std::process::ExitStatus` as a Ruby Hash: `exit_code` (Integer or nil) and
1266
+ /// `success` (Boolean). Returned by the live `Sandbox#wait` / `#stop_and_wait`.
1267
+ fn exit_status_to_hash(status: std::process::ExitStatus) -> RHash {
1268
+ let hash = ruby().hash_new();
1269
+ let _ = hash.aset("exit_code", status.code());
1270
+ let _ = hash.aset("success", status.success());
1271
+ hash
1272
+ }
1273
+
1207
1274
  fn stop_result_to_hash(result: &SandboxStopResult) -> RHash {
1208
1275
  let hash = ruby().hash_new();
1209
1276
  let _ = hash.aset("name", result.name.clone());
@@ -1215,19 +1282,85 @@ fn stop_result_to_hash(result: &SandboxStopResult) -> RHash {
1215
1282
  hash
1216
1283
  }
1217
1284
 
1218
- fn handle_to_hash(handle: &SandboxHandle) -> RHash {
1219
- let hash = ruby().hash_new();
1220
- let _ = hash.aset("name", handle.name().to_string());
1221
- let _ = hash.aset("status", sandbox_status_str(handle.status()));
1222
- let _ = hash.aset(
1223
- "created_at_ms",
1224
- handle.created_at().map(|dt| dt.timestamp_millis()),
1225
- );
1226
- let _ = hash.aset(
1227
- "updated_at_ms",
1228
- handle.updated_at().map(|dt| dt.timestamp_millis()),
1229
- );
1230
- hash
1285
+ //--------------------------------------------------------------------------------------------------
1286
+ // SandboxHandle the controllable lightweight handle
1287
+ //--------------------------------------------------------------------------------------------------
1288
+
1289
+ /// Wraps a core `microsandbox::sandbox::SandboxHandle` (returned by
1290
+ /// `Sandbox.get`/`list`/`list_with`). Carries metadata accessors plus the rich
1291
+ /// lifecycle surface that moved off the live `Sandbox` in v0.5.8 — mirroring the
1292
+ /// official Python (`PySandboxHandle`) and Node (`SandboxHandle`) SDKs. Status is
1293
+ /// a synchronous snapshot read off the handle (no round-trip).
1294
+ #[magnus::wrap(class = "Microsandbox::Native::SandboxHandle", free_immediately, size)]
1295
+ pub struct SbHandle {
1296
+ inner: SandboxHandle,
1297
+ }
1298
+
1299
+ impl SbHandle {
1300
+ fn from_inner(inner: SandboxHandle) -> Self {
1301
+ Self { inner }
1302
+ }
1303
+
1304
+ fn name(&self) -> String {
1305
+ self.inner.name().to_string()
1306
+ }
1307
+
1308
+ /// Status snapshot captured when the handle was fetched (synchronous).
1309
+ fn status(&self) -> String {
1310
+ sandbox_status_str(self.inner.status_snapshot()).to_string()
1311
+ }
1312
+
1313
+ fn created_at_ms(&self) -> Option<i64> {
1314
+ self.inner.created_at().map(|dt| dt.timestamp_millis())
1315
+ }
1316
+
1317
+ fn updated_at_ms(&self) -> Option<i64> {
1318
+ self.inner.updated_at().map(|dt| dt.timestamp_millis())
1319
+ }
1320
+
1321
+ /// Graceful stop (SIGTERM→SIGKILL escalation, 10s default) and wait.
1322
+ fn stop(&self) -> Result<(), Error> {
1323
+ block_on(self.inner.stop()).map_err(error::to_ruby)
1324
+ }
1325
+
1326
+ /// Graceful stop with a custom escalation timeout (seconds).
1327
+ fn stop_with_timeout(&self, secs: f64) -> Result<(), Error> {
1328
+ block_on(self.inner.stop_with_timeout(Duration::from_secs_f64(secs)))
1329
+ .map_err(error::to_ruby)
1330
+ }
1331
+
1332
+ /// Force kill (SIGKILL) and wait.
1333
+ fn kill(&self) -> Result<(), Error> {
1334
+ block_on(self.inner.kill()).map_err(error::to_ruby)
1335
+ }
1336
+
1337
+ /// Force kill, waiting up to `secs` for the process to disappear.
1338
+ fn kill_with_timeout(&self, secs: f64) -> Result<(), Error> {
1339
+ block_on(self.inner.kill_with_timeout(Duration::from_secs_f64(secs)))
1340
+ .map_err(error::to_ruby)
1341
+ }
1342
+
1343
+ /// Send the graceful-shutdown request and return without waiting.
1344
+ fn request_stop(&self) -> Result<(), Error> {
1345
+ block_on(self.inner.request_stop()).map_err(error::to_ruby)
1346
+ }
1347
+
1348
+ /// Send the force-kill request and return without waiting.
1349
+ fn request_kill(&self) -> Result<(), Error> {
1350
+ block_on(self.inner.request_kill()).map_err(error::to_ruby)
1351
+ }
1352
+
1353
+ /// Send the drain request (SIGUSR1) and return without waiting.
1354
+ fn request_drain(&self) -> Result<(), Error> {
1355
+ block_on(self.inner.request_drain()).map_err(error::to_ruby)
1356
+ }
1357
+
1358
+ /// Block until the sandbox reaches a terminal state; returns a stop-result
1359
+ /// Hash (name, status, exit_code, signal, observed_at_ms, source).
1360
+ fn wait_until_stopped(&self) -> Result<RHash, Error> {
1361
+ let result = block_on(self.inner.wait_until_stopped()).map_err(error::to_ruby)?;
1362
+ Ok(stop_result_to_hash(&result))
1363
+ }
1231
1364
  }
1232
1365
 
1233
1366
  //--------------------------------------------------------------------------------------------------
@@ -1328,15 +1461,12 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
1328
1461
  class.define_method("shell", method!(Sandbox::shell, 2))?;
1329
1462
  class.define_method("exec_stream", method!(Sandbox::exec_stream, 3))?;
1330
1463
  class.define_method("shell_stream", method!(Sandbox::shell_stream, 2))?;
1331
- class.define_method("stop", method!(Sandbox::stop, 1))?;
1332
- class.define_method("kill", method!(Sandbox::kill, 1))?;
1333
- class.define_method("request_stop", method!(Sandbox::request_stop, 0))?;
1334
- class.define_method("request_kill", method!(Sandbox::request_kill, 0))?;
1335
- class.define_method("request_drain", method!(Sandbox::request_drain, 0))?;
1336
- class.define_method(
1337
- "wait_until_stopped",
1338
- method!(Sandbox::wait_until_stopped, 0),
1339
- )?;
1464
+ class.define_method("stop", method!(Sandbox::stop, 0))?;
1465
+ class.define_method("stop_and_wait", method!(Sandbox::stop_and_wait, 0))?;
1466
+ class.define_method("kill", method!(Sandbox::kill, 0))?;
1467
+ class.define_method("drain", method!(Sandbox::drain, 0))?;
1468
+ class.define_method("wait", method!(Sandbox::wait, 0))?;
1469
+ class.define_method("status", method!(Sandbox::status, 0))?;
1340
1470
  class.define_method("owns_lifecycle", method!(Sandbox::owns_lifecycle, 0))?;
1341
1471
  class.define_method("detach", method!(Sandbox::detach, 0))?;
1342
1472
  class.define_method("metrics", method!(Sandbox::metrics, 0))?;
@@ -1367,5 +1497,22 @@ pub fn define(ruby: &Ruby, native: &RModule) -> Result<(), Error> {
1367
1497
  class.define_method("attach", method!(Sandbox::attach, 3))?;
1368
1498
  class.define_method("attach_shell", method!(Sandbox::attach_shell, 0))?;
1369
1499
 
1500
+ let handle = native.define_class("SandboxHandle", ruby.class_object())?;
1501
+ handle.define_method("name", method!(SbHandle::name, 0))?;
1502
+ handle.define_method("status", method!(SbHandle::status, 0))?;
1503
+ handle.define_method("created_at_ms", method!(SbHandle::created_at_ms, 0))?;
1504
+ handle.define_method("updated_at_ms", method!(SbHandle::updated_at_ms, 0))?;
1505
+ handle.define_method("stop", method!(SbHandle::stop, 0))?;
1506
+ handle.define_method("stop_with_timeout", method!(SbHandle::stop_with_timeout, 1))?;
1507
+ handle.define_method("kill", method!(SbHandle::kill, 0))?;
1508
+ handle.define_method("kill_with_timeout", method!(SbHandle::kill_with_timeout, 1))?;
1509
+ handle.define_method("request_stop", method!(SbHandle::request_stop, 0))?;
1510
+ handle.define_method("request_kill", method!(SbHandle::request_kill, 0))?;
1511
+ handle.define_method("request_drain", method!(SbHandle::request_drain, 0))?;
1512
+ handle.define_method(
1513
+ "wait_until_stopped",
1514
+ method!(SbHandle::wait_until_stopped, 0),
1515
+ )?;
1516
+
1370
1517
  Ok(())
1371
1518
  }
@@ -65,7 +65,12 @@ fn create(name: String, opts: RHash) -> Result<RHash, Error> {
65
65
  let vol = block_on(builder.create()).map_err(error::to_ruby)?;
66
66
  let hash = ruby().hash_new();
67
67
  hash.aset("name", vol.name().to_string())?;
68
- hash.aset("path", vol.path().display().to_string())?;
68
+ // `path()` is fallible as of v0.5.8 (returns `Unsupported` for cloud
69
+ // volumes); the volume just created here is local, so this resolves.
70
+ hash.aset(
71
+ "path",
72
+ vol.path().map_err(error::to_ruby)?.display().to_string(),
73
+ )?;
69
74
  Ok(hash)
70
75
  }
71
76
 
@@ -65,4 +65,10 @@ module Microsandbox
65
65
 
66
66
  # Runtime compatibility ---------------------------------------------------
67
67
  define_error(:UnsupportedOperationError, "unsupported-operation")
68
+
69
+ # Cloud / backend routing errors (v0.5.8) ---------------------------------
70
+ # `UnsupportedError` (a backend feature gap, e.g. an op not yet wired on the
71
+ # cloud backend) is distinct from `UnsupportedOperationError` above.
72
+ define_error(:CloudHttpError, "cloud-http")
73
+ define_error(:UnsupportedError, "unsupported")
68
74
  end
@@ -22,25 +22,28 @@ module Microsandbox
22
22
 
23
23
  # @return [String, nil] {#data} decoded as UTF-8 (lenient)
24
24
  def text
25
- @data && @data.dup.force_encoding(Encoding::UTF_8)
25
+ @data&.dup&.force_encoding(Encoding::UTF_8)
26
26
  end
27
27
 
28
- def started? = @type == :started
29
- def stdout? = @type == :stdout
30
- def stderr? = @type == :stderr
31
- def exited? = @type == :exited
32
- def failed? = @type == :failed
33
- def stdin_error? = @type == :stdin_error
28
+ def started? = @type == :started
29
+ def stdout? = @type == :stdout
30
+ def stderr? = @type == :stderr
31
+ def exited? = @type == :exited
32
+ def failed? = @type == :failed
33
+ def stdin_error? = @type == :stdin_error
34
34
 
35
35
  def inspect
36
- "#<Microsandbox::ExecEvent type=#{@type}#{@pid ? " pid=#{@pid}" : ""}" \
37
- "#{@code ? " code=#{@code}" : ""}#{@data ? " data=#{@data.bytesize}B" : ""}>"
36
+ "#<Microsandbox::ExecEvent type=#{@type}#{" pid=#{@pid}" if @pid}" \
37
+ "#{" code=#{@code}" if @code}#{" data=#{@data.bytesize}B" if @data}>"
38
38
  end
39
39
  end
40
40
 
41
- # The terminal status of a streamed execution, from {ExecHandle#wait}.
41
+ # The terminal status of a streamed execution ({ExecHandle#wait}) or of a
42
+ # sandbox process ({Sandbox#wait}, {Sandbox#stop_and_wait}).
42
43
  class ExitStatus
43
- # @return [Integer]
44
+ # @return [Integer, nil] the exit code, or nil when the process was
45
+ # terminated by a signal and so carries no code (e.g. a SIGKILL'd sandbox
46
+ # from {Sandbox#kill} then {Sandbox#wait}) — check {#success?} in that case.
44
47
  attr_reader :exit_code
45
48
 
46
49
  def initialize(data)
@@ -30,9 +30,9 @@ module Microsandbox
30
30
  @modified_ms && Time.at(@modified_ms / 1000.0)
31
31
  end
32
32
 
33
- def file? = @type == :file
34
- def directory? = @type == :directory
35
- def symlink? = @type == :symlink
33
+ def file? = @type == :file
34
+ def directory? = @type == :directory
35
+ def symlink? = @type == :symlink
36
36
 
37
37
  def inspect
38
38
  "#<Microsandbox::FsEntry path=#{@path.inspect} type=#{@type} size=#{@size}>"
@@ -57,10 +57,10 @@ module Microsandbox
57
57
  @created_ms = data["created_ms"]
58
58
  end
59
59
 
60
- def readonly? = @readonly
61
- def file? = @type == :file
62
- def directory? = @type == :directory
63
- def symlink? = @type == :symlink
60
+ def readonly? = @readonly
61
+ def file? = @type == :file
62
+ def directory? = @type == :directory
63
+ def symlink? = @type == :symlink
64
64
 
65
65
  # @return [Time, nil] last-modified time, if known
66
66
  def modified
@@ -56,7 +56,7 @@ module Microsandbox
56
56
  # The result of {Image.prune}.
57
57
  class ImagePruneReport
58
58
  attr_reader :image_refs_removed, :manifests_removed, :layers_removed,
59
- :fsmeta_removed, :vmdk_removed, :bytes_reclaimed
59
+ :fsmeta_removed, :vmdk_removed, :bytes_reclaimed
60
60
 
61
61
  def initialize(data)
62
62
  @image_refs_removed = data["image_refs_removed"]
@@ -16,23 +16,23 @@ module Microsandbox
16
16
  module_function
17
17
 
18
18
  # Match any destination.
19
- def any = { "destination_kind" => "any" }
19
+ def any = {"destination_kind" => "any"}
20
20
 
21
21
  # A single IP address (stored as a /32 or /128).
22
- def ip(value) = { "destination_kind" => "ip", "destination" => value.to_s }
22
+ def ip(value) = {"destination_kind" => "ip", "destination" => value.to_s}
23
23
 
24
24
  # An IP network in CIDR notation (e.g. "10.0.0.0/8").
25
- def cidr(value) = { "destination_kind" => "cidr", "destination" => value.to_s }
25
+ def cidr(value) = {"destination_kind" => "cidr", "destination" => value.to_s}
26
26
 
27
27
  # An exact domain name (matched against the resolved-hostname cache / SNI).
28
- def domain(value) = { "destination_kind" => "domain", "destination" => value.to_s }
28
+ def domain(value) = {"destination_kind" => "domain", "destination" => value.to_s}
29
29
 
30
30
  # A domain suffix — matches the apex and any subdomain (e.g. ".internal").
31
- def domain_suffix(value) = { "destination_kind" => "domain_suffix", "destination" => value.to_s }
31
+ def domain_suffix(value) = {"destination_kind" => "domain_suffix", "destination" => value.to_s}
32
32
 
33
33
  # A predefined group: :public, :loopback, :private, :link_local, :metadata,
34
34
  # :multicast, or :host.
35
- def group(value) = { "destination_kind" => "group", "destination" => value.to_s.tr("_", "-") }
35
+ def group(value) = {"destination_kind" => "group", "destination" => value.to_s.tr("_", "-")}
36
36
  end
37
37
 
38
38
  # Factory for a single network-policy **rule**. A rule pairs an action
@@ -64,7 +64,7 @@ module Microsandbox
64
64
 
65
65
  # @api private
66
66
  def build(action, destination, direction, protocol, protocols, port, ports)
67
- rule = { "action" => action, "direction" => direction.to_s }
67
+ rule = {"action" => action, "direction" => direction.to_s}
68
68
  rule.merge!(normalize_destination(destination))
69
69
  protos = (Array(protocols) + Array(protocol)).compact.map(&:to_s)
70
70
  rule["protocols"] = protos unless protos.empty?
@@ -78,7 +78,7 @@ module Microsandbox
78
78
  case dest
79
79
  when nil then {}
80
80
  when Hash then dest.each_with_object({}) { |(k, v), a| a[k.to_s] = v }
81
- when String, Symbol then { "destination" => dest.to_s }
81
+ when String, Symbol then {"destination" => dest.to_s}
82
82
  else raise ArgumentError, "invalid rule destination: #{dest.inspect}"
83
83
  end
84
84
  end
@@ -149,7 +149,7 @@ module Microsandbox
149
149
  # @param deny_domain_suffixes [Array<String>] domain suffixes to deny
150
150
  # @return [NetworkPolicy]
151
151
  def custom(default_egress: :deny, default_ingress: :allow, rules: [],
152
- deny_domains: [], deny_domain_suffixes: [])
152
+ deny_domains: [], deny_domain_suffixes: [])
153
153
  h = {}
154
154
  h["default_egress"] = action_str(default_egress) unless default_egress.nil?
155
155
  h["default_ingress"] = action_str(default_ingress) unless default_ingress.nil?
@@ -163,12 +163,12 @@ module Microsandbox
163
163
  def coerce(network)
164
164
  case network
165
165
  when NetworkPolicy then network.to_h
166
- when String, Symbol then { "preset" => canonical_preset(network) }
166
+ when String, Symbol then {"preset" => canonical_preset(network)}
167
167
  when Hash then from_hash(network)
168
168
  else
169
169
  raise ArgumentError,
170
- "network: expects a preset name, a Microsandbox::NetworkPolicy, or a Hash " \
171
- "(got #{network.class})"
170
+ "network: expects a preset name, a Microsandbox::NetworkPolicy, or a Hash " \
171
+ "(got #{network.class})"
172
172
  end
173
173
  end
174
174
 
@@ -186,11 +186,11 @@ module Microsandbox
186
186
  if sym.key?(:preset)
187
187
  if sym.key?(:rules) || sym.key?(:default_egress) || sym.key?(:default_ingress)
188
188
  raise ArgumentError,
189
- "network preset: cannot be combined with rules:/default_egress:/" \
190
- "default_ingress: (the preset already defines its rules and defaults); " \
191
- "only deny_domains:/deny_domain_suffixes: may be layered on a preset"
189
+ "network preset: cannot be combined with rules:/default_egress:/" \
190
+ "default_ingress: (the preset already defines its rules and defaults); " \
191
+ "only deny_domains:/deny_domain_suffixes: may be layered on a preset"
192
192
  end
193
- h = { "preset" => canonical_preset(sym[:preset]) }
193
+ h = {"preset" => canonical_preset(sym[:preset])}
194
194
  add_deny_lists(h, sym[:deny_domains], sym[:deny_domain_suffixes])
195
195
  h
196
196
  elsif sym.key?(:rules) || sym.key?(:default_egress) || sym.key?(:default_ingress)
@@ -213,7 +213,7 @@ module Microsandbox
213
213
  ds = Array(sym[:deny_domain_suffixes]).map(&:to_s)
214
214
  return {} if dd.empty? && ds.empty?
215
215
 
216
- h = { "default_egress" => "allow", "default_ingress" => "allow" }
216
+ h = {"default_egress" => "allow", "default_ingress" => "allow"}
217
217
  add_deny_lists(h, dd, ds)
218
218
  h
219
219
  end
@@ -233,8 +233,8 @@ module Microsandbox
233
233
  key = name.to_s.downcase
234
234
  PRESET_ALIASES[key] ||
235
235
  raise(ArgumentError,
236
- "unknown network preset #{name.inspect} " \
237
- "(expected one of public_only/none/allow_all/non_local)")
236
+ "unknown network preset #{name.inspect} " \
237
+ "(expected one of public_only/none/allow_all/non_local)")
238
238
  end
239
239
 
240
240
  def action_str(action)