1use 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;
28pub const MAX_LONG: usize = 4000;
30const TIMEOUT: Duration = Duration::from_secs(8);
32const MAX_RETRIES: u32 = 10;
33const START_TRIES: u32 = 15;
36
37const 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
53fn 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
66pub 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
78pub 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#[derive(Clone, Debug)]
86struct Params {
87 maxl: usize,
89 maxlx: Option<usize>,
90 time: u8,
92 npad: u8,
93 padc: u8,
94 eol: u8,
95 qctl: u8,
96 qbin: u8,
98 chkt: u8,
99 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 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 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 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#[derive(Clone, Debug)]
181struct Link {
182 send_max: usize,
184 long: bool,
185 chkt: u8,
186 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 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 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 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 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 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
356fn is_prefix(c: u8) -> bool {
358 (33..=62).contains(&c) || (96..=126).contains(&c)
359}
360
361#[derive(Debug, PartialEq, Eq)]
363struct Packet {
364 seq: u8,
365 kind: u8,
366 data: Vec<u8>,
367}
368
369fn 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 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 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 Init,
420 File,
422 Data,
424 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
438pub struct Kermit {
440 role: Role,
441 state: State,
442 link: Link,
443 seq: u8,
444 last: Vec<u8>,
446 pending_data: Vec<u8>,
448 retries: u32,
449 last_heard: Instant,
450 pub(crate) c: Common,
451}
452
453impl Kermit {
454 pub fn receive(dir: PathBuf) -> Kermit {
457 Kermit::new(Role::Receive { dir, file: None, saved: Vec::new() }, Direction::Download)
458 }
459
460 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 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 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 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 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 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 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 (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 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 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 if reply.first() == Some(&b'N') {
752 self.skip_file(out);
753 } else {
754 self.start_data(out);
755 }
756 }
757 State::Data => {
758 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 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 fn send_data(&mut self, out: &mut Vec<u8>) {
833 let room = self.link.room();
834 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 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 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
875fn 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 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 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 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 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}