zsh/ported/modules/socket.rs
1//! Unix domain socket module — port of `Src/Modules/socket.c`.
2//!
3//! C source has zero `struct ...` / `enum ...` definitions. The
4//! Rust port matches: zero types, only the function ports
5//! (`bin_zsocket`, `setup_`/`features_`/`enables_`/`boot_`/
6//! `cleanup_`/`finish_`).
7
8/// Direct port of `bin_zsocket(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/socket.c:57`.
9/// C signature matches exactly: `static int bin_zsocket(char *nam,
10/// char **args, Options ops, UNUSED(int func))`.
11/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
12use crate::ported::zsh_h::{OPT_ISSET, OPT_ARG, FDT_UNUSED, FDT_EXTERNAL};
13pub fn bin_zsocket(nam: &str, args: &[String], // c:57
14 ops: &crate::ported::zsh_h::options, _func: i32) -> i32 {
15 let mut soun: libc::sockaddr_un = unsafe { std::mem::zeroed() };
16 let mut sfd: i32;
17 let mut err: i32 = 1; // c:60
18 let mut verbose = 0i32;
19 let mut test = 0i32;
20 let mut targetfd: i32 = 0;
21 let mut soun: libc::sockaddr_un = unsafe { std::mem::zeroed() };
22 let mut sfd: i32;
23
24 if OPT_ISSET(ops, b'v') { verbose = 1; } // c:64-65
25 if OPT_ISSET(ops, b't') { test = 1; } // c:67-68
26
27 if OPT_ISSET(ops, b'd') { // c:70
28 let darg = OPT_ARG(ops, b'd').unwrap_or("");
29 targetfd = darg.parse::<i32>().unwrap_or(0); // c:71 atoi
30 if targetfd == 0 { // c:72
31 crate::ported::utils::zwarnnam(nam,
32 &format!("{} is an invalid argument to -d", darg)); // c:73
33 return 1; // c:75
34 }
35 // c:78-82 — `if (targetfd <= max_zsh_fd && fdtable[targetfd] != FDT_UNUSED)`.
36 // Static-link path: query the per-process fdtable accessor.
37 if crate::ported::utils::fdtable_get(targetfd) != FDT_UNUSED { // c:78
38 crate::ported::utils::zwarnnam(nam, // c:79
39 &format!("file descriptor {} is in use by the shell", targetfd));
40 return 1; // c:81
41 }
42 }
43
44 if OPT_ISSET(ops, b'l') { // c:85
45 if args.is_empty() { // c:88
46 crate::ported::utils::zwarnnam(nam, "-l requires an argument");
47 return 1; // c:90
48 }
49 let localfn = args[0].as_str(); // c:93
50 sfd = unsafe { libc::socket(libc::PF_UNIX, libc::SOCK_STREAM, 0) }; // c:95
51 if sfd == -1 { // c:97
52 crate::ported::utils::zwarnnam(nam,
53 &format!("socket error: {} ", std::io::Error::last_os_error())); // c:98
54 return 1; // c:99
55 }
56 soun.sun_family = libc::AF_UNIX as _; // c:102
57 let path_bytes = localfn.as_bytes();
58 let max_len = soun.sun_path.len() - 1;
59 let copy_len = path_bytes.len().min(max_len);
60 for (k, &b) in path_bytes[..copy_len].iter().enumerate() { // c:103 strncpy
61 soun.sun_path[k] = b as libc::c_char;
62 }
63 let r = unsafe { // c:105
64 libc::bind(sfd, &soun as *const _ as *const libc::sockaddr,
65 std::mem::size_of::<libc::sockaddr_un>() as libc::socklen_t)
66 };
67 if r != 0 { // c:106
68 crate::ported::utils::zwarnnam(nam,
69 &format!("could not bind to {}: {}", localfn,
70 std::io::Error::last_os_error())); // c:107
71 unsafe { libc::close(sfd); } // c:108
72 return 1; // c:109
73 }
74 if unsafe { libc::listen(sfd, 1) } != 0 { // c:112
75 crate::ported::utils::zwarnnam(nam,
76 &format!("could not listen on socket: {}",
77 std::io::Error::last_os_error())); // c:114
78 unsafe { libc::close(sfd); } // c:115
79 return 1; // c:116
80 }
81 crate::ported::utils::addmodulefd(sfd, crate::ported::zsh_h::FDT_EXTERNAL); // c:119 FDT_EXTERNAL
82 if targetfd != 0 { // c:121
83 sfd = crate::ported::utils::redup(sfd, targetfd); // c:122
84 } else {
85 sfd = crate::ported::utils::movefd(sfd); // c:126 movefd
86 }
87 if sfd == -1 { // c:128
88 crate::ported::utils::zerrnam(nam,
89 &format!("cannot duplicate fd {}: {}", sfd,
90 std::io::Error::last_os_error())); // c:129
91 return 1; // c:130
92 }
93 crate::ported::utils::fdtable_set(sfd, FDT_EXTERNAL); // c:134
94 crate::ported::params::setiparam("REPLY", sfd as i64); // c:136 setiparam_no_convert
95 if verbose != 0 { // c:138
96 println!("{} listener is on fd {}", localfn, sfd); // c:139
97 }
98 return 0; // c:141
99 } else if OPT_ISSET(ops, b'a') { // c:143
100 if args.is_empty() { // c:147
101 crate::ported::utils::zwarnnam(nam, "-a requires an argument");
102 return 1; // c:149
103 }
104 let lfd = args[0].parse::<i32>().unwrap_or(0); // c:152 atoi
105 if lfd == 0 { // c:154
106 crate::ported::utils::zwarnnam(nam, "invalid numerical argument");
107 return 1; // c:156
108 }
109 if test != 0 { // c:159
110 // c:163 HAVE_POLL branch.
111 let mut pfd = libc::pollfd { fd: lfd, events: libc::POLLIN, revents: 0 };
112 let r = unsafe { libc::poll(&mut pfd, 1, 0) }; // c:166
113 if r == 0 { return 1; } // c:166
114 else if r == -1 { // c:167
115 crate::ported::utils::zwarnnam(nam,
116 &format!("poll error: {}",
117 std::io::Error::last_os_error())); // c:169
118 return 1; // c:170
119 }
120 }
121 let mut len: libc::socklen_t =
122 std::mem::size_of::<libc::sockaddr_un>() as libc::socklen_t; // c:194
123 let rfd: i32;
124 loop { // c:195
125 let r = unsafe { libc::accept(lfd, // c:196
126 &mut soun as *mut _ as *mut libc::sockaddr, &mut len) };
127 if r >= 0 { rfd = r; break; }
128 let osek = std::io::Error::last_os_error().raw_os_error();
129 if osek != Some(libc::EINTR)
130 || crate::ported::utils::errflag
131 .load(std::sync::atomic::Ordering::Relaxed) != 0 { rfd = r; break; }
132 }
133 if rfd == -1 { // c:199
134 crate::ported::utils::zwarnnam(nam,
135 &format!("could not accept connection: {}",
136 std::io::Error::last_os_error())); // c:200
137 return 1; // c:201
138 }
139 crate::ported::utils::addmodulefd(rfd, crate::ported::zsh_h::FDT_EXTERNAL); // c:204 FDT_EXTERNAL
140 if targetfd != 0 { // c:206
141 sfd = crate::ported::utils::redup(rfd, targetfd); // c:207
142 if sfd < 0 { // c:208
143 crate::ported::utils::zerrnam(nam,
144 &format!("could not duplicate socket fd to {}: {}",
145 targetfd, std::io::Error::last_os_error())); // c:209
146 unsafe { libc::close(rfd); } // c:210
147 return 1; // c:211
148 }
149 crate::ported::utils::fdtable_set(sfd, FDT_EXTERNAL); // c:213
150 } else {
151 sfd = rfd; // c:217
152 }
153 crate::ported::params::setiparam("REPLY", sfd as i64); // c:220 setiparam_no_convert
154 if verbose != 0 { // c:222
155 let path = soun.sun_path.iter()
156 .take_while(|&&c| c != 0)
157 .map(|&c| c as u8 as char).collect::<String>();
158 println!("new connection from {} is on fd {}", path, sfd); // c:223
159 }
160 } else { // c:225
161 if args.is_empty() { // c:227
162 crate::ported::utils::zwarnnam(nam, "zsocket requires an argument");
163 return 1; // c:229
164 }
165 sfd = unsafe { libc::socket(libc::PF_UNIX, libc::SOCK_STREAM, 0) }; // c:233
166 if sfd == -1 { // c:235
167 crate::ported::utils::zwarnnam(nam,
168 &format!("socket creation failed: {}",
169 std::io::Error::last_os_error())); // c:236
170 return 1; // c:237
171 }
172 soun.sun_family = libc::AF_UNIX as _; // c:240
173 let path_bytes = args[0].as_bytes();
174 let max_len = soun.sun_path.len() - 1;
175 let copy_len = path_bytes.len().min(max_len);
176 for (k, &b) in path_bytes[..copy_len].iter().enumerate() { // c:241 strncpy
177 soun.sun_path[k] = b as libc::c_char;
178 }
179 err = unsafe { // c:243
180 libc::connect(sfd,
181 &soun as *const _ as *const libc::sockaddr,
182 std::mem::size_of::<libc::sockaddr_un>() as libc::socklen_t)
183 };
184 if err != 0 { // c:243
185 crate::ported::utils::zwarnnam(nam,
186 &format!("connection failed: {}",
187 std::io::Error::last_os_error())); // c:244
188 unsafe { libc::close(sfd); } // c:245
189 return 1; // c:246
190 }
191 crate::ported::utils::addmodulefd(sfd, crate::ported::zsh_h::FDT_EXTERNAL); // c:251 FDT_EXTERNAL
192 if targetfd != 0 { // c:253
193 if crate::ported::utils::redup(sfd, targetfd) < 0 { // c:254
194 crate::ported::utils::zerrnam(nam,
195 &format!("could not duplicate socket fd to {}: {}",
196 targetfd, std::io::Error::last_os_error())); // c:255
197 unsafe { libc::close(sfd); } // c:256
198 return 1; // c:257
199 }
200 sfd = targetfd; // c:259
201 crate::ported::utils::fdtable_set(sfd, FDT_EXTERNAL); // c:260
202 }
203 crate::ported::params::setiparam("REPLY", sfd as i64); // c:263 setiparam_no_convert
204 if verbose != 0 { // c:265
205 let path = &args[0];
206 println!("{} is now on fd {}", path, sfd); // c:266
207 }
208 }
209 let _ = (err, verbose, test, targetfd); // silence unused-binding paths
210 0 // c:271
211}
212
213
214
215// ===========================================================
216// Methods moved verbatim from src/ported/exec.rs because their
217// C counterpart's source file maps 1:1 to this Rust module.
218// ===========================================================
219
220// =====================================================================
221// static struct builtin bintab[] c:280
222// static struct features module_features c:284
223// =====================================================================
224
225use crate::ported::zsh_h::module;
226
227// `bintab` — port of `static struct builtin bintab[]` (socket.c:280).
228
229
230// `module_features` — port of `static struct features module_features`
231// from socket.c:284.
232
233
234
235/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/socket.c:291`.
236#[allow(unused_variables)]
237pub fn setup_(m: *const module) -> i32 { // c:291
238 0 // c:306
239}
240
241/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/socket.c:298`.
242/// C body: `*features = featuresarray(m, &module_features); return 0;`
243pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 { // c:298
244 *features = featuresarray(m, module_features());
245 0 // c:313
246}
247
248/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/socket.c:306`.
249/// C body: `return handlefeatures(m, &module_features, enables);`
250pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 { // c:306
251 handlefeatures(m, module_features(), enables) // c:320
252}
253
254/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/socket.c:313`.
255#[allow(unused_variables)]
256pub fn boot_(m: *const module) -> i32 { // c:313
257 0 // c:327
258}
259
260/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/socket.c:320`.
261/// C body: `return setfeatureenables(m, &module_features, NULL);`
262pub fn cleanup_(m: *const module) -> i32 { // c:320
263 setfeatureenables(m, module_features(), None) // c:327
264}
265
266/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/socket.c:327`.
267#[allow(unused_variables)]
268pub fn finish_(m: *const module) -> i32 { // c:327
269 0 // c:327
270}
271
272use crate::ported::zsh_h::features as features_t;
273use std::sync::{Mutex, OnceLock};
274
275static MODULE_FEATURES: OnceLock<Mutex<features_t>> = OnceLock::new();
276
277
278// Local stubs for the per-module entry points. C uses generic
279// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
280// 3275/3370/3445) but those take `Builtin` + `Features` pointer
281// fields the Rust port doesn't carry. The hardcoded descriptor
282// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
283// WARNING: NOT IN SOCKET.C — Rust-only module-framework shim.
284// C uses generic featuresarray/handlefeatures/setfeatureenables from
285// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
286// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
287fn featuresarray(_m: *const module, _f: &Mutex<features_t>) -> Vec<String> {
288 vec!["b:zsocket".to_string()]
289}
290
291// WARNING: NOT IN SOCKET.C — Rust-only module-framework shim.
292// C uses generic featuresarray/handlefeatures/setfeatureenables from
293// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
294// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
295fn handlefeatures(
296 _m: *const module,
297 _f: &Mutex<features_t>,
298 enables: &mut Option<Vec<i32>>,
299) -> i32 {
300 if enables.is_none() {
301 *enables = Some(vec![1; 1]);
302 }
303 0
304}
305
306// WARNING: NOT IN SOCKET.C — Rust-only module-framework shim.
307// C uses generic featuresarray/handlefeatures/setfeatureenables from
308// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
309// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
310fn setfeatureenables(
311 _m: *const module,
312 _f: &Mutex<features_t>,
313 _e: Option<&[i32]>,
314) -> i32 {
315 0
316}
317
318// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
319// ─── RUST-ONLY ACCESSORS ───
320//
321// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
322// RwLock<T>>` globals declared above. C zsh uses direct global
323// access; Rust needs these wrappers because `OnceLock::get_or_init`
324// is the only way to lazily construct shared state. These fns sit
325// here so the body of this file reads in C source order without
326// the accessor wrappers interleaved between real port fns.
327// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
328
329// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
330// ─── RUST-ONLY ACCESSORS ───
331//
332// Singleton accessor fns for `OnceLock<Mutex<T>>` / `OnceLock<
333// RwLock<T>>` globals declared above. C zsh uses direct global
334// access; Rust needs these wrappers because `OnceLock::get_or_init`
335// is the only way to lazily construct shared state. These fns sit
336// here so the body of this file reads in C source order without
337// the accessor wrappers interleaved between real port fns.
338// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
339
340// WARNING: NOT IN SOCKET.C — Rust-only module-framework shim.
341// C uses generic featuresarray/handlefeatures/setfeatureenables from
342// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
343// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
344fn module_features() -> &'static Mutex<features_t> {
345 MODULE_FEATURES.get_or_init(|| Mutex::new(features_t {
346 bn_list: None,
347 bn_size: 1,
348 cd_list: None,
349 cd_size: 0,
350 mf_list: None,
351 mf_size: 0,
352 pd_list: None,
353 pd_size: 0,
354 n_abstract: 0,
355 }))
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361
362 /// c:88-90 — `zsocket -l` with no path arg MUST fail-fast BEFORE
363 /// any libc::socket(2) call. A regression where the missing-arg
364 /// check is bypassed would leak a socket fd per invocation.
365 #[test]
366 fn zsocket_l_without_arg_fails_before_socket_call() {
367 let mut ops = empty_ops();
368 ops.ind[b'l' as usize] = 1;
369 assert_eq!(bin_zsocket("zsocket", &[], &ops, 0), 1);
370 }
371
372 /// c:71-75 — non-numeric `-d <fd>` MUST fail-fast (atoi → 0 → bad
373 /// fd) BEFORE socket(2). A regression that lets `0` through would
374 /// dup2 the new socket onto stdin silently.
375 #[test]
376 fn zsocket_d_non_numeric_fails_before_dup2() {
377 let mut ops = empty_ops();
378 ops.ind[b'd' as usize] = (1 << 2) | 1;
379 ops.args.push("not-a-number".to_string());
380 assert_eq!(bin_zsocket("zsocket", &[], &ops, 0), 1);
381 }
382
383 fn empty_ops() -> crate::ported::zsh_h::options {
384 crate::ported::zsh_h::options {
385 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
386 args: Vec::new(), argscount: 0, argsalloc: 0,
387 }
388 }
389
390 /// c:225-229 — `zsocket` (default, connect-mode) with NO args must
391 /// fail-fast with "zsocket requires an argument". Catches a
392 /// regression where the missing-arg path leaks an unconnected
393 /// socket fd.
394 #[test]
395 fn zsocket_connect_mode_without_args_returns_one() {
396 let ops = empty_ops();
397 assert_eq!(bin_zsocket("zsocket", &[], &ops, 0), 1);
398 }
399
400 /// c:144-149 — `zsocket -a` with NO args must fail-fast with
401 /// "-a requires an argument". Symmetrical to the `-l` check at
402 /// c:88-90 (already pinned) but for the accept-mode path.
403 #[test]
404 fn zsocket_a_without_arg_fails_before_accept() {
405 let mut ops = empty_ops();
406 ops.ind[b'a' as usize] = 1;
407 assert_eq!(bin_zsocket("zsocket", &[], &ops, 0), 1);
408 }
409
410 /// c:152-156 — `zsocket -a 0` (or any non-numeric → atoi → 0) must
411 /// fail with "invalid numerical argument". `0` is never a valid
412 /// listening fd because the user can't have just created one and
413 /// taken stdin away.
414 #[test]
415 fn zsocket_a_zero_listen_fd_fails() {
416 let mut ops = empty_ops();
417 ops.ind[b'a' as usize] = 1;
418 assert_eq!(bin_zsocket("zsocket", &["0".to_string()], &ops, 0), 1);
419 // non-numeric also flows through atoi → 0
420 assert_eq!(bin_zsocket("zsocket", &["not-numeric".to_string()], &ops, 0), 1);
421 }
422
423 /// c:291-327 — module-lifecycle stubs (`setup_`, `boot_`,
424 /// `cleanup_`, `finish_`) all return 0 in the C source. The Rust
425 /// port must match.
426 #[test]
427 fn module_lifecycle_shims_all_return_zero() {
428 let m = std::ptr::null();
429 assert_eq!(setup_(m), 0);
430 assert_eq!(boot_(m), 0);
431 assert_eq!(cleanup_(m), 0);
432 assert_eq!(finish_(m), 0);
433 }
434
435 /// c:298 — `features_` populates the feature list and returns 0.
436 /// Specific contents aren't pinned by C; just verify the function
437 /// is callable without panicking and returns the success sentinel.
438 #[test]
439 fn features_returns_success() {
440 let mut features = Vec::new();
441 assert_eq!(features_(std::ptr::null(), &mut features), 0);
442 }
443}