pharos_native/platform/mod.rs
1#[cfg(feature = "wasi-host-bridge")]
2use std::collections::HashSet;
3#[cfg(feature = "wasi-host-bridge")]
4use std::env;
5#[cfg(feature = "wasi-host-bridge")]
6use std::io::{self, Read};
7
8use std::path::Path;
9#[cfg(feature = "wasi-host-bridge")]
10use std::process::{Child, Command, Stdio};
11#[cfg(feature = "wasi-host-bridge")]
12use std::sync::{
13 Arc,
14 atomic::{AtomicBool, Ordering},
15};
16#[cfg(feature = "wasi-host-bridge")]
17use std::thread;
18#[cfg(feature = "wasi-host-bridge")]
19use std::thread::JoinHandle;
20#[cfg(feature = "wasi-host-bridge")]
21use std::time::{Duration, Instant};
22
23/// Public constant `MODULE_STATUS` exposed by this module.
24///
25/// # Examples
26///
27/// ```rust,ignore
28/// let _ = pharos_native::platform::MODULE_STATUS;
29/// ```
30///
31/// # Related
32///
33/// - [`crate::platform`] for the surrounding module surface.
34pub const MODULE_STATUS: &str = "PENDING";
35
36#[derive(Debug, Clone, Copy, Eq, PartialEq)]
37/// Public enum `HostTarget` used by this module.
38///
39/// # Variants
40///
41/// - `MacosDarwinX86_64`: Supported `HostTarget` variant.
42/// - `Wasm32Wasip1`: Supported `HostTarget` variant.
43/// - `Wasm32UnknownUnknown`: Supported `HostTarget` variant.
44///
45/// # Examples
46///
47/// ```rust,ignore
48/// let _ = pharos_native::platform::HostTarget::MacosDarwinX86_64;
49/// ```
50///
51/// # Related
52///
53/// - [`crate::platform`] for constructors, helpers, and neighboring runtime types.
54pub enum HostTarget {
55 MacosDarwinX86_64,
56 Wasm32Wasip1,
57 Wasm32UnknownUnknown,
58}
59
60#[derive(Debug, Clone, Copy, Eq, PartialEq)]
61/// Public struct `TargetConstraint` used by this module.
62///
63/// # Fields
64///
65/// - `product_target`: Stored field on `TargetConstraint`.
66/// - `rust_target`: Stored field on `TargetConstraint`.
67/// - `constraints`: Stored field on `TargetConstraint`.
68///
69/// # Examples
70///
71/// ```rust,ignore
72/// let _ = std::mem::size_of::<pharos_native::platform::TargetConstraint>();
73/// ```
74///
75/// # Related
76///
77/// - [`crate::platform`] for constructors, helpers, and neighboring runtime types.
78pub struct TargetConstraint {
79 pub product_target: &'static str,
80 pub rust_target: HostTarget,
81 pub constraints: &'static [&'static str],
82}
83
84const MACOS_CONSTRAINTS: &[&str] = &[
85 "authoritative local target for Phase 1 first build, lockfile generation, and SBOM generation",
86 "native runtime, PostgreSQL rustls TLS, SMTP rustls TLS, and SQLite feasibility remain enabled",
87];
88
89const WASI_P1_CONSTRAINTS: &[&str] = &[
90 "uses the wasm32-wasip1 target",
91 "must fail closed for raw sockets, process spawning, SMTP, PostgreSQL, and unsupported async filesystem behavior",
92];
93
94const WASM_WEB_CONSTRAINTS: &[&str] = &[
95 "reserved for the host-mediated wasm entrypoint target",
96 "no networking, process, SMTP, or PostgreSQL capability may be compiled into this target",
97];
98
99/// Public constant `TARGET_CONSTRAINTS` exposed by this module.
100///
101/// # Examples
102///
103/// ```rust,ignore
104/// let _ = pharos_native::platform::TARGET_CONSTRAINTS;
105/// ```
106///
107/// # Related
108///
109/// - [`crate::platform`] for the surrounding module surface.
110pub const TARGET_CONSTRAINTS: &[TargetConstraint] = &[
111 TargetConstraint {
112 product_target: "native host runtime",
113 rust_target: HostTarget::MacosDarwinX86_64,
114 constraints: MACOS_CONSTRAINTS,
115 },
116 TargetConstraint {
117 product_target: "wasi guest",
118 rust_target: HostTarget::Wasm32Wasip1,
119 constraints: WASI_P1_CONSTRAINTS,
120 },
121 TargetConstraint {
122 product_target: "wasm entrypoint",
123 rust_target: HostTarget::Wasm32UnknownUnknown,
124 constraints: WASM_WEB_CONSTRAINTS,
125 },
126];
127
128/// Performs target constraints.
129///
130/// # Returns
131///
132/// Returns the computed value produced by [`crate::platform::target_constraints`].
133///
134/// # Examples
135///
136/// ```rust,ignore
137/// let _result = pharos_native::platform::target_constraints();
138/// ```
139///
140/// # Related
141///
142/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
143pub fn target_constraints() -> &'static [TargetConstraint] {
144 TARGET_CONSTRAINTS
145}
146
147#[allow(non_snake_case)]
148/// Performs acrt initialize.
149///
150/// # Returns
151///
152/// Returns the computed value produced by [`crate::platform::__acrt_initialize`].
153///
154/// # Examples
155///
156/// ```rust,ignore
157/// let _result = pharos_native::platform::__acrt_initialize();
158/// ```
159///
160/// # Related
161///
162/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
163pub fn __acrt_initialize() -> i32 {
164 1
165}
166
167#[allow(non_snake_case)]
168/// Performs acrt uninitialize.
169///
170/// # Parameters
171///
172/// - `_terminating`: Input consumed by [`crate::platform::__acrt_uninitialize`].
173///
174/// # Returns
175///
176/// Returns the computed value produced by [`crate::platform::__acrt_uninitialize`].
177///
178/// # Examples
179///
180/// ```rust,ignore
181/// let _result = pharos_native::platform::__acrt_uninitialize(todo!());
182/// ```
183///
184/// # Related
185///
186/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
187pub fn __acrt_uninitialize(_terminating: bool) -> i32 {
188 1
189}
190
191#[allow(non_snake_case)]
192/// Performs acrt uninitialize critical.
193///
194/// # Parameters
195///
196/// - `_terminating`: Input consumed by [`crate::platform::__acrt_uninitialize_critical`].
197///
198/// # Returns
199///
200/// Returns the computed value produced by [`crate::platform::__acrt_uninitialize_critical`].
201///
202/// # Examples
203///
204/// ```rust,ignore
205/// let _result = pharos_native::platform::__acrt_uninitialize_critical(todo!());
206/// ```
207///
208/// # Related
209///
210/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
211pub fn __acrt_uninitialize_critical(_terminating: bool) -> i32 {
212 1
213}
214
215#[allow(non_snake_case)]
216/// Performs acrt thread attach.
217///
218/// # Returns
219///
220/// Returns the computed value produced by [`crate::platform::__acrt_thread_attach`].
221///
222/// # Examples
223///
224/// ```rust,ignore
225/// let _result = pharos_native::platform::__acrt_thread_attach();
226/// ```
227///
228/// # Related
229///
230/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
231pub fn __acrt_thread_attach() -> i32 {
232 1
233}
234
235#[allow(non_snake_case)]
236/// Performs acrt thread detach.
237///
238/// # Returns
239///
240/// Returns the computed value produced by [`crate::platform::__acrt_thread_detach`].
241///
242/// # Examples
243///
244/// ```rust,ignore
245/// let _result = pharos_native::platform::__acrt_thread_detach();
246/// ```
247///
248/// # Related
249///
250/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
251pub fn __acrt_thread_detach() -> i32 {
252 1
253}
254
255/// Performs is c termination complete.
256///
257/// # Returns
258///
259/// Returns the computed value produced by [`crate::platform::_is_c_termination_complete`].
260///
261/// # Examples
262///
263/// ```rust,ignore
264/// let _result = pharos_native::platform::_is_c_termination_complete();
265/// ```
266///
267/// # Related
268///
269/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
270pub fn _is_c_termination_complete() -> i32 {
271 0
272}
273
274#[allow(non_snake_case)]
275/// Performs vcrt uninitialize critical.
276///
277/// # Returns
278///
279/// Returns the computed value produced by [`crate::platform::__vcrt_uninitialize_critical`].
280///
281/// # Examples
282///
283/// ```rust,ignore
284/// let _result = pharos_native::platform::__vcrt_uninitialize_critical();
285/// ```
286///
287/// # Related
288///
289/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
290pub fn __vcrt_uninitialize_critical() -> i32 {
291 1
292}
293
294#[allow(non_snake_case)]
295/// Performs vcrt initialize.
296///
297/// # Returns
298///
299/// Returns the computed value produced by [`crate::platform::__vcrt_initialize`].
300///
301/// # Examples
302///
303/// ```rust,ignore
304/// let _result = pharos_native::platform::__vcrt_initialize();
305/// ```
306///
307/// # Related
308///
309/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
310pub fn __vcrt_initialize() -> i32 {
311 1
312}
313
314#[allow(non_snake_case)]
315/// Performs vcrt uninitialize.
316///
317/// # Parameters
318///
319/// - `_terminating`: Input consumed by [`crate::platform::__vcrt_uninitialize`].
320///
321/// # Returns
322///
323/// Returns the computed value produced by [`crate::platform::__vcrt_uninitialize`].
324///
325/// # Examples
326///
327/// ```rust,ignore
328/// let _result = pharos_native::platform::__vcrt_uninitialize(todo!());
329/// ```
330///
331/// # Related
332///
333/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
334pub fn __vcrt_uninitialize(_terminating: bool) -> i32 {
335 1
336}
337
338#[allow(non_snake_case)]
339/// Performs vcrt thread attach.
340///
341/// # Returns
342///
343/// Returns the computed value produced by [`crate::platform::__vcrt_thread_attach`].
344///
345/// # Examples
346///
347/// ```rust,ignore
348/// let _result = pharos_native::platform::__vcrt_thread_attach();
349/// ```
350///
351/// # Related
352///
353/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
354pub fn __vcrt_thread_attach() -> i32 {
355 1
356}
357
358#[allow(non_snake_case)]
359/// Performs vcrt thread detach.
360///
361/// # Returns
362///
363/// Returns the computed value produced by [`crate::platform::__vcrt_thread_detach`].
364///
365/// # Examples
366///
367/// ```rust,ignore
368/// let _result = pharos_native::platform::__vcrt_thread_detach();
369/// ```
370///
371/// # Related
372///
373/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
374pub fn __vcrt_thread_detach() -> i32 {
375 1
376}
377
378#[cfg(feature = "wasi-host-bridge")]
379const MAX_CAPTURE_OUTPUT_BYTES: usize = 8 * 1024 * 1024;
380#[cfg(feature = "wasi-host-bridge")]
381const CAPTURE_READ_CHUNK_BYTES: usize = 4096;
382#[cfg(feature = "wasi-host-bridge")]
383const MAX_CAPTURE_WALL_CLOCK: Duration = Duration::from_secs(30);
384#[cfg(feature = "wasi-host-bridge")]
385/// Public constant `CHILD_PROCESS_TIMEOUT_CODE` exposed by this module.
386///
387/// # Examples
388///
389/// ```rust,ignore
390/// let _ = pharos_native::platform::CHILD_PROCESS_TIMEOUT_CODE;
391/// ```
392///
393/// # Related
394///
395/// - [`crate::platform`] for the surrounding module surface.
396pub const CHILD_PROCESS_TIMEOUT_CODE: i32 = 70;
397#[cfg(feature = "wasi-host-bridge")]
398const CAPTURE_POLL_INTERVAL: Duration = Duration::from_millis(5);
399#[cfg(feature = "wasi-host-bridge")]
400const MAX_CAPTURE_ARGUMENTS: usize = 1024;
401#[cfg(feature = "wasi-host-bridge")]
402const MAX_CAPTURE_ARGUMENT_BYTES: usize = 64 * 1024;
403#[cfg(feature = "wasi-host-bridge")]
404const MAX_ENV_COMPONENT_BYTES: usize = 64 * 1024;
405
406#[cfg(feature = "wasi-host-bridge")]
407/// Reads one child-process pipe chunk into a bounded output buffer.
408///
409/// # Parameters
410///
411/// - `reader`: Reader supplying child-process output bytes.
412/// - `buffer`: Input consumed by [`crate::platform::append_pipe_to_buffer`].
413/// - `open`: Input consumed by [`crate::platform::append_pipe_to_buffer`].
414///
415/// # Returns
416///
417/// Returns the computed value produced by [`crate::platform::append_pipe_to_buffer`].
418///
419/// # Examples
420///
421/// ```rust,ignore
422/// let _result = pharos_native::platform::append_pipe_to_buffer(todo!(), &mut [], todo!(), todo!());
423/// ```
424///
425/// # Related
426///
427/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
428pub(crate) fn append_pipe_to_buffer<R: Read>(
429 reader: &mut R,
430 buffer: &mut Vec<u8>,
431 open: &mut bool,
432) -> io::Result<()> {
433 if !*open {
434 return Ok(());
435 }
436
437 let remaining = MAX_CAPTURE_OUTPUT_BYTES.saturating_sub(buffer.len());
438 let read_limit = remaining.saturating_add(1).min(CAPTURE_READ_CHUNK_BYTES);
439 let mut chunk = [0_u8; CAPTURE_READ_CHUNK_BYTES];
440 let bytes_read = reader.read(&mut chunk[..read_limit])?;
441 if bytes_read == 0 {
442 *open = false;
443 return Ok(());
444 }
445 if bytes_read > remaining {
446 return Err(io::Error::new(
447 io::ErrorKind::InvalidData,
448 "child-process output exceeds the capture limit",
449 ));
450 }
451 buffer.extend_from_slice(&chunk[..bytes_read]);
452 Ok(())
453}
454
455#[cfg(feature = "wasi-host-bridge")]
456/// Builds one validated `KEY=VALUE` environment assignment for a child.
457///
458/// # Parameters
459///
460/// - `key`: Environment variable name to validate and encode.
461/// - `value`: Input consumed by [`crate::platform::build_child_env_assignment`].
462///
463/// # Returns
464///
465/// Returns [`Some`] when [`crate::platform::build_child_env_assignment`] can produce a value and [`None`] otherwise.
466///
467/// # Examples
468///
469/// ```rust,ignore
470/// let _result = pharos_native::platform::build_child_env_assignment(todo!(), "value");
471/// ```
472///
473/// # Related
474///
475/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
476pub(crate) fn build_child_env_assignment(key: Option<&str>, value: Option<&str>) -> Option<String> {
477 let key = key.filter(|value| !value.is_empty())?;
478 let value = value.filter(|value| !value.is_empty())?;
479 if key.len() > MAX_ENV_COMPONENT_BYTES
480 || value.len() > MAX_ENV_COMPONENT_BYTES
481 || key.contains('=')
482 || key.contains('\0')
483 || value.contains('\0')
484 {
485 return None;
486 }
487 Some(format!("{key}={value}"))
488}
489
490#[cfg(feature = "wasi-host-bridge")]
491/// Builds the inherited environment with valid requested keys applied once.
492///
493/// # Parameters
494///
495/// - `env_keys`: Keys to override or append in the child environment.
496/// - `env_values`: Input consumed by [`crate::platform::build_child_env`].
497///
498/// # Returns
499///
500/// Returns the computed value produced by [`crate::platform::build_child_env`].
501///
502/// # Performance
503///
504/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::platform::build_child_env`].
505///
506/// # Examples
507///
508/// ```rust,ignore
509/// let _result = pharos_native::platform::build_child_env(todo!(), todo!(), todo!(), todo!());
510/// ```
511///
512/// # Related
513///
514/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
515pub(crate) fn build_child_env<K, V>(env_keys: &[K], env_values: &[V]) -> Vec<(String, String)>
516where
517 K: AsRef<str>,
518 V: AsRef<str>,
519{
520 let assignments: Vec<(String, String)> = env_keys
521 .iter()
522 .zip(env_values.iter())
523 .filter_map(|(key, value)| {
524 let assignment = build_child_env_assignment(Some(key.as_ref()), Some(value.as_ref()))?;
525 let (key, value) = assignment.split_once('=')?;
526 Some((key.to_owned(), value.to_owned()))
527 })
528 .collect();
529 let overridden: HashSet<&str> = assignments.iter().map(|(key, _)| key.as_str()).collect();
530 let mut child_env: Vec<(String, String)> = env::vars()
531 .filter(|(key, _)| !overridden.contains(key.as_str()))
532 .collect();
533 child_env.extend(assignments);
534 child_env
535}
536
537#[cfg(feature = "wasi-host-bridge")]
538/// Releases an owned child environment using Rust's normal drop semantics.
539///
540/// # Parameters
541///
542/// - `child_env`: Environment vector being consumed and dropped.
543///
544/// # Returns
545///
546/// Returns after [`crate::platform::free_child_env`] applies its in-place effect.
547///
548/// # Examples
549///
550/// ```rust,ignore
551/// let _result = pharos_native::platform::free_child_env(todo!(), todo!());
552/// ```
553///
554/// # Related
555///
556/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
557pub(crate) fn free_child_env<K, V>(child_env: &mut Vec<(K, V)>) {
558 child_env.clear();
559}
560
561/// Returns whether a path currently identifies a directory.
562///
563/// # Parameters
564///
565/// - `path`: Input consumed by [`crate::platform::path_is_directory`].
566///
567/// # Returns
568///
569/// Returns `true` when the condition checked by [`crate::platform::path_is_directory`] holds and `false` otherwise.
570///
571/// # Examples
572///
573/// ```rust,ignore
574/// let _result = pharos_native::platform::path_is_directory(std::path::Path::new("fixture.json"));
575/// ```
576///
577/// # Related
578///
579/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
580pub fn path_is_directory(path: &Path) -> bool {
581 !path.as_os_str().is_empty() && path.is_dir()
582}
583
584/// Tests whether one string begins with another string.
585///
586/// # Parameters
587///
588/// - `value`: Input consumed by [`crate::platform::starts_with`].
589/// - `prefix`: Input consumed by [`crate::platform::starts_with`].
590///
591/// # Returns
592///
593/// Returns `true` when the condition checked by [`crate::platform::starts_with`] holds and `false` otherwise.
594///
595/// # Examples
596///
597/// ```rust,ignore
598/// let _result = pharos_native::platform::starts_with("value", todo!());
599/// ```
600///
601/// # Related
602///
603/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
604pub fn starts_with(value: &str, prefix: &str) -> bool {
605 value.starts_with(prefix)
606}
607
608#[cfg(feature = "wasi-host-bridge")]
609#[derive(Debug)]
610struct CapturePipeResult {
611 bytes: Result<Vec<u8>, i32>,
612}
613
614#[cfg(feature = "wasi-host-bridge")]
615/// Captures pipe.
616fn capture_pipe<R: Read + Send + 'static>(
617 mut reader: R,
618 failed: Arc<AtomicBool>,
619) -> JoinHandle<CapturePipeResult> {
620 thread::spawn(move || {
621 let mut buffer = Vec::new();
622 let mut open = true;
623 while open {
624 if append_pipe_to_buffer(&mut reader, &mut buffer, &mut open).is_err() {
625 failed.store(true, Ordering::Release);
626 return CapturePipeResult { bytes: Err(69) };
627 }
628 }
629 CapturePipeResult { bytes: Ok(buffer) }
630 })
631}
632
633#[cfg(feature = "wasi-host-bridge")]
634/// Captures argv.
635fn capture_argv<A: AsRef<str>>(argv: &[A]) -> Result<Vec<String>, i32> {
636 if argv.is_empty() || argv.len() > MAX_CAPTURE_ARGUMENTS {
637 return Err(69);
638 }
639 argv.iter()
640 .map(|argument| {
641 let argument = argument.as_ref();
642 if argument.is_empty()
643 || argument.len() > MAX_CAPTURE_ARGUMENT_BYTES
644 || argument.contains('\0')
645 {
646 Err(69)
647 } else {
648 Ok(argument.to_owned())
649 }
650 })
651 .collect()
652}
653
654#[cfg(feature = "wasi-host-bridge")]
655/// Terminates a supervised child and its process group where supported, then reaps it.
656///
657/// # Parameters
658///
659/// - `child`: Input consumed by [`crate::platform::terminate_child_process_tree`].
660///
661/// # Returns
662///
663/// Returns after [`crate::platform::terminate_child_process_tree`] applies its in-place effect.
664///
665/// # Examples
666///
667/// ```rust,ignore
668/// let _result = pharos_native::platform::terminate_child_process_tree(todo!());
669/// ```
670///
671/// # Related
672///
673/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
674pub fn terminate_child_process_tree(child: &mut Child) {
675 #[cfg(unix)]
676 {
677 let group = format!("-{}", child.id());
678 let _ = Command::new("/bin/kill")
679 .args(["-TERM", "--", group.as_str()])
680 .stdout(Stdio::null())
681 .stderr(Stdio::null())
682 .status();
683 thread::sleep(Duration::from_millis(20));
684 let _ = Command::new("/bin/kill")
685 .args(["-KILL", "--", group.as_str()])
686 .stdout(Stdio::null())
687 .stderr(Stdio::null())
688 .status();
689 }
690 #[cfg(windows)]
691 {
692 let pid = child.id().to_string();
693 let taskkill = env::var_os("SystemRoot")
694 .map(|root| Path::new(&root).join("System32").join("taskkill.exe"))
695 .unwrap_or_else(|| Path::new("taskkill.exe").to_path_buf());
696 let _ = Command::new(taskkill)
697 .args(["/PID", pid.as_str(), "/T", "/F"])
698 .stdout(Stdio::null())
699 .stderr(Stdio::null())
700 .status();
701 }
702 let _ = child.kill();
703 let _ = child.wait();
704}
705
706/// Executes an argv vector directly with bounded output and wall-clock lifetime.
707///
708/// No shell is involved. Each stream is drained concurrently and is capped at
709/// `MAX_CAPTURE_OUTPUT_BYTES`. A timeout terminates the child process tree where
710/// supported and always reaps the direct child before returning.
711#[cfg(feature = "wasi-host-bridge")]
712fn capture_execv_output_split_env_with_timeout<A, K, V>(
713 argv: &[A],
714 env_keys: &[K],
715 env_values: &[V],
716 timeout: Duration,
717) -> Result<(i32, Vec<u8>, Vec<u8>), i32>
718where
719 A: AsRef<str>,
720 K: AsRef<str>,
721 V: AsRef<str>,
722{
723 let argv = capture_argv(argv)?;
724 let mut child_env = build_child_env(env_keys, env_values);
725 let mut command = Command::new(&argv[0]);
726 command
727 .args(&argv[1..])
728 .env_clear()
729 .stdout(Stdio::piped())
730 .stderr(Stdio::piped());
731 #[cfg(unix)]
732 {
733 use std::os::unix::process::CommandExt;
734 command.process_group(0);
735 }
736 for (key, value) in &child_env {
737 command.env(key, value);
738 }
739 free_child_env(&mut child_env);
740
741 let mut child = command.spawn().map_err(|_| 69)?;
742 let stdout = child.stdout.take().ok_or(69)?;
743 let stderr = child.stderr.take().ok_or(69)?;
744 let failed = Arc::new(AtomicBool::new(false));
745 let stdout_thread = capture_pipe(stdout, Arc::clone(&failed));
746 let stderr_thread = capture_pipe(stderr, Arc::clone(&failed));
747 let deadline = Instant::now().checked_add(timeout);
748
749 let status = loop {
750 if failed.load(Ordering::Acquire) {
751 terminate_child_process_tree(&mut child);
752 break child.try_wait().map_err(|_| 69)?.ok_or(69)?;
753 }
754 if deadline.is_some_and(|value| Instant::now() >= value) {
755 terminate_child_process_tree(&mut child);
756 let _ = stdout_thread.join();
757 let _ = stderr_thread.join();
758 return Err(CHILD_PROCESS_TIMEOUT_CODE);
759 }
760 match child.try_wait() {
761 Ok(Some(status)) => break status,
762 Ok(None) => thread::sleep(CAPTURE_POLL_INTERVAL),
763 Err(_) => {
764 terminate_child_process_tree(&mut child);
765 return Err(69);
766 }
767 }
768 };
769
770 let stdout = stdout_thread.join().map_err(|_| 69)?.bytes?;
771 let stderr = stderr_thread.join().map_err(|_| 69)?.bytes?;
772 Ok((status.code().unwrap_or(69), stdout, stderr))
773}
774
775#[cfg(feature = "wasi-host-bridge")]
776/// Executes an argv vector directly and captures stdout and stderr separately.
777///
778/// # Parameters
779///
780/// - `argv`: Input consumed by [`crate::platform::capture_execv_output_split_env`].
781/// - `env_keys`: Input consumed by [`crate::platform::capture_execv_output_split_env`].
782/// - `env_values`: Input consumed by [`crate::platform::capture_execv_output_split_env`].
783///
784/// # Returns
785///
786/// Returns the successful value produced by [`crate::platform::capture_execv_output_split_env`] or an error when the operation fails.
787///
788/// # Errors
789///
790/// Returns an error value when [`crate::platform::capture_execv_output_split_env`] cannot complete successfully.
791///
792/// # Examples
793///
794/// ```rust,ignore
795/// let _result = pharos_native::platform::capture_execv_output_split_env(todo!(), todo!(), todo!(), todo!(), todo!(), todo!());
796/// ```
797///
798/// # Related
799///
800/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
801pub fn capture_execv_output_split_env<A, K, V>(
802 argv: &[A],
803 env_keys: &[K],
804 env_values: &[V],
805) -> Result<(i32, Vec<u8>, Vec<u8>), i32>
806where
807 A: AsRef<str>,
808 K: AsRef<str>,
809 V: AsRef<str>,
810{
811 capture_execv_output_split_env_with_timeout(argv, env_keys, env_values, MAX_CAPTURE_WALL_CLOCK)
812}
813
814#[cfg(feature = "wasi-host-bridge")]
815/// Executes argv with validated environment overrides and merges both streams.
816///
817/// # Parameters
818///
819/// - `argv`: Input consumed by [`crate::platform::capture_execv_output_env`].
820/// - `env_keys`: Input consumed by [`crate::platform::capture_execv_output_env`].
821/// - `env_values`: Input consumed by [`crate::platform::capture_execv_output_env`].
822///
823/// # Returns
824///
825/// Returns the successful value produced by [`crate::platform::capture_execv_output_env`] or an error when the operation fails.
826///
827/// # Errors
828///
829/// Returns an error value when [`crate::platform::capture_execv_output_env`] cannot complete successfully.
830///
831/// # Examples
832///
833/// ```rust,ignore
834/// let _result = pharos_native::platform::capture_execv_output_env(todo!(), todo!(), todo!(), todo!(), todo!());
835/// ```
836///
837/// # Related
838///
839/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
840pub fn capture_execv_output_env<A, K, V>(
841 argv: &[A],
842 env_keys: &[K],
843 env_values: &[V],
844) -> Result<(i32, Vec<u8>), i32>
845where
846 A: AsRef<str>,
847 K: AsRef<str>,
848 V: AsRef<str>,
849{
850 let (status, stdout, stderr) = capture_execv_output_split_env(argv, env_keys, env_values)?;
851 if stdout
852 .len()
853 .checked_add(stderr.len())
854 .is_none_or(|length| length > MAX_CAPTURE_OUTPUT_BYTES)
855 {
856 return Err(69);
857 }
858 let mut output = Vec::with_capacity(stdout.len() + stderr.len());
859 output.extend_from_slice(&stdout);
860 output.extend_from_slice(&stderr);
861 Ok((status, output))
862}
863
864#[cfg(feature = "wasi-host-bridge")]
865/// Executes argv directly and captures bounded merged output.
866///
867/// # Parameters
868///
869/// - `argv`: Input consumed by [`crate::platform::capture_execv_output`].
870/// - `Vec<u8>`: Input consumed by [`crate::platform::capture_execv_output`].
871///
872/// # Returns
873///
874/// Returns the successful value produced by [`crate::platform::capture_execv_output`] or an error when the operation fails.
875///
876/// # Errors
877///
878/// Returns an error value when [`crate::platform::capture_execv_output`] cannot complete successfully.
879///
880/// # Examples
881///
882/// ```rust,ignore
883/// let _result = pharos_native::platform::capture_execv_output(todo!(), todo!());
884/// ```
885///
886/// # Related
887///
888/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
889pub fn capture_execv_output<A: AsRef<str>>(argv: &[A]) -> Result<(i32, Vec<u8>), i32> {
890 let empty: [&str; 0] = [];
891 capture_execv_output_env(argv, &empty, &empty)
892}
893
894/// Checks writability without creating files or invoking external commands.
895///
896/// # Parameters
897///
898/// - `path`: Input consumed by [`crate::platform::path_is_writable`].
899///
900/// # Returns
901///
902/// Returns `true` when the condition checked by [`crate::platform::path_is_writable`] holds and `false` otherwise.
903///
904/// # Examples
905///
906/// ```rust,ignore
907/// let _result = pharos_native::platform::path_is_writable(std::path::Path::new("fixture.json"));
908/// ```
909///
910/// # Related
911///
912/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
913pub fn path_is_writable(path: &Path) -> bool {
914 if path.as_os_str().is_empty() {
915 return false;
916 }
917 let directory = if path.is_dir() {
918 path.to_path_buf()
919 } else {
920 path.parent()
921 .unwrap_or_else(|| Path::new("."))
922 .to_path_buf()
923 };
924 if !directory.is_dir() {
925 return false;
926 }
927 #[cfg(unix)]
928 {
929 use std::os::unix::fs::PermissionsExt;
930 directory
931 .metadata()
932 .map(|metadata| metadata.permissions().mode() & 0o222 != 0)
933 .unwrap_or(false)
934 }
935 #[cfg(not(unix))]
936 {
937 true
938 }
939}
940
941const PLATFORM_BUFFER_LIMIT: usize = 16 * 1024 * 1024;
942
943#[derive(Debug, Clone, Default, Eq, PartialEq)]
944/// Public struct `PlatformBuffer` used by this module.
945///
946/// # Fields
947///
948/// - `bytes`: Stored field on `PlatformBuffer`.
949///
950/// # Examples
951///
952/// ```rust,ignore
953/// let _ = std::mem::size_of::<pharos_native::platform::PlatformBuffer>();
954/// ```
955///
956/// # Related
957///
958/// - [`crate::platform`] for constructors, helpers, and neighboring runtime types.
959pub struct PlatformBuffer {
960 pub bytes: Vec<u8>,
961}
962
963/// Initializes a bounded platform buffer.
964///
965/// # Parameters
966///
967/// - `buffer`: Input consumed by [`crate::platform::buffer_init`].
968///
969/// # Returns
970///
971/// Returns after [`crate::platform::buffer_init`] applies its in-place effect.
972///
973/// # Examples
974///
975/// ```rust,ignore
976/// let _result = pharos_native::platform::buffer_init(&mut []);
977/// ```
978///
979/// # Related
980///
981/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
982pub fn buffer_init(buffer: &mut PlatformBuffer) {
983 buffer.bytes.clear();
984}
985
986/// Releases a platform buffer.
987///
988/// # Parameters
989///
990/// - `buffer`: Input consumed by [`crate::platform::buffer_free`].
991///
992/// # Returns
993///
994/// Returns after [`crate::platform::buffer_free`] applies its in-place effect.
995///
996/// # Examples
997///
998/// ```rust,ignore
999/// let _result = pharos_native::platform::buffer_free(&mut []);
1000/// ```
1001///
1002/// # Related
1003///
1004/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
1005pub fn buffer_free(buffer: &mut PlatformBuffer) {
1006 buffer.bytes.clear();
1007 buffer.bytes.shrink_to_fit();
1008}
1009
1010/// Reserves bounded capacity for a platform buffer.
1011///
1012/// # Parameters
1013///
1014/// - `buffer`: Input consumed by [`crate::platform::buffer_reserve`].
1015/// - `needed`: Input consumed by [`crate::platform::buffer_reserve`].
1016///
1017/// # Returns
1018///
1019/// Returns `true` when the condition checked by [`crate::platform::buffer_reserve`] holds and `false` otherwise.
1020///
1021/// # Examples
1022///
1023/// ```rust,ignore
1024/// let _result = pharos_native::platform::buffer_reserve(&mut [], todo!());
1025/// ```
1026///
1027/// # Related
1028///
1029/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
1030pub fn buffer_reserve(buffer: &mut PlatformBuffer, needed: usize) -> bool {
1031 needed <= PLATFORM_BUFFER_LIMIT
1032 && buffer
1033 .bytes
1034 .try_reserve(needed.saturating_sub(buffer.bytes.len()))
1035 .is_ok()
1036}
1037
1038/// Appends bytes to a platform buffer.
1039///
1040/// # Parameters
1041///
1042/// - `buffer`: Input consumed by [`crate::platform::buffer_append`].
1043/// - `data`: Input consumed by [`crate::platform::buffer_append`].
1044///
1045/// # Returns
1046///
1047/// Returns `true` when the condition checked by [`crate::platform::buffer_append`] holds and `false` otherwise.
1048///
1049/// # Examples
1050///
1051/// ```rust,ignore
1052/// let _result = pharos_native::platform::buffer_append(&mut [], b"");
1053/// ```
1054///
1055/// # Related
1056///
1057/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
1058pub fn buffer_append(buffer: &mut PlatformBuffer, data: &[u8]) -> bool {
1059 if !buffer_reserve(buffer, buffer.bytes.len().saturating_add(data.len())) {
1060 return false;
1061 }
1062 buffer.bytes.extend_from_slice(data);
1063 true
1064}
1065
1066/// Duplicates a bounded byte range as UTF-8 text.
1067///
1068/// # Parameters
1069///
1070/// - `text`: Input consumed by [`crate::platform::dup_range`].
1071/// - `length`: Input consumed by [`crate::platform::dup_range`].
1072///
1073/// # Returns
1074///
1075/// Returns [`Some`] when [`crate::platform::dup_range`] can produce a value and [`None`] otherwise.
1076///
1077/// # Examples
1078///
1079/// ```rust,ignore
1080/// let _result = pharos_native::platform::dup_range("{}", todo!());
1081/// ```
1082///
1083/// # Related
1084///
1085/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
1086pub fn dup_range(text: &str, length: usize) -> Option<String> {
1087 text.get(..length.min(text.len())).map(ToOwned::to_owned)
1088}
1089
1090/// Trims ASCII whitespace in place.
1091///
1092/// # Parameters
1093///
1094/// - `text`: Input consumed by [`crate::platform::trim_in_place`].
1095///
1096/// # Returns
1097///
1098/// Returns after [`crate::platform::trim_in_place`] applies its in-place effect.
1099///
1100/// # Examples
1101///
1102/// ```rust,ignore
1103/// let _result = pharos_native::platform::trim_in_place("{}");
1104/// ```
1105///
1106/// # Related
1107///
1108/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
1109pub fn trim_in_place(text: &mut String) {
1110 *text = text.trim().to_owned();
1111}
1112
1113/// Joins two paths without allowing an absolute right-hand replacement.
1114///
1115/// # Parameters
1116///
1117/// - `left`: Input consumed by [`crate::platform::path_join`].
1118/// - `right`: Input consumed by [`crate::platform::path_join`].
1119///
1120/// # Returns
1121///
1122/// Returns [`Some`] when [`crate::platform::path_join`] can produce a value and [`None`] otherwise.
1123///
1124/// # Examples
1125///
1126/// ```rust,ignore
1127/// let _result = pharos_native::platform::path_join(todo!(), todo!());
1128/// ```
1129///
1130/// # Related
1131///
1132/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
1133pub fn path_join(left: &str, right: &str) -> Option<String> {
1134 if left.is_empty() || right.is_empty() || Path::new(right).is_absolute() || right.contains('\0')
1135 {
1136 return None;
1137 }
1138 Some(Path::new(left).join(right).to_string_lossy().into_owned())
1139}
1140
1141/// Returns a path's parent component as owned text.
1142///
1143/// # Parameters
1144///
1145/// - `path`: Input consumed by [`crate::platform::path_dirname`].
1146///
1147/// # Returns
1148///
1149/// Returns [`Some`] when [`crate::platform::path_dirname`] can produce a value and [`None`] otherwise.
1150///
1151/// # Examples
1152///
1153/// ```rust,ignore
1154/// let _result = pharos_native::platform::path_dirname(std::path::Path::new("fixture.json"));
1155/// ```
1156///
1157/// # Related
1158///
1159/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
1160pub fn path_dirname(path: &str) -> Option<String> {
1161 Path::new(path)
1162 .parent()
1163 .map(|value| value.to_string_lossy().into_owned())
1164}
1165
1166/// Checks whether a path exists without following user-controlled commands.
1167///
1168/// # Parameters
1169///
1170/// - `path`: Input consumed by [`crate::platform::path_exists`].
1171///
1172/// # Returns
1173///
1174/// Returns `true` when the condition checked by [`crate::platform::path_exists`] holds and `false` otherwise.
1175///
1176/// # Examples
1177///
1178/// ```rust,ignore
1179/// let _result = pharos_native::platform::path_exists(std::path::Path::new("fixture.json"));
1180/// ```
1181///
1182/// # Related
1183///
1184/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
1185pub fn path_exists(path: &str) -> bool {
1186 !path.is_empty() && Path::new(path).exists()
1187}
1188
1189/// Reads a bounded UTF-8 file.
1190///
1191/// # Parameters
1192///
1193/// - `path`: Input consumed by [`crate::platform::read_file_text`].
1194///
1195/// # Returns
1196///
1197/// Returns [`Some`] when [`crate::platform::read_file_text`] can produce a value and [`None`] otherwise.
1198///
1199/// # Performance
1200///
1201/// Performance is proportional to the size of the supplied inputs and any parsed or loaded data consumed by [`crate::platform::read_file_text`].
1202///
1203/// # Examples
1204///
1205/// ```rust,ignore
1206/// let _result = pharos_native::platform::read_file_text(std::path::Path::new("fixture.json"));
1207/// ```
1208///
1209/// # Related
1210///
1211/// - [`crate::platform`] for adjacent helpers that share the same runtime surface.
1212pub fn read_file_text(path: &Path) -> Option<String> {
1213 let metadata = std::fs::metadata(path).ok()?;
1214 (metadata.len() <= PLATFORM_BUFFER_LIMIT as u64)
1215 .then(|| std::fs::read_to_string(path).ok())
1216 .flatten()
1217}
1218
1219#[cfg(all(test, feature = "wasi-host-bridge"))]
1220mod tests {
1221 use super::{CHILD_PROCESS_TIMEOUT_CODE, capture_execv_output_split_env_with_timeout};
1222 use std::time::{Duration, Instant};
1223
1224 #[cfg(unix)]
1225 #[test]
1226 /// Captures timeout terminates a child process group.
1227 fn capture_timeout_terminates_a_child_process_group() {
1228 let started = Instant::now();
1229 let result = capture_execv_output_split_env_with_timeout(
1230 &["/bin/sh", "-c", "sleep 5 & wait"],
1231 &[] as &[&str],
1232 &[] as &[&str],
1233 Duration::from_millis(100),
1234 );
1235 assert_eq!(result, Err(CHILD_PROCESS_TIMEOUT_CODE));
1236 assert!(started.elapsed() < Duration::from_secs(2));
1237 }
1238}