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