use std::collections::BTreeMap;
use serde_json::Value;
use crate::{LocalizedString, constants::CAP_FILESYSTEM};
pub type Constraint = Value;
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct CapabilityRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<LocalizedString>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub params: BTreeMap<String, Value>,
#[serde(default, alias = "allow", skip_serializing_if = "Vec::is_empty")]
pub constraints: Vec<Constraint>,
}
impl CapabilityRequest {
pub fn constraints_as<T: serde::de::DeserializeOwned>(
&self,
) -> Result<Vec<T>, serde_json::Error> {
self.constraints
.iter()
.map(|c| serde_json::from_value::<T>(c.clone()))
.collect()
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Capabilities(pub BTreeMap<String, CapabilityRequest>);
impl Capabilities {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn has(&self, id: &str) -> bool {
self.0.contains_key(id)
}
pub fn get(&self, id: &str) -> Option<&CapabilityRequest> {
self.0.get(id)
}
pub fn fs_mount_root(&self) -> Option<&str> {
self.0
.get(CAP_FILESYSTEM)?
.params
.get("mount-root")?
.as_str()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &CapabilityRequest)> {
self.0.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_serde_skips_empty_and_aliases_allow() {
let req = CapabilityRequest {
constraints: vec![serde_json::json!({ "host": "*" })],
..Default::default()
};
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v, serde_json::json!({ "constraints": [{ "host": "*" }] }));
let from_allow: CapabilityRequest =
serde_json::from_value(serde_json::json!({ "allow": [{ "host": "x" }] })).unwrap();
assert_eq!(
from_allow.constraints,
vec![serde_json::json!({ "host": "x" })]
);
}
#[test]
fn constraints_as_parses_typed() {
use crate::FilesystemAllow;
let req = CapabilityRequest {
constraints: vec![serde_json::json!({ "path": "/x/**", "mode": "rw" })],
..Default::default()
};
let parsed = req.constraints_as::<FilesystemAllow>().unwrap();
assert_eq!(parsed.len(), 1);
assert_eq!(parsed[0].path, "/x/**");
}
#[test]
fn description_round_trips_as_bare_string() {
let req: CapabilityRequest =
serde_json::from_value(serde_json::json!({ "description": "hello" })).unwrap();
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v, serde_json::json!({ "description": "hello" }));
}
#[test]
fn capabilities_cbor_is_map_keyed_by_id() {
use crate::cbor;
let mut caps = Capabilities::default();
caps.0.insert(
"wasi:filesystem".into(),
CapabilityRequest {
constraints: vec![serde_json::json!({ "path": "/data/**", "mode": "rw" })],
..Default::default()
},
);
let bytes = cbor::to_cbor(&caps);
let back: Capabilities = cbor::from_cbor(&bytes).unwrap();
assert!(back.has("wasi:filesystem"));
assert_eq!(
back.get("wasi:filesystem").unwrap().constraints,
vec![serde_json::json!({ "path": "/data/**", "mode": "rw" })]
);
}
}