1use std::collections::BTreeMap;
9
10use serde_json::Value;
11
12use crate::{LocalizedString, constants::CAP_FILESYSTEM};
13
14pub type Constraint = Value;
19
20#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
23pub struct CapabilityRequest {
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub description: Option<LocalizedString>,
27 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
29 pub params: BTreeMap<String, Value>,
30 #[serde(default, alias = "allow", skip_serializing_if = "Vec::is_empty")]
33 pub constraints: Vec<Constraint>,
34}
35
36impl CapabilityRequest {
37 pub fn constraints_as<T: serde::de::DeserializeOwned>(
40 &self,
41 ) -> Result<Vec<T>, serde_json::Error> {
42 self.constraints
43 .iter()
44 .map(|c| serde_json::from_value::<T>(c.clone()))
45 .collect()
46 }
47}
48
49#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
52#[serde(transparent)]
53pub struct Capabilities(pub BTreeMap<String, CapabilityRequest>);
54
55impl Capabilities {
56 pub fn is_empty(&self) -> bool {
58 self.0.is_empty()
59 }
60
61 pub fn has(&self, id: &str) -> bool {
63 self.0.contains_key(id)
64 }
65
66 pub fn get(&self, id: &str) -> Option<&CapabilityRequest> {
68 self.0.get(id)
69 }
70
71 pub fn fs_mount_root(&self) -> Option<&str> {
73 self.0
74 .get(CAP_FILESYSTEM)?
75 .params
76 .get("mount-root")?
77 .as_str()
78 }
79
80 pub fn fs_mounts(&self) -> Result<Vec<crate::FilesystemMount>, serde_json::Error> {
83 match self
84 .0
85 .get(CAP_FILESYSTEM)
86 .and_then(|r| r.params.get("mounts"))
87 {
88 Some(v) => serde_json::from_value(v.clone()),
89 None => Ok(Vec::new()),
90 }
91 }
92
93 pub fn iter(&self) -> impl Iterator<Item = (&String, &CapabilityRequest)> {
95 self.0.iter()
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn request_serde_skips_empty_and_aliases_allow() {
105 let req = CapabilityRequest {
106 constraints: vec![serde_json::json!({ "host": "*" })],
107 ..Default::default()
108 };
109 let v = serde_json::to_value(&req).unwrap();
110 assert_eq!(v, serde_json::json!({ "constraints": [{ "host": "*" }] }));
111
112 let from_allow: CapabilityRequest =
114 serde_json::from_value(serde_json::json!({ "allow": [{ "host": "x" }] })).unwrap();
115 assert_eq!(
116 from_allow.constraints,
117 vec![serde_json::json!({ "host": "x" })]
118 );
119 }
120
121 #[test]
122 fn constraints_as_parses_typed() {
123 use crate::FilesystemAllow;
124 let req = CapabilityRequest {
125 constraints: vec![serde_json::json!({ "path": "/x/**", "mode": "rw" })],
126 ..Default::default()
127 };
128 let parsed = req.constraints_as::<FilesystemAllow>().unwrap();
129 assert_eq!(parsed.len(), 1);
130 assert_eq!(parsed[0].path, "/x/**");
131 }
132
133 #[test]
134 fn description_round_trips_as_bare_string() {
135 let req: CapabilityRequest =
138 serde_json::from_value(serde_json::json!({ "description": "hello" })).unwrap();
139 let v = serde_json::to_value(&req).unwrap();
140 assert_eq!(v, serde_json::json!({ "description": "hello" }));
141 }
142
143 #[test]
144 fn fs_mounts_parses_params_mounts() {
145 use crate::MountType;
146 let mut caps = Capabilities::default();
147 caps.0.insert(
148 "wasi:filesystem".into(),
149 CapabilityRequest {
150 params: {
151 let mut p = BTreeMap::new();
152 p.insert(
153 "mounts".to_string(),
154 serde_json::json!([{ "guest": "/ows", "host": "~/.ows" }]),
155 );
156 p
157 },
158 ..Default::default()
159 },
160 );
161 let mounts = caps.fs_mounts().unwrap();
162 assert_eq!(mounts.len(), 1);
163 assert_eq!(mounts[0].kind, MountType::Bind);
164 assert_eq!(mounts[0].guest.as_deref(), Some("/ows"));
165 }
166
167 #[test]
168 fn fs_mounts_absent_is_empty() {
169 let caps = Capabilities::default();
170 assert!(caps.fs_mounts().unwrap().is_empty());
171 }
172
173 #[test]
174 fn capabilities_cbor_is_map_keyed_by_id() {
175 use crate::cbor;
176 let mut caps = Capabilities::default();
177 caps.0.insert(
178 "wasi:filesystem".into(),
179 CapabilityRequest {
180 constraints: vec![serde_json::json!({ "path": "/data/**", "mode": "rw" })],
181 ..Default::default()
182 },
183 );
184
185 let bytes = cbor::to_cbor(&caps);
186 let back: Capabilities = cbor::from_cbor(&bytes).unwrap();
187
188 assert!(back.has("wasi:filesystem"));
189 assert_eq!(
190 back.get("wasi:filesystem").unwrap().constraints,
191 vec![serde_json::json!({ "path": "/data/**", "mode": "rw" })]
192 );
193 }
194}