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