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
8use crate::ported::params::setiparam_no_convert;
9use crate::ported::utils::{
10    addmodulefd, errflag, fdtable_get, fdtable_set, movefd, redup, zerrnam, zwarnnam,
11};
12/// Direct port of `bin_zsocket(char *nam, char **args, Options ops, UNUSED(int func))` from `Src/Modules/socket.c:57`.
13/// C signature matches exactly: `static int bin_zsocket(char *nam,
14/// char **args, Options ops, UNUSED(int func))`.
15/// WARNING: param names don't match C — Rust=(nam, args, _func) vs C=(nam, args, ops, func)
16use crate::ported::zsh_h::{
17    features, module, options, FDT_EXTERNAL, FDT_UNUSED, OPT_ARG, OPT_ISSET,
18};
19use std::sync::{Mutex, OnceLock};
20/// `bin_zsocket` — see implementation.
21pub fn bin_zsocket(
22    nam: &str,
23    args: &[String], // c:57
24    ops: &options,
25    _func: i32,
26) -> i32 {
27    let mut soun: libc::sockaddr_un = unsafe { std::mem::zeroed() };
28    let mut sfd: i32;
29    let mut err: i32 = 1; // c:60
30    let mut verbose = 0i32;
31    let mut test = 0i32;
32    let mut targetfd: i32 = 0;
33    let mut soun: libc::sockaddr_un = unsafe { std::mem::zeroed() };
34    let mut sfd: i32;
35
36    if OPT_ISSET(ops, b'v') {
37        verbose = 1;
38    } // c:64-65
39    if OPT_ISSET(ops, b't') {
40        test = 1;
41    } // c:67-68
42
43    if OPT_ISSET(ops, b'd') {
44        // c:70
45        let darg = OPT_ARG(ops, b'd').unwrap_or("");
46        targetfd = darg.parse::<i32>().unwrap_or(0); // c:71 atoi
47        if targetfd == 0 {
48            // c:72
49            zwarnnam(nam, &format!("{} is an invalid argument to -d", darg)); // c:73
50            return 1; // c:75
51        }
52        // c:78-82 — `if (targetfd <= max_zsh_fd && fdtable[targetfd] != FDT_UNUSED)`.
53        // Static-link path: query the per-process fdtable accessor.
54        if fdtable_get(targetfd) != FDT_UNUSED {
55            // c:78
56            zwarnnam(
57                nam, // c:79
58                &format!("file descriptor {} is in use by the shell", targetfd),
59            );
60            return 1; // c:81
61        } else {
62        }
63    }
64
65    if OPT_ISSET(ops, b'l') {
66        // c:85
67        if args.is_empty() {
68            // c:88
69            zwarnnam(nam, "-l requires an argument");
70            return 1; // c:90
71        }
72        let localfn = args[0].as_str(); // c:93
73        sfd = unsafe { libc::socket(libc::PF_UNIX, libc::SOCK_STREAM, 0) }; // c:95
74        if sfd == -1 {
75            // c:97
76            zwarnnam(
77                nam,
78                &format!("socket error: {} ", std::io::Error::last_os_error()),
79            ); // c:98
80            return 1; // c:99
81        }
82        soun.sun_family = libc::AF_UNIX as _; // c:102
83        let path_bytes = localfn.as_bytes();
84        let max_len = soun.sun_path.len() - 1;
85        let copy_len = path_bytes.len().min(max_len);
86        for (k, &b) in path_bytes[..copy_len].iter().enumerate() {
87            // c:103 strncpy
88            soun.sun_path[k] = b as libc::c_char;
89        }
90        let r = unsafe {
91            // c:105
92            libc::bind(
93                sfd,
94                &soun as *const _ as *const libc::sockaddr,
95                size_of::<libc::sockaddr_un>() as libc::socklen_t,
96            )
97        };
98        if r != 0 {
99            // c:106
100            zwarnnam(
101                nam,
102                &format!(
103                    "could not bind to {}: {}",
104                    localfn,
105                    std::io::Error::last_os_error()
106                ),
107            ); // c:107
108            unsafe {
109                libc::close(sfd);
110            } // c:108
111            return 1; // c:109
112        }
113        if unsafe { libc::listen(sfd, 1) } != 0 {
114            // c:112
115            zwarnnam(
116                nam,
117                &format!(
118                    "could not listen on socket: {}",
119                    std::io::Error::last_os_error()
120                ),
121            ); // c:114
122            unsafe {
123                libc::close(sfd);
124            } // c:115
125            return 1; // c:116
126        }
127        addmodulefd(sfd, FDT_EXTERNAL); // c:119 FDT_EXTERNAL
128        if targetfd != 0 {
129            // c:121
130            sfd = redup(sfd, targetfd); // c:122
131        } else {
132            sfd = movefd(sfd); // c:126 movefd
133        }
134        if sfd == -1 {
135            // c:128
136            zerrnam(
137                nam,
138                &format!(
139                    "cannot duplicate fd {}: {}",
140                    sfd,
141                    std::io::Error::last_os_error()
142                ),
143            ); // c:129
144            return 1; // c:130
145        }
146        fdtable_set(sfd, FDT_EXTERNAL); // c:134
147        setiparam_no_convert("REPLY", sfd as i64); // c:135 setiparam_no_convert
148        if verbose != 0 {
149            // c:138
150            println!("{} listener is on fd {}", localfn, sfd); // c:139
151        }
152        return 0; // c:141
153    } else if OPT_ISSET(ops, b'a') {
154        // c:143
155        if args.is_empty() {
156            // c:147
157            zwarnnam(nam, "-a requires an argument");
158            return 1; // c:149
159        }
160        let lfd = args[0].parse::<i32>().unwrap_or(0); // c:152 atoi
161        if lfd == 0 {
162            // c:154
163            zwarnnam(nam, "invalid numerical argument");
164            return 1; // c:156
165        }
166        if test != 0 {
167            // c:159
168            // c:163 HAVE_POLL branch.
169            let mut pfd = libc::pollfd {
170                fd: lfd,
171                events: libc::POLLIN,
172                revents: 0,
173            };
174            let r = unsafe { libc::poll(&mut pfd, 1, 0) }; // c:166
175            if r == 0 {
176                return 1;
177            }
178            // c:166
179            else if r == -1 {
180                // c:167
181                zwarnnam(
182                    nam,
183                    &format!("poll error: {}", std::io::Error::last_os_error()),
184                ); // c:169
185                return 1; // c:170
186            }
187        }
188        let mut len: libc::socklen_t = size_of::<libc::sockaddr_un>() as libc::socklen_t; // c:194
189        let rfd: i32;
190        loop {
191            // c:195
192            let r = unsafe {
193                libc::accept(
194                    lfd, // c:196
195                    &mut soun as *mut _ as *mut libc::sockaddr,
196                    &mut len,
197                )
198            };
199            if r >= 0 {
200                rfd = r;
201                break;
202            }
203            let osek = std::io::Error::last_os_error().raw_os_error();
204            if osek != Some(libc::EINTR) || errflag.load(std::sync::atomic::Ordering::Relaxed) != 0
205            {
206                rfd = r;
207                break;
208            } else {
209            }
210        }
211        if rfd == -1 {
212            // c:199
213            zwarnnam(
214                nam,
215                &format!(
216                    "could not accept connection: {}",
217                    std::io::Error::last_os_error()
218                ),
219            ); // c:200
220            return 1; // c:201
221        }
222        addmodulefd(rfd, FDT_EXTERNAL); // c:204 FDT_EXTERNAL
223        if targetfd != 0 {
224            // c:206
225            sfd = redup(rfd, targetfd); // c:207
226            if sfd < 0 {
227                // c:208
228                zerrnam(
229                    nam,
230                    &format!(
231                        "could not duplicate socket fd to {}: {}",
232                        targetfd,
233                        std::io::Error::last_os_error()
234                    ),
235                ); // c:209
236                   // c:214 — `zclose(rfd);` — rfd was registered as
237                   // FDT_EXTERNAL at c:208 above (addmodulefd call).
238                   // Raw libc::close would leave the marker stale on
239                   // the freed fd (same leak shape as the init_io
240                   // SHTTY fix ff15efec5f and the tcp_close fix
241                   // 9b4dae375a).
242                let _ = crate::ported::utils::zclose(rfd);
243                return 1; // c:215
244            }
245            fdtable_set(sfd, FDT_EXTERNAL); // c:217
246        } else {
247            sfd = rfd; // c:217
248        }
249        setiparam_no_convert("REPLY", sfd as i64); // c:223 setiparam_no_convert
250        if verbose != 0 {
251            // c:222
252            let path = soun
253                .sun_path
254                .iter()
255                .take_while(|&&c| c != 0)
256                .map(|&c| c as u8 as char)
257                .collect::<String>();
258            println!("new connection from {} is on fd {}", path, sfd); // c:223
259        }
260    } else {
261        // c:225
262        if args.is_empty() {
263            // c:227
264            zwarnnam(nam, "zsocket requires an argument");
265            return 1; // c:229
266        }
267        sfd = unsafe { libc::socket(libc::PF_UNIX, libc::SOCK_STREAM, 0) }; // c:233
268        if sfd == -1 {
269            // c:235
270            zwarnnam(
271                nam,
272                &format!(
273                    "socket creation failed: {}",
274                    std::io::Error::last_os_error()
275                ),
276            ); // c:236
277            return 1; // c:237
278        }
279        soun.sun_family = libc::AF_UNIX as _; // c:240
280        let path_bytes = args[0].as_bytes();
281        let max_len = soun.sun_path.len() - 1;
282        let copy_len = path_bytes.len().min(max_len);
283        for (k, &b) in path_bytes[..copy_len].iter().enumerate() {
284            // c:241 strncpy
285            soun.sun_path[k] = b as libc::c_char;
286        }
287        err = unsafe {
288            // c:243
289            libc::connect(
290                sfd,
291                &soun as *const _ as *const libc::sockaddr,
292                size_of::<libc::sockaddr_un>() as libc::socklen_t,
293            )
294        };
295        if err != 0 {
296            // c:243
297            zwarnnam(
298                nam,
299                &format!("connection failed: {}", std::io::Error::last_os_error()),
300            ); // c:244
301            unsafe {
302                libc::close(sfd);
303            } // c:245
304            return 1; // c:246
305        }
306        addmodulefd(sfd, FDT_EXTERNAL); // c:251 FDT_EXTERNAL
307        if targetfd != 0 {
308            // c:253
309            if redup(sfd, targetfd) < 0 {
310                // c:254
311                zerrnam(
312                    nam,
313                    &format!(
314                        "could not duplicate socket fd to {}: {}",
315                        targetfd,
316                        std::io::Error::last_os_error()
317                    ),
318                ); // c:256
319                   // c:257 — `zclose(sfd);` — sfd was just registered as
320                   // FDT_EXTERNAL at c:252 above; raw libc::close would
321                   // leave the marker stale (same fix shape as the c:214
322                   // case earlier in this builtin and the init_io fix
323                   // ff15efec5f).
324                let _ = crate::ported::utils::zclose(sfd);
325                return 1; // c:258
326            }
327            sfd = targetfd; // c:260
328            fdtable_set(sfd, FDT_EXTERNAL); // c:260
329        }
330        setiparam_no_convert("REPLY", sfd as i64); // c:264 setiparam_no_convert
331        if verbose != 0 {
332            // c:265
333            let path = &args[0];
334            println!("{} is now on fd {}", path, sfd); // c:266
335        }
336    }
337    let _ = (err, verbose, test, targetfd); // silence unused-binding paths
338    0 // c:271
339}
340
341// ===========================================================
342// Methods moved verbatim from src/ported/vm_helper because their
343// C counterpart's source file maps 1:1 to this Rust module.
344// ===========================================================
345
346// =====================================================================
347// static struct builtin bintab[]                                    c:280
348// static struct features module_features                            c:284
349// =====================================================================
350
351// `bintab` — port of `static struct builtin bintab[]` (socket.c:280).
352
353// `module_features` — port of `static struct features module_features`
354// from socket.c:284.
355
356/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/socket.c:291`.
357#[allow(unused_variables)]
358pub fn setup_(m: *const module) -> i32 {
359    // c:291
360    0 // c:306
361}
362
363/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/socket.c:298`.
364/// C body: `*features = featuresarray(m, &module_features); return 0;`
365pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
366    // c:298
367    *features = featuresarray(m, module_features());
368    0 // c:313
369}
370
371/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/socket.c:306`.
372/// C body: `return handlefeatures(m, &module_features, enables);`
373pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
374    // c:306
375    handlefeatures(m, module_features(), enables) // c:320
376}
377
378/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/socket.c:313`.
379#[allow(unused_variables)]
380pub fn boot_(m: *const module) -> i32 {
381    // c:313
382    0 // c:327
383}
384
385/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/socket.c:320`.
386/// C body: `return setfeatureenables(m, &module_features, NULL);`
387pub fn cleanup_(m: *const module) -> i32 {
388    // c:320
389    setfeatureenables(m, module_features(), None) // c:327
390}
391
392/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/socket.c:327`.
393#[allow(unused_variables)]
394pub fn finish_(m: *const module) -> i32 {
395    // c:327
396    0 // c:327
397}
398
399static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
400
401// Local stubs for the per-module entry points. C uses generic
402// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
403// 3275/3370/3445) but those take `Builtin` + `Features` pointer
404// fields the Rust port doesn't carry. The hardcoded descriptor
405// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
406// WARNING: NOT IN SOCKET.C — Rust-only module-framework shim.
407// C uses generic featuresarray/handlefeatures/setfeatureenables from
408// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
409// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
410fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
411    vec!["b:zsocket".to_string()]
412}
413
414// WARNING: NOT IN SOCKET.C — Rust-only module-framework shim.
415// C uses generic featuresarray/handlefeatures/setfeatureenables from
416// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
417// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
418fn handlefeatures(_m: *const module, _f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
419    if enables.is_none() {
420        *enables = Some(vec![1; 1]);
421    }
422    0
423}
424
425// WARNING: NOT IN SOCKET.C — Rust-only module-framework shim.
426// C uses generic featuresarray/handlefeatures/setfeatureenables from
427// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
428// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
429fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
430    0
431}
432
433// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
434// ─── RUST-ONLY ACCESSORS ───
435//
436// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
437// RwLock<T>>` globals declared above. C zsh uses direct global
438// access; Rust needs these wrappers because `OnceLock::get_or_init`
439// is the only way to lazily construct shared state. These ported sit
440// here so the body of this file reads in C source order without
441// the accessor wrappers interleaved between real port ported.
442// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
443
444// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
445// ─── RUST-ONLY ACCESSORS ───
446//
447// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
448// RwLock<T>>` globals declared above. C zsh uses direct global
449// access; Rust needs these wrappers because `OnceLock::get_or_init`
450// is the only way to lazily construct shared state. These ported sit
451// here so the body of this file reads in C source order without
452// the accessor wrappers interleaved between real port ported.
453// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
454
455// WARNING: NOT IN SOCKET.C — Rust-only module-framework shim.
456// C uses generic featuresarray/handlefeatures/setfeatureenables from
457// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
458// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
459fn module_features() -> &'static Mutex<features> {
460    MODULE_FEATURES.get_or_init(|| {
461        Mutex::new(features {
462            bn_list: None,
463            bn_size: 1,
464            cd_list: None,
465            cd_size: 0,
466            mf_list: None,
467            mf_size: 0,
468            pd_list: None,
469            pd_size: 0,
470            n_abstract: 0,
471        })
472    })
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478
479    /// c:88-90 — `zsocket -l` with no path arg MUST fail-fast BEFORE
480    /// any libc::socket(2) call. A regression where the missing-arg
481    /// check is bypassed would leak a socket fd per invocation.
482    #[test]
483    fn zsocket_l_without_arg_fails_before_socket_call() {
484        let _g = crate::test_util::global_state_lock();
485        let mut ops = empty_ops();
486        ops.ind[b'l' as usize] = 1;
487        assert_eq!(bin_zsocket("zsocket", &[], &ops, 0), 1);
488    }
489
490    /// c:71-75 — non-numeric `-d <fd>` MUST fail-fast (atoi → 0 → bad
491    /// fd) BEFORE socket(2). A regression that lets `0` through would
492    /// dup2 the new socket onto stdin silently.
493    #[test]
494    fn zsocket_d_non_numeric_fails_before_dup2() {
495        let _g = crate::test_util::global_state_lock();
496        let mut ops = empty_ops();
497        ops.ind[b'd' as usize] = (1 << 2) | 1;
498        ops.args.push("not-a-number".to_string());
499        assert_eq!(bin_zsocket("zsocket", &[], &ops, 0), 1);
500    }
501
502    fn empty_ops() -> options {
503        options {
504            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
505            args: Vec::new(),
506            argscount: 0,
507            argsalloc: 0,
508        }
509    }
510
511    /// c:225-229 — `zsocket` (default, connect-mode) with NO args must
512    /// fail-fast with "zsocket requires an argument". Catches a
513    /// regression where the missing-arg path leaks an unconnected
514    /// socket fd.
515    #[test]
516    fn zsocket_connect_mode_without_args_returns_one() {
517        let _g = crate::test_util::global_state_lock();
518        let ops = empty_ops();
519        assert_eq!(bin_zsocket("zsocket", &[], &ops, 0), 1);
520    }
521
522    /// c:144-149 — `zsocket -a` with NO args must fail-fast with
523    /// "-a requires an argument". Symmetrical to the `-l` check at
524    /// c:88-90 (already pinned) but for the accept-mode path.
525    #[test]
526    fn zsocket_a_without_arg_fails_before_accept() {
527        let _g = crate::test_util::global_state_lock();
528        let mut ops = empty_ops();
529        ops.ind[b'a' as usize] = 1;
530        assert_eq!(bin_zsocket("zsocket", &[], &ops, 0), 1);
531    }
532
533    /// c:152-156 — `zsocket -a 0` (or any non-numeric → atoi → 0) must
534    /// fail with "invalid numerical argument". `0` is never a valid
535    /// listening fd because the user can't have just created one and
536    /// taken stdin away.
537    #[test]
538    fn zsocket_a_zero_listen_fd_fails() {
539        let _g = crate::test_util::global_state_lock();
540        let mut ops = empty_ops();
541        ops.ind[b'a' as usize] = 1;
542        assert_eq!(bin_zsocket("zsocket", &["0".to_string()], &ops, 0), 1);
543        // non-numeric also flows through atoi → 0
544        assert_eq!(
545            bin_zsocket("zsocket", &["not-numeric".to_string()], &ops, 0),
546            1
547        );
548    }
549
550    /// c:291-327 — module-lifecycle stubs (`setup_`, `boot_`,
551    /// `cleanup_`, `finish_`) all return 0 in the C source. The Rust
552    /// port must match.
553    #[test]
554    fn module_lifecycle_shims_all_return_zero() {
555        let _g = crate::test_util::global_state_lock();
556        let m = std::ptr::null();
557        assert_eq!(setup_(m), 0);
558        assert_eq!(boot_(m), 0);
559        assert_eq!(cleanup_(m), 0);
560        assert_eq!(finish_(m), 0);
561    }
562
563    /// c:298 — `features_` populates the feature list and returns 0.
564    /// Specific contents aren't pinned by C; just verify the function
565    /// is callable without panicking and returns the success sentinel.
566    #[test]
567    fn features_returns_success() {
568        let _g = crate::test_util::global_state_lock();
569        let mut features = Vec::new();
570        assert_eq!(features_(std::ptr::null(), &mut features), 0);
571    }
572
573    // ═══════════════════════════════════════════════════════════════════
574    // Additional C-parity tests for Src/Modules/socket.c bin_zsocket
575    // option handling.
576    // ═══════════════════════════════════════════════════════════════════
577
578    fn ops_with_flag(flag: u8) -> options {
579        let mut ind = [0u8; crate::ported::zsh_h::MAX_OPS];
580        ind[flag as usize] = 1;
581        options {
582            ind,
583            args: Vec::new(),
584            argscount: 0,
585            argsalloc: 0,
586        }
587    }
588
589    /// c:88 — `bin_zsocket -l` with no args returns 1 ("requires arg").
590    #[test]
591    fn bin_zsocket_l_no_args_returns_one() {
592        let _g = crate::test_util::global_state_lock();
593        let ops = ops_with_flag(b'l');
594        let r = bin_zsocket("zsocket", &[], &ops, 0);
595        assert_eq!(r, 1, "-l with no args → usage error");
596    }
597
598    /// c:57 — `bin_zsocket` with no flags or args runs without panic
599    /// (no-op invocation; returns 1 for missing operation per usage check).
600    #[test]
601    fn bin_zsocket_no_args_no_panic() {
602        let _g = crate::test_util::global_state_lock();
603        let ops = options {
604            ind: [0; crate::ported::zsh_h::MAX_OPS],
605            args: Vec::new(),
606            argscount: 0,
607            argsalloc: 0,
608        };
609        let _ = bin_zsocket("zsocket", &[], &ops, 0);
610        // No panic = pass.
611    }
612
613    /// c:64-65 — `-v` flag alone (verbose, no connect/listen) returns
614    /// nonzero per usage check (verbose without action).
615    #[test]
616    fn bin_zsocket_v_alone_no_panic() {
617        let _g = crate::test_util::global_state_lock();
618        let ops = ops_with_flag(b'v');
619        let _ = bin_zsocket("zsocket", &[], &ops, 0);
620    }
621
622    /// c:70-75 — `-d` flag set runs the targetfd validation path.
623    /// Pin no-panic only (OPT_ARG wiring details vary between ports).
624    #[test]
625    fn bin_zsocket_d_flag_no_panic() {
626        let _g = crate::test_util::global_state_lock();
627        let ops = ops_with_flag(b'd');
628        let _ = bin_zsocket("zsocket", &[], &ops, 0);
629    }
630
631    /// c:67-68 — `-t` (test) flag alone doesn't panic.
632    #[test]
633    fn bin_zsocket_t_alone_no_panic() {
634        let _g = crate::test_util::global_state_lock();
635        let ops = ops_with_flag(b't');
636        let _ = bin_zsocket("zsocket", &[], &ops, 0);
637    }
638
639    /// c:351 — `setup_(NULL)` returns 0 (split from combined test).
640    #[test]
641    fn socket_setup_returns_zero_pin() {
642        let _g = crate::test_util::global_state_lock();
643        assert_eq!(setup_(std::ptr::null()), 0);
644    }
645
646    /// c:373 — `boot_(NULL)` returns 0.
647    #[test]
648    fn socket_boot_returns_zero_pin() {
649        let _g = crate::test_util::global_state_lock();
650        assert_eq!(boot_(std::ptr::null()), 0);
651    }
652
653    /// c:380 — `cleanup_(NULL)` returns 0.
654    #[test]
655    fn socket_cleanup_returns_zero_pin() {
656        let _g = crate::test_util::global_state_lock();
657        assert_eq!(cleanup_(std::ptr::null()), 0);
658    }
659
660    /// c:387 — `finish_(NULL)` returns 0.
661    #[test]
662    fn socket_finish_returns_zero_pin() {
663        let _g = crate::test_util::global_state_lock();
664        assert_eq!(finish_(std::ptr::null()), 0);
665    }
666
667    // ═══════════════════════════════════════════════════════════════════
668    // Additional C-parity tests for Src/Modules/socket.c
669    // c:21 bin_zsocket / c:351-387 lifecycle
670    // ═══════════════════════════════════════════════════════════════════
671
672    /// c:21 — `bin_zsocket` return value in u8 exit-code range.
673    #[test]
674    fn bin_zsocket_return_in_exit_code_range() {
675        let _g = crate::test_util::global_state_lock();
676        let ops = empty_ops();
677        for args in [
678            vec![],
679            vec!["/tmp/zshrs_test_sock".to_string()],
680            vec!["".to_string()],
681        ] {
682            let r = bin_zsocket("zsocket", &args, &ops, 0);
683            assert!(
684                (0..256).contains(&r),
685                "exit code {} must fit in u8 range for {:?}",
686                r,
687                args
688            );
689        }
690    }
691
692    /// c:21 — `bin_zsocket` empty socket path returns nonzero.
693    #[test]
694    fn bin_zsocket_empty_path_returns_nonzero() {
695        let _g = crate::test_util::global_state_lock();
696        let ops = empty_ops();
697        let r = bin_zsocket("zsocket", &["".to_string()], &ops, 0);
698        assert_ne!(r, 0, "empty path → error");
699    }
700
701    /// c:21 — `bin_zsocket` is deterministic for no-args.
702    #[test]
703    fn bin_zsocket_no_args_deterministic() {
704        let _g = crate::test_util::global_state_lock();
705        let ops = empty_ops();
706        let first = bin_zsocket("zsocket", &[], &ops, 0);
707        for _ in 0..5 {
708            assert_eq!(bin_zsocket("zsocket", &[], &ops, 0), first);
709        }
710    }
711
712    /// c:21 — `bin_zsocket -l` and `bin_zsocket -a` with non-numeric
713    /// args don't panic.
714    ///
715    /// `-l <path>` calls `bind(2)` on the path, which CREATES a Unix
716    /// socket file at that path; `-a <path>` connects but also can
717    /// touch the filesystem. The prior bare `"abc"` / `"xyz"` args
718    /// left strays in the repo root that surfaced in `git status`
719    /// after every `cargo test` run. Route through `$TMPDIR` and
720    /// pre+post cleanup.
721    #[test]
722    fn bin_zsocket_a_l_flags_with_arbitrary_arg_no_panic() {
723        let _g = crate::test_util::global_state_lock();
724        let xyz_path = std::env::temp_dir().join("__zshrs_test_zsocket_xyz");
725        let abc_path = std::env::temp_dir().join("__zshrs_test_zsocket_abc");
726        let _ = std::fs::remove_file(&xyz_path);
727        let _ = std::fs::remove_file(&abc_path);
728        let mut ops = empty_ops();
729        ops.ind[b'a' as usize] = 1;
730        let _ = bin_zsocket("zsocket", &[xyz_path.to_string_lossy().into()], &ops, 0);
731        let mut ops2 = empty_ops();
732        ops2.ind[b'l' as usize] = 1;
733        let _ = bin_zsocket("zsocket", &[abc_path.to_string_lossy().into()], &ops2, 0);
734        let _ = std::fs::remove_file(&xyz_path);
735        let _ = std::fs::remove_file(&abc_path);
736    }
737
738    /// c:351-387 — full lifecycle setup→features→enables→boot→cleanup→finish.
739    #[test]
740    fn socket_full_lifecycle_returns_zero_for_all() {
741        let _g = crate::test_util::global_state_lock();
742        let null = std::ptr::null();
743        assert_eq!(setup_(null), 0);
744        let mut feats = Vec::new();
745        let _ = features_(null, &mut feats);
746        let mut enables: Option<Vec<i32>> = None;
747        let _ = enables_(null, &mut enables);
748        assert_eq!(boot_(null), 0);
749        assert_eq!(cleanup_(null), 0);
750        assert_eq!(finish_(null), 0);
751    }
752
753    /// c:351 — setup_ idempotent.
754    #[test]
755    fn socket_setup_idempotent() {
756        let _g = crate::test_util::global_state_lock();
757        for _ in 0..10 {
758            assert_eq!(setup_(std::ptr::null()), 0);
759        }
760    }
761
762    /// c:387 — finish_ idempotent.
763    #[test]
764    fn socket_finish_idempotent() {
765        let _g = crate::test_util::global_state_lock();
766        for _ in 0..10 {
767            assert_eq!(finish_(std::ptr::null()), 0);
768        }
769    }
770
771    /// c:380 — cleanup_ idempotent.
772    #[test]
773    fn socket_cleanup_idempotent() {
774        let _g = crate::test_util::global_state_lock();
775        for _ in 0..10 {
776            assert_eq!(cleanup_(std::ptr::null()), 0);
777        }
778    }
779
780    /// c:373 — boot_ idempotent.
781    #[test]
782    fn socket_boot_idempotent() {
783        let _g = crate::test_util::global_state_lock();
784        for _ in 0..10 {
785            assert_eq!(boot_(std::ptr::null()), 0);
786        }
787    }
788
789    /// c:21 — `bin_zsocket` with multibyte path doesn't panic.
790    #[test]
791    fn bin_zsocket_multibyte_path_no_panic() {
792        let _g = crate::test_util::global_state_lock();
793        let ops = empty_ops();
794        let _ = bin_zsocket("zsocket", &["/tmp/日本".to_string()], &ops, 0);
795    }
796
797    // ═══════════════════════════════════════════════════════════════════
798    // Additional C-parity tests for Src/Modules/socket.c
799    // c:21 bin_zsocket + c:351-387 lifecycle type/exit-code pins
800    // ═══════════════════════════════════════════════════════════════════
801
802    /// c:21 — `bin_zsocket` returns i32 (compile-time pin).
803    #[test]
804    fn bin_zsocket_returns_i32_type() {
805        let _g = crate::test_util::global_state_lock();
806        let ops = empty_ops();
807        let _: i32 = bin_zsocket("zsocket", &[], &ops, 0);
808    }
809
810    /// c:21 — `bin_zsocket` exit codes are non-negative.
811    #[test]
812    fn bin_zsocket_exit_codes_non_negative() {
813        let _g = crate::test_util::global_state_lock();
814        let ops = empty_ops();
815        for argv in [
816            vec![],
817            vec!["/tmp/sock".into()],
818            vec!["".into()],
819            vec!["/tmp/sock".into(), "extra".into()],
820        ] {
821            let r = bin_zsocket("zsocket", &argv, &ops, 0);
822            assert!(
823                r >= 0,
824                "exit code must be non-negative, got {} for {:?}",
825                r,
826                argv
827            );
828        }
829    }
830
831    /// c:21 — `bin_zsocket` empty path is deterministic (no hidden state).
832    #[test]
833    fn bin_zsocket_empty_path_deterministic() {
834        let _g = crate::test_util::global_state_lock();
835        let ops = empty_ops();
836        let first = bin_zsocket("zsocket", &["".to_string()], &ops, 0);
837        for _ in 0..5 {
838            assert_eq!(
839                bin_zsocket("zsocket", &["".to_string()], &ops, 0),
840                first,
841                "empty-path zsocket must be pure across calls"
842            );
843        }
844    }
845
846    /// c:21 — `bin_zsocket` with both -a and -l flags is safe (no panic).
847    /// C source resolves precedence inside the body; what matters here
848    /// is that calling with both flags set doesn't crash.
849    #[test]
850    fn bin_zsocket_both_a_and_l_flags_no_panic() {
851        let _g = crate::test_util::global_state_lock();
852        let mut ops = empty_ops();
853        ops.ind[b'a' as usize] = 1;
854        ops.ind[b'l' as usize] = 1;
855        let _ = bin_zsocket("zsocket", &["/tmp/sock".to_string()], &ops, 0);
856    }
857
858    /// c:21 — `bin_zsocket` -d <fd> with non-numeric arg doesn't panic.
859    #[test]
860    fn bin_zsocket_d_flag_with_non_numeric_no_panic() {
861        let _g = crate::test_util::global_state_lock();
862        let mut ops = empty_ops();
863        ops.ind[b'd' as usize] = 1;
864        let _ = bin_zsocket("zsocket", &["not-a-number".to_string()], &ops, 0);
865    }
866
867    /// c:21 — `bin_zsocket` with very long path doesn't panic.
868    #[test]
869    fn bin_zsocket_long_path_no_panic() {
870        let _g = crate::test_util::global_state_lock();
871        let ops = empty_ops();
872        let path = "/tmp/".to_string() + &"x".repeat(2000);
873        let _ = bin_zsocket("zsocket", &[path], &ops, 0);
874    }
875
876    /// c:351 — `setup_` returns i32 (compile-time pin).
877    #[test]
878    fn socket_setup_returns_i32_type() {
879        let _g = crate::test_util::global_state_lock();
880        let _: i32 = setup_(std::ptr::null());
881    }
882
883    /// c:358 — `features_` returns i32 (compile-time pin).
884    #[test]
885    fn socket_features_returns_i32_type() {
886        let _g = crate::test_util::global_state_lock();
887        let mut v: Vec<String> = Vec::new();
888        let _: i32 = features_(std::ptr::null(), &mut v);
889    }
890
891    /// c:366 — `enables_` returns i32 with None enables-out param safe.
892    #[test]
893    fn socket_enables_with_none_returns_i32() {
894        let _g = crate::test_util::global_state_lock();
895        let mut e: Option<Vec<i32>> = None;
896        let _: i32 = enables_(std::ptr::null(), &mut e);
897    }
898
899    /// c:366 — `enables_` deterministic for null callback.
900    #[test]
901    fn socket_enables_deterministic_for_null_in() {
902        let _g = crate::test_util::global_state_lock();
903        let mut a: Option<Vec<i32>> = None;
904        let first = enables_(std::ptr::null(), &mut a);
905        for _ in 0..3 {
906            let mut b: Option<Vec<i32>> = None;
907            assert_eq!(
908                enables_(std::ptr::null(), &mut b),
909                first,
910                "enables_ must be deterministic"
911            );
912        }
913    }
914
915    /// c:351/358/366/373/380/387 — each lifecycle hook returns 0 individually.
916    #[test]
917    fn socket_each_lifecycle_hook_returns_zero_individually() {
918        let _g = crate::test_util::global_state_lock();
919        let null = std::ptr::null();
920        let mut v: Vec<String> = Vec::new();
921        let mut e: Option<Vec<i32>> = None;
922        assert_eq!(setup_(null), 0, "c:351 setup_");
923        assert_eq!(features_(null, &mut v), 0, "c:358 features_");
924        assert_eq!(enables_(null, &mut e), 0, "c:366 enables_");
925        assert_eq!(boot_(null), 0, "c:373 boot_");
926        assert_eq!(cleanup_(null), 0, "c:380 cleanup_");
927        assert_eq!(finish_(null), 0, "c:387 finish_");
928    }
929
930    // ═══════════════════════════════════════════════════════════════════
931    // Additional C-parity pins for Src/Modules/socket.c
932    // c:21 bin_zsocket / c:351-387 lifecycle hooks
933    // ═══════════════════════════════════════════════════════════════════
934
935    /// c:351 — `setup_` is idempotent.
936    #[test]
937    fn socket_setup_idempotent_repeated_calls() {
938        let _g = crate::test_util::global_state_lock();
939        for _ in 0..10 {
940            assert_eq!(setup_(std::ptr::null()), 0);
941        }
942    }
943
944    /// c:380 — `cleanup_` is idempotent.
945    #[test]
946    fn socket_cleanup_idempotent_repeated_calls() {
947        let _g = crate::test_util::global_state_lock();
948        for _ in 0..10 {
949            assert_eq!(cleanup_(std::ptr::null()), 0);
950        }
951    }
952
953    /// c:387 — `finish_` is idempotent.
954    #[test]
955    fn socket_finish_idempotent_repeated_calls() {
956        let _g = crate::test_util::global_state_lock();
957        for _ in 0..10 {
958            assert_eq!(finish_(std::ptr::null()), 0);
959        }
960    }
961
962    /// c:380 — `cleanup_` return type i32 (compile-time pin).
963    #[test]
964    fn socket_cleanup_returns_i32_type() {
965        let _g = crate::test_util::global_state_lock();
966        let _: i32 = cleanup_(std::ptr::null());
967    }
968
969    /// c:387 — `finish_` return type i32 (compile-time pin).
970    #[test]
971    fn socket_finish_returns_i32_type() {
972        let _g = crate::test_util::global_state_lock();
973        let _: i32 = finish_(std::ptr::null());
974    }
975
976    /// c:373 — `boot_` return type i32 (compile-time pin).
977    #[test]
978    fn socket_boot_returns_i32_type() {
979        let _g = crate::test_util::global_state_lock();
980        let _: i32 = boot_(std::ptr::null());
981    }
982
983    /// c:21 — `bin_zsocket` empty args returns non-negative.
984    #[test]
985    fn bin_zsocket_empty_args_non_negative() {
986        let _g = crate::test_util::global_state_lock();
987        let ops = crate::ported::zsh_h::options {
988            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
989            args: Vec::new(),
990            argscount: 0,
991            argsalloc: 0,
992        };
993        let r = bin_zsocket("zsocket", &[], &ops, 0);
994        assert!(r >= 0, "bin_zsocket empty args must return ≥ 0, got {}", r);
995    }
996
997    /// c:21 — `bin_zsocket` various func values don't panic.
998    #[test]
999    fn bin_zsocket_various_func_values_no_panic() {
1000        let _g = crate::test_util::global_state_lock();
1001        let ops = crate::ported::zsh_h::options {
1002            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1003            args: Vec::new(),
1004            argscount: 0,
1005            argsalloc: 0,
1006        };
1007        for func in [-1, 0, 1, 100, i32::MAX] {
1008            let _ = bin_zsocket("zsocket", &[], &ops, func);
1009        }
1010    }
1011
1012    /// c:21 — `bin_zsocket` deterministic for identical args.
1013    #[test]
1014    fn bin_zsocket_deterministic_identical_args() {
1015        let _g = crate::test_util::global_state_lock();
1016        let ops = crate::ported::zsh_h::options {
1017            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
1018            args: Vec::new(),
1019            argscount: 0,
1020            argsalloc: 0,
1021        };
1022        let args = vec!["test".to_string()];
1023        let r1 = bin_zsocket("zsocket", &args, &ops, 0);
1024        let r2 = bin_zsocket("zsocket", &args, &ops, 0);
1025        assert_eq!(r1, r2, "bin_zsocket must be deterministic");
1026    }
1027
1028    /// c:351 — `setup_` returns i32 type.
1029    #[test]
1030    fn socket_setup_returns_i32_type_alt_pin() {
1031        let _g = crate::test_util::global_state_lock();
1032        let _: i32 = setup_(std::ptr::null());
1033    }
1034
1035    /// c:366 — `enables_` with Some(non-empty) doesn't panic.
1036    #[test]
1037    fn socket_enables_with_some_non_empty_no_panic() {
1038        let _g = crate::test_util::global_state_lock();
1039        let mut e: Option<Vec<i32>> = Some(vec![1, 2, 3]);
1040        let _ = enables_(std::ptr::null(), &mut e);
1041    }
1042
1043    /// c:351/373 — setup_ then boot_ sequence returns 0 from both.
1044    #[test]
1045    fn socket_setup_then_boot_returns_zero_each() {
1046        let _g = crate::test_util::global_state_lock();
1047        let null = std::ptr::null();
1048        assert_eq!(setup_(null), 0);
1049        assert_eq!(boot_(null), 0);
1050    }
1051}