Skip to main content

shape_runtime/stdlib/
env.rs

1//! Native `env` module for environment variable and system info access.
2//!
3//! Exports: env.get, env.has, env.all, env.args, env.cwd, env.os, env.arch
4//!
5//! Policy gated: requires Env permission at runtime.
6
7use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
8use shape_value::ValueWord;
9use std::sync::Arc;
10
11/// Create the `env` module with environment variable and system info functions.
12pub fn create_env_module() -> ModuleExports {
13    let mut module = ModuleExports::new("std::core::env");
14    module.description = "Environment variables and system information".to_string();
15
16    // env.get(name: string) -> Option<string>
17    module.add_function_with_schema(
18        "get",
19        |args: &[ValueWord], ctx: &ModuleContext| {
20            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Env)?;
21            let name = args
22                .first()
23                .and_then(|a| a.as_str())
24                .ok_or_else(|| "env.get() requires a variable name string".to_string())?;
25
26            match std::env::var(name) {
27                Ok(val) => Ok(ValueWord::from_some(ValueWord::from_string(Arc::new(val)))),
28                Err(_) => Ok(ValueWord::none()),
29            }
30        },
31        ModuleFunction {
32            description: "Get the value of an environment variable, or none if not set".to_string(),
33            params: vec![ModuleParam {
34                name: "name".to_string(),
35                type_name: "string".to_string(),
36                required: true,
37                description: "Environment variable name".to_string(),
38                ..Default::default()
39            }],
40            return_type: Some("Option<string>".to_string()),
41        },
42    );
43
44    // env.has(name: string) -> bool
45    module.add_function_with_schema(
46        "has",
47        |args: &[ValueWord], ctx: &ModuleContext| {
48            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Env)?;
49            let name = args
50                .first()
51                .and_then(|a| a.as_str())
52                .ok_or_else(|| "env.has() requires a variable name string".to_string())?;
53
54            Ok(ValueWord::from_bool(std::env::var(name).is_ok()))
55        },
56        ModuleFunction {
57            description: "Check if an environment variable is set".to_string(),
58            params: vec![ModuleParam {
59                name: "name".to_string(),
60                type_name: "string".to_string(),
61                required: true,
62                description: "Environment variable name".to_string(),
63                ..Default::default()
64            }],
65            return_type: Some("bool".to_string()),
66        },
67    );
68
69    // env.all() -> HashMap<string, string>
70    module.add_function_with_schema(
71        "all",
72        |_args: &[ValueWord], ctx: &ModuleContext| {
73            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Env)?;
74            let vars: Vec<(String, String)> = std::env::vars().collect();
75            let mut keys = Vec::with_capacity(vars.len());
76            let mut values = Vec::with_capacity(vars.len());
77
78            for (k, v) in vars.into_iter() {
79                keys.push(ValueWord::from_string(Arc::new(k)));
80                values.push(ValueWord::from_string(Arc::new(v)));
81            }
82
83            Ok(ValueWord::from_hashmap_pairs(keys, values))
84        },
85        ModuleFunction {
86            description: "Get all environment variables as a HashMap".to_string(),
87            params: vec![],
88            return_type: Some("HashMap<string, string>".to_string()),
89        },
90    );
91
92    // env.args() -> Array<string>
93    module.add_function_with_schema(
94        "args",
95        |_args: &[ValueWord], ctx: &ModuleContext| {
96            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Env)?;
97            let args: Vec<ValueWord> = std::env::args()
98                .map(|a| ValueWord::from_string(Arc::new(a)))
99                .collect();
100            Ok(ValueWord::from_array(Arc::new(args)))
101        },
102        ModuleFunction {
103            description: "Get command-line arguments as an array of strings".to_string(),
104            params: vec![],
105            return_type: Some("Array<string>".to_string()),
106        },
107    );
108
109    // env.cwd() -> string
110    module.add_function_with_schema(
111        "cwd",
112        |_args: &[ValueWord], ctx: &ModuleContext| {
113            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Env)?;
114            let cwd = std::env::current_dir().map_err(|e| format!("env.cwd() failed: {}", e))?;
115            let path_str = cwd.to_string_lossy().into_owned();
116            Ok(ValueWord::from_string(Arc::new(path_str)))
117        },
118        ModuleFunction {
119            description: "Get the current working directory".to_string(),
120            params: vec![],
121            return_type: Some("string".to_string()),
122        },
123    );
124
125    // env.os() -> string
126    module.add_function_with_schema(
127        "os",
128        |_args: &[ValueWord], ctx: &ModuleContext| {
129            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Env)?;
130            Ok(ValueWord::from_string(Arc::new(
131                std::env::consts::OS.to_string(),
132            )))
133        },
134        ModuleFunction {
135            description: "Get the operating system name (e.g. linux, macos, windows)".to_string(),
136            params: vec![],
137            return_type: Some("string".to_string()),
138        },
139    );
140
141    // env.arch() -> string
142    module.add_function_with_schema(
143        "arch",
144        |_args: &[ValueWord], ctx: &ModuleContext| {
145            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::Env)?;
146            Ok(ValueWord::from_string(Arc::new(
147                std::env::consts::ARCH.to_string(),
148            )))
149        },
150        ModuleFunction {
151            description: "Get the CPU architecture (e.g. x86_64, aarch64)".to_string(),
152            params: vec![],
153            return_type: Some("string".to_string()),
154        },
155    );
156
157    module
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    fn s(val: &str) -> ValueWord {
165        ValueWord::from_string(Arc::new(val.to_string()))
166    }
167
168    fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
169        let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
170        crate::module_exports::ModuleContext {
171            schemas: registry,
172            invoke_callable: None,
173            raw_invoker: None,
174            function_hashes: None,
175            vm_state: None,
176            granted_permissions: None,
177            scope_constraints: None,
178            set_pending_resume: None,
179            set_pending_frame_resume: None,
180        }
181    }
182
183    #[test]
184    fn test_env_module_creation() {
185        let module = create_env_module();
186        assert_eq!(module.name, "std::core::env");
187        assert!(module.has_export("get"));
188        assert!(module.has_export("has"));
189        assert!(module.has_export("all"));
190        assert!(module.has_export("args"));
191        assert!(module.has_export("cwd"));
192        assert!(module.has_export("os"));
193        assert!(module.has_export("arch"));
194    }
195
196    #[test]
197    fn test_env_get_path() {
198        let module = create_env_module();
199        let ctx = test_ctx();
200        let f = module.get_export("get").unwrap();
201        // PATH should always be set
202        let result = f(&[s("PATH")], &ctx).unwrap();
203        let inner = result.as_some_inner().expect("PATH should be set");
204        assert!(!inner.as_str().unwrap().is_empty());
205    }
206
207    #[test]
208    fn test_env_get_missing() {
209        let module = create_env_module();
210        let ctx = test_ctx();
211        let f = module.get_export("get").unwrap();
212        let result = f(&[s("__SHAPE_NONEXISTENT_VAR_12345__")], &ctx).unwrap();
213        assert!(result.is_none());
214    }
215
216    #[test]
217    fn test_env_get_requires_string() {
218        let module = create_env_module();
219        let ctx = test_ctx();
220        let f = module.get_export("get").unwrap();
221        assert!(f(&[ValueWord::from_f64(42.0)], &ctx).is_err());
222    }
223
224    #[test]
225    fn test_env_has_path() {
226        let module = create_env_module();
227        let ctx = test_ctx();
228        let f = module.get_export("has").unwrap();
229        let result = f(&[s("PATH")], &ctx).unwrap();
230        assert_eq!(result.as_bool(), Some(true));
231    }
232
233    #[test]
234    fn test_env_has_missing() {
235        let module = create_env_module();
236        let ctx = test_ctx();
237        let f = module.get_export("has").unwrap();
238        let result = f(&[s("__SHAPE_NONEXISTENT_VAR_12345__")], &ctx).unwrap();
239        assert_eq!(result.as_bool(), Some(false));
240    }
241
242    #[test]
243    fn test_env_all_returns_hashmap() {
244        let module = create_env_module();
245        let ctx = test_ctx();
246        let f = module.get_export("all").unwrap();
247        let result = f(&[], &ctx).unwrap();
248        let (keys, _values, _index) = result.as_hashmap().expect("should be hashmap");
249        // Should have at least PATH
250        assert!(!keys.is_empty());
251    }
252
253    #[test]
254    fn test_env_args_returns_array() {
255        let module = create_env_module();
256        let ctx = test_ctx();
257        let f = module.get_export("args").unwrap();
258        let result = f(&[], &ctx).unwrap();
259        let arr = result.as_any_array().expect("should be array").to_generic();
260        // At least the binary name
261        assert!(!arr.is_empty());
262    }
263
264    #[test]
265    fn test_env_cwd_returns_string() {
266        let module = create_env_module();
267        let ctx = test_ctx();
268        let f = module.get_export("cwd").unwrap();
269        let result = f(&[], &ctx).unwrap();
270        let cwd = result.as_str().expect("should be string");
271        assert!(!cwd.is_empty());
272    }
273
274    #[test]
275    fn test_env_os_returns_string() {
276        let module = create_env_module();
277        let ctx = test_ctx();
278        let f = module.get_export("os").unwrap();
279        let result = f(&[], &ctx).unwrap();
280        let os = result.as_str().expect("should be string");
281        assert!(!os.is_empty());
282        // Should be one of the known OS values
283        assert!(
284            ["linux", "macos", "windows", "freebsd", "android", "ios"].contains(&os),
285            "unexpected OS: {}",
286            os
287        );
288    }
289
290    #[test]
291    fn test_env_arch_returns_string() {
292        let module = create_env_module();
293        let ctx = test_ctx();
294        let f = module.get_export("arch").unwrap();
295        let result = f(&[], &ctx).unwrap();
296        let arch = result.as_str().expect("should be string");
297        assert!(!arch.is_empty());
298    }
299
300    #[test]
301    fn test_env_schemas() {
302        let module = create_env_module();
303
304        let get_schema = module.get_schema("get").unwrap();
305        assert_eq!(get_schema.params.len(), 1);
306        assert_eq!(get_schema.return_type.as_deref(), Some("Option<string>"));
307
308        let all_schema = module.get_schema("all").unwrap();
309        assert_eq!(all_schema.params.len(), 0);
310
311        let os_schema = module.get_schema("os").unwrap();
312        assert_eq!(os_schema.return_type.as_deref(), Some("string"));
313    }
314}