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 let data = self.extend_package(&found, data);
283 (found, data)
284 };
285 if let Some(f) = self.progress_hook {
286 f(self);
287 }
288 if !self.printer.log_opened() && self.job_name.is_some() {
289 self.open_log_file();
290 }
291 if self.job_name.is_none() {
292 let base = Path::new(&name_str)
293 .file_stem()
294 .map(|s| s.to_string_lossy().into_owned())
295 .unwrap_or_else(|| name_str.clone());
296 self.job_name = Some(base.into_bytes());
297 self.open_log_file();
298 }
299 let display = found.to_string_lossy().into_owned().into_bytes();
300 let shown = if display.contains(&b' ') {
302 [&b"\""[..], &display, b"\""].concat()
303 } else {
304 display.clone()
305 };
306 self.check_in_open();
307 if self.printer.term_offset + shown.len() > self.printer.max_print_line.saturating_sub(2) {
308 self.printer.print_ln();
309 } else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
310 self.printer.print_char(' ' as u32);
311 }
312 self.printer.print_char('(' as u32);
313 self.input.open_parens += 1;
314 self.printer.print_bytes(&shown);
315 self.printer.update_terminal();
316 let source = LineSource::File {
317 data: Rc::from(data),
318 pos: 0,
319 };
320 self.begin_file_reading(source, display, true);
321 self.synctex_input_opened(&found);
322 self.read_first_line();
324 }
325
326 fn read_first_line(&mut self) {
327 let end_line_char = self.eqtb.int(pg::assign_int::ENDLINECHAR);
328 if let Some(Level::Text(t)) = self.input.stack.last_mut() {
329 t.line = 0;
330 let line = match &mut t.source {
331 LineSource::File { data, pos } => match crate::input::next_line(data, *pos) {
332 Some((bytes, next)) => {
333 let l = crate::input::decode_line(bytes);
334 *pos = next;
335 Some(l)
336 }
337 None => None,
338 },
339 _ => None,
340 };
341 t.line = 1;
342 t.sync.1 = 1;
343 t.buf = line.unwrap_or_default();
344 if (0..=0x10_FFFF).contains(&end_line_char) {
345 t.buf.push(end_line_char as u32);
346 }
347 t.loc = 0;
348 t.state = crate::input::LexState::NewLine;
349 }
350 self.sync_input_changed();
351 }
352
353 pub fn prompt_file_name(&mut self, name: &[u8], s: &str, e: &str) -> ! {
356 if s == "input file name" {
357 self.print_err("I can't find file `");
358 } else {
359 self.print_err("I can't write on file `");
360 }
361 self.printer.print_bytes(name);
362 self.printer.print_str("'.");
363 if e == ".tex" {
364 self.show_context();
365 }
366 self.printer.print_ln();
367 if s == "input file name" {
370 self.printer
371 .print_str("(Press Enter to retry, or Control-D to exit)");
372 }
373 self.printer.print_nl("Please type another ");
374 self.printer.print_str(s);
375 self.fatal_error("*** (job aborted, file error in nonstop mode)");
376 }
377
378 pub fn open_log_file(&mut self) {
380 let old = self.printer.selector;
381 if self.job_name.is_none() {
382 self.job_name = Some(b"texput".to_vec());
383 }
384 let job = String::from_utf8_lossy(self.job_name.as_ref().unwrap()).into_owned();
385 let name = format!("{job}.log");
386 if !self.name_ok(&name, retex_kpath::Access::Write) {
387 self.prompt_file_name(name.as_bytes(), "transcript file name", ".log");
388 }
389 let mut path = PathBuf::from(&name);
390 if let Some(dir) = &self.files.output_dir {
391 path = dir.join(path);
392 }
393 let mut file = std::fs::File::create(&path);
394 if file.is_err()
398 && self.files.output_dir.is_none()
399 && let Some(dir) = self
400 .kpse
401 .as_ref()
402 .and_then(|k| k.borrow().var_value("TEXMFOUTPUT"))
403 .filter(|d| !d.is_empty())
404 {
405 let dir = PathBuf::from(dir);
406 file = std::fs::File::create(dir.join(&name));
407 if file.is_ok() {
408 path = dir.join(&name);
409 self.files.output_dir = Some(dir);
410 self.files.log_in_texmfoutput = true;
411 }
412 }
413 match file {
414 Ok(f) => self
415 .printer
416 .set_log(Box::new(std::io::BufWriter::with_capacity(1 << 16, f))),
417 Err(_) => self.prompt_file_name(name.as_bytes(), "transcript file name", ".log"),
418 }
419 self.files.log_name = Some(path);
420 let banner = self.log_banner();
422 self.printer.log_raw(banner.as_bytes());
423 let shell = self.shell_escape_banner();
424 if shell.is_empty() {
425 self.printer.file_offset = banner.len();
426 } else {
427 self.printer.log_raw(b"\n");
428 self.printer.log_raw(shell.as_bytes());
429 self.printer.file_offset = shell.len();
430 }
431 self.printer.selector = Selector::LogOnly;
432 self.printer.print_nl("**");
433 let line = self.files.command_line.clone();
434 self.printer.print_bytes(&line);
435 self.printer.print_ln();
436 self.printer.selector = match old {
437 Selector::NoPrint => Selector::LogOnly,
438 Selector::TermOnly => Selector::TermAndLog,
439 s => s,
440 };
441 }
442
443 pub fn shell_escape_banner(&self) -> &'static str {
447 match self.shell_escape {
448 0 => "",
449 1 => " system commands enabled.",
450 _ => " restricted system commands enabled.",
451 }
452 }
453
454 fn log_banner(&self) -> String {
455 let c = self.start_time.civil();
456 const MONTHS: [&str; 12] = [
457 "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
458 ];
459 format!(
460 "This is retex, Version {} (LuaTeX 1.22.0 compatible) ({}) {} {} {} {:02}:{:02}",
461 env!("CARGO_PKG_VERSION"),
462 if self.ini_version {
463 "INITEX".to_owned()
464 } else {
465 self.format_ident
466 .trim()
467 .trim_start_matches('(')
468 .trim_end_matches(')')
469 .to_owned()
470 },
471 c.day,
472 MONTHS[(c.month as usize).saturating_sub(1).min(11)],
473 c.year,
474 c.hour,
475 c.minute
476 )
477 }
478
479 pub fn terminal_exhausted(&mut self) {
482 if self.printer.selector == Selector::NoPrint || self.printer.selector == Selector::TermOnly
483 {
484 self.open_log_file();
485 }
486 if self.err.interaction > crate::error::Interaction::NonStop {
487 let empty = matches!(self.input.stack.last(), Some(Level::Text(t)) if t.buf.is_empty());
488 if empty {
489 self.printer
490 .print_nl("(Please type a command or say `\\end')");
491 }
492 self.printer.print_ln();
493 let mut line = self.prompt_input("*");
494 let elc = self.eqtb.int(pg::assign_int::ENDLINECHAR);
495 if (0..=0x10_FFFF).contains(&elc) {
496 line.push(elc as u32);
497 }
498 if let Some(Level::Text(t)) = self.input.stack.last_mut() {
499 t.buf = line;
500 t.loc = 0;
501 t.state = crate::input::LexState::NewLine;
502 }
503 return;
504 }
505 self.fatal_error("*** (job aborted, no legal \\end found)");
506 }
507
508 pub fn open_or_close_in(&mut self) {
510 let c = self.cur_chr;
511 self.scan_four_bit_int();
512 let n = self.cur_val as usize;
513 self.files.read_files[n] = ReadFile::Closed;
514 if c != 0 {
515 self.scan_optional_equals();
516 let name = self.scan_file_name();
517 if name.first() == Some(&b'|') {
518 if let Some(out) = self.read_pipe(&name) {
519 self.files.read_files[n] = ReadFile::JustOpen(LineReader::new(out));
520 }
521 return;
522 }
523 let name = String::from_utf8_lossy(&name).into_owned();
524 if !self.name_ok(&name, retex_kpath::Access::Read) {
525 return;
526 }
527 if let Some(p) = self.find_input_file(&name, retex_kpath::FileFormat::Tex)
528 && let Some(data) = self.read_file_recorded(&p)
529 {
530 self.files.read_files[n] = ReadFile::JustOpen(LineReader::new(data));
531 } else {
532 self.record_absent_file(&name);
533 }
534 }
535 }
536
537 pub fn read_toks(&mut self, n: i32, r: crate::hash::CsId, j: u32) -> Vec<Token> {
541 self.input.scanner_status = ScannerStatus::Defining;
542 self.input.warning_index = r;
543 let mut out: Vec<Token> = vec![Token::END_MATCH];
544 let m: usize = if (0..=15).contains(&n) {
545 n as usize
546 } else {
547 16
548 };
549 let s = self.input.align_state;
550 self.input.align_state = 1_000_000;
551 loop {
552 let line: Vec<u8> = match self.files.read_files.get_mut(m) {
554 None | Some(ReadFile::Closed) => {
555 self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
558 self.fatal_error("*** (cannot \\read from terminal in nonstop modes)");
560 }
561 Some(ReadFile::JustOpen(lines)) => {
562 let first = lines.pop_front();
563 match first {
564 Some(l) => {
565 let rest = std::mem::take(lines);
566 self.files.read_files[m] = ReadFile::Normal(rest);
567 l
568 }
569 None => {
570 self.files.read_files[m] = ReadFile::Closed;
571 Vec::new()
572 }
573 }
574 }
575 Some(ReadFile::Normal(lines)) => match lines.pop_front() {
576 Some(l) => l,
577 None => {
578 self.files.read_files[m] = ReadFile::Closed;
579 if self.input.align_state != 1_000_000 {
580 self.runaway();
581 self.print_err("File ended within ");
582 self.print_esc_str("read");
583 self.help(&["This \\read has unbalanced braces."]);
584 self.input.align_state = 1_000_000;
585 self.error();
586 }
587 Vec::new()
588 }
589 },
590 };
591 let end_line_char = self.eqtb.int(pg::assign_int::ENDLINECHAR);
592 let mut buf = crate::input::decode_line(&line);
593 if (0..=0x10_FFFF).contains(&end_line_char) {
594 buf.push(end_line_char as u32);
595 }
596 if j == 1 {
597 for &c in &buf {
599 out.push(if c == ' ' as u32 {
600 Token::SPACE
601 } else {
602 Token::other(c)
603 });
604 }
605 break;
606 }
607 self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
608 if let Some(Level::Text(t)) = self.input.stack.last_mut() {
609 t.buf = buf;
610 t.loc = 0;
611 t.state = crate::input::LexState::NewLine;
612 }
613 loop {
614 self.get_token();
615 if self.cur_tok.0 == 0 {
616 break;
617 }
618 if self.input.align_state < 1_000_000 {
619 loop {
621 self.get_token();
622 if self.cur_tok.0 == 0 {
623 break;
624 }
625 }
626 self.input.align_state = 1_000_000;
627 break;
628 }
629 out.push(self.cur_tok);
630 }
631 self.end_file_reading();
632 if self.input.align_state == 1_000_000 {
633 break;
634 }
635 }
636 self.input.scanner_status = ScannerStatus::Normal;
637 self.input.align_state = s;
638 out
639 }
640
641 pub fn pseudo_start(&mut self, text_tokens: bool) {
643 let list = self.scan_general_text();
644 let s = self.print_to_string(|e| e.show_token_list(&list, 0, list.len(), i64::MAX));
645 let nl = self.eqtb.int(pg::assign_int::NEWLINECHAR);
650 let mut lines: VecDeque<Vec<u8>> = VecDeque::new();
651 let mut rest = s.as_slice();
652 while !rest.is_empty() {
653 let end = if (0..128).contains(&nl) {
654 rest.iter().position(|&b| i32::from(b) == nl)
655 } else {
656 None
657 };
658 let (line, tail) = match end {
659 Some(i) => (&rest[..i], &rest[i + 1..]),
660 None => (rest, &rest[rest.len()..]),
661 };
662 lines.push_back(line.to_vec());
663 rest = tail;
664 }
665 let tracing = self.eqtb.int(pg::assign_int::TRACINGSCANTOKENS) > 0 && !text_tokens;
666 self.begin_file_reading(
667 LineSource::Lines {
668 lines,
669 catcodes: None,
670 pseudo: !text_tokens,
671 },
672 Vec::new(),
673 tracing,
674 );
675 if let Some(Level::Text(t)) = self.input.stack.last_mut() {
676 t.line = 0;
677 t.buf.clear();
678 t.loc = 0;
679 t.eof_seen = text_tokens;
680 }
681 if tracing {
682 if self.printer.term_offset > self.printer.max_print_line.saturating_sub(3) {
683 self.printer.print_ln();
684 } else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
685 self.printer.print_char(' ' as u32);
686 }
687 self.printer.print_str("( ");
688 self.input.open_parens += 1;
689 self.printer.update_terminal();
690 }
691 }
692
693 pub fn file_closed_checks(&mut self) {}
695
696 pub fn close_files_and_terminate(&mut self) {
699 for n in 0..128u32 {
700 if self.printer.write_open(n) {
701 self.printer.close_write(n);
702 }
703 }
704 self.print_memory_stats();
705 let pdf_mode = self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0;
709 let fatal = self.err.history == crate::error::History::FatalErrorStop && !self.pdf.fatal;
710 if fatal && pdf_mode {
711 self.pdf.fatal = true;
712 self.finish_output();
713 self.printer
714 .print_nl("! ==> Fatal error occurred, no output PDF file produced!");
715 if self.printer.log_opened() {
718 self.printer.print_ln();
719 }
720 let _ = std::fs::remove_file(self.output_path("pdf"));
721 self.finish_log();
722 return;
723 }
724 if fatal {
725 self.printer
726 .print_nl("! ==> Fatal error occurred, bad output DVI file produced!");
727 self.printer.print_ln();
728 }
729 self.finish_output();
730 if self.total_pages == 0 {
731 self.printer.print_nl(
732 if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0 {
733 "warning (pdf backend): no pages of output."
734 } else {
735 "No pages of output."
736 },
737 );
738 self.printer.print_ln();
739 if self.pdf.stream_written
740 && self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0
741 {
742 self.pdf_fatal("already written content discarded, no output file produced.");
743 }
744 }
745 if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0
747 && self.pdf.max_obj() > 0
748 && !self.pdf.fatal
749 && self.printer.log_opened()
750 {
751 if self.printer.file_offset > 0 {
752 self.printer.print_ln();
753 }
754 let n = self.pdf.max_obj();
755 let names = self.pdf.named_dests();
756 let extra = 1
760 + 8 * self.pdf.outlines.len()
761 + 10 * self.box_resources.len()
762 + self.pdf.obj_words;
763 let (objs, streams) = self.pdf.objstm_stats;
764 let compressed = if streams > 0 {
765 format!(
766 " {objs} compressed objects within {streams} object stream{}\n",
767 if streams == 1 { "" } else { "s" }
768 )
769 } else {
770 String::new()
771 };
772 self.printer.log_raw(
773 format!(
774 "\nPDF statistics: {n} PDF objects out of {} (max. 8388607)\n{compressed} \
775 {names} named destinations out of {} (max. 131072)\n \
776 {extra} words of extra memory for PDF output out of {} (max. 100000000)\n",
777 n.max(1000),
778 names.max(1000),
779 extra.max(10000),
780 )
781 .as_bytes(),
782 );
783 }
784 if self.printer.log_opened() {
785 self.printer.log_raw(b"\n");
786 }
787 self.finish_synctex();
788 self.finish_log();
789 }
790
791 fn print_memory_stats(&mut self) {
795 if self.diagnostics.is_some()
796 || !self.printer.log_opened()
797 || self
798 .eqtb
799 .int(crate::primitives_gen::assign_int::TRACINGSTATS)
800 <= 0
801 {
802 return;
803 }
804 let sel = self.printer.selector;
805 if sel == Selector::TermAndLog {
806 self.printer.selector = Selector::LogOnly;
807 }
808 if self.printer.file_offset > 0 {
810 self.printer.print_ln();
811 }
812 self.printer.print_ln();
813 self.printer
814 .print_str("Here is how much of LuaTeX's memory you used:");
815 let cs = self.hash.len();
816 self.printer.print_nl(&format!(
817 " {cs} multiletter control sequences out of 65536+600000"
818 ));
819 let fonts = self.fonts.fonts.len();
820 self.printer.print_nl(&format!(" {fonts} fonts"));
821 self.printer.print_ln();
822 self.printer.selector = sel;
823 }
824
825 pub(crate) fn finish_log(&mut self) {
827 if self.printer.log_opened() {
828 self.printer.close_log();
829 self.printer.selector = Selector::TermOnly;
830 if let Some(name) = self.files.log_name.clone() {
831 let shown = match &self.files.output_dir {
834 Some(dir) if !self.files.log_in_texmfoutput => {
835 name.strip_prefix(dir).map(PathBuf::from).unwrap_or(name)
836 }
837 _ => name,
838 };
839 self.printer.print_nl("Transcript written on ");
840 self.printer.print_bytes(shown.to_string_lossy().as_bytes());
841 self.printer.print_char('.' as u32);
842 }
843 }
844 self.printer.print_ln();
845 self.printer.flush();
846 }
847}
848
849pub type WrittenSink = std::rc::Rc<std::cell::RefCell<HashMap<PathBuf, Vec<u8>>>>;
851
852pub struct RecordingWriter {
855 inner: std::io::BufWriter<std::fs::File>,
856 path: PathBuf,
857 sink: WrittenSink,
858}
859
860impl RecordingWriter {
861 pub fn new(f: std::fs::File, path: PathBuf, sink: WrittenSink) -> Self {
863 sink.borrow_mut().insert(path.clone(), Vec::new());
864 RecordingWriter {
865 inner: std::io::BufWriter::new(f),
866 path,
867 sink,
868 }
869 }
870}
871
872impl RecordingWriter {
873 pub fn resume(f: std::fs::File, path: PathBuf, sink: WrittenSink) -> Self {
876 sink.borrow_mut().entry(path.clone()).or_default();
877 RecordingWriter {
878 inner: std::io::BufWriter::new(f),
879 path,
880 sink,
881 }
882 }
883}
884
885impl std::io::Write for RecordingWriter {
886 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
887 if let Some(v) = self.sink.borrow_mut().get_mut(&self.path) {
888 v.extend_from_slice(buf);
889 }
890 self.inner.write(buf)
891 }
892
893 fn flush(&mut self) -> std::io::Result<()> {
894 self.inner.flush()
895 }
896}