Skip to main content

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}