Skip to main content

retex_engine/
files.rs

1//! Files: `\input`, the log file, `\openin`/`\read`, `\scantokens`
2//! pseudo-files and the record of files read and written that the pass
3//! driver uses (*TeX: The Program* §511–§538, §480–§486; e-TeX).
4
5use 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/// The state of a `\read` stream.
17#[derive(Clone, Debug)]
18pub enum ReadFile {
19    /// Closed.
20    Closed,
21    /// Open; no line read yet.
22    JustOpen(LineReader),
23    /// Open, at least one line read.
24    Normal(LineReader),
25}
26
27/// The lines of a file opened by `\openin`, split when they are read:
28/// files are often opened only to test that they exist.
29#[derive(Clone, Debug, Default)]
30pub struct LineReader {
31    data: Vec<u8>,
32    pos: usize,
33}
34
35impl LineReader {
36    /// Reads the lines of `data`.
37    pub fn new(data: Vec<u8>) -> Self {
38        LineReader { data, pos: 0 }
39    }
40
41    /// The next line (without its terminator), if any.
42    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/// A file that was read or written during a pass, with a content hash.
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct FileRecord {
53    /// The path as opened.
54    pub path: PathBuf,
55    /// FNV-1a hash of the content, or `None` if the file did not exist.
56    pub hash: Option<u64>,
57}
58
59/// File-related state.
60#[derive(Debug)]
61pub struct FileState {
62    /// `\read` streams 0–15.
63    pub read_files: Vec<ReadFile>,
64    /// Whether a file name is being scanned (`name_in_progress`).
65    pub name_in_progress: bool,
66    /// The directory for output files (`-output-directory`).
67    pub output_dir: Option<PathBuf>,
68    /// Whether `output_dir` is retex's own directory for auxiliary files
69    /// (`.retex/…`), not one the user asked for.
70    pub private_output_dir: bool,
71    /// Files read during this pass, in order of first access.
72    pub read_log: Vec<FileRecord>,
73    /// Content of files written during this pass, by path.
74    pub written: HashMap<PathBuf, Vec<u8>>,
75    /// The first line given on the command line.
76    pub command_line: Vec<u8>,
77    /// The log file name, once opened.
78    pub log_name: Option<PathBuf>,
79    /// Whether the log went to TEXMFOUTPUT (the current directory was not
80    /// writable).
81    pub log_in_texmfoutput: bool,
82    /// Further files the job depends on that the pass driver does not
83    /// compare between passes (images, OpenType fonts), for `--watch` and
84    /// `-recorder`.
85    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    /// Whether `\read` stream `n` is open.
107    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
115/// FNV-1a hash used to detect content changes between passes.
116pub 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    /// Reads a whole file, recording it for the pass driver.
127    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    /// Records a file lookup from Lua (`kpse.find_file`, which expl3 uses to
139    /// test whether a file exists) for the pass driver: files of the job
140    /// (relative names) are recorded with their content hash, or as absent.
141    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    /// Where a job file `name` that was not found would appear: in the
160    /// output directory, where the job writes its files, if there is one.
161    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    /// Records that file `name` was looked for and not found, so that the
169    /// pass driver reruns when this pass writes it (e.g. the `.aux` file
170    /// LaTeX tests for with `\filesize`).
171    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    /// Whether file `name` may be opened for `access` (kpathsea's
182    /// `openin_any` and `openout_any`); if not, kpathsea's warning goes to
183    /// the standard error stream, as with LuaTeX. Names are checked as
184    /// given, before any output directory or search path applies.
185    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    /// Looks a TeX input file up: in the output directory first, then via
199    /// kpathsea.
200    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    /// Scans a file name (`scan_file_name`). LuaTeX also accepts a name in
221    /// braces and names in double quotes.
222    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    /// The output of the command of a file name `|command` (a pipe), when
256    /// shell escape allows running it.
257    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    /// `\input` (`start_input`).
267    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            // `\input|"command"` reads the command's output.
272            (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        // Observed: LuaTeX shows a name with spaces in double quotes.
302        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        // Read the first line of the new file.
324        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    /// Asks for a file name after a lookup failure (`prompt_file_name`). In
355    /// non-interactive modes this is fatal.
356    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        // Observed: LuaTeX does not mention the default extension, and
369        // offers to retry only for input files.
370        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    /// Opens the log file and prints the banner (`open_log_file`).
380    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        // Without an output directory, a log that cannot be written in the
396        // current directory goes to TEXMFOUTPUT, and so do the files
397        // written after it (web2c manual, "Output file location").
398        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        // Print the banner line, including the date and time.
422        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    /// The first line of the log file.
445    /// The line after the banner that tells the shell escape mode (none
446    /// when it is disabled).
447    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    /// The terminal line is exhausted: in `\errorstopmode`/`\scrollmode`
481    /// ask for more input (§360), otherwise stop.
482    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    /// `\openin` and `\closein` (`open_or_close_in`).
510    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    /// Reads a line from a `\read` stream into a token list (`read_toks`,
539    /// §482). `j` is 1 for `\readline` (e-TeX). The result starts with the
540    /// `END_MATCH` token, as a macro body without parameters.
541    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            // Input and store tokens from the next line of the file.
554            let line: Vec<u8> = match self.files.read_files.get_mut(m) {
555                None | Some(ReadFile::Closed) => {
556                    // The (empty) line being read shows in the error
557                    // context as `<read *>` (§483).
558                    self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
559                    // Interactive terminal reading is not supported.
560                    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                // \readline: spaces become space tokens, the rest other.
599                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                    // An unmatched `}` aborts the line.
621                    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    /// `\scantokens` and `\scantextokens` (e-TeX `pseudo_start`).
643    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        // Split at the new-line character. As in etex.ch, each line ends
647        // at a new-line character or at the end of the string, so a final
648        // new-line character starts no further line and an empty string
649        // has no line at all.
650        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    /// e-TeX's check when a file ends inside groups or conditionals.
695    pub fn file_closed_checks(&mut self) {}
696
697    /// Closes everything at the end of the run
698    /// (`close_files_and_terminate`).
699    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        // Observed after a fatal error (an emergency stop): in PDF mode
707        // there is no PDF file and the log ends with LuaTeX's notice; in
708        // DVI mode the notice comes before the usual last lines.
709        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            // Observed: without a log (it could not be written) the line
717            // is left open.
718            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        // LuaTeX reports the objects it allocated (log only).
747        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            // The extra memory is not modelled exactly: LuaTeX uses 8
758            // words per outline entry, ten per box resource and five per
759            // object (observed), and more for threads.
760            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    /// LuaTeX's memory usage report, which ends the log when
793    /// `\tracingstats` is positive (compatibility mode only; just the
794    /// counts retex keeps).
795    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        // Observed: after an empty line.
810        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    /// Closes the log and names it on the terminal.
827    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                // The name as opened: in an -output-directory, LuaTeX shows
833                // it without the directory (observed).
834                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
850/// Shared record of the contents written to files during a pass.
851pub type WrittenSink = std::rc::Rc<std::cell::RefCell<HashMap<PathBuf, Vec<u8>>>>;
852
853/// A file writer that also records everything written, so that the pass
854/// driver can compare it with what the next pass reads.
855pub struct RecordingWriter {
856    inner: std::io::BufWriter<std::fs::File>,
857    path: PathBuf,
858    sink: WrittenSink,
859}
860
861impl RecordingWriter {
862    /// Wraps `f`, recording its content under `path`.
863    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    /// Wraps `f`, appending to what is already recorded under `path` (a
875    /// file reopened after resuming from a snapshot).
876    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}