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            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        // Observed: LuaTeX shows a name with spaces in double quotes.
301        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        // Read the first line of the new file.
323        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    /// Asks for a file name after a lookup failure (`prompt_file_name`). In
354    /// non-interactive modes this is fatal.
355    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        // Observed: LuaTeX does not mention the default extension, and
368        // offers to retry only for input files.
369        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    /// Opens the log file and prints the banner (`open_log_file`).
379    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        // Without an output directory, a log that cannot be written in the
395        // current directory goes to TEXMFOUTPUT, and so do the files
396        // written after it (web2c manual, "Output file location").
397        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        // Print the banner line, including the date and time.
421        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    /// The first line of the log file.
444    /// The line after the banner that tells the shell escape mode (none
445    /// when it is disabled).
446    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    /// The terminal line is exhausted: in `\errorstopmode`/`\scrollmode`
480    /// ask for more input (§360), otherwise stop.
481    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    /// `\openin` and `\closein` (`open_or_close_in`).
509    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    /// Reads a line from a `\read` stream into a token list (`read_toks`,
538    /// §482). `j` is 1 for `\readline` (e-TeX). The result starts with the
539    /// `END_MATCH` token, as a macro body without parameters.
540    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            // Input and store tokens from the next line of the file.
553            let line: Vec<u8> = match self.files.read_files.get_mut(m) {
554                None | Some(ReadFile::Closed) => {
555                    // The (empty) line being read shows in the error
556                    // context as `<read *>` (§483).
557                    self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
558                    // Interactive terminal reading is not supported.
559                    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                // \readline: spaces become space tokens, the rest other.
598                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                    // An unmatched `}` aborts the line.
620                    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    /// `\scantokens` and `\scantextokens` (e-TeX `pseudo_start`).
642    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        // Split at the new-line character. As in etex.ch, each line ends
646        // at a new-line character or at the end of the string, so a final
647        // new-line character starts no further line and an empty string
648        // has no line at all.
649        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    /// e-TeX's check when a file ends inside groups or conditionals.
694    pub fn file_closed_checks(&mut self) {}
695
696    /// Closes everything at the end of the run
697    /// (`close_files_and_terminate`).
698    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        // Observed after a fatal error (an emergency stop): in PDF mode
706        // there is no PDF file and the log ends with LuaTeX's notice; in
707        // DVI mode the notice comes before the usual last lines.
708        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            // Observed: without a log (it could not be written) the line
716            // is left open.
717            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        // LuaTeX reports the objects it allocated (log only).
746        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            // The extra memory is not modelled exactly: LuaTeX uses 8
757            // words per outline entry, ten per box resource and five per
758            // object (observed), and more for threads.
759            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    /// LuaTeX's memory usage report, which ends the log when
792    /// `\tracingstats` is positive (compatibility mode only; just the
793    /// counts retex keeps).
794    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        // Observed: after an empty line.
809        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    /// Closes the log and names it on the terminal.
826    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                // The name as opened: in an -output-directory, LuaTeX shows
832                // it without the directory (observed).
833                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
849/// Shared record of the contents written to files during a pass.
850pub type WrittenSink = std::rc::Rc<std::cell::RefCell<HashMap<PathBuf, Vec<u8>>>>;
851
852/// A file writer that also records everything written, so that the pass
853/// driver can compare it with what the next pass reads.
854pub struct RecordingWriter {
855    inner: std::io::BufWriter<std::fs::File>,
856    path: PathBuf,
857    sink: WrittenSink,
858}
859
860impl RecordingWriter {
861    /// Wraps `f`, recording its content under `path`.
862    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    /// Wraps `f`, appending to what is already recorded under `path` (a
874    /// file reopened after resuming from a snapshot).
875    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}