Skip to main content

shape_runtime/stdlib_io/
network_ops.rs

1//! Network operation implementations for the io module.
2//!
3//! Phase 2d migration: ported to the typed marshal layer (cluster #2
4//! option γ for IoHandle-touching functions). Mirrors the shape used by
5//! `file_ops.rs::register_file_io_handle_ops` — `Arc<IoHandleData>`
6//! parameters via `FromSlot`, `register_typed_fn_N` / `_N_full` for
7//! optional `n`, returns built from `ConcreteReturn::IoHandle` /
8//! `String` / `I64` / `Bool` and `TypedReturn::TypedObject` for
9//! `udp_recv`'s `{data, addr}` shape.
10//!
11//! TCP: tcp_connect, tcp_listen, tcp_accept, tcp_read, tcp_write, tcp_close
12//! UDP: udp_bind, udp_send, udp_recv
13//!
14//! All operations use blocking std::net (not tokio).
15
16use crate::marshal::{
17    register_typed_fn_1, register_typed_fn_2, register_typed_fn_2_full, register_typed_fn_3,
18};
19use crate::module_exports::{ModuleExports, ModuleParam};
20use crate::typed_module_exports::{ConcreteReturn, ConcreteType, TypedReturn};
21use shape_value::heap_value::{IoHandleData, IoResource};
22use std::io::{Read, Write};
23use std::sync::Arc;
24
25/// Register the 9 network IO functions on the io module.
26/// Cluster #2 (option γ) per docs/defections.md 2026-05-06.
27pub fn register_network_io(module: &mut ModuleExports) {
28    // ── TCP ────────────────────────────────────────────────────────────────
29
30    // io.tcp_connect(addr: string) -> IoHandle
31    register_typed_fn_1::<_, Arc<String>>(
32        module,
33        "tcp_connect",
34        "Connect to a TCP server",
35        "addr",
36        "string",
37        ConcreteType::IoHandle,
38        |addr, ctx| {
39            let addr = addr.as_str();
40            crate::module_exports::check_net_permission(
41                ctx,
42                shape_abi_v1::Permission::NetConnect,
43                addr,
44            )?;
45            let stream = std::net::TcpStream::connect(addr)
46                .map_err(|e| format!("io.tcp_connect(\"{}\"): {}", addr, e))?;
47            let handle = IoHandleData::new_tcp_stream(stream, addr.to_string());
48            Ok(TypedReturn::Concrete(ConcreteReturn::IoHandle(Arc::new(
49                handle,
50            ))))
51        },
52    );
53
54    // io.tcp_listen(addr: string) -> IoHandle
55    register_typed_fn_1::<_, Arc<String>>(
56        module,
57        "tcp_listen",
58        "Bind a TCP listener",
59        "addr",
60        "string",
61        ConcreteType::IoHandle,
62        |addr, ctx| {
63            let addr = addr.as_str();
64            crate::module_exports::check_net_permission(
65                ctx,
66                shape_abi_v1::Permission::NetListen,
67                addr,
68            )?;
69            let listener = std::net::TcpListener::bind(addr)
70                .map_err(|e| format!("io.tcp_listen(\"{}\"): {}", addr, e))?;
71            let handle = IoHandleData::new_tcp_listener(listener, addr.to_string());
72            Ok(TypedReturn::Concrete(ConcreteReturn::IoHandle(Arc::new(
73                handle,
74            ))))
75        },
76    );
77
78    // io.tcp_accept(listener: IoHandle) -> IoHandle
79    register_typed_fn_1::<_, Arc<IoHandleData>>(
80        module,
81        "tcp_accept",
82        "Accept the next incoming TCP connection",
83        "listener",
84        "IoHandle",
85        ConcreteType::IoHandle,
86        |handle, ctx| {
87            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::NetListen)?;
88            let guard = handle
89                .resource
90                .lock()
91                .map_err(|_| "io.tcp_accept(): lock poisoned".to_string())?;
92            let resource = guard
93                .as_ref()
94                .ok_or_else(|| "io.tcp_accept(): handle is closed".to_string())?;
95            match resource {
96                IoResource::TcpListener(listener) => {
97                    let (stream, peer) = listener
98                        .accept()
99                        .map_err(|e| format!("io.tcp_accept(): {}", e))?;
100                    let peer_str = peer.to_string();
101                    let client = IoHandleData::new_tcp_stream(stream, peer_str);
102                    Ok(TypedReturn::Concrete(ConcreteReturn::IoHandle(Arc::new(
103                        client,
104                    ))))
105                }
106                _ => Err("io.tcp_accept(): handle is not a TcpListener".to_string()),
107            }
108        },
109    );
110
111    // io.tcp_read(handle: IoHandle, n?: int) -> string
112    register_typed_fn_2_full::<_, Arc<IoHandleData>, i64>(
113        module,
114        "tcp_read",
115        "Read up to n bytes from a TCP stream",
116        [
117            ModuleParam {
118                name: "handle".to_string(),
119                type_name: "IoHandle".to_string(),
120                required: true,
121                description: "TcpStream handle".to_string(),
122                ..Default::default()
123            },
124            ModuleParam {
125                name: "n".to_string(),
126                type_name: "int".to_string(),
127                required: false,
128                description: "Max bytes to read (default: 65536)".to_string(),
129                default_snippet: Some("65536".to_string()),
130                ..Default::default()
131            },
132        ],
133        ConcreteType::String,
134        |handle, n, ctx| {
135            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::NetConnect)?;
136            let mut guard = handle
137                .resource
138                .lock()
139                .map_err(|_| "io.tcp_read(): lock poisoned".to_string())?;
140            let resource = guard
141                .as_mut()
142                .ok_or_else(|| "io.tcp_read(): handle is closed".to_string())?;
143            match resource {
144                IoResource::TcpStream(stream) => {
145                    let buf_size = if n > 0 { n as usize } else { 65536 };
146                    let mut buf = vec![0u8; buf_size];
147                    let bytes_read = stream
148                        .read(&mut buf)
149                        .map_err(|e| format!("io.tcp_read(): {}", e))?;
150                    buf.truncate(bytes_read);
151                    let s = String::from_utf8(buf)
152                        .map_err(|e| format!("io.tcp_read(): invalid UTF-8: {}", e))?;
153                    Ok(TypedReturn::Concrete(ConcreteReturn::String(s)))
154                }
155                _ => Err("io.tcp_read(): handle is not a TcpStream".to_string()),
156            }
157        },
158    );
159
160    // io.tcp_write(handle: IoHandle, data: string) -> int
161    register_typed_fn_2::<_, Arc<IoHandleData>, Arc<String>>(
162        module,
163        "tcp_write",
164        "Write a string to a TCP stream, returning bytes written",
165        [("handle", "IoHandle"), ("data", "string")],
166        ConcreteType::Int,
167        |handle, data, ctx| {
168            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::NetConnect)?;
169            let mut guard = handle
170                .resource
171                .lock()
172                .map_err(|_| "io.tcp_write(): lock poisoned".to_string())?;
173            let resource = guard
174                .as_mut()
175                .ok_or_else(|| "io.tcp_write(): handle is closed".to_string())?;
176            match resource {
177                IoResource::TcpStream(stream) => {
178                    let written = stream
179                        .write(data.as_bytes())
180                        .map_err(|e| format!("io.tcp_write(): {}", e))?;
181                    Ok(TypedReturn::Concrete(ConcreteReturn::I64(written as i64)))
182                }
183                _ => Err("io.tcp_write(): handle is not a TcpStream".to_string()),
184            }
185        },
186    );
187
188    // io.tcp_close(handle: IoHandle) -> bool
189    register_typed_fn_1::<_, Arc<IoHandleData>>(
190        module,
191        "tcp_close",
192        "Close a TCP stream or listener, returning whether it was open",
193        "handle",
194        "IoHandle",
195        ConcreteType::Bool,
196        |handle, ctx| {
197            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::NetConnect)?;
198            Ok(TypedReturn::Concrete(ConcreteReturn::Bool(handle.close())))
199        },
200    );
201
202    // ── UDP ────────────────────────────────────────────────────────────────
203
204    // io.udp_bind(addr: string) -> IoHandle
205    register_typed_fn_1::<_, Arc<String>>(
206        module,
207        "udp_bind",
208        "Bind a UDP socket to addr",
209        "addr",
210        "string",
211        ConcreteType::IoHandle,
212        |addr, ctx| {
213            let addr = addr.as_str();
214            crate::module_exports::check_net_permission(
215                ctx,
216                shape_abi_v1::Permission::NetListen,
217                addr,
218            )?;
219            let socket = std::net::UdpSocket::bind(addr)
220                .map_err(|e| format!("io.udp_bind(\"{}\"): {}", addr, e))?;
221            let local = socket
222                .local_addr()
223                .map(|a| a.to_string())
224                .unwrap_or_else(|_| addr.to_string());
225            let handle = IoHandleData::new_udp_socket(socket, local);
226            Ok(TypedReturn::Concrete(ConcreteReturn::IoHandle(Arc::new(
227                handle,
228            ))))
229        },
230    );
231
232    // io.udp_send(handle: IoHandle, data: string, target: string) -> int
233    register_typed_fn_3::<_, Arc<IoHandleData>, Arc<String>, Arc<String>>(
234        module,
235        "udp_send",
236        "Send a UDP datagram to target, returning bytes sent",
237        [
238            ("handle", "IoHandle"),
239            ("data", "string"),
240            ("target", "string"),
241        ],
242        ConcreteType::Int,
243        |handle, data, target, ctx| {
244            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::NetConnect)?;
245            let guard = handle
246                .resource
247                .lock()
248                .map_err(|_| "io.udp_send(): lock poisoned".to_string())?;
249            let resource = guard
250                .as_ref()
251                .ok_or_else(|| "io.udp_send(): handle is closed".to_string())?;
252            match resource {
253                IoResource::UdpSocket(socket) => {
254                    let sent = socket
255                        .send_to(data.as_bytes(), target.as_str())
256                        .map_err(|e| format!("io.udp_send(): {}", e))?;
257                    Ok(TypedReturn::Concrete(ConcreteReturn::I64(sent as i64)))
258                }
259                _ => Err("io.udp_send(): handle is not a UdpSocket".to_string()),
260            }
261        },
262    );
263
264    // io.udp_recv(handle: IoHandle, n?: int) -> object { data: string, addr: string }
265    register_typed_fn_2_full::<_, Arc<IoHandleData>, i64>(
266        module,
267        "udp_recv",
268        "Receive a UDP datagram, returning {data, addr}",
269        [
270            ModuleParam {
271                name: "handle".to_string(),
272                type_name: "IoHandle".to_string(),
273                required: true,
274                description: "UdpSocket handle".to_string(),
275                ..Default::default()
276            },
277            ModuleParam {
278                name: "n".to_string(),
279                type_name: "int".to_string(),
280                required: false,
281                description: "Max receive buffer size (default: 65536)".to_string(),
282                default_snippet: Some("65536".to_string()),
283                ..Default::default()
284            },
285        ],
286        ConcreteType::TypedObject,
287        |handle, n, ctx| {
288            crate::module_exports::check_permission(ctx, shape_abi_v1::Permission::NetConnect)?;
289            let guard = handle
290                .resource
291                .lock()
292                .map_err(|_| "io.udp_recv(): lock poisoned".to_string())?;
293            let resource = guard
294                .as_ref()
295                .ok_or_else(|| "io.udp_recv(): handle is closed".to_string())?;
296            match resource {
297                IoResource::UdpSocket(socket) => {
298                    let buf_size = if n > 0 { n as usize } else { 65536 };
299                    let mut buf = vec![0u8; buf_size];
300                    let (bytes_read, src_addr) = socket
301                        .recv_from(&mut buf)
302                        .map_err(|e| format!("io.udp_recv(): {}", e))?;
303                    buf.truncate(bytes_read);
304                    let data = String::from_utf8(buf)
305                        .map_err(|e| format!("io.udp_recv(): invalid UTF-8: {}", e))?;
306                    Ok(TypedReturn::TypedObject(vec![
307                        ("data".to_string(), ConcreteReturn::String(data)),
308                        (
309                            "addr".to_string(),
310                            ConcreteReturn::String(src_addr.to_string()),
311                        ),
312                    ]))
313                }
314                _ => Err("io.udp_recv(): handle is not a UdpSocket".to_string()),
315            }
316        },
317    );
318}