smolvm_protocol/
publish_socket.rs1use serde::{Deserialize, Serialize};
10use std::str::FromStr;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum SocketDirection {
16 Expose,
19 Mount,
23}
24
25impl SocketDirection {
26 pub fn as_str(&self) -> &'static str {
28 match self {
29 SocketDirection::Expose => "expose",
30 SocketDirection::Mount => "mount",
31 }
32 }
33
34 pub fn host_listens(&self) -> bool {
41 matches!(self, SocketDirection::Expose)
42 }
43}
44
45impl FromStr for SocketDirection {
46 type Err = String;
47 fn from_str(s: &str) -> Result<Self, Self::Err> {
48 match s.to_ascii_lowercase().as_str() {
49 "expose" => Ok(SocketDirection::Expose),
50 "mount" => Ok(SocketDirection::Mount),
51 other => Err(format!(
52 "invalid socket direction '{other}' (expected 'expose' or 'mount')"
53 )),
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct PublishedSocket {
65 pub vsock_port: u32,
67 pub guest_path: String,
70 pub direction: SocketDirection,
72}
73
74pub fn encode(sockets: &[PublishedSocket]) -> String {
79 sockets
80 .iter()
81 .map(|s| format!("{}|{}|{}", s.vsock_port, s.direction.as_str(), s.guest_path))
82 .collect::<Vec<_>>()
83 .join(";")
84}
85
86pub fn decode(encoded: &str) -> Vec<PublishedSocket> {
90 encoded
91 .split(';')
92 .filter(|e| !e.is_empty())
93 .filter_map(|entry| {
94 let mut parts = entry.splitn(3, '|');
95 let vsock_port = parts.next()?.parse::<u32>().ok()?;
96 let direction = parts.next()?.parse::<SocketDirection>().ok()?;
97 let guest_path = parts.next()?.to_string();
98 if guest_path.is_empty() {
99 return None;
100 }
101 Some(PublishedSocket {
102 vsock_port,
103 guest_path,
104 direction,
105 })
106 })
107 .collect()
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn direction_roundtrips_and_maps_to_listen_flag() {
116 assert_eq!(
117 "expose".parse::<SocketDirection>().unwrap(),
118 SocketDirection::Expose
119 );
120 assert_eq!(
121 "MOUNT".parse::<SocketDirection>().unwrap(),
122 SocketDirection::Mount
123 );
124 assert!("bogus".parse::<SocketDirection>().is_err());
125 assert!(SocketDirection::Expose.host_listens());
126 assert!(!SocketDirection::Mount.host_listens());
127 }
128
129 #[test]
130 fn encode_decode_roundtrip() {
131 let socks = vec![
132 PublishedSocket {
133 vsock_port: 6100,
134 guest_path: "/var/run/app.sock".into(),
135 direction: SocketDirection::Expose,
136 },
137 PublishedSocket {
138 vsock_port: 6101,
139 guest_path: "/tmp/host.sock".into(),
140 direction: SocketDirection::Mount,
141 },
142 ];
143 let encoded = encode(&socks);
144 assert_eq!(
145 encoded,
146 "6100|expose|/var/run/app.sock;6101|mount|/tmp/host.sock"
147 );
148 assert_eq!(decode(&encoded), socks);
149 }
150
151 #[test]
152 fn decode_skips_malformed_entries() {
153 assert!(decode("").is_empty());
155 let mixed = "6100|expose|/ok.sock;garbage;9|nope|/x.sock;abc|expose|/y.sock;6102|mount|";
156 let out = decode(mixed);
157 assert_eq!(out.len(), 1);
158 assert_eq!(out[0].guest_path, "/ok.sock");
159 }
160}