Skip to main content

badterm_core/
kermit.rs

1//! Kermit file transfers, as C-Kermit, G-Kermit and Kermit-95 speak them,
2//! in both directions.
3//!
4//! Packets are `SOH LEN SEQ TYPE DATA CHECK CR`, every field but the mark
5//! made printable (`tochar(x) = x + 32`). The two sides trade their
6//! parameters in the Send-Init (`S`) packet and its acknowledgement; this
7//! side offers long packets (up to [`MAX_LONG`] bytes), CRC-16 block checks
8//! (type 3), `~` run-length compression and file attributes (for the size),
9//! and takes whatever the other side can do. No sliding windows: each
10//! packet waits for its ACK, which is fine on a telnet or SSH link.
11//!
12//! A download starts by itself when the BBS's Kermit sends its `S` packet
13//! (see [`find_send_init`]); an upload is started by the user once the
14//! BBS's Kermit is receiving.
15
16use std::collections::VecDeque;
17use std::fs::{self, File};
18use std::io::{BufWriter, Read, Write};
19use std::path::PathBuf;
20use std::time::{Duration, Instant};
21
22use anyhow::{Result, bail};
23
24use crate::transfer::{self, Common, Direction, Outcome, Progress};
25
26const SOH: u8 = 0x01;
27const CR: u8 = 0x0D;
28/// The longest packet offered (its data and check).
29pub const MAX_LONG: usize = 4000;
30/// How long to wait for a packet before resending (or NAKing).
31const TIMEOUT: Duration = Duration::from_secs(8);
32const MAX_RETRIES: u32 = 10;
33/// A receiver waiting for the sender's Send-Init gives up after this many
34/// timeouts.
35const START_TRIES: u32 = 15;
36
37/// Capability bits.
38const CAP_LONG: u8 = 2;
39const CAP_ATTRS: u8 = 8;
40
41fn tochar(x: u8) -> u8 {
42    x + 32
43}
44
45fn unchar(c: u8) -> u8 {
46    c.wrapping_sub(32)
47}
48
49fn ctl(c: u8) -> u8 {
50    c ^ 64
51}
52
53/// The Kermit block checks: 1 (6-bit sum), 2 (12-bit sum), 3 (CRC-16).
54fn block_check(kind: u8, data: &[u8]) -> Vec<u8> {
55    let sum: u32 = data.iter().map(|&b| b as u32).sum();
56    match kind {
57        2 => vec![tochar(((sum >> 6) & 63) as u8), tochar((sum & 63) as u8)],
58        3 => {
59            let crc = crc16_kermit(data);
60            vec![tochar(((crc >> 12) & 0x0F) as u8), tochar(((crc >> 6) & 0x3F) as u8), tochar((crc & 0x3F) as u8)]
61        }
62        _ => vec![tochar(((sum + ((sum & 192) >> 6)) & 63) as u8)],
63    }
64}
65
66/// CRC-16/KERMIT: CCITT, reflected (0x8408), starting at 0.
67pub fn crc16_kermit(data: &[u8]) -> u16 {
68    let mut crc: u16 = 0;
69    for &b in data {
70        crc ^= b as u16;
71        for _ in 0..8 {
72            crc = if crc & 1 != 0 { (crc >> 1) ^ 0x8408 } else { crc >> 1 };
73        }
74    }
75    crc
76}
77
78/// Where a Send-Init packet starts in `data`, if there is one: what a BBS's
79/// Kermit sends when told to send a file.
80pub fn find_send_init(data: &[u8]) -> Option<usize> {
81    data.windows(4).position(|w| w[0] == SOH && (b' '..=b'~').contains(&w[1]) && w[2] == b' ' && w[3] == b'S')
82}
83
84/// The parameters one side announces in its Send-Init.
85#[derive(Clone, Debug)]
86struct Params {
87    /// The longest packet it can take, short (up to 94) and long.
88    maxl: usize,
89    maxlx: Option<usize>,
90    /// Seconds before it wants to be timed out.
91    time: u8,
92    npad: u8,
93    padc: u8,
94    eol: u8,
95    qctl: u8,
96    /// 'Y' (will if asked), 'N' (won't), or the prefix it wants.
97    qbin: u8,
98    chkt: u8,
99    /// The repeat prefix, or ' ' for none.
100    rept: u8,
101    capas: u8,
102}
103
104impl Params {
105    fn ours() -> Params {
106        Params {
107            maxl: 94,
108            maxlx: Some(MAX_LONG),
109            time: TIMEOUT.as_secs() as u8,
110            npad: 0,
111            padc: 0,
112            eol: CR,
113            qctl: b'#',
114            qbin: b'Y',
115            chkt: 3,
116            rept: b'~',
117            capas: CAP_LONG | CAP_ATTRS,
118        }
119    }
120
121    fn encode(&self) -> Vec<u8> {
122        let maxlx = self.maxlx.unwrap_or(0);
123        vec![
124            tochar(self.maxl as u8),
125            tochar(self.time),
126            tochar(self.npad),
127            ctl(self.padc),
128            tochar(self.eol),
129            self.qctl,
130            self.qbin,
131            b'0' + self.chkt,
132            self.rept,
133            tochar(self.capas),
134            tochar(1),
135            tochar((maxlx / 95) as u8),
136            tochar((maxlx % 95) as u8),
137        ]
138    }
139
140    /// Read the other side's; fields it leaves out take their defaults.
141    fn decode(d: &[u8]) -> Params {
142        let at = |i: usize| d.get(i).copied();
143        let mut p = Params {
144            maxl: at(0).map_or(80, |c| unchar(c).clamp(10, 94) as usize),
145            maxlx: None,
146            time: at(1).map_or(5, unchar),
147            npad: at(2).map_or(0, unchar),
148            padc: at(3).map_or(0, ctl),
149            eol: at(4).map(unchar).filter(|&c| c > 0).unwrap_or(CR),
150            qctl: at(5).filter(|&c| c != b' ').unwrap_or(b'#'),
151            qbin: at(6).filter(|&c| c != b' ').unwrap_or(b'N'),
152            chkt: at(7).map(|c| c.wrapping_sub(b'0')).filter(|c| (1..=3).contains(c)).unwrap_or(1),
153            rept: at(8).unwrap_or(b' '),
154            capas: 0,
155        };
156        // Capability bytes go on while their low bit is set.
157        let mut i = 9;
158        while let Some(c) = at(i) {
159            if i == 9 {
160                p.capas = unchar(c);
161            }
162            i += 1;
163            if unchar(c) & 1 == 0 {
164                break;
165            }
166        }
167        // Then the window size, and the long packet length.
168        if p.capas & CAP_LONG != 0 {
169            let len = match (at(i + 1), at(i + 2)) {
170                (Some(a), Some(b)) => unchar(a) as usize * 95 + unchar(b) as usize,
171                _ => 500,
172            };
173            p.maxlx = Some(if len == 0 { 500 } else { len });
174        }
175        p
176    }
177}
178
179/// What both sides agreed on, from one side's point of view.
180#[derive(Clone, Debug)]
181struct Link {
182    /// The longest packet the other side takes from us.
183    send_max: usize,
184    long: bool,
185    chkt: u8,
186    /// The prefixes: ours for sending, theirs for reading what they send.
187    our_qctl: u8,
188    their_qctl: u8,
189    qbin: Option<u8>,
190    rept: Option<u8>,
191    attrs: bool,
192    eol: u8,
193    npad: u8,
194    padc: u8,
195}
196
197impl Link {
198    /// Before the Send-Init exchange: the basics.
199    fn initial() -> Link {
200        Link {
201            send_max: 80,
202            long: false,
203            chkt: 1,
204            our_qctl: b'#',
205            their_qctl: b'#',
206            qbin: None,
207            rept: None,
208            attrs: false,
209            eol: CR,
210            npad: 0,
211            padc: 0,
212        }
213    }
214
215    fn agree(ours: &Params, theirs: &Params) -> Link {
216        let long = ours.capas & theirs.capas & CAP_LONG != 0;
217        let qbin = match (ours.qbin, theirs.qbin) {
218            (b'Y', c) | (c, b'Y') if c != b'Y' && c != b'N' && is_prefix(c) => Some(c),
219            (a, b) if a == b && is_prefix(a) => Some(a),
220            _ => None,
221        };
222        let rept = (ours.rept == theirs.rept && is_prefix(ours.rept)).then_some(ours.rept);
223        Link {
224            send_max: if long { theirs.maxlx.unwrap_or(500).min(MAX_LONG) } else { theirs.maxl },
225            long,
226            chkt: if ours.chkt == theirs.chkt { ours.chkt } else { 1 },
227            our_qctl: ours.qctl,
228            their_qctl: theirs.qctl,
229            qbin,
230            rept,
231            attrs: ours.capas & theirs.capas & CAP_ATTRS != 0,
232            eol: theirs.eol,
233            npad: theirs.npad,
234            padc: theirs.padc,
235        }
236    }
237
238    /// A packet on the wire. Send-Init packets and their ACKs are always
239    /// checked with type 1.
240    fn packet(&self, seq: u8, kind: u8, data: &[u8], chkt: u8) -> Vec<u8> {
241        let chk_len = chkt as usize;
242        let mut body = Vec::with_capacity(data.len() + 10);
243        let short_len = data.len() + 2 + chk_len;
244        if short_len <= 94 {
245            body.push(tochar(short_len as u8));
246            body.push(tochar(seq));
247            body.push(kind);
248        } else {
249            let ext = data.len() + chk_len;
250            body.extend_from_slice(&[tochar(0), tochar(seq), kind, tochar((ext / 95) as u8), tochar((ext % 95) as u8)]);
251            body.extend(block_check(1, &body));
252        }
253        body.extend_from_slice(data);
254        let chk = block_check(chkt, &body);
255        let mut out = vec![self.padc; self.npad as usize];
256        out.push(SOH);
257        out.extend(body);
258        out.extend(chk);
259        out.push(self.eol);
260        out
261    }
262
263    /// How much data fits in one packet for the other side.
264    fn room(&self) -> usize {
265        if self.long { self.send_max - self.chkt as usize - 7 } else { self.send_max - 2 - self.chkt as usize }
266    }
267
268    /// Encode as much of `src` as fits in `room` bytes; returns the encoded
269    /// data and how many bytes of `src` it holds.
270    fn encode(&self, src: &[u8], room: usize) -> (Vec<u8>, usize) {
271        let mut out = Vec::with_capacity(room);
272        let mut i = 0;
273        while i < src.len() {
274            let c = src[i];
275            let mut run = 1;
276            if self.rept.is_some() {
277                while i + run < src.len() && src[i + run] == c && run < 94 {
278                    run += 1;
279                }
280            }
281            let one = self.encode_byte(c);
282            let unit = match self.rept {
283                Some(r) if run >= 3 => {
284                    let mut u = vec![r, tochar(run as u8)];
285                    u.extend_from_slice(&one);
286                    u
287                }
288                _ => {
289                    run = 1;
290                    one
291                }
292            };
293            if out.len() + unit.len() > room {
294                break;
295            }
296            out.extend(unit);
297            i += run;
298        }
299        (out, i)
300    }
301
302    fn encode_byte(&self, mut c: u8) -> Vec<u8> {
303        let mut out = Vec::with_capacity(3);
304        if let Some(q) = self.qbin
305            && c & 0x80 != 0
306        {
307            out.push(q);
308            c &= 0x7F;
309        }
310        let low = c & 0x7F;
311        if low < 32 || low == 127 {
312            out.extend_from_slice(&[self.our_qctl, ctl(c)]);
313        } else if low == self.our_qctl || Some(low) == self.qbin || Some(low) == self.rept {
314            out.extend_from_slice(&[self.our_qctl, c]);
315        } else {
316            out.push(c);
317        }
318        out
319    }
320
321    /// Decode a packet's data from the other side.
322    fn decode(&self, data: &[u8]) -> Vec<u8> {
323        let mut out = Vec::with_capacity(data.len());
324        let mut i = 0;
325        while i < data.len() {
326            let mut count = 1;
327            if let Some(r) = self.rept
328                && data[i] == r
329                && i + 1 < data.len()
330            {
331                count = unchar(data[i + 1]) as usize;
332                i += 2;
333            }
334            let mut high = 0;
335            if let Some(q) = self.qbin
336                && data.get(i) == Some(&q)
337            {
338                high = 0x80;
339                i += 1;
340            }
341            let Some(&b) = data.get(i) else { break };
342            i += 1;
343            let c = if b == self.their_qctl {
344                let Some(&q) = data.get(i) else { break };
345                i += 1;
346                if (0x3F..=0x5F).contains(&(q & 0x7F)) { ctl(q) } else { q }
347            } else {
348                b
349            };
350            out.extend(std::iter::repeat_n(c | high, count));
351        }
352        out
353    }
354}
355
356/// A prefix character Kermit allows (`!` to `>`, or `` ` `` to `~`).
357fn is_prefix(c: u8) -> bool {
358    (33..=62).contains(&c) || (96..=126).contains(&c)
359}
360
361/// A packet read from the other side.
362#[derive(Debug, PartialEq, Eq)]
363struct Packet {
364    seq: u8,
365    kind: u8,
366    data: Vec<u8>,
367}
368
369/// Take the next packet from `buf`: Ok(None) until a whole one is there,
370/// Err with its sequence number for a damaged one. Noise before it goes.
371fn read_packet(buf: &mut Vec<u8>, chkt: u8) -> std::result::Result<Option<Packet>, Option<u8>> {
372    let Some(start) = buf.iter().position(|&b| b == SOH) else {
373        buf.clear();
374        return Ok(None);
375    };
376    buf.drain(..start);
377    if buf.len() < 4 {
378        return Ok(None);
379    }
380    let len = unchar(buf[1]) as usize;
381    let (seq, kind) = (unchar(buf[2]), buf[3]);
382    // Send-Init packets and ACKs to them are type 1.
383    let chk_len = if kind == b'S' { 1 } else { chkt as usize };
384    let (header, total) = if len == 0 {
385        if buf.len() < 7 {
386            return Ok(None);
387        }
388        if block_check(1, &buf[1..6]) != buf[6..7] {
389            buf.drain(..1);
390            return Err(Some(seq & 63));
391        }
392        (7, 7 + unchar(buf[4]) as usize * 95 + unchar(buf[5]) as usize)
393    } else if len < 3 {
394        buf.drain(..1);
395        return Err(None);
396    } else {
397        (4, 2 + len)
398    };
399    if total > MAX_LONG + 16 || total < header + chk_len {
400        buf.drain(..1);
401        return Err(None);
402    }
403    if buf.len() < total {
404        return Ok(None);
405    }
406    let (body, chk) = buf[1..total].split_at(total - 1 - chk_len);
407    if block_check(chk_len as u8, body) != chk {
408        // Only its mark goes: a good packet may follow inside it.
409        buf.drain(..1);
410        return Err(Some(seq & 63));
411    }
412    let raw: Vec<u8> = buf.drain(..total).collect();
413    Ok(Some(Packet { seq: seq & 63, kind, data: raw[header..total - chk_len].to_vec() }))
414}
415
416#[derive(Clone, Copy, Debug, PartialEq, Eq)]
417enum State {
418    /// Receiving: waiting for the Send-Init. Sending: waiting for its ACK.
419    Init,
420    /// Receiving: waiting for a file header or the end of the batch.
421    File,
422    /// Receiving: the file's attributes or data. Sending: data under way.
423    Data,
424    /// Sending: the header (F), attributes (A), end of file (Z), or end of
425    /// batch (B) is out, waiting for its ACK.
426    Header,
427    Attrs,
428    Eof,
429    Break,
430    Done,
431}
432
433enum Role {
434    Receive { dir: PathBuf, file: Option<(BufWriter<File>, PathBuf)>, saved: Vec<PathBuf> },
435    Send { queue: VecDeque<PathBuf>, file: Option<(File, PathBuf)>, sent: Vec<PathBuf>, eof: bool, skip: bool },
436}
437
438/// A Kermit transfer, either way.
439pub struct Kermit {
440    role: Role,
441    state: State,
442    link: Link,
443    seq: u8,
444    /// The last packet sent, for resending.
445    last: Vec<u8>,
446    /// Read but not yet sent (sending), as packets go out.
447    pending_data: Vec<u8>,
448    retries: u32,
449    last_heard: Instant,
450    pub(crate) c: Common,
451}
452
453impl Kermit {
454    /// Receive files into `dir`. Feed it the BBS's Send-Init packet (or
455    /// wait for it).
456    pub fn receive(dir: PathBuf) -> Kermit {
457        Kermit::new(Role::Receive { dir, file: None, saved: Vec::new() }, Direction::Download)
458    }
459
460    /// Send files to a Kermit that is ready to receive.
461    pub fn send(paths: Vec<PathBuf>) -> Result<Kermit> {
462        if paths.is_empty() {
463            bail!("no files to upload");
464        }
465        for p in &paths {
466            if !p.is_file() {
467                bail!("not a file: {}", p.display());
468            }
469        }
470        let mut k = Kermit::new(
471            Role::Send { queue: paths.into(), file: None, sent: Vec::new(), eof: false, skip: false },
472            Direction::Upload,
473        );
474        k.c.progress.files_queued = k.queued();
475        Ok(k)
476    }
477
478    fn new(role: Role, direction: Direction) -> Kermit {
479        Kermit {
480            role,
481            state: State::Init,
482            link: Link::initial(),
483            seq: 0,
484            last: Vec::new(),
485            pending_data: Vec::new(),
486            retries: 0,
487            last_heard: Instant::now(),
488            c: Common::new(direction),
489        }
490    }
491
492    pub(crate) fn progress(&self) -> &Progress {
493        &self.c.progress
494    }
495
496    pub fn enqueue(&mut self, path: PathBuf) -> Result<()> {
497        match &mut self.role {
498            Role::Send { queue, .. } => {
499                queue.push_back(path);
500                self.c.progress.files_queued = self.queued();
501                Ok(())
502            }
503            Role::Receive { .. } => bail!("a download is in progress"),
504        }
505    }
506
507    fn queued(&self) -> usize {
508        match &self.role {
509            Role::Send { queue, .. } => queue.len(),
510            Role::Receive { .. } => 0,
511        }
512    }
513
514    fn sending(&self) -> bool {
515        matches!(self.role, Role::Send { .. })
516    }
517
518    /// The check type in use for the packets of the current state.
519    fn chkt(&self) -> u8 {
520        if self.state == State::Init { 1 } else { self.link.chkt }
521    }
522
523    fn put(&mut self, kind: u8, data: &[u8], out: &mut Vec<u8>) {
524        let chkt = if kind == b'S' || self.state == State::Init { 1 } else { self.link.chkt };
525        self.last = self.link.packet(self.seq, kind, data, chkt);
526        out.extend_from_slice(&self.last);
527        self.retries = 0;
528        self.last_heard = Instant::now();
529    }
530
531    /// Acknowledge packet `seq` (receiving).
532    fn ack(&mut self, seq: u8, data: &[u8], out: &mut Vec<u8>) {
533        self.seq = seq;
534        self.put(b'Y', data, out);
535    }
536
537    fn fail(&mut self, msg: impl Into<String>, out: &mut Vec<u8>) {
538        if self.c.outcome.is_some() {
539            return;
540        }
541        let msg = msg.into();
542        let data = self.link.encode(msg.as_bytes(), 80).0;
543        let p = self.link.packet(self.seq, b'E', &data, self.chkt());
544        out.extend_from_slice(&p);
545        self.drop_partial();
546        self.c.outcome = Some(Outcome::Failed(msg));
547    }
548
549    fn drop_partial(&mut self) {
550        if let Role::Receive { file, .. } = &mut self.role
551            && let Some((_, path)) = file.take()
552        {
553            let _ = fs::remove_file(path);
554        }
555    }
556
557    pub(crate) fn cancel(&mut self, out: &mut Vec<u8>) {
558        self.c.cancelled = true;
559        if self.c.outcome.is_none() {
560            let p = self.link.packet(self.seq, b'E', b"Cancelled by the user", self.chkt());
561            out.extend_from_slice(&p);
562        }
563        self.drop_partial();
564        self.c.outcome.get_or_insert(Outcome::Cancelled);
565    }
566
567    /// The sender starts by itself: call once, before the first feed.
568    pub fn start(&mut self, out: &mut Vec<u8>) {
569        if self.sending() && self.state == State::Init && self.last.is_empty() {
570            let data = Params::ours().encode();
571            self.put(b'S', &data, out);
572        }
573    }
574
575    pub(crate) fn tick(&mut self, now: Instant, out: &mut Vec<u8>) {
576        if self.c.outcome.is_some() || self.state == State::Done {
577            return;
578        }
579        if now.duration_since(self.last_heard) < TIMEOUT {
580            return;
581        }
582        self.last_heard = now;
583        self.retries += 1;
584        let limit = if !self.sending() && self.state == State::Init { START_TRIES } else { MAX_RETRIES };
585        if self.retries > limit {
586            return self.fail("the other side stopped answering", out);
587        }
588        if self.sending() || !self.last.is_empty() {
589            out.extend_from_slice(&self.last);
590        } else {
591            // A receiver that has heard nothing NAKs packet 0.
592            let p = self.link.packet(0, b'N', &[], 1);
593            out.extend_from_slice(&p);
594        }
595    }
596
597    pub(crate) fn pump(&mut self, out: &mut Vec<u8>) {
598        while self.c.outcome.is_none() && !self.c.pending.is_empty() {
599            let chkt = self.chkt();
600            match read_packet(&mut self.c.pending, chkt) {
601                Ok(None) => break,
602                Ok(Some(p)) => {
603                    self.last_heard = Instant::now();
604                    if p.kind == b'E' {
605                        let msg = String::from_utf8_lossy(&self.link.decode(&p.data)).into_owned();
606                        self.drop_partial();
607                        self.c.outcome = Some(Outcome::Failed(format!("the other side stopped: {msg}")));
608                        break;
609                    }
610                    if self.sending() { self.sender_got(p, out) } else { self.receiver_got(p, out) }
611                }
612                Err(seq) => {
613                    // Damaged: ask again (receiving), or resend (sending).
614                    if self.sending() {
615                        out.extend_from_slice(&self.last);
616                    } else {
617                        let p = self.link.packet(seq.unwrap_or(self.seq.wrapping_add(1) & 63), b'N', &[], chkt);
618                        out.extend_from_slice(&p);
619                    }
620                }
621            }
622        }
623    }
624
625    fn receiver_got(&mut self, p: Packet, out: &mut Vec<u8>) {
626        let expected = if self.state == State::Init { 0 } else { (self.seq + 1) & 63 };
627        if p.seq != expected {
628            // A repeat of the one just acknowledged: acknowledge it again.
629            if p.seq == self.seq && !self.last.is_empty() {
630                out.extend_from_slice(&self.last);
631            }
632            return;
633        }
634        match (self.state, p.kind) {
635            (State::Init, b'S') => {
636                let theirs = Params::decode(&p.data);
637                let ours = Params::ours();
638                self.ack(p.seq, &ours.encode(), out);
639                self.link = Link::agree(&ours, &theirs);
640                self.state = State::File;
641            }
642            (State::File, b'F') => {
643                let wire = self.link.decode(&p.data);
644                let name = transfer::sanitize_file_name(&wire);
645                match self.open(&name) {
646                    Ok(path) => {
647                        let used = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or(name);
648                        let data = self.link.encode(used.as_bytes(), self.link.room()).0;
649                        self.ack(p.seq, &data, out);
650                        self.state = State::Data;
651                    }
652                    Err(e) => self.fail(format!("saving {name}: {e}"), out),
653                }
654            }
655            // A text header (`X`), to show rather than save: not shown.
656            (State::File, b'X') => self.ack(p.seq, &[], out),
657            (State::File, b'B') => {
658                self.ack(p.seq, &[], out);
659                self.state = State::Done;
660                if let Role::Receive { saved, .. } = &self.role {
661                    self.c.outcome = Some(Outcome::Completed(saved.clone()));
662                }
663            }
664            (State::Data, b'A') => {
665                self.c.progress.file_size = attribute_size(&p.data);
666                self.ack(p.seq, b"Y", out);
667            }
668            (State::Data, b'D') => {
669                let data = self.link.decode(&p.data);
670                if let Role::Receive { file: Some((f, path)), .. } = &mut self.role
671                    && let Err(e) = f.write_all(&data)
672                {
673                    let msg = format!("writing {}: {e}", path.display());
674                    return self.fail(msg, out);
675                }
676                self.c.progress.file_bytes += data.len() as u64;
677                self.ack(p.seq, &[], out);
678            }
679            (State::Data, b'Z') => {
680                // "D" in the data: the sender discarded the file.
681                let discard = self.link.decode(&p.data).first() == Some(&b'D');
682                if let Role::Receive { file, saved, .. } = &mut self.role
683                    && let Some((mut f, path)) = file.take()
684                {
685                    if discard {
686                        drop(f);
687                        let _ = fs::remove_file(&path);
688                    } else if let Err(e) = f.flush() {
689                        let msg = format!("writing {}: {e}", path.display());
690                        return self.fail(msg, out);
691                    } else {
692                        saved.push(path);
693                        self.c.progress.files_done += 1;
694                    }
695                }
696                self.ack(p.seq, &[], out);
697                self.state = State::File;
698            }
699            (_, kind) => self.fail(format!("unexpected Kermit packet '{}'", kind as char), out),
700        }
701    }
702
703    fn open(&mut self, name: &str) -> std::io::Result<PathBuf> {
704        let Role::Receive { dir, file, .. } = &mut self.role else { unreachable!("receiving") };
705        fs::create_dir_all(&*dir)?;
706        let path = transfer::unique_path(dir, name)?;
707        *file = Some((BufWriter::new(File::create(&path)?), path.clone()));
708        let p = &mut self.c.progress;
709        p.file_name = name.to_string();
710        p.file_size = None;
711        p.file_bytes = 0;
712        p.file_started = Instant::now();
713        Ok(path)
714    }
715
716    fn sender_got(&mut self, p: Packet, out: &mut Vec<u8>) {
717        // A NAK for the next packet acknowledges this one.
718        let acked = match p.kind {
719            b'Y' => p.seq == self.seq,
720            b'N' => p.seq == (self.seq + 1) & 63,
721            _ => false,
722        };
723        if !acked {
724            if p.kind == b'N' && p.seq == self.seq {
725                self.retries += 1;
726                if self.retries > MAX_RETRIES {
727                    return self.fail("too many retries", out);
728                }
729                out.extend_from_slice(&self.last);
730            }
731            return;
732        }
733        let reply = if p.kind == b'Y' { p.data } else { Vec::new() };
734        self.seq = (self.seq + 1) & 63;
735        match self.state {
736            State::Init => {
737                self.link = Link::agree(&Params::ours(), &Params::decode(&reply));
738                self.next_file(out);
739            }
740            State::Header if self.link.attrs => {
741                let size = self.file_size();
742                let digits = size.to_string();
743                let mut data = vec![b'1', tochar(digits.len() as u8)];
744                data.extend_from_slice(digits.as_bytes());
745                self.state = State::Attrs;
746                self.put(b'A', &data, out);
747            }
748            State::Header => self.start_data(out),
749            State::Attrs => {
750                // "N" in the reply: the receiver does not want the file.
751                if reply.first() == Some(&b'N') {
752                    self.skip_file(out);
753                } else {
754                    self.start_data(out);
755                }
756            }
757            State::Data => {
758                // "X" stops this file, "Z" the whole batch.
759                match reply.first() {
760                    Some(b'X') => self.skip_file(out),
761                    Some(b'Z') => {
762                        if let Role::Send { queue, .. } = &mut self.role {
763                            queue.clear();
764                        }
765                        self.skip_file(out);
766                    }
767                    _ => self.send_data(out),
768                }
769            }
770            State::Eof => {
771                if let Role::Send { file, sent, skip, .. } = &mut self.role
772                    && let Some((_, path)) = file.take()
773                {
774                    if !*skip {
775                        sent.push(path);
776                        self.c.progress.files_done += 1;
777                    }
778                    *skip = false;
779                }
780                self.next_file(out);
781            }
782            State::Break => {
783                self.state = State::Done;
784                if let Role::Send { sent, .. } = &self.role {
785                    self.c.outcome = Some(Outcome::Completed(sent.clone()));
786                }
787            }
788            State::File | State::Done => {}
789        }
790    }
791
792    fn file_size(&self) -> u64 {
793        match &self.role {
794            Role::Send { file: Some((f, _)), .. } => f.metadata().map(|m| m.len()).unwrap_or(0),
795            _ => 0,
796        }
797    }
798
799    /// Open the next file and send its header, or end the batch.
800    fn next_file(&mut self, out: &mut Vec<u8>) {
801        let Role::Send { queue, file, eof, .. } = &mut self.role else { return };
802        let Some(path) = queue.pop_front() else {
803            self.state = State::Break;
804            return self.put(b'B', &[], out);
805        };
806        let f = match File::open(&path) {
807            Ok(f) => f,
808            Err(e) => return self.fail(format!("reading {}: {e}", path.display()), out),
809        };
810        let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| "file".into());
811        let size = f.metadata().map(|m| m.len()).ok();
812        *file = Some((f, path));
813        *eof = false;
814        self.pending_data.clear();
815        let p = &mut self.c.progress;
816        p.file_name = name.clone();
817        p.file_size = size;
818        p.file_bytes = 0;
819        p.file_started = Instant::now();
820        p.files_queued = queue.len();
821        let data = self.link.encode(name.as_bytes(), self.link.room()).0;
822        self.state = State::Header;
823        self.put(b'F', &data, out);
824    }
825
826    fn start_data(&mut self, out: &mut Vec<u8>) {
827        self.state = State::Data;
828        self.send_data(out);
829    }
830
831    /// Send the next data packet, or the end of the file.
832    fn send_data(&mut self, out: &mut Vec<u8>) {
833        let room = self.link.room();
834        // Keep enough read ahead to fill a packet even with no compression.
835        if let Role::Send { file: Some((f, path)), eof, .. } = &mut self.role
836            && !*eof
837            && self.pending_data.len() < room * 2
838        {
839            let mut buf = vec![0u8; room * 4];
840            match f.read(&mut buf) {
841                Ok(0) => *eof = true,
842                Ok(n) => self.pending_data.extend_from_slice(&buf[..n]),
843                Err(e) => {
844                    let msg = format!("reading {}: {e}", path.display());
845                    return self.fail(msg, out);
846                }
847            }
848        }
849        if self.pending_data.is_empty() {
850            let at_end = matches!(self.role, Role::Send { eof: true, .. });
851            if !at_end {
852                // The read above got nothing yet but the file isn't done.
853                return self.send_data(out);
854            }
855            self.state = State::Eof;
856            return self.put(b'Z', &[], out);
857        }
858        let (data, used) = self.link.encode(&self.pending_data, room);
859        self.pending_data.drain(..used);
860        self.c.progress.file_bytes += used as u64;
861        self.put(b'D', &data, out);
862    }
863
864    /// Stop sending this file: an end of file that says "discard".
865    fn skip_file(&mut self, out: &mut Vec<u8>) {
866        if let Role::Send { skip, .. } = &mut self.role {
867            *skip = true;
868        }
869        self.pending_data.clear();
870        self.state = State::Eof;
871        self.put(b'Z', b"D", out);
872    }
873}
874
875/// The file size from an attribute packet: `1` (bytes) or `!` (KiB).
876fn attribute_size(data: &[u8]) -> Option<u64> {
877    let mut i = 0;
878    let mut size = None;
879    while i + 1 < data.len() {
880        let (tag, len) = (data[i], unchar(data[i + 1]) as usize);
881        let value = data.get(i + 2..i + 2 + len)?;
882        let n = std::str::from_utf8(value).ok().and_then(|s| s.trim().parse::<u64>().ok());
883        match tag {
884            b'1' => size = n.or(size),
885            b'!' if size.is_none() => size = n.map(|k| k * 1024),
886            _ => {}
887        }
888        i += 2 + len;
889    }
890    size
891}
892
893#[cfg(test)]
894mod tests {
895    use super::*;
896
897    fn temp_dir(name: &str) -> PathBuf {
898        let d = std::env::temp_dir().join(format!("badterm-kermit-{name}-{}", std::process::id()));
899        let _ = fs::remove_dir_all(&d);
900        fs::create_dir_all(&d).unwrap();
901        d
902    }
903
904    /// Run a sender against a receiver in memory, optionally damaging
905    /// every `damage`-th chunk on the way to the receiver.
906    fn run(tx: &mut Kermit, rx: &mut Kermit, damage: Option<usize>) {
907        let (mut to_rx, mut to_tx) = (Vec::new(), Vec::new());
908        tx.start(&mut to_rx);
909        for n in 0..200_000 {
910            if tx.c.outcome.is_some() && rx.c.outcome.is_some() {
911                return;
912            }
913            let mut data = std::mem::take(&mut to_rx);
914            if damage.is_some_and(|d| n % d == d - 1) && data.len() > 8 {
915                let i = data.len() / 2;
916                data[i] ^= 0x01;
917            }
918            rx.c.input(&data);
919            rx.pump(&mut to_tx);
920            let data = std::mem::take(&mut to_tx);
921            tx.c.input(&data);
922            tx.pump(&mut to_rx);
923            if to_rx.is_empty() && to_tx.is_empty() {
924                // Lost to damage: what a timeout would do.
925                tx.last_heard = Instant::now() - TIMEOUT;
926                tx.tick(Instant::now(), &mut to_rx);
927            }
928        }
929        panic!("transfer did not finish");
930    }
931
932    #[test]
933    fn checks_and_prefixes() {
934        assert_eq!(crc16_kermit(b"123456789"), 0x2189);
935        let link = Link { qbin: Some(b'&'), rept: Some(b'~'), ..Link::agree(&Params::ours(), &Params::ours()) };
936        let src = b"ab\x01#\x80&~cccccc\xff\r\n".to_vec();
937        let (enc, used) = link.encode(&src, 1000);
938        assert_eq!(used, src.len());
939        assert!(enc.iter().all(|&b| (32..127).contains(&b)), "{enc:?}");
940        assert_eq!(link.decode(&enc), src);
941        // A short room ends on a whole unit.
942        let (part, n) = link.encode(&src, 5);
943        assert_eq!(link.decode(&part), src[..n]);
944        assert_eq!(attribute_size(b"#(20251031!\"12"), Some(12 * 1024));
945        assert_eq!(attribute_size(b"1#300!\"12"), Some(300));
946        assert_eq!(find_send_init(b"junk\x01) S~"), Some(4));
947    }
948
949    #[test]
950    fn round_trip_two_files() {
951        let (src, dst) = (temp_dir("src"), temp_dir("dst"));
952        let mut big = Vec::new();
953        for i in 0..150_000u32 {
954            big.push((i.wrapping_mul(2_654_435_761) >> 13) as u8);
955        }
956        big.extend_from_slice(&[0; 500]);
957        fs::write(src.join("big.bin"), &big).unwrap();
958        fs::write(src.join("empty.txt"), b"").unwrap();
959        let mut tx = Kermit::send(vec![src.join("big.bin"), src.join("empty.txt")]).unwrap();
960        let mut rx = Kermit::receive(dst.clone());
961        run(&mut tx, &mut rx, None);
962        assert_eq!(tx.c.outcome, Some(Outcome::Completed(vec![src.join("big.bin"), src.join("empty.txt")])));
963        assert_eq!(rx.c.outcome, Some(Outcome::Completed(vec![dst.join("big.bin"), dst.join("empty.txt")])));
964        assert_eq!(fs::read(dst.join("big.bin")).unwrap(), big);
965        assert_eq!(fs::read(dst.join("empty.txt")).unwrap(), b"");
966        // Long packets with CRCs were agreed.
967        assert!(tx.link.long && tx.link.chkt == 3 && tx.link.rept == Some(b'~'));
968    }
969
970    #[test]
971    fn damaged_packets_are_resent() {
972        let (src, dst) = (temp_dir("dsrc"), temp_dir("ddst"));
973        let data: Vec<u8> = (0..40_000u32).map(|i| (i % 251) as u8).collect();
974        fs::write(src.join("a.dat"), &data).unwrap();
975        let mut tx = Kermit::send(vec![src.join("a.dat")]).unwrap();
976        let mut rx = Kermit::receive(dst.clone());
977        run(&mut tx, &mut rx, Some(7));
978        assert!(matches!(rx.c.outcome, Some(Outcome::Completed(_))), "{:?}", rx.c.outcome);
979        assert_eq!(fs::read(dst.join("a.dat")).unwrap(), data);
980    }
981
982    #[test]
983    fn cancel_sends_an_error_packet() {
984        let mut rx = Kermit::receive(temp_dir("cancel"));
985        let mut out = Vec::new();
986        rx.cancel(&mut out);
987        assert_eq!(out[0], SOH);
988        assert_eq!(out[3], b'E');
989        assert_eq!(rx.c.outcome, Some(Outcome::Cancelled));
990    }
991}