zsh/ported/modules/zftp.rs
1//! ZFTP module - port of Modules/zftp.c
2//!
3//! it's a TELNET based protocol, but don't think I like doing this // c:56
4//! Number of connections actually open // c:210
5//! zfclosing is set if zftp_close() is active // c:219
6//! List of active sessions // c:310
7//!
8//! Provides a builtin FTP client for zsh.
9
10use crate::ported::builtin::{LASTVAL, SFCONTEXT};
11use crate::ported::params::getiparam;
12use crate::ported::utils::{errflag, getshfunc, zwarnnam};
13use crate::ported::zsh_h::{module, options, SFC_HOOK};
14use indexmap::IndexMap;
15use std::io::{self, BufRead, BufReader, Read, Write};
16use std::net::{TcpStream, ToSocketAddrs};
17use std::os::unix::io::AsRawFd;
18use std::path::Path;
19use std::sync::atomic::Ordering;
20use std::sync::{Mutex, OnceLock};
21use std::time::Duration;
22
23/// Port of `zftp_session(UNUSED(char *name), char **args, UNUSED(int flags))` from `Src/Modules/zftp.c:2889`.
24#[allow(unused_variables)]
25pub fn zftp_session(name: &str, args: &[&str], flags: i32) -> i32 {
26 // c:2889
27 if args.is_empty() {
28 // c:2889
29 // c:2892-2895 — walk zfsessions list, print each name.
30 if let Ok(state) = zftp_state().lock() {
31 for sess_name in state.sessions.keys() {
32 // c:2894
33 println!("{}", sess_name); // c:2895
34 }
35 }
36 return 0; // c:2896
37 }
38 // c:2903-2904 — no-op if already in the requested session.
39 let current = zftp_state()
40 .lock()
41 .ok()
42 .and_then(|s| s.current.clone())
43 .unwrap_or_default();
44 if args[0] == current {
45 return 0; // c:2915
46 }
47 savesession(); // c:2915
48 switchsession(args[0]); // c:2915
49 0 // c:2915
50}
51
52/// Port of `typedef struct zftp_session *Zftp_session;` from
53/// `Src/Modules/zftp.c:50`. Pointer-style typedef alias used by every
54/// `zftp_*` callsite that takes a session arg.
55#[allow(non_camel_case_types)]
56pub type Zftp_session = Box<zftp_session>;
57
58// =====================================================================
59// `struct zfheader` from `Src/Modules/zftp.c:114` — block-mode header.
60// =====================================================================
61
62/// Port of `struct zfheader` from `Src/Modules/zftp.c:114`.
63/// ```c
64/// struct zfheader {
65/// char flags;
66/// unsigned char bytes[2];
67/// };
68/// ```
69#[allow(non_camel_case_types)]
70pub struct zfheader {
71 pub flags: i8, // c:115
72 pub bytes: [u8; 2], // c:116
73}
74
75// =====================================================================
76// `struct zftpcmd` from `Src/Modules/zftp.c:128` — subcommand entry.
77// =====================================================================
78
79/// Port of `struct zftpcmd` from `Src/Modules/zftp.c:128`.
80/// ```c
81/// struct zftpcmd {
82/// const char *nam;
83/// int (*fun) (char *, char **, int);
84/// int min, max, flags;
85/// };
86/// ```
87#[allow(non_camel_case_types)]
88pub struct zftpcmd {
89 pub nam: &'static str, // c:129
90 pub fun: fn(&str, &[&str], i32) -> i32, // c:130
91 pub min: i32, // c:131
92 /// `max` field.
93 pub max: i32,
94 /// `flags` field.
95 pub flags: i32,
96}
97
98/// Port of `typedef struct zftpcmd *Zftpcmd` from `Src/Modules/zftp.c:151`.
99#[allow(non_camel_case_types)]
100pub type Zftpcmd = Box<zftpcmd>;
101
102/// `lastcode` — file-scope global from `Src/Modules/zftp.c:228`:
103/// `static int lastcode;`. Numeric form of `lastcodestr`.
104pub static lastcode: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
105
106/// `ZFST_TYPE(x)` macro — extract type-flag bits.
107/// Port of `#define ZFST_TYPE(x) (x & ZFST_TMSK)` from
108/// `Src/Modules/zftp.c`.
109#[allow(non_snake_case)]
110#[inline]
111pub fn ZFST_TYPE(x: i32) -> i32 {
112 x & ZFST_TMSK
113}
114
115/// `ZFST_MODE(x)` macro — extract mode-flag bits.
116/// Port of `#define ZFST_MODE(x) (x & ZFST_MMSK)` from
117/// `Src/Modules/zftp.c`.
118#[allow(non_snake_case)]
119#[inline]
120pub fn ZFST_MODE(x: i32) -> i32 {
121 x & ZFST_MMSK
122}
123
124/// Port of `struct zftp_session` from `Src/Modules/zftp.c:299`.
125///
126/// C definition (verbatim):
127/// ```c
128/// struct zftp_session {
129/// char *name; /* name of session */
130/// char **params; /* parameters ordered as in zfparams */
131/// char **userparams; /* user parameters set by zftp_params */
132/// FILE *cin; /* control input file */
133/// Tcp_session control; /* the control connection */
134/// int dfd; /* data connection */
135/// int has_size; /* understands SIZE? */
136/// int has_mdtm; /* understands MDTM? */
137/// };
138///
139/// typedef struct zftp_session *Zftp_session; // c:50
140/// ```
141///
142/// Field names + order match C exactly. `cin` (control input file) is
143/// modelled as `Option<TcpStream>` since Rust doesn't expose libc
144/// FILE* directly; `control` (the Tcp_session) collapses into the
145/// same TcpStream slot in the static-link path.
146#[derive(Debug)]
147#[allow(non_camel_case_types)]
148pub struct zftp_session {
149 pub name: String, // c:300 char *name
150 pub params: Vec<String>, // c:301 char **params
151 pub userparams: Vec<String>, // c:302 char **userparams
152 pub cin: Option<TcpStream>, // c:303 FILE *cin (control input)
153 pub control: Option<TcpStream>, // c:304 Tcp_session control
154 pub dfd: i32, // c:305 int dfd
155 pub has_size: i32, // c:306 int has_size
156 pub has_mdtm: i32, // c:307 int has_mdtm
157
158 // Below: ergonomic Rust fields not in C `struct zftp_session` but
159 // needed by the Rust wrapper to track connection state without the
160 // C `params` array indexing convention. Document the mapping back
161 // to C `params[]` slots in comments.
162 pub host: Option<String>, // C: params[ZFPM_HOST]
163 pub port: u16, // C: params[ZFPM_PORT] (parsed)
164 pub user: Option<String>, // C: params[ZFPM_USER]
165 pub pwd: Option<String>, // C: params[ZFPM_PASSWORD]
166 pub connected: bool, // C: cin != NULL
167 pub logged_in: bool, // C: derived from greeting parse
168 /// `transfer_type` field. Mirrors the ZFST_TYPE bits of
169 /// `zfstatusp[zfsessno]` (c:267 `#define ZFST_TYPE(x) (x & ZFST_TMSK)`).
170 /// This is the "next transfer type" the user has requested.
171 pub transfer_type: i32,
172 /// `current_type` field. Mirrors the ZFST_CTYP bits of
173 /// `zfstatusp[zfsessno]` (c:272 `#define ZFST_CTYP(x) ((x >> ZFST_TBIT)
174 /// & ZFST_TMSK)`). This is the type currently negotiated with the
175 /// server. zfsettype (c:2405) sends TYPE only when transfer_type !=
176 /// current_type and updates current_type after a successful response.
177 pub current_type: i32,
178 /// `transfer_mode` field.
179 pub transfer_mode: i32,
180 /// `passive` field.
181 pub passive: bool,
182 /// Mirrors the ZFST_SYST bit of `zfstatusp[zfsessno]` (c:240).
183 /// Set after a successful SYST probe so re-login on the same
184 /// session doesn't re-issue SYST.
185 pub syst_probed: bool,
186 /// Mirrors the ZFST_NOPS bit of `zfstatusp[zfsessno]` (c:240).
187 /// Set after the server returns 5xx for PASV so subsequent
188 /// `zfopendata` calls skip PASV and go directly to PORT mode.
189 /// Reset to false on session re-open.
190 pub nops_probed: bool,
191 /// Mirrors the ZFST_TRSZ bit (0x0080, "tried getting `size' from
192 /// reply"). Set by zfgetdata's first RETR (c:1102) regardless of
193 /// whether the size hint parsed; gates the c:1093 "only parse the
194 /// 150 reply on the FIRST RETR" check and the c:2577 SIZE-probe
195 /// cache test in zfgetput.
196 pub trsz: bool,
197 /// Mirrors the ZFST_NOSZ bit (0x0040, "server doesn't send
198 /// `(XXXX bytes)' reply"). Set alongside trsz at c:1102, CLEARED
199 /// at c:1109 when the byte-count hint parses — trsz && !nosz means
200 /// the server embeds sizes in 150 replies, so zfgetput skips the
201 /// separate SIZE round-trip (c:2577-2585).
202 pub nosz: bool,
203}
204
205/// `zfprefs` — file-scope `static int zfprefs;` from
206/// Src/Modules/zftp.c:218. Bitfield of ZFPF_SNDP|ZFPF_PASV|ZFPF_DUMB.
207/// Default set by boot_ (c:3206) to ZFPF_SNDP|ZFPF_PASV.
208#[allow(non_upper_case_globals)]
209pub static zfprefs: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0); // c:218
210
211/// Port of `zfhandler(int sig)` from `Src/Modules/zftp.c:366`.
212/// C: `static void zfhandler(int sig)` — SIGALRM handler. Sets the
213/// `zfdrrrring` flag so the next zfread/zfgetline returns -1 and exits
214/// its setjmp-protected critical section.
215#[allow(non_snake_case)]
216pub extern "C" fn zfhandler(sig: i32) {
217 // c:366
218 if sig == libc::SIGALRM {
219 // c:368
220 ZFDRRRRING.store(1, Ordering::Relaxed); // c:369
221 // c:370-374 — errno = ETIMEDOUT (or EIO).
222 unsafe {
223 *errno_ptr() = libc::ETIMEDOUT;
224 }
225 // c:375 — longjmp(zfalrmbuf, 1). Rust port doesn't use setjmp;
226 // the ZFDRRRRING flag is the timeout signal each blocking
227 // read/write polls.
228 }
229 // c:377 DPUTS — unreachable in static-link path.
230}
231
232/// Port of `zfalarm(int tmout)` from `Src/Modules/zftp.c:384`.
233/// C: `static void zfalarm(int tmout)` — set up alarm + SIGALRM handler.
234#[allow(non_snake_case)]
235pub fn zfalarm(tmout: i32) {
236 // c:384
237 ZFDRRRRING.store(0, Ordering::Relaxed); // c:384
238 // c:387-392 — fire alarm even when tmout is 0 so a pending non-zero
239 // main-shell alarm doesn't bleed into the FTP code path.
240 if ZFALARMED.load(Ordering::Relaxed) != 0 {
241 // c:393
242 unsafe {
243 libc::alarm(tmout as u32);
244 } // c:394
245 return; // c:395
246 }
247 // c:397 — signal(SIGALRM, zfhandler);
248 unsafe {
249 libc::signal(libc::SIGALRM, zfhandler as libc::sighandler_t);
250 }
251 // c:398 — oalremain = alarm(tmout);
252 let oalremain = unsafe { libc::alarm(tmout as u32) };
253 OALREMAIN.store(oalremain, Ordering::Relaxed);
254 if oalremain != 0 {
255 // c:399
256 // c:400 — oaltime = zmonotime(NULL);
257 let now = std::time::SystemTime::now()
258 .duration_since(std::time::UNIX_EPOCH)
259 .map(|d| d.as_secs() as i64)
260 .unwrap_or(0);
261 OALTIME.store(now, Ordering::Relaxed);
262 }
263 ZFALARMED.store(1, Ordering::Relaxed); // c:405
264}
265
266/// Port of `zfpipe()` from `Src/Modules/zftp.c:412`.
267/// C: `static void zfpipe(void)` — ignore SIGPIPE so write() returns
268/// EPIPE instead of killing the shell.
269#[allow(non_snake_case)]
270pub fn zfpipe() {
271 // c:412
272 // c:412 — signal(SIGPIPE, SIG_IGN);
273 unsafe {
274 libc::signal(libc::SIGPIPE, libc::SIG_IGN);
275 }
276}
277
278/// Port of `zfunalarm()` from Src/Modules/zftp.c:422.
279/// C: `void zfunalarm(void)` — restores the prior alarm if `oalremain`
280/// was nonzero, else cancels with `alarm(0)`. Adjusts for elapsed time.
281#[allow(non_snake_case)]
282pub fn zfunalarm() {
283 // c:422
284 let oalremain = OALREMAIN.load(Ordering::Relaxed); // c:422
285 if oalremain != 0 {
286 // c:423
287 // c:432-433 — `time_t tdiff = zmonotime(NULL) - oaltime;`
288 let oaltime = OALTIME.load(Ordering::Relaxed); // c:432
289 let now = std::time::SystemTime::now()
290 .duration_since(std::time::UNIX_EPOCH)
291 .map(|d| d.as_secs() as i64)
292 .unwrap_or(0);
293 let tdiff = now - oaltime; // c:432
294 let secs = if (oalremain as i64) < tdiff {
295 1
296 } else {
297 // c:434
298 (oalremain as i64 - tdiff) as u32
299 };
300 unsafe {
301 libc::alarm(secs);
302 } // c:453
303 } else {
304 unsafe {
305 libc::alarm(0);
306 } // c:453
307 }
308}
309
310/// Port of `zfunpipe()` from Src/Modules/zftp.c:453.
311/// C: `void zfunpipe(void)` — restores the SIGPIPE disposition that
312/// existed before `zfpipe()` ignored it.
313#[allow(non_snake_case)]
314pub fn zfunpipe() {
315 // c:453
316 // c:453 — `if (sigtrapped[SIGPIPE]) { ... } else signal_default(SIGPIPE);`
317 // The static-link path doesn't expose `sigtrapped[]`/`siglists[]` yet,
318 // so reset to default disposition unconditionally — matches the
319 // common case where SIGPIPE wasn't trapped.
320 unsafe {
321 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
322 } // c:460
323}
324
325/// Port of `zfmovefd(int fd)` from `Src/Modules/zftp.c:472`.
326///
327/// Direct line-by-line port. Bumps an fd past the reserved
328/// shell-internal range (0..=9) via `fcntl(F_DUPFD, 10)` then
329/// closes the original — keeps ZFTP control sockets out of the
330/// user's redirection range. Returns -1 on dup failure.
331#[allow(non_snake_case)]
332pub fn zfmovefd(fd: i32) -> i32 {
333 // c:474 — `if (fd != -1 && fd < 10) { ... }`
334 if fd != -1 && fd < 10 {
335 // c:476 — `int fe = fcntl(fd, F_DUPFD, 10);`
336 let fe = unsafe { libc::fcntl(fd, libc::F_DUPFD, 10) };
337 // c:480 — `close(fd);`
338 unsafe {
339 libc::close(fd);
340 }
341 // c:481 — `fd = fe;`
342 return fe;
343 }
344 fd // c:483
345}
346
347/// Port of `zfsetparam(char *name, void *val, int flags)` from `Src/Modules/zftp.c:494`.
348/// C: `static void zfsetparam(char *name, void *val, int flags)` — install
349/// the named ZFTP_* param via assignsparam, applying PM_READONLY when the
350/// ZFPM_READONLY flag is set.
351#[allow(non_snake_case)]
352pub fn zfsetparam(name: &str, val: &str, flags: i32) {
353 // c:494
354 // c:494 — int type = (flags & ZFPM_INTEGER) ? PM_INTEGER : PM_SCALAR;
355 // Rust setsparam doesn't yet distinguish int vs scalar at creation;
356 // the underlying assignsparam path stores both as strings, and
357 // PM_INTEGER conversion happens at read time via getstrvalue.
358 let _ = flags & ZFPM_INTEGER;
359
360 // c:499-509 — getnode + IFUNSET / PM_UNSET handling. The Rust paramtab
361 // doesn't expose IFUNSET semantics yet — assignsparam always writes.
362 if (flags & ZFPM_IFUNSET) != 0 {
363 // c:507
364 // Only set if not currently set. Best-effort check via env lookup
365 // since paramtab isn't bucket-2 consolidated for the executor.
366 // c:508 — `if (pm = (Param)paramtab->getnode(paramtab, name))`.
367 // C exits if the param already exists. Check paramtab,
368 // not OS env.
369 if crate::ported::params::paramtab()
370 .read()
371 .map_or(false, |t| t.contains_key(name))
372 {
373 return; // c:508-509 pm = NULL → skip
374 }
375 }
376
377 // c:516-519 — pm->gsu.{i,s}->setfn(pm, val).
378 // Faithful port of c:499-506: when the param doesn't exist
379 // (or is PM_UNSET), createparam + apply PM_READONLY when
380 // ZFPM_READONLY is set. Then call setsparam to install the
381 // value.
382 //
383 // Prior port silently dropped the PM_READONLY flag — the param
384 // got created without it, so subsequent user writes to
385 // ZFTP_* (e.g. \$ZFTP_HOST after open) silently succeeded
386 // instead of getting the 'read-only variable' error C emits.
387 let needs_create = !crate::ported::params::paramtab()
388 .read()
389 .map(|t| {
390 // c:499-500 — `!getnode2 || PM_UNSET`. Treat absent or
391 // PM_UNSET as "create new".
392 t.get(name)
393 .map(|p| (p.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) != 0)
394 .unwrap_or(true)
395 })
396 .unwrap_or(true);
397 // c:497 — `int type = (flags & ZFPM_INTEGER) ? PM_INTEGER : PM_SCALAR;`
398 let want_type: u32 = if (flags & ZFPM_INTEGER) != 0 {
399 crate::ported::zsh_h::PM_INTEGER
400 } else {
401 crate::ported::zsh_h::PM_SCALAR
402 };
403 if needs_create {
404 // c:505 — `if ((pm = createparam(name, type)) ...`
405 let _ = crate::ported::params::createparam(name, want_type as i32);
406 // c:505-506 — `&& (flags & ZFPM_READONLY) ...
407 // pm->node.flags |= PM_READONLY;`
408 if (flags & ZFPM_READONLY) != 0 {
409 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
410 if let Some(pm) = tab.get_mut(name) {
411 pm.node.flags |= crate::ported::zsh_h::PM_READONLY as i32;
412 }
413 }
414 }
415 }
416 // c:510-514 — `if (!pm || PM_TYPE(pm->node.flags) != type) {
417 // if (type == PM_SCALAR) zsfree((char *)val);
418 // return;
419 // }`
420 //
421 // Type-mismatch guard. After createparam (or the IFUNSET skip), the
422 // resolved param may have a different PM_TYPE than `want_type` — for
423 // example, the user did `typeset -i ZFTP_HOST=42` before connecting,
424 // so the existing param is PM_INTEGER while zfsetparam wants
425 // PM_SCALAR. C silently bails to avoid forcing a type conversion;
426 // prior Rust port skipped the guard and let setsparam re-assign
427 // regardless, clobbering the user's typed override.
428 let actual_type = match crate::ported::params::paramtab().read() {
429 Ok(t) => t
430 .get(name)
431 .map(|p| crate::ported::zsh_h::PM_TYPE(p.node.flags as u32)),
432 Err(_) => None,
433 };
434 match actual_type {
435 Some(t) if t == want_type => {} // proceed
436 Some(_) | None => return, // c:514 — wrong type or no param
437 }
438 // c:516-519 — `if (type == PM_INTEGER)
439 // pm->gsu.i->setfn(pm, *(off_t *)val);
440 // else
441 // pm->gsu.s->setfn(pm, (char *)val);`
442 //
443 // Dispatch per the resolved param type. For ZFPM_INTEGER, route
444 // through setiparam so the integer is stored in `u_val` directly
445 // instead of as a string parsed back to int through assignsparam.
446 // Prior port always called setsparam regardless of want_type —
447 // for ZFPM_INTEGER params (ZFTP_PORT, ZFTP_PID, ZFTP_SIZE, etc.)
448 // the value got stored as a String in `u_str`, and `${ZFTP_PORT}`
449 // reads went through the scalar getter producing the decimal
450 // representation. Functionally equivalent for the read side, but
451 // diverged from C's storage shape: a downstream PM_INTEGER-aware
452 // consumer querying `pm->u.val` directly would see 0 instead of
453 // the actual port number.
454 if (flags & ZFPM_INTEGER) != 0 {
455 let n = val.parse::<i64>().unwrap_or(0);
456 let _ = crate::ported::params::setiparam(name, n); // c:517
457 } else {
458 crate::ported::params::setsparam(name, val); // c:519
459 }
460}
461
462/// Port of `zfunsetparam(char *name)` from Src/Modules/zftp.c:529.
463/// C: `static void zfunsetparam(char *name)` — clears PM_READONLY then
464/// calls `unsetparam_pm(pm, 0, 1)`.
465#[allow(non_snake_case)]
466pub fn zfunsetparam(name: &str) {
467 // c:529
468 // Faithful port of c:531-536:
469 // if ((pm = (Param) paramtab->getnode(paramtab, name))) {
470 // pm->node.flags &= ~PM_READONLY;
471 // unsetparam_pm(pm, 0, 1);
472 // }
473 //
474 // Prior port called std::env::remove_var which only touches the
475 // OS environment, not paramtab. ZFTP_* params live in paramtab
476 // (created via createparam in zfsetparam), and many carry
477 // PM_READONLY — std::env::remove_var on a PM_READONLY paramtab
478 // entry would silently no-op for the shell but might clear the
479 // env var ambient state, leading to divergent shell-vs-env views.
480 //
481 // C's empty-name lookup returns NULL (paramtab miss); Rust's
482 // params::unsetparam similarly no-ops on empty.
483 if name.is_empty() {
484 return;
485 }
486 // c:531-535 — clear PM_READONLY first so the unset succeeds even
487 // on readonly params (the whole point of this helper — connection
488 // teardown needs to flush ZFTP_HOST etc. regardless of readonly).
489 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
490 if let Some(pm) = tab.get_mut(name) {
491 pm.node.flags &= !(crate::ported::zsh_h::PM_READONLY as i32);
492 }
493 }
494 // c:535 — unsetparam_pm(pm, 0, 1). The free unsetparam helper
495 // wraps the getnode + unsetparam_pm flow and removes from paramtab.
496 crate::ported::params::unsetparam(name);
497}
498
499/// Port of `zfargstring(char *cmd, char **args)` from `Src/Modules/zftp.c:546`.
500/// C: `char *zfargstring(char *cmd, char **args)` — joins cmd + args.
501#[allow(non_snake_case)]
502pub fn zfargstring(cmd: &str, args: &[&str]) -> String {
503 // c:546-562 — join cmd + space-separated args + CRLF terminator:
504 //
505 // strcpy(line, cmd);
506 // for (aptr = args; *aptr; aptr++) {
507 // strcat(line, " ");
508 // strcat(line, *aptr);
509 // }
510 // strcat(line, "\r\n");
511 //
512 // The "\r\n" is part of THIS fn's contract (c:559) — C callers
513 // (zftp_dir c:2318, zftp_quote c:2695-2696) pass the result to
514 // zfsendcmd verbatim. A prior Rust version omitted it and made
515 // every call site compensate with `+ "\r\n"`.
516 let mut s = cmd.to_string();
517 for a in args {
518 s.push(' '); // c:556
519 s.push_str(a); // c:557
520 }
521 s.push_str("\r\n"); // c:559
522 s
523}
524
525/// Port of `zfgetline(char *ln, int lnsize, int tmout)` from `Src/Modules/zftp.c:571`.
526/// C: `int zfgetline(char *ln, int lnsize, int tmout)` — read a single
527/// CRLF-terminated line from the control connection, handling TELNET
528/// IAC command escapes and SIGALRM-driven timeout.
529#[allow(non_snake_case)]
530pub fn zfgetline(ln: &mut [u8], lnsize: i32, tmout: i32) -> i32 {
531 // c:571
532 // c:573-575 — locals at function top (Rule 5).
533 let mut ch: i32; // c:573 int ch
534 let mut added: i32 = 0; // c:573 added
535 // c:575 — char *pcur = ln, cmdbuf[3];
536 let mut pcur: usize = 0; // pointer index into ln
537 let mut cmdbuf: [u8; 3] = [0; 3];
538
539 ZCFINISH.store(0, Ordering::Relaxed); // c:577 zcfinish = 0
540 let lnsize = lnsize - 1; // c:579 leave room for null
541 if !ln.is_empty() {
542 ln[0] = 0; // c:581 ln[0] = '\0'
543 }
544
545 // c:583-587 — setjmp guard via ZFDRRRRING flag.
546 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
547 // c:583
548 unsafe {
549 libc::alarm(0);
550 } // c:584
551 zwarnnam("zftp", "timeout getting response"); // c:585
552 return 6; // c:586
553 }
554 zfalarm(tmout); // c:588
555
556 // c:597-678 — for (;;) read loop with TELNET IAC handling.
557 let mut state = match zftp_state().lock() {
558 Ok(s) => s,
559 Err(_) => return 6,
560 };
561 let sess = match state
562 .current
563 .clone()
564 .and_then(|k| state.sessions.get_mut(&k))
565 {
566 Some(s) => s,
567 None => return 6,
568 };
569 let stream = match sess.cin.as_mut() {
570 Some(s) => s,
571 None => return 6,
572 };
573 let mut byte = [0u8; 1];
574
575 'main: loop {
576 // c:597 for (;;)
577 // c:598 — ch = fgetc(zfsess->cin);
578 ch = match stream.read(&mut byte) {
579 Ok(0) => -1, // EOF
580 Ok(_) => byte[0] as i32,
581 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, // c:602 EINTR retry
582 Err(_) => -1,
583 };
584
585 match ch {
586 -1 => {
587 // c:601 EOF
588 ZCFINISH.store(2, Ordering::Relaxed); // c:606
589 }
590 0x0d => {
591 // c:609 '\r'
592 ch = match stream.read(&mut byte) {
593 // c:611
594 Ok(0) => -1,
595 Ok(_) => byte[0] as i32,
596 Err(_) => -1,
597 };
598 if ch == -1 {
599 // c:612 EOF
600 ZCFINISH.store(2, Ordering::Relaxed); // c:613
601 } else if ch == 0x0a {
602 // c:616 '\n'
603 ZCFINISH.store(1, Ordering::Relaxed); // c:617
604 } else if ch == 0x00 {
605 // c:620 '\0'
606 ch = 0x0d; // c:621
607 } else {
608 ch = 0x0d; // c:625
609 }
610 }
611 0x0a => {
612 // c:628 '\n' (unexpected)
613 ZCFINISH.store(1, Ordering::Relaxed); // c:630
614 }
615 255 => {
616 // c:633 IAC
617 ch = match stream.read(&mut byte) {
618 // c:638
619 Ok(0) => -1,
620 Ok(_) => byte[0] as i32,
621 Err(_) => -1,
622 };
623 match ch {
624 251 | 252 => {
625 // c:640-641 WILL/WONT
626 ch = match stream.read(&mut byte) {
627 // c:642
628 Ok(0) => -1,
629 Ok(_) => byte[0] as i32,
630 Err(_) => -1,
631 };
632 cmdbuf[0] = 255; // c:644 IAC
633 cmdbuf[1] = 254; // c:645 DONT
634 cmdbuf[2] = ch as u8; // c:646
635 // c:647 — write_loop(zfsess->control->fd, cmdbuf, 3);
636 if let Some(ctrl) = sess.control.as_mut() {
637 let _ = ctrl.write_all(&cmdbuf);
638 }
639 continue 'main; // c:648
640 }
641 253 | 254 => {
642 // c:650-651 DO/DONT
643 ch = match stream.read(&mut byte) {
644 // c:652
645 Ok(0) => -1,
646 Ok(_) => byte[0] as i32,
647 Err(_) => -1,
648 };
649 cmdbuf[0] = 255; // c:654 IAC
650 cmdbuf[1] = 252; // c:655 WONT
651 cmdbuf[2] = ch as u8; // c:656
652 if let Some(ctrl) = sess.control.as_mut() {
653 let _ = ctrl.write_all(&cmdbuf);
654 }
655 continue 'main; // c:658
656 }
657 -1 => {
658 // c:660 EOF
659 ZCFINISH.store(2, Ordering::Relaxed);
660 // c:662
661 }
662 _ => {} // c:665 default
663 }
664 }
665 _ => {}
666 }
667
668 // c:671-672 — if (zcfinish) break;
669 if ZCFINISH.load(Ordering::Relaxed) != 0 {
670 break;
671 }
672 // c:673-676 — if (added < lnsize) { *pcur++ = ch; added++; }
673 if added < lnsize && pcur < ln.len() {
674 ln[pcur] = ch as u8;
675 pcur += 1;
676 added += 1;
677 }
678 // c:677 — junk if no room, keep reading.
679 }
680
681 unsafe {
682 libc::alarm(0);
683 } // c:680
684 if pcur < ln.len() {
685 ln[pcur] = 0; // c:702 *pcur = '\0'
686 }
687 // c:583 setjmp counterpart — alarm fired during the read loop.
688 // ZFDRRRRING was zeroed inside zfalarm; non-zero now means
689 // zfhandler set it during the blocked fgetc. Mirrors the
690 // post-syscall fix added to zfread (cfe7560f58) and zfwrite
691 // (00c1c36dc9). Without it, a SIGALRM during line read drained
692 // through to ZCFINISH unaffected — the caller saw a partial
693 // line with the timeout signal silently lost.
694 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
695 zwarnnam("zftp", "timeout getting response"); // c:585
696 return 6; // c:586
697 }
698 // c:702 — return (zcfinish & 2);
699 ZCFINISH.load(Ordering::Relaxed) & 2
700}
701
702/// Port of `zfgetmsg()` from `Src/Modules/zftp.c:702`.
703/// C: `static int zfgetmsg(void)` — read a complete FTP server reply
704/// (possibly multi-line), parse the 3-digit code, update lastcode +
705/// lastcodestr + lastmsg + ZFTP_REPLY, return the first-digit status
706/// (1/2/3/4/5) or 6 on error/disconnect.
707#[allow(non_snake_case)]
708pub fn zfgetmsg() -> i32 {
709 // c:702
710 // c:702-705 — char line[256], *ptr, *verbose;
711 // int stopit, printing = 0, tmout;
712 let mut line = [0u8; 256];
713 let mut printing: i32 = 0;
714 let stopit_initial: bool;
715 let tmout: i32;
716
717 // c:707-708 — if (!zfsess->control) return 6;
718 {
719 let state = match zftp_state().lock() {
720 Ok(s) => s,
721 Err(_) => return 6,
722 };
723 let sess = match state.current.as_deref().and_then(|k| state.sessions.get(k)) {
724 Some(s) => s,
725 None => return 6,
726 };
727 if sess.control.is_none() {
728 return 6; // c:708
729 }
730 }
731
732 // c:709-710 — zsfree(lastmsg); lastmsg = NULL;
733 if let Ok(mut m) = lastmsg.lock() {
734 m.clear();
735 }
736
737 // c:712 — tmout = getiparam("ZFTP_TMOUT");
738 // c:712 — `tmout = getiparam("ZFTP_TMOUT");`. Read paramtab, not OS env.
739 tmout = getiparam("ZFTP_TMOUT") as i32;
740
741 // c:714 — zfgetline(line, 256, tmout);
742 zfgetline(&mut line, 256, tmout);
743 // c:715 — ptr = line; (use string slice + offset index instead)
744 let mut ptr_off: usize = 0;
745 let line_str = std::str::from_utf8(&line)
746 .unwrap_or("")
747 .trim_end_matches('\0')
748 .to_string();
749
750 // c:716 — if (zfdrrrring || !idigit(ptr[0..3])) — timeout or not FTP.
751 let is_digit = |b: u8| b.is_ascii_digit();
752 let timeout_or_bad = ZFDRRRRING.load(Ordering::Relaxed) != 0
753 || line.len() < 3
754 || !is_digit(line[0])
755 || !is_digit(line[1])
756 || !is_digit(line[2]);
757 if timeout_or_bad {
758 // c:716
759 ZCFINISH.store(2, Ordering::Relaxed); // c:718
760 if ZFCLOSING.load(Ordering::Relaxed) == 0 {
761 // c:719
762 zfclose(0); // c:720
763 }
764 if let Ok(mut m) = lastmsg.lock() {
765 m.clear();
766 } // c:721
767 if let Ok(mut cs) = lastcodestr.lock() {
768 // c:722
769 cs.copy_from_slice(b"000\0");
770 }
771 zfsetparam("ZFTP_REPLY", "", ZFPM_READONLY); // c:723
772 return 6; // c:724
773 }
774
775 // c:726-729 — extract first 3 bytes into lastcodestr, parse to int.
776 let code_str: String = std::str::from_utf8(&line[..3]).unwrap_or("0").to_string();
777 if let Ok(mut cs) = lastcodestr.lock() {
778 cs[0] = line[0];
779 cs[1] = line[1];
780 cs[2] = line[2];
781 cs[3] = 0;
782 }
783 let code: i32 = code_str.parse().unwrap_or(0);
784 lastcode.store(code, Ordering::Relaxed);
785 ptr_off += 3;
786 // c:730 — zfsetparam("ZFTP_CODE", lastcodestr, ZFPM_READONLY);
787 zfsetparam("ZFTP_CODE", &code_str, ZFPM_READONLY);
788 // c:731 — stopit = (*ptr++ != '-');
789 stopit_initial = line.get(ptr_off).copied() != Some(b'-');
790 ptr_off += 1;
791 let mut stopit = stopit_initial;
792
793 // c:733-744 — verbose check + initial-line printing.
794 // c:734 — `if (!(verbose = getsparam_u("ZFTP_VERBOSE"))) verbose = "";`
795 // — the `_u` variant strips Meta-escape encoding. ZFTP_VERBOSE
796 // is a string of digit characters (e.g. "045") that gets fed
797 // to strchr against `lastcodestr[0]` at c:736 and against '0'
798 // at c:740 — strchr operates on raw bytes, not metafied form.
799 // Prior port used non-_u getsparam — for any ZFTP_VERBOSE
800 // value containing a Meta-escaped byte (rare but possible
801 // via `$'\xfd'` etc.), the metafied byte pairs would survive
802 // into the strchr-equivalent `.contains()` calls below and
803 // miss matches.
804 let verbose = crate::ported::params::getsparam_u("ZFTP_VERBOSE").unwrap_or_default(); // c:734
805 if verbose.contains(line[0] as char) {
806 // c:736
807 printing = 1; // c:738
808 eprint!("{}", line_str); // c:739
809 } else if verbose.contains('0') && !stopit {
810 // c:740
811 printing = 2; // c:742
812 eprint!("{}", &line_str[ptr_off..]); // c:743
813 }
814 if printing != 0 {
815 // c:746
816 eprintln!(); // c:747
817 }
818
819 // c:749-775 — multi-line continuation loop.
820 while ZCFINISH.load(Ordering::Relaxed) != 2 && !stopit {
821 line.fill(0); // reset
822 ptr_off = 0;
823 zfgetline(&mut line, 256, tmout); // c:750
824 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
825 // c:752
826 line[0] = 0; // c:753
827 break; // c:754
828 }
829 // c:757-764 — code-prefix check.
830 if &line[..3] == &code_str.as_bytes()[..3] {
831 // c:757
832 if line[3] == b' ' {
833 // c:758
834 stopit = true; // c:759
835 ptr_off = 4; // c:760
836 } else if line[3] == b'-' {
837 // c:761
838 ptr_off = 4; // c:762
839 }
840 } else if &line[..4] == b" " {
841 // c:763
842 ptr_off = 4; // c:764
843 }
844
845 // c:766-774 — print intermediate line per `printing` mode.
846 let cont_line = std::str::from_utf8(&line)
847 .unwrap_or("")
848 .trim_end_matches('\0');
849 if printing == 2 {
850 // c:766
851 if !stopit {
852 // c:767
853 eprintln!("{}", &cont_line[ptr_off..]); // c:768-769
854 }
855 } else if printing != 0 {
856 // c:771
857 eprintln!("{}", cont_line); // c:772-773
858 }
859 }
860
861 // c:777-778 — fflush(stderr);
862 if printing != 0 {
863 let _ = io::stderr().flush();
864 }
865
866 // c:781 — lastmsg = ztrdup(ptr); (the trailing portion of last line)
867 let last_msg_str: String = std::str::from_utf8(&line)
868 .unwrap_or("")
869 .trim_end_matches('\0')
870 .chars()
871 .skip(ptr_off)
872 .collect();
873 if let Ok(mut m) = lastmsg.lock() {
874 *m = last_msg_str.clone();
875 }
876 // c:785 — zfsetparam("ZFTP_REPLY", ztrdup(line), ZFPM_READONLY);
877 let whole_line = std::str::from_utf8(&line)
878 .unwrap_or("")
879 .trim_end_matches('\0');
880 zfsetparam("ZFTP_REPLY", whole_line, ZFPM_READONLY);
881
882 // c:791-797 — EOF or 421: close + warn.
883 let zcfin = ZCFINISH.load(Ordering::Relaxed);
884 let cur_code = lastcode.load(Ordering::Relaxed);
885 if (zcfin == 2 || cur_code == 421) && ZFCLOSING.load(Ordering::Relaxed) == 0 {
886 ZCFINISH.store(2, Ordering::Relaxed); // c:792
887 zfclose(0); // c:793
888 zwarnnam(
889 "zftp", // c:795
890 "remote server has closed connection",
891 );
892 return 6; // c:796
893 }
894 // c:798-801 — 530 not-logged-in.
895 if cur_code == 530 {
896 // c:798
897 return 6; // c:800
898 }
899 // c:807-810 — 120 wait-and-retry.
900 if cur_code == 120 {
901 // c:807
902 zwarnnam(
903 "zftp", // c:808
904 &format!("delay expected, waiting: {}", last_msg_str),
905 );
906 return zfgetmsg(); // c:809
907 }
908 // c:813 — return lastcodestr[0] - '0';
909 (code_str.as_bytes()[0] - b'0') as i32
910}
911
912/// Port of `zfsendcmd(char *cmd)` from `Src/Modules/zftp.c:825`.
913/// C: `static int zfsendcmd(char *cmd)` — write the command to the
914/// control fd with an alarm-guarded timeout, then read the server
915/// reply via zfgetmsg.
916#[allow(non_snake_case)]
917pub fn zfsendcmd(cmd: &str) -> i32 {
918 // c:825
919 // c:832 — int ret, tmout;
920 let ret: isize;
921 let tmout: i32;
922
923 // c:834-835 — if (!zfsess->control) return 6;
924 let mut state = match zftp_state().lock() {
925 Ok(s) => s,
926 Err(_) => return 6,
927 };
928 let sess = match state
929 .current
930 .clone()
931 .and_then(|k| state.sessions.get_mut(&k))
932 {
933 // c:834
934 Some(s) => s,
935 None => return 6,
936 };
937 if sess.control.is_none() {
938 // c:834
939 return 6; // c:835
940 }
941
942 // c:836 — tmout = getiparam("ZFTP_TMOUT");
943 // c:712 — `tmout = getiparam("ZFTP_TMOUT");`. Read paramtab, not OS env.
944 tmout = getiparam("ZFTP_TMOUT") as i32;
945
946 // c:837-841 — `if (setjmp(zfalrmbuf)) { alarm(0);
947 // zwarnnam("zftp", "timeout sending message");
948 // return 6; }`. ZFDRRRRING adapter same as
949 // zfread (cfe7560f58), zfwrite (00c1c36dc9), zfgetline (bef84af815).
950 // Two check sites match C's setjmp semantics: before alarm
951 // install + after write returns.
952 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
953 // Stale alarm from prior call.
954 unsafe {
955 libc::alarm(0);
956 }
957 zwarnnam("zftp", "timeout sending message");
958 return 6;
959 }
960 zfalarm(tmout); // c:842
961
962 // c:843 — `ret = write(zfsess->control->fd, cmd, strlen(cmd));`
963 let bytes = cmd.as_bytes();
964 ret = match sess.control.as_mut() {
965 Some(stream) => match stream.write(bytes) {
966 Ok(n) => {
967 let _ = stream.flush();
968 n as isize
969 }
970 Err(_) => -1,
971 },
972 None => -1,
973 };
974 // c:844 — `alarm(0);`
975 unsafe {
976 libc::alarm(0);
977 }
978
979 // c:837 setjmp counterpart for THIS call — alarm fired during write.
980 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
981 zwarnnam("zftp", "timeout sending message"); // c:839
982 return 6; // c:840
983 }
984
985 // c:846-849 — write failure.
986 if ret <= 0 {
987 zwarnnam(
988 // c:847
989 "zftp send",
990 &format!(
991 "failure sending control message: {}",
992 io::Error::last_os_error()
993 ),
994 );
995 return 6; // c:848
996 }
997
998 // c:851 — return zfgetmsg();
999 drop(state);
1000 zfgetmsg()
1001}
1002
1003/// Port of `zfopendata(char *name, union tcp_sockaddr *zdsockp, int *is_passivep)` from `Src/Modules/zftp.c:859`.
1004/// C: `static int zfopendata(char *name, union tcp_sockaddr *zdsockp,
1005/// int *is_passivep)` — set up a data connection (PASV-preferred,
1006/// PORT-mode fallback). Returns 0 success, 1 failure. Stores the
1007/// resulting fd in `zfsess->dfd` and sets `*is_passivep`.
1008///
1009/// Rust port: `is_passive` is returned via the tuple instead of a
1010/// `*int` out-param (Rust doesn't expose `union tcp_sockaddr` so the
1011/// `zdsockp` slot is gone). PORT-mode falls back to `TcpListener` for
1012/// the local bind+listen+accept; PASV path uses `TcpStream::connect`
1013/// and stores the connected stream's fd as `sess.dfd`. The non-PASV
1014/// branch requires the caller to accept(2) before reading.
1015#[allow(non_snake_case)]
1016/// Port of `zfopendata(char *name, union tcp_sockaddr *zdsockp, int *is_passivep)` from `Src/Modules/zftp.c:859`.
1017/// WARNING: param names don't match C — Rust=(name) vs C=(name, zdsockp, is_passivep)
1018pub fn zfopendata(name: &str) -> (i32, bool) {
1019 // c:859
1020 // c:862-865 — error if neither SNDP nor PASV preference is set.
1021 let prefs = zfprefs.load(Ordering::Relaxed); // c:862
1022 if (prefs & (ZFPF_SNDP | ZFPF_PASV)) == 0 {
1023 // c:863
1024 zwarnnam(name, "Must set preference S or P to transfer data"); // c:864
1025 return (1, false); // c:865
1026 }
1027 // c:871 — `if (!(zfstatusp[zfsessno] & ZFST_NOPS) && (zfprefs & ZFPF_PASV))`
1028 // — try PASV only when the bit is set AND the server hasn't
1029 // already 5xx'd PASV in this session.
1030 let nops_known = zftp_state()
1031 .lock()
1032 .ok()
1033 .and_then(|s| {
1034 s.current
1035 .as_deref()
1036 .and_then(|k| s.sessions.get(k))
1037 .map(|sess| sess.nops_probed)
1038 })
1039 .unwrap_or(false);
1040 let try_pasv: bool = !nops_known && (prefs & ZFPF_PASV) != 0; // c:871
1041
1042 if try_pasv {
1043 // c:879 — psv_cmd = "PASV\r\n"; (EPSV unsupported in Rust port).
1044 if zfsendcmd("PASV\r\n") == 6 {
1045 // c:881
1046 return (1, false); // c:882
1047 }
1048 let code = lastcode.load(Ordering::Relaxed);
1049 if (500..=504).contains(&code) {
1050 // c:884 — PASV unsupported by server.
1051 // c:888 — `zfstatusp[zfsessno] |= ZFST_NOPS;` — record so
1052 // subsequent zfopendata calls skip PASV entirely.
1053 // Prior port skipped this — every subsequent data
1054 // transfer in the same session re-sent PASV to a
1055 // server that had already 502'd it, wasting one
1056 // round-trip per transfer.
1057 if let Ok(mut st) = zftp_state().lock() {
1058 if let Some(sess) = st.current.clone().and_then(|k| st.sessions.get_mut(&k)) {
1059 sess.nops_probed = true; // c:888
1060 }
1061 }
1062 zfclosedata(); // c:889
1063 // c:890 — `return zfopendata(...);` — C recurses.
1064 // Rust falls through to the PORT-mode code below;
1065 // same end-state since try_pasv was the only
1066 // PASV-specific gate.
1067 } else {
1068 // c:899 — parse the PASV reply.
1069 let last = lastmsg.lock().ok().map(|m| m.clone()).unwrap_or_default();
1070 let (ip, port) = match parse_pasv_response(&last) {
1071 // c:925
1072 Ok(t) => t,
1073 Err(_) => {
1074 zwarnnam(name, &format!("bad response to PASV: {}", last));
1075 zfclosedata();
1076 return (1, false); // c:946
1077 }
1078 };
1079 // c:954 — connect from data socket to remote (ip:port).
1080 let addr = format!("{}:{}", ip, port);
1081 let stream = match TcpStream::connect(&addr) {
1082 // c:958
1083 Ok(s) => s,
1084 Err(_) => {
1085 zwarnnam(name, "can't open data socket");
1086 zfclosedata();
1087 return (1, false);
1088 }
1089 };
1090 let dfd_raw = stream.as_raw_fd();
1091 // Keep the stream alive past this fn so the fd stays open;
1092 // the session owns the fd via `dfd`.
1093 std::mem::forget(stream);
1094 if let Ok(mut state) = zftp_state().lock() {
1095 if let Some(sess) = state
1096 .current
1097 .clone()
1098 .and_then(|k| state.sessions.get_mut(&k))
1099 {
1100 sess.dfd = dfd_raw; // c:961 dfd stored
1101 }
1102 }
1103 return (0, true); // c:1041 SUCCESS PASV
1104 }
1105 }
1106
1107 // c:967-1037 — PORT-mode (active FTP): bind+listen locally, send
1108 // PORT a,b,c,d,p1,p2, return so caller can accept().
1109 // c:977 — refuse PORT mode if SNDP preference isn't set (e.g. when
1110 // PASV-only and the server rejected PASV).
1111 if (zfprefs.load(Ordering::Relaxed) & ZFPF_SNDP) == 0 {
1112 // c:977
1113 zwarnnam(name, "only sendport mode available for data"); // c:978
1114 return (1, false); // c:979
1115 }
1116 // c:982-992 — `*zdsockp = zfsess->control->sock;` then zero the
1117 // port and bind:
1118 //
1119 // *zdsockp = zfsess->control->sock;
1120 // ...
1121 // zdsockp->in.sin_port = 0; /* to be set by bind() */
1122 // len = sizeof(struct sockaddr_in);
1123 // ...
1124 // if (bind(zfsess->dfd, (struct sockaddr *)zdsockp, len) < 0)
1125 //
1126 // The data socket binds to the CONTROL connection's LOCAL address
1127 // — the interface the server can actually reach back on — and
1128 // that address is what the c:1003+ PORT command advertises. A
1129 // prior bind to "0.0.0.0:0" made local_addr() report 0.0.0.0, so
1130 // active mode sent `PORT 0,0,0,0,p1,p2` — every server either
1131 // rejects it or dials a black hole; PORT-mode transfers could
1132 // never work, on any host.
1133 let ctrl_local_ip: std::net::IpAddr = zftp_state()
1134 .lock()
1135 .ok()
1136 .and_then(|s| {
1137 s.current
1138 .as_deref()
1139 .and_then(|k| s.sessions.get(k))
1140 .and_then(|sess| {
1141 sess.control
1142 .as_ref()
1143 .and_then(|c| c.local_addr().ok())
1144 .map(|a| a.ip())
1145 })
1146 })
1147 .unwrap_or(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED));
1148 let listener = match std::net::TcpListener::bind((ctrl_local_ip, 0)) {
1149 // c:994 bind(zfsess->dfd, zdsockp, len)
1150 Ok(l) => l,
1151 Err(_) => {
1152 zwarnnam(name, "can't bind data socket");
1153 return (1, false);
1154 }
1155 };
1156 let local = match listener.local_addr() {
1157 Ok(a) => a,
1158 Err(_) => {
1159 zwarnnam(name, "getsockname failed");
1160 return (1, false);
1161 }
1162 };
1163 // c:986 — listen with backlog 1; std::net::TcpListener::bind already listens.
1164 let ipv4 = match local.ip() {
1165 std::net::IpAddr::V4(v) => v.octets(),
1166 std::net::IpAddr::V6(_) => {
1167 zwarnnam(name, "PORT mode requires IPv4");
1168 return (1, false);
1169 }
1170 };
1171 let port = local.port();
1172 // c:1003-1017 — PORT cmd format: "PORT h1,h2,h3,h4,p1,p2\r\n".
1173 let port_cmd = format!(
1174 "PORT {},{},{},{},{},{}\r\n",
1175 ipv4[0],
1176 ipv4[1],
1177 ipv4[2],
1178 ipv4[3],
1179 port >> 8,
1180 port & 0xff,
1181 );
1182 if zfsendcmd(&port_cmd) > 2 {
1183 // c:1018
1184 zwarnnam(name, "PORT command failed");
1185 return (1, false); // c:1019
1186 }
1187 // c:1029 — store listening fd as dfd; caller does accept().
1188 let lfd = listener.as_raw_fd();
1189 std::mem::forget(listener);
1190 if let Ok(mut state) = zftp_state().lock() {
1191 if let Some(sess) = state
1192 .current
1193 .clone()
1194 .and_then(|k| state.sessions.get_mut(&k))
1195 {
1196 sess.dfd = lfd; // c:1029
1197 }
1198 }
1199 (0, false) // c:1037 SUCCESS PORT
1200}
1201
1202/// Port of `zfclosedata()` from `Src/Modules/zftp.c:1043`.
1203/// C: `static void zfclosedata(void)` — early-return when no dfd is
1204/// live, otherwise close(dfd) + dfd = -1.
1205#[allow(non_snake_case)]
1206pub fn zfclosedata() {
1207 // c:1043
1208 if let Ok(mut state) = zftp_state().lock() {
1209 if let Some(sess) = state
1210 .current
1211 .clone()
1212 .and_then(|k| state.sessions.get_mut(&k))
1213 {
1214 if sess.dfd == -1 {
1215 // c:1045
1216 return; // c:1046
1217 }
1218 unsafe {
1219 libc::close(sess.dfd);
1220 } // c:1047 close(dfd)
1221 sess.dfd = -1; // c:1048
1222 }
1223 }
1224}
1225
1226/// Port of `zfgetdata(char *name, char *rest, char *cmd, int getsize)` from `Src/Modules/zftp.c:1065`.
1227/// C: `static int zfgetdata(char *name, char *rest, char *cmd, int getsize)` —
1228/// open the data connection (PASV or PORT mode via zfopendata),
1229/// optionally send REST, send the transfer command, and (PORT mode)
1230/// accept(2) on the listening socket. Returns 0 success, 1 failure.
1231#[allow(non_snake_case)]
1232pub fn zfgetdata(name: &str, rest: &str, cmd: &str, getsize: i32) -> i32 {
1233 // c:1065
1234 // c:1065-1069 — locals at fn top.
1235 let is_passive: bool; // c:1068
1236
1237 // c:1071-1072 — zfopendata: full PASV/PORT setup; sets sess.dfd.
1238 let (rc, ip) = zfopendata(name); // c:1071
1239 if rc != 0 {
1240 // c:1071
1241 return 1; // c:1072
1242 }
1243 is_passive = ip;
1244
1245 // c:1084-1087 — REST command for resume.
1246 if !rest.is_empty() && zfsendcmd(rest) > 3 {
1247 zfclosedata();
1248 return 1;
1249 }
1250
1251 // c:1089-1092 — send the transfer command (RETR / STOR / etc.).
1252 if zfsendcmd(cmd) > 2 {
1253 // c:1089
1254 zfclosedata(); // c:1090
1255 return 1; // c:1091
1256 }
1257
1258 // c:1093-1116 — parse "Opening data connection for file (N bytes)"
1259 // hint to populate ZFTP_SIZE without a separate SIZE request:
1260 //
1261 // if (getsize || (!(zfstatusp[zfsessno] & ZFST_TRSZ) &&
1262 // !strncmp(cmd, "RETR", 4))) {
1263 // char *ptr = strstr(lastmsg, "bytes");
1264 // zfstatusp[zfsessno] |= ZFST_NOSZ|ZFST_TRSZ;
1265 // if (ptr) {
1266 // ...walk back to digit run...
1267 // if (idigit(*ptr)) {
1268 // zfstatusp[zfsessno] &= ~ZFST_NOSZ;
1269 // if (getsize) { ...zfsetparam("ZFTP_SIZE", ...)... }
1270 // }
1271 // }
1272 // }
1273 //
1274 // The ZFST_TRSZ gate limits the free-parse attempt to the FIRST
1275 // RETR; NOSZ|TRSZ pessimistically assume no embedded size, then
1276 // NOSZ clears when the hint parses. zfgetput's c:2577 cache test
1277 // reads these to skip the SIZE round-trip on servers that embed
1278 // sizes. Prior port re-parsed every RETR and never recorded the
1279 // bits, so the SIZE-avoidance optimization the c:1098 comment
1280 // describes never engaged.
1281 let trsz_known = zftp_state()
1282 .lock()
1283 .ok()
1284 .and_then(|s| {
1285 s.current
1286 .as_deref()
1287 .and_then(|k| s.sessions.get(k))
1288 .map(|sess| sess.trsz)
1289 })
1290 .unwrap_or(false);
1291 if getsize != 0 || (!trsz_known && cmd.starts_with("RETR")) {
1292 // c:1093-1094
1293 let cur_last = lastmsg.lock().ok().map(|m| m.clone()).unwrap_or_default();
1294 // c:1102 — pessimistic default: tried, assume no size.
1295 if let Ok(mut st) = zftp_state().lock() {
1296 if let Some(sess) = st.current.clone().and_then(|k| st.sessions.get_mut(&k)) {
1297 sess.trsz = true; // c:1102 ZFST_TRSZ
1298 sess.nosz = true; // c:1102 ZFST_NOSZ
1299 }
1300 }
1301 if let Some(byte_idx) = cur_last.find("bytes") {
1302 // c:1101 strstr
1303 // c:1104-1107 — walk backward to the start of the digit run.
1304 let prefix = &cur_last[..byte_idx];
1305 let trimmed: String = prefix
1306 .chars()
1307 .rev()
1308 .skip_while(|c| !c.is_ascii_digit())
1309 .take_while(|c| c.is_ascii_digit())
1310 .collect::<String>()
1311 .chars()
1312 .rev()
1313 .collect();
1314 if !trimmed.is_empty() {
1315 // c:1108 idigit(*ptr)
1316 // c:1109 — server DOES embed sizes; clear NOSZ.
1317 if let Ok(mut st) = zftp_state().lock() {
1318 if let Some(sess) = st.current.clone().and_then(|k| st.sessions.get_mut(&k)) {
1319 sess.nosz = false; // c:1109
1320 }
1321 }
1322 if getsize != 0 {
1323 // c:1110
1324 zfsetparam("ZFTP_SIZE", &trimmed, ZFPM_READONLY | ZFPM_INTEGER);
1325 // c:1112
1326 }
1327 }
1328 }
1329 }
1330
1331 // c:1118-1143 — PORT-mode accept handling. PASV: dfd is already
1332 // the data fd, just zfmovefd it. PORT: accept() on the listening
1333 // fd to obtain the real data fd, then close the listener.
1334 let dfd_raw: i32;
1335 if !is_passive {
1336 // c:1118
1337 // c:1124-1128 — accept the connection from the server.
1338 let mut sa: libc::sockaddr_storage = unsafe { std::mem::zeroed() };
1339 let mut sl: libc::socklen_t = size_of::<libc::sockaddr_storage>() as _;
1340 let listen_fd = zftp_state()
1341 .lock()
1342 .ok()
1343 .and_then(|s| {
1344 s.current
1345 .as_deref()
1346 .and_then(|k| s.sessions.get(k))
1347 .map(|x| x.dfd)
1348 })
1349 .unwrap_or(-1);
1350 let newfd = unsafe { libc::accept(listen_fd, &mut sa as *mut _ as *mut _, &mut sl) };
1351 if newfd < 0 {
1352 // c:1129
1353 zwarnnam(
1354 name,
1355 &format!("unable to accept data: {}", io::Error::last_os_error()),
1356 );
1357 zfclosedata(); // c:1131
1358 return 1;
1359 }
1360 // c:1133 — close the original listening fd, install the accepted one.
1361 unsafe {
1362 libc::close(listen_fd);
1363 }
1364 dfd_raw = zfmovefd(newfd);
1365 } else {
1366 // c:1136
1367 // c:1139-1142 — zfmovefd(zfsess->dfd) for PASV.
1368 let cur_dfd = zftp_state()
1369 .lock()
1370 .ok()
1371 .and_then(|s| {
1372 s.current
1373 .as_deref()
1374 .and_then(|k| s.sessions.get(k))
1375 .map(|x| x.dfd)
1376 })
1377 .unwrap_or(-1);
1378 dfd_raw = zfmovefd(cur_dfd);
1379 }
1380 if let Ok(mut state) = zftp_state().lock() {
1381 if let Some(sess) = state
1382 .current
1383 .clone()
1384 .and_then(|k| state.sessions.get_mut(&k))
1385 {
1386 sess.dfd = dfd_raw; // c:1142
1387 }
1388 }
1389
1390 // c:1156-1163 — SO_LINGER 120s.
1391 let li = libc::linger {
1392 l_onoff: 1,
1393 l_linger: 120,
1394 };
1395 unsafe {
1396 libc::setsockopt(
1397 dfd_raw,
1398 libc::SOL_SOCKET,
1399 libc::SO_LINGER,
1400 &li as *const _ as *const libc::c_void,
1401 size_of::<libc::linger>() as libc::socklen_t,
1402 );
1403 }
1404 // c:1167-1170 — IP_TOS = IPTOS_THROUGHPUT.
1405 let tos: libc::c_int = 0x08; // IPTOS_THROUGHPUT
1406 unsafe {
1407 libc::setsockopt(
1408 dfd_raw,
1409 libc::IPPROTO_IP,
1410 libc::IP_TOS,
1411 &tos as *const _ as *const libc::c_void,
1412 size_of::<libc::c_int>() as libc::socklen_t,
1413 );
1414 }
1415 // c:1174 — fcntl(dfd, F_SETFD, FD_CLOEXEC).
1416 unsafe {
1417 libc::fcntl(dfd_raw, libc::F_SETFD, libc::FD_CLOEXEC);
1418 }
1419
1420 0 // c:1177
1421}
1422
1423/// Port of `zfstats(char *fnam, int remote, off_t *retsize, char **retmdtm, int fd)` from `Src/Modules/zftp.c:1193`.
1424/// C: `static int zfstats(char *fnam, int remote, off_t *retsize, char **retmdtm, int fd)` —
1425/// query file size + mtime, remote via SIZE/MDTM commands or local
1426/// via stat(2)/fstat(2).
1427#[allow(non_snake_case)]
1428/// WARNING: param names don't match C — Rust=(fnam, remote, retmdtm, fd) vs C=(fnam, remote, retsize, retmdtm, fd)
1429pub fn zfstats(
1430 fnam: &str,
1431 remote: i32, // c:1193
1432 mut retsize: Option<&mut libc::off_t>,
1433 mut retmdtm: Option<&mut Option<String>>,
1434 fd: i32,
1435) -> i32 {
1436 // C's retsize/retmdtm are NULLABLE pointers — callers request only
1437 // what they want: zfgetput passes `zfstats(*args, recv, &sz, NULL,
1438 // 0)` (c:2580). A prior non-optional signature forced every caller
1439 // through BOTH probes: zfgetput's per-transfer stats fired an MDTM
1440 // round-trip the C never sends, and on an MDTM-less server the
1441 // c:1241 NOPE path returned 2 — failing the whole size lookup the
1442 // caller actually wanted.
1443 // c:1195-1197 — locals at fn top.
1444 let mut sz: libc::off_t = -1; // c:1195
1445 let mut mt: Option<String> = None; // c:1196 char *mt
1446 let ret: i32; // c:1197
1447
1448 if let Some(rs) = retsize.as_mut() {
1449 **rs = -1; // c:1199-1200
1450 }
1451 if let Some(rm) = retmdtm.as_mut() {
1452 **rm = None; // c:1201-1202
1453 }
1454
1455 if remote != 0 {
1456 // c:1203
1457 // c:1205-1207 — `if ((zfsess->has_size == ZFCP_NOPE && retsize) ||
1458 // (zfsess->has_mdtm == ZFCP_NOPE && retmdtm))
1459 // return 2;`
1460 // Early-out when the server's known to not support the
1461 // SPECIFIC probes this caller wants.
1462 let (had_size_nope, had_mdtm_nope) = {
1463 let st = match zftp_state().lock() {
1464 Ok(s) => s,
1465 Err(_) => return 1,
1466 };
1467 match st.current.as_deref().and_then(|k| st.sessions.get(k)) {
1468 Some(sess) => (sess.has_size == ZFCP_NOPE, sess.has_mdtm == ZFCP_NOPE),
1469 None => (false, false),
1470 }
1471 };
1472 if (had_size_nope && retsize.is_some()) || (had_mdtm_nope && retmdtm.is_some()) {
1473 return 2; // c:1207
1474 }
1475
1476 // c:1213 — `zfsettype(ZFST_TYPE(zfstatusp[zfsessno]));` — the
1477 // session's REQUESTED type slice, not a hardcoded IMAGE: SIZE
1478 // results are type-dependent (RFC 3659 — ASCII SIZE counts
1479 // post-translation bytes), so the probe must run under the
1480 // type the upcoming transfer will use.
1481 let req_type = zftp_state()
1482 .lock()
1483 .ok()
1484 .and_then(|s| {
1485 s.current
1486 .as_deref()
1487 .and_then(|k| s.sessions.get(k))
1488 .map(|sess| ZFST_TYPE(sess.transfer_type))
1489 })
1490 .unwrap_or(ZFST_IMAG);
1491 zfsettype(req_type); // c:1213
1492
1493 if retsize.is_some() {
1494 // c:1214-1228 — SIZE command path.
1495 let cmd = format!("SIZE {}\r\n", fnam); // c:1215
1496 ret = zfsendcmd(&cmd); // c:1216
1497 if ret == 6 {
1498 // c:1218
1499 return 1; // c:1219
1500 }
1501 let code = lastcode.load(Ordering::Relaxed);
1502 if code < 300 {
1503 // c:1220
1504 // c:1221 — `sz = zstrtol(lastmsg, 0, 10);`
1505 sz = lastmsg
1506 .lock()
1507 .ok()
1508 .map(|m| m.trim().parse::<libc::off_t>().unwrap_or(-1))
1509 .unwrap_or(-1);
1510 // c:1222 — `zfsess->has_size = ZFCP_YUPP;`
1511 if let Ok(mut st) = zftp_state().lock() {
1512 if let Some(sess) = st.current.clone().and_then(|k| st.sessions.get_mut(&k)) {
1513 sess.has_size = ZFCP_YUPP;
1514 }
1515 }
1516 } else if (500..=504).contains(&code) {
1517 // c:1223 — server doesn't speak SIZE.
1518 if let Ok(mut st) = zftp_state().lock() {
1519 if let Some(sess) = st.current.clone().and_then(|k| st.sessions.get_mut(&k)) {
1520 sess.has_size = ZFCP_NOPE; // c:1224
1521 }
1522 }
1523 return 2; // c:1225
1524 } else if code == 550 {
1525 // c:1226 — file doesn't exist.
1526 return 1; // c:1227
1527 }
1528 }
1529
1530 if retmdtm.is_some() {
1531 // c:1231-1245 — MDTM command path.
1532 let cmd = format!("MDTM {}\r\n", fnam); // c:1232
1533 let ret2 = zfsendcmd(&cmd); // c:1233
1534 if ret2 == 6 {
1535 // c:1235
1536 return 1; // c:1236
1537 }
1538 let code = lastcode.load(Ordering::Relaxed);
1539 if code < 300 {
1540 // c:1237
1541 // c:1238 — `mt = ztrdup(lastmsg);`
1542 mt = lastmsg.lock().ok().map(|m| m.clone());
1543 // c:1239 — `zfsess->has_mdtm = ZFCP_YUPP;`
1544 if let Ok(mut st) = zftp_state().lock() {
1545 if let Some(sess) = st.current.clone().and_then(|k| st.sessions.get_mut(&k)) {
1546 sess.has_mdtm = ZFCP_YUPP;
1547 }
1548 }
1549 } else if (500..=504).contains(&code) {
1550 // c:1240 — server doesn't speak MDTM.
1551 if let Ok(mut st) = zftp_state().lock() {
1552 if let Some(sess) = st.current.clone().and_then(|k| st.sessions.get_mut(&k)) {
1553 sess.has_mdtm = ZFCP_NOPE; // c:1241
1554 }
1555 }
1556 return 2; // c:1242
1557 } else if code == 550 {
1558 // c:1243
1559 return 1; // c:1244
1560 }
1561 }
1562 } else {
1563 // c:1246
1564 // c:1248-1263 — local file: stat or fstat.
1565 let mut statbuf: libc::stat = unsafe { std::mem::zeroed() }; // c:1248
1566 let cn = std::ffi::CString::new(fnam).unwrap_or_default();
1567 let rc = if fd == -1 {
1568 // c:1252
1569 unsafe { libc::stat(cn.as_ptr(), &mut statbuf) }
1570 } else {
1571 unsafe { libc::fstat(fd, &mut statbuf) }
1572 };
1573 if rc < 0 {
1574 // c:1252
1575 return 1; // c:1253
1576 }
1577 sz = statbuf.st_size as libc::off_t; // c:1255
1578
1579 // c:1257-1263 — format mtime as YYYYMMDDHHMMSS via gmtime.
1580 let mtime = statbuf.st_mtime;
1581 let mut tmbuf = [0u8; 20];
1582 let tmbuf_len = unsafe {
1583 let mut tm: libc::tm = std::mem::zeroed();
1584 libc::gmtime_r(&mtime, &mut tm); // c:1259
1585 // c:1261 — ztrftime(tmbuf, 20, "%Y%m%d%H%M%S", tm, 0);
1586 let fmt = std::ffi::CString::new("%Y%m%d%H%M%S").unwrap();
1587 libc::strftime(
1588 tmbuf.as_mut_ptr() as *mut libc::c_char,
1589 20,
1590 fmt.as_ptr(),
1591 &tm,
1592 )
1593 };
1594 mt = std::str::from_utf8(&tmbuf[..tmbuf_len])
1595 .ok()
1596 .map(|s| s.to_string());
1597 }
1598
1599 if let Some(rs) = retsize.as_mut() {
1600 **rs = sz; // c:1265-1266
1601 }
1602 if let Some(rm) = retmdtm.as_mut() {
1603 **rm = mt; // c:1267-1268
1604 }
1605 0 // c:1269
1606}
1607
1608/// Port of `zfstarttrans(char *nam, int recv, off_t sz)` from `Src/Modules/zftp.c:1276`.
1609/// C: `static void zfstarttrans(char *nam, int recv, off_t sz)` — sets
1610/// the ZFTP_SIZE/ZFTP_FILE/ZFTP_TRANSFER/ZFTP_COUNT params.
1611#[allow(non_snake_case)]
1612pub fn zfstarttrans(nam: &str, recv: i32, sz: libc::off_t) {
1613 // c:1276
1614 let cnt: libc::off_t = 0; // c:1276
1615 // c:1284-1285 — only set ZFTP_SIZE when sz > 0 (avoid lying about
1616 // pipe-sourced unknown size).
1617 if sz > 0 {
1618 // c:1284
1619 zfsetparam("ZFTP_SIZE", &sz.to_string(), ZFPM_READONLY | ZFPM_INTEGER); // c:1285
1620 }
1621 zfsetparam("ZFTP_FILE", nam, ZFPM_READONLY); // c:1286
1622 zfsetparam(
1623 "ZFTP_TRANSFER", // c:1287
1624 if recv != 0 { "G" } else { "P" },
1625 ZFPM_READONLY,
1626 );
1627 zfsetparam("ZFTP_COUNT", &cnt.to_string(), ZFPM_READONLY | ZFPM_INTEGER); // c:1288
1628}
1629
1630/// Port of `zfendtrans()` from `Src/Modules/zftp.c:1295`.
1631/// C: `static void zfendtrans(void)` — unsets the ZFTP_* transfer params.
1632#[allow(non_snake_case)]
1633pub fn zfendtrans() {
1634 // c:1295
1635 zfunsetparam("ZFTP_SIZE"); // c:1295
1636 zfunsetparam("ZFTP_FILE"); // c:1298
1637 zfunsetparam("ZFTP_TRANSFER"); // c:1299
1638 zfunsetparam("ZFTP_COUNT"); // c:1300
1639}
1640
1641/// Port of `zfread(int fd, char *bf, off_t sz, int tmout)` from `Src/Modules/zftp.c:1307`.
1642/// C: `static int zfread(int fd, char *bf, off_t sz, int tmout)` — read
1643/// up to `sz` bytes from fd; with `tmout > 0` install a SIGALRM-driven
1644/// timeout that aborts the read.
1645#[allow(non_snake_case)]
1646pub fn zfread(fd: i32, bf: &mut [u8], sz: libc::off_t, tmout: i32) -> i32 {
1647 // c:1307
1648 let ret: isize; // c:1307 int ret
1649
1650 // c:1311-1312 — no timeout: plain read.
1651 if tmout == 0 {
1652 let n = unsafe { libc::read(fd, bf.as_mut_ptr() as *mut libc::c_void, sz as libc::size_t) };
1653 return n as i32; // c:1312
1654 }
1655
1656 // c:1314-1318 — `if (setjmp(zfalrmbuf)) { alarm(0); zwarnnam(...,
1657 // "timeout on network read"); return -1; }`. C uses
1658 // setjmp/longjmp; the SIGALRM handler calls longjmp(zfalrmbuf, 1)
1659 // which unwinds back into this if-block. Rust port can't use
1660 // setjmp/longjmp (UB across drop boundaries) so models the same
1661 // semantic via the ZFDRRRRING atomic flag that zfhandler sets at
1662 // alarm fire. Two check sites needed:
1663 // 1. Before alarm install — handle "previous alarm fired between
1664 // zfread calls" (rare but possible if user code chained reads
1665 // with no intermediate zfalarm reset).
1666 // 2. After read returns — handle "alarm fired during THIS read,
1667 // kernel returned EINTR but Rust has no setjmp jump-back".
1668 // Prior port had only the first check; the second was the
1669 // gap — a SIGALRM-interrupted read returned -1 silently with
1670 // no "timeout on network read" warning, leaving the user
1671 // unable to distinguish timeout from other I/O failure.
1672 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
1673 // c:1314 setjmp — stale alarm from before this call.
1674 unsafe {
1675 libc::alarm(0);
1676 } // c:1315
1677 zwarnnam("zftp", "timeout on network read"); // c:1316
1678 return -1; // c:1317
1679 }
1680 zfalarm(tmout); // c:1319
1681
1682 // c:1321 — `ret = read(fd, bf, sz);`
1683 ret = unsafe { libc::read(fd, bf.as_mut_ptr() as *mut libc::c_void, sz as libc::size_t) };
1684 // c:1324 — `alarm(0);`
1685 unsafe {
1686 libc::alarm(0);
1687 }
1688 // c:1314 setjmp counterpart for THIS call — alarm fired during read.
1689 // ZFDRRRRING was zeroed inside zfalarm at line 212; if non-zero now,
1690 // the handler set it during read.
1691 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
1692 zwarnnam("zftp", "timeout on network read"); // c:1316
1693 return -1; // c:1317
1694 }
1695 ret as i32 // c:1325
1696}
1697
1698/// Port of `zfwrite(int fd, char *bf, off_t sz, int tmout)` from Src/Modules/zftp.c:1332.
1699/// C: `int zfwrite(int fd, char *bf, off_t sz, int tmout)` — write with
1700/// optional alarm timeout.
1701#[allow(non_snake_case)]
1702pub fn zfwrite(fd: i32, bf: &[u8], sz: i64, tmout: i32) -> i32 {
1703 // c:1332
1704 // c:1332 — `if (!tmout) return write(fd, bf, sz);`
1705 if tmout == 0 {
1706 // c:1335
1707 return unsafe {
1708 libc::write(fd, bf.as_ptr() as *const _, sz as usize) as i32 // c:1336
1709 };
1710 }
1711 // c:1339-1342 — `if (setjmp(zfalrmbuf)) { alarm(0); zwarnnam(...,
1712 // "timeout on network write"); return -1; }`.
1713 // Same setjmp pattern as zfread (c:1314-1318) — see the
1714 // accompanying zfread fix (cfe7560f58) for the ZFDRRRRING
1715 // adapter rationale. Two check sites:
1716 // 1. Before alarm install — stale alarm from before this call.
1717 // 2. After write returns — alarm fired during THIS write.
1718 // Prior port had neither: the "future refactor should plumb a
1719 // real timeout via select(2)/poll(2)" comment papered over the
1720 // missing semantic. ZFDRRRRING was zeroed inside zfalarm at
1721 // line 212; if non-zero post-write, zfhandler set it during the
1722 // blocked write.
1723 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
1724 // Stale alarm from earlier call.
1725 unsafe {
1726 libc::alarm(0);
1727 }
1728 zwarnnam("zftp", "timeout on network write");
1729 return -1;
1730 }
1731 zfalarm(tmout); // c:1344
1732 let ret = unsafe {
1733 libc::write(fd, bf.as_ptr() as *const _, sz as usize) as i32 // c:1346
1734 };
1735 unsafe {
1736 libc::alarm(0);
1737 } // c:1349
1738 // Post-write check matching the zfread fix.
1739 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
1740 zwarnnam("zftp", "timeout on network write"); // c:1341
1741 return -1; // c:1342
1742 }
1743 ret // c:1350
1744}
1745
1746/// Port of `static int zfread_eof` file-static from
1747/// `Src/Modules/zftp.c:1359`. Set by zfread_block when the ZFHD_EOFB
1748/// flag arrives; cleared at the top of every fresh transfer.
1749pub static zfread_eof: std::sync::atomic::AtomicI32 = // c:1359
1750 std::sync::atomic::AtomicI32::new(0);
1751
1752/// Port of `zfread_block(int fd, char *bf, off_t sz, int tmout)` from `Src/Modules/zftp.c:1359`.
1753/// C: `static int zfread_block(int fd, char *bf, off_t sz, int tmout)` —
1754/// read a block-mode framed record: a 3-byte zfheader followed by
1755/// `blksz` payload bytes. Loops over restart-marker blocks (ZFHD_MARK)
1756/// until a real data block or end-of-record (ZFHD_EOFB) arrives.
1757#[allow(non_snake_case)]
1758pub fn zfread_block(fd: i32, bf: &mut [u8], sz: libc::off_t, tmout: i32) -> i32 {
1759 // c:1359
1760 // c:1361-1364 — locals at fn top.
1761 let mut n: i32; // c:1361 int n
1762 let mut hdr = zfheader {
1763 flags: 0,
1764 bytes: [0u8; 2],
1765 }; // c:1362
1766 let mut blksz: libc::off_t = 0; // c:1363 off_t blksz
1767 let mut cnt: libc::off_t; // c:1363 off_t cnt
1768 let mut bfptr: usize; // c:1364 char *bfptr (offset into bf)
1769
1770 // c:1365-1403 — outer do-while loop: keep reading until we get a
1771 // non-marker block (or hit EOF).
1772 loop {
1773 // c:1365 do {
1774 // c:1367-1369 — read header bytes, retry on EINTR.
1775 let mut hdr_buf = [0u8; 3];
1776 loop {
1777 // c:1367 do
1778 n = zfread(fd, &mut hdr_buf, 3, tmout); // c:1368
1779 if !(n < 0
1780 && io::Error::last_os_error().raw_os_error() // c:1369 EINTR retry
1781 == Some(libc::EINTR))
1782 {
1783 break;
1784 }
1785 }
1786 // c:1370-1373 — short read → fail unless interrupted by SIGALRM.
1787 if n != 3 && ZFDRRRRING.load(Ordering::Relaxed) == 0 {
1788 zwarnnam("zftp", "failure reading FTP block header");
1789 return n; // c:1372
1790 }
1791 hdr.flags = hdr_buf[0] as i8;
1792 hdr.bytes[0] = hdr_buf[1];
1793 hdr.bytes[1] = hdr_buf[2];
1794 // c:1375-1376 — ZFHD_EOFB sets the file-static eof flag.
1795 if (hdr.flags as i32 & ZFHD_EOFB) != 0 {
1796 zfread_eof.store(1, Ordering::Relaxed); // c:1376
1797 }
1798 // c:1377 — network byte order: blksz = (b[0] << 8) | b[1].
1799 blksz = ((hdr.bytes[0] as libc::off_t) << 8) | (hdr.bytes[1] as libc::off_t);
1800 // c:1378-1385 — caller's buffer too small.
1801 if blksz > sz {
1802 zwarnnam("zftp", "block too large to handle");
1803 unsafe {
1804 *errno_ptr() = libc::EIO;
1805 } // c:1383
1806 return -1; // c:1384
1807 }
1808 // c:1386-1397 — drain the payload.
1809 bfptr = 0; // c:1386 bfptr = bf
1810 cnt = blksz; // c:1387
1811 while cnt > 0 {
1812 // c:1388
1813 let want = cnt as usize;
1814 let end = bfptr + want;
1815 if end > bf.len() {
1816 return -1;
1817 }
1818 n = zfread(fd, &mut bf[bfptr..end], cnt, tmout); // c:1389
1819 if n > 0 {
1820 // c:1390
1821 bfptr += n as usize; // c:1391
1822 cnt -= n as libc::off_t; // c:1392
1823 } else if n < 0
1824 && (errflag.load(Ordering::Relaxed) != 0
1825 || ZFDRRRRING.load(Ordering::Relaxed) != 0
1826 || io::Error::last_os_error().raw_os_error() != Some(libc::EINTR))
1827 {
1828 // c:1393
1829 return n; // c:1394
1830 } else {
1831 break; // c:1396
1832 }
1833 }
1834 // c:1398-1402 — short data block.
1835 if cnt != 0 {
1836 zwarnnam("zftp", "short data block");
1837 unsafe {
1838 *errno_ptr() = libc::EIO;
1839 } // c:1400
1840 return -1; // c:1401
1841 }
1842 // c:1403 — } while ((hdr.flags & ZFHD_MARK) && !zfread_eof);
1843 if !((hdr.flags as i32 & ZFHD_MARK) != 0 && zfread_eof.load(Ordering::Relaxed) == 0) {
1844 break;
1845 }
1846 }
1847 // c:1404 — return (hdr.flags & ZFHD_MARK) ? 0 : blksz;
1848 if (hdr.flags as i32 & ZFHD_MARK) != 0 {
1849 0
1850 } else {
1851 blksz as i32
1852 }
1853}
1854
1855/// Port of `zfwrite_block(int fd, char *bf, off_t sz, int tmout)` from Src/Modules/zftp.c:1411.
1856/// C: `int zfwrite_block(int fd, char *bf, off_t sz, int tmout)` —
1857/// frame the data with a `struct zfheader` and write block + payload.
1858#[allow(non_snake_case)]
1859pub fn zfwrite_block(fd: i32, bf: &[u8], sz: i64, tmout: i32) -> i32 {
1860 // c:1411
1861 let mut hdr = zfheader {
1862 bytes: [0u8; 2],
1863 flags: 0i8,
1864 }; // c:1411
1865 let mut n: i32;
1866 // c:1418-1424 — emit header, retry on EINTR.
1867 loop {
1868 hdr.bytes[0] = ((sz & 0xff00) >> 8) as u8; // c:1419
1869 hdr.bytes[1] = (sz & 0xff) as u8; // c:1420
1870 hdr.flags = if sz != 0 { 0i8 } else { ZFHD_EOFB as i8 }; // c:1421
1871 let hdr_bytes = unsafe { std::slice::from_raw_parts(&hdr as *const _ as *const u8, 3) };
1872 n = zfwrite(fd, hdr_bytes, 3, tmout); // c:1422
1873 if !(n < 0 && unsafe { *errno_ptr() } == libc::EINTR) {
1874 break;
1875 } // c:1424
1876 }
1877 if n != 3 {
1878 // c:1426
1879 return n; // c:1428
1880 }
1881 if sz != 0 {
1882 // c:1431
1883 n = zfwrite(fd, bf, sz, tmout); // c:1432
1884 }
1885 n // c:1434
1886}
1887
1888/// Port of `zfsenddata(char *name, int recv, int progress, off_t startat)` from `Src/Modules/zftp.c:1456`.
1889/// C: `static int zfsenddata(char *name, int recv, int progress, off_t startat)` —
1890/// move data between local fd (0/1) and the data connection fd
1891/// (`dfd`). Handles BINARY+ASCII mode, optional block-mode framing,
1892/// progress callback, and the abort/SYNCH sequence on error.
1893#[allow(non_snake_case)]
1894pub fn zfsenddata(name: &str, recv: i32, progress: i32, startat: libc::off_t) -> i32 {
1895 // c:1456
1896 // c:1458-1459 — buffer sizes.
1897 const ZF_BUFSIZE: usize = 32768;
1898 const ZF_ASCSIZE: usize = ZF_BUFSIZE / 2;
1899 // c:1461-1466 — locals at fn top.
1900 let mut n: i32; // c:1461 int n
1901 let mut ret: i32 = 0; // c:1461 ret = 0
1902 let gotack: i32 = 0; // c:1461 gotack = 0
1903 let fdin: i32; // c:1461
1904 let fdout: i32; // c:1461
1905 let mut fromasc: i32 = 0; // c:1461 fromasc = 0
1906 let mut toasc: i32 = 0; // c:1461 toasc = 0
1907 let mut rtmout: i32 = 0; // c:1462
1908 let mut wtmout: i32 = 0; // c:1462
1909 let mut lsbuf = vec![0u8; ZF_BUFSIZE]; // c:1463
1910 let mut ascbuf: Vec<u8> = Vec::new(); // c:1463 ascbuf = NULL
1911 let mut sofar: libc::off_t = 0; // c:1464
1912 let mut last_sofar: libc::off_t = 0; // c:1464
1913
1914 // c:1469-1481 — pre-transfer progress hook. Fires once at the
1915 // top of zfsenddata when ZFTP_COUNT starts at zero so the user
1916 // shfunc can set up its meter. After it runs, we seed `sofar`
1917 // with `startat` (resume-from offset) per c:1480.
1918 if progress != 0 {
1919 if let Some(mut shfunc) = getshfunc("zftp_progress") {
1920 // c:1473 — `osc = sfcontext;`
1921 let osc = SFCONTEXT.load(Ordering::Relaxed);
1922 SFCONTEXT.store(SFC_HOOK, Ordering::Relaxed); // c:1475
1923 // c:1477 — `doshfunc(shfunc, NULL, 1);`.
1924 let body_runner = || -> i32 {
1925 crate::ported::exec::run_function_body("zftp_progress", &[]).unwrap_or(0)
1926 };
1927 let _ = crate::ported::exec::doshfunc(
1928 &mut shfunc,
1929 vec!["zftp_progress".to_string()],
1930 true,
1931 body_runner,
1932 );
1933 SFCONTEXT.store(osc, Ordering::Relaxed); // c:1478
1934 // c:1480 — `sofar = last_sofar = startat;`.
1935 sofar = startat;
1936 last_sofar = startat;
1937 }
1938 }
1939
1940 // c:1482-1498 — direction-dependent fd + ascii-flag setup.
1941 let mut use_block_mode = false;
1942 {
1943 let state = match zftp_state().lock() {
1944 Ok(s) => s,
1945 Err(_) => return 1,
1946 };
1947 let sess = match state.current.as_deref().and_then(|k| state.sessions.get(k)) {
1948 Some(s) => s,
1949 None => return 1,
1950 };
1951 if recv != 0 {
1952 // c:1482
1953 fdin = sess.dfd; // c:1483
1954 fdout = 1; // c:1484
1955 // c:1485 — `rtmout = getiparam("ZFTP_TMOUT");`. paramtab read.
1956 rtmout = getiparam("ZFTP_TMOUT") as i32;
1957 // c:1486 — `if (ZFST_CTYP(zfstatusp[zfsessno]) == ZFST_ASCI)`
1958 // CTYP is the type the SERVER currently has (current_type),
1959 // not the user-requested ZFST_TYPE slice. zfsettype sends
1960 // TYPE lazily (c:1213/c:2559) and only updates CTYP on a
1961 // successful response — translating on the requested slice
1962 // would \r\n-decode a stream the server sent as IMAGE.
1963 if sess.current_type == ZFST_ASCI as i32 {
1964 // c:1486
1965 fromasc = 1; // c:1487
1966 }
1967 if sess.transfer_mode == ZFST_BLOC as i32 {
1968 // c:1488
1969 use_block_mode = true; // c:1489
1970 }
1971 } else {
1972 // c:1490
1973 fdin = 0; // c:1491
1974 fdout = sess.dfd; // c:1492
1975 // c:1493 — `wtmout = getiparam("ZFTP_TMOUT");`. paramtab read.
1976 wtmout = getiparam("ZFTP_TMOUT") as i32;
1977 // c:1494 — `if (ZFST_CTYP(...) == ZFST_ASCI)` — same CTYP
1978 // (server-side current type) read as the recv arm above.
1979 if sess.current_type == ZFST_ASCI as i32 {
1980 // c:1494
1981 toasc = 1; // c:1495
1982 }
1983 if sess.transfer_mode == ZFST_BLOC as i32 {
1984 // c:1496
1985 use_block_mode = true; // c:1497
1986 }
1987 }
1988 }
1989
1990 if progress != 0 {
1991 sofar = startat; // c:1480
1992 last_sofar = sofar;
1993 }
1994 let _ = last_sofar;
1995
1996 // c:1500-1501 — ascbuf for ASCII translation buffer.
1997 if toasc != 0 {
1998 ascbuf = vec![0u8; ZF_ASCSIZE]; // c:1501
1999 }
2000 zfpipe(); // c:1502
2001 zfread_eof.store(0, Ordering::Relaxed); // c:1503
2002
2003 // c:1504-1614 — main transfer loop.
2004 while ret == 0 && zfread_eof.load(Ordering::Relaxed) == 0 {
2005 // c:1505-1506 — read into either ascbuf or lsbuf.
2006 n = if toasc != 0 {
2007 if use_block_mode {
2008 zfread_block(fdin, &mut ascbuf, ZF_ASCSIZE as libc::off_t, rtmout)
2009 } else {
2010 zfread(fdin, &mut ascbuf, ZF_ASCSIZE as libc::off_t, rtmout)
2011 }
2012 } else if use_block_mode {
2013 zfread_block(fdin, &mut lsbuf, ZF_BUFSIZE as libc::off_t, rtmout)
2014 } else {
2015 zfread(fdin, &mut lsbuf, ZF_BUFSIZE as libc::off_t, rtmout)
2016 };
2017
2018 if n > 0 {
2019 // c:1507
2020 // c:1509-1520 — toasc: \n → \r\n.
2021 if toasc != 0 {
2022 let mut iptr = 0usize;
2023 let mut optr = 0usize;
2024 let mut cnt = n;
2025 while cnt > 0 {
2026 if ascbuf[iptr] == b'\n' {
2027 // c:1514
2028 if optr < lsbuf.len() {
2029 lsbuf[optr] = b'\r';
2030 optr += 1;
2031 }
2032 n += 1; // c:1516
2033 }
2034 if optr < lsbuf.len() {
2035 lsbuf[optr] = ascbuf[iptr];
2036 optr += 1;
2037 }
2038 iptr += 1;
2039 cnt -= 1;
2040 }
2041 }
2042 // c:1521-1532 — fromasc: \r\n → \n.
2043 if fromasc != 0 {
2044 if let Some(_start) = lsbuf[..n as usize].iter().position(|&b| b == b'\r') {
2045 let mut optr = 0usize;
2046 let mut iptr = 0usize;
2047 let len = n as usize;
2048 while iptr < len {
2049 if lsbuf[iptr] != b'\r' || iptr + 1 >= len || lsbuf[iptr + 1] != b'\n' {
2050 lsbuf[optr] = lsbuf[iptr];
2051 optr += 1;
2052 } else {
2053 n -= 1; // c:1529
2054 }
2055 iptr += 1;
2056 }
2057 }
2058 }
2059 // c:1533-1591 — write loop with EINTR + partial-write handling.
2060 let mut optr_off: usize = 0;
2061 sofar += n as libc::off_t; // c:1535
2062 loop {
2063 // c:1537 for(;;)
2064 let chunk = &lsbuf[optr_off..optr_off + n as usize];
2065 let newn: i32 = if use_block_mode && recv == 0 {
2066 zfwrite_block(fdout, chunk, n as libc::off_t, wtmout)
2067 } else {
2068 zfwrite(fdout, chunk, n as libc::off_t, wtmout)
2069 };
2070 if newn == n {
2071 break;
2072 } // c:1546
2073 if newn < 0 {
2074 // c:1548
2075 let errno = io::Error::last_os_error().raw_os_error();
2076 let drrr = ZFDRRRRING.load(Ordering::Relaxed) != 0;
2077 let efl = errflag.load(Ordering::Relaxed) != 0;
2078 if errno != Some(libc::EINTR) || efl || drrr {
2079 // c:1578
2080 if !drrr && (efl || errno != Some(libc::EPIPE)) {
2081 // c:1579-1580
2082 ret = if recv != 0 { 2 } else { 1 };
2083 zwarnnam(
2084 name, // c:1582
2085 &format!("write failed: {}", io::Error::last_os_error()),
2086 );
2087 } else {
2088 ret = if recv != 0 { 3 } else { 1 };
2089 }
2090 break;
2091 }
2092 continue; // c:1587
2093 }
2094 optr_off += newn as usize; // c:1589
2095 n -= newn; // c:1590
2096 }
2097 } else if n < 0 {
2098 // c:1592
2099 let errno = io::Error::last_os_error().raw_os_error();
2100 let drrr = ZFDRRRRING.load(Ordering::Relaxed) != 0;
2101 let efl = errflag.load(Ordering::Relaxed) != 0;
2102 if errno != Some(libc::EINTR) || efl || drrr {
2103 // c:1593
2104 if !drrr && (efl || errno != Some(libc::EPIPE)) {
2105 // c:1594
2106 ret = if recv != 0 { 1 } else { 2 };
2107 zwarnnam(
2108 name, // c:1597
2109 &format!("read failed: {}", io::Error::last_os_error()),
2110 );
2111 } else {
2112 ret = if recv != 0 { 1 } else { 3 };
2113 }
2114 break;
2115 }
2116 } else {
2117 // c:1602
2118 break; // c:1603
2119 }
2120 // c:1604-1613 — progress hook (zftp_progress shfunc dispatch):
2121 //
2122 // if (!ret && sofar != last_sofar && progress &&
2123 // (shfunc = getshfunc("zftp_progress"))) {
2124 // int osc = sfcontext;
2125 //
2126 // zfsetparam("ZFTP_COUNT", &sofar, ZFPM_READONLY|ZFPM_INTEGER);
2127 // sfcontext = SFC_HOOK;
2128 // doshfunc(shfunc, NULL, 1);
2129 // sfcontext = osc;
2130 // last_sofar = sofar;
2131 // }
2132 //
2133 // The hook lookup is part of the GATE — with no zftp_progress
2134 // function defined, C touches NOTHING: no ZFTP_COUNT update,
2135 // no last_sofar bump. The mid-transfer ZFTP_COUNT param exists
2136 // solely as the hook's input. A prior else-arm here updated
2137 // ZFTP_COUNT on every chunk regardless (param churn the C
2138 // never does) and bumped last_sofar outside the gate.
2139 if ret == 0 && sofar != last_sofar && progress != 0 {
2140 if let Some(mut shfunc) = getshfunc("zftp_progress") {
2141 // c:1605
2142 let osc = SFCONTEXT.load(Ordering::Relaxed); // c:1606
2143 zfsetparam(
2144 "ZFTP_COUNT",
2145 &sofar.to_string(),
2146 ZFPM_READONLY | ZFPM_INTEGER,
2147 ); // c:1608
2148 SFCONTEXT.store(SFC_HOOK, Ordering::Relaxed); // c:1609
2149 // c:1610 — `doshfunc(shfunc, NULL, 1);`. NULL doshargs
2150 // → argv = [fn-name only]; body_runner routes through
2151 // the host body-only entry.
2152 let body_runner = || -> i32 {
2153 crate::ported::exec::run_function_body("zftp_progress", &[]).unwrap_or(0)
2154 };
2155 let _ = crate::ported::exec::doshfunc(
2156 &mut shfunc,
2157 vec!["zftp_progress".to_string()],
2158 true,
2159 body_runner,
2160 );
2161 SFCONTEXT.store(osc, Ordering::Relaxed); // c:1611
2162 last_sofar = sofar; // c:1612
2163 }
2164 }
2165 }
2166 zfunpipe(); // c:1615
2167 ZFDRRRRING.store(0, Ordering::Relaxed); // c:1620
2168
2169 // c:1621-1625 — block-mode EOF marker on send completion.
2170 if errflag.load(Ordering::Relaxed) == 0 && ret == 0 && recv == 0 && use_block_mode {
2171 let eof_buf = [0u8; 1];
2172 if zfwrite_block(fdout, &eof_buf, 0, wtmout) < 0 {
2173 ret = 1; // c:1624
2174 }
2175 }
2176
2177 // c:1626-1676 — abort/SYNCH sequence on error.
2178 if errflag.load(Ordering::Relaxed) != 0 || ret > 1 {
2179 // c:1642 — IAC=255, IP=244, SYNCH=242 per Telnet RFC 854.
2180 let msg: [u8; 4] = [255, 244, 255, 242]; // c:1642
2181 if ret == 2 {
2182 // c:1644
2183 zwarnnam(name, "aborting data transfer..."); // c:1645
2184 }
2185 // c:1647 — holdintr(); block SIGINT around the abort handshake.
2186 crate::ported::signals::holdintr(); // c:1647
2187 // c:1651-1652 — send IAC IP IAC + SYNCH OOB on control connection.
2188 if let Ok(state) = zftp_state().lock() {
2189 if let Some(sess) = state.current.as_deref().and_then(|k| state.sessions.get(k)) {
2190 if let Some(ref ctrl) = sess.control {
2191 let cfd = ctrl.as_raw_fd();
2192 unsafe {
2193 libc::send(cfd, msg.as_ptr() as *const libc::c_void, 3, 0); // c:1651
2194 libc::send(
2195 cfd,
2196 msg[3..].as_ptr() as *const libc::c_void,
2197 1,
2198 libc::MSG_OOB,
2199 ); // c:1652
2200 }
2201 }
2202 }
2203 }
2204 zfsendcmd("ABOR\r\n"); // c:1654
2205 if lastcode.load(Ordering::Relaxed) != 226 {
2206 // c:1672
2207 ret = 1; // c:1673
2208 }
2209 // c:1675 — noholdintr(); restore SIGINT handling.
2210 crate::ported::signals::noholdintr(); // c:1675
2211 }
2212
2213 // c:1678-1679 — free ascbuf (Rust Drop).
2214 drop(ascbuf);
2215 zfclosedata(); // c:1680
2216 if gotack == 0 && zfgetmsg() > 2 {
2217 // c:1681
2218 ret = 1; // c:1682
2219 }
2220 if ret != 0 {
2221 1
2222 } else {
2223 0
2224 } // c:1683
2225}
2226
2227// =====================================================================
2228// `enum { ZFST_* }` from `Src/Modules/zftp.c` — bit-packed shared-fd
2229// status word used by the `zfstatfd` mechanism so a subshell can
2230// detect type/mode/connection changes in the parent shell.
2231// =====================================================================
2232
2233/// `ZF_BUFSIZE` from `Src/Modules/zftp.c:1458`. Default I/O block
2234/// size for the zftp byte-stream pump.
2235pub const ZF_BUFSIZE: usize = 32_768; // c:1458
2236
2237/// `ZF_ASCSIZE` from `Src/Modules/zftp.c:1459`.
2238/// `#define ZF_ASCSIZE (ZF_BUFSIZE/2)`. Smaller buffer for ASCII
2239/// mode (line-by-line CRLF translation can grow output up to 2x).
2240pub const ZF_ASCSIZE: usize = ZF_BUFSIZE / 2; // c:1459
2241
2242// Subcommand dispatch table for zftp. Each `zftp_<subcmd>` C function
2243// has the canonical signature `int zftp_<subcmd>(char *name, char **args, int flags)`.
2244// The C source parses the first argv element as the subcommand name
2245// and dispatches via `zftpcmdtab[]`. Rust port: each free fn matches
2246// the C signature and routes through the global `ZFTP_STATE` to call
2247// the corresponding `zftp_globals::<method>` on the live state.
2248
2249/// Port of `zftp_open(char *name, char **args, int flags)` from `Src/Modules/zftp.c:1690`.
2250/// C: `int zftp_open(char *name, char **args, int flags)` — opens a
2251/// TCP control connection (with optional `host[:port]` or IPv6
2252/// `[host]:port` syntax, falls back to `zfsess->userparams` when no
2253/// args, reads the 220 banner via `zfgetmsg()`, sets
2254/// ZFTP_HOST/PORT/IP/MODE params, and chains to `zftp_login()` when
2255/// extra args are present.
2256pub fn zftp_open(name: &str, args: &[&str], flags: i32) -> i32 {
2257 // c:1690
2258 let mut port: i32 = -1; // c:1698 port = -1
2259 let portnam: String; // c:1695 portnam = "ftp"
2260 let hostnam: String; // c:1696 hostnam
2261 let hostsuffix: String; // c:1696 hostsuffix
2262 let tmout: i32; // c:1697 tmout
2263
2264 // c:1701-1708 — fall back to userparams when no positional args.
2265 let mut effective: Vec<String> = args.iter().map(|s| s.to_string()).collect();
2266 if effective.is_empty() {
2267 // c:1701 !*args
2268 let up = zftp_state()
2269 .lock()
2270 .ok()
2271 .and_then(|s| {
2272 s.current
2273 .as_deref()
2274 .and_then(|k| s.sessions.get(k))
2275 .map(|sess| sess.userparams.clone())
2276 })
2277 .unwrap_or_default();
2278 if !up.is_empty() {
2279 effective = up; // c:1703 args = userparams
2280 } else {
2281 zwarnnam(name, "no host specified"); // c:1705
2282 return 1; // c:1706
2283 }
2284 }
2285
2286 // c:1715-1716 — close any existing connection.
2287 let already_open = zftp_state()
2288 .lock()
2289 .ok()
2290 .and_then(|s| {
2291 s.current
2292 .as_deref()
2293 .and_then(|k| s.sessions.get(k))
2294 .map(|sess| sess.control.is_some())
2295 })
2296 .unwrap_or(false);
2297 if already_open {
2298 // c:1715
2299 zfclose(0); // c:1716
2300 }
2301
2302 // c:1718 — dupstring(args[0]) — Rust String owns its bytes.
2303 let raw = effective[0].clone();
2304 // c:1720-1730 — IPv6 `[host]:port` bracket parse.
2305 if let Some(stripped) = raw.strip_prefix('[') {
2306 let close_idx = match stripped.find(']') {
2307 Some(i) => i,
2308 None => {
2309 zwarnnam(name, &format!("Invalid host format: {}", raw)); // c:1726
2310 return 1; // c:1727
2311 }
2312 };
2313 let after = &stripped[close_idx + 1..];
2314 if !after.is_empty() && !after.starts_with(':') {
2315 // c:1725
2316 zwarnnam(name, &format!("Invalid host format: {}", raw));
2317 return 1;
2318 }
2319 hostnam = stripped[..close_idx].to_string(); // c:1722
2320 hostsuffix = after.to_string(); // c:1729
2321 } else {
2322 hostnam = raw.clone();
2323 hostsuffix = raw; // c:1733 else branch
2324 }
2325
2326 // c:1735-1751 — :port suffix; numeric → htons-friendly, else look up.
2327 if let Some((_pre, post)) = hostsuffix.split_once(':') {
2328 // c:1735
2329 let trimmed = post.trim();
2330 match trimmed.parse::<i32>() {
2331 // c:1740 zstrtol
2332 Ok(p) => {
2333 // c:1750 numeric
2334 port = p;
2335 portnam = "ftp".to_string();
2336 }
2337 Err(_) => {
2338 // c:1744 non-numeric
2339 portnam = trimmed.to_string(); // c:1745
2340 port = -1; // c:1746
2341 }
2342 }
2343 } else {
2344 portnam = "ftp".to_string();
2345 }
2346
2347 // c:1755-1758 — protoent/servent lookups: skipped in Rust port
2348 // (TcpStream handles tcp directly). Port resolution falls back to
2349 // the standard /etc/services for non-numeric ports.
2350 let resolved_port: u16 = if port > 0 {
2351 port as u16
2352 } else {
2353 match portnam.as_str() {
2354 "ftp" => 21,
2355 "ftps" => 990,
2356 _ => {
2357 zwarnnam(name, &format!("Can't find port for service `{}'", portnam)); // c:1768
2358 return 1; // c:1769
2359 }
2360 }
2361 };
2362
2363 ZCFINISH.store(2, Ordering::Relaxed); // c:1772 zcfinish = 2
2364
2365 // c:1775 — `tmout = getiparam("ZFTP_TMOUT");`. Read paramtab; if
2366 // unset, fall back to 60-second connect timeout.
2367 tmout = {
2368 let v = getiparam("ZFTP_TMOUT") as i32;
2369 if v > 0 {
2370 v
2371 } else {
2372 60
2373 }
2374 };
2375
2376 // c:1778-1789 — `if (setjmp(zfalrmbuf)) { alarm(0);
2377 // queue_signals();
2378 // if ((hname = getsparam_u("ZFTP_HOST")) && *hname)
2379 // zwarnnam(name, "timeout connecting to %s", hname);
2380 // else
2381 // zwarnnam(name, "timeout on host name lookup");
2382 // unqueue_signals();
2383 // zfclose(0);
2384 // return 1;
2385 // }`
2386 // ZFDRRRRING adapter same as the other zftp setjmp ports (cfe7560f58,
2387 // 00c1c36dc9, bef84af815, f68bff9298). The diagnostic is host-aware:
2388 // if ZFTP_HOST was already set by a prior open call (re-open after
2389 // previous timeout), the message names the host; otherwise it
2390 // reports "timeout on host name lookup" since the resolve hasn't
2391 // completed yet.
2392 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
2393 unsafe {
2394 libc::alarm(0);
2395 }
2396 let hname = crate::ported::params::getsparam_u("ZFTP_HOST").unwrap_or_default();
2397 if !hname.is_empty() {
2398 zwarnnam(name, &format!("timeout connecting to {}", hname));
2399 } else {
2400 zwarnnam(name, "timeout on host name lookup");
2401 }
2402 zfclose(0); // c:1787
2403 return 1; // c:1788
2404 }
2405 // c:1790 — zfalarm(tmout): installed for the connect() phase.
2406 zfalarm(tmout);
2407
2408 // c:1803 — zsh_getipnodebyname → ToSocketAddrs resolves both v4+v6.
2409 let target = format!("{}:{}", hostnam, resolved_port);
2410 let addrs: Vec<std::net::SocketAddr> = match target.to_socket_addrs() {
2411 Ok(it) => it.collect(),
2412 Err(_) => {
2413 unsafe {
2414 libc::alarm(0);
2415 }
2416 zwarnnam(name, &format!("host not found: {}", hostnam)); // c:1815
2417 return 1; // c:1817
2418 }
2419 };
2420 if addrs.is_empty() {
2421 unsafe {
2422 libc::alarm(0);
2423 }
2424 zwarnnam(name, &format!("host not found: {}", hostnam));
2425 return 1;
2426 }
2427 // c:1818 — ZFTP_HOST.
2428 zfsetparam("ZFTP_HOST", &hostnam, ZFPM_READONLY); // c:1818
2429 // c:1824 — ZFTP_PORT as integer.
2430 zfsetparam(
2431 "ZFTP_PORT",
2432 &resolved_port.to_string(),
2433 ZFPM_READONLY | ZFPM_INTEGER,
2434 ); // c:1824
2435
2436 // c:1838 — tcp_socket + tcp_connect: connect_timeout loops addrs.
2437 let mut last_err: Option<io::Error> = None;
2438 let mut connected: Option<(TcpStream, std::net::SocketAddr)> = None;
2439 for addr in addrs.iter() {
2440 // c:1860 for addrp
2441 if errflag.load(Ordering::Relaxed) != 0 {
2442 break;
2443 }
2444 match TcpStream::connect_timeout(addr, Duration::from_secs(tmout.max(1) as u64)) {
2445 Ok(s) => {
2446 connected = Some((s, *addr));
2447 break;
2448 } // c:1867 SUCCEEDED
2449 Err(e) => {
2450 last_err = Some(e);
2451 } // c:1866 retry
2452 }
2453 }
2454 let (stream, used_addr) = match connected {
2455 Some(v) => v,
2456 None => {
2457 unsafe {
2458 libc::alarm(0);
2459 }
2460 zfunsetparam("ZFTP_HOST"); // c:1847
2461 zfunsetparam("ZFTP_PORT"); // c:1848
2462 let msg = last_err
2463 .as_ref()
2464 .map(|e| e.to_string())
2465 .unwrap_or_else(|| "connect failed".to_string());
2466 zwarnnam(name, &format!("connect failed: {}", msg)); // c:1879
2467 return 1; // c:1880
2468 }
2469 };
2470
2471 // c:1886-1890 — record peer IP.
2472 let pbuf = used_addr.ip().to_string();
2473 zfsetparam("ZFTP_IP", &pbuf, ZFPM_READONLY); // c:1887
2474
2475 unsafe {
2476 libc::alarm(0);
2477 } // c:1882
2478
2479 // c:1778 setjmp counterpart for THIS call — alarm fired during
2480 // resolve or connect. ZFDRRRRING was zeroed inside zfalarm; non-
2481 // zero now means zfhandler tripped during the blocked
2482 // to_socket_addrs or TcpStream::connect_timeout. Without this
2483 // check, a SIGALRM-cut-short connect that happened to succeed
2484 // (or appeared to via Rust's connect-timeout returning Ok on a
2485 // partial handshake) would proceed into the FTP-login phase
2486 // with a half-formed connection — confusing diagnostics later
2487 // instead of the C-faithful "timeout connecting to %s" up front.
2488 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
2489 let hname = crate::ported::params::getsparam_u("ZFTP_HOST").unwrap_or_default();
2490 if !hname.is_empty() {
2491 zwarnnam(name, &format!("timeout connecting to {}", hname));
2492 } else {
2493 zwarnnam(name, "timeout on host name lookup");
2494 }
2495 zfclose(0); // c:1787
2496 return 1; // c:1788
2497 }
2498
2499 ZFNOPEN.fetch_add(1, Ordering::Relaxed); // c:1852 zfnopen++
2500
2501 // c:1888 — zcfinish = 0 (we can now talk).
2502 ZCFINISH.store(0, Ordering::Relaxed); // c:1894
2503
2504 // c:1903-1904 — F_SETFD/FD_CLOEXEC on the fd.
2505 let fd = stream.as_raw_fd();
2506 unsafe {
2507 libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
2508 }
2509
2510 // c:1912-1914 — SO_OOBINLINE: in-line OOB data for control conn.
2511 unsafe {
2512 let one: libc::c_int = 1;
2513 libc::setsockopt(
2514 fd,
2515 libc::SOL_SOCKET,
2516 libc::SO_OOBINLINE,
2517 &one as *const _ as *const _,
2518 size_of::<libc::c_int>() as libc::socklen_t,
2519 );
2520 }
2521 // c:1917-1919 — IP_TOS / IPTOS_LOWDELAY for control.
2522 #[cfg(any(target_os = "linux", target_os = "macos"))]
2523 unsafe {
2524 let lowdelay: libc::c_int = 0x10; // IPTOS_LOWDELAY
2525 libc::setsockopt(
2526 fd,
2527 libc::IPPROTO_IP,
2528 libc::IP_TOS,
2529 &lowdelay as *const _ as *const _,
2530 size_of::<libc::c_int>() as libc::socklen_t,
2531 );
2532 }
2533
2534 // c:1923-1936 — store TcpStream as both control and cin.
2535 let stream_clone = match stream.try_clone() {
2536 Ok(c) => c,
2537 Err(_) => {
2538 zwarnnam(name, "file handling error"); // c:1932
2539 zfclose(0);
2540 return 1;
2541 }
2542 };
2543 if let Ok(mut state) = zftp_state().lock() {
2544 if let Some(sess) = state
2545 .current
2546 .clone()
2547 .and_then(|k| state.sessions.get_mut(&k))
2548 {
2549 sess.control = Some(stream);
2550 sess.cin = Some(stream_clone);
2551 sess.connected = true;
2552 sess.host = Some(hostnam.clone());
2553 sess.port = resolved_port;
2554 }
2555 }
2556
2557 // c:1947-1950 — read 220 banner.
2558 if zfgetmsg() >= 4 {
2559 // c:1947
2560 zfclose(0); // c:1948
2561 return 1; // c:1949
2562 }
2563
2564 // c:1952-1954 — has_size, has_mdtm, dfd reset; initial status word.
2565 if let Ok(mut state) = zftp_state().lock() {
2566 if let Some(sess) = state
2567 .current
2568 .clone()
2569 .and_then(|k| state.sessions.get_mut(&k))
2570 {
2571 sess.has_size = ZFCP_UNKN; // c:1952
2572 sess.has_mdtm = ZFCP_UNKN; // c:1952
2573 sess.dfd = -1; // c:1953
2574 sess.transfer_type = ZFST_ASCI; // c:1955 initial ASCII
2575 }
2576 }
2577
2578 // c:1981-1985 — ZFTP_MODE param, then chain into zftp_login when
2579 // more args remain.
2580 zfsetparam("ZFTP_MODE", "S", ZFPM_READONLY); // c:1982
2581 if effective.len() > 1 {
2582 // c:1984 *++args
2583 let rest: Vec<&str> = effective[1..].iter().map(|s| s.as_str()).collect();
2584 return zftp_login(name, &rest, flags); // c:1985
2585 }
2586
2587 // c:1988 — control alive?
2588 let alive = zftp_state()
2589 .lock()
2590 .ok()
2591 .and_then(|s| {
2592 s.current
2593 .as_deref()
2594 .and_then(|k| s.sessions.get(k))
2595 .map(|sess| sess.control.is_some())
2596 })
2597 .unwrap_or(false);
2598 if alive {
2599 0
2600 } else {
2601 1
2602 } // c:1988 return !control
2603}
2604
2605/// Port of `zfgetinfo(char *prompt, int noecho)` from `Src/Modules/zftp.c:1999`.
2606/// C: `static char * zfgetinfo(char *prompt, int noecho)` — prompt
2607/// the tty (echoing or with ECHO masked off for passwords) and read
2608/// one line of input.
2609#[allow(non_snake_case)]
2610pub fn zfgetinfo(prompt: &str, noecho: i32) -> Option<String> {
2611 // c:1999
2612 // c:2001-2006 — locals.
2613 let mut resettty: i32 = 0; // c:2001
2614 let mut instr = String::new(); // c:2005 char instr[256]
2615 let len: usize = 0; // c:2006 (unused in Rust path)
2616 let _ = len;
2617
2618 let saved_termios: Option<libc::termios>;
2619
2620 // c:2013 — if (isatty(0)) prompt + tty setup.
2621 if unsafe { libc::isatty(0) } != 0 {
2622 // c:2013
2623 if noecho != 0 {
2624 // c:2014-2033 — `ti = shttyinfo; ti.tio.c_lflag &= ~ECHO;
2625 // settyinfo(&ti); resettty = 1;`
2626 //
2627 // C reads the saved shell-tty state (shttyinfo global) so
2628 // the SHTTY fd (which may differ from stdin if the shell
2629 // re-dup'd) is used consistently. shttyinfo isn't ported
2630 // as a singleton yet, so use gettyinfo() which reads
2631 // SHTTY's current termios via fdgettyinfo(SHTTY) — the
2632 // canonical port at utils.rs:1926. settyinfo() at
2633 // utils.rs:1964 writes back via fdsettyinfo(SHTTY, ti)
2634 // with TCSADRAIN and EINTR-retry.
2635 //
2636 // Prior port used raw tcgetattr(0) + tcsetattr(0, TCSANOW)
2637 // which (a) read/wrote stdin instead of SHTTY (wrong when
2638 // SHTTY was re-dup'd, e.g. `zsh < /dev/tty &`), and (b)
2639 // used TCSANOW instead of TCSADRAIN, dropping any pending
2640 // output before the mode change — visible as cut-off
2641 // prompts on slow terminals.
2642 if let Some(mut ti) = crate::ported::utils::gettyinfo() {
2643 saved_termios = Some(ti); // c:2026 ti = shttyinfo (save for restore)
2644 ti.c_lflag &= !libc::ECHO; // c:2028
2645 let _ = crate::ported::utils::settyinfo(&ti); // c:2032
2646 resettty = 1; // c:2033
2647 } else {
2648 saved_termios = None;
2649 }
2650 } else {
2651 saved_termios = None;
2652 }
2653 // c:2035-2037 — fflush(stdin) + write prompt to stderr.
2654 eprint!("{}", prompt); // c:2036
2655 let _ = io::stderr().flush(); // c:2037
2656 } else {
2657 saved_termios = None;
2658 }
2659
2660 // c:2040-2043 — fgets(instr, 256, stdin); strip trailing \n.
2661 let stdin = io::stdin();
2662 let mut handle = stdin.lock();
2663 match handle.read_line(&mut instr) {
2664 // c:2040
2665 Ok(0) => instr.clear(), // c:2041 NULL → empty
2666 Ok(_) => {
2667 // c:2042-2043 strip \n
2668 if instr.ends_with('\n') {
2669 instr.pop();
2670 }
2671 }
2672 Err(_) => instr.clear(),
2673 }
2674
2675 // c:2045 — strret = dupstring(instr); (just keep instr as the result)
2676 let strret = instr.clone();
2677
2678 // c:2047-2052 — restore termios if we modified it.
2679 if resettty != 0 {
2680 // c:2047
2681 println!(); // c:2049 '\n' didn't echo
2682 let _ = io::stdout().flush(); // c:2050
2683 if let Some(ti) = saved_termios {
2684 // c:2051 — `settyinfo(&shttyinfo);` — restore via the
2685 // canonical helper so the SHTTY fd + TCSADRAIN + EINTR
2686 // retry match C exactly.
2687 let _ = crate::ported::utils::settyinfo(&ti);
2688 }
2689 }
2690
2691 Some(strret) // c:2054
2692}
2693
2694/// Port of `zftp_params(UNUSED(char *name), char **args, UNUSED(int flags))` from `Src/Modules/zftp.c:2064`.
2695/// C: list, clear ("-"), or set the current session's `userparams` array.
2696#[allow(unused_variables)]
2697pub fn zftp_params(name: &str, args: &[&str], flags: i32) -> i32 {
2698 // c:2064
2699 let prompts: [&str; 4] = ["Host: ", "User: ", "Password: ", "Account: "]; // c:2067
2700 // c:2071-2083 — no args: print current userparams (mask the password slot).
2701 if args.is_empty() {
2702 // c:2071 !*args
2703 let state = match zftp_state().lock() {
2704 Ok(s) => s,
2705 Err(_) => return 1,
2706 };
2707 let sess = match state.current.as_deref().and_then(|k| state.sessions.get(k)) {
2708 Some(s) => s,
2709 None => return 1,
2710 };
2711 if sess.userparams.is_empty() {
2712 // c:2082 else
2713 return 1; // c:2083
2714 }
2715 let mut out = io::stdout().lock();
2716 for (i, p) in sess.userparams.iter().enumerate() {
2717 // c:2073 for aptr,i
2718 if i == 2 {
2719 // c:2074 i == 2
2720 let len = p.len(); // c:2075 strlen
2721 for _ in 0..len {
2722 // c:2076 for j<len
2723 let _ = out.write_all(b"*"); // c:2077 fputc '*'
2724 }
2725 let _ = out.write_all(b"\n"); // c:2078 fputc '\n'
2726 } else {
2727 let _ = writeln!(out, "{}", p); // c:2080 fprintf
2728 }
2729 }
2730 return 0; // c:2081
2731 }
2732 // c:2084-2089 — single "-" arg: clear userparams.
2733 if args[0] == "-" {
2734 // c:2084 !strcmp "-"
2735 if let Ok(mut state) = zftp_state().lock() {
2736 if let Some(sess) = state
2737 .current
2738 .clone()
2739 .and_then(|k| state.sessions.get_mut(&k))
2740 {
2741 sess.userparams.clear(); // c:2085-2087 freearray
2742 }
2743 }
2744 return 0; // c:2088
2745 }
2746 // c:2090-2103 — replace userparams with new array.
2747 let len = args.len(); // c:2090 arrlen
2748 let mut newarr: Vec<String> = Vec::with_capacity(len); // c:2091 zshcalloc
2749 let efl_atomic = &errflag;
2750 for (i, aptr) in args.iter().enumerate() {
2751 // c:2092 for aptr,i
2752 if efl_atomic.load(Ordering::Relaxed) != 0 {
2753 // c:2092 !errflag
2754 break;
2755 }
2756 let str_val: String;
2757 if let Some(rest) = aptr.strip_prefix('?') {
2758 // c:2094 **aptr == '?'
2759 let prompt: &str = if !rest.is_empty() {
2760 rest
2761 } else {
2762 prompts[i.min(3)]
2763 }; // c:2095
2764 match zfgetinfo(prompt, if i == 2 { 1 } else { 0 }) {
2765 // c:2095 i == 2
2766 Some(s) => str_val = s,
2767 None => {
2768 return 1;
2769 }
2770 }
2771 } else if let Some(rest) = aptr.strip_prefix('\\') {
2772 // c:2097 **aptr=='\\'
2773 str_val = rest.to_string();
2774 } else {
2775 str_val = (*aptr).to_string();
2776 }
2777 newarr.push(str_val); // c:2098 ztrdup
2778 }
2779 if efl_atomic.load(Ordering::Relaxed) != 0 {
2780 // c:2100 if errflag
2781 // c:2101-2104 — free newarr; Rust Drop handles it.
2782 return 1; // c:2105
2783 }
2784 if let Ok(mut state) = zftp_state().lock() {
2785 if let Some(sess) = state
2786 .current
2787 .clone()
2788 .and_then(|k| state.sessions.get_mut(&k))
2789 {
2790 sess.userparams = newarr; // c:2107-2109
2791 }
2792 }
2793 0 // c:2110
2794}
2795
2796/// Port of `zftp_login(char *name, char **args, UNUSED(int flags))` from `Src/Modules/zftp.c:2118`.
2797/// C: send USER/PASS/ACCT, drive the reply state machine, set
2798/// ZFTP_USER/ACCOUNT/SYSTEM/TYPE parameters, probe SYST type, then
2799/// pull current directory via `zfgetcwd()`.
2800#[allow(unused_variables)]
2801pub fn zftp_login(name: &str, args: &[&str], flags: i32) -> i32 {
2802 // c:2118
2803 let mut ucmd: String; // c:2120 char *ucmd
2804 let mut passwd: Option<String> = None; // c:2120 *passwd = NULL
2805 let mut acct: Option<String> = None; // c:2120 *acct = NULL
2806 let user: String; // c:2121 char *user
2807 let mut stopit: i32; // c:2122 int stopit
2808 let mut arg_idx: usize = 0;
2809
2810 // c:2124-2125 — already logged in; REIN to reset.
2811 let already_logged_in = zftp_state()
2812 .lock()
2813 .ok()
2814 .and_then(|s| {
2815 s.current
2816 .as_deref()
2817 .and_then(|k| s.sessions.get(k))
2818 .map(|sess| sess.logged_in)
2819 })
2820 .unwrap_or(false);
2821 if already_logged_in && zfsendcmd("REIN\r\n") >= 4 {
2822 // c:2124
2823 return 1; // c:2125
2824 }
2825
2826 // c:2127 — clear ZFST_LOGI.
2827 if let Ok(mut state) = zftp_state().lock() {
2828 if let Some(sess) = state
2829 .current
2830 .clone()
2831 .and_then(|k| state.sessions.get_mut(&k))
2832 {
2833 sess.logged_in = false; // c:2127
2834 }
2835 }
2836
2837 // c:2128-2132 — user from args[0] or prompt.
2838 if arg_idx < args.len() {
2839 // c:2128 *args
2840 user = args[arg_idx].to_string(); // c:2129 user = *args++
2841 arg_idx += 1;
2842 } else {
2843 user = match zfgetinfo("User: ", 0) {
2844 // c:2131
2845 Some(s) => s,
2846 None => return 1,
2847 };
2848 }
2849
2850 // c:2134 — tricat("USER ", user, "\r\n").
2851 ucmd = format!("USER {}\r\n", user);
2852 stopit = 0; // c:2135
2853
2854 // c:2137-2138 — first send; ret==6 (write fail) → stopit=2.
2855 if zfsendcmd(&ucmd) == 6 {
2856 // c:2137
2857 stopit = 2; // c:2138
2858 }
2859
2860 // c:2140-2174 — state-machine on lastcode.
2861 let efl_atomic = &errflag;
2862 while stopit == 0 && efl_atomic.load(Ordering::Relaxed) == 0 {
2863 // c:2140
2864 let code = lastcode.load(Ordering::Relaxed);
2865 match code {
2866 230 | 202 => {
2867 // c:2142-2144
2868 stopit = 1; // c:2145
2869 }
2870 331 => {
2871 // c:2148 need password
2872 let pw = if arg_idx < args.len() {
2873 // c:2149
2874 let p = args[arg_idx].to_string(); // c:2150
2875 arg_idx += 1;
2876 p
2877 } else {
2878 match zfgetinfo("Password: ", 1) {
2879 // c:2152
2880 Some(s) => s,
2881 None => {
2882 stopit = 2;
2883 break;
2884 }
2885 }
2886 };
2887 passwd = Some(pw.clone()); // c:2120/2150 binding
2888 // c:2153 zsfree(ucmd); c:2154 tricat("PASS ", passwd, "\r\n").
2889 ucmd = format!("PASS {}\r\n", pw);
2890 if zfsendcmd(&ucmd) == 6 {
2891 // c:2155
2892 stopit = 2; // c:2156
2893 }
2894 }
2895 332 | 532 => {
2896 // c:2160-2161 need account
2897 let ac = if arg_idx < args.len() {
2898 // c:2162
2899 let a = args[arg_idx].to_string(); // c:2163
2900 arg_idx += 1;
2901 a
2902 } else {
2903 match zfgetinfo("Account: ", 0) {
2904 // c:2165
2905 Some(s) => s,
2906 None => {
2907 stopit = 2;
2908 break;
2909 }
2910 }
2911 };
2912 acct = Some(ac.clone());
2913 ucmd = format!("ACCT {}\r\n", ac); // c:2167
2914 if zfsendcmd(&ucmd) == 6 {
2915 // c:2168
2916 stopit = 2; // c:2169
2917 }
2918 }
2919 // c:2173-2179 — 421/501/503/530/550/default → unrecoverable.
2920 _ => {
2921 stopit = 2; // c:2180
2922 }
2923 }
2924 }
2925 // c:2184 zsfree(ucmd) — Rust Drop.
2926 let _ = passwd; // suppress unused-warn; password kept only for parity
2927
2928 // c:2185-2186 — control gone after exchange.
2929 let control_alive = zftp_state()
2930 .lock()
2931 .ok()
2932 .and_then(|s| {
2933 s.current
2934 .as_deref()
2935 .and_then(|k| s.sessions.get(k))
2936 .map(|sess| sess.control.is_some())
2937 })
2938 .unwrap_or(false);
2939 if !control_alive {
2940 // c:2185
2941 return 1; // c:2186
2942 }
2943 // c:2187-2190 — login failed.
2944 let code = lastcode.load(Ordering::Relaxed);
2945 if stopit == 2 || (code != 230 && code != 202) {
2946 // c:2187
2947 zwarnnam(name, "login failed"); // c:2188
2948 return 1; // c:2189
2949 }
2950
2951 // c:2192-2197 — warn on unused trailing args.
2952 if arg_idx < args.len() {
2953 // c:2192
2954 let cnt = args.len() - arg_idx; // c:2193-2194
2955 zwarnnam(
2956 name,
2957 &format!("warning: {} command arguments not used", cnt), // c:2195
2958 );
2959 }
2960
2961 // c:2198 — set ZFST_LOGI on the session.
2962 if let Ok(mut state) = zftp_state().lock() {
2963 if let Some(sess) = state
2964 .current
2965 .clone()
2966 .and_then(|k| state.sessions.get_mut(&k))
2967 {
2968 sess.logged_in = true; // c:2198
2969 sess.user = Some(user.clone());
2970 }
2971 }
2972 // c:2199 — ZFTP_USER readonly param.
2973 zfsetparam("ZFTP_USER", &user, ZFPM_READONLY); // c:2199
2974 if let Some(ref a) = acct {
2975 // c:2200
2976 zfsetparam("ZFTP_ACCOUNT", a, ZFPM_READONLY); // c:2201
2977 }
2978
2979 // c:2207-2226 — SYST probe gated by per-session ZFST_SYST cache bit
2980 // AND zfprefs ZFPF_DUMB bit (when DUMB set, skip the probe entirely).
2981 let already_probed = zftp_state()
2982 .lock()
2983 .ok()
2984 .and_then(|s| {
2985 s.current
2986 .as_deref()
2987 .and_then(|k| s.sessions.get(k))
2988 .map(|sess| sess.syst_probed)
2989 })
2990 .unwrap_or(false);
2991 let dumb = (zfprefs.load(Ordering::Relaxed) & ZFPF_DUMB) != 0; // c:2207
2992 // c:2206-2225 — SYST probe block. Structure differs from a one-liner
2993 // because the ZFST_SYST cache bit must be set REGARDLESS of whether
2994 // zfsendcmd succeeds:
2995 // if (!DUMB && !(status & ZFST_SYST)) {
2996 // if (zfsendcmd("SYST\r\n") == 2) {
2997 // ... parse + zfsetparam("ZFTP_SYSTEM", ...) ...
2998 // }
2999 // zfstatusp[zfsessno] |= ZFST_SYST; <-- always set
3000 // }
3001 //
3002 // C only retries SYST when the cache is unset; setting the cache
3003 // unconditionally inside the `!ZFST_SYST` guard means failed probes
3004 // (server returns 5xx for SYST) don't re-probe on every subsequent
3005 // zftp call within the same session.
3006 //
3007 // Prior Rust port collapsed both checks into a single `&&` chain so
3008 // `sess.syst_probed = true` only ran when the SYST send returned 2.
3009 // Servers that 5xx'd SYST got re-probed on every dispatch, wasting
3010 // one round-trip per zftp subcommand.
3011 if !dumb && !already_probed {
3012 if zfsendcmd("SYST\r\n") == 2 {
3013 // c:2208
3014 let systype = lastmsg.lock().ok().map(|m| m.clone()).unwrap_or_default();
3015 if systype.starts_with("UNIX Type: L8") {
3016 // c:2212-2218
3017 if let Ok(mut state) = zftp_state().lock() {
3018 if let Some(sess) = state
3019 .current
3020 .clone()
3021 .and_then(|k| state.sessions.get_mut(&k))
3022 {
3023 sess.transfer_type = ZFST_IMAG; // c:2220
3024 }
3025 }
3026 }
3027 zfsetparam("ZFTP_SYSTEM", &systype, ZFPM_READONLY); // c:2222
3028 }
3029 // c:2224 — `zfstatusp[zfsessno] |= ZFST_SYST;` — set the cache
3030 // bit unconditionally so a failed-SYST server isn't re-probed.
3031 if let Ok(mut state) = zftp_state().lock() {
3032 if let Some(sess) = state
3033 .current
3034 .clone()
3035 .and_then(|k| state.sessions.get_mut(&k))
3036 {
3037 sess.syst_probed = true; // c:2224
3038 }
3039 }
3040 }
3041
3042 // c:2228-2230 — ZFTP_TYPE param.
3043 let ttype = zftp_state()
3044 .lock()
3045 .ok()
3046 .and_then(|s| {
3047 s.current
3048 .as_deref()
3049 .and_then(|k| s.sessions.get(k))
3050 .map(|sess| sess.transfer_type)
3051 })
3052 .unwrap_or(ZFST_ASCI);
3053 let tbuf = if ZFST_TYPE(ttype) == ZFST_ASCI {
3054 "A"
3055 } else {
3056 "I"
3057 }; // c:2228
3058 zfsetparam("ZFTP_TYPE", tbuf, ZFPM_READONLY); // c:2229
3059
3060 // c:2236 — fetch current directory.
3061 zfgetcwd() // c:2236
3062}
3063
3064/// Port of `zftp_test(UNUSED(char *name), UNUSED(char **args), UNUSED(int flags))` from `Src/Modules/zftp.c:2251`.
3065/// C: `static int zftp_test(char *name, char **args, int flags)` —
3066/// returns 0 when the current session has a live control connection,
3067/// 1 otherwise (zftpcmdtab flags = ZFTP_TEST).
3068#[allow(unused_variables)]
3069pub fn zftp_test(name: &str, args: &[&str], flags: i32) -> i32 {
3070 // c:2251
3071 // c:2251 — early-return when no control connection.
3072 let control_fd = zftp_state().lock().ok().and_then(|s| {
3073 s.current
3074 .as_deref()
3075 .and_then(|k| s.sessions.get(k))
3076 .and_then(|sess| sess.control.as_ref().map(|c| c.as_raw_fd()))
3077 });
3078 let fd = match control_fd {
3079 // c:2262
3080 Some(f) => f,
3081 None => return 1, // c:2263
3082 };
3083 // c:2266-2280 — poll(2) with 0 timeout. POLLIN events on the
3084 // control fd mean the server pushed an unsolicited message (e.g.
3085 // "421 Timeout") — consume it via zfgetmsg.
3086 let mut pfd = libc::pollfd {
3087 fd,
3088 events: libc::POLLIN,
3089 revents: 0,
3090 };
3091 let ret = unsafe { libc::poll(&mut pfd, 1, 0) }; // c:2272
3092 let errno = io::Error::last_os_error().raw_os_error().unwrap_or(0);
3093 if ret < 0 && errno != libc::EINTR && errno != libc::EAGAIN {
3094 // c:2273
3095 zfclose(0); // c:2274
3096 } else if ret > 0 && pfd.revents != 0 {
3097 // c:2275
3098 zfgetmsg(); // c:2277 handles 421
3099 }
3100 // c:2305 — return zfsess->control ? 0 : 2;
3101 let still_alive = zftp_state()
3102 .lock()
3103 .ok()
3104 .and_then(|s| {
3105 s.current
3106 .as_deref()
3107 .and_then(|k| s.sessions.get(k))
3108 .map(|sess| sess.control.is_some())
3109 })
3110 .unwrap_or(false);
3111 if still_alive {
3112 0
3113 } else {
3114 2
3115 } // c:2305
3116}
3117
3118/// Port of `zftp_dir(char *name, char **args, int flags)` from `Src/Modules/zftp.c:2305`.
3119pub fn zftp_dir(name: &str, args: &[&str], flags: i32) -> i32 {
3120 // c:2305
3121 let cmd: String; // c:2305 char *cmd
3122 let ret: i32; // c:2309 int ret
3123 // c:2316 — zfsettype(ZFST_ASCI); RFC959 requires ASCII for LIST.
3124 zfsettype(ZFST_ASCI);
3125 // c:2318 — cmd = zfargstring(NLST or LIST, args);
3126 let verb = if (flags & ZFTP_NLST) != 0 {
3127 "NLST"
3128 } else {
3129 "LIST"
3130 };
3131 cmd = zfargstring(verb, args); // c:2318 — terminator from zfargstring c:559
3132 // c:2319 — ret = zfgetdata(name, NULL, cmd, 0);
3133 ret = zfgetdata(name, "", &cmd, 0);
3134 // c:2332 zsfree(cmd) — Rust Drop.
3135 if ret != 0 {
3136 return 1;
3137 } // c:2332-2322
3138 let _ = io::stdout().flush(); // c:2332
3139 zfsenddata(name, 1, 0, 0) // c:2332
3140}
3141
3142/// Port of `zftp_cd(UNUSED(char *name), char **args, int flags)` from `Src/Modules/zftp.c:2332`.
3143/// C: send `CDUP\r\n` or `CWD <dir>\r\n` based on flags + arg shape.
3144/// Then call zfgetcwd to update the cached pwd.
3145#[allow(unused_variables)]
3146pub fn zftp_cd(name: &str, args: &[&str], flags: i32) -> i32 {
3147 // c:2332
3148 let ret: i32; // c:2332 int ret
3149 // c:2337-2340 — CDUP when flag set OR arg is ".." / "../".
3150 let arg0 = args.first().copied().unwrap_or("");
3151 if (flags & ZFTP_CDUP) != 0 || arg0 == ".." || arg0 == "../" {
3152 // c:2337
3153 ret = zfsendcmd("CDUP\r\n"); // c:2339
3154 } else {
3155 // c:2340
3156 let cmd = format!("CWD {}\r\n", arg0); // c:2341 tricat
3157 ret = zfsendcmd(&cmd); // c:2342
3158 // c:2343 zsfree — Rust Drop.
3159 }
3160 if ret > 2 {
3161 return 1;
3162 } // c:2345
3163 // c:2347-2349 — `if (zfgetcwd()) return 1;`
3164 if zfgetcwd() != 0 {
3165 return 1;
3166 }
3167 0 // c:2351
3168}
3169
3170/// Port of `zfgetcwd()` from `Src/Modules/zftp.c:2358`.
3171/// C: `static int zfgetcwd(void)` — DUMB short-circuit, send PWD,
3172/// parse the reply (directory between optional `"` delimiters per
3173/// c:2370-2382), set `$ZFTP_PWD`, then fire the `zftp_chpwd` hook
3174/// (c:2389-2394). Fully implemented below — the lastmsg parse and
3175/// hook dispatch both run in this body.
3176#[allow(non_snake_case)]
3177pub fn zfgetcwd() -> i32 {
3178 // c:2358 — short-circuit when ZFPF_DUMB is set (don't fiddle with
3179 // variables in dumb mode).
3180 if (zfprefs.load(Ordering::Relaxed) & ZFPF_DUMB) != 0 {
3181 // c:2364
3182 return 1; // c:2365
3183 }
3184 if zfsendcmd("PWD\r\n") > 2 {
3185 // c:2366
3186 zfunsetparam("ZFTP_PWD"); // c:2367
3187 return 1; // c:2368
3188 }
3189 // c:2370-2382 — parse the PWD reply to extract the directory
3190 // path between optional `"` delimiters.
3191 //
3192 // ptr = lastmsg;
3193 // while (*ptr == ' ') ptr++;
3194 // if (!*ptr) return 1; /* ultra safe */
3195 // if (*ptr == '"') { ptr++; endc = '"'; }
3196 // else endc = ' ';
3197 // for (eptr = ptr; *eptr && *eptr != endc; eptr++);
3198 // zfsetparam("ZFTP_PWD", ztrduppfx(ptr, eptr-ptr), ZFPM_READONLY);
3199 //
3200 // Prior port collapsed this to "set if PWD command succeeded"
3201 // without actually setting ZFTP_PWD to the parsed path. Scripts
3202 // reading `$ZFTP_PWD` after `zftp cd somewhere` got a stale value
3203 // from a previous chdir or empty string entirely.
3204 let pwd_msg = lastmsg.lock().ok().map(|m| m.clone()).unwrap_or_default();
3205 let trimmed = pwd_msg.trim_start_matches(' '); // c:2371-2372
3206 if trimmed.is_empty() {
3207 // c:2373 — `if (!*ptr) return 1;`
3208 return 1; // c:2374
3209 }
3210 let (rest, endc) = if let Some(s) = trimmed.strip_prefix('"') {
3211 // c:2375-2378 — `if (*ptr == '"') { ptr++; endc = '"'; }`
3212 (s, '"')
3213 } else {
3214 // c:2378-2379 — `else endc = ' ';`
3215 (trimmed, ' ')
3216 };
3217 let path: String = rest.chars().take_while(|&c| c != endc).collect(); // c:2380-2381
3218 zfsetparam("ZFTP_PWD", &path, ZFPM_READONLY); // c:2382
3219
3220 let cwd_ret = 0; // c:2396 — `return 0;` post-parse success
3221
3222 // c:2388-2393 — zftp_chpwd hook: fire the shfunc with SFC_HOOK
3223 // context after ZFTP_PWD has been updated.
3224 if let Some(mut shfunc) = getshfunc("zftp_chpwd") {
3225 let osc = SFCONTEXT.load(Ordering::Relaxed);
3226 SFCONTEXT.store(SFC_HOOK, Ordering::Relaxed);
3227 // c:2393 — `doshfunc(shfunc, NULL, 1);`.
3228 let body_runner =
3229 || -> i32 { crate::ported::exec::run_function_body("zftp_chpwd", &[]).unwrap_or(0) };
3230 let _ = crate::ported::exec::doshfunc(
3231 &mut shfunc,
3232 vec!["zftp_chpwd".to_string()],
3233 true,
3234 body_runner,
3235 );
3236 SFCONTEXT.store(osc, Ordering::Relaxed);
3237 }
3238 cwd_ret
3239}
3240
3241/// Port of `zfsettype(int type)` from `Src/Modules/zftp.c:2404-2417`.
3242/// C: `static int zfsettype(int type)` — when the requested type differs
3243/// from the server's current type (ZFST_CTYP), send `TYPE A` or `TYPE I`
3244/// and on >2 response return 1 leaving CTYP unchanged; on success clear
3245/// the CTYP bits in `zfstatusp[zfsessno]` then set them from `type`.
3246/// `type` is renamed `typ` in Rust because `type` is a keyword.
3247#[allow(non_snake_case)]
3248pub fn zfsettype(typ: i32) -> i32 {
3249 // c:2407 — char buf[] = "TYPE X\r\n";
3250 let mut buf: [u8; 8] = *b"TYPE X\r\n";
3251 // c:2408 — already at this type? return 0.
3252 let cur_ctyp = zftp_state()
3253 .lock()
3254 .ok()
3255 .and_then(|s| {
3256 s.current
3257 .as_deref()
3258 .and_then(|k| s.sessions.get(k))
3259 .map(|sess| sess.current_type)
3260 })
3261 .unwrap_or(ZFST_CASC);
3262 if ZFST_TYPE(typ) == cur_ctyp {
3263 return 0; // c:2409
3264 }
3265 // c:2410 — buf[5] = (ZFST_TYPE(type) == ZFST_ASCI) ? 'A' : 'I';
3266 buf[5] = if ZFST_TYPE(typ) == ZFST_ASCI {
3267 b'A'
3268 } else {
3269 b'I'
3270 };
3271 let cmd = std::str::from_utf8(&buf).unwrap_or("TYPE A\r\n");
3272 // c:2411 — if (zfsendcmd(buf) > 2) return 1;
3273 if zfsendcmd(cmd) > 2 {
3274 return 1; // c:2412
3275 }
3276 // c:2413-2415 — clear current-type bits, then set from `type`.
3277 if let Ok(mut state) = zftp_state().lock() {
3278 if let Some(sess) = state
3279 .current
3280 .clone()
3281 .and_then(|k| state.sessions.get_mut(&k))
3282 {
3283 // C: zfstatusp[zfsessno] &= ~(ZFST_TMSK << ZFST_TBIT);
3284 // zfstatusp[zfsessno] |= type << ZFST_TBIT;
3285 // Rust models the CTYP slot as a dedicated field rather than
3286 // a bit slice, so the equivalent is `current_type = ZFST_TYPE(typ)`.
3287 sess.current_type = ZFST_TYPE(typ); // c:2413-2415
3288 }
3289 }
3290 0 // c:2416
3291}
3292
3293/// Port of `zftp_type(char *name, char **args, int flags)` from `Src/Modules/zftp.c:2426`.
3294/// C: set the transfer-type byte (A=ASCII, I=binary). When ZFTP_TASC/
3295/// ZFTP_TBIN flags are set (ascii/binary subcommands route here),
3296/// pick the type from the flag; otherwise read from `args[0]`. With no
3297/// args, print the current type.
3298pub fn zftp_type(name: &str, args: &[&str], flags: i32) -> i32 {
3299 // c:2426
3300 let mut tbuf: [u8; 2] = [b'A', 0]; // c:2428 char tbuf[2] = "A"
3301 let nt: u8; // c:2428 char nt
3302 let str: &str; // c:2428 char *str
3303 let _ = str;
3304
3305 if (flags & (ZFTP_TBIN | ZFTP_TASC)) != 0 {
3306 // c:2429
3307 nt = if (flags & ZFTP_TBIN) != 0 { b'I' } else { b'A' }; // c:2430
3308 } else if args.first().copied().unwrap_or("").is_empty() {
3309 // c:2431
3310 // No args: print current type ('A' or 'I').
3311 let ttype = zftp_state()
3312 .lock()
3313 .ok()
3314 .and_then(|s| {
3315 s.current
3316 .as_deref()
3317 .and_then(|k| s.sessions.get(k))
3318 .map(|sess| sess.transfer_type)
3319 })
3320 .unwrap_or(ZFST_IMAG as i32);
3321 let is_ascii = (ttype & ZFST_ASCI) != 0;
3322 println!("{}", if is_ascii { 'A' } else { 'I' }); // c:2436-2437
3323 let _ = io::stdout().flush(); // c:2438
3324 return 0; // c:2439
3325 } else {
3326 str = args[0];
3327 let c0 = str.as_bytes()[0].to_ascii_uppercase(); // c:2441 toupper
3328 if str.len() > 1 || (c0 != b'A' && c0 != b'B' && c0 != b'I') {
3329 // c:2446
3330 zwarnnam(
3331 name, // c:2447
3332 &format!("transfer type {} not recognised", str),
3333 );
3334 return 1; // c:2448
3335 }
3336 nt = if c0 == b'B' { b'I' } else { c0 }; // c:2451-2452
3337 }
3338
3339 // c:2455-2456 — update zfstatusp[zfsessno] (kept on the session.transfer_type).
3340 if let Ok(mut state) = zftp_state().lock() {
3341 if let Some(sess) = state
3342 .current
3343 .clone()
3344 .and_then(|k| state.sessions.get_mut(&k))
3345 {
3346 sess.transfer_type = if nt == b'I' { ZFST_IMAG } else { ZFST_ASCI } as i32;
3347 }
3348 }
3349 tbuf[0] = nt; // c:2464
3350 let tb = std::str::from_utf8(&tbuf[..1]).unwrap_or("A");
3351 zfsetparam("ZFTP_TYPE", tb, ZFPM_READONLY); // c:2464
3352 0 // c:2464
3353}
3354
3355/// Port of `zftp_mode(char *name, char **args, UNUSED(int flags))` from `Src/Modules/zftp.c:2464`.
3356/// C: set stream-mode (S=stream, B=block). With no arg, print current.
3357#[allow(unused_variables)]
3358pub fn zftp_mode(name: &str, args: &[&str], flags: i32) -> i32 {
3359 // c:2464
3360 let str: &str;
3361 let nt: u8; // c:2467 int nt
3362
3363 if args.first().copied().unwrap_or("").is_empty() {
3364 // c:2469
3365 let tmode = zftp_state()
3366 .lock()
3367 .ok()
3368 .and_then(|s| {
3369 s.current
3370 .as_deref()
3371 .and_then(|k| s.sessions.get(k))
3372 .map(|sess| sess.transfer_mode)
3373 })
3374 .unwrap_or(ZFST_STRE as i32);
3375 let is_stream = tmode == ZFST_STRE;
3376 println!("{}", if is_stream { 'S' } else { 'B' }); // c:2470-2471
3377 let _ = io::stdout().flush(); // c:2472
3378 return 0; // c:2473
3379 }
3380 str = args[0];
3381 nt = str.as_bytes()[0].to_ascii_uppercase(); // c:2475
3382 if str.len() > 1 || (nt != b'S' && nt != b'B') {
3383 // c:2476
3384 zwarnnam(
3385 name, // c:2477
3386 &format!("transfer mode {} not recognised", str),
3387 );
3388 return 1; // c:2478
3389 }
3390 let cmd = format!("MODE {}\r\n", nt as char); // c:2480 cmd[5] = nt
3391 if zfsendcmd(&cmd) > 2 {
3392 // c:2481
3393 return 1; // c:2482
3394 }
3395 // c:2483-2484 — update session transfer_mode.
3396 if let Ok(mut state) = zftp_state().lock() {
3397 if let Some(sess) = state
3398 .current
3399 .clone()
3400 .and_then(|k| state.sessions.get_mut(&k))
3401 {
3402 sess.transfer_mode = if nt == b'S' { ZFST_STRE } else { ZFST_BLOC } as i32;
3403 }
3404 }
3405 let mb = (nt as char).to_string();
3406 zfsetparam("ZFTP_MODE", &mb, ZFPM_READONLY); // c:2491
3407 0 // c:2491
3408}
3409
3410/// Port of `zftp_local(UNUSED(char *name), char **args, int flags)` from `Src/Modules/zftp.c:2491`.
3411#[allow(unused_variables)]
3412pub fn zftp_local(name: &str, args: &[&str], flags: i32) -> i32 {
3413 // c:2491
3414 let more = args.len() > 1; // c:2493 more = !!args[1]
3415 let mut ret: i32 = 0; // c:2493 ret = 0
3416 let dofd = args.is_empty(); // c:2493 dofd = !*args
3417 let mut i = 0usize;
3418 loop {
3419 // c:2494 while (*args || dofd)
3420 if !dofd && i >= args.len() {
3421 break;
3422 }
3423 let arg = if dofd { "" } else { args[i] };
3424 let mut sz: libc::off_t = 0; // c:2495 off_t sz
3425 let mut mt: Option<String> = None; // c:2496 char *mt
3426 // c:2497-2498 — zfstats(*args, !(flags & ZFTP_HERE), &sz, &mt, dofd ? 0 : -1);
3427 let remote = if (flags & ZFTP_HERE) != 0 { 0 } else { 1 };
3428 let fd = if dofd { 0 } else { -1 };
3429 // c:2497-2498 — zftp_local wants BOTH probes (&sz, &mt).
3430 let newret = zfstats(arg, remote, Some(&mut sz), Some(&mut mt), fd);
3431 if newret == 2 {
3432 // c:2499
3433 return 2; // c:2500
3434 } else if newret != 0 {
3435 // c:2501
3436 ret = 1; // c:2502
3437 // c:2503-2504 — Rust Drop.
3438 i += 1; // c:2505
3439 continue; // c:2506
3440 }
3441 if more {
3442 // c:2508
3443 print!("{} ", arg); // c:2509-2510
3444 }
3445 let mt_s = mt.unwrap_or_default();
3446 println!("{} {}", sz, mt_s); // c:2517
3447 if dofd {
3448 break;
3449 } // c:2520-2521
3450 i += 1; // c:2522
3451 }
3452 let _ = io::stdout().flush(); // c:2544
3453 ret // c:2544
3454}
3455
3456/// Port of `zftp_getput(char *name, char **args, int flags)` from `Src/Modules/zftp.c:2544`.
3457pub fn zftp_getput(name: &str, args: &[&str], flags: i32) -> i32 {
3458 // c:2544
3459 let mut ret: i32 = 0; // c:2546 ret = 0
3460 let recv = (flags & ZFTP_RECV) != 0; // c:2546 recv
3461 let mut getsize: i32 = 0; // c:2546 getsize = 0
3462 let progress: i32 = 1; // c:2546 progress = 1
3463 let cmd_pfx = if recv {
3464 "RETR "
3465 }
3466 // c:2547
3467 else if (flags & ZFTP_APPE) != 0 {
3468 "APPE "
3469 } else {
3470 "STOR "
3471 };
3472
3473 // c:2559 — zfsettype(ZFST_TYPE(zfstatusp[zfsessno]));
3474 let ttype = zftp_state()
3475 .lock()
3476 .ok()
3477 .and_then(|s| {
3478 s.current
3479 .as_deref()
3480 .and_then(|k| s.sessions.get(k))
3481 .map(|sess| sess.transfer_type)
3482 })
3483 .unwrap_or(ZFST_IMAG as i32);
3484 zfsettype(ttype);
3485
3486 if recv {
3487 let _ = io::stdout().flush();
3488 } // c:2561-2562
3489
3490 // c:2563 — for (; *args; args++) — with REST advancing args twice.
3491 let mut i = 0usize;
3492 while i < args.len() {
3493 let arg = args[i];
3494 let mut rest_cmd: String = String::new(); // c:2564 char *rest = NULL
3495 let mut startat: libc::off_t = 0; // c:2565
3496
3497 // c:2566-2587 — getsize hint via zfstats + initial progress
3498 // callback. Only fires when a zftp_progress shfunc is defined.
3499 if progress != 0 && getshfunc("zftp_progress").is_some() {
3500 let mut sz: libc::off_t = -1; // c:2567
3501 let mut _mdtm: Option<String> = None;
3502 // c:2576-2585 — SIZE probe gate:
3503 // if ((!(zfprefs & ZFPF_DUMB) &&
3504 // (zfstatusp[zfsessno] & (ZFST_NOSZ|ZFST_TRSZ))
3505 // != ZFST_TRSZ)
3506 // || !recv) {
3507 // zfstats(...);
3508 // if (recv && sz == -1) getsize = 1;
3509 // } else
3510 // getsize = 1;
3511 // "Probe SIZE if (not DUMB AND the cache doesn't say the
3512 // server embeds sizes) OR we're sending (need the LOCAL
3513 // file's size)." `(status & (NOSZ|TRSZ)) == ZFST_TRSZ`
3514 // ⟺ trsz && !nosz ⟺ first RETR's 150 reply parsed a
3515 // byte count — skip the SIZE round-trip and let zfgetdata
3516 // re-parse the reply (getsize = 1, c:2585). The session
3517 // trsz/nosz mirrors are written by zfgetdata (c:1102/1109).
3518 let dumb = (zfprefs.load(Ordering::Relaxed) & ZFPF_DUMB) != 0; // c:2576
3519 let server_embeds_size = zftp_state()
3520 .lock()
3521 .ok()
3522 .and_then(|s| {
3523 s.current
3524 .as_deref()
3525 .and_then(|k| s.sessions.get(k))
3526 .map(|sess| sess.trsz && !sess.nosz)
3527 })
3528 .unwrap_or(false); // c:2577 (status & (NOSZ|TRSZ)) == ZFST_TRSZ
3529 if (!dumb && !server_embeds_size) || !recv {
3530 // c:2576-2578
3531 // c:2580 — `zfstats(*args, recv, &sz, NULL, 0);` —
3532 // size only, NO mdtm probe (NULL second out-param).
3533 let _ = zfstats(
3534 arg,
3535 if recv { 1 } else { 0 }, // c:2580
3536 Some(&mut sz),
3537 None,
3538 0,
3539 );
3540 if recv && sz == -1 {
3541 // c:2582
3542 getsize = 1; // c:2583
3543 }
3544 } else {
3545 getsize = 1; // c:2585
3546 }
3547 zfstarttrans(arg, if recv { 1 } else { 0 }, sz); // c:2587
3548 }
3549
3550 // c:2589-2592 — REST resume.
3551 if (flags & ZFTP_REST) != 0 && i + 1 < args.len() {
3552 startat = args[i + 1].parse().unwrap_or(0);
3553 rest_cmd = format!("REST {}\r\n", args[i + 1]);
3554 }
3555
3556 // c:2594 — ln = tricat(cmd, *args, "\r\n");
3557 let ln = format!("{}{}\r\n", cmd_pfx, arg);
3558
3559 // c:2596 — zfgetdata returns 0 on success, 1 on failure.
3560 let gd = zfgetdata(name, &rest_cmd, &ln, getsize);
3561 if gd != 0 {
3562 ret = 2; // c:2597
3563 } else {
3564 // c:2598 — zfsenddata(name, recv, progress, startat).
3565 if zfsenddata(name, if recv { 1 } else { 0 }, progress, startat) != 0 {
3566 ret = 1; // c:2599
3567 }
3568 }
3569 // c:2600 — zsfree(ln) — Rust Drop.
3570
3571 // c:2601-2616 — final progress callback:
3572 //
3573 // /*
3574 // * The progress report isn't started till zfsenddata(), where
3575 // * it's the first item. Hence we send a final progress report
3576 // * if and only if we called zfsenddata();
3577 // */
3578 // if (progress && ret != 2 &&
3579 // (shfunc = getshfunc("zftp_progress"))) {
3580 // /* progress to finish: ZFTP_TRANSFER set to GF or PF */
3581 // int osc = sfcontext;
3582 //
3583 // zfsetparam("ZFTP_TRANSFER", ztrdup(recv ? "GF" : "PF"),
3584 // ZFPM_READONLY);
3585 // sfcontext = SFC_HOOK;
3586 // doshfunc(shfunc, NULL, 1);
3587 // sfcontext = osc;
3588 // }
3589 //
3590 // The hook lookup is part of the gate (same shape as the
3591 // per-chunk c:1604 block): no zftp_progress function → no
3592 // "GF"/"PF" finish-marker write — zfendtrans (c:2624) unsets
3593 // ZFTP_TRANSFER moments later regardless. A prior else-arm
3594 // wrote the marker even without a hook.
3595 if progress != 0 && ret != 2 {
3596 if let Some(mut shfunc) = getshfunc("zftp_progress") {
3597 // c:2607
3598 let osc = SFCONTEXT.load(Ordering::Relaxed); // c:2609
3599 zfsetparam(
3600 "ZFTP_TRANSFER", // c:2611-2612
3601 if recv { "GF" } else { "PF" },
3602 ZFPM_READONLY,
3603 );
3604 SFCONTEXT.store(SFC_HOOK, Ordering::Relaxed); // c:2613
3605 // c:2614 — `doshfunc(shfunc, NULL, 1);`.
3606 let body_runner = || -> i32 {
3607 crate::ported::exec::run_function_body("zftp_progress", &[]).unwrap_or(0)
3608 };
3609 let _ = crate::ported::exec::doshfunc(
3610 &mut shfunc,
3611 vec!["zftp_progress".to_string()],
3612 true,
3613 body_runner,
3614 );
3615 SFCONTEXT.store(osc, Ordering::Relaxed); // c:2615
3616 }
3617 }
3618 // c:2617-2620 — REST consumed two args.
3619 if (flags & ZFTP_REST) != 0 {
3620 i += 1;
3621 }
3622 // c:2621-2622 — break on errflag.
3623 if errflag.load(Ordering::Relaxed) != 0 {
3624 break;
3625 }
3626 let _ = getsize;
3627 getsize = 0; // reset per-iteration
3628 i += 1;
3629 }
3630 zfendtrans(); // c:2635
3631 if ret != 0 {
3632 1
3633 } else {
3634 0
3635 } // c:2635
3636}
3637
3638/// Port of `zftp_delete(UNUSED(char *name), char **args, UNUSED(int flags))` from `Src/Modules/zftp.c:2635`.
3639/// C: walk `args`, send `DELE <name>\r\n` for each. Returns 1 if any
3640/// DELE got a 3xx+ reply, else 0.
3641#[allow(unused_variables)]
3642pub fn zftp_delete(name: &str, args: &[&str], flags: i32) -> i32 {
3643 // c:2635
3644 let mut ret: i32 = 0; // c:2635 int ret = 0
3645 let cmd: String; // c:2638 char *cmd
3646 let _ = cmd;
3647 for aptr in args.iter() {
3648 // c:2639 for (aptr = args; *aptr; aptr++)
3649 let cmd = format!("DELE {}\r\n", aptr); // c:2640 tricat("DELE ", *aptr, "\r\n")
3650 if zfsendcmd(&cmd) > 2 {
3651 // c:2652
3652 ret = 1; // c:2652
3653 }
3654 // c:2652 zsfree(cmd) — Rust Drop.
3655 }
3656 ret // c:2652
3657}
3658
3659/// Port of `zftp_mkdir(UNUSED(char *name), char **args, int flags)` from `Src/Modules/zftp.c:2652`.
3660/// C: send `MKD <args[0]>\r\n` (or `RMD` when ZFTP_DELE flag set —
3661/// the `rmdir` subcommand routes through this fn with that bit).
3662#[allow(unused_variables)]
3663pub fn zftp_mkdir(name: &str, args: &[&str], flags: i32) -> i32 {
3664 // c:2652
3665 let ret: i32; // c:2652 int ret
3666 if args.is_empty() {
3667 return 1;
3668 }
3669 let cmd_pfx = if (flags & ZFTP_DELE) != 0 {
3670 "RMD "
3671 } else {
3672 "MKD "
3673 }; // c:2666
3674 let cmd = format!("{}{}\r\n", cmd_pfx, args[0]); // c:2666 tricat
3675 ret = (zfsendcmd(&cmd) > 2) as i32; // c:2666
3676 // c:2666 zsfree — Rust Drop.
3677 ret // c:2666
3678}
3679
3680/// Port of `zftp_rename(UNUSED(char *name), char **args, UNUSED(int flags))` from `Src/Modules/zftp.c:2666`.
3681/// C: send RNFR (rename-from), expect 3xx, then send RNTO (rename-to),
3682/// expect 2xx. Two-phase rename per RFC959.
3683#[allow(unused_variables)]
3684pub fn zftp_rename(name: &str, args: &[&str], flags: i32) -> i32 {
3685 // c:2666
3686 let mut ret: i32; // c:2666 int ret
3687 let cmd: String; // c:2669 char *cmd
3688 let _ = cmd;
3689 if args.len() < 2 {
3690 return 1;
3691 }
3692
3693 let cmd = format!("RNFR {}\r\n", args[0]); // c:2671 tricat("RNFR ", args[0], "\r\n")
3694 ret = 1; // c:2672
3695 if zfsendcmd(&cmd) == 3 {
3696 // c:2673
3697 // c:2674 zsfree(cmd) — Rust Drop.
3698 let cmd = format!("RNTO {}\r\n", args[1]); // c:2675
3699 if zfsendcmd(&cmd) == 2 {
3700 // c:2676
3701 ret = 0; // c:2690
3702 }
3703 }
3704 // c:2690 zsfree(cmd) — Rust Drop.
3705 ret // c:2690
3706}
3707
3708/// Port of `zftp_quote(UNUSED(char *name), char **args, int flags)` from `Src/Modules/zftp.c:2690`.
3709/// C: send a raw FTP command, optionally prefixed with `SITE ` when
3710/// ZFTP_SITE flag is set (the `site` subcommand routes here with the
3711/// bit). The first arg is the verb; subsequent args are appended.
3712#[allow(unused_variables)]
3713pub fn zftp_quote(name: &str, args: &[&str], flags: i32) -> i32 {
3714 // c:2690
3715 let ret: i32; // c:2690 int ret = 0
3716 let cmd: String; // c:2693 char *cmd
3717 let _ = cmd;
3718 let argv: Vec<&str> = args.to_vec();
3719 let cmd = if (flags & ZFTP_SITE) != 0 {
3720 // c:2695
3721 zfargstring("SITE", &argv) // c:2695 — terminator from zfargstring c:559
3722 } else {
3723 if argv.is_empty() {
3724 return 1;
3725 }
3726 zfargstring(argv[0], &argv[1..]) // c:2696
3727 };
3728 ret = (zfsendcmd(&cmd) > 2) as i32; // c:2697
3729 // c:2698 zsfree — Rust Drop.
3730 ret // c:2700
3731}
3732
3733/// Port of `zfclose(int leaveparams)` from `Src/Modules/zftp.c:2711`.
3734/// C: `void zfclose(int leaveparams)` — close the control connection
3735/// (and optionally tear down the ZFTP_* params), run the zftp_chpwd
3736/// hook, reset zfclosing+zfdrrrring tidy-up flags.
3737#[allow(non_snake_case)]
3738pub fn zfclose(leaveparams: i32) {
3739 // c:2711
3740 // c:2715-2716 — early-return when no live control connection.
3741 let alive = zftp_state()
3742 .lock()
3743 .ok()
3744 .and_then(|s| {
3745 s.current
3746 .as_deref()
3747 .and_then(|k| s.sessions.get(k))
3748 .map(|sess| sess.control.is_some())
3749 })
3750 .unwrap_or(false);
3751 if !alive {
3752 // c:2715
3753 return; // c:2716
3754 }
3755 // c:2718 — zfclosing = 1.
3756 ZFCLOSING.store(1, Ordering::Relaxed); // c:2718
3757 // c:2719-2725 — send QUIT before teardown unless server already EOF'd.
3758 if ZCFINISH.load(Ordering::Relaxed) != 2 {
3759 // c:2719
3760 let _ = zfsendcmd("QUIT\r\n"); // c:2724
3761 }
3762 // c:2727-2737 — drop cin + control TcpStream + decrement zfnopen.
3763 if let Ok(mut state) = zftp_state().lock() {
3764 if let Some(sess) = state
3765 .current
3766 .clone()
3767 .and_then(|k| state.sessions.get_mut(&k))
3768 {
3769 sess.cin = None; // c:2735 fclose(cin)
3770 sess.control = None; // c:2745 tcp_close
3771 sess.dfd = -1;
3772 sess.connected = false;
3773 sess.logged_in = false;
3774 }
3775 }
3776 ZFNOPEN.fetch_sub(1, Ordering::Relaxed); // c:2743
3777
3778 // c:2749-2759 — zfstatusp ZFST_CLOS + close zfstatfd when last open
3779 // session goes away. zfstatfd substrate not ported — skipped.
3780
3781 // c:2761-2774 — !leaveparams: unset ZFTP_* params + zftp_chpwd hook.
3782 if leaveparams == 0 {
3783 // c:2761
3784 for n in [
3785 "ZFTP_HOST",
3786 "ZFTP_PORT",
3787 "ZFTP_IP",
3788 "ZFTP_SYSTEM", // c:2763 zfparams[]
3789 "ZFTP_USER",
3790 "ZFTP_ACCOUNT",
3791 "ZFTP_PWD",
3792 "ZFTP_TYPE",
3793 "ZFTP_MODE",
3794 ] {
3795 zfunsetparam(n); // c:2764
3796 }
3797 // c:2767-2773 — zftp_chpwd shfunc dispatch.
3798 if let Some(mut shfunc) = getshfunc("zftp_chpwd") {
3799 // c:2767
3800 let osc = SFCONTEXT.load(Ordering::Relaxed);
3801 SFCONTEXT.store(SFC_HOOK, Ordering::Relaxed); // c:2770
3802 // c:2771 — `doshfunc(shfunc, NULL, 1);`.
3803 let body_runner = || -> i32 {
3804 crate::ported::exec::run_function_body("zftp_chpwd", &[]).unwrap_or(0)
3805 };
3806 let _ = crate::ported::exec::doshfunc(
3807 &mut shfunc,
3808 vec!["zftp_chpwd".to_string()],
3809 true,
3810 body_runner,
3811 );
3812 SFCONTEXT.store(osc, Ordering::Relaxed); // c:2772
3813 }
3814 }
3815 // c:2777 — zfclosing = zfdrrrring = 0.
3816 ZFCLOSING.store(0, Ordering::Relaxed); // c:2777
3817 ZFDRRRRING.store(0, Ordering::Relaxed); // c:2777
3818}
3819
3820/// Port of `zftp_close(UNUSED(char *name), UNUSED(char **args), UNUSED(int flags))` from `Src/Modules/zftp.c:2782`.
3821/// C: `static int zftp_close(UNUSED(char *name), UNUSED(char **args),
3822/// UNUSED(int flags))` — closes the current session's control
3823/// connection. Body is a single zfclose(0) call.
3824#[allow(unused_variables)]
3825pub fn zftp_close(name: &str, args: &[&str], flags: i32) -> i32 {
3826 // c:2782
3827 zfclose(0); // c:2782
3828 0 // c:2785
3829}
3830
3831/// Port of `newsession(char *nm)` from `Src/Modules/zftp.c:2803`.
3832///
3833/// Direct line-by-line port. Walks `zfsessions` looking for an
3834/// existing session named `nm` (c:2806-2812); if missing, allocates a
3835/// fresh `zftp_session`, registers it in the global state, sets
3836/// `dfd = -1`, and seeds an empty `params` slot (c:2814-2823). Both
3837/// paths leave `zfsess` (Rust: `current`) pointing at the named
3838/// session and refresh `ZFTP_SESSION` (c:2825). C signature is
3839/// `static void` — no return.
3840#[allow(non_snake_case)]
3841pub fn newsession(nm: &str) {
3842 // c:2806-2812 — walk zfsessions looking for a session matching `nm`.
3843 // In Rust the linked-list walk collapses to an IndexMap
3844 // contains_key; on hit we drop through (C `break`s out
3845 // of the loop with zfsess pointing at the match — the
3846 // walk assigns zfsess on every step) without
3847 // re-inserting.
3848 if let Ok(mut state) = zftp_state().lock() {
3849 if !state.sessions.contains_key(nm) {
3850 // c:2814-2823 — alloc a fresh session, register it.
3851 let mut sess = zftp_session::new(nm); // c:2814 zshcalloc(sizeof(struct zftp_session))
3852 sess.name = nm.to_string(); // c:2815 zfsess->name = ztrdup(nm)
3853 sess.dfd = -1; // c:2816 zfsess->dfd = -1
3854 sess.params.clear(); // c:2817 — empty params slot
3855 state.sessions.insert(nm.to_string(), sess); // c:2818 zaddlinknode(zfsessions, zfsess)
3856 // c:2820-2822 — zfsesscnt++ + zfstatusp realloc.
3857 // Rust per-session status lives on zftp_session.transfer_type;
3858 // counter is implicit in sessions.len().
3859 }
3860 // c:2806-2812 / c:2814 — either path leaves zfsess pointing at
3861 // the named session.
3862 state.current = Some(nm.to_string());
3863 }
3864 // c:2825 — zfsetparam("ZFTP_SESSION", ztrdup(zfsess->name), ZFPM_READONLY);
3865 zfsetparam("ZFTP_SESSION", nm, ZFPM_READONLY);
3866}
3867
3868/// Port of `savesession()` from `Src/Modules/zftp.c:2832`.
3869/// C: `static void savesession(void)` — copy each ZFTP_* shell param
3870/// into zfsess->params so session-switching preserves the values.
3871#[allow(non_snake_case)]
3872pub fn savesession() {
3873 // c:2832
3874 // c:2832 — char **ps, **pd, *val; (Rust uses indexing over slices)
3875 let val: String;
3876 let _ = val;
3877
3878 if let Ok(mut state) = zftp_state().lock() {
3879 let sess = match state
3880 .current
3881 .clone()
3882 .and_then(|k| state.sessions.get_mut(&k))
3883 {
3884 Some(s) => s,
3885 None => return,
3886 };
3887 // c:2836-2845 — for each zfparams[i], copy the current shell param.
3888 sess.params.clear();
3889 for ps in ZFPARAMS {
3890 // c:2836
3891 // c:2840 — `val = getsparam(*ps);`. paramtab is bucket-2-
3892 // consolidated now; read directly. Was a fake env
3893 // read which never picked up shell-internal params.
3894 let val = crate::ported::params::getsparam(ps).unwrap_or_default();
3895 // c:2856 / c:2843 — *pd = ztrdup(val) or NULL.
3896 sess.params.push(val);
3897 }
3898 // c:2856 — *pd = NULL; (terminator) — Rust Vec is self-terminating.
3899 }
3900}
3901
3902/// Port of `switchsession(char *nm)` from `Src/Modules/zftp.c:2856-2870`.
3903/// C body (verbatim):
3904/// newsession(nm);
3905/// for (ps = zfparams, pd = zfsess->params; *ps; ps++, pd++) {
3906/// if (*pd) {
3907/// zfsetparam(*ps, *pd, ZFPM_READONLY);
3908/// *pd = NULL;
3909/// } else
3910/// zfunsetparam(*ps);
3911/// }
3912#[allow(non_snake_case)]
3913pub fn switchsession(nm: &str) {
3914 // c:2860 — newsession(nm); creates the session if missing AND
3915 // sets zfsess + zfsessno.
3916 newsession(nm); // c:2860
3917 // c:2862-2869 — walk the new session's saved-params slot. For each
3918 // zfparams[i], if the saved slot has a value restore the shell
3919 // param (readonly) and clear the slot; otherwise unset. Prior Rust
3920 // port skipped this loop entirely, leaving stale ZFTP_* params
3921 // from the prior session visible after `zftp session NEWSESS`.
3922 let saved: Vec<Option<String>> = if let Ok(mut state) = zftp_state().lock() {
3923 match state
3924 .current
3925 .clone()
3926 .and_then(|k| state.sessions.get_mut(&k))
3927 {
3928 Some(sess) => {
3929 let copy: Vec<Option<String>> = ZFPARAMS
3930 .iter()
3931 .enumerate()
3932 .map(|(i, _)| sess.params.get(i).filter(|v| !v.is_empty()).cloned())
3933 .collect();
3934 // c:2866 — `*pd = NULL;` after restoring; clear the
3935 // saved slot so a re-switch back to this session
3936 // doesn't double-restore stale values.
3937 sess.params.clear();
3938 copy
3939 }
3940 None => return,
3941 }
3942 } else {
3943 return;
3944 };
3945 for (i, ps) in ZFPARAMS.iter().enumerate() {
3946 // c:2862
3947 match saved.get(i).cloned().flatten() {
3948 Some(val) => {
3949 // c:2864 — zfsetparam(*ps, *pd, ZFPM_READONLY);
3950 zfsetparam(ps, &val, ZFPM_READONLY);
3951 }
3952 None => {
3953 // c:2867 — zfunsetparam(*ps);
3954 zfunsetparam(ps);
3955 }
3956 }
3957 }
3958}
3959
3960// === auto-generated stubs ===
3961// Direct ports of static helpers from Src/Modules/zftp.c not
3962// yet covered above. zshrs links modules statically; live
3963// state owned by the module's typed struct. Name-parity shims.
3964
3965/// Port of `freesession(Zftp_session sptr)` from `Src/Modules/zftp.c:2874`.
3966/// C: `static void freesession(Zftp_session sptr)` — release `sptr`'s
3967/// name + params + userparams + the struct itself.
3968#[allow(non_snake_case)]
3969pub fn freesession(sptr: &mut zftp_session) {
3970 // c:2874
3971 // c:2874 — zsfree(sptr->name);
3972 sptr.name.clear();
3973 // c:2878-2881 — walk zfparams + sptr->params freeing each param value.
3974 sptr.params.clear();
3975 // c:2882-2883 — if (sptr->userparams) freearray(sptr->userparams);
3976 sptr.userparams.clear();
3977 // c:2884 — zfree(sptr, sizeof(struct zftp_session)); the caller's
3978 // owning Box::drop releases the struct memory.
3979}
3980
3981/// Port of `zftp_rmsession(UNUSED(char *name), char **args, UNUSED(int flags))` from `Src/Modules/zftp.c:2915`.
3982#[allow(unused_variables)]
3983pub fn zftp_rmsession(name: &str, args: &[&str], flags: i32) -> i32 {
3984 // c:2915
3985 // c:2915-2920 — locals: no, sptr, newsess (Zftp_session linked-list).
3986 let mut found_name: Option<String> = None; // sptr identity
3987 let mut is_current: bool = false; // sptr == zfsess
3988 let mut newsess: Option<String> = None; // c:2920
3989
3990 // c:2922-2928 — find session by name (or current if no arg).
3991 {
3992 let state = match zftp_state().lock() {
3993 Ok(s) => s,
3994 Err(_) => return 1,
3995 };
3996 let current = state.current.as_deref().unwrap_or("default").to_string();
3997 let target = args
3998 .first()
3999 .copied()
4000 .map(String::from)
4001 .unwrap_or_else(|| current.clone());
4002 for sess_name in state.sessions.keys() {
4003 // c:2923
4004 if *sess_name == target {
4005 found_name = Some(sess_name.to_string());
4006 is_current = *sess_name == current;
4007 break;
4008 }
4009 }
4010 }
4011 let target_name = match found_name {
4012 // c:2929
4013 Some(n) => n,
4014 None => return 1, // c:2930
4015 };
4016
4017 // c:2932-2956 — closing logic differs by current-vs-other.
4018 if is_current {
4019 // c:2932
4020 zfclosedata(); // c:2934
4021 zfclose(0); // c:2935
4022 // c:2941-2946 — pick a new current session if any others remain.
4023 let other = zftp_state()
4024 .lock()
4025 .ok()
4026 .map(|s| {
4027 s.sessions
4028 .keys()
4029 .find(|n| n.as_str() != target_name)
4030 .map(String::from)
4031 })
4032 .unwrap_or(None);
4033 if let Some(o) = other {
4034 // c:2945
4035 newsess = Some(o);
4036 }
4037 } else {
4038 // c:2947
4039 // c:2948-2956 — temporarily switch to target, close-non-destructive,
4040 // then switch back. Rust collapses this to a direct close on
4041 // the named session.
4042 let prev = zftp_state().lock().ok().and_then(|s| s.current.clone());
4043 if let Ok(mut state) = zftp_state().lock() {
4044 state.current = Some(target_name.clone()); // c:2949 zfsess = sptr
4045 }
4046 zfclosedata(); // c:2954
4047 zfclose(1); // c:2955 (leaveparams=1)
4048 if let (Some(p), Ok(mut state)) = (prev, zftp_state().lock()) {
4049 state.current = Some(p); // c:2956 zfsess = oldsess
4050 }
4051 }
4052
4053 // c:2958 — remnode(zfsessions, nptr); shift_remove keeps the
4054 // remaining entries in list order like C's remnode.
4055 if let Ok(mut state) = zftp_state().lock() {
4056 state.sessions.shift_remove(&target_name);
4057 }
4058 if let Some(n) = newsess {
4059 // c:2982
4060 switchsession(&n); // c:2983
4061 } else if zftp_state()
4062 .lock()
4063 .map(|s| s.sessions.is_empty())
4064 .unwrap_or(false)
4065 {
4066 // c:2985-2992 — we've just deleted the last session, so we
4067 // need to start again from scratch (newsession sets zfsess).
4068 newsession("default");
4069 }
4070 0 // c:2995
4071}
4072
4073/// Port of `bin_zftp(char *name, char **args, UNUSED(Options ops), UNUSED(int func))` from `Src/Modules/zftp.c:3002`.
4074/// `zftp` builtin entry point — C-faithful signature matching
4075/// `static int bin_zftp(char *name, char **args, Options ops, int func)`
4076/// from Src/Modules/zftp.c:3002. Acquires ZFTP_STATE, dispatches by
4077/// subcommand string, emits any captured output to stdout/stderr
4078/// based on status, returns the bare i32 status C's execbuiltin path
4079/// consumes.
4080#[allow(non_snake_case)]
4081/// WARNING: param names don't match C — Rust=(_nam, args, _func) vs C=(name, args, ops, func)
4082pub fn bin_zftp(
4083 _nam: &str,
4084 args: &[String], // c:3002
4085 _ops: &options,
4086 _func: i32,
4087) -> i32 {
4088 let argv: Vec<&str> = args.iter().map(|s| s.as_str()).collect();
4089
4090 // c:3074-3104 — recompute `zfprefs` from `$ZFTP_PREFS` on every
4091 // invocation. Letters: S=sendport (ZFPF_SNDP), P=passive
4092 // (ZFPF_PASV; ignored once SNDP is set per c:3090-3091), D=dumb
4093 // (ZFPF_DUMB). Unknown letters warn but don't abort.
4094 //
4095 // Prior Rust bin_zftp never read ZFTP_PREFS, so the boot_ default
4096 // of `PS` (ZFPF_SNDP|ZFPF_PASV from c:3219) was the only value
4097 // zfprefs ever held — user `ZFTP_PREFS=D` (dumb mode) was a
4098 // silent no-op.
4099 //
4100 // c:3074 + c:3105 — `queue_signals(); ... unqueue_signals();`.
4101 // C wraps the getsparam_u read + zfprefs mutation pair in
4102 // queue_signals so a SIGINT mid-parse can't observe a half-rewritten
4103 // zfprefs bitfield (a partial `D` parse without the prior `PS` left
4104 // intact would silently drop both sendport and passive modes for
4105 // any subsequent transfer in the signal handler chain). Mirror
4106 // exactly so the visible side-effect window matches C.
4107 crate::ported::signals_h::queue_signals(); // c:3074
4108 if let Some(prefs) = crate::ported::params::getsparam_u("ZFTP_PREFS") {
4109 zfprefs.store(0, Ordering::Relaxed); // c:3076 — zfprefs = 0;
4110 for ch in prefs.chars() {
4111 // c:3077
4112 match ch.to_ascii_uppercase() {
4113 // c:3078 toupper
4114 'S' => {
4115 // c:3079
4116 zfprefs.fetch_or(ZFPF_SNDP, Ordering::Relaxed); // c:3081
4117 }
4118 'P' => {
4119 // c:3084 — skip when SNDP already set (c:3090-3091).
4120 let cur = zfprefs.load(Ordering::Relaxed);
4121 if (cur & ZFPF_SNDP) == 0 {
4122 zfprefs.fetch_or(ZFPF_PASV, Ordering::Relaxed); // c:3091
4123 }
4124 }
4125 'D' => {
4126 // c:3094
4127 zfprefs.fetch_or(ZFPF_DUMB, Ordering::Relaxed); // c:3096
4128 }
4129 _ => {
4130 // c:3099
4131 zwarnnam(_nam, &format!("preference {} not recognized", ch));
4132 }
4133 }
4134 }
4135 }
4136 crate::ported::signals_h::unqueue_signals(); // c:3105
4137
4138 // c:3052-3061 — pre-dispatch connection probe:
4139 //
4140 // if (zfsess->control && !(zptr->flags & (ZFTP_TEST|ZFTP_SESS))) {
4141 // /*
4142 // * Test the connection for a bad fd or incoming message, but
4143 // * only if the connection was last heard of open, and
4144 // * if we are not about to call the test command anyway.
4145 // * Not worth it unless we have select() or poll().
4146 // */
4147 // ret = zftp_test("zftp test", NULL, 0);
4148 // }
4149 //
4150 // zftp_test (c:2270-2293): zero-timeout poll on the control fd;
4151 // poll error (non-EINTR/EAGAIN) → zfclose(0); readable → zfgetmsg()
4152 // drains the pending reply (the "421 Timeout" case — zfgetmsg's
4153 // own EOF handling closes the session). Returns 2 when the session
4154 // got dumped. The c:3063-3071 ZFTP_CONN gate then suppresses the
4155 // "not connected." message when ret == 2 ("enough messages
4156 // already"). Prior dispatcher skipped the probe entirely, so a
4157 // server-side timeout/disconnect surfaced as a confusing failure
4158 // of the NEXT command instead of the canonical 421 drain.
4159 let probe_dumped: bool = {
4160 let is_test = argv.first() == Some(&"test"); // ZFTP_TEST c:147
4161 let is_sess = matches!(argv.first(), Some(&"session") | Some(&"rmsession")); // ZFTP_SESS c:148
4162 let ctrl_fd: Option<i32> = zftp_state().lock().ok().and_then(|s| {
4163 s.current
4164 .as_deref()
4165 .and_then(|k| s.sessions.get(k))
4166 .and_then(|sess| {
4167 use std::os::unix::io::AsRawFd;
4168 sess.control.as_ref().map(|c| c.as_raw_fd())
4169 })
4170 });
4171 match (ctrl_fd, is_test || is_sess) {
4172 (Some(fd), false) => {
4173 // c:2270-2272 — poll(&pfd, 1, 0).
4174 let mut pfd = libc::pollfd {
4175 fd,
4176 events: libc::POLLIN,
4177 revents: 0,
4178 };
4179 let ret = unsafe { libc::poll(&mut pfd, 1, 0) };
4180 if ret < 0 {
4181 let eno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
4182 if eno != libc::EINTR && eno != libc::EAGAIN {
4183 zfclose(0); // c:2273
4184 }
4185 } else if ret > 0 && pfd.revents != 0 {
4186 /* handles 421 (maybe a bit noisily?) */
4187 zfgetmsg(); // c:2276
4188 }
4189 // c:2293 — dumped iff control is gone now.
4190 zftp_state()
4191 .lock()
4192 .ok()
4193 .and_then(|s| {
4194 s.current
4195 .as_deref()
4196 .and_then(|k| s.sessions.get(k))
4197 .and_then(|sess| sess.control.as_ref().map(|_| ()))
4198 })
4199 .is_none()
4200 }
4201 _ => false,
4202 }
4203 };
4204
4205 let mut zftp_guard = zftp_state().lock().unwrap_or_else(|e| {
4206 zftp_state_clear_poison();
4207 e.into_inner()
4208 });
4209 let zftp = &mut *zftp_guard;
4210 let args = &argv[..];
4211 let (status, output): (i32, String) = (|| {
4212 if args.is_empty() {
4213 return (1, "zftp: subcommand required\n".to_string());
4214 }
4215
4216 // c:3063-3072 — ZFTP_CONN gate with ret==2 suppression:
4217 // if ((zptr->flags & ZFTP_CONN) && !zfsess->control) {
4218 // if (ret != 2) {
4219 // /* with ret == 2, we just got dumped out in the
4220 // * test, so enough messages already. */
4221 // zwarnnam(fullname, "not connected.");
4222 // }
4223 // return 1;
4224 // }
4225 // The per-arm "not connected." messages below are the normal
4226 // path; when the probe just closed the session, exit 1 with
4227 // no message.
4228 if probe_dumped
4229 && !matches!(
4230 args[0],
4231 "open" | "params" | "test" | "session" | "rmsession"
4232 )
4233 {
4234 return (1, String::new()); // c:3064-3071
4235 }
4236
4237 match args[0] {
4238 "open" => {
4239 if args.len() < 2 {
4240 return (1, "zftp open: host required\n".to_string());
4241 }
4242
4243 let host = args[1];
4244 let port: Option<u16> = args.get(2).and_then(|s| s.parse().ok());
4245
4246 let session_name = zftp.current.as_deref().unwrap_or("default").to_string();
4247
4248 let sess = zftp
4249 .sessions
4250 .entry(session_name.clone())
4251 .or_insert_with(|| zftp_session::new(&session_name));
4252
4253 match sess.connect(host, port) {
4254 Ok(resp) => {
4255 if (resp.0 >= 100 && resp.0 < 400) {
4256 zftp.current = Some(session_name.clone());
4257 (0, resp.1)
4258 } else {
4259 (1, resp.1)
4260 }
4261 }
4262 Err(e) => (1, format!("zftp open: {}\n", e)),
4263 }
4264 }
4265
4266 "login" | "user" => {
4267 if args.len() < 2 {
4268 return (1, "zftp login: user required\n".to_string());
4269 }
4270
4271 let user = args[1];
4272 let pass = args.get(2).copied();
4273
4274 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4275 Some(s) => s,
4276 None => return (1, "zftp login: not connected.\n".to_string()),
4277 };
4278
4279 match sess.login(user, pass) {
4280 Ok(resp) => {
4281 if (resp.0 >= 200 && resp.0 < 300) {
4282 (0, resp.1)
4283 } else {
4284 (1, resp.1)
4285 }
4286 }
4287 Err(e) => (1, format!("zftp login: {}\n", e)),
4288 }
4289 }
4290
4291 "cd" => {
4292 if args.len() < 2 {
4293 return (1, "zftp cd: path required\n".to_string());
4294 }
4295
4296 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4297 Some(s) => s,
4298 None => return (1, "zftp cd: not connected.\n".to_string()),
4299 };
4300
4301 match sess.cd(args[1]) {
4302 Ok(resp) => {
4303 if (resp.0 >= 200 && resp.0 < 300) {
4304 (0, resp.1)
4305 } else {
4306 (1, resp.1)
4307 }
4308 }
4309 Err(e) => (1, format!("zftp cd: {}\n", e)),
4310 }
4311 }
4312
4313 "cdup" => {
4314 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4315 Some(s) => s,
4316 None => return (1, "zftp cdup: not connected.\n".to_string()),
4317 };
4318
4319 match sess.cdup() {
4320 Ok(resp) => {
4321 if (resp.0 >= 200 && resp.0 < 300) {
4322 (0, resp.1)
4323 } else {
4324 (1, resp.1)
4325 }
4326 }
4327 Err(e) => (1, format!("zftp cdup: {}\n", e)),
4328 }
4329 }
4330
4331 "pwd" => {
4332 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4333 Some(s) => s,
4334 None => return (1, "zftp pwd: not connected.\n".to_string()),
4335 };
4336
4337 match sess.pwd() {
4338 Ok((resp, pwd)) => {
4339 if let Some(p) = pwd {
4340 (0, format!("{}\n", p))
4341 } else {
4342 (1, resp.1)
4343 }
4344 }
4345 Err(e) => (1, format!("zftp pwd: {}\n", e)),
4346 }
4347 }
4348
4349 "dir" | "ls" => {
4350 let path = args.get(1).copied();
4351 let use_nlst = args[0] == "ls";
4352
4353 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4354 Some(s) => s,
4355 None => return (1, "zftp dir: not connected.\n".to_string()),
4356 };
4357
4358 let result = if use_nlst {
4359 sess.nlst(path)
4360 } else {
4361 sess.list(path)
4362 };
4363
4364 match result {
4365 Ok((resp, lines)) => {
4366 if (resp.0 >= 200 && resp.0 < 300) {
4367 (0, lines.join("\n") + "\n")
4368 } else {
4369 (1, resp.1)
4370 }
4371 }
4372 Err(e) => (1, format!("zftp dir: {}\n", e)),
4373 }
4374 }
4375
4376 "get" => {
4377 if args.len() < 2 {
4378 return (1, "zftp get: remote file required\n".to_string());
4379 }
4380
4381 let remote = args[1];
4382 let local = args.get(2).unwrap_or(&remote);
4383
4384 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4385 Some(s) => s,
4386 None => return (1, "zftp get: not connected.\n".to_string()),
4387 };
4388
4389 match sess.get(remote, Path::new(local)) {
4390 Ok(resp) => {
4391 if (resp.0 >= 200 && resp.0 < 300) {
4392 (0, String::new())
4393 } else {
4394 (1, resp.1)
4395 }
4396 }
4397 Err(e) => (1, format!("zftp get: {}\n", e)),
4398 }
4399 }
4400
4401 "put" => {
4402 if args.len() < 2 {
4403 return (1, "zftp put: local file required\n".to_string());
4404 }
4405
4406 let local = args[1];
4407 let remote = args.get(2).unwrap_or(&local);
4408
4409 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4410 Some(s) => s,
4411 None => return (1, "zftp put: not connected.\n".to_string()),
4412 };
4413
4414 match sess.put(Path::new(local), remote) {
4415 Ok(resp) => {
4416 if (resp.0 >= 200 && resp.0 < 300) {
4417 (0, String::new())
4418 } else {
4419 (1, resp.1)
4420 }
4421 }
4422 Err(e) => (1, format!("zftp put: {}\n", e)),
4423 }
4424 }
4425
4426 "delete" => {
4427 if args.len() < 2 {
4428 return (1, "zftp delete: file required\n".to_string());
4429 }
4430
4431 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4432 Some(s) => s,
4433 None => return (1, "zftp delete: not connected.\n".to_string()),
4434 };
4435
4436 match sess.delete(args[1]) {
4437 Ok(resp) => {
4438 if (resp.0 >= 200 && resp.0 < 300) {
4439 (0, String::new())
4440 } else {
4441 (1, resp.1)
4442 }
4443 }
4444 Err(e) => (1, format!("zftp delete: {}\n", e)),
4445 }
4446 }
4447
4448 "mkdir" => {
4449 if args.len() < 2 {
4450 return (1, "zftp mkdir: directory required\n".to_string());
4451 }
4452
4453 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4454 Some(s) => s,
4455 None => return (1, "zftp mkdir: not connected.\n".to_string()),
4456 };
4457
4458 match sess.mkdir(args[1]) {
4459 Ok(resp) => {
4460 if (resp.0 >= 200 && resp.0 < 300) {
4461 (0, String::new())
4462 } else {
4463 (1, resp.1)
4464 }
4465 }
4466 Err(e) => (1, format!("zftp mkdir: {}\n", e)),
4467 }
4468 }
4469
4470 "rmdir" => {
4471 if args.len() < 2 {
4472 return (1, "zftp rmdir: directory required\n".to_string());
4473 }
4474
4475 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4476 Some(s) => s,
4477 None => return (1, "zftp rmdir: not connected.\n".to_string()),
4478 };
4479
4480 match sess.rmdir(args[1]) {
4481 Ok(resp) => {
4482 if (resp.0 >= 200 && resp.0 < 300) {
4483 (0, String::new())
4484 } else {
4485 (1, resp.1)
4486 }
4487 }
4488 Err(e) => (1, format!("zftp rmdir: {}\n", e)),
4489 }
4490 }
4491
4492 "rename" => {
4493 if args.len() < 3 {
4494 return (1, "zftp rename: from and to required\n".to_string());
4495 }
4496
4497 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4498 Some(s) => s,
4499 None => return (1, "zftp rename: not connected.\n".to_string()),
4500 };
4501
4502 match sess.rename(args[1], args[2]) {
4503 Ok(resp) => {
4504 if (resp.0 >= 200 && resp.0 < 300) {
4505 (0, String::new())
4506 } else {
4507 (1, resp.1)
4508 }
4509 }
4510 Err(e) => (1, format!("zftp rename: {}\n", e)),
4511 }
4512 }
4513
4514 "type" | "ascii" | "binary" => {
4515 let transfer_type = match args[0] {
4516 "ascii" => ZFST_ASCI,
4517 "binary" => ZFST_IMAG,
4518 "type" => {
4519 if args.len() < 2 {
4520 let sess =
4521 match zftp.current.as_deref().and_then(|k| zftp.sessions.get(k)) {
4522 Some(s) => s,
4523 None => return (1, "zftp type: not connected.\n".to_string()),
4524 };
4525 return (
4526 0,
4527 format!(
4528 "{}\n",
4529 if sess.transfer_type == ZFST_ASCI {
4530 "ascii"
4531 } else {
4532 "binary"
4533 }
4534 ),
4535 );
4536 }
4537 match args[1].to_lowercase().as_str() {
4538 "a" | "ascii" => ZFST_ASCI,
4539 "i" | "binary" | "image" => ZFST_IMAG,
4540 _ => return (1, format!("zftp type: unknown type {}\n", args[1])),
4541 }
4542 }
4543 _ => unreachable!(),
4544 };
4545
4546 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4547 Some(s) => s,
4548 None => return (1, "zftp type: not connected.\n".to_string()),
4549 };
4550
4551 match sess.set_type(transfer_type) {
4552 Ok(resp) => {
4553 if (resp.0 >= 200 && resp.0 < 300) {
4554 (0, String::new())
4555 } else {
4556 (1, resp.1)
4557 }
4558 }
4559 Err(e) => (1, format!("zftp type: {}\n", e)),
4560 }
4561 }
4562
4563 "bslashquote" => {
4564 if args.len() < 2 {
4565 return (1, "zftp bslashquote: command required\n".to_string());
4566 }
4567
4568 let cmd = args[1..].join(" ");
4569
4570 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4571 Some(s) => s,
4572 None => return (1, "zftp bslashquote: not connected.\n".to_string()),
4573 };
4574
4575 match sess.bslashquote(&cmd) {
4576 Ok(resp) => (
4577 if (resp.0 >= 100 && resp.0 < 400) {
4578 0
4579 } else {
4580 1
4581 },
4582 resp.1,
4583 ),
4584 Err(e) => (1, format!("zftp bslashquote: {}\n", e)),
4585 }
4586 }
4587
4588 "close" | "quit" => {
4589 let sess = match zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k)) {
4590 Some(s) => s,
4591 None => return (0, String::new()),
4592 };
4593
4594 match sess.close() {
4595 Ok(_) => (0, String::new()),
4596 Err(e) => (1, format!("zftp close: {}\n", e)),
4597 }
4598 }
4599
4600 "session" => {
4601 // c:2891-2897 — no args: plain name list, one per line:
4602 //
4603 // for (nptr = firstnode(zfsessions); nptr; incnode(nptr))
4604 // printf("%s\n", ((Zftp_session)nptr->dat)->name);
4605 //
4606 // NO current-session marker, no indentation — a prior
4607 // arm decorated with "* "/" " prefixes C never prints.
4608 if args.len() < 2 {
4609 let mut out = String::new();
4610 for name in zftp.sessions.keys() {
4611 // c:2894 firstnode walk
4612 out.push_str(&format!("{}\n", name)); // c:2895
4613 }
4614 return (0, out);
4615 }
4616 // c:2903-2904 — same-session no-op; c:2906-2907 —
4617 // savesession + switchsession. switchsession = newsession
4618 // (create-if-missing + set zfsess, c:2806-2823) inlined
4619 // here because bin_zftp already holds the state lock.
4620 let name = args[1];
4621 zftp.sessions
4622 .entry(name.to_string())
4623 .or_insert_with(|| zftp_session::new(name)); // c:2814-2818
4624 zftp.current = Some(name.to_string()); // c:2806-2812 zfsess
4625 (0, String::new())
4626 }
4627
4628 "rmsession" => {
4629 // c:2922-2930 — target is the named session, or the
4630 // CURRENT one when no name is given:
4631 //
4632 // for (no = 0, nptr = firstnode(zfsessions); nptr; ...) {
4633 // sptr = (Zftp_session) nptr->dat;
4634 // if ((!*args && sptr == zfsess) ||
4635 // (*args && !strcmp(sptr->name, *args)))
4636 // break;
4637 // }
4638 // if (!nptr)
4639 // return 1;
4640 //
4641 // Not-found exits 1 SILENTLY (no diagnostic). A prior
4642 // arm required a name and printed invented messages
4643 // for both the no-args and not-found cases.
4644 let target: String = match args.get(1) {
4645 Some(n) => (*n).to_string(),
4646 None => match zftp.current.as_deref() {
4647 Some(c) => c.to_string(), // c:2925 sptr == zfsess
4648 None => return (1, String::new()), // c:2929-2930
4649 },
4650 };
4651 // c:2958 — remnode(zfsessions, nptr); shift_remove
4652 // keeps the remaining entries in list order like C's
4653 // remnode.
4654 if zftp.sessions.shift_remove(&target).is_none() {
4655 return (1, String::new()); // c:2929-2930 silent
4656 }
4657 if zftp.sessions.is_empty() {
4658 // c:2985-2992 — we've just deleted the last
4659 // session, so we need to start again from
4660 // scratch: newsession("default").
4661 zftp.sessions
4662 .insert("default".to_string(), zftp_session::new("default"));
4663 zftp.current = Some("default".to_string());
4664 } else if zftp.current.as_deref() == Some(target.as_str()) {
4665 // c:2941-2946 + c:2983 — freed the current
4666 // session: switch to the first in the list
4667 // excluding the one just freed.
4668 zftp.current = zftp.sessions.keys().next().cloned();
4669 }
4670 (0, String::new())
4671 }
4672
4673 "test" => {
4674 // c:158 zftpcmdtab — dispatches to zftp_test, which
4675 // does a real poll(2) liveness check at c:2271-2293
4676 // (consumes any unsolicited "421 Timeout" via zfgetmsg
4677 // then returns `zfsess->control ? 0 : 2`). The Rust
4678 // dispatch here can't call zftp_test directly because
4679 // bin_zftp holds the global zftp_state lock and
4680 // zftp_test would re-acquire it — deadlock.
4681 //
4682 // Inline the c:2271-2293 logic: read the control fd
4683 // off the held session, poll(2) it, and on POLLIN
4684 // call into a helper that doesn't re-lock. Without
4685 // the poll, server-side timeouts that closed the
4686 // control fd would report "connected" until the next
4687 // subcommand tried to use the dead fd.
4688 let control_fd: i32 = zftp
4689 .current
4690 .as_deref()
4691 .and_then(|k| zftp.sessions.get(k))
4692 .and_then(|s| s.control.as_ref().map(|c| c.as_raw_fd()))
4693 .unwrap_or(-1);
4694 if control_fd < 0 {
4695 (1, String::new()) // c:2263 — no control fd
4696 } else {
4697 // c:2271-2280 — poll(0). POLLIN means unsolicited
4698 // server message (almost always 421 Timeout).
4699 let mut pfd = libc::pollfd {
4700 fd: control_fd,
4701 events: libc::POLLIN,
4702 revents: 0,
4703 };
4704 let ret = unsafe { libc::poll(&mut pfd, 1, 0) };
4705 let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
4706 if ret < 0 && errno != libc::EINTR && errno != libc::EAGAIN {
4707 // c:2273 — bad fd or hard error: connection dead.
4708 if let Some(sess) =
4709 zftp.current.clone().and_then(|k| zftp.sessions.get_mut(&k))
4710 {
4711 sess.control = None;
4712 sess.connected = false;
4713 sess.dfd = -1;
4714 }
4715 (2, String::new()) // c:2305 — control gone
4716 } else {
4717 // c:2305 — return 0 if control still alive.
4718 let alive = zftp
4719 .current
4720 .as_deref()
4721 .and_then(|k| zftp.sessions.get(k))
4722 .map(|s| s.control.is_some())
4723 .unwrap_or(false);
4724 if alive {
4725 (0, String::new())
4726 } else {
4727 (2, String::new())
4728 }
4729 }
4730 }
4731 }
4732
4733 // c:3013-3015 — `if (!zptr->nam) { zwarnnam(name,
4734 // "no such subcommand: %s", cnam); return 1; }`. C uses
4735 // "no such subcommand:" (with colon); prior Rust port used
4736 // "unknown subcommand" (no colon). Match the exact C
4737 // diagnostic so test harnesses pattern-matching the
4738 // documented error string see the same bytes.
4739 _ => (1, format!("zftp: no such subcommand: {}\n", args[0])),
4740 }
4741 })();
4742 drop(zftp_guard);
4743
4744 // c:3109-3115 — post-dispatch tidy-up. Sub-commands that opened a
4745 // SIGALRM via zfalarm() expect bin_zftp to disarm it after dispatch
4746 // (the alarm survives the subcommand returning normally). If
4747 // ZFDRRRRING was set (timeout fired mid-syscall, signal flag bumped
4748 // by zfhandler) we tear down the connection, suppressing QUIT via
4749 // zcfinish=2 so zfclose doesn't sit on a doomed socket waiting for
4750 // a goodbye reply. zfclose acquires the state lock so it MUST come
4751 // after `drop(zftp_guard)` above.
4752 if ZFALARMED.load(Ordering::Relaxed) != 0 {
4753 // c:3109
4754 zfunalarm(); // c:3110
4755 }
4756 if ZFDRRRRING.load(Ordering::Relaxed) != 0 {
4757 // c:3111
4758 ZCFINISH.store(2, Ordering::Relaxed); // c:3113 — skip QUIT
4759 zfclose(0); // c:3114
4760 }
4761 // c:3116-3123 — zfstatfd shared-status fd-write for sibling subshells.
4762 // The shared-status fd substrate isn't ported; the per-session
4763 // status fields on zftp_session already survive across subcommands
4764 // since they live on the in-process zftp_state singleton.
4765
4766 if !output.is_empty() {
4767 if status == 0 {
4768 print!("{}", output);
4769 } else {
4770 eprint!("{}", output);
4771 }
4772 }
4773 status
4774}
4775
4776/// Port of `zftp_cleanup()` from `Src/Modules/zftp.c:3128`. Walks
4777/// every session sending QUIT (via zfclose) on the current one and
4778/// closing the control fd on others, then clears the session table.
4779pub fn zftp_cleanup() -> i32 {
4780 // c:3128
4781 // c:3128 — Zftp_session cursess = zfsess; (snapshot current).
4782 let cursess = zftp_state().lock().ok().and_then(|s| s.current.clone());
4783 let session_names: Vec<String> = zftp_state()
4784 .lock()
4785 .ok()
4786 .map(|s| s.sessions.keys().map(|n| n.to_string()).collect())
4787 .unwrap_or_default();
4788 // c:3135-3142 — walk every session: zfclosedata + zfclose(leaveparams).
4789 for nm in session_names {
4790 // c:3136
4791 // c:3137 — zfsess = (Zftp_session)nptr->dat; (switch to session).
4792 if let Ok(mut s) = zftp_state().lock() {
4793 s.current = Some(nm.clone()); // c:3137 zfsess = nptr->dat
4794 }
4795 zfclosedata(); // c:3140
4796 // c:3144 — zfclose(zfsess != cursess): non-current sessions keep
4797 // their params; current session goes through full unset.
4798 let leaveparams = if Some(&nm) != cursess.as_ref() { 1 } else { 0 };
4799 zfclose(leaveparams); // c:3144
4800 }
4801 // c:3146-3151 — clear lastmsg, ZFTP_SESSION, freelinklist sessions.
4802 if let Ok(mut m) = lastmsg.lock() {
4803 m.clear(); // c:3147
4804 }
4805 zfunsetparam("ZFTP_SESSION"); // c:3149
4806 if let Ok(mut state) = zftp_state().lock() {
4807 *state = zftp_globals::default(); // c:3150 freelinklist
4808 }
4809 // c:3152 — zfree(zfstatusp): per-session status array. zfstatusp
4810 // substrate isn't ported (per-session bits live on zftp_session
4811 // directly), so nothing to free.
4812 0
4813}
4814
4815impl zftp_session {
4816 /// Port of `newsession(char *nm)` from `Src/Modules/zftp.c`. C uses
4817 /// `zshcalloc(sizeof(struct zftp_session))` then sets `name`
4818 /// (c:2891 `zfsess->name = ztrdup(name);`) and pre-allocates the
4819 /// `params` / `userparams` arrays. Same default state.
4820 pub fn new(name: &str) -> Self {
4821 Self {
4822 // C-faithful fields from `struct zftp_session` (c:299):
4823 name: name.to_string(), // c:300
4824 params: Vec::new(), // c:301
4825 userparams: Vec::new(), // c:302
4826 cin: None, // c:303 NULL
4827 control: None, // c:304 NULL
4828 dfd: -1, // c:305 (closed)
4829 has_size: 0, // c:306
4830 has_mdtm: 0, // c:307
4831 // Ergonomic Rust-side state mirroring C's params[] indices:
4832 host: None,
4833 port: 21,
4834 user: None,
4835 pwd: None,
4836 connected: false,
4837 logged_in: false,
4838 transfer_type: ZFST_IMAG,
4839 // c:2226 — initial current_type is ZFST_CASC (0); the SYST probe
4840 // in zftp_login sends the first TYPE I when it detects UNIX L8.
4841 current_type: ZFST_CASC,
4842 transfer_mode: ZFST_STRE,
4843 passive: true,
4844 syst_probed: false,
4845 nops_probed: false,
4846 trsz: false, // ZFST_TRSZ fresh-session default
4847 nosz: false, // ZFST_NOSZ fresh-session default
4848 }
4849 }
4850
4851 /// Port of `zftp_open(char *name, char **args, int flags)` from `Src/Modules/zftp.c:1690`.
4852 fn send_command(&mut self, cmd: &str) -> io::Result<()> {
4853 if let Some(ref mut stream) = self.cin {
4854 write!(stream, "{}\r\n", cmd)?;
4855 stream.flush()
4856 } else {
4857 Err(io::Error::new(io::ErrorKind::NotConnected, "not connected"))
4858 }
4859 }
4860
4861 /// Port of `zfgetmsg()` from `Src/Modules/zftp.c:702`.
4862 fn read_response(&mut self) -> io::Result<FtpResponse> {
4863 let stream = self
4864 .cin
4865 .as_mut()
4866 .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, "not connected"))?;
4867
4868 let mut reader = BufReader::new(stream.try_clone()?);
4869 let mut full_message = String::new();
4870 let mut code = 0u32;
4871 let mut multiline = false;
4872 let mut first_code = String::new();
4873
4874 loop {
4875 let mut line = String::new();
4876 reader.read_line(&mut line)?;
4877 let line = line.trim_end();
4878
4879 if line.len() < 3 {
4880 continue;
4881 }
4882
4883 if code == 0 {
4884 first_code = line[..3].to_string();
4885 code = first_code.parse().unwrap_or(0);
4886
4887 if line.len() > 3 && line.chars().nth(3) == Some('-') {
4888 multiline = true;
4889 }
4890 }
4891
4892 full_message.push_str(line);
4893 full_message.push('\n');
4894
4895 if multiline {
4896 if line.starts_with(&first_code)
4897 && line.len() > 3
4898 && line.chars().nth(3) == Some(' ')
4899 {
4900 break;
4901 }
4902 } else {
4903 break;
4904 }
4905 }
4906
4907 Ok((code as i32, full_message))
4908 }
4909
4910 /// Port of `zftp_open(char *name, char **args, int flags)` from `Src/Modules/zftp.c:1690`.
4911 /// Connect to FTP server — DNS resolution on background thread to avoid hangs
4912 pub fn connect(&mut self, host: &str, port: Option<u16>) -> io::Result<FtpResponse> {
4913 let port = port.unwrap_or(21);
4914 let addr_str = format!("{}:{}", host, port);
4915 let dns_timeout = Duration::from_secs(10);
4916
4917 // DNS on background thread
4918 let (tx, rx) = std::sync::mpsc::channel();
4919 let dns = addr_str.clone();
4920 std::thread::Builder::new()
4921 .name("zftp-dns".to_string())
4922 .spawn(move || {
4923 let _ = tx.send(dns.to_socket_addrs().map(|a| a.collect::<Vec<_>>()));
4924 })
4925 .map_err(io::Error::other)?;
4926
4927 let addrs = rx
4928 .recv_timeout(dns_timeout)
4929 .map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "DNS resolution timed out"))?
4930 .map_err(|e| {
4931 // C: `zwarnnam("zftp", "host not found: %s", host);` at
4932 // `Src/Modules/zftp.c:1715`. Route through canonical
4933 // zwarnnam so the user sees a real shell warning
4934 // instead of a tracing log line.
4935 zwarnnam("zftp", &format!("host not found: {}: {}", host, e));
4936 e
4937 })?;
4938
4939 let sock_addr = addrs
4940 .into_iter()
4941 .next()
4942 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid address"))?;
4943
4944 let stream = TcpStream::connect_timeout(&sock_addr, Duration::from_secs(30))?;
4945
4946 stream.set_read_timeout(Some(Duration::from_secs(60)))?;
4947 stream.set_write_timeout(Some(Duration::from_secs(60)))?;
4948
4949 self.cin = Some(stream);
4950 self.host = Some(host.to_string());
4951 self.port = port;
4952 self.connected = true;
4953
4954 self.read_response()
4955 }
4956
4957 /// Port of `zftp_login(char *name, char **args, UNUSED(int flags))` from `Src/Modules/zftp.c:2118`.
4958 /// Login to FTP server
4959 pub fn login(&mut self, user: &str, pass: Option<&str>) -> io::Result<FtpResponse> {
4960 self.send_command(&format!("USER {}", user))?;
4961 let resp = self.read_response()?;
4962
4963 if resp.0 == 331 {
4964 let password = pass.unwrap_or("");
4965 self.send_command(&format!("PASS {}", password))?;
4966 let resp = self.read_response()?;
4967
4968 if (resp.0 >= 200 && resp.0 < 300) {
4969 self.logged_in = true;
4970 self.user = Some(user.to_string());
4971 }
4972 return Ok(resp);
4973 }
4974
4975 if (resp.0 >= 200 && resp.0 < 300) {
4976 self.logged_in = true;
4977 self.user = Some(user.to_string());
4978 }
4979
4980 Ok(resp)
4981 }
4982
4983 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
4984 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
4985 /// Set transfer type
4986 pub fn set_type(&mut self, transfer_type: i32) -> io::Result<FtpResponse> {
4987 // C inline pattern: `(typ & ZFST_IMAG) ? "I" : "A"`
4988 let typ_letter = if (transfer_type & ZFST_IMAG) != 0 {
4989 "I"
4990 } else {
4991 "A"
4992 };
4993 self.send_command(&format!("TYPE {}", typ_letter))?;
4994 let resp = self.read_response()?;
4995 if (resp.0 >= 200 && resp.0 < 300) {
4996 self.transfer_type = transfer_type;
4997 }
4998 Ok(resp)
4999 }
5000
5001 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5002 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5003 /// Change directory
5004 pub fn cd(&mut self, path: &str) -> io::Result<FtpResponse> {
5005 self.send_command(&format!("CWD {}", path))?;
5006 self.read_response()
5007 }
5008
5009 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5010 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5011 /// Change to parent directory
5012 pub fn cdup(&mut self) -> io::Result<FtpResponse> {
5013 self.send_command("CDUP")?;
5014 self.read_response()
5015 }
5016
5017 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5018 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5019 /// Get current directory
5020 pub fn pwd(&mut self) -> io::Result<(FtpResponse, Option<String>)> {
5021 self.send_command("PWD")?;
5022 let resp = self.read_response()?;
5023
5024 let pwd = if (resp.0 >= 200 && resp.0 < 300) {
5025 if let Some(start) = resp.1.find('"') {
5026 if let Some(end) = resp.1[start + 1..].find('"') {
5027 Some(resp.1[start + 1..start + 1 + end].to_string())
5028 } else {
5029 None
5030 }
5031 } else {
5032 None
5033 }
5034 } else {
5035 None
5036 };
5037
5038 Ok((resp, pwd))
5039 }
5040
5041 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5042 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5043 /// List directory
5044 pub fn list(&mut self, path: Option<&str>) -> io::Result<(FtpResponse, Vec<String>)> {
5045 let data_stream = self.enter_passive_mode()?;
5046
5047 let cmd = match path {
5048 Some(p) => format!("LIST {}", p),
5049 None => "LIST".to_string(),
5050 };
5051 self.send_command(&cmd)?;
5052 let resp = self.read_response()?;
5053
5054 if !(resp.0 >= 100 && resp.0 < 400) {
5055 return Ok((resp, Vec::new()));
5056 }
5057
5058 let mut reader = BufReader::new(data_stream);
5059 let mut lines = Vec::new();
5060 let mut line = String::new();
5061 while reader.read_line(&mut line)? > 0 {
5062 lines.push(line.trim_end().to_string());
5063 line.clear();
5064 }
5065
5066 let final_resp = self.read_response()?;
5067
5068 Ok((final_resp, lines))
5069 }
5070
5071 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5072 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5073 /// List filenames only
5074 pub fn nlst(&mut self, path: Option<&str>) -> io::Result<(FtpResponse, Vec<String>)> {
5075 let data_stream = self.enter_passive_mode()?;
5076
5077 let cmd = match path {
5078 Some(p) => format!("NLST {}", p),
5079 None => "NLST".to_string(),
5080 };
5081 self.send_command(&cmd)?;
5082 let resp = self.read_response()?;
5083
5084 if !(resp.0 >= 100 && resp.0 < 400) {
5085 return Ok((resp, Vec::new()));
5086 }
5087
5088 let mut reader = BufReader::new(data_stream);
5089 let mut lines = Vec::new();
5090 let mut line = String::new();
5091 while reader.read_line(&mut line)? > 0 {
5092 lines.push(line.trim_end().to_string());
5093 line.clear();
5094 }
5095
5096 let final_resp = self.read_response()?;
5097
5098 Ok((final_resp, lines))
5099 }
5100
5101 /// Port of `zftp_open(char *name, char **args, int flags)` from `Src/Modules/zftp.c:1690`.
5102 fn enter_passive_mode(&mut self) -> io::Result<TcpStream> {
5103 self.send_command("PASV")?;
5104 let resp = self.read_response()?;
5105
5106 if !(resp.0 >= 200 && resp.0 < 300) {
5107 return Err(io::Error::other(resp.1));
5108 }
5109
5110 // c:934-936 — "lastmsg already has the reply code expunged":
5111 // the C parse operates on lastmsg, which zfgetmsg stored with
5112 // the "NNN " / "NNN-" prefix stripped (c:781). read_response
5113 // returns the raw line, so strip the 4-byte code prefix here;
5114 // passing it through would parse "227" as the first IP octet.
5115 let (ip, port) = parse_pasv_response(resp.1.get(4..).unwrap_or(""))?;
5116 let addr = format!("{}:{}", ip, port);
5117
5118 TcpStream::connect_timeout(
5119 &addr.to_socket_addrs()?.next().ok_or_else(|| {
5120 io::Error::new(io::ErrorKind::InvalidInput, "invalid PASV address")
5121 })?,
5122 Duration::from_secs(30),
5123 )
5124 }
5125
5126 /// Port of `zfstats(char *fnam, int remote, off_t *retsize, char **retmdtm, int fd)` from `Src/Modules/zftp.c:1193`.
5127 /// Download a file
5128 pub fn get(&mut self, remote: &str, local: &Path) -> io::Result<FtpResponse> {
5129 let mut data_stream = self.enter_passive_mode()?;
5130
5131 self.send_command(&format!("RETR {}", remote))?;
5132 let resp = self.read_response()?;
5133
5134 if !(resp.0 >= 100 && resp.0 < 400) {
5135 return Ok(resp);
5136 }
5137
5138 let mut file = std::fs::File::create(local)?;
5139 let mut buf = [0u8; 8192];
5140 loop {
5141 let n = data_stream.read(&mut buf)?;
5142 if n == 0 {
5143 break;
5144 }
5145 file.write_all(&buf[..n])?;
5146 }
5147
5148 self.read_response()
5149 }
5150
5151 /// Port of `zfstats(char *fnam, int remote, off_t *retsize, char **retmdtm, int fd)` from `Src/Modules/zftp.c:1193`.
5152 /// Upload a file
5153 pub fn put(&mut self, local: &Path, remote: &str) -> io::Result<FtpResponse> {
5154 let mut data_stream = self.enter_passive_mode()?;
5155
5156 self.send_command(&format!("STOR {}", remote))?;
5157 let resp = self.read_response()?;
5158
5159 if !(resp.0 >= 100 && resp.0 < 400) {
5160 return Ok(resp);
5161 }
5162
5163 let mut file = std::fs::File::open(local)?;
5164 let mut buf = [0u8; 8192];
5165 loop {
5166 let n = file.read(&mut buf)?;
5167 if n == 0 {
5168 break;
5169 }
5170 data_stream.write_all(&buf[..n])?;
5171 }
5172 drop(data_stream);
5173
5174 self.read_response()
5175 }
5176
5177 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5178 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5179 /// Delete a file
5180 pub fn delete(&mut self, path: &str) -> io::Result<FtpResponse> {
5181 self.send_command(&format!("DELE {}", path))?;
5182 self.read_response()
5183 }
5184
5185 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5186 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5187 /// Make directory
5188 pub fn mkdir(&mut self, path: &str) -> io::Result<FtpResponse> {
5189 self.send_command(&format!("MKD {}", path))?;
5190 self.read_response()
5191 }
5192
5193 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5194 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5195 /// Remove directory
5196 pub fn rmdir(&mut self, path: &str) -> io::Result<FtpResponse> {
5197 self.send_command(&format!("RMD {}", path))?;
5198 self.read_response()
5199 }
5200
5201 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5202 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5203 /// Rename file
5204 pub fn rename(&mut self, from: &str, to: &str) -> io::Result<FtpResponse> {
5205 self.send_command(&format!("RNFR {}", from))?;
5206 let resp = self.read_response()?;
5207
5208 if !(resp.0 >= 300 && resp.0 < 400) {
5209 return Ok(resp);
5210 }
5211
5212 self.send_command(&format!("RNTO {}", to))?;
5213 self.read_response()
5214 }
5215
5216 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5217 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5218 /// Get file size
5219 pub fn size(&mut self, path: &str) -> io::Result<(FtpResponse, Option<u64>)> {
5220 self.send_command(&format!("SIZE {}", path))?;
5221 let resp = self.read_response()?;
5222
5223 let size = if (resp.0 >= 200 && resp.0 < 300) {
5224 resp.1
5225 .split_whitespace()
5226 .last()
5227 .and_then(|s| s.parse().ok())
5228 } else {
5229 None
5230 };
5231
5232 Ok((resp, size))
5233 }
5234
5235 /// WARNING: NOT IN ZFTP.C — method on Rust-only `zftp_session` wrapper.
5236 /// C inlines this pattern at every callsite; Rust factors it onto the wrapper.
5237 /// Send raw command
5238 pub fn bslashquote(&mut self, cmd: &str) -> io::Result<FtpResponse> {
5239 self.send_command(cmd)?;
5240 self.read_response()
5241 }
5242
5243 /// Port of `zftp_open(char *name, char **args, int flags)` from `Src/Modules/zftp.c:1690`.
5244 /// Close connection
5245 pub fn close(&mut self) -> io::Result<FtpResponse> {
5246 if !self.connected {
5247 return Ok((0, "not connected".to_string()));
5248 }
5249
5250 let resp = if let Ok(()) = self.send_command("QUIT") {
5251 self.read_response()
5252 .unwrap_or_else(|_| (221, "Goodbye".to_string()))
5253 } else {
5254 (221, "Goodbye".to_string())
5255 };
5256
5257 self.cin = None;
5258 self.connected = false;
5259 self.logged_in = false;
5260 self.host = None;
5261 self.user = None;
5262 self.pwd = None;
5263
5264 Ok(resp)
5265 }
5266}
5267
5268/// Port of `zftpexithook(UNUSED(Hookdef d), UNUSED(void *dummy))` from Src/Modules/zftp.c:3156.
5269/// C: `static int zftpexithook(UNUSED(Hookdef d), UNUSED(void *dummy))`
5270/// — calls `zftp_cleanup()`, returns 0.
5271#[allow(non_snake_case)]
5272#[allow(unused_variables)]
5273pub fn zftpexithook(d: *mut crate::ported::zsh_h::hookdef, dummy: *mut std::ffi::c_void) -> i32 {
5274 let _ = (d, dummy);
5275 zftp_cleanup(); // c:3156
5276 0 // c:3159
5277}
5278
5279// `bintab` — port of `static struct builtin bintab[]` (zftp.c).
5280
5281// `module_features` — port of `static struct features module_features`
5282// from zftp.c:3163.
5283
5284/// Port of `setup_(UNUSED(Module m))` from `Src/Modules/zftp.c:3174`.
5285#[allow(unused_variables)]
5286pub fn setup_(m: *const module) -> i32 {
5287 // c:3174
5288 // C body c:3176-3177 — `return 0`. Faithful empty-body port.
5289 0
5290}
5291
5292/// Port of `features_(UNUSED(Module m), UNUSED(char ***features))` from `Src/Modules/zftp.c:3181`.
5293pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
5294 *features = featuresarray(m, module_features());
5295 0
5296}
5297
5298/// Port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from `Src/Modules/zftp.c:3189`.
5299pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
5300 handlefeatures(m, module_features(), enables)
5301}
5302
5303// ===========================================================
5304// Methods moved verbatim from src/ported/vm_helper because their
5305// C counterpart's source file maps 1:1 to this Rust module.
5306// Phase: module-shims
5307// ===========================================================
5308
5309// BEGIN moved-from-exec-rs
5310// (impl ShellExecutor block moved to src/exec_shims.rs — see file marker)
5311
5312// END moved-from-exec-rs
5313
5314// =====================================================================
5315// static struct features module_features c:3163 (zftp.c)
5316// =====================================================================
5317
5318/// Port of `boot_(UNUSED(Module m))` from `Src/Modules/zftp.c:3196`.
5319#[allow(unused_variables)]
5320pub fn boot_(m: *const module) -> i32 {
5321 // c:3196
5322 // C body c:3198-3214:
5323 // off_t tmout_def = 60;
5324 // zfsetparam("ZFTP_VERBOSE", "450", ZFPM_IFUNSET);
5325 // zfsetparam("ZFTP_TMOUT", &tmout_def, ZFPM_IFUNSET|ZFPM_INTEGER);
5326 // zfsetparam("ZFTP_PREFS", "PS", ZFPM_IFUNSET);
5327 // zfprefs = ZFPF_SNDP|ZFPF_PASV;
5328 // zfsessions = znewlinklist(); newsession("default");
5329 // addhookfunc("exit", zftpexithook);
5330 zfsetparam("ZFTP_VERBOSE", "450", ZFPM_IFUNSET); // c:3203
5331 zfsetparam("ZFTP_TMOUT", "60", ZFPM_IFUNSET | ZFPM_INTEGER); // c:3219
5332 zfsetparam("ZFTP_PREFS", "PS", ZFPM_IFUNSET); // c:3219
5333 zfprefs.store(
5334 ZFPF_SNDP | ZFPF_PASV, // c:3219
5335 Ordering::Relaxed,
5336 );
5337 newsession("default"); // c:3219
5338 // c:3219 — `addhookfunc("exit", zftpexithook)` — register the
5339 // process-exit cleanup so all live ftp sessions get torn down
5340 // before the shell exits.
5341 crate::ported::module::addhookfunc("exit", zftpexithook as crate::ported::zsh_h::Hookfn);
5342 0
5343}
5344
5345/// Port of `cleanup_(UNUSED(Module m))` from `Src/Modules/zftp.c:3219`.
5346pub fn cleanup_(m: *const module) -> i32 {
5347 // c:3219
5348 // c:3228 — `deletehookfunc("exit", zftpexithook)` — drop the
5349 // exit-hook registration before the module unloads.
5350 crate::ported::module::deletehookfunc("exit", zftpexithook as crate::ported::zsh_h::Hookfn);
5351 // c:3228 — `zftp_cleanup()`: close every live session.
5352 zftp_cleanup(); // c:3228
5353 setfeatureenables(m, module_features(), None) // c:3228
5354}
5355
5356/// Port of `finish_(UNUSED(Module m))` from `Src/Modules/zftp.c:3228`.
5357#[allow(unused_variables)]
5358pub fn finish_(m: *const module) -> i32 {
5359 // c:3228
5360 // C body c:3230-3231 — `return 0`. Faithful empty-body port; the
5361 // cleanup of zfsessions happens in cleanup_.
5362 0
5363}
5364
5365// `TransferType` enum removed — was Rust-only invention. C uses the
5366// `ZFST_ASCI` (0x0000) / `ZFST_IMAG` (0x0001) bits from the ZFST_*
5367// status word (c:246-247) for the next-transfer type, and
5368// `ZFST_CASC` (0x0000) / `ZFST_CIMA` (0x0002) for the current-transfer
5369// type. Callers store the type as `i32` and compare via the `ZFST_TYPE`
5370// macro (c:267): `ZFST_TYPE(x) (x & ZFST_TMSK)`.
5371//
5372// Inline-test pattern matching C `if (zfst_status & ZFST_IMAG) ...`
5373// at every TYPE-letter dispatch (e.g. zftp.c around `zftp_type` body):
5374// `if (typ & ZFST_IMAG) != 0 { "I" } else { "A" }`
5375
5376// `TransferMode` enum removed — was Rust-only invention. C uses the
5377// `ZFST_STRE` (0x0000) / `ZFST_BLOC` (0x0004) bits from the ZFST_*
5378// status word (defined later in this file at the c:245 enum). Callers
5379// store the mode as `i32` and compare against those constants directly:
5380// `if (mode & ZFST_BLOC) != 0 { "B" } else { "S" }`
5381// — same inline-test pattern the C source uses at every MODE send-site.
5382
5383/// FTP server response (3-digit code + message).
5384/// Port of the response handling inside `zfgetmsg()` from
5385/// `lastmsg` — file-scope global from `Src/Modules/zftp.c:227`:
5386/// `static char *lastmsg, lastcodestr[4];`. Holds the most recent
5387/// FTP server reply message text (post-3-digit-code body).
5388pub static lastmsg: Mutex<String> = Mutex::new(String::new());
5389
5390/// `lastcodestr` — file-scope global from `Src/Modules/zftp.c:227`.
5391/// 3-digit FTP reply code as ASCII (`"000".."599"`), zero-terminated
5392/// to 4 bytes in C; mirrored as a 4-byte Mutex array for parity.
5393pub static lastcodestr: Mutex<[u8; 4]> = Mutex::new([b'0', b'0', b'0', 0]);
5394
5395// `FtpResponse` struct removed — was Rust-only invention. C source
5396// returns plain `int` from every reply-handling fn and reads the
5397// `lastcode` / `lastmsg` globals (c:227-228) inline at each check
5398// site. Callers in this Rust port use the same pattern: each fn
5399// returns `i32` (matching C `int`), and `lastmsg.lock().unwrap()`
5400// + `lastcode.load(Relaxed)` provide the C-equivalent inline reads.
5401//
5402// For ergonomic call sites the type alias below carries both halves
5403// without inventing a new struct shape:
5404/// `FtpResponse` type alias.
5405#[allow(non_camel_case_types)]
5406pub type FtpResponse = (i32, String);
5407
5408// =====================================================================
5409// `enum { ZFHD_* }` from `Src/Modules/zftp.c:119` — block-header flags.
5410// =====================================================================
5411
5412/// `ZFHD_MARK` — restart marker.
5413pub const ZFHD_MARK: i32 = 16; // c:120
5414/// `ZFHD_ERRS` — suspected errors in block.
5415pub const ZFHD_ERRS: i32 = 32; // c:121
5416/// `ZFHD_EOFB` — block is end of record.
5417pub const ZFHD_EOFB: i32 = 64; // c:122
5418/// `ZFHD_EORB` — block is end of file.
5419pub const ZFHD_EORB: i32 = 128; // c:123
5420
5421/// `readwrite_t` — function pointer typedef from
5422/// `Src/Modules/zftp.c:126`: `typedef int (*readwrite_t)(int, char *, off_t, int);`
5423#[allow(non_camel_case_types)]
5424pub type readwrite_t = fn(i32, &mut [u8], libc::off_t, i32) -> i32;
5425
5426// =====================================================================
5427// `enum { ZFTP_* }` from `Src/Modules/zftp.c:134` — zftpcmd.flags bits.
5428// =====================================================================
5429
5430/// `ZFTP_CONN` — must be connected.
5431pub const ZFTP_CONN: i32 = 0x0001; // c:135
5432/// `ZFTP_LOGI` — must be logged in.
5433pub const ZFTP_LOGI: i32 = 0x0002; // c:136
5434/// `ZFTP_TBIN` — set transfer type image.
5435pub const ZFTP_TBIN: i32 = 0x0004; // c:137
5436/// `ZFTP_TASC` — set transfer type ASCII.
5437pub const ZFTP_TASC: i32 = 0x0008; // c:138
5438/// `ZFTP_NLST` — use NLST rather than LIST.
5439pub const ZFTP_NLST: i32 = 0x0010; // c:139
5440/// `ZFTP_DELE` — a delete rather than a make.
5441pub const ZFTP_DELE: i32 = 0x0020; // c:140
5442/// `ZFTP_SITE` — a site rather than a quote.
5443pub const ZFTP_SITE: i32 = 0x0040; // c:141
5444/// `ZFTP_APPE` — append rather than overwrite.
5445pub const ZFTP_APPE: i32 = 0x0080; // c:142
5446/// `ZFTP_HERE` — here rather than over there.
5447pub const ZFTP_HERE: i32 = 0x0100; // c:143
5448/// `ZFTP_CDUP` — CDUP rather than CWD.
5449pub const ZFTP_CDUP: i32 = 0x0200; // c:144
5450/// `ZFTP_REST` — restart: set point in remote file.
5451pub const ZFTP_REST: i32 = 0x0400; // c:145
5452/// `ZFTP_RECV` — receive rather than send.
5453pub const ZFTP_RECV: i32 = 0x0800; // c:146
5454/// `ZFTP_TEST` — test command, don't test.
5455pub const ZFTP_TEST: i32 = 0x1000; // c:147
5456/// `ZFTP_SESS` — session command, don't need status.
5457pub const ZFTP_SESS: i32 = 0x2000; // c:148
5458
5459/// `static char *zfparams[]` from `Src/Modules/zftp.c:197` — list of
5460/// non-special params to unset when a connection closes.
5461pub static ZFPARAMS: &[&str] = &[
5462 "ZFTP_HOST",
5463 "ZFTP_PORT",
5464 "ZFTP_IP",
5465 "ZFTP_SYSTEM",
5466 "ZFTP_USER",
5467 "ZFTP_ACCOUNT",
5468 "ZFTP_PWD",
5469 "ZFTP_TYPE",
5470 "ZFTP_MODE", // c:198-199
5471];
5472
5473// =====================================================================
5474// `enum { ZFPM_* }` from `Src/Modules/zftp.c:204` — zfsetparam flags.
5475// =====================================================================
5476
5477/// `ZFPM_READONLY` — make parameter readonly.
5478pub const ZFPM_READONLY: i32 = 0x01; // c:205
5479/// `ZFPM_IFUNSET` — only set if not already set.
5480pub const ZFPM_IFUNSET: i32 = 0x02; // c:206
5481/// `ZFPM_INTEGER` — passed pointer to off_t.
5482pub const ZFPM_INTEGER: i32 = 0x04; // c:207
5483
5484/// `zfnopen` — file-scope global from `Src/Modules/zftp.c:211`:
5485/// `static int zfnopen;` — number of connections actually open.
5486pub static ZFNOPEN: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
5487
5488/// `zcfinish` — file-scope global from `Src/Modules/zftp.c:218`:
5489/// `static int zcfinish;` — 0 keep going, 1 line finished, 2 EOF.
5490pub static ZCFINISH: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
5491
5492/// `zfclosing` — file-scope global from `Src/Modules/zftp.c:220`:
5493/// `static int zfclosing;` — set when zftp_close() is active.
5494pub static ZFCLOSING: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
5495
5496// =====================================================================
5497// `enum { ZFCP_* }` from `Src/Modules/zftp.c` — server-capability
5498// tri-state for SIZE / MDTM probes.
5499// =====================================================================
5500
5501/// `ZFCP_UNKN` — dunno if it works on this server. Port of c:`enum`
5502/// in `Src/Modules/zftp.c`.
5503pub const ZFCP_UNKN: i32 = 0;
5504/// `ZFCP_YUPP` — server supports the feature.
5505pub const ZFCP_YUPP: i32 = 1;
5506/// `ZFCP_NOPE` — server doesn't support the feature.
5507pub const ZFCP_NOPE: i32 = 2;
5508
5509/// `ZFST_ASCI` — type for next transfer is ASCII.
5510pub const ZFST_ASCI: i32 = 0x0000;
5511/// `ZFST_IMAG` — type for next transfer is image (binary).
5512pub const ZFST_IMAG: i32 = 0x0001;
5513/// `ZFST_TMSK` — mask for type flags.
5514pub const ZFST_TMSK: i32 = 0x0001;
5515/// `ZFST_TBIT` — number of bits in type flags.
5516pub const ZFST_TBIT: i32 = 0x0001;
5517/// `ZFST_CASC` — current type is ASCII (default).
5518pub const ZFST_CASC: i32 = 0x0000;
5519/// `ZFST_CIMA` — current type is image.
5520pub const ZFST_CIMA: i32 = 0x0002;
5521/// `ZFST_STRE` — stream mode (default).
5522pub const ZFST_STRE: i32 = 0x0000;
5523/// `ZFST_BLOC` — block mode.
5524pub const ZFST_BLOC: i32 = 0x0004;
5525/// `ZFST_MMSK` — mask for mode flags.
5526pub const ZFST_MMSK: i32 = 0x0004;
5527/// `ZFST_LOGI` — user logged in.
5528pub const ZFST_LOGI: i32 = 0x0008;
5529/// `ZFST_SYST` — done SYST type check.
5530pub const ZFST_SYST: i32 = 0x0010;
5531/// `ZFST_NOPS` — server doesn't understand PASV.
5532pub const ZFST_NOPS: i32 = 0x0020;
5533/// `ZFST_NOSZ` — server doesn't send `(XXXX bytes)' reply.
5534pub const ZFST_NOSZ: i32 = 0x0040;
5535/// `ZFST_TRSZ` — tried getting 'size' from reply.
5536pub const ZFST_TRSZ: i32 = 0x0080;
5537/// `ZFST_CLOS` — connection closed.
5538pub const ZFST_CLOS: i32 = 0x0100;
5539
5540/// `ZFPF_SNDP` — use send port (active mode) preference.
5541pub const ZFPF_SNDP: i32 = 0x01; // c:280
5542/// `ZFPF_PASV` — try passive mode preference.
5543pub const ZFPF_PASV: i32 = 0x02; // c:281
5544/// `ZFPF_DUMB` — don't do clever things with variables.
5545pub const ZFPF_DUMB: i32 = 0x04; // c:282
5546
5547/// FTP sessions manager.
5548/// Port of the file-static `zfsess_node` linked list +
5549/// `zfsess_current` pointer Src/Modules/zftp.c keeps —
5550/// `zftp_session()` (line 2889) drives the switch,
5551/// `zftp_rmsession()` (line 2915) the removal.
5552// `Zftp` struct renamed to `zftp_globals` — C has no `struct zftp`;
5553// the equivalent state in `Src/Modules/zftp.c` lives in file-scope
5554// globals (`Zfsess zfsessions` linked list head + `Zfsess
5555// zfsesscurrent`). Rust collapses these into one container accessed
5556// via `ZFTP_STATE_INNER`; the suffix names this as a Rust extension
5557// that bags the module's C-static state.
5558/// `zftp_globals` — see fields for layout.
5559#[allow(non_camel_case_types)]
5560#[derive(Debug, Default)]
5561pub struct zftp_globals {
5562 // c:311 — `static LinkList zfsessions;` appended by zaddlinknode
5563 // (c:2819): IndexMap keeps the same insertion order the C linked
5564 // list has, so listing/first-remaining walks match C.
5565 sessions: IndexMap<String, zftp_session>,
5566 current: Option<String>,
5567}
5568
5569/// Global ZFTP session state — port of the C file-scope statics
5570/// `static Zftp_session zfsessions[]` and friends in zftp.c. Holds
5571/// all sessions and the currently-active one. Free ported above route
5572/// through this so subcommand dispatch matches C behaviour.
5573static ZFTP_STATE_INNER: OnceLock<Mutex<zftp_globals>> = OnceLock::new();
5574
5575/// WARNING: NOT IN ZFTP.C — platform-gated `errno` pointer; C reads errno directly after syscalls
5576/// (equivalent C logic at Src/Modules/zftp.c:25).
5577/// Platform-gated `errno` pointer. zsh's C source writes `errno` directly
5578/// after `select(2)`/`read(2)` races; macOS exposes `__error()`, Linux/Android
5579/// expose `__errno_location()`, BSDs use `__errno`. Returning a raw pointer
5580/// keeps the original `*errno = X` write-shape from the C source intact.
5581#[inline]
5582fn errno_ptr() -> *mut libc::c_int {
5583 #[cfg(any(target_os = "linux", target_os = "android"))]
5584 unsafe {
5585 libc::__errno_location()
5586 }
5587 #[cfg(any(
5588 target_os = "macos",
5589 target_os = "ios",
5590 target_os = "freebsd",
5591 target_os = "dragonfly"
5592 ))]
5593 unsafe {
5594 libc::__error()
5595 }
5596 #[cfg(any(target_os = "openbsd", target_os = "netbsd"))]
5597 unsafe {
5598 extern "C" {
5599 fn __errno() -> *mut libc::c_int;
5600 }
5601 __errno()
5602 }
5603}
5604
5605/// Port of `zfopendata(char *name, union tcp_sockaddr *zdsockp, int *is_passivep)` from `Src/Modules/zftp.c:859`.
5606/// Helper: parse a `227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)`
5607/// FTP reply into (ip, port). Direct extraction of the sscanf path
5608/// inside C zfopendata (Src/Modules/zftp.c:925-941). Allowlisted as
5609/// a Rust-only architectural helper since C does the parse inline.
5610fn parse_pasv_response(msg: &str) -> io::Result<(String, u16)> {
5611 // c:925 inline
5612 // c:937-939 — `for (ptr = lastmsg; *ptr; ptr++) if (idigit(*ptr)) break;`.
5613 // C walks to the FIRST DIGIT in lastmsg and sscanf from there — it
5614 // doesn't require parentheses. RFC 959's PASV reply text is
5615 // unspecified ("Entering Passive Mode" is conventional but not
5616 // mandatory), so some servers / proxies omit the `(...)` wrap.
5617 //
5618 // Prior Rust port required both `(` and `)` and bailed with
5619 // "invalid PASV response" otherwise; against a paren-less server
5620 // PASV transfers would refuse to open and fall through to the
5621 // less-efficient PORT-mode path on every transfer.
5622 let start = msg
5623 .bytes()
5624 .position(|b| b.is_ascii_digit())
5625 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid PASV response"))?;
5626
5627 // c:940-941 — `sscanf("%d,%d,...", nums...)` then `(unsigned char) nums[i]`.
5628 // C parses each value as int, then *truncates* to u8 via cast. Previous
5629 // Rust port parsed as u16 and computed `(nums[4] << 8) + nums[5]` which
5630 // would PANIC (debug) / wrap (release) when a malicious or malformed
5631 // server sent values > 255. Match C's truncate-to-u8 semantics so
5632 // out-of-range octets behave the same as C (low 8 bits used; no panic).
5633 //
5634 // Take the first 6 comma-separated number tokens from `start`
5635 // onward; trailing text after the 6th value is ignored (matches
5636 // sscanf's lazy match).
5637 let nums: Vec<i32> = msg[start..]
5638 .split(|c: char| !c.is_ascii_digit() && c != '-')
5639 .filter(|s| !s.is_empty())
5640 .take(6)
5641 .filter_map(|s| s.parse().ok())
5642 .collect();
5643
5644 if nums.len() != 6 {
5645 return Err(io::Error::new(
5646 io::ErrorKind::InvalidData,
5647 "invalid PASV numbers",
5648 ));
5649 }
5650
5651 // c:947-948 — `iaddr[i] = (unsigned char) nums[i];` — low-byte cast.
5652 let oct = |n: i32| -> u8 { n as u8 };
5653 let ip = format!(
5654 "{}.{}.{}.{}",
5655 oct(nums[0]),
5656 oct(nums[1]),
5657 oct(nums[2]),
5658 oct(nums[3])
5659 );
5660 // c:949-950 — `iport[0] = (unsigned char) nums[4]; iport[1] = ... nums[5];`.
5661 // Then `memcpy(&sin_port, iport, 2)` reads as network-order u16 =
5662 // `(iport[0] << 8) | iport[1]`.
5663 let port = ((oct(nums[4]) as u16) << 8) | (oct(nums[5]) as u16);
5664
5665 Ok((ip, port))
5666}
5667
5668/// Clear poison on the zftp state mutex (test teardown helper).
5669pub fn zftp_state_clear_poison() {
5670 if let Some(m) = ZFTP_STATE_INNER.get() {
5671 m.clear_poison();
5672 }
5673}
5674
5675// File-static globals for zfalarm/zfunalarm — c:386-389.
5676/// `zfdrrrring` — file-static from `Src/Modules/zftp.c:340`. Set by
5677/// `zfhandler()` on SIGALRM, polled by zfread/zfgetline to bail out.
5678pub static ZFDRRRRING: std::sync::atomic::AtomicI32 = // c:340
5679 std::sync::atomic::AtomicI32::new(0);
5680
5681/// `zfalarmed` — file-static from `Src/Modules/zftp.c:346`. Tracks
5682/// whether `zfalarm()` has installed the SIGALRM handler.
5683pub static ZFALARMED: std::sync::atomic::AtomicI32 = // c:346
5684 std::sync::atomic::AtomicI32::new(0);
5685/// `OALREMAIN` static.
5686pub static OALREMAIN: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
5687/// `OALTIME` static.
5688pub static OALTIME: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
5689
5690// `zftp_cleanup` is defined above at c:3128; the exit hook calls it.
5691
5692static MODULE_FEATURES: OnceLock<Mutex<crate::ported::zsh_h::features>> = OnceLock::new();
5693
5694// Local stubs for the per-module entry points. C uses generic
5695// `featuresarray`/`handlefeatures`/`setfeatureenables` (module.c:
5696// 3275/3370/3445) but those take `Builtin` + `Features` pointer
5697// fields the Rust port doesn't carry. The hardcoded descriptor
5698// list mirrors the C bintab/conddefs/mathfuncs/paramdefs.
5699// WARNING: NOT IN ZFTP.C — Rust-only module-framework shim.
5700// C uses generic featuresarray/handlefeatures/setfeatureenables from
5701// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
5702// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
5703fn featuresarray(_m: *const module, _f: &Mutex<crate::ported::zsh_h::features>) -> Vec<String> {
5704 vec!["b:zftp".to_string()]
5705}
5706
5707// WARNING: NOT IN ZFTP.C — Rust-only module-framework shim.
5708// C uses generic featuresarray/handlefeatures/setfeatureenables from
5709// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
5710// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
5711fn handlefeatures(
5712 _m: *const module,
5713 _f: &Mutex<crate::ported::zsh_h::features>,
5714 enables: &mut Option<Vec<i32>>,
5715) -> i32 {
5716 if enables.is_none() {
5717 *enables = Some(vec![1; 1]);
5718 }
5719 0
5720}
5721
5722// WARNING: NOT IN ZFTP.C — Rust-only module-framework shim.
5723// C uses generic featuresarray/handlefeatures/setfeatureenables from
5724// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
5725// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
5726fn setfeatureenables(
5727 _m: *const module,
5728 _f: &Mutex<crate::ported::zsh_h::features>,
5729 _e: Option<&[i32]>,
5730) -> i32 {
5731 0
5732}
5733
5734// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5735// ─── RUST-ONLY ACCESSORS ───
5736//
5737// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
5738// RwLock<T>>` globals declared above. C zsh uses direct global
5739// access; Rust needs these wrappers because `OnceLock::get_or_init`
5740// is the only way to lazily construct shared state. These ported sit
5741// here so the body of this file reads in C source order without
5742// the accessor wrappers interleaved between real port ported.
5743// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5744
5745// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5746// ─── RUST-ONLY ACCESSORS ───
5747//
5748// Singleton accessor ported for `OnceLock<Mutex<T>>` / `OnceLock<
5749// RwLock<T>>` globals declared above. C zsh uses direct global
5750// access; Rust needs these wrappers because `OnceLock::get_or_init`
5751// is the only way to lazily construct shared state. These ported sit
5752// here so the body of this file reads in C source order without
5753// the accessor wrappers interleaved between real port ported.
5754// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5755
5756/// Accessor for the live zftp module state. C's `Src/Modules/zftp.c`
5757/// keeps per-session state in scattered file-statics (`zfsess`,
5758/// `zfsessions`, `zfcommand`, ...); this fn returns the single
5759/// `Mutex<zftp_globals>` aggregating those into one shared store.
5760pub fn zftp_state() -> &'static Mutex<zftp_globals> {
5761 ZFTP_STATE_INNER.get_or_init(|| Mutex::new(zftp_globals::default()))
5762}
5763
5764// WARNING: NOT IN ZFTP.C — Rust-only module-framework shim.
5765// C uses generic featuresarray/handlefeatures/setfeatureenables from
5766// Src/module.c:3275/3370/3445 with C-side Builtin/Features pointers;
5767// Rust per-module shims hardcode the bintab/conddefs/mathfuncs/paramdefs.
5768fn module_features() -> &'static Mutex<crate::ported::zsh_h::features> {
5769 MODULE_FEATURES.get_or_init(|| {
5770 Mutex::new(crate::ported::zsh_h::features {
5771 bn_list: None,
5772 bn_size: 1,
5773 cd_list: None,
5774 cd_size: 0,
5775 mf_list: None,
5776 mf_size: 0,
5777 pd_list: None,
5778 pd_size: 0,
5779 n_abstract: 0,
5780 })
5781 })
5782}
5783
5784#[cfg(test)]
5785mod tests {
5786 use super::*;
5787
5788 #[test]
5789 fn test_transfer_type() {
5790 let _g = crate::test_util::global_state_lock();
5791 // Inline-test pattern matching C: `(typ & ZFST_IMAG) ? "I" : "A"`
5792 let ascii_letter = if (ZFST_ASCI & ZFST_IMAG) != 0 {
5793 "I"
5794 } else {
5795 "A"
5796 };
5797 let image_letter = if (ZFST_IMAG & ZFST_IMAG) != 0 {
5798 "I"
5799 } else {
5800 "A"
5801 };
5802 assert_eq!(ascii_letter, "A");
5803 assert_eq!(image_letter, "I");
5804 }
5805
5806 #[test]
5807 fn test_transfer_mode() {
5808 let _g = crate::test_util::global_state_lock();
5809 // Inline-test pattern matching C: `(mode & ZFST_BLOC) ? "B" : "S"`
5810 let stream_letter = if (ZFST_STRE & ZFST_BLOC) != 0 {
5811 "B"
5812 } else {
5813 "S"
5814 };
5815 let block_letter = if (ZFST_BLOC & ZFST_BLOC) != 0 {
5816 "B"
5817 } else {
5818 "S"
5819 };
5820 assert_eq!(stream_letter, "S");
5821 assert_eq!(block_letter, "B");
5822 }
5823
5824 /// FTP reply-code class predicates per RFC 959. C tests these
5825 /// inline at every reply-check call site (e.g. `if (lastcode < 400)`).
5826 fn is_positive(c: i32) -> bool {
5827 c >= 100 && c < 400
5828 }
5829 fn is_positive_completion(c: i32) -> bool {
5830 c >= 200 && c < 300
5831 }
5832 fn is_positive_intermediate(c: i32) -> bool {
5833 c >= 300 && c < 400
5834 }
5835 fn is_negative(c: i32) -> bool {
5836 c >= 400
5837 }
5838
5839 #[test]
5840 fn test_ftp_response_positive() {
5841 let _g = crate::test_util::global_state_lock();
5842 let resp: FtpResponse = (200, "OK".to_string());
5843 assert!(is_positive(resp.0));
5844 assert!(is_positive_completion(resp.0));
5845 assert!(!is_negative(resp.0));
5846 }
5847
5848 #[test]
5849 fn test_ftp_response_intermediate() {
5850 let _g = crate::test_util::global_state_lock();
5851 let resp: FtpResponse = (331, "Password required".to_string());
5852 assert!(is_positive(resp.0));
5853 assert!(is_positive_intermediate(resp.0));
5854 assert!(!is_positive_completion(resp.0));
5855 }
5856
5857 #[test]
5858 fn test_ftp_response_negative() {
5859 let _g = crate::test_util::global_state_lock();
5860 let resp: FtpResponse = (550, "File not found".to_string());
5861 assert!(is_negative(resp.0));
5862 assert!(!is_positive(resp.0));
5863 }
5864
5865 #[test]
5866 fn test_ftp_session_new() {
5867 let _g = crate::test_util::global_state_lock();
5868 let sess = zftp_session::new("test");
5869 assert_eq!(sess.name, "test");
5870 assert!(!sess.connected);
5871 assert!(!sess.logged_in);
5872 }
5873
5874 #[test]
5875 fn test_parse_pasv_response() {
5876 let _g = crate::test_util::global_state_lock();
5877 // c:934-936 — input is lastmsg, which "already has the reply
5878 // code expunged" (no leading "227 ").
5879 let msg = "Entering Passive Mode (192,168,1,1,4,1)";
5880 let (ip, port) = parse_pasv_response(msg).unwrap();
5881 assert_eq!(ip, "192.168.1.1");
5882 assert_eq!(port, 1025);
5883 }
5884
5885 #[test]
5886 fn test_parse_pasv_response_invalid() {
5887 let _g = crate::test_util::global_state_lock();
5888 let msg = "invalid";
5889 assert!(parse_pasv_response(msg).is_err());
5890 }
5891
5892 #[test]
5893 fn test_zftp_new() {
5894 let _g = crate::test_util::global_state_lock();
5895 let zftp = zftp_globals::default();
5896 assert!(zftp.sessions.is_empty());
5897 }
5898
5899 /// c:2814-2818 — newsession registers the named session and
5900 /// points zfsess at it.
5901 #[test]
5902 fn test_zftp_create_session() {
5903 let _g = crate::test_util::global_state_lock();
5904 zftp_cleanup();
5905 newsession("test");
5906 let state = zftp_state().lock().unwrap();
5907 assert!(state.sessions.contains_key("test"));
5908 assert_eq!(state.current.as_deref(), Some("test"));
5909 drop(state);
5910 zftp_cleanup();
5911 }
5912
5913 /// c:2958 — rmsession removes the entry; c:2929-2930 — not-found
5914 /// exits 1; c:2985-2992 — removing the last session recreates
5915 /// "default".
5916 #[test]
5917 fn test_zftp_remove_session() {
5918 let _g = crate::test_util::global_state_lock();
5919 zftp_cleanup();
5920 newsession("test");
5921 assert_eq!(zftp_rmsession("rmsession", &["test"], 0), 0);
5922 let state = zftp_state().lock().unwrap();
5923 assert!(!state.sessions.contains_key("test"));
5924 // c:2992 — last session removed → fresh "default".
5925 assert!(state.sessions.contains_key("default"));
5926 drop(state);
5927 assert_eq!(zftp_rmsession("rmsession", &["test"], 0), 1); // c:2930
5928 zftp_cleanup();
5929 }
5930
5931 /// c:2806-2812 — newsession on an existing name switches zfsess
5932 /// to it without creating a duplicate.
5933 #[test]
5934 fn test_zftp_set_current() {
5935 let _g = crate::test_util::global_state_lock();
5936 zftp_cleanup();
5937 newsession("test");
5938 newsession("test2");
5939 assert_eq!(
5940 zftp_state().lock().unwrap().current.as_deref(),
5941 Some("test2")
5942 );
5943 newsession("test"); // existing — switch only, no insert
5944 let state = zftp_state().lock().unwrap();
5945 assert_eq!(state.current.as_deref(), Some("test"));
5946 assert_eq!(state.sessions.len(), 2);
5947 drop(state);
5948 zftp_cleanup();
5949 }
5950
5951 #[test]
5952 fn test_builtin_zftp_no_args() {
5953 let _g = crate::test_util::global_state_lock();
5954 let status = bin_zftp(
5955 "zftp",
5956 &[].iter().map(|s: &&str| s.to_string()).collect::<Vec<_>>(),
5957 &options {
5958 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5959 args: Vec::new(),
5960 argscount: 0,
5961 argsalloc: 0,
5962 },
5963 0,
5964 );
5965 assert_eq!(status, 1);
5966 }
5967
5968 /// Port of `zftp_open(char *name, char **args, int flags)` from `Src/Modules/zftp.c:1690`.
5969 #[test]
5970 fn test_builtin_zftp_session() {
5971 let _g = crate::test_util::global_state_lock();
5972 // Reset global state for test isolation.
5973 zftp_cleanup();
5974 let status = bin_zftp(
5975 "zftp",
5976 &["session", "test"]
5977 .iter()
5978 .map(|s: &&str| s.to_string())
5979 .collect::<Vec<_>>(),
5980 &options {
5981 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
5982 args: Vec::new(),
5983 argscount: 0,
5984 argsalloc: 0,
5985 },
5986 0,
5987 );
5988 assert_eq!(status, 0);
5989 assert!(zftp_state().lock().unwrap().sessions.contains_key("test"));
5990 zftp_cleanup();
5991 }
5992
5993 #[test]
5994 fn test_builtin_zftp_test_not_connected() {
5995 let _g = crate::test_util::global_state_lock();
5996 let status = bin_zftp(
5997 "zftp",
5998 &["test"]
5999 .iter()
6000 .map(|s: &&str| s.to_string())
6001 .collect::<Vec<_>>(),
6002 &options {
6003 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
6004 args: Vec::new(),
6005 argscount: 0,
6006 argsalloc: 0,
6007 },
6008 0,
6009 );
6010 assert_eq!(status, 1);
6011 }
6012
6013 fn zftp_empty_ops() -> options {
6014 options {
6015 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
6016 args: Vec::new(),
6017 argscount: 0,
6018 argsalloc: 0,
6019 }
6020 }
6021
6022 /// c:3002 — unknown subcommand returns 1 (zwarnnam "unknown
6023 /// subcommand"). Regression accepting bogus names would let
6024 /// `zftp xyz_not_real` silently succeed and confuse scripts.
6025 #[test]
6026 fn zftp_unknown_subcommand_returns_one() {
6027 let _g = crate::test_util::global_state_lock();
6028 zftp_cleanup();
6029 let ops = zftp_empty_ops();
6030 let r = bin_zftp(
6031 "zftp",
6032 &["definitely_not_a_subcommand_xyz".to_string()],
6033 &ops,
6034 0,
6035 );
6036 assert_eq!(r, 1);
6037 zftp_cleanup();
6038 }
6039
6040 /// `zftp_state` is empty after cleanup. A regression that
6041 /// pre-populates a "default" session would let `zftp test` succeed
6042 /// before any open command — wrong shell semantics.
6043 #[test]
6044 fn zftp_state_empty_after_cleanup() {
6045 let _g = crate::test_util::global_state_lock();
6046 zftp_cleanup();
6047 assert!(zftp_state().lock().unwrap().sessions.is_empty());
6048 }
6049
6050 /// `Src/Modules/zftp.c:267` — `#define ZFST_TYPE(x) (x & ZFST_TMSK)`.
6051 /// The type mask isolates the transfer-type bit (`ZFST_IMAG` = 0x0001)
6052 /// from the rest of the zfstatus word. Pin the mask semantics so a
6053 /// regen that flips `ZFST_TMSK` silently to a wider value mis-extracts
6054 /// other status bits (mode/login/syst/etc) as if they were type.
6055 #[test]
6056 fn zfst_type_isolates_transfer_type_bit() {
6057 let _g = crate::test_util::global_state_lock();
6058 // Pure ASCII → 0
6059 assert_eq!(ZFST_TYPE(ZFST_ASCI), 0);
6060 // Pure IMAGE → 1
6061 assert_eq!(ZFST_TYPE(ZFST_IMAG), 1);
6062 // Mode + image flags set → still 1 (mode masked out)
6063 assert_eq!(ZFST_TYPE(ZFST_IMAG | ZFST_BLOC), 1);
6064 // Image + LOGI + SYST → still 1
6065 assert_eq!(ZFST_TYPE(ZFST_IMAG | ZFST_LOGI | ZFST_SYST), 1);
6066 // No image bit + lots of other flags → 0
6067 assert_eq!(ZFST_TYPE(ZFST_LOGI | ZFST_SYST | ZFST_BLOC), 0);
6068 }
6069
6070 /// `Src/Modules/zftp.c:267` — `#define ZFST_MODE(x) (x & ZFST_MMSK)`.
6071 /// Mode mask isolates the stream-vs-block bit (`ZFST_BLOC` = 0x0004).
6072 /// Same regression risk as ZFST_TYPE: a too-wide MMSK would mis-claim
6073 /// LOGI / SYST / NOPS / NOSZ / TRSZ / CLOS bits as "mode."
6074 #[test]
6075 fn zfst_mode_isolates_transfer_mode_bit() {
6076 let _g = crate::test_util::global_state_lock();
6077 assert_eq!(ZFST_MODE(ZFST_STRE), 0);
6078 assert_eq!(ZFST_MODE(ZFST_BLOC), 4);
6079 // BLOC + IMAG → still 4 (type masked out)
6080 assert_eq!(ZFST_MODE(ZFST_BLOC | ZFST_IMAG), 4);
6081 // BLOC + LOGI + SYST → still 4
6082 assert_eq!(ZFST_MODE(ZFST_BLOC | ZFST_LOGI | ZFST_SYST), 4);
6083 // LOGI + SYST + NOPS + NOSZ + TRSZ + CLOS, no BLOC → 0
6084 let many = ZFST_LOGI | ZFST_SYST | ZFST_NOPS | ZFST_NOSZ | ZFST_TRSZ | ZFST_CLOS;
6085 assert_eq!(ZFST_MODE(many), 0);
6086 }
6087
6088 /// `Src/Modules/zftp.c:546-562` — `zfargstring(cmd, args)` joins
6089 /// `cmd` with space-separated `args` and appends the c:559 CRLF
6090 /// terminator (part of the fn's contract — C callers pass the
6091 /// result straight to zfsendcmd). Empty argv yields cmd + CRLF.
6092 #[test]
6093 fn zfargstring_empty_args_returns_cmd() {
6094 let _g = crate::test_util::global_state_lock();
6095 assert_eq!(zfargstring("RETR", &[]), "RETR\r\n");
6096 assert_eq!(zfargstring("QUIT", &[]), "QUIT\r\n");
6097 assert_eq!(zfargstring("", &[]), "\r\n");
6098 }
6099
6100 /// `Src/Modules/zftp.c:546-570` — one argument case: single space
6101 /// between cmd and arg, no trailing whitespace.
6102 #[test]
6103 fn zfargstring_single_arg_one_space() {
6104 let _g = crate::test_util::global_state_lock();
6105 assert_eq!(zfargstring("RETR", &["file.txt"]), "RETR file.txt\r\n");
6106 assert_eq!(zfargstring("USER", &["anonymous"]), "USER anonymous\r\n");
6107 }
6108
6109 /// `Src/Modules/zftp.c:546-570` — multi-arg: space-separated, no
6110 /// double-spacing or trailing space. The C body builds the buffer
6111 /// via `sprintf(...,"%s",...)` with explicit space separators.
6112 #[test]
6113 fn zfargstring_multi_arg_space_joined() {
6114 let _g = crate::test_util::global_state_lock();
6115 assert_eq!(
6116 zfargstring("USER", &["anonymous", "pass@example.com"]),
6117 "USER anonymous pass@example.com\r\n"
6118 );
6119 assert_eq!(
6120 zfargstring("PORT", &["192", "168", "1", "1", "4", "1"]),
6121 "PORT 192 168 1 1 4 1\r\n"
6122 );
6123 }
6124
6125 /// `Src/Modules/zftp.c:546-570` — empty args don't get filtered
6126 /// out (C source's `sprintf("%s ",arg)` with empty arg → bare space).
6127 /// Pin the exact behavior so a "smart" regression doesn't add a
6128 /// silent skip-empty filter that would silently drop legitimate
6129 /// (but empty) positional args.
6130 #[test]
6131 fn zfargstring_empty_arg_emits_space() {
6132 let _g = crate::test_util::global_state_lock();
6133 assert_eq!(zfargstring("CMD", &["", "after"]), "CMD after\r\n");
6134 assert_eq!(zfargstring("CMD", &["before", ""]), "CMD before \r\n");
6135 }
6136
6137 /// `Src/Modules/zftp.c:3902-3905` — ZFST_ASCI is 0, ZFST_IMAG is 1.
6138 /// These are the only two type values; their mutual exclusion is
6139 /// what the c:267 mask depends on. Pin the exact values so a
6140 /// regen that flips them silently inverts the ASCII vs binary
6141 /// transfer selection across the entire zftp subcommand surface.
6142 #[test]
6143 fn zfst_type_constants_are_zero_and_one() {
6144 let _g = crate::test_util::global_state_lock();
6145 assert_eq!(ZFST_ASCI, 0);
6146 assert_eq!(ZFST_IMAG, 1);
6147 assert_eq!(ZFST_TMSK, 1);
6148 assert_eq!(ZFST_ASCI | ZFST_IMAG, ZFST_TMSK);
6149 }
6150
6151 /// `Src/Modules/zftp.c:3914-3919` — Mode bit values + mask.
6152 /// ZFST_STRE=0, ZFST_BLOC=4, ZFST_MMSK=4. The MMSK == BLOC contract
6153 /// is load-bearing: type=0/1 in bit 0-0, mode=4 in bit 2. Pin so
6154 /// a regen that shifts MMSK to bit 1 silently overlaps the type bit.
6155 #[test]
6156 fn zfst_mode_constants_have_correct_bit_position() {
6157 let _g = crate::test_util::global_state_lock();
6158 assert_eq!(ZFST_STRE, 0);
6159 assert_eq!(ZFST_BLOC, 0x04);
6160 assert_eq!(ZFST_MMSK, 0x04);
6161 assert_eq!(ZFST_STRE | ZFST_BLOC, ZFST_MMSK);
6162 // The type mask MUST NOT overlap the mode mask.
6163 assert_eq!(
6164 ZFST_TMSK & ZFST_MMSK,
6165 0,
6166 "c:3907/3919 — type and mode bit-fields must be disjoint"
6167 );
6168 }
6169
6170 /// `Src/Modules/zftp.c:3920-3931` — Higher status bits. Pin the
6171 /// exact values so a regen reshuffling them breaks every call site
6172 /// that does `if (zfstatusp[s] & ZFST_LOGI)` style checks.
6173 #[test]
6174 fn zfst_status_flag_bits_are_pairwise_distinct() {
6175 let _g = crate::test_util::global_state_lock();
6176 let flags = [
6177 ZFST_LOGI, ZFST_SYST, ZFST_NOPS, ZFST_NOSZ, ZFST_TRSZ, ZFST_CLOS,
6178 ];
6179 // All distinct
6180 let mut sorted = flags.to_vec();
6181 sorted.sort();
6182 sorted.dedup();
6183 assert_eq!(
6184 sorted.len(),
6185 flags.len(),
6186 "ZFST_* status flags must be pairwise distinct"
6187 );
6188 // None overlap with type or mode masks
6189 for f in flags {
6190 assert_eq!(
6191 f & ZFST_TMSK,
6192 0,
6193 "ZFST flag 0x{:x} must not overlap type mask",
6194 f
6195 );
6196 assert_eq!(
6197 f & ZFST_MMSK,
6198 0,
6199 "ZFST flag 0x{:x} must not overlap mode mask",
6200 f
6201 );
6202 }
6203 }
6204
6205 /// `Src/Modules/zftp.c:940-950` — `sscanf("%d,...")` then
6206 /// `(unsigned char) nums[i]` cast. The PASV reply `(h1,h2,h3,h4,
6207 /// p1,p2)` builds IP as h1.h2.h3.h4 and port as p1*256+p2. Pin
6208 /// the well-formed case round-trip. Inputs are lastmsg-form: the
6209 /// reply code is already expunged (c:934-936).
6210 #[test]
6211 fn parse_pasv_response_well_formed_round_trips() {
6212 let _g = crate::test_util::global_state_lock();
6213 let (ip, port) = parse_pasv_response("Entering Passive Mode (192,168,1,1,4,1)").unwrap();
6214 assert_eq!(ip, "192.168.1.1");
6215 assert_eq!(port, 4 * 256 + 1, "p1*256+p2 = 1025");
6216 // High-port case: p1=255, p2=255 → port=65535.
6217 let (_, port) = parse_pasv_response("ok (10,0,0,1,255,255)").unwrap();
6218 assert_eq!(port, 65535);
6219 // Low-port: p1=0, p2=21 → port=21.
6220 let (_, port) = parse_pasv_response("ok (127,0,0,1,0,21)").unwrap();
6221 assert_eq!(port, 21);
6222 }
6223
6224 /// `Src/Modules/zftp.c:947-948` — `(unsigned char) nums[i]` cast
6225 /// TRUNCATES to the low 8 bits. The previous Rust port stored
6226 /// `Vec<u16>` and computed `(nums[4] << 8) + nums[5]` which would
6227 /// PANIC in debug mode when a malicious or malformed server sent
6228 /// `nums[4] > 255` (e.g. `nums[4]=300` → `300 << 8 = 76800`
6229 /// overflows u16). The fix matches C's `(unsigned char)` cast
6230 /// and uses u16 only AFTER the low-byte truncation. Pin the
6231 /// no-panic contract for out-of-range octets.
6232 #[test]
6233 fn parse_pasv_response_out_of_range_octet_truncates_low_byte() {
6234 let _g = crate::test_util::global_state_lock();
6235 // p1=300 → low byte = 44 (300 & 0xff). port = 44*256 + 1 = 11265.
6236 let (_, port) = parse_pasv_response("ok (192,168,1,1,300,1)").unwrap();
6237 assert_eq!(
6238 port,
6239 44 * 256 + 1,
6240 "c:949 — (unsigned char)300 = 44; port = 44*256+1"
6241 );
6242 // No panic on absurd-but-parseable values. Previous Rust port
6243 // would panic in debug here.
6244 let (_, port) = parse_pasv_response("ok (1,2,3,4,1000,2000)").unwrap();
6245 let expected_p1 = (1000i32 as u8) as u16; // 232
6246 let expected_p2 = (2000i32 as u8) as u16; // 208
6247 assert_eq!(port, (expected_p1 << 8) | expected_p2);
6248 }
6249
6250 /// `Src/Modules/zftp.c:947` — IP octets also get the
6251 /// `(unsigned char)` truncation. Pin: `nums[i] > 255` cast to u8
6252 /// then formatted. Catches a regression that prints raw
6253 /// out-of-range values directly (would yield invalid IPs like
6254 /// "300.168.1.1").
6255 #[test]
6256 fn parse_pasv_response_ip_octets_truncate_to_u8() {
6257 let _g = crate::test_util::global_state_lock();
6258 // h1=300 → 300 & 0xff = 44.
6259 let (ip, _) = parse_pasv_response("ok (300,168,1,1,0,21)").unwrap();
6260 assert_eq!(
6261 ip, "44.168.1.1",
6262 "c:947 — octets truncate; out-of-range becomes low byte"
6263 );
6264 }
6265
6266 /// `Src/Modules/zftp.c:940-941` — `sscanf(...) != 6` is the error
6267 /// gate: FEWER than 6 numbers errors; MORE than 6 succeeds (sscanf
6268 /// consumes the first 6 and ignores the rest). Pin so a server
6269 /// with a short PASV reply doesn't silently produce a wrong
6270 /// IP/port, and a chatty one still parses.
6271 #[test]
6272 fn parse_pasv_response_wrong_number_count_errors() {
6273 let _g = crate::test_util::global_state_lock();
6274 assert!(
6275 parse_pasv_response("ok (1,2,3,4)").is_err(),
6276 "only 4 numbers → error"
6277 );
6278 assert!(
6279 parse_pasv_response("ok (1,2,3,4,5)").is_err(),
6280 "only 5 numbers → error"
6281 );
6282 // c:940 — sscanf returns 6 with 7 numbers present; the 7th is
6283 // ignored, not an error.
6284 let (ip, port) = parse_pasv_response("ok (1,2,3,4,5,6,7)").unwrap();
6285 assert_eq!(ip, "1.2.3.4", "first 4 of 7 are the IP");
6286 assert_eq!(port, 5 * 256 + 6, "5th/6th are the port; 7th ignored");
6287 }
6288
6289 /// `Src/Modules/zftp.c:937-941` — no digits at all → the first-
6290 /// digit scan runs off the string and sscanf gets nothing → error.
6291 /// (C's IPv4 path doesn't require parens; the error here is the
6292 /// absence of any numbers.)
6293 #[test]
6294 fn parse_pasv_response_missing_parens_errors() {
6295 let _g = crate::test_util::global_state_lock();
6296 assert!(
6297 parse_pasv_response("ok no parens here").is_err(),
6298 "missing both parens → error"
6299 );
6300 assert!(
6301 parse_pasv_response("ok (no_close").is_err(),
6302 "missing close paren → error"
6303 );
6304 assert!(
6305 parse_pasv_response("ok no_open)").is_err(),
6306 "missing open paren → error"
6307 );
6308 }
6309
6310 // ─── zsh-corpus pins for ZFST_TYPE / ZFST_MODE macros ──────────
6311
6312 /// `ZFST_TYPE(x)` masks to the type bits.
6313 #[test]
6314 fn zftp_corpus_zfst_type_masks_type_bits() {
6315 assert_eq!(ZFST_TYPE(ZFST_ASCI), ZFST_ASCI, "ASCI type bit");
6316 assert_eq!(ZFST_TYPE(ZFST_IMAG), ZFST_IMAG, "IMAG type bit");
6317 }
6318
6319 /// `ZFST_TYPE` ignores mode bits.
6320 #[test]
6321 fn zftp_corpus_zfst_type_ignores_mode_bits() {
6322 let combined = ZFST_IMAG | ZFST_BLOC;
6323 assert_eq!(ZFST_TYPE(combined), ZFST_IMAG, "ZFST_TYPE strips mode bits");
6324 }
6325
6326 /// `ZFST_MODE` masks mode bits.
6327 #[test]
6328 fn zftp_corpus_zfst_mode_masks_mode_bits() {
6329 assert_eq!(ZFST_MODE(ZFST_STRE), ZFST_STRE, "stream mode");
6330 assert_eq!(ZFST_MODE(ZFST_BLOC), ZFST_BLOC, "block mode");
6331 }
6332
6333 /// `ZFST_MODE` ignores type bits.
6334 #[test]
6335 fn zftp_corpus_zfst_mode_ignores_type_bits() {
6336 let combined = ZFST_IMAG | ZFST_BLOC;
6337 assert_eq!(ZFST_MODE(combined), ZFST_BLOC, "ZFST_MODE strips type bits");
6338 }
6339
6340 /// `ZFST_TMSK` and `ZFST_MMSK` don't overlap.
6341 #[test]
6342 fn zftp_corpus_zfst_type_mode_masks_disjoint() {
6343 assert_eq!(
6344 ZFST_TMSK & ZFST_MMSK,
6345 0,
6346 "type-mask and mode-mask must be disjoint"
6347 );
6348 }
6349
6350 /// `ZFST_TYPE(0)` and `ZFST_MODE(0)` both return 0.
6351 #[test]
6352 fn zftp_corpus_zfst_zero_input_returns_zero() {
6353 assert_eq!(ZFST_TYPE(0), 0);
6354 assert_eq!(ZFST_MODE(0), 0);
6355 }
6356
6357 // ═══════════════════════════════════════════════════════════════════
6358 // C-parity tests pinning Src/Modules/zftp.c macros + status bits.
6359 // ═══════════════════════════════════════════════════════════════════
6360
6361 /// `ZFST_TYPE(ZFST_TMSK | ZFST_BLOC)` masks out only type bit.
6362 /// C `#define ZFST_TYPE(x) (x & ZFST_TMSK)` (c:267).
6363 #[test]
6364 fn ZFST_TYPE_masks_only_type_bit() {
6365 let mixed = ZFST_TMSK | ZFST_BLOC;
6366 assert_eq!(ZFST_TYPE(mixed), ZFST_TMSK, "type extracted, mode dropped");
6367 }
6368
6369 /// `ZFST_MODE(ZFST_TMSK | ZFST_BLOC)` masks out only mode bit.
6370 /// C `#define ZFST_MODE(x) (x & ZFST_MMSK)` (c:273).
6371 #[test]
6372 fn ZFST_MODE_masks_only_mode_bit() {
6373 let mixed = ZFST_TMSK | ZFST_BLOC;
6374 assert_eq!(ZFST_MODE(mixed), ZFST_BLOC, "mode extracted, type dropped");
6375 }
6376
6377 /// `ZFST_TYPE | ZFST_MODE` reconstruct the original status.
6378 /// No bit overlap between masks.
6379 #[test]
6380 fn ZFST_TYPE_and_MODE_reconstruct_original_status() {
6381 let original = ZFST_TMSK | ZFST_BLOC;
6382 let t = ZFST_TYPE(original);
6383 let m = ZFST_MODE(original);
6384 assert_eq!(t | m, original, "TYPE | MODE = original status");
6385 }
6386
6387 /// `ZFST_ASCI` is the zero/default type value.
6388 /// C `#define ZFST_ASCI 0x0000`.
6389 #[test]
6390 fn ZFST_ASCI_is_zero_default() {
6391 assert_eq!(ZFST_ASCI, 0);
6392 assert_eq!(ZFST_TYPE(ZFST_ASCI), 0, "ASCII type = zero default");
6393 }
6394
6395 // ═══════════════════════════════════════════════════════════════════
6396 // Additional C-parity tests for Src/Modules/zftp.c utilities.
6397 // ═══════════════════════════════════════════════════════════════════
6398
6399 /// c:546-562 — `zfargstring("CMD", &[])` returns the cmd plus the
6400 /// CRLF terminator (c:559 `strcat(line, "\r\n")`); no trailing
6401 /// space, no extra chars before the CRLF.
6402 #[test]
6403 fn zfargstring_no_args_returns_cmd_verbatim() {
6404 assert_eq!(zfargstring("RETR", &[]), "RETR\r\n");
6405 assert_eq!(zfargstring("", &[]), "\r\n");
6406 }
6407
6408 /// c:546-562 — single arg joined with single space, CRLF appended
6409 /// (c:556-559).
6410 #[test]
6411 fn zfargstring_one_arg_joins_with_single_space() {
6412 assert_eq!(zfargstring("CWD", &["/tmp"]), "CWD /tmp\r\n");
6413 assert_eq!(zfargstring("USER", &["anonymous"]), "USER anonymous\r\n");
6414 }
6415
6416 /// c:546-562 — multiple args joined with single space each, CRLF
6417 /// appended (c:556-559).
6418 #[test]
6419 fn zfargstring_multiple_args_each_space_separated() {
6420 let r = zfargstring("FOO", &["a", "b", "c"]);
6421 assert_eq!(r, "FOO a b c\r\n", "exactly one space between each arg");
6422 }
6423
6424 /// c:546-562 — empty arg becomes empty word with surrounding
6425 /// spaces preserved (c:556-557 strcat " " then strcat "").
6426 #[test]
6427 fn zfargstring_empty_arg_preserves_separators() {
6428 let r = zfargstring("CMD", &["", "after"]);
6429 // C: "CMD" + " " + "" + " " + "after" + "\r\n" = "CMD after\r\n"
6430 assert_eq!(r, "CMD after\r\n", "empty arg → double space");
6431 }
6432
6433 /// c:267, c:273 — type and mode masks are non-overlapping bit
6434 /// fields. Pin: AND of the masks is exactly 0.
6435 #[test]
6436 fn ZFST_TMSK_and_MMSK_have_no_overlap() {
6437 assert_eq!(ZFST_TMSK & ZFST_MMSK, 0, "type/mode masks must not overlap");
6438 }
6439
6440 /// c:267 — `ZFST_TYPE` is idempotent: applying twice == once.
6441 #[test]
6442 fn ZFST_TYPE_is_idempotent() {
6443 let s = ZFST_TMSK | ZFST_BLOC;
6444 assert_eq!(ZFST_TYPE(ZFST_TYPE(s)), ZFST_TYPE(s));
6445 }
6446
6447 /// c:273 — `ZFST_MODE` is idempotent.
6448 #[test]
6449 fn ZFST_MODE_is_idempotent() {
6450 let s = ZFST_TMSK | ZFST_BLOC;
6451 assert_eq!(ZFST_MODE(ZFST_MODE(s)), ZFST_MODE(s));
6452 }
6453
6454 /// `ZFST_TYPE` of a value with only mode bits returns 0 (no
6455 /// type bits to extract).
6456 #[test]
6457 fn ZFST_TYPE_of_mode_only_returns_zero() {
6458 assert_eq!(ZFST_TYPE(ZFST_BLOC), 0);
6459 }
6460
6461 /// `ZFST_MODE` of a value with only type bits returns 0 (no
6462 /// mode bits to extract).
6463 #[test]
6464 fn ZFST_MODE_of_type_only_returns_zero() {
6465 assert_eq!(ZFST_MODE(ZFST_TMSK), 0);
6466 }
6467
6468 /// `zfargstring` does NOT add a trailing space after the last
6469 /// arg (would corrupt FTP commands which are CRLF-terminated).
6470 #[test]
6471 fn zfargstring_no_trailing_space() {
6472 let r = zfargstring("STOR", &["file.txt"]);
6473 assert!(!r.ends_with(' '), "no trailing space: {:?}", r);
6474 }
6475
6476 /// `zfargstring` does NOT add a leading space before the cmd.
6477 #[test]
6478 fn zfargstring_no_leading_space() {
6479 let r = zfargstring("STOR", &["file.txt"]);
6480 assert!(!r.starts_with(' '), "no leading space: {:?}", r);
6481 }
6482
6483 // ═══════════════════════════════════════════════════════════════════
6484 // Additional C-parity tests for Src/Modules/zftp.c sessions + lifecycle.
6485 // ═══════════════════════════════════════════════════════════════════
6486
6487 /// c:2814-2815 — `newsession("name")` registers the session with
6488 /// the name preserved and points zfsess at it.
6489 #[test]
6490 fn newsession_returns_session_with_name() {
6491 let _g = crate::test_util::global_state_lock();
6492 newsession("test_sess_xyz_zshrs");
6493 let state = zftp_state().lock().unwrap();
6494 assert_eq!(
6495 state
6496 .sessions
6497 .get("test_sess_xyz_zshrs")
6498 .map(|s| s.name.as_str()),
6499 Some("test_sess_xyz_zshrs")
6500 );
6501 assert_eq!(state.current.as_deref(), Some("test_sess_xyz_zshrs"));
6502 }
6503
6504 /// c:2816 — fresh session has dfd = -1 (no data fd open).
6505 #[test]
6506 fn newsession_fresh_has_no_data_fd() {
6507 let _g = crate::test_util::global_state_lock();
6508 newsession("test_dfd_xyz");
6509 assert_eq!(
6510 zftp_state()
6511 .lock()
6512 .unwrap()
6513 .sessions
6514 .get("test_dfd_xyz")
6515 .map(|s| s.dfd),
6516 Some(-1),
6517 "fresh session must have dfd=-1"
6518 );
6519 }
6520
6521 /// c:2817 — fresh session has empty params.
6522 #[test]
6523 fn newsession_fresh_has_empty_params() {
6524 let _g = crate::test_util::global_state_lock();
6525 newsession("test_params_xyz");
6526 assert!(
6527 zftp_state()
6528 .lock()
6529 .unwrap()
6530 .sessions
6531 .get("test_params_xyz")
6532 .map(|s| s.params.is_empty())
6533 .unwrap_or(false),
6534 "fresh params must be empty"
6535 );
6536 }
6537
6538 /// c:2806 — newsession called twice with same name doesn't duplicate
6539 /// the registered session.
6540 #[test]
6541 fn newsession_idempotent_for_same_name() {
6542 let _g = crate::test_util::global_state_lock();
6543 // Get baseline count.
6544 newsession("zshrs_dup_test_session");
6545 let count1 = zftp_state().lock().unwrap().sessions.len();
6546 newsession("zshrs_dup_test_session");
6547 let count2 = zftp_state().lock().unwrap().sessions.len();
6548 assert_eq!(count1, count2, "duplicate newsession must not grow table");
6549 }
6550
6551 /// c:3090 — `zftp_close(_, [], _)` no panic.
6552 #[test]
6553 fn zftp_close_empty_args_no_panic() {
6554 let _g = crate::test_util::global_state_lock();
6555 let _ = zftp_close("close", &[], 0);
6556 }
6557
6558 /// c:3200 — `zftp_rmsession(_, [], _)` no panic.
6559 #[test]
6560 fn zftp_rmsession_empty_args_no_panic() {
6561 let _g = crate::test_util::global_state_lock();
6562 let _ = zftp_rmsession("rmsession", &[], 0);
6563 }
6564
6565 /// c:2937 — `zftp_mkdir(_, [], _)` returns nonzero (needs arg).
6566 #[test]
6567 fn zftp_mkdir_no_args_returns_nonzero() {
6568 let _g = crate::test_util::global_state_lock();
6569 let r = zftp_mkdir("mkdir", &[], 0);
6570 assert_ne!(r, 0, "no args → usage error");
6571 }
6572
6573 /// c:2958 — `zftp_rename(_, [], _)` returns nonzero.
6574 #[test]
6575 fn zftp_rename_no_args_returns_nonzero() {
6576 let _g = crate::test_util::global_state_lock();
6577 let r = zftp_rename("rename", &[], 0);
6578 assert_ne!(r, 0, "no args → usage error");
6579 }
6580
6581 /// c:2987 — `zftp_quote(_, [], _)` returns nonzero.
6582 #[test]
6583 fn zftp_quote_no_args_returns_nonzero() {
6584 let _g = crate::test_util::global_state_lock();
6585 let r = zftp_quote("quote", &[], 0);
6586 assert_ne!(r, 0);
6587 }
6588
6589 /// c:4243 — setup_(NULL) returns 0.
6590 #[test]
6591 fn zftp_setup_returns_zero_pin() {
6592 let _g = crate::test_util::global_state_lock();
6593 assert_eq!(setup_(std::ptr::null()), 0);
6594 }
6595
6596 // ═══════════════════════════════════════════════════════════════════
6597 // Additional C-parity tests for Src/Modules/zftp.c
6598 // c:25 zftp_session / c:111 ZFST_TYPE / c:120 ZFST_MODE /
6599 // c:307 zfmovefd / c:327 zfsetparam / c:363 zfunsetparam /
6600 // c:377 zfargstring
6601 // ═══════════════════════════════════════════════════════════════════
6602
6603 /// c:111 — `ZFST_TYPE` returns i32 type pin.
6604 #[test]
6605 fn zfst_type_returns_i32_type() {
6606 let _: i32 = ZFST_TYPE(0);
6607 }
6608
6609 /// c:120 — `ZFST_MODE` returns i32 type pin.
6610 #[test]
6611 fn zfst_mode_returns_i32_type() {
6612 let _: i32 = ZFST_MODE(0);
6613 }
6614
6615 /// c:111 — `ZFST_TYPE` is pure full sweep.
6616 #[test]
6617 fn zfst_type_is_pure_full_sweep() {
6618 for v in [0i32, 1, 7, 0xff, i32::MAX] {
6619 let first = ZFST_TYPE(v);
6620 for _ in 0..3 {
6621 assert_eq!(ZFST_TYPE(v), first, "ZFST_TYPE({}) pure", v);
6622 }
6623 }
6624 }
6625
6626 /// c:120 — `ZFST_MODE` is pure.
6627 #[test]
6628 fn zfst_mode_is_pure_full_sweep() {
6629 for v in [0i32, 1, 7, 0xff, i32::MAX] {
6630 let first = ZFST_MODE(v);
6631 for _ in 0..3 {
6632 assert_eq!(ZFST_MODE(v), first, "ZFST_MODE({}) pure", v);
6633 }
6634 }
6635 }
6636
6637 /// c:307 — `zfmovefd(-1)` invalid fd returns i32 type.
6638 #[test]
6639 fn zfmovefd_invalid_returns_i32_type() {
6640 let _g = crate::test_util::global_state_lock();
6641 let _: i32 = zfmovefd(-1);
6642 }
6643
6644 /// c:327 — `zfsetparam("", "", 0)` empty inputs safe.
6645 #[test]
6646 fn zfsetparam_empty_inputs_no_panic() {
6647 let _g = crate::test_util::global_state_lock();
6648 zfsetparam("", "", 0);
6649 }
6650
6651 /// c:363 — `zfunsetparam("")` empty name PANICS in zshrs port
6652 /// ("failed to remove environment variable \"\": Invalid argument").
6653 /// C body calls `unsetparam` which silently no-ops on empty names;
6654 /// Rust port forwards to `std::env::remove_var("")` which traps
6655 /// on empty key per recent Rust safety hardening (since 1.86).
6656 /// Should guard empty-name short-circuit per C semantic.
6657 #[test]
6658 fn zfunsetparam_empty_name_no_panic() {
6659 let _g = crate::test_util::global_state_lock();
6660 zfunsetparam("");
6661 }
6662
6663 /// c:377 — `zfargstring("", &[])` empty returns String.
6664 #[test]
6665 fn zfargstring_empty_returns_string_type() {
6666 let _: String = zfargstring("", &[]);
6667 }
6668
6669 /// c:377 — `zfargstring` is pure for empty input.
6670 #[test]
6671 fn zfargstring_empty_is_pure() {
6672 let first = zfargstring("", &[]);
6673 for _ in 0..3 {
6674 assert_eq!(
6675 zfargstring("", &[]),
6676 first,
6677 "zfargstring empty must be pure"
6678 );
6679 }
6680 }
6681
6682 /// c:25 — `zftp_session` returns i32 type pin.
6683 #[test]
6684 fn zftp_session_returns_i32_type() {
6685 let _g = crate::test_util::global_state_lock();
6686 let _: i32 = zftp_session("nothing", &[], 0);
6687 }
6688}