use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SocketDirection {
Expose,
Mount,
}
impl SocketDirection {
pub fn as_str(&self) -> &'static str {
match self {
SocketDirection::Expose => "expose",
SocketDirection::Mount => "mount",
}
}
pub fn host_listens(&self) -> bool {
matches!(self, SocketDirection::Expose)
}
}
impl FromStr for SocketDirection {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"expose" => Ok(SocketDirection::Expose),
"mount" => Ok(SocketDirection::Mount),
other => Err(format!(
"invalid socket direction '{other}' (expected 'expose' or 'mount')"
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublishedSocket {
pub vsock_port: u32,
pub guest_path: String,
pub direction: SocketDirection,
}
pub fn encode(sockets: &[PublishedSocket]) -> String {
sockets
.iter()
.map(|s| format!("{}|{}|{}", s.vsock_port, s.direction.as_str(), s.guest_path))
.collect::<Vec<_>>()
.join(";")
}
pub fn decode(encoded: &str) -> Vec<PublishedSocket> {
encoded
.split(';')
.filter(|e| !e.is_empty())
.filter_map(|entry| {
let mut parts = entry.splitn(3, '|');
let vsock_port = parts.next()?.parse::<u32>().ok()?;
let direction = parts.next()?.parse::<SocketDirection>().ok()?;
let guest_path = parts.next()?.to_string();
if guest_path.is_empty() {
return None;
}
Some(PublishedSocket {
vsock_port,
guest_path,
direction,
})
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn direction_roundtrips_and_maps_to_listen_flag() {
assert_eq!(
"expose".parse::<SocketDirection>().unwrap(),
SocketDirection::Expose
);
assert_eq!(
"MOUNT".parse::<SocketDirection>().unwrap(),
SocketDirection::Mount
);
assert!("bogus".parse::<SocketDirection>().is_err());
assert!(SocketDirection::Expose.host_listens());
assert!(!SocketDirection::Mount.host_listens());
}
#[test]
fn encode_decode_roundtrip() {
let socks = vec![
PublishedSocket {
vsock_port: 6100,
guest_path: "/var/run/app.sock".into(),
direction: SocketDirection::Expose,
},
PublishedSocket {
vsock_port: 6101,
guest_path: "/tmp/host.sock".into(),
direction: SocketDirection::Mount,
},
];
let encoded = encode(&socks);
assert_eq!(
encoded,
"6100|expose|/var/run/app.sock;6101|mount|/tmp/host.sock"
);
assert_eq!(decode(&encoded), socks);
}
#[test]
fn decode_skips_malformed_entries() {
assert!(decode("").is_empty());
let mixed = "6100|expose|/ok.sock;garbage;9|nope|/x.sock;abc|expose|/y.sock;6102|mount|";
let out = decode(mixed);
assert_eq!(out.len(), 1);
assert_eq!(out[0].guest_path, "/ok.sock");
}
}