1use std::collections::VecDeque;
10use std::fs::{self, File};
11use std::io::{BufWriter, Read, Write};
12use std::path::PathBuf;
13use std::time::{Duration, Instant, UNIX_EPOCH};
14
15use anyhow::{Result, bail};
16
17use crate::transfer::{self, Common, Direction, Outcome, Progress};
18
19const SOH: u8 = 0x01;
20const STX: u8 = 0x02;
21const EOT: u8 = 0x04;
22const ACK: u8 = 0x06;
23const NAK: u8 = 0x15;
24const CAN: u8 = 0x18;
25const SUB: u8 = 0x1A;
27
28const START_EVERY: Duration = Duration::from_secs(3);
30const START_TRIES: u32 = 20;
31const MAX_RETRIES: u32 = 10;
33const STREAM_BURST: usize = 64;
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum XyProtocol {
38 Ymodem,
39 YmodemG,
41 Xmodem1k,
42 XmodemCrc,
43 Xmodem,
45}
46
47impl XyProtocol {
48 pub const ALL: [XyProtocol; 5] =
49 [XyProtocol::Ymodem, XyProtocol::YmodemG, XyProtocol::Xmodem1k, XyProtocol::XmodemCrc, XyProtocol::Xmodem];
50
51 pub fn name(self) -> &'static str {
52 match self {
53 XyProtocol::Ymodem => "YMODEM",
54 XyProtocol::YmodemG => "YMODEM-G",
55 XyProtocol::Xmodem1k => "XMODEM-1K",
56 XyProtocol::XmodemCrc => "XMODEM-CRC",
57 XyProtocol::Xmodem => "XMODEM",
58 }
59 }
60
61 pub fn batch(self) -> bool {
63 matches!(self, XyProtocol::Ymodem | XyProtocol::YmodemG)
64 }
65
66 fn crc(self) -> bool {
67 self != XyProtocol::Xmodem
68 }
69
70 fn streaming(self) -> bool {
71 self == XyProtocol::YmodemG
72 }
73
74 fn block_size(self) -> usize {
75 match self {
76 XyProtocol::Xmodem | XyProtocol::XmodemCrc => 128,
77 _ => 1024,
78 }
79 }
80
81 fn start_char(self) -> u8 {
83 match self {
84 XyProtocol::Xmodem => NAK,
85 XyProtocol::YmodemG => b'G',
86 _ => b'C',
87 }
88 }
89}
90
91pub fn crc16(data: &[u8]) -> u16 {
93 let mut crc: u16 = 0;
94 for &b in data {
95 crc ^= (b as u16) << 8;
96 for _ in 0..8 {
97 crc = if crc & 0x8000 != 0 { (crc << 1) ^ 0x1021 } else { crc << 1 };
98 }
99 }
100 crc
101}
102
103fn block(num: u8, data: &[u8], crc: bool) -> Vec<u8> {
106 let mut out = Vec::with_capacity(data.len() + 5);
107 out.push(if data.len() == 1024 { STX } else { SOH });
108 out.push(num);
109 out.push(!num);
110 out.extend_from_slice(data);
111 if crc {
112 out.extend_from_slice(&crc16(data).to_be_bytes());
113 } else {
114 out.push(data.iter().fold(0u8, |a, &b| a.wrapping_add(b)));
115 }
116 out
117}
118
119#[derive(Clone, Copy, Debug, PartialEq, Eq)]
120enum RState {
121 Header,
123 Data,
124 Done,
125}
126
127pub struct XyReceive {
129 proto: XyProtocol,
130 dir: PathBuf,
131 name: String,
133 state: RState,
134 expected: u8,
135 starting: bool,
137 last_ask: Option<Instant>,
138 asks: u32,
139 retries: u32,
140 file: Option<(BufWriter<File>, PathBuf)>,
141 remaining: Option<u64>,
143 held: Vec<u8>,
145 eots: u32,
146 saved: Vec<PathBuf>,
147 pub(crate) c: Common,
148}
149
150impl XyReceive {
151 pub fn new(proto: XyProtocol, dir: PathBuf, name: Option<String>) -> XyReceive {
152 let name =
153 name.map(|n| transfer::sanitize_file_name(n.as_bytes())).unwrap_or_else(|| "xmodem-download".to_string());
154 XyReceive {
155 proto,
156 dir,
157 name,
158 state: if proto.batch() { RState::Header } else { RState::Data },
159 expected: 1,
160 starting: true,
161 last_ask: None,
162 asks: 0,
163 retries: 0,
164 file: None,
165 remaining: None,
166 held: Vec::new(),
167 eots: 0,
168 saved: Vec::new(),
169 c: Common::new(Direction::Download),
170 }
171 }
172
173 pub(crate) fn progress(&self) -> &Progress {
174 &self.c.progress
175 }
176
177 pub fn protocol(&self) -> XyProtocol {
178 self.proto
179 }
180
181 fn fail(&mut self, msg: impl Into<String>, out: &mut Vec<u8>) {
182 if self.c.outcome.is_none() {
183 out.extend_from_slice(transfer::CANCEL);
184 self.c.outcome = Some(Outcome::Failed(msg.into()));
185 }
186 self.drop_file();
187 }
188
189 fn drop_file(&mut self) {
191 if let Some((_, path)) = self.file.take() {
192 let _ = fs::remove_file(path);
193 }
194 }
195
196 fn ask(&mut self, out: &mut Vec<u8>) {
197 out.push(self.proto.start_char());
198 self.last_ask = Some(Instant::now());
199 self.starting = true;
200 }
201
202 pub(crate) fn tick(&mut self, now: Instant, out: &mut Vec<u8>) {
204 if self.c.outcome.is_some() || self.state == RState::Done {
205 return;
206 }
207 if self.starting {
208 if self.last_ask.is_none_or(|t| now.duration_since(t) >= START_EVERY) {
209 self.asks += 1;
210 if self.asks > START_TRIES {
211 return self.fail("the sender never started", out);
212 }
213 self.ask(out);
214 }
215 return;
216 }
217 if now.duration_since(self.c.last_input) >= transfer::TIMEOUT {
218 self.c.last_input = now;
219 self.retries += 1;
220 if self.retries > MAX_RETRIES || self.proto.streaming() {
221 return self.fail("the sender stopped", out);
222 }
223 out.push(NAK);
224 }
225 }
226
227 pub(crate) fn pump(&mut self, out: &mut Vec<u8>) {
228 if self.state == RState::Done && self.c.outcome.is_none() {
229 self.c.outcome = Some(Outcome::Completed(self.saved.clone()));
230 }
231 while self.c.outcome.is_none() && !self.c.pending.is_empty() {
232 let first = self.c.pending[0];
233 match first {
234 SOH | STX => {
235 let size = if first == SOH { 128 } else { 1024 };
236 let need = 3 + size + if self.proto.crc() { 2 } else { 1 };
237 if self.c.pending.len() < need {
238 return;
239 }
240 let raw: Vec<u8> = self.c.pending.drain(..need).collect();
241 let (num, inv, data) = (raw[1], raw[2], &raw[3..3 + size]);
242 let good = num == !inv
243 && if self.proto.crc() {
244 raw[3 + size..] == crc16(data).to_be_bytes()
245 } else {
246 raw[3 + size] == data.iter().fold(0u8, |a, &b| a.wrapping_add(b))
247 };
248 if !good {
249 if self.proto.streaming() {
250 return self.fail("a damaged block (YMODEM-G cannot resend)", out);
251 }
252 out.push(NAK);
253 continue;
254 }
255 self.retries = 0;
256 self.block(num, data.to_vec(), out);
257 }
258 EOT => {
259 self.c.pending.remove(0);
260 self.eot(out);
261 }
262 CAN => {
263 if self.c.pending.len() < 2 {
264 return;
265 }
266 if self.c.pending[1] == CAN {
267 self.drop_file();
268 self.c.aborted();
269 return;
270 }
271 self.c.pending.remove(0);
272 }
273 _ => {
275 self.c.pending.remove(0);
276 }
277 }
278 }
279 if self.state == RState::Done && self.c.outcome.is_none() {
280 self.c.outcome = Some(Outcome::Completed(self.saved.clone()));
281 }
282 }
283
284 fn block(&mut self, num: u8, data: Vec<u8>, out: &mut Vec<u8>) {
285 let streaming = self.proto.streaming();
286 match self.state {
287 RState::Header if num == 0 => {
288 self.starting = false;
289 out.push(ACK);
290 if data[0] == 0 {
292 self.state = RState::Done;
293 return;
294 }
295 let name_end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
296 let name = transfer::sanitize_file_name(&data[..name_end]);
297 let rest = &data[(name_end + 1).min(data.len())..];
298 let info = String::from_utf8_lossy(&rest[..rest.iter().position(|&b| b == 0).unwrap_or(rest.len())])
299 .into_owned();
300 let size = info.split_whitespace().next().and_then(|s| s.parse::<u64>().ok());
301 if let Err(e) = self.open(&name, size) {
302 return self.fail(e.to_string(), out);
303 }
304 self.state = RState::Data;
305 self.expected = 1;
306 self.ask(out);
308 }
309 RState::Header => {
310 if !streaming {
311 out.push(NAK);
312 }
313 }
314 RState::Data if num == 0 && self.proto.batch() && self.expected == 1 => {
316 out.push(ACK);
317 self.ask(out);
318 }
319 RState::Data if num == self.expected => {
320 if self.starting && !self.proto.batch() {
321 let name = self.name.clone();
322 if let Err(e) = self.open(&name, None) {
323 return self.fail(e.to_string(), out);
324 }
325 }
326 self.starting = false;
327 self.expected = self.expected.wrapping_add(1);
328 if let Err(e) = self.write(data) {
329 return self.fail(e, out);
330 }
331 if !streaming {
332 out.push(ACK);
333 }
334 }
335 RState::Data if num == self.expected.wrapping_sub(1) => {
336 if !streaming {
338 out.push(ACK);
339 }
340 }
341 RState::Data => self.fail(format!("blocks out of order (got {num}, wanted {})", self.expected), out),
342 RState::Done => {}
343 }
344 }
345
346 fn open(&mut self, name: &str, size: Option<u64>) -> std::io::Result<()> {
347 fs::create_dir_all(&self.dir)?;
348 let path = transfer::unique_path(&self.dir, name)?;
349 let f = File::create(&path)?;
350 self.file = Some((BufWriter::new(f), path));
351 self.remaining = size;
352 self.held.clear();
353 self.eots = 0;
354 let p = &mut self.c.progress;
355 p.file_name = name.to_string();
356 p.file_size = size;
357 p.file_bytes = 0;
358 p.file_started = Instant::now();
359 Ok(())
360 }
361
362 fn write(&mut self, mut data: Vec<u8>) -> Result<(), String> {
363 if let Some(left) = &mut self.remaining {
364 data.truncate((*left).min(data.len() as u64) as usize);
365 *left -= data.len() as u64;
366 } else {
367 std::mem::swap(&mut self.held, &mut data);
370 }
371 self.c.progress.file_bytes += data.len() as u64;
372 match &mut self.file {
373 Some((f, path)) => f.write_all(&data).map_err(|e| format!("writing {}: {e}", path.display())),
374 None => Ok(()),
375 }
376 }
377
378 fn eot(&mut self, out: &mut Vec<u8>) {
379 if self.state != RState::Data || self.file.is_none() && self.proto.batch() {
380 out.push(ACK);
383 return;
384 }
385 if self.file.is_none() {
386 let name = self.name.clone();
388 if let Err(e) = self.open(&name, None) {
389 return self.fail(e.to_string(), out);
390 }
391 self.starting = false;
392 }
393 if !self.proto.streaming() && self.eots == 0 {
395 self.eots = 1;
396 out.push(NAK);
397 return;
398 }
399 out.push(ACK);
400 if let Err(e) = self.finish_file() {
401 return self.fail(e, out);
402 }
403 if self.proto.batch() {
404 self.state = RState::Header;
405 self.ask(out);
406 } else {
407 self.state = RState::Done;
408 }
409 }
410
411 fn finish_file(&mut self) -> Result<(), String> {
412 let mut last = std::mem::take(&mut self.held);
413 while last.last() == Some(&SUB) {
414 last.pop();
415 }
416 self.c.progress.file_bytes += last.len() as u64;
417 if let Some((mut f, path)) = self.file.take() {
418 f.write_all(&last).and_then(|()| f.flush()).map_err(|e| format!("writing {}: {e}", path.display()))?;
419 self.saved.push(path);
420 }
421 self.c.progress.files_done += 1;
422 Ok(())
423 }
424
425 pub(crate) fn cancel(&mut self, out: &mut Vec<u8>) {
426 self.c.cancelled = true;
427 out.extend_from_slice(transfer::CANCEL);
428 self.drop_file();
429 self.c.outcome.get_or_insert(Outcome::Cancelled);
430 }
431}
432
433#[derive(Clone, Copy, Debug, PartialEq, Eq)]
434enum SState {
435 Start,
438 HeaderAck,
439 DataStart,
441 BlockAck,
443 Streaming,
444 EotAck,
445 EndStart,
447 EndAck,
448 Done,
449}
450
451pub struct XySend {
453 proto: XyProtocol,
454 queue: VecDeque<PathBuf>,
455 file: Option<(File, PathBuf)>,
456 size: u64,
457 sent_bytes: u64,
458 sent: Vec<PathBuf>,
459 state: SState,
460 num: u8,
461 last: Vec<u8>,
463 crc: bool,
464 streaming: bool,
465 retries: u32,
466 pub(crate) c: Common,
467}
468
469impl XySend {
470 pub fn new(proto: XyProtocol, paths: Vec<PathBuf>) -> Result<XySend> {
471 if paths.is_empty() {
472 bail!("no files to upload");
473 }
474 if !proto.batch() && paths.len() > 1 {
475 bail!("{} sends one file at a time; YMODEM sends several", proto.name());
476 }
477 for p in &paths {
478 transfer::check_upload(p)?;
479 }
480 let mut c = Common::new(Direction::Upload);
481 c.progress.files_queued = paths.len();
482 Ok(XySend {
483 proto,
484 queue: paths.into(),
485 file: None,
486 size: 0,
487 sent_bytes: 0,
488 sent: Vec::new(),
489 state: SState::Start,
490 num: 0,
491 last: Vec::new(),
492 crc: proto.crc(),
493 streaming: false,
494 retries: 0,
495 c,
496 })
497 }
498
499 pub(crate) fn progress(&self) -> &Progress {
500 &self.c.progress
501 }
502
503 pub fn protocol(&self) -> XyProtocol {
504 self.proto
505 }
506
507 pub fn enqueue(&mut self, path: PathBuf) -> Result<()> {
508 if !self.proto.batch() {
509 bail!("{} sends one file at a time", self.proto.name());
510 }
511 if matches!(self.state, SState::EndStart | SState::EndAck | SState::Done) {
512 bail!("the upload is already finishing");
513 }
514 transfer::check_upload(&path)?;
515 self.queue.push_back(path);
516 self.c.progress.files_queued = self.queue.len();
517 Ok(())
518 }
519
520 fn fail(&mut self, msg: impl Into<String>, out: &mut Vec<u8>) {
521 if self.c.outcome.is_none() {
522 out.extend_from_slice(transfer::CANCEL);
523 self.c.outcome = Some(Outcome::Failed(msg.into()));
524 }
525 }
526
527 fn send(&mut self, bytes: Vec<u8>, out: &mut Vec<u8>) {
528 out.extend_from_slice(&bytes);
529 self.last = bytes;
530 }
531
532 fn next_file(&mut self) -> Result<bool, String> {
534 let Some(path) = self.queue.pop_front() else { return Ok(false) };
535 self.c.progress.files_queued = self.queue.len();
536 let f = File::open(&path).map_err(|e| format!("opening {}: {e}", path.display()))?;
537 self.size = f.metadata().map(|m| m.len()).unwrap_or(0);
538 self.sent_bytes = 0;
539 let p = &mut self.c.progress;
540 p.file_name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
541 p.file_size = Some(self.size);
542 p.file_bytes = 0;
543 p.file_started = Instant::now();
544 self.file = Some((f, path));
545 Ok(true)
546 }
547
548 fn header(&self) -> Vec<u8> {
550 let (f, path) = self.file.as_ref().expect("a file is open");
551 let mut data = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default().into_bytes();
552 data.push(0);
553 let mtime = f
554 .metadata()
555 .ok()
556 .and_then(|m| m.modified().ok())
557 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
558 .map_or(0, |d| d.as_secs());
559 data.extend_from_slice(format!("{} {mtime:o} 0", self.size).as_bytes());
560 data.push(0);
561 let len = if data.len() <= 128 { 128 } else { 1024 };
562 data.resize(len, 0);
563 block(0, &data, true)
564 }
565
566 fn data_block(&mut self) -> Result<Option<Vec<u8>>, String> {
568 let left = self.size.saturating_sub(self.sent_bytes);
569 if left == 0 {
570 return Ok(None);
571 }
572 let size = if self.crc && self.proto.block_size() == 1024 && left > 128 { 1024 } else { 128 };
574 let mut data = vec![0; size];
575 let (f, path) = self.file.as_mut().expect("a file is open");
576 let mut n = 0;
577 while n < size {
578 match f.read(&mut data[n..]) {
579 Ok(0) => break,
580 Ok(k) => n += k,
581 Err(e) => return Err(format!("reading {}: {e}", path.display())),
582 }
583 }
584 if n == 0 {
585 return Err(format!("{} got shorter while sending", path.display()));
586 }
587 data[n..].fill(SUB);
588 self.sent_bytes += n as u64;
589 self.c.progress.file_bytes = self.sent_bytes;
590 self.num = self.num.wrapping_add(1);
591 Ok(Some(block(self.num, &data, self.crc)))
592 }
593
594 fn next_block(&mut self, out: &mut Vec<u8>) {
596 match self.data_block() {
597 Ok(Some(b)) => {
598 self.send(b, out);
599 self.state = if self.streaming { SState::Streaming } else { SState::BlockAck };
600 }
601 Ok(None) => {
602 self.send(vec![EOT], out);
603 self.state = SState::EotAck;
604 }
605 Err(e) => self.fail(e, out),
606 }
607 }
608
609 fn started_with(&mut self, b: u8) {
612 self.crc = b != NAK;
613 self.streaming = b == b'G';
614 }
615
616 pub(crate) fn pump(&mut self, out: &mut Vec<u8>) {
617 if self.state == SState::Start && self.file.is_none() && self.c.outcome.is_none() {
618 match self.next_file() {
619 Ok(true) => {}
620 Ok(false) => self.state = SState::EndStart,
621 Err(e) => return self.fail(e, out),
622 }
623 }
624 while self.c.outcome.is_none() {
625 if self.state == SState::Streaming {
626 if !self.c.pending.is_empty() && self.c.pending.contains(&CAN) {
628 } else {
630 for _ in 0..STREAM_BURST {
631 self.next_block(out);
632 if self.state != SState::Streaming {
633 break;
634 }
635 }
636 if self.state == SState::Streaming {
637 return;
638 }
639 continue;
640 }
641 }
642 if self.c.pending.is_empty() {
643 return;
644 }
645 let b = self.c.pending.remove(0);
646 if b == CAN {
647 if self.c.pending.first() == Some(&CAN) {
648 self.c.aborted();
649 return;
650 }
651 continue;
652 }
653 let go = matches!(b, b'C' | b'G' | NAK);
654 match self.state {
655 SState::Start if go => {
656 self.started_with(b);
657 self.num = 0;
658 if self.proto.batch() {
659 let h = self.header();
660 self.send(h, out);
661 self.state = SState::HeaderAck;
662 } else {
663 self.next_block(out);
664 }
665 }
666 SState::HeaderAck if b == ACK => self.state = SState::DataStart,
667 SState::HeaderAck | SState::DataStart if b == b'C' || b == b'G' => {
669 self.started_with(b);
670 self.next_block(out);
671 }
672 SState::HeaderAck if b == NAK => self.resend(out),
673 SState::BlockAck if b == ACK => {
674 self.retries = 0;
675 self.next_block(out);
676 }
677 SState::BlockAck | SState::EotAck if b == NAK => self.resend(out),
678 SState::EotAck if b == ACK => {
679 self.retries = 0;
680 if let Some((_, path)) = self.file.take() {
681 self.sent.push(path);
682 }
683 self.c.progress.files_done += 1;
684 if self.proto.batch() {
685 self.state = SState::Start;
686 match self.next_file() {
687 Ok(true) => {}
688 Ok(false) => self.state = SState::EndStart,
689 Err(e) => return self.fail(e, out),
690 }
691 } else {
692 self.state = SState::Done;
693 self.c.outcome = Some(Outcome::Completed(self.sent.clone()));
694 }
695 }
696 SState::EndStart if go => {
697 self.send(block(0, &[0; 128], true), out);
698 self.state = SState::EndAck;
699 }
700 SState::EndAck if b == ACK => {
701 self.state = SState::Done;
702 self.c.outcome = Some(Outcome::Completed(self.sent.clone()));
703 }
704 SState::EndAck if b == NAK => self.resend(out),
705 _ => {}
706 }
707 }
708 }
709
710 fn resend(&mut self, out: &mut Vec<u8>) {
711 self.retries += 1;
712 if self.retries > MAX_RETRIES {
713 return self.fail("too many errors", out);
714 }
715 out.extend_from_slice(&self.last);
716 }
717
718 pub(crate) fn tick(&mut self, now: Instant, out: &mut Vec<u8>) {
719 if self.c.outcome.is_some() || now.duration_since(self.c.last_input) < transfer::TIMEOUT {
720 return;
721 }
722 self.c.last_input = now;
723 match self.state {
724 SState::Start | SState::EndStart => {
725 self.c.timeouts += 1;
726 if self.c.timeouts >= transfer::MAX_TIMEOUTS {
727 self.fail("the receiver never started", out);
728 }
729 }
730 SState::Streaming => {}
731 _ => self.resend(out),
732 }
733 }
734
735 pub(crate) fn cancel(&mut self, out: &mut Vec<u8>) {
736 self.c.cancelled = true;
737 out.extend_from_slice(transfer::CANCEL);
738 self.c.outcome.get_or_insert(Outcome::Cancelled);
739 }
740}
741
742#[cfg(test)]
743mod tests {
744 use super::*;
745
746 fn temp_dir(name: &str) -> PathBuf {
747 let d = std::env::temp_dir().join(format!("badterm-xy-{name}-{}", std::process::id()));
748 let _ = fs::remove_dir_all(&d);
749 fs::create_dir_all(&d).unwrap();
750 d
751 }
752
753 fn run(tx: &mut XySend, rx: &mut XyReceive) {
755 let mut to_rx: Vec<u8> = Vec::new();
756 let mut to_tx: Vec<u8> = Vec::new();
757 rx.tick(Instant::now(), &mut to_tx);
758 for _ in 0..100_000 {
759 if tx.c.outcome.is_some() && rx.c.outcome.is_some() {
760 return;
761 }
762 let mut out = Vec::new();
763 tx.c.input(&std::mem::take(&mut to_tx));
764 tx.pump(&mut out);
765 to_rx.extend(out);
766 let mut out = Vec::new();
767 rx.c.input(&std::mem::take(&mut to_rx));
768 rx.pump(&mut out);
769 to_tx.extend(out);
770 }
771 panic!("stuck: tx {:?} {:?}, rx {:?} {:?}", tx.state, tx.c.outcome, rx.state, rx.c.outcome);
772 }
773
774 #[test]
775 fn crc16_check_value() {
776 assert_eq!(crc16(b"123456789"), 0x31C3);
777 }
778
779 #[test]
780 fn every_protocol_round_trips() {
781 let src = temp_dir("src");
782 let files: Vec<(PathBuf, Vec<u8>)> = [(0usize, 0u8), (1, 7), (128, 9), (1025, 3), (5000, SUB)]
785 .iter()
786 .enumerate()
787 .map(|(i, &(len, fill))| {
788 let mut data: Vec<u8> = (0..len as u32).map(|j| (j * 31 + i as u32) as u8).collect();
789 if fill == SUB && len > 0 {
790 data[len - 3..].fill(SUB);
791 }
792 let p = src.join(format!("file{i}.bin"));
793 fs::write(&p, &data).unwrap();
794 (p, data)
795 })
796 .collect();
797 for proto in XyProtocol::ALL {
798 let dst = temp_dir(proto.name());
799 let one = if proto == XyProtocol::Xmodem { 0 } else { 3 };
800 let paths: Vec<PathBuf> =
801 if proto.batch() { files.iter().map(|(p, _)| p.clone()).collect() } else { vec![files[one].0.clone()] };
802 let mut tx = XySend::new(proto, paths.clone()).unwrap();
803 let mut rx = XyReceive::new(proto, dst.clone(), Some("got.bin".into()));
804 run(&mut tx, &mut rx);
805 assert_eq!(tx.c.outcome, Some(Outcome::Completed(paths.clone())), "{}", proto.name());
806 let Some(Outcome::Completed(saved)) = rx.c.outcome.clone() else { panic!("{}", proto.name()) };
807 assert_eq!(saved.len(), paths.len(), "{}", proto.name());
808 for (path, (_, data)) in saved.iter().zip(files.iter().skip(if proto.batch() { 0 } else { one })) {
809 let got = fs::read(path).unwrap();
810 assert_eq!(&got, data, "{} {}", proto.name(), path.display());
811 }
812 if !proto.batch() {
813 assert_eq!(saved[0].file_name().unwrap(), "got.bin");
814 }
815 }
816 }
817
818 #[test]
819 fn damaged_blocks_are_resent() {
820 let src = temp_dir("noise");
821 let data: Vec<u8> = (0..3000u32).map(|i| (i * 7) as u8).collect();
822 fs::write(src.join("a.bin"), &data).unwrap();
823 let dst = temp_dir("noise-out");
824 let mut tx = XySend::new(XyProtocol::Ymodem, vec![src.join("a.bin")]).unwrap();
825 let mut rx = XyReceive::new(XyProtocol::Ymodem, dst, None);
826 let mut to_rx: Vec<u8> = Vec::new();
827 let mut to_tx = Vec::new();
828 rx.tick(Instant::now(), &mut to_tx);
829 let mut flipped = false;
830 for _ in 0..10_000 {
831 let mut out = Vec::new();
832 tx.c.input(&std::mem::take(&mut to_tx));
833 tx.pump(&mut out);
834 if !flipped && out.len() > 1000 && out[0] == STX && out[1] == 1 {
836 out[500] ^= 0xFF;
837 flipped = true;
838 }
839 to_rx.extend(out);
840 let mut out = Vec::new();
841 rx.c.input(&std::mem::take(&mut to_rx));
842 rx.pump(&mut out);
843 to_tx.extend(out);
844 if tx.c.outcome.is_some() && rx.c.outcome.is_some() {
845 break;
846 }
847 }
848 assert!(flipped);
849 let Some(Outcome::Completed(saved)) = rx.c.outcome.clone() else { panic!("{:?}", rx.c.outcome) };
850 assert_eq!(fs::read(&saved[0]).unwrap(), data);
851 }
852
853 #[test]
854 fn xmodem_takes_one_file_and_cancel_is_heard() {
855 let src = temp_dir("one");
856 fs::write(src.join("a"), b"a").unwrap();
857 fs::write(src.join("b"), b"b").unwrap();
858 assert!(XySend::new(XyProtocol::Xmodem, vec![src.join("a"), src.join("b")]).is_err());
859 let mut rx = XyReceive::new(XyProtocol::XmodemCrc, temp_dir("cancel"), None);
860 let mut out = Vec::new();
861 rx.c.input(&[CAN, CAN, CAN]);
862 rx.pump(&mut out);
863 assert!(matches!(rx.c.outcome, Some(Outcome::Failed(_))));
864 }
865}