1use std::collections::{HashMap, VecDeque};
6use std::path::{Path, PathBuf};
7use std::rc::Rc;
8
9use crate::Engine;
10use crate::cmd::Cmd;
11use crate::input::{Level, LineSource, ScannerStatus};
12use crate::primitives_gen as pg;
13use crate::print::Selector;
14use crate::token::Token;
15
16#[derive(Clone, Debug)]
18pub enum ReadFile {
19 Closed,
21 JustOpen(LineReader),
23 Normal(LineReader),
25}
26
27#[derive(Clone, Debug, Default)]
30pub struct LineReader {
31 data: Vec<u8>,
32 pos: usize,
33}
34
35impl LineReader {
36 pub fn new(data: Vec<u8>) -> Self {
38 LineReader { data, pos: 0 }
39 }
40
41 pub fn pop_front(&mut self) -> Option<Vec<u8>> {
43 let (line, next) = crate::input::next_line(&self.data, self.pos)?;
44 let line = line.to_vec();
45 self.pos = next;
46 Some(line)
47 }
48}
49
50#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct FileRecord {
53 pub path: PathBuf,
55 pub hash: Option<u64>,
57}
58
59#[derive(Debug)]
61pub struct FileState {
62 pub read_files: Vec<ReadFile>,
64 pub name_in_progress: bool,
66 pub output_dir: Option<PathBuf>,
68 pub read_log: Vec<FileRecord>,
70 pub written: HashMap<PathBuf, Vec<u8>>,
72 pub command_line: Vec<u8>,
74 pub log_name: Option<PathBuf>,
76 pub log_in_texmfoutput: bool,
79 pub watched: Vec<PathBuf>,
82}
83
84impl Default for FileState {
85 fn default() -> Self {
86 FileState {
87 read_files: vec![ReadFile::Closed; 16],
88 name_in_progress: false,
89 output_dir: None,
90 read_log: Vec::new(),
91 written: HashMap::new(),
92 command_line: Vec::new(),
93 log_name: None,
94 log_in_texmfoutput: false,
95 watched: Vec::new(),
96 }
97 }
98}
99
100impl FileState {
101 pub fn read_open(&self, n: u32) -> bool {
103 !matches!(
104 self.read_files.get(n as usize),
105 None | Some(ReadFile::Closed)
106 )
107 }
108}
109
110pub fn content_hash(data: &[u8]) -> u64 {
112 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
113 for &b in data {
114 h ^= u64::from(b);
115 h = h.wrapping_mul(0x0100_0000_01b3);
116 }
117 h
118}
119
120impl Engine {
121 pub fn read_file_recorded(&mut self, path: &Path) -> Option<Vec<u8>> {
123 let data = std::fs::read(path).ok();
124 if !self.files.read_log.iter().any(|r| r.path == path) {
125 self.files.read_log.push(FileRecord {
126 path: path.to_path_buf(),
127 hash: data.as_deref().map(content_hash),
128 });
129 }
130 data
131 }
132
133 pub fn record_probe(&mut self, name: &str, found: Option<&Path>) {
137 let path = match found {
138 Some(p) if p.is_relative() => p.to_path_buf(),
139 Some(_) => return,
140 None if !Path::new(name).is_absolute() && !name.contains("//") => {
141 self.absent_path(name)
142 }
143 None => return,
144 };
145 if self.files.read_log.iter().any(|r| r.path == path) {
146 return;
147 }
148 let hash = found
149 .and_then(|p| std::fs::read(p).ok())
150 .map(|d| content_hash(&d));
151 self.files.read_log.push(FileRecord { path, hash });
152 }
153
154 fn absent_path(&self, name: &str) -> PathBuf {
157 match &self.files.output_dir {
158 Some(d) if Path::new(name).is_relative() => d.join(name),
159 _ => PathBuf::from(name),
160 }
161 }
162
163 pub fn record_absent_file(&mut self, name: &str) {
167 let p = self.absent_path(name);
168 if !self.files.read_log.iter().any(|r| r.path == p) {
169 self.files.read_log.push(FileRecord {
170 path: p,
171 hash: None,
172 });
173 }
174 }
175
176 pub fn name_ok(&self, name: &str, access: retex_kpath::Access) -> bool {
181 let Some(k) = &self.kpse else {
182 return true;
183 };
184 let refusal = match k.borrow().name_ok(name, access, false) {
185 Ok(()) => return true,
186 Err(r) => r,
187 };
188 let program = std::env::args().next().unwrap_or_else(|| "retex".into());
189 eprint!("\n{program}: {}\n", refusal.message(name));
190 false
191 }
192
193 pub fn find_input_file(&mut self, name: &str, fmt: retex_kpath::FileFormat) -> Option<PathBuf> {
196 if let Some(dir) = &self.files.output_dir {
197 for cand in [format!("{name}.tex"), name.to_owned()] {
198 let p = dir.join(&cand);
199 if p.is_file() {
200 return Some(p);
201 }
202 }
203 }
204 if let Some(k) = &self.kpse {
205 return k.borrow_mut().find_file(name, fmt);
206 }
207 let p = PathBuf::from(name);
208 if p.is_file() {
209 return Some(p);
210 }
211 let p = PathBuf::from(format!("{name}.tex"));
212 p.is_file().then_some(p)
213 }
214
215 pub fn scan_file_name(&mut self) -> Vec<u8> {
218 self.files.name_in_progress = true;
219 let mut name: Vec<u8> = Vec::new();
220 self.get_nonblank_noncall();
221 if self.cur_cmd == Cmd::LeftBrace {
222 self.back_input();
223 let toks = self.scan_toks_expanded();
224 let s = self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
225 name = s.into_iter().filter(|&b| b != b'"').collect();
226 self.files.name_in_progress = false;
227 return name;
228 }
229 let mut quoted = false;
230 loop {
231 if self.cur_cmd > Cmd::OtherChar || self.cur_cs != crate::hash::NO_CS {
232 self.back_input();
233 break;
234 }
235 let c = self.cur_chr;
236 if c == ' ' as u32 && !quoted {
237 break;
238 }
239 if c == '"' as u32 {
240 quoted = !quoted;
241 } else {
242 crate::utf8x::push(&mut name, c);
243 }
244 self.get_x_token();
245 }
246 self.files.name_in_progress = false;
247 name
248 }
249
250 fn read_pipe(&mut self, name: &[u8]) -> Option<Vec<u8>> {
253 let cmd = name.strip_prefix(b"|")?;
254 if self.shell_escape == 0 {
255 return None;
256 }
257 let host = self.lua.clone()?;
258 host.pipe_read(self, cmd)
259 }
260
261 pub fn start_input(&mut self) {
263 let name = self.scan_file_name();
264 let name_str = String::from_utf8_lossy(&name).into_owned();
265 let (found, data) = if let Some(out) = self.read_pipe(&name) {
266 (PathBuf::from(&name_str), out)
268 } else {
269 let allowed = self.name_ok(&name_str, retex_kpath::Access::Read);
270 let found = match allowed
271 .then(|| self.find_input_file(&name_str, retex_kpath::FileFormat::Tex))
272 .flatten()
273 {
274 Some(p) => p,
275 None => self.prompt_file_name(&name, "input file name", ".tex"),
276 };
277 let data = self.read_file_recorded(&found).unwrap_or_default();
278 (found, data)
279 };
280 if let Some(f) = self.progress_hook {
281 f(self);
282 }
283 if !self.printer.log_opened() && self.job_name.is_some() {
284 self.open_log_file();
285 }
286 if self.job_name.is_none() {
287 let base = Path::new(&name_str)
288 .file_stem()
289 .map(|s| s.to_string_lossy().into_owned())
290 .unwrap_or_else(|| name_str.clone());
291 self.job_name = Some(base.into_bytes());
292 self.open_log_file();
293 }
294 let display = found.to_string_lossy().into_owned().into_bytes();
295 let shown = if display.contains(&b' ') {
297 [&b"\""[..], &display, b"\""].concat()
298 } else {
299 display.clone()
300 };
301 if self.printer.term_offset + shown.len() > self.printer.max_print_line.saturating_sub(2) {
302 self.printer.print_ln();
303 } else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
304 self.printer.print_char(' ' as u32);
305 }
306 self.check_in_open();
307 self.printer.print_char('(' as u32);
308 self.input.open_parens += 1;
309 self.printer.print_bytes(&shown);
310 self.printer.update_terminal();
311 let source = LineSource::File {
312 data: Rc::from(data),
313 pos: 0,
314 };
315 self.begin_file_reading(source, display, true);
316 self.synctex_input_opened(&found);
317 self.read_first_line();
319 }
320
321 fn read_first_line(&mut self) {
322 let end_line_char = self.eqtb.int(pg::assign_int::ENDLINECHAR);
323 if let Some(Level::Text(t)) = self.input.stack.last_mut() {
324 t.line = 0;
325 let line = match &mut t.source {
326 LineSource::File { data, pos } => match crate::input::next_line(data, *pos) {
327 Some((bytes, next)) => {
328 let l = crate::input::decode_line(bytes);
329 *pos = next;
330 Some(l)
331 }
332 None => None,
333 },
334 _ => None,
335 };
336 t.line = 1;
337 t.sync.1 = 1;
338 t.buf = line.unwrap_or_default();
339 if (0..=0x10_FFFF).contains(&end_line_char) {
340 t.buf.push(end_line_char as u32);
341 }
342 t.loc = 0;
343 t.state = crate::input::LexState::NewLine;
344 }
345 self.sync_input_changed();
346 }
347
348 pub fn prompt_file_name(&mut self, name: &[u8], s: &str, e: &str) -> ! {
351 if s == "input file name" {
352 self.print_err("I can't find file `");
353 } else {
354 self.print_err("I can't write on file `");
355 }
356 self.printer.print_bytes(name);
357 self.printer.print_str("'.");
358 if e == ".tex" {
359 self.show_context();
360 }
361 self.printer.print_ln();
362 if s == "input file name" {
365 self.printer
366 .print_str("(Press Enter to retry, or Control-D to exit)");
367 }
368 self.printer.print_nl("Please type another ");
369 self.printer.print_str(s);
370 self.fatal_error("*** (job aborted, file error in nonstop mode)");
371 }
372
373 pub fn open_log_file(&mut self) {
375 let old = self.printer.selector;
376 if self.job_name.is_none() {
377 self.job_name = Some(b"texput".to_vec());
378 }
379 let job = String::from_utf8_lossy(self.job_name.as_ref().unwrap()).into_owned();
380 let name = format!("{job}.log");
381 if !self.name_ok(&name, retex_kpath::Access::Write) {
382 self.prompt_file_name(name.as_bytes(), "transcript file name", ".log");
383 }
384 let mut path = PathBuf::from(&name);
385 if let Some(dir) = &self.files.output_dir {
386 path = dir.join(path);
387 }
388 let mut file = std::fs::File::create(&path);
389 if file.is_err()
393 && self.files.output_dir.is_none()
394 && let Some(dir) = self
395 .kpse
396 .as_ref()
397 .and_then(|k| k.borrow().var_value("TEXMFOUTPUT"))
398 .filter(|d| !d.is_empty())
399 {
400 let dir = PathBuf::from(dir);
401 file = std::fs::File::create(dir.join(&name));
402 if file.is_ok() {
403 path = dir.join(&name);
404 self.files.output_dir = Some(dir);
405 self.files.log_in_texmfoutput = true;
406 }
407 }
408 match file {
409 Ok(f) => self
410 .printer
411 .set_log(Box::new(std::io::BufWriter::with_capacity(1 << 16, f))),
412 Err(_) => self.prompt_file_name(name.as_bytes(), "transcript file name", ".log"),
413 }
414 self.files.log_name = Some(path);
415 let banner = self.log_banner();
417 self.printer.log_raw(banner.as_bytes());
418 let shell = self.shell_escape_banner();
419 if shell.is_empty() {
420 self.printer.file_offset = banner.len();
421 } else {
422 self.printer.log_raw(b"\n");
423 self.printer.log_raw(shell.as_bytes());
424 self.printer.file_offset = shell.len();
425 }
426 self.printer.selector = Selector::LogOnly;
427 self.printer.print_nl("**");
428 let line = self.files.command_line.clone();
429 self.printer.print_bytes(&line);
430 self.printer.print_ln();
431 self.printer.selector = match old {
432 Selector::NoPrint => Selector::LogOnly,
433 Selector::TermOnly => Selector::TermAndLog,
434 s => s,
435 };
436 }
437
438 pub fn shell_escape_banner(&self) -> &'static str {
442 match self.shell_escape {
443 0 => "",
444 1 => " system commands enabled.",
445 _ => " restricted system commands enabled.",
446 }
447 }
448
449 fn log_banner(&self) -> String {
450 let c = self.start_time.civil();
451 const MONTHS: [&str; 12] = [
452 "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
453 ];
454 format!(
455 "This is retex, Version {} (LuaTeX 1.22.0 compatible) ({}) {} {} {} {:02}:{:02}",
456 env!("CARGO_PKG_VERSION"),
457 if self.ini_version {
458 "INITEX".to_owned()
459 } else {
460 self.format_ident
461 .trim()
462 .trim_start_matches('(')
463 .trim_end_matches(')')
464 .to_owned()
465 },
466 c.day,
467 MONTHS[(c.month as usize).saturating_sub(1).min(11)],
468 c.year,
469 c.hour,
470 c.minute
471 )
472 }
473
474 pub fn terminal_exhausted(&mut self) {
477 if self.printer.selector == Selector::NoPrint || self.printer.selector == Selector::TermOnly
478 {
479 self.open_log_file();
480 }
481 if self.err.interaction > crate::error::Interaction::NonStop {
482 let empty = matches!(self.input.stack.last(), Some(Level::Text(t)) if t.buf.is_empty());
483 if empty {
484 self.printer
485 .print_nl("(Please type a command or say `\\end')");
486 }
487 self.printer.print_ln();
488 let mut line = self.prompt_input("*");
489 let elc = self.eqtb.int(pg::assign_int::ENDLINECHAR);
490 if (0..=0x10_FFFF).contains(&elc) {
491 line.push(elc as u32);
492 }
493 if let Some(Level::Text(t)) = self.input.stack.last_mut() {
494 t.buf = line;
495 t.loc = 0;
496 t.state = crate::input::LexState::NewLine;
497 }
498 return;
499 }
500 self.fatal_error("*** (job aborted, no legal \\end found)");
501 }
502
503 pub fn open_or_close_in(&mut self) {
505 let c = self.cur_chr;
506 self.scan_four_bit_int();
507 let n = self.cur_val as usize;
508 self.files.read_files[n] = ReadFile::Closed;
509 if c != 0 {
510 self.scan_optional_equals();
511 let name = self.scan_file_name();
512 if name.first() == Some(&b'|') {
513 if let Some(out) = self.read_pipe(&name) {
514 self.files.read_files[n] = ReadFile::JustOpen(LineReader::new(out));
515 }
516 return;
517 }
518 let name = String::from_utf8_lossy(&name).into_owned();
519 if !self.name_ok(&name, retex_kpath::Access::Read) {
520 return;
521 }
522 if let Some(p) = self.find_input_file(&name, retex_kpath::FileFormat::Tex)
523 && let Some(data) = self.read_file_recorded(&p)
524 {
525 self.files.read_files[n] = ReadFile::JustOpen(LineReader::new(data));
526 } else {
527 self.record_absent_file(&name);
528 }
529 }
530 }
531
532 pub fn read_toks(&mut self, n: i32, r: crate::hash::CsId, j: u32) -> Vec<Token> {
536 self.input.scanner_status = ScannerStatus::Defining;
537 self.input.warning_index = r;
538 let mut out: Vec<Token> = vec![Token::END_MATCH];
539 let m: usize = if (0..=15).contains(&n) {
540 n as usize
541 } else {
542 16
543 };
544 let s = self.input.align_state;
545 self.input.align_state = 1_000_000;
546 loop {
547 let line: Vec<u8> = match self.files.read_files.get_mut(m) {
549 None | Some(ReadFile::Closed) => {
550 self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
553 self.fatal_error("*** (cannot \\read from terminal in nonstop modes)");
555 }
556 Some(ReadFile::JustOpen(lines)) => {
557 let first = lines.pop_front();
558 match first {
559 Some(l) => {
560 let rest = std::mem::take(lines);
561 self.files.read_files[m] = ReadFile::Normal(rest);
562 l
563 }
564 None => {
565 self.files.read_files[m] = ReadFile::Closed;
566 Vec::new()
567 }
568 }
569 }
570 Some(ReadFile::Normal(lines)) => match lines.pop_front() {
571 Some(l) => l,
572 None => {
573 self.files.read_files[m] = ReadFile::Closed;
574 if self.input.align_state != 1_000_000 {
575 self.runaway();
576 self.print_err("File ended within ");
577 self.print_esc_str("read");
578 self.help(&["This \\read has unbalanced braces."]);
579 self.input.align_state = 1_000_000;
580 self.error();
581 }
582 Vec::new()
583 }
584 },
585 };
586 let end_line_char = self.eqtb.int(pg::assign_int::ENDLINECHAR);
587 let mut buf = crate::input::decode_line(&line);
588 if (0..=0x10_FFFF).contains(&end_line_char) {
589 buf.push(end_line_char as u32);
590 }
591 if j == 1 {
592 for &c in &buf {
594 out.push(if c == ' ' as u32 {
595 Token::SPACE
596 } else {
597 Token::other(c)
598 });
599 }
600 break;
601 }
602 self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
603 if let Some(Level::Text(t)) = self.input.stack.last_mut() {
604 t.buf = buf;
605 t.loc = 0;
606 t.state = crate::input::LexState::NewLine;
607 }
608 loop {
609 self.get_token();
610 if self.cur_tok.0 == 0 {
611 break;
612 }
613 if self.input.align_state < 1_000_000 {
614 loop {
616 self.get_token();
617 if self.cur_tok.0 == 0 {
618 break;
619 }
620 }
621 self.input.align_state = 1_000_000;
622 break;
623 }
624 out.push(self.cur_tok);
625 }
626 self.end_file_reading();
627 if self.input.align_state == 1_000_000 {
628 break;
629 }
630 }
631 self.input.scanner_status = ScannerStatus::Normal;
632 self.input.align_state = s;
633 out
634 }
635
636 pub fn pseudo_start(&mut self, text_tokens: bool) {
638 let list = self.scan_general_text();
639 let s = self.print_to_string(|e| e.show_token_list(&list, 0, list.len(), i64::MAX));
640 let nl = self.eqtb.int(pg::assign_int::NEWLINECHAR);
645 let mut lines: VecDeque<Vec<u8>> = VecDeque::new();
646 let mut rest = s.as_slice();
647 while !rest.is_empty() {
648 let end = if (0..128).contains(&nl) {
649 rest.iter().position(|&b| i32::from(b) == nl)
650 } else {
651 None
652 };
653 let (line, tail) = match end {
654 Some(i) => (&rest[..i], &rest[i + 1..]),
655 None => (rest, &rest[rest.len()..]),
656 };
657 lines.push_back(line.to_vec());
658 rest = tail;
659 }
660 let tracing = self.eqtb.int(pg::assign_int::TRACINGSCANTOKENS) > 0 && !text_tokens;
661 self.begin_file_reading(
662 LineSource::Lines {
663 lines,
664 catcodes: None,
665 pseudo: !text_tokens,
666 },
667 Vec::new(),
668 tracing,
669 );
670 if let Some(Level::Text(t)) = self.input.stack.last_mut() {
671 t.line = 0;
672 t.buf.clear();
673 t.loc = 0;
674 t.eof_seen = text_tokens;
675 }
676 if tracing {
677 if self.printer.term_offset > self.printer.max_print_line.saturating_sub(3) {
678 self.printer.print_ln();
679 } else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
680 self.printer.print_char(' ' as u32);
681 }
682 self.printer.print_str("( ");
683 self.input.open_parens += 1;
684 self.printer.update_terminal();
685 }
686 }
687
688 pub fn file_closed_checks(&mut self) {}
690
691 pub fn close_files_and_terminate(&mut self) {
694 for n in 0..128u32 {
695 if self.printer.write_open(n) {
696 self.printer.close_write(n);
697 }
698 }
699 self.print_memory_stats();
700 let pdf_mode = self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0;
704 let fatal = self.err.history == crate::error::History::FatalErrorStop && !self.pdf.fatal;
705 if fatal && pdf_mode {
706 self.pdf.fatal = true;
707 self.finish_output();
708 self.printer
709 .print_nl("! ==> Fatal error occurred, no output PDF file produced!");
710 if self.printer.log_opened() {
713 self.printer.print_ln();
714 }
715 let _ = std::fs::remove_file(self.output_path("pdf"));
716 self.finish_log();
717 return;
718 }
719 if fatal {
720 self.printer
721 .print_nl("! ==> Fatal error occurred, bad output DVI file produced!");
722 self.printer.print_ln();
723 }
724 self.finish_output();
725 if self.total_pages == 0 {
726 self.printer.print_nl(
727 if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0 {
728 "warning (pdf backend): no pages of output."
729 } else {
730 "No pages of output."
731 },
732 );
733 self.printer.print_ln();
734 if self.pdf.stream_written
735 && self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0
736 {
737 self.pdf_fatal("already written content discarded, no output file produced.");
738 }
739 }
740 if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0
742 && self.pdf.max_obj() > 0
743 && !self.pdf.fatal
744 && self.printer.log_opened()
745 {
746 if self.printer.file_offset > 0 {
747 self.printer.print_ln();
748 }
749 let n = self.pdf.max_obj();
750 let names = self.pdf.named_dests();
751 let extra = 1
755 + 8 * self.pdf.outlines.len()
756 + 10 * self.box_resources.len()
757 + self.pdf.obj_words;
758 let (objs, streams) = self.pdf.objstm_stats;
759 let compressed = if streams > 0 {
760 format!(
761 " {objs} compressed objects within {streams} object stream{}\n",
762 if streams == 1 { "" } else { "s" }
763 )
764 } else {
765 String::new()
766 };
767 self.printer.log_raw(
768 format!(
769 "\nPDF statistics: {n} PDF objects out of {} (max. 8388607)\n{compressed} \
770 {names} named destinations out of {} (max. 131072)\n \
771 {extra} words of extra memory for PDF output out of {} (max. 100000000)\n",
772 n.max(1000),
773 names.max(1000),
774 extra.max(10000),
775 )
776 .as_bytes(),
777 );
778 }
779 if self.printer.log_opened() {
780 self.printer.log_raw(b"\n");
781 }
782 self.finish_synctex();
783 self.finish_log();
784 }
785
786 fn print_memory_stats(&mut self) {
790 if self.diagnostics.is_some()
791 || !self.printer.log_opened()
792 || self
793 .eqtb
794 .int(crate::primitives_gen::assign_int::TRACINGSTATS)
795 <= 0
796 {
797 return;
798 }
799 let sel = self.printer.selector;
800 if sel == Selector::TermAndLog {
801 self.printer.selector = Selector::LogOnly;
802 }
803 self.printer
804 .print_nl("Here is how much of LuaTeX's memory you used:");
805 let cs = self.hash.len();
806 self.printer.print_nl(&format!(
807 " {cs} multiletter control sequences out of 65536+600000"
808 ));
809 let fonts = self.fonts.fonts.len();
810 self.printer.print_nl(&format!(" {fonts} fonts"));
811 self.printer.print_ln();
812 self.printer.selector = sel;
813 }
814
815 pub(crate) fn finish_log(&mut self) {
817 if self.printer.log_opened() {
818 self.printer.close_log();
819 self.printer.selector = Selector::TermOnly;
820 if let Some(name) = self.files.log_name.clone() {
821 let shown = match &self.files.output_dir {
824 Some(dir) if !self.files.log_in_texmfoutput => {
825 name.strip_prefix(dir).map(PathBuf::from).unwrap_or(name)
826 }
827 _ => name,
828 };
829 self.printer.print_nl("Transcript written on ");
830 self.printer.print_bytes(shown.to_string_lossy().as_bytes());
831 self.printer.print_char('.' as u32);
832 }
833 }
834 self.printer.print_ln();
835 self.printer.flush();
836 }
837}
838
839pub type WrittenSink = std::rc::Rc<std::cell::RefCell<HashMap<PathBuf, Vec<u8>>>>;
841
842pub struct RecordingWriter {
845 inner: std::io::BufWriter<std::fs::File>,
846 path: PathBuf,
847 sink: WrittenSink,
848}
849
850impl RecordingWriter {
851 pub fn new(f: std::fs::File, path: PathBuf, sink: WrittenSink) -> Self {
853 sink.borrow_mut().insert(path.clone(), Vec::new());
854 RecordingWriter {
855 inner: std::io::BufWriter::new(f),
856 path,
857 sink,
858 }
859 }
860}
861
862impl RecordingWriter {
863 pub fn resume(f: std::fs::File, path: PathBuf, sink: WrittenSink) -> Self {
866 sink.borrow_mut().entry(path.clone()).or_default();
867 RecordingWriter {
868 inner: std::io::BufWriter::new(f),
869 path,
870 sink,
871 }
872 }
873}
874
875impl std::io::Write for RecordingWriter {
876 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
877 if let Some(v) = self.sink.borrow_mut().get_mut(&self.path) {
878 v.extend_from_slice(buf);
879 }
880 self.inner.write(buf)
881 }
882
883 fn flush(&mut self) -> std::io::Result<()> {
884 self.inner.flush()
885 }
886}