Skip to main content

nodejs/stdlib/
process.rs

1//! Node `process` global — the subset packages read at load time.
2//!
3//! Data properties (`process.env`, `process.argv`, `process.platform`, the
4//! `stdout`/`stderr` stream stand-ins, …) are served through `constant`;
5//! callable members (`process.cwd()`, `process.hrtime()`, the EventEmitter-style
6//! `on`/`emit` no-ops, …) through `call`. `process.nextTick` is intentionally NOT
7//! handled here — it stays on the core microtask path in `builtins.rs`.
8
9use crate::host::{with_host, JsObj};
10use fusevm::Value;
11use indexmap::IndexMap;
12
13/// Callable members. `nextTick` is deliberately absent (handled in `builtins`).
14pub const METHODS: &[&str] = &[
15    "cwd",
16    "chdir",
17    "exit",
18    "hrtime",
19    "uptime",
20    "memoryUsage",
21    "cpuUsage",
22    "umask",
23    "binding",
24    "emit",
25    "on",
26    "once",
27    "off",
28    "addListener",
29    "removeListener",
30    "removeAllListeners",
31    "listeners",
32    "emitWarning",
33    "kill",
34    "getuid",
35    "getgid",
36    "geteuid",
37    "getegid",
38    "getgroups",
39    "setuid",
40    "setgid",
41    "seteuid",
42    "setegid",
43    "setgroups",
44    "initgroups",
45    "ref",
46    "unref",
47    "abort",
48    "getActiveResourcesInfo",
49    "resourceUsage",
50    "threadCpuUsage",
51    "availableMemory",
52    "constrainedMemory",
53    "getBuiltinModule",
54    "openStdin",
55    "hasUncaughtExceptionCaptureCallback",
56    "setUncaughtExceptionCaptureCallback",
57    "addUncaughtExceptionCaptureCallback",
58    "execve",
59    "reallyExit",
60    "loadEnvFile",
61    "setSourceMapsEnabled",
62];
63
64thread_local! {
65    /// The single `process.setUncaughtExceptionCaptureCallback` slot. Stored as a
66    /// heap handle (thread-local like the JS heap); read by
67    /// `hasUncaughtExceptionCaptureCallback`.
68    static UNCAUGHT_CAPTURE: std::cell::RefCell<Option<Value>> =
69        const { std::cell::RefCell::new(None) };
70}
71
72/// Data properties, served through `namespace_property` → `stdlib::constant`.
73pub fn constant(name: &str) -> Option<Value> {
74    Some(match name {
75        "env" => env_object(),
76        "argv" => argv(),
77        "argv0" => with_host(|h| h.new_str(exec_path())),
78        "execPath" => with_host(|h| h.new_str(exec_path())),
79        "execArgv" => with_host(|h| h.new_array(Vec::new())),
80        "platform" => with_host(|h| h.new_str(super::os::platform())),
81        "arch" => with_host(|h| h.new_str(super::os::arch())),
82        "pid" => Value::Float(std::process::id() as f64),
83        "ppid" => Value::Float(0.0),
84        "title" => with_host(|h| h.new_str("node")),
85        // A best-effort Node-compatible version string. Kept low so a dep's
86        // `if (semver.lt(process.version, ...))` gate takes the conservative path.
87        "version" => with_host(|h| h.new_str("v26.5.0")),
88        "versions" => versions(),
89        "stdout" => std_stream(1),
90        "stderr" => std_stream(2),
91        "stdin" => std_stream(0),
92        _ => return None,
93    })
94}
95
96pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
97    Some(match method {
98        "cwd" => {
99            let d = std::env::current_dir()
100                .map(|p| p.to_string_lossy().into_owned())
101                .unwrap_or_default();
102            Ok(with_host(|h| h.new_str(d)))
103        }
104        // `hrtime()` → `[seconds, nanoseconds]` since an arbitrary epoch (here the
105        // monotonic clock via `Instant` is unavailable statically, so use the
106        // system clock — sufficient for the timing scaffolding deps set up).
107        "hrtime" => Ok(hrtime(args)),
108        "uptime" => Ok(Value::Float(0.0)),
109        "memoryUsage" => Ok(memory_usage()),
110        "cpuUsage" => Ok(with_host(|h| {
111            let mut m = IndexMap::new();
112            m.insert("user".into(), Value::Float(0.0));
113            m.insert("system".into(), Value::Float(0.0));
114            h.new_object(m)
115        })),
116        "umask" => Ok(Value::Float(0.0)),
117        "binding" => Err(crate::host::type_error("process.binding is not supported")),
118        // EventEmitter-style registration is accepted and ignored (returns the
119        // process namespace so `.on(...).on(...)` chains work); no signals fire.
120        "on" | "once" | "off" | "addListener" | "removeListener" | "removeAllListeners" => {
121            Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into()))))
122        }
123        "listeners" => Ok(with_host(|h| h.new_array(Vec::new()))),
124        "emit" => Ok(Value::Bool(false)),
125        "emitWarning" => Ok(Value::Undef),
126        "chdir" | "exit" | "kill" | "reallyExit" | "setSourceMapsEnabled" => Ok(Value::Undef),
127
128        // POSIX identity queries (libc; pure reads, always safe).
129        "getuid" => Ok(Value::Float(unsafe { libc::getuid() } as f64)),
130        "geteuid" => Ok(Value::Float(unsafe { libc::geteuid() } as f64)),
131        "getgid" => Ok(Value::Float(unsafe { libc::getgid() } as f64)),
132        "getegid" => Ok(Value::Float(unsafe { libc::getegid() } as f64)),
133        "getgroups" => {
134            let groups = supplementary_groups();
135            Ok(with_host(|h| {
136                h.new_array(groups.into_iter().map(Value::Float).collect())
137            }))
138        }
139
140        // POSIX identity mutation (libc; best-effort — silently ignored when the
141        // process lacks the privilege, matching a no-throw best-effort surface).
142        "setuid" | "seteuid" | "setgid" | "setegid" => {
143            let id = super::arg_num(args, 0);
144            if id.is_finite() {
145                let id = id as u32;
146                // SAFETY: id is a plain uid/gid number; a failed call just returns -1.
147                unsafe {
148                    match method {
149                        "setuid" => libc::setuid(id),
150                        "seteuid" => libc::seteuid(id),
151                        "setgid" => libc::setgid(id),
152                        _ => libc::setegid(id),
153                    };
154                }
155            }
156            Ok(Value::Undef)
157        }
158        "setgroups" => {
159            let groups = gid_array(args.first());
160            // SAFETY: `groups` is a valid gid buffer of the given length.
161            unsafe {
162                libc::setgroups(groups.len() as _, groups.as_ptr());
163            }
164            Ok(Value::Undef)
165        }
166        "initgroups" => {
167            let user = super::arg_str(args, 0);
168            let extra = super::arg_num(args, 1);
169            if let Ok(c) = std::ffi::CString::new(user) {
170                let gid = if extra.is_finite() { extra as u32 } else { 0 };
171                // SAFETY: `c` is NUL-terminated; a failed call just returns -1.
172                unsafe {
173                    libc::initgroups(c.as_ptr(), gid as _);
174                }
175            }
176            Ok(Value::Undef)
177        }
178
179        // `ref`/`unref` on the process object are chainable no-ops (no libuv
180        // handle refcount to touch); return the process namespace.
181        "ref" | "unref" => Ok(with_host(|h| h.alloc(JsObj::Builtin("process".into())))),
182        "abort" => std::process::abort(),
183        "getActiveResourcesInfo" => Ok(with_host(|h| h.new_array(Vec::new()))),
184        "resourceUsage" => Ok(resource_usage()),
185        "threadCpuUsage" => Ok(thread_cpu_usage()),
186        "availableMemory" | "constrainedMemory" => Ok(Value::Float(0.0)),
187        "getBuiltinModule" => {
188            let id = super::arg_str(args, 0);
189            let id = id.strip_prefix("node:").unwrap_or(&id);
190            match crate::stdlib::resolve(id) {
191                Some(ns) => Ok(with_host(|h| h.alloc(JsObj::Builtin(ns.to_string())))),
192                None => Ok(Value::Undef),
193            }
194        }
195        "openStdin" => Ok(std_stream(0)),
196
197        "hasUncaughtExceptionCaptureCallback" => {
198            Ok(Value::Bool(UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some())))
199        }
200        "setUncaughtExceptionCaptureCallback" => {
201            let cb = args.first().cloned().unwrap_or(Value::Undef);
202            let clear = matches!(cb, Value::Undef) || with_host(|h| h.is_null(&cb));
203            if clear {
204                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = None);
205            } else if UNCAUGHT_CAPTURE.with(|c| c.borrow().is_some()) {
206                return Some(Err(crate::host::type_error(
207                    "`process.setUncaughtExceptionCaptureCallback()` was called \
208                     while a capture callback was already active",
209                )));
210            } else {
211                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
212            }
213            Ok(Value::Undef)
214        }
215        "addUncaughtExceptionCaptureCallback" => {
216            let cb = args.first().cloned().unwrap_or(Value::Undef);
217            if !matches!(cb, Value::Undef) {
218                UNCAUGHT_CAPTURE.with(|c| *c.borrow_mut() = Some(cb));
219            }
220            Ok(Value::Undef)
221        }
222        "execve" => exec_ve(args),
223        "loadEnvFile" => load_env_file(&super::arg_str(args, 0)),
224        _ => return None,
225    })
226}
227
228/// `process.env` as a plain object built from the real environment.
229fn env_object() -> Value {
230    with_host(|h| {
231        let mut m = IndexMap::new();
232        for (k, v) in std::env::vars() {
233            m.insert(k, h.new_str(v));
234        }
235        h.new_object(m)
236    })
237}
238
239/// `process.argv`: `[execPath, entryScript, ...userArgs]` (best effort).
240fn argv() -> Value {
241    with_host(|h| {
242        let items: Vec<Value> = std::env::args().map(|a| h.new_str(a)).collect();
243        h.new_array(items)
244    })
245}
246
247fn exec_path() -> String {
248    std::env::current_exe()
249        .map(|p| p.to_string_lossy().into_owned())
250        .unwrap_or_else(|_| "node".into())
251}
252
253/// `process.versions` — a small map; only `node` is commonly gated on.
254fn versions() -> Value {
255    with_host(|h| {
256        let mut m = IndexMap::new();
257        m.insert("node".into(), h.new_str("26.5.0"));
258        m.insert("v8".into(), h.new_str("0.0.0"));
259        h.new_object(m)
260    })
261}
262
263/// A minimal `process.stdout`/`stderr`/`stdin` stand-in: enough surface
264/// (`fd`, `isTTY`, `writable`, a `write`) for load-time probes like
265/// `tty.isatty(process.stderr.fd)`.
266fn std_stream(fd: i32) -> Value {
267    with_host(|h| {
268        let mut m = IndexMap::new();
269        m.insert("@@native".into(), h.new_str("WriteStream"));
270        m.insert("fd".into(), Value::Float(fd as f64));
271        // SAFETY: isatty is a pure query on the fd number.
272        let is_tty = unsafe { libc::isatty(fd) == 1 };
273        m.insert("isTTY".into(), Value::Bool(is_tty));
274        m.insert("writable".into(), Value::Bool(fd != 0));
275        m.insert("readable".into(), Value::Bool(fd == 0));
276        // A tty stream exposes its terminal dimensions (real ioctl reading).
277        if is_tty {
278            if let Some((cols, rows)) = super::tty::window_size(fd) {
279                m.insert("columns".into(), Value::Float(cols as f64));
280                m.insert("rows".into(), Value::Float(rows as f64));
281            }
282        }
283        h.new_object(m)
284    })
285}
286
287/// Instance methods of a `process.stdout`/`stderr` `WriteStream`: `write`/`end`
288/// emit the chunk raw (no newline) to the stream's fd, so ordering interleaves
289/// correctly with `console.log`.
290pub fn stream_instance_call(recv: &Value, method: &str, args: &[Value]) -> Result<Value, String> {
291    match method {
292        "write" | "end" => {
293            let fd = with_host(|h| match h.get(recv) {
294                Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
295                _ => 1.0,
296            });
297            let chunk = super::arg_str(args, 0);
298            use std::io::Write as _;
299            if fd == 2.0 {
300                let _ = std::io::stderr().write_all(chunk.as_bytes());
301                let _ = std::io::stderr().flush();
302            } else {
303                let _ = std::io::stdout().write_all(chunk.as_bytes());
304                let _ = std::io::stdout().flush();
305            }
306            Ok(Value::Bool(true))
307        }
308        // A no-op stream surface so `.on('data')`/`.once`/`.end()` chaining loads.
309        "on" | "once" | "removeListener" | "cork" | "uncork" | "setEncoding" => Ok(recv.clone()),
310        // `tty.WriteStream` cursor/erase control — emit the corresponding ANSI
311        // escape to the stream's fd (best-effort; only meaningful on a real tty).
312        "cursorTo" | "moveCursor" | "clearLine" | "clearScreenDown" => {
313            let seq = tty_control(method, args);
314            write_fd(stream_fd(recv), seq.as_bytes());
315            Ok(Value::Bool(true))
316        }
317        "getWindowSize" => {
318            let (c, r) = super::tty::window_size(stream_fd(recv) as i32).unwrap_or((80, 24));
319            Ok(with_host(|h| {
320                h.new_array(vec![Value::Float(c as f64), Value::Float(r as f64)])
321            }))
322        }
323        // A truecolor terminal advertises 24-bit depth; hasColors(count) is true
324        // for any request within that range.
325        "getColorDepth" => Ok(Value::Float(24.0)),
326        "hasColors" => Ok(Value::Bool(true)),
327        _ => Err(crate::host::type_error(&format!(
328            "{method} is not a function"
329        ))),
330    }
331}
332
333fn hrtime(args: &[Value]) -> Value {
334    let now = std::time::SystemTime::now()
335        .duration_since(std::time::UNIX_EPOCH)
336        .unwrap_or_default();
337    let (mut secs, mut nanos) = (now.as_secs() as f64, now.subsec_nanos() as f64);
338    // `hrtime(prev)` returns the diff from a prior reading.
339    if let Some(Value::Obj(_)) = args.first() {
340        if let Some(prev) = with_host(|h| match h.get(&args[0]) {
341            Some(JsObj::Array(a)) if a.len() == 2 => Some((h.to_number(&a[0]), h.to_number(&a[1]))),
342            _ => None,
343        }) {
344            secs -= prev.0;
345            nanos -= prev.1;
346        }
347    }
348    with_host(|h| h.new_array(vec![Value::Float(secs), Value::Float(nanos)]))
349}
350
351fn memory_usage() -> Value {
352    with_host(|h| {
353        let mut m = IndexMap::new();
354        for k in ["rss", "heapTotal", "heapUsed", "external", "arrayBuffers"] {
355            m.insert(k.into(), Value::Float(0.0));
356        }
357        h.new_object(m)
358    })
359}
360
361/// The `fd` numeric property of a stream stand-in (default stdout).
362fn stream_fd(recv: &Value) -> f64 {
363    with_host(|h| match h.get(recv) {
364        Some(JsObj::Object(p)) => p.get("fd").map(|v| h.to_number(v)).unwrap_or(1.0),
365        _ => 1.0,
366    })
367}
368
369/// Write raw bytes to the process's stdout/stderr (chosen by fd).
370fn write_fd(fd: f64, bytes: &[u8]) {
371    use std::io::Write as _;
372    if fd == 2.0 {
373        let _ = std::io::stderr().write_all(bytes);
374        let _ = std::io::stderr().flush();
375    } else {
376        let _ = std::io::stdout().write_all(bytes);
377        let _ = std::io::stdout().flush();
378    }
379}
380
381/// The ANSI control sequence for a `tty.WriteStream` cursor/erase method.
382fn tty_control(method: &str, args: &[Value]) -> String {
383    match method {
384        // cursorTo(x[, y]) → absolute column (`\e[<x+1>G`) or position.
385        "cursorTo" => {
386            let x = super::arg_num(args, 0);
387            let y = super::arg_num(args, 1);
388            let x = if x.is_finite() { x as i64 } else { 0 };
389            if y.is_finite() {
390                format!("\x1b[{};{}H", y as i64 + 1, x + 1)
391            } else {
392                format!("\x1b[{}G", x + 1)
393            }
394        }
395        // moveCursor(dx, dy) → relative moves.
396        "moveCursor" => {
397            let dx = super::arg_num(args, 0);
398            let dy = super::arg_num(args, 1);
399            let mut s = String::new();
400            let dx = if dx.is_finite() { dx as i64 } else { 0 };
401            let dy = if dy.is_finite() { dy as i64 } else { 0 };
402            if dx > 0 {
403                s.push_str(&format!("\x1b[{dx}C"));
404            } else if dx < 0 {
405                s.push_str(&format!("\x1b[{}D", -dx));
406            }
407            if dy > 0 {
408                s.push_str(&format!("\x1b[{dy}B"));
409            } else if dy < 0 {
410                s.push_str(&format!("\x1b[{}A", -dy));
411            }
412            s
413        }
414        // clearLine(dir): -1 left, 1 right, 0 whole line.
415        "clearLine" => match super::arg_num(args, 0) {
416            d if d < 0.0 => "\x1b[1K".into(),
417            d if d > 0.0 => "\x1b[0K".into(),
418            _ => "\x1b[2K".into(),
419        },
420        // clearScreenDown → erase from cursor to end of screen.
421        _ => "\x1b[0J".into(),
422    }
423}
424
425/// The process's supplementary group ids (`getgroups(2)`).
426fn supplementary_groups() -> Vec<f64> {
427    // SAFETY: first call queries the count, second fills a buffer of that size.
428    unsafe {
429        let n = libc::getgroups(0, std::ptr::null_mut());
430        if n <= 0 {
431            return Vec::new();
432        }
433        let mut buf = vec![0 as libc::gid_t; n as usize];
434        let filled = libc::getgroups(n, buf.as_mut_ptr());
435        if filled < 0 {
436            return Vec::new();
437        }
438        buf.truncate(filled as usize);
439        buf.into_iter().map(|g| g as f64).collect()
440    }
441}
442
443/// Read a JS array of numbers as a gid buffer.
444fn gid_array(v: Option<&Value>) -> Vec<libc::gid_t> {
445    let Some(v) = v else { return Vec::new() };
446    with_host(|h| match h.get(v) {
447        Some(JsObj::Array(a)) => a.iter().map(|x| h.to_number(x) as libc::gid_t).collect(),
448        _ => Vec::new(),
449    })
450}
451
452/// `getrusage(RUSAGE_SELF)` — `None` if the syscall fails.
453fn get_rusage() -> Option<libc::rusage> {
454    // SAFETY: getrusage fills a zeroed rusage; RUSAGE_SELF is a valid `who`.
455    unsafe {
456        let mut ru: libc::rusage = std::mem::zeroed();
457        (libc::getrusage(libc::RUSAGE_SELF, &mut ru) == 0).then_some(ru)
458    }
459}
460
461/// microseconds from a `timeval`.
462fn tv_micros(t: &libc::timeval) -> f64 {
463    t.tv_sec as f64 * 1e6 + t.tv_usec as f64
464}
465
466/// `process.resourceUsage()` — the full `getrusage` breakdown (zeros on failure).
467fn resource_usage() -> Value {
468    let ru = get_rusage();
469    with_host(|h| {
470        let mut m = IndexMap::new();
471        let (utime, stime) = ru
472            .as_ref()
473            .map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
474            .unwrap_or((0.0, 0.0));
475        m.insert("userCPUTime".into(), Value::Float(utime));
476        m.insert("systemCPUTime".into(), Value::Float(stime));
477        let fields = [
478            ("maxRSS", ru.as_ref().map(|r| r.ru_maxrss)),
479            ("sharedMemorySize", ru.as_ref().map(|r| r.ru_ixrss)),
480            ("unsharedDataSize", ru.as_ref().map(|r| r.ru_idrss)),
481            ("unsharedStackSize", ru.as_ref().map(|r| r.ru_isrss)),
482            ("minorPageFault", ru.as_ref().map(|r| r.ru_minflt)),
483            ("majorPageFault", ru.as_ref().map(|r| r.ru_majflt)),
484            ("swappedOut", ru.as_ref().map(|r| r.ru_nswap)),
485            ("fsRead", ru.as_ref().map(|r| r.ru_inblock)),
486            ("fsWrite", ru.as_ref().map(|r| r.ru_oublock)),
487            ("ipcSent", ru.as_ref().map(|r| r.ru_msgsnd)),
488            ("ipcReceived", ru.as_ref().map(|r| r.ru_msgrcv)),
489            ("signalsCount", ru.as_ref().map(|r| r.ru_nsignals)),
490            ("voluntaryContextSwitches", ru.as_ref().map(|r| r.ru_nvcsw)),
491            (
492                "involuntaryContextSwitches",
493                ru.as_ref().map(|r| r.ru_nivcsw),
494            ),
495        ];
496        for (k, v) in fields {
497            m.insert(k.into(), Value::Float(v.unwrap_or(0) as f64));
498        }
499        h.new_object(m)
500    })
501}
502
503/// `process.threadCpuUsage()` — best-effort via process-wide `getrusage` (no
504/// per-thread accounting substrate), reported as `{user, system}` microseconds.
505fn thread_cpu_usage() -> Value {
506    let (u, s) = get_rusage()
507        .map(|r| (tv_micros(&r.ru_utime), tv_micros(&r.ru_stime)))
508        .unwrap_or((0.0, 0.0));
509    with_host(|h| {
510        let mut m = IndexMap::new();
511        m.insert("user".into(), Value::Float(u));
512        m.insert("system".into(), Value::Float(s));
513        h.new_object(m)
514    })
515}
516
517/// `process.execve(file, args[, env])` — replace the process image (never returns
518/// on success; throws the OS error otherwise).
519fn exec_ve(args: &[Value]) -> Result<Value, String> {
520    use std::ffi::CString;
521    let prog = CString::new(super::arg_str(args, 0))
522        .map_err(|_| crate::host::type_error("process.execve: invalid file path"))?;
523
524    let argv_strs: Vec<String> = with_host(|h| match args.get(1).and_then(|v| h.get(v)) {
525        Some(JsObj::Array(a)) => a.iter().map(|x| h.str_of(x)).collect(),
526        _ => Vec::new(),
527    });
528    let env_strs: Vec<String> = {
529        let from_arg = with_host(|h| match args.get(2).and_then(|v| h.get(v)) {
530            Some(JsObj::Object(p)) => Some(
531                p.iter()
532                    .map(|(k, v)| format!("{k}={}", h.str_of(v)))
533                    .collect::<Vec<_>>(),
534            ),
535            _ => None,
536        });
537        from_arg.unwrap_or_else(|| std::env::vars().map(|(k, v)| format!("{k}={v}")).collect())
538    };
539
540    let to_c = |s: String| {
541        CString::new(s).map_err(|_| crate::host::type_error("process.execve: NUL in argument"))
542    };
543    let argv_c: Vec<CString> = argv_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
544    let env_c: Vec<CString> = env_strs.into_iter().map(to_c).collect::<Result<_, _>>()?;
545
546    let mut argv_p: Vec<*const libc::c_char> = argv_c.iter().map(|c| c.as_ptr()).collect();
547    argv_p.push(std::ptr::null());
548    let mut envp_p: Vec<*const libc::c_char> = env_c.iter().map(|c| c.as_ptr()).collect();
549    envp_p.push(std::ptr::null());
550
551    // SAFETY: argv/envp are NUL-terminated arrays of valid C strings kept alive
552    // above; on success execve never returns.
553    unsafe {
554        libc::execve(prog.as_ptr(), argv_p.as_ptr(), envp_p.as_ptr());
555    }
556    Err(crate::host::type_error(&format!(
557        "process.execve failed: {}",
558        std::io::Error::last_os_error()
559    )))
560}
561
562/// `process.loadEnvFile([path])` — parse a `.env` file into `process.env`
563/// (persisted through the real environment so a later `process.env` read sees it).
564fn load_env_file(path: &str) -> Result<Value, String> {
565    let path = if path.is_empty() { ".env" } else { path };
566    let text =
567        std::fs::read_to_string(path).map_err(|e| format!("Error: ENOENT: {e}, open '{path}'"))?;
568    for line in text.lines() {
569        let line = line.trim();
570        if line.is_empty() || line.starts_with('#') {
571            continue;
572        }
573        let line = line.strip_prefix("export ").unwrap_or(line);
574        let Some((key, val)) = line.split_once('=') else {
575            continue;
576        };
577        let key = key.trim();
578        if key.is_empty() {
579            continue;
580        }
581        let mut val = val.trim();
582        if val.len() >= 2
583            && ((val.starts_with('"') && val.ends_with('"'))
584                || (val.starts_with('\'') && val.ends_with('\'')))
585        {
586            val = &val[1..val.len() - 1];
587        }
588        std::env::set_var(key, val);
589    }
590    Ok(Value::Undef)
591}