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), for `--watch`.
84    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    /// Whether `\read` stream `n` is open.
106    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
114/// FNV-1a hash used to detect content changes between passes.
115pub 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    /// Reads a whole file, recording it for the pass driver.
126    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    /// Records a file lookup from Lua (`kpse.find_file`, which expl3 uses to
138    /// test whether a file exists) for the pass driver: files of the job
139    /// (relative names) are recorded with their content hash, or as absent.
140    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    /// Where a job file `name` that was not found would appear: in the
159    /// output directory, where the job writes its files, if there is one.
160    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    /// Records that file `name` was looked for and not found, so that the
168    /// pass driver reruns when this pass writes it (e.g. the `.aux` file
169    /// LaTeX tests for with `\filesize`).
170    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    /// Whether file `name` may be opened for `access` (kpathsea's
181    /// `openin_any` and `openout_any`); if not, kpathsea's warning goes to
182    /// the standard error stream, as with LuaTeX. Names are checked as
183    /// given, before any output directory or search path applies.
184    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    /// Looks a TeX input file up: in the output directory first, then via
198    /// kpathsea.
199    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    /// Scans a file name (`scan_file_name`). LuaTeX also accepts a name in
220    /// braces and names in double quotes.
221    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    /// The output of the command of a file name `|command` (a pipe), when
255    /// shell escape allows running it.
256    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    /// `\input` (`start_input`).
266    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            // `\input|"command"` reads the command's output.
271            (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        // Observed: LuaTeX shows a name with spaces in double quotes.
300        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        // Read the first line of the new file.
322        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    /// Asks for a file name after a lookup failure (`prompt_file_name`). In
353    /// non-interactive modes this is fatal.
354    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        // Observed: LuaTeX does not mention the default extension, and
367        // offers to retry only for input files.
368        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    /// Opens the log file and prints the banner (`open_log_file`).
378    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        // Without an output directory, a log that cannot be written in the
394        // current directory goes to TEXMFOUTPUT, and so do the files
395        // written after it (web2c manual, "Output file location").
396        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        // Print the banner line, including the date and time.
420        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    /// The first line of the log file.
443    /// The line after the banner that tells the shell escape mode (none
444    /// when it is disabled).
445    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    /// The terminal line is exhausted: in `\errorstopmode`/`\scrollmode`
479    /// ask for more input (§360), otherwise stop.
480    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    /// `\openin` and `\closein` (`open_or_close_in`).
508    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    /// Reads a line from a `\read` stream into a token list (`read_toks`,
537    /// §482). `j` is 1 for `\readline` (e-TeX). The result starts with the
538    /// `END_MATCH` token, as a macro body without parameters.
539    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            // Input and store tokens from the next line of the file.
552            let line: Vec<u8> = match self.files.read_files.get_mut(m) {
553                None | Some(ReadFile::Closed) => {
554                    // The (empty) line being read shows in the error
555                    // context as `<read *>` (§483).
556                    self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
557                    // Interactive terminal reading is not supported.
558                    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                // \readline: spaces become space tokens, the rest other.
597                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                    // An unmatched `}` aborts the line.
619                    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    /// `\scantokens` and `\scantextokens` (e-TeX `pseudo_start`).
641    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        // Split at the new-line character. As in etex.ch, each line ends
645        // at a new-line character or at the end of the string, so a final
646        // new-line character starts no further line and an empty string
647        // has no line at all.
648        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    /// e-TeX's check when a file ends inside groups or conditionals.
693    pub fn file_closed_checks(&mut self) {}
694
695    /// Closes everything at the end of the run
696    /// (`close_files_and_terminate`).
697    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        // Observed after a fatal error (an emergency stop): in PDF mode
705        // there is no PDF file and the log ends with LuaTeX's notice; in
706        // DVI mode the notice comes before the usual last lines.
707        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            // Observed: without a log (it could not be written) the line
715            // is left open.
716            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        // LuaTeX reports the objects it allocated (log only).
745        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            // The extra memory is not modelled exactly: LuaTeX uses 8
756            // words per outline entry, ten per box resource and five per
757            // object (observed), and more for threads.
758            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    /// LuaTeX's memory usage report, which ends the log when
791    /// `\tracingstats` is positive (compatibility mode only; just the
792    /// counts retex keeps).
793    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        // Observed: after an empty line.
808        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    /// Closes the log and names it on the terminal.
825    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                // The name as opened: in an -output-directory, LuaTeX shows
831                // it without the directory (observed).
832                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
848/// Shared record of the contents written to files during a pass.
849pub type WrittenSink = std::rc::Rc<std::cell::RefCell<HashMap<PathBuf, Vec<u8>>>>;
850
851/// A file writer that also records everything written, so that the pass
852/// driver can compare it with what the next pass reads.
853pub struct RecordingWriter {
854    inner: std::io::BufWriter<std::fs::File>,
855    path: PathBuf,
856    sink: WrittenSink,
857}
858
859impl RecordingWriter {
860    /// Wraps `f`, recording its content under `path`.
861    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    /// Wraps `f`, appending to what is already recorded under `path` (a
873    /// file reopened after resuming from a snapshot).
874    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}