1use std::collections::VecDeque;
12use std::fs::{self, File};
13use std::io::{BufWriter, Read, Seek, SeekFrom, Write};
14use std::path::{Path, PathBuf};
15use std::time::{Duration, Instant, UNIX_EPOCH};
16
17use anyhow::{Context, Result, bail};
18use zmodem2::{Action, Event, FileInfo, Position, Receiver, Sender};
19
20pub use crate::kermit::Kermit;
21pub use crate::xmodem::{XyProtocol, XyReceive, XySend};
22
23pub const ZRQINIT: &[u8] = b"**\x18B00";
25pub const ZRINIT: &[u8] = b"**\x18B01";
27pub const CANCEL: &[u8] = b"\x18\x18\x18\x18\x18\x18\x18\x18\x18\x18\x08\x08\x08\x08\x08\x08\x08\x08\x08\x08";
29
30pub(crate) const TIMEOUT: Duration = Duration::from_secs(10);
32pub(crate) const MAX_TIMEOUTS: u32 = 6;
34pub(crate) const MAX_FILE_SIZE: u64 = u32::MAX as u64;
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum Direction {
38 Download,
39 Upload,
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub enum Outcome {
44 Completed(Vec<PathBuf>),
46 Cancelled,
47 Failed(String),
48}
49
50#[derive(Clone, Debug)]
51pub struct Progress {
52 pub direction: Direction,
53 pub file_name: String,
54 pub file_bytes: u64,
55 pub file_size: Option<u64>,
56 pub files_done: usize,
57 pub files_queued: usize,
59 pub file_started: Instant,
60 pub resumed_at: Option<u64>,
62}
63
64impl Progress {
65 pub(crate) fn new(direction: Direction) -> Progress {
66 Progress {
67 direction,
68 file_name: String::new(),
69 file_bytes: 0,
70 file_size: None,
71 files_done: 0,
72 files_queued: 0,
73 file_started: Instant::now(),
74 resumed_at: None,
75 }
76 }
77
78 pub fn rate(&self) -> f64 {
81 let secs = self.file_started.elapsed().as_secs_f64();
82 let bytes = self.file_bytes.saturating_sub(self.resumed_at.unwrap_or(0));
83 if secs > 0.0 { bytes as f64 / secs } else { 0.0 }
84 }
85}
86
87pub(crate) struct Common {
89 pub(crate) pending: Vec<u8>,
90 pub(crate) progress: Progress,
91 pub(crate) outcome: Option<Outcome>,
92 pub(crate) last_input: Instant,
93 pub(crate) timeouts: u32,
94 pub(crate) cancelled: bool,
95}
96
97impl Common {
98 pub(crate) fn new(direction: Direction) -> Common {
99 Common {
100 pending: Vec::new(),
101 progress: Progress::new(direction),
102 outcome: None,
103 last_input: Instant::now(),
104 timeouts: 0,
105 cancelled: false,
106 }
107 }
108
109 pub(crate) fn input(&mut self, data: &[u8]) {
110 self.pending.extend_from_slice(data);
111 self.last_input = Instant::now();
112 self.timeouts = 0;
113 }
114
115 fn timed_out(&mut self, now: Instant) -> bool {
117 if self.outcome.is_some() || now.duration_since(self.last_input) < TIMEOUT {
118 return false;
119 }
120 self.last_input = now;
121 self.timeouts += 1;
122 if self.timeouts >= MAX_TIMEOUTS {
123 self.outcome = Some(Outcome::Failed("the other side stopped responding".into()));
124 }
125 true
126 }
127
128 pub(crate) fn aborted(&mut self) {
129 let outcome = if self.cancelled { Outcome::Cancelled } else { Outcome::Failed("cancelled by the BBS".into()) };
130 self.outcome.get_or_insert(outcome);
131 }
132
133 fn leftover(&mut self) -> Vec<u8> {
135 std::mem::take(&mut self.pending)
136 }
137}
138
139pub const PARTIAL_SUFFIX: &str = ".part";
142
143pub struct Download {
144 rx: Receiver,
145 dir: PathBuf,
146 file: Option<(BufWriter<File>, PathBuf, String)>,
148 accept: Option<u32>,
151 saved: Vec<PathBuf>,
152 c: Common,
153}
154
155impl Download {
156 pub fn new(dir: PathBuf) -> Result<Download> {
157 let mut rx = Receiver::with_flow_control(0, true).map_err(|e| anyhow::anyhow!("{e}"))?;
160 rx.set_manual_file_accept(true);
162 Ok(Download { rx, dir, file: None, accept: None, saved: Vec::new(), c: Common::new(Direction::Download) })
163 }
164
165 fn fail(&mut self, msg: String) {
166 self.c.outcome.get_or_insert(Outcome::Failed(msg));
167 let _ = self.rx.abort();
168 }
169
170 fn open(&mut self, wire_name: &[u8], size: Option<u64>) -> Option<u32> {
174 self.close();
175 let name = sanitize_file_name(wire_name);
176 let part = self.dir.join(format!("{name}{PARTIAL_SUFFIX}"));
177 let have = fs::metadata(&part).map(|m| m.len()).unwrap_or(0);
178 let resume = size.is_some_and(|s| have > 0 && have < s && have <= u32::MAX as u64);
179 let result = fs::create_dir_all(&self.dir)
180 .and_then(|()| if resume { fs::OpenOptions::new().append(true).open(&part) } else { File::create(&part) });
181 match result {
182 Ok(f) => {
183 let offset = if resume { have } else { 0 };
184 self.file = Some((BufWriter::new(f), part, name.clone()));
185 let p = &mut self.c.progress;
186 p.file_name = name;
187 p.file_size = size;
188 p.file_bytes = offset;
189 p.resumed_at = resume.then_some(offset);
190 p.file_started = Instant::now();
191 Some(offset as u32)
192 }
193 Err(e) => {
194 self.fail(format!("creating {} in {}: {e}", part.display(), self.dir.display()));
195 None
196 }
197 }
198 }
199
200 fn close_file(&mut self, complete: bool) -> Option<PathBuf> {
203 let (mut f, part, name) = self.file.take()?;
204 if let Err(e) = f.flush() {
205 self.fail(format!("writing {}: {e}", part.display()));
206 return None;
207 }
208 drop(f);
209 if !complete {
210 return None;
211 }
212 match unique_path(&self.dir, &name).and_then(|dest| fs::rename(&part, &dest).map(|()| dest)) {
213 Ok(dest) => Some(dest),
214 Err(e) => {
215 self.fail(format!("saving {name}: {e}"));
216 None
217 }
218 }
219 }
220
221 fn close(&mut self) {
222 self.close_file(false);
223 }
224
225 fn pump(&mut self, out: &mut Vec<u8>) {
226 loop {
227 match self.rx.poll() {
228 Action::WriteWire(b) => {
229 out.extend_from_slice(b);
230 let n = b.len();
231 self.rx.wire_written(n);
232 }
233 Action::WriteFile(b) => {
234 let n = b.len();
235 let written = match &mut self.file {
236 Some((f, _, _)) => f.write_all(b),
237 None => Ok(()),
238 };
239 if let Err(e) = written {
240 self.fail(format!("writing {}: {e}", self.c.progress.file_name));
241 continue;
242 }
243 self.c.progress.file_bytes += n as u64;
244 if let Err(e) = self.rx.file_written(n) {
245 log::debug!("zmodem file_written: {e}");
246 }
247 }
248 Action::Event(Event::FileStarted(info)) => {
249 let name = info.name.to_vec();
250 let size = info.size.map(|s| u64::from(s.get()));
251 self.accept = self.open(&name, size);
252 }
253 Action::Event(Event::FileCompleted) => {
254 let path = self.close_file(true);
255 self.saved.extend(path);
256 self.c.progress.files_done += 1;
257 }
258 Action::Event(Event::SessionCompleted) => {
259 self.close();
260 self.c.outcome.get_or_insert(Outcome::Completed(self.saved.clone()));
261 }
262 Action::Event(Event::Aborted) => {
263 self.close();
264 self.c.aborted();
265 }
266 Action::Idle => {
267 if let Some(offset) = self.accept {
268 match self.rx.accept_file_at(offset) {
269 Ok(()) => {
270 self.accept = None;
271 continue;
272 }
273 Err(zmodem2::Error::Backpressure) => {}
275 Err(e) => {
276 self.accept = None;
277 self.fail(format!("starting {}: {e}", self.c.progress.file_name));
278 }
279 }
280 }
281 if self.c.outcome.is_some() || self.c.pending.is_empty() {
282 break;
283 }
284 match self.rx.submit_wire(&self.c.pending) {
285 Ok(0) => break,
286 Ok(n) => {
287 self.c.pending.drain(..n);
288 }
289 Err(e) => {
290 log::debug!("zmodem receive error: {e}");
292 self.c.pending.clear();
293 }
294 }
295 }
296 _ => break,
297 }
298 }
299 }
300}
301
302pub struct Upload {
303 tx: Sender,
304 queue: VecDeque<PathBuf>,
305 file: Option<(File, PathBuf)>,
306 sent: Vec<PathBuf>,
307 advance: bool,
309 finishing: bool,
310 buf: Vec<u8>,
311 c: Common,
312}
313
314impl Upload {
315 pub fn new(paths: Vec<PathBuf>) -> Result<Upload> {
316 if paths.is_empty() {
317 bail!("no files to upload");
318 }
319 for p in &paths {
320 check_upload(p)?;
321 }
322 let mut tx = Sender::new().map_err(|e| anyhow::anyhow!("{e}"))?;
323 tx.set_streaming_window(usize::MAX);
324 let mut up = Upload {
325 tx,
326 queue: paths.into(),
327 file: None,
328 sent: Vec::new(),
329 advance: false,
330 finishing: false,
331 buf: Vec::new(),
332 c: Common::new(Direction::Upload),
333 };
334 up.start_next();
335 Ok(up)
336 }
337
338 pub fn enqueue(&mut self, path: PathBuf) -> Result<()> {
340 if self.finishing || self.c.outcome.is_some() {
341 bail!("the upload is already finishing");
342 }
343 check_upload(&path)?;
344 self.queue.push_back(path);
345 self.c.progress.files_queued = self.queue.len();
346 Ok(())
347 }
348
349 fn fail(&mut self, msg: String) {
350 self.c.outcome.get_or_insert(Outcome::Failed(msg));
351 self.tx.abort();
352 }
353
354 fn start_next(&mut self) {
355 let Some(path) = self.queue.pop_front() else {
356 self.finishing = true;
357 if let Err(e) = self.tx.finish() {
358 self.fail(format!("finishing upload: {e}"));
359 }
360 return;
361 };
362 self.c.progress.files_queued = self.queue.len();
363 let opened = File::open(&path).and_then(|f| f.metadata().map(|m| (f, m)));
364 let (f, meta) = match opened {
365 Ok(v) => v,
366 Err(e) => return self.fail(format!("opening {}: {e}", path.display())),
367 };
368 let name = path.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_else(|| "file".into());
369 let size = meta.len();
370 let mut info = FileInfo::new(name.as_bytes(), Some(Position::new(size as u32)));
371 if let Some(t) = meta.modified().ok().and_then(|t| t.duration_since(UNIX_EPOCH).ok()) {
372 info = info.with_modified(t.as_secs() as u32);
373 }
374 if let Err(e) = self.tx.start_file(info) {
375 return self.fail(format!("starting {name}: {e}"));
376 }
377 let p = &mut self.c.progress;
378 p.file_name = name;
379 p.file_size = Some(size);
380 p.file_bytes = 0;
381 p.file_started = Instant::now();
382 self.file = Some((f, path));
383 }
384
385 fn read_chunk(&mut self, offset: u64, max_len: usize) -> std::io::Result<usize> {
386 let Some((f, _)) = &mut self.file else { return Ok(0) };
387 self.buf.resize(max_len, 0);
388 f.seek(SeekFrom::Start(offset))?;
389 f.read(&mut self.buf)
390 }
391
392 fn pump(&mut self, out: &mut Vec<u8>) {
393 loop {
394 match self.tx.poll() {
395 Action::WriteWire(b) => {
396 out.extend_from_slice(b);
397 let n = b.len();
398 self.tx.wire_written(n);
399 }
400 Action::ReadFile { offset, max_len } => {
401 let offset = u64::from(offset.get());
402 match self.read_chunk(offset, max_len) {
403 Ok(0) => self.fail(format!("{} got shorter while sending", self.c.progress.file_name)),
404 Ok(n) => {
405 if let Err(e) = self.tx.submit_file(&self.buf[..n]) {
406 log::debug!("zmodem submit_file: {e}");
407 }
408 self.c.progress.file_bytes = offset + n as u64;
409 }
410 Err(e) => self.fail(format!("reading {}: {e}", self.c.progress.file_name)),
411 }
412 }
413 Action::Event(Event::FileCompleted) => {
414 if let Some((_, path)) = self.file.take() {
415 self.sent.push(path);
416 }
417 self.c.progress.files_done += 1;
418 self.advance = true;
419 }
420 Action::Event(Event::SessionCompleted) => {
421 self.c.outcome.get_or_insert(Outcome::Completed(self.sent.clone()));
422 }
423 Action::Event(Event::Aborted) => self.c.aborted(),
424 Action::Event(_) => {}
425 Action::Idle => {
426 if self.advance && self.c.outcome.is_none() {
427 self.advance = false;
428 self.start_next();
429 continue;
430 }
431 if self.c.outcome.is_some() || self.c.pending.is_empty() {
432 break;
433 }
434 match self.tx.submit_wire(&self.c.pending) {
435 Ok(0) => break,
436 Ok(n) => {
437 self.c.pending.drain(..n);
438 }
439 Err(e) => {
440 log::debug!("zmodem send error: {e}");
441 self.c.pending.clear();
442 }
443 }
444 }
445 _ => break,
446 }
447 }
448 }
449}
450
451pub(crate) fn check_upload(p: &Path) -> Result<()> {
452 let meta = fs::metadata(p).with_context(|| format!("{}", p.display()))?;
453 if !meta.is_file() {
454 bail!("{} is not a file", p.display());
455 }
456 if meta.len() > MAX_FILE_SIZE {
457 bail!("{} is larger than the 4 GiB limit", p.display());
458 }
459 Ok(())
460}
461
462pub enum Transfer {
463 Download(Download),
464 Upload(Upload),
465 XyDownload(XyReceive),
467 XyUpload(XySend),
468 Kermit(Kermit),
470}
471
472impl Transfer {
473 fn parts(&mut self) -> &mut Common {
474 match self {
475 Transfer::Download(d) => &mut d.c,
476 Transfer::Upload(u) => &mut u.c,
477 Transfer::XyDownload(d) => &mut d.c,
478 Transfer::XyUpload(u) => &mut u.c,
479 Transfer::Kermit(k) => &mut k.c,
480 }
481 }
482
483 fn pump(&mut self, out: &mut Vec<u8>) {
484 match self {
485 Transfer::Download(d) => d.pump(out),
486 Transfer::Upload(u) => u.pump(out),
487 Transfer::XyDownload(d) => d.pump(out),
488 Transfer::XyUpload(u) => u.pump(out),
489 Transfer::Kermit(k) => k.pump(out),
490 }
491 }
492
493 pub fn name(&self) -> &'static str {
495 match self {
496 Transfer::Download(_) | Transfer::Upload(_) => "ZMODEM",
497 Transfer::XyDownload(d) => d.protocol().name(),
498 Transfer::XyUpload(u) => u.protocol().name(),
499 Transfer::Kermit(_) => "Kermit",
500 }
501 }
502
503 pub fn start(&mut self, out: &mut Vec<u8>) {
505 if let Transfer::Kermit(k) = self {
506 k.start(out);
507 }
508 }
509
510 pub fn feed(&mut self, data: &[u8], out: &mut Vec<u8>) {
512 self.parts().input(data);
513 self.pump(out);
514 }
515
516 pub fn tick(&mut self, now: Instant, out: &mut Vec<u8>) {
518 match self {
520 Transfer::XyDownload(d) => {
521 d.tick(now, out);
522 return self.pump(out);
523 }
524 Transfer::XyUpload(u) => {
525 u.tick(now, out);
526 return self.pump(out);
527 }
528 Transfer::Kermit(k) => {
529 k.tick(now, out);
530 return self.pump(out);
531 }
532 _ => {}
533 }
534 if !self.parts().timed_out(now) {
535 return;
536 }
537 let gave_up = self.parts().outcome.is_some();
538 match self {
539 Transfer::Download(d) if gave_up => {
540 let _ = d.rx.abort();
541 }
542 Transfer::Download(d) => {
543 let _ = d.rx.timeout();
544 }
545 Transfer::Upload(u) if gave_up => u.tx.abort(),
546 Transfer::Upload(u) => {
547 let _ = u.tx.timeout();
548 }
549 Transfer::XyDownload(_) | Transfer::XyUpload(_) | Transfer::Kermit(_) => {}
550 }
551 self.pump(out);
552 }
553
554 pub fn cancel(&mut self, out: &mut Vec<u8>) {
556 self.parts().cancelled = true;
557 match self {
558 Transfer::Download(d) => {
559 let _ = d.rx.abort();
560 }
561 Transfer::Upload(u) => u.tx.abort(),
562 Transfer::XyDownload(d) => return d.cancel(out),
563 Transfer::XyUpload(u) => return u.cancel(out),
564 Transfer::Kermit(k) => return k.cancel(out),
565 }
566 self.pump(out);
567 self.parts().outcome.get_or_insert(Outcome::Cancelled);
569 }
570
571 pub fn progress(&self) -> &Progress {
572 match self {
573 Transfer::Download(d) => &d.c.progress,
574 Transfer::Upload(u) => &u.c.progress,
575 Transfer::XyDownload(d) => d.progress(),
576 Transfer::XyUpload(u) => u.progress(),
577 Transfer::Kermit(k) => k.progress(),
578 }
579 }
580
581 pub fn is_finished(&self) -> bool {
582 match self {
583 Transfer::Download(d) => d.c.outcome.is_some(),
584 Transfer::Upload(u) => u.c.outcome.is_some(),
585 Transfer::XyDownload(d) => d.c.outcome.is_some(),
586 Transfer::XyUpload(u) => u.c.outcome.is_some(),
587 Transfer::Kermit(k) => k.c.outcome.is_some(),
588 }
589 }
590
591 pub fn finish(mut self) -> (Outcome, Vec<u8>) {
594 let c = self.parts();
595 let leftover = c.leftover();
596 let outcome = c.outcome.take().unwrap_or_else(|| Outcome::Failed("interrupted".into()));
597 if let (Transfer::Download(d), Outcome::Failed(_) | Outcome::Cancelled) = (&mut self, &outcome) {
598 d.close();
599 }
600 (outcome, leftover)
601 }
602
603 pub fn enqueue(&mut self, path: PathBuf) -> Result<()> {
604 match self {
605 Transfer::Upload(u) => u.enqueue(path),
606 Transfer::XyUpload(u) => u.enqueue(path),
607 Transfer::Kermit(k) => k.enqueue(path),
608 Transfer::Download(_) | Transfer::XyDownload(_) => bail!("a download is in progress"),
609 }
610 }
611}
612
613pub fn sanitize_file_name(wire: &[u8]) -> String {
616 let s = String::from_utf8_lossy(wire);
617 let base = s.rsplit(['/', '\\']).next().unwrap_or("");
618 let cleaned: String = base
619 .chars()
620 .map(|c| if c.is_control() || matches!(c, ':' | '*' | '?' | '"' | '<' | '>' | '|') { '_' } else { c })
621 .collect();
622 let cleaned = cleaned.trim().trim_start_matches('.').to_string();
623 if cleaned.is_empty() { "download".to_string() } else { cleaned }
624}
625
626pub(crate) fn unique_path(dir: &Path, name: &str) -> std::io::Result<PathBuf> {
628 let first = dir.join(name);
629 if !first.exists() {
630 return Ok(first);
631 }
632 let (stem, ext) = match name.rsplit_once('.') {
633 Some((s, e)) if !s.is_empty() => (s, format!(".{e}")),
634 _ => (name, String::new()),
635 };
636 for i in 1..10_000 {
637 let p = dir.join(format!("{stem} ({i}){ext}"));
638 if !p.exists() {
639 return Ok(p);
640 }
641 }
642 Err(std::io::Error::new(std::io::ErrorKind::AlreadyExists, format!("too many copies of {name}")))
643}
644
645#[derive(Debug)]
651pub struct TrailerFilter {
652 until: Instant,
653 o_left: u8,
654}
655
656impl TrailerFilter {
657 const WINDOW: Duration = Duration::from_secs(2);
658
659 pub fn new() -> TrailerFilter {
660 TrailerFilter { until: Instant::now() + Self::WINDOW, o_left: 2 }
661 }
662
663 pub fn strip(&mut self, data: &mut Vec<u8>) -> bool {
666 if Instant::now() > self.until {
667 return false;
668 }
669 let mut end = None;
671 for (n, &b) in data.iter().enumerate() {
672 match b {
673 b'\r' | b'\n' | 0x8D | 0x8A | 0x11 | 0x13 => {}
674 b'O' if self.o_left > 0 => {
675 self.o_left -= 1;
676 if self.o_left == 0 {
677 end = Some(n + 1);
679 break;
680 }
681 }
682 _ => {
683 end = Some(n);
684 break;
685 }
686 }
687 }
688 match end {
689 Some(cut) => {
690 data.drain(..cut);
691 false
692 }
693 None => {
694 data.clear();
695 true
696 }
697 }
698 }
699}
700
701impl Default for TrailerFilter {
702 fn default() -> Self {
703 Self::new()
704 }
705}
706
707pub fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
709 hay.windows(needle.len()).position(|w| w == needle)
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715
716 fn temp_dir(name: &str) -> PathBuf {
717 let d = std::env::temp_dir().join(format!("badterm-test-{name}-{}", std::process::id()));
718 let _ = fs::remove_dir_all(&d);
719 fs::create_dir_all(&d).unwrap();
720 d
721 }
722
723 fn run(up: &mut Transfer, down: &mut Transfer) {
725 let (mut to_down, mut to_up) = (Vec::new(), Vec::new());
726 up.feed(&[], &mut to_down);
727 for _ in 0..100_000 {
728 if up.is_finished() && down.is_finished() {
729 return;
730 }
731 let data = std::mem::take(&mut to_down);
732 down.feed(&data, &mut to_up);
733 let data = std::mem::take(&mut to_up);
734 up.feed(&data, &mut to_down);
735 }
736 panic!("transfer did not finish");
737 }
738
739 #[test]
740 fn upload_to_download_round_trip() {
741 let src = temp_dir("src");
742 let dst = temp_dir("dst");
743 let mut contents = Vec::new();
744 for i in 0..300_000u32 {
745 contents.push((i.wrapping_mul(2_654_435_761) >> 13) as u8);
746 }
747 contents.extend_from_slice(&[0x18, 0x11, 0x13, 0xFF, b'\r', 0]);
749 fs::write(src.join("big.bin"), &contents).unwrap();
750 fs::write(src.join("empty.txt"), b"").unwrap();
751 fs::write(dst.join("big.bin"), b"already here").unwrap();
752
753 let mut up = Transfer::Upload(Upload::new(vec![src.join("big.bin"), src.join("empty.txt")]).unwrap());
754 let mut down = Transfer::Download(Download::new(dst.clone()).unwrap());
755 run(&mut up, &mut down);
756
757 let (up_outcome, _) = up.finish();
758 let (down_outcome, _) = down.finish();
759 assert_eq!(up_outcome, Outcome::Completed(vec![src.join("big.bin"), src.join("empty.txt")]));
760 assert_eq!(down_outcome, Outcome::Completed(vec![dst.join("big (1).bin"), dst.join("empty.txt")]));
762 assert_eq!(fs::read(dst.join("big (1).bin")).unwrap(), contents);
763 assert_eq!(fs::read(dst.join("big.bin")).unwrap(), b"already here");
764 assert_eq!(fs::read(dst.join("empty.txt")).unwrap(), b"");
765 }
766
767 #[test]
768 fn cancel_sends_abort() {
769 let dst = temp_dir("cancel");
770 let mut down = Transfer::Download(Download::new(dst).unwrap());
771 let mut out = Vec::new();
772 down.cancel(&mut out);
773 assert!(find(&out, &CANCEL[..10]).is_some());
774 assert_eq!(down.finish().0, Outcome::Cancelled);
775 }
776
777 #[test]
778 fn trailer_filter_spans_chunks() {
779 let mut f = TrailerFilter::new();
780 let mut a = b"\r\x8a".to_vec();
781 assert!(f.strip(&mut a));
782 assert!(a.is_empty());
783 let mut b = b"OO\r\nMenu: ".to_vec();
785 assert!(!f.strip(&mut b));
786 assert_eq!(b, b"\r\nMenu: ");
787 let mut f = TrailerFilter::new();
789 let mut c = b"OOOps".to_vec();
790 f.strip(&mut c);
791 assert_eq!(c, b"Ops");
792 }
793
794 #[test]
795 fn sanitizes_names() {
796 assert_eq!(sanitize_file_name(b"../../etc/passwd"), "passwd");
797 assert_eq!(sanitize_file_name(b"C:\\DOS\\GAME.ZIP"), "GAME.ZIP");
798 assert_eq!(sanitize_file_name(b".."), "download");
799 assert_eq!(sanitize_file_name(b".hidden"), "hidden");
800 assert_eq!(sanitize_file_name(b"a\x1bb?.txt"), "a_b_.txt");
801 }
802}