#![allow(clippy::missing_errors_doc)]
use core::ffi::c_void;
use windows::core::{GUID, HSTRING, PWSTR};
use windows::Win32::System::HostComputeNetwork::{
HcnCreateNamespace, HcnDeleteNamespace, HcnEnumerateNamespaces, HcnModifyNamespace,
HcnOpenNamespace, HcnQueryNamespaceProperties,
};
use crate::error::{HnsError, HnsResult};
use crate::handle::{HcnNamespaceHandle, OwnedNamespace};
use crate::schema::{
HostComputeNamespace, ModifyNamespaceSettingRequest, ModifyRequestType, NamespaceType,
SchemaVersion,
};
#[derive(Debug)]
pub struct Namespace {
id: GUID,
handle: OwnedNamespace,
}
impl Namespace {
pub fn create_host_default() -> HnsResult<Self> {
let id = GUID::new().map_err(|e| HnsError::Other {
hresult: e.code().0,
message: format!("GUID::new failed: {e}"),
})?;
let spec = HostComputeNamespace {
ty: NamespaceType::Host,
schema_version: SchemaVersion::default(),
..HostComputeNamespace::default()
};
Self::create(id, &spec)
}
pub fn create(id: GUID, spec: &HostComputeNamespace) -> HnsResult<Self> {
let settings_json = serde_json::to_string(spec)?;
let settings_hstring = HSTRING::from(settings_json);
let mut raw: *mut c_void = core::ptr::null_mut();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnCreateNamespace(&id, &settings_hstring, &mut raw, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnCreateNamespace"))?;
}
if raw.is_null() {
return Err(HnsError::Other {
hresult: 0,
message: "HcnCreateNamespace returned null handle".to_string(),
});
}
let handle = unsafe { OwnedNamespace::from_raw(raw as HcnNamespaceHandle) };
let real_id = query_handle_id(handle.as_raw()).unwrap_or(id);
let ns = Self {
id: real_id,
handle,
};
ns.reconcile_orphan_endpoints();
Ok(ns)
}
fn reconcile_orphan_endpoints(&self) {
let Some(json) = query_handle_raw(self.handle.as_raw(), "{}") else {
return;
};
let live: std::collections::HashSet<String> = crate::endpoint::list("{}")
.ok()
.unwrap_or_default()
.into_iter()
.map(|g| {
format!("{g:?}")
.trim_matches(|c: char| c == '{' || c == '}')
.to_ascii_lowercase()
})
.collect();
let Ok(v) = serde_json::from_str::<serde_json::Value>(&json) else {
return;
};
let Some(resources) = v.get("ResourceList").and_then(|r| r.as_array()) else {
return;
};
for r in resources {
if r.get("Type").and_then(|t| t.as_str()) != Some("Endpoint") {
continue;
}
let Some(id_str) = r
.get("Data")
.and_then(|d| d.get("Id"))
.and_then(|s| s.as_str())
else {
continue;
};
let id_norm = id_str
.trim_matches(|c: char| c == '{' || c == '}')
.to_ascii_lowercase();
if live.contains(&id_norm) {
continue;
}
let body = format!(
r#"{{"ResourceType":"Endpoint","RequestType":"Remove","Settings":{{"EndpointId":"{id_norm}"}}}}"#
);
if let Err(e) = self.modify_json(&body) {
tracing::warn!(
endpoint = %id_norm,
error = %e,
"reconcile: failed to remove orphan endpoint ref from default namespace",
);
}
}
}
pub fn open(id: GUID) -> HnsResult<Self> {
let mut raw: *mut c_void = core::ptr::null_mut();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnOpenNamespace(&id, &mut raw, Some(&mut err_record)).map_err(|e| {
classify_error(e.code(), err_record, format!("HcnOpenNamespace({id:?})"))
})?;
}
if raw.is_null() {
return Err(HnsError::NotFound {
id: format!("{id:?}"),
});
}
let handle = unsafe { OwnedNamespace::from_raw(raw as HcnNamespaceHandle) };
Ok(Self { id, handle })
}
pub fn delete(id: GUID) -> HnsResult<()> {
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnDeleteNamespace(&id, Some(&mut err_record)).map_err(|e| {
classify_error(e.code(), err_record, format!("HcnDeleteNamespace({id:?})"))
})?;
}
Ok(())
}
pub fn add_endpoint(&self, endpoint_id: GUID) -> HnsResult<()> {
let req = ModifyNamespaceSettingRequest {
resource_type: "Endpoint".to_string(),
request_type: ModifyRequestType::Add,
settings: serde_json::json!({ "EndpointId": format_endpoint_id(endpoint_id) }),
};
self.modify_json(&serde_json::to_string(&req)?)
}
pub fn remove_endpoint(&self, endpoint_id: GUID) -> HnsResult<()> {
let req = ModifyNamespaceSettingRequest {
resource_type: "Endpoint".to_string(),
request_type: ModifyRequestType::Remove,
settings: serde_json::json!({ "EndpointId": format_endpoint_id(endpoint_id) }),
};
self.modify_json(&serde_json::to_string(&req)?)
}
pub fn modify_json(&self, modification_json: &str) -> HnsResult<()> {
let mod_hstring = HSTRING::from(modification_json);
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnModifyNamespace(self.handle.as_raw(), &mod_hstring, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnModifyNamespace"))?;
}
Ok(())
}
pub fn query(&self, query_json: &str) -> HnsResult<HostComputeNamespace> {
let query_hstring = HSTRING::from(query_json);
let mut out_properties: PWSTR = PWSTR::null();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnQueryNamespaceProperties(
self.handle.as_raw(),
&query_hstring,
&mut out_properties,
Some(&mut err_record),
)
.map_err(|e| classify_error(e.code(), err_record, "HcnQueryNamespaceProperties"))?;
}
let json = decode_pwstr(out_properties);
let parsed: HostComputeNamespace = serde_json::from_str(&json)?;
Ok(parsed)
}
pub fn list_endpoints(&self) -> HnsResult<Vec<String>> {
let props = self.query("{}")?;
Ok(props
.resources
.iter()
.filter(|r| r.ty == "Endpoint")
.map(|r| r.id.clone())
.collect())
}
#[must_use]
pub fn id(&self) -> GUID {
self.id
}
#[must_use]
pub fn handle(&self) -> &OwnedNamespace {
&self.handle
}
}
pub fn list(query_json: &str) -> HnsResult<Vec<GUID>> {
let query_hstring = HSTRING::from(query_json);
let mut out_namespaces: PWSTR = PWSTR::null();
let mut err_record: PWSTR = PWSTR::null();
unsafe {
HcnEnumerateNamespaces(&query_hstring, &mut out_namespaces, Some(&mut err_record))
.map_err(|e| classify_error(e.code(), err_record, "HcnEnumerateNamespaces"))?;
}
let json = decode_pwstr(out_namespaces);
if json.is_empty() {
return Ok(Vec::new());
}
let arr: Vec<String> = serde_json::from_str(&json).unwrap_or_default();
let mut guids = Vec::with_capacity(arr.len());
for s in arr {
let bare = s.trim_matches(|c: char| c == '{' || c == '}');
let guid = GUID::try_from(bare).map_err(|e| HnsError::Other {
hresult: 0,
message: format!("bad GUID from HcnEnumerateNamespaces: {s}: {e}"),
})?;
guids.push(guid);
}
Ok(guids)
}
fn format_endpoint_id(id: GUID) -> String {
format!("{id:?}")
.trim_matches(|c: char| c == '{' || c == '}')
.to_ascii_lowercase()
}
fn query_handle_raw(handle: HcnNamespaceHandle, query_json: &str) -> Option<String> {
let query_hstring = HSTRING::from(query_json);
let mut out_properties: PWSTR = PWSTR::null();
let mut err_record: PWSTR = PWSTR::null();
let hr = unsafe {
HcnQueryNamespaceProperties(
handle,
&query_hstring,
&mut out_properties,
Some(&mut err_record),
)
};
if hr.is_err() {
return None;
}
Some(decode_pwstr(out_properties))
}
fn query_handle_id(handle: HcnNamespaceHandle) -> Option<GUID> {
let json = query_handle_raw(handle, "{}")?;
let v: serde_json::Value = serde_json::from_str(&json).ok()?;
let id_str = v.get("ID").and_then(|s| s.as_str())?;
GUID::try_from(id_str)
.or_else(|_| GUID::try_from(format!("{{{id_str}}}").as_str()))
.ok()
}
fn decode_pwstr(p: PWSTR) -> String {
use windows::Win32::Foundation::{LocalFree, HLOCAL};
if p.is_null() {
return String::new();
}
let s = unsafe { p.to_string().unwrap_or_default() };
unsafe {
let _ = LocalFree(Some(HLOCAL(p.0.cast())));
}
s
}
fn classify_error<S: Into<String>>(
hr: windows::core::HRESULT,
err_record: PWSTR,
context: S,
) -> HnsError {
let ctx: String = context.into();
let decoded = decode_pwstr(err_record);
let msg = if decoded.is_empty() {
ctx
} else {
format!("{ctx}: {decoded}")
};
HnsError::from_hresult(hr, msg)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::ModifyRequestType;
#[test]
fn decode_pwstr_null_returns_empty() {
let s = decode_pwstr(PWSTR::null());
assert!(s.is_empty());
}
#[test]
fn classify_error_access_denied_hresult() {
let err = classify_error(windows::core::HRESULT(-0x7FFF_FFFB), PWSTR::null(), "ctx");
assert!(matches!(err, HnsError::AccessDenied { .. }));
}
#[test]
fn classify_error_preserves_context_when_errrecord_empty() {
let err = classify_error(
windows::core::HRESULT(-0x1234_5678),
PWSTR::null(),
"HcnCreateNamespace",
);
if let HnsError::Other { message, .. } = err {
assert_eq!(message, "HcnCreateNamespace");
} else {
panic!("expected Other, got {err:?}");
}
}
#[test]
fn add_endpoint_request_serialises_with_resource_type_1() {
let endpoint_id = GUID::from_u128(0x1234_5678_9abc_def0_1122_3344_5566_7788);
let req = ModifyNamespaceSettingRequest {
resource_type: "Endpoint".to_string(),
request_type: ModifyRequestType::Add,
settings: serde_json::json!({
"EndpointId": format_endpoint_id(endpoint_id)
}),
};
let v: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(v["ResourceType"], serde_json::json!("Endpoint"));
assert_eq!(v["RequestType"], serde_json::json!("Add"));
let ep = v["Settings"]["EndpointId"].as_str().unwrap();
assert!(
ep.contains("12345678"),
"EndpointId should contain GUID data: {v}"
);
assert!(
!ep.contains('{') && !ep.contains('}'),
"EndpointId must be un-braced for HcnModifyNamespace: {ep}"
);
assert_eq!(ep, ep.to_ascii_lowercase(), "EndpointId must be lowercase");
}
#[test]
fn remove_endpoint_request_uses_remove_verb() {
let endpoint_id = GUID::zeroed();
let req = ModifyNamespaceSettingRequest {
resource_type: "Endpoint".to_string(),
request_type: ModifyRequestType::Remove,
settings: serde_json::json!({
"EndpointId": format_endpoint_id(endpoint_id)
}),
};
let v: serde_json::Value = serde_json::to_value(&req).unwrap();
assert_eq!(v["RequestType"], serde_json::json!("Remove"));
assert_eq!(v["ResourceType"], serde_json::json!("Endpoint"));
}
}