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    /// Files read during this pass, in order of first access.
69    pub read_log: Vec<FileRecord>,
70    /// Content of files written during this pass, by path.
71    pub written: HashMap<PathBuf, Vec<u8>>,
72    /// The first line given on the command line.
73    pub command_line: Vec<u8>,
74    /// The log file name, once opened.
75    pub log_name: Option<PathBuf>,
76    /// Whether the log went to TEXMFOUTPUT (the current directory was not
77    /// writable).
78    pub log_in_texmfoutput: bool,
79    /// Further files the job depends on that the pass driver does not
80    /// compare between passes (images), for `--watch`.
81    pub watched: Vec<PathBuf>,
82}
83
84impl Default for FileState {
85    fn default() -> Self {
86        FileState {
87            read_files: vec![ReadFile::Closed; 16],
88            name_in_progress: false,
89            output_dir: None,
90            read_log: Vec::new(),
91            written: HashMap::new(),
92            command_line: Vec::new(),
93            log_name: None,
94            log_in_texmfoutput: false,
95            watched: Vec::new(),
96        }
97    }
98}
99
100impl FileState {
101    /// Whether `\read` stream `n` is open.
102    pub fn read_open(&self, n: u32) -> bool {
103        !matches!(
104            self.read_files.get(n as usize),
105            None | Some(ReadFile::Closed)
106        )
107    }
108}
109
110/// FNV-1a hash used to detect content changes between passes.
111pub fn content_hash(data: &[u8]) -> u64 {
112    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
113    for &b in data {
114        h ^= u64::from(b);
115        h = h.wrapping_mul(0x0100_0000_01b3);
116    }
117    h
118}
119
120impl Engine {
121    /// Reads a whole file, recording it for the pass driver.
122    pub fn read_file_recorded(&mut self, path: &Path) -> Option<Vec<u8>> {
123        let data = std::fs::read(path).ok();
124        if !self.files.read_log.iter().any(|r| r.path == path) {
125            self.files.read_log.push(FileRecord {
126                path: path.to_path_buf(),
127                hash: data.as_deref().map(content_hash),
128            });
129        }
130        data
131    }
132
133    /// Records a file lookup from Lua (`kpse.find_file`, which expl3 uses to
134    /// test whether a file exists) for the pass driver: files of the job
135    /// (relative names) are recorded with their content hash, or as absent.
136    pub fn record_probe(&mut self, name: &str, found: Option<&Path>) {
137        let path = match found {
138            Some(p) if p.is_relative() => p.to_path_buf(),
139            Some(_) => return,
140            None if !Path::new(name).is_absolute() && !name.contains("//") => {
141                self.absent_path(name)
142            }
143            None => return,
144        };
145        if self.files.read_log.iter().any(|r| r.path == path) {
146            return;
147        }
148        let hash = found
149            .and_then(|p| std::fs::read(p).ok())
150            .map(|d| content_hash(&d));
151        self.files.read_log.push(FileRecord { path, hash });
152    }
153
154    /// Where a job file `name` that was not found would appear: in the
155    /// output directory, where the job writes its files, if there is one.
156    fn absent_path(&self, name: &str) -> PathBuf {
157        match &self.files.output_dir {
158            Some(d) if Path::new(name).is_relative() => d.join(name),
159            _ => PathBuf::from(name),
160        }
161    }
162
163    /// Records that file `name` was looked for and not found, so that the
164    /// pass driver reruns when this pass writes it (e.g. the `.aux` file
165    /// LaTeX tests for with `\filesize`).
166    pub fn record_absent_file(&mut self, name: &str) {
167        let p = self.absent_path(name);
168        if !self.files.read_log.iter().any(|r| r.path == p) {
169            self.files.read_log.push(FileRecord {
170                path: p,
171                hash: None,
172            });
173        }
174    }
175
176    /// Whether file `name` may be opened for `access` (kpathsea's
177    /// `openin_any` and `openout_any`); if not, kpathsea's warning goes to
178    /// the standard error stream, as with LuaTeX. Names are checked as
179    /// given, before any output directory or search path applies.
180    pub fn name_ok(&self, name: &str, access: retex_kpath::Access) -> bool {
181        let Some(k) = &self.kpse else {
182            return true;
183        };
184        let refusal = match k.borrow().name_ok(name, access, false) {
185            Ok(()) => return true,
186            Err(r) => r,
187        };
188        let program = std::env::args().next().unwrap_or_else(|| "retex".into());
189        eprint!("\n{program}: {}\n", refusal.message(name));
190        false
191    }
192
193    /// Looks a TeX input file up: in the output directory first, then via
194    /// kpathsea.
195    pub fn find_input_file(&mut self, name: &str, fmt: retex_kpath::FileFormat) -> Option<PathBuf> {
196        if let Some(dir) = &self.files.output_dir {
197            for cand in [format!("{name}.tex"), name.to_owned()] {
198                let p = dir.join(&cand);
199                if p.is_file() {
200                    return Some(p);
201                }
202            }
203        }
204        if let Some(k) = &self.kpse {
205            return k.borrow_mut().find_file(name, fmt);
206        }
207        let p = PathBuf::from(name);
208        if p.is_file() {
209            return Some(p);
210        }
211        let p = PathBuf::from(format!("{name}.tex"));
212        p.is_file().then_some(p)
213    }
214
215    /// Scans a file name (`scan_file_name`). LuaTeX also accepts a name in
216    /// braces and names in double quotes.
217    pub fn scan_file_name(&mut self) -> Vec<u8> {
218        self.files.name_in_progress = true;
219        let mut name: Vec<u8> = Vec::new();
220        self.get_nonblank_noncall();
221        if self.cur_cmd == Cmd::LeftBrace {
222            self.back_input();
223            let toks = self.scan_toks_expanded();
224            let s = self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
225            name = s.into_iter().filter(|&b| b != b'"').collect();
226            self.files.name_in_progress = false;
227            return name;
228        }
229        let mut quoted = false;
230        loop {
231            if self.cur_cmd > Cmd::OtherChar || self.cur_cs != crate::hash::NO_CS {
232                self.back_input();
233                break;
234            }
235            let c = self.cur_chr;
236            if c == ' ' as u32 && !quoted {
237                break;
238            }
239            if c == '"' as u32 {
240                quoted = !quoted;
241            } else {
242                crate::utf8x::push(&mut name, c);
243            }
244            self.get_x_token();
245        }
246        self.files.name_in_progress = false;
247        name
248    }
249
250    /// The output of the command of a file name `|command` (a pipe), when
251    /// shell escape allows running it.
252    fn read_pipe(&mut self, name: &[u8]) -> Option<Vec<u8>> {
253        let cmd = name.strip_prefix(b"|")?;
254        if self.shell_escape == 0 {
255            return None;
256        }
257        let host = self.lua.clone()?;
258        host.pipe_read(self, cmd)
259    }
260
261    /// `\input` (`start_input`).
262    pub fn start_input(&mut self) {
263        let name = self.scan_file_name();
264        let name_str = String::from_utf8_lossy(&name).into_owned();
265        let (found, data) = if let Some(out) = self.read_pipe(&name) {
266            // `\input|"command"` reads the command's output.
267            (PathBuf::from(&name_str), out)
268        } else {
269            let allowed = self.name_ok(&name_str, retex_kpath::Access::Read);
270            let found = match allowed
271                .then(|| self.find_input_file(&name_str, retex_kpath::FileFormat::Tex))
272                .flatten()
273            {
274                Some(p) => p,
275                None => self.prompt_file_name(&name, "input file name", ".tex"),
276            };
277            let data = self.read_file_recorded(&found).unwrap_or_default();
278            (found, data)
279        };
280        if let Some(f) = self.progress_hook {
281            f(self);
282        }
283        if !self.printer.log_opened() && self.job_name.is_some() {
284            self.open_log_file();
285        }
286        if self.job_name.is_none() {
287            let base = Path::new(&name_str)
288                .file_stem()
289                .map(|s| s.to_string_lossy().into_owned())
290                .unwrap_or_else(|| name_str.clone());
291            self.job_name = Some(base.into_bytes());
292            self.open_log_file();
293        }
294        let display = found.to_string_lossy().into_owned().into_bytes();
295        // Observed: LuaTeX shows a name with spaces in double quotes.
296        let shown = if display.contains(&b' ') {
297            [&b"\""[..], &display, b"\""].concat()
298        } else {
299            display.clone()
300        };
301        if self.printer.term_offset + shown.len() > self.printer.max_print_line.saturating_sub(2) {
302            self.printer.print_ln();
303        } else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
304            self.printer.print_char(' ' as u32);
305        }
306        self.check_in_open();
307        self.printer.print_char('(' as u32);
308        self.input.open_parens += 1;
309        self.printer.print_bytes(&shown);
310        self.printer.update_terminal();
311        let source = LineSource::File {
312            data: Rc::from(data),
313            pos: 0,
314        };
315        self.begin_file_reading(source, display, true);
316        self.synctex_input_opened(&found);
317        // Read the first line of the new file.
318        self.read_first_line();
319    }
320
321    fn read_first_line(&mut self) {
322        let end_line_char = self.eqtb.int(pg::assign_int::ENDLINECHAR);
323        if let Some(Level::Text(t)) = self.input.stack.last_mut() {
324            t.line = 0;
325            let line = match &mut t.source {
326                LineSource::File { data, pos } => match crate::input::next_line(data, *pos) {
327                    Some((bytes, next)) => {
328                        let l = crate::input::decode_line(bytes);
329                        *pos = next;
330                        Some(l)
331                    }
332                    None => None,
333                },
334                _ => None,
335            };
336            t.line = 1;
337            t.sync.1 = 1;
338            t.buf = line.unwrap_or_default();
339            if (0..=0x10_FFFF).contains(&end_line_char) {
340                t.buf.push(end_line_char as u32);
341            }
342            t.loc = 0;
343            t.state = crate::input::LexState::NewLine;
344        }
345        self.sync_input_changed();
346    }
347
348    /// Asks for a file name after a lookup failure (`prompt_file_name`). In
349    /// non-interactive modes this is fatal.
350    pub fn prompt_file_name(&mut self, name: &[u8], s: &str, e: &str) -> ! {
351        if s == "input file name" {
352            self.print_err("I can't find file `");
353        } else {
354            self.print_err("I can't write on file `");
355        }
356        self.printer.print_bytes(name);
357        self.printer.print_str("'.");
358        if e == ".tex" {
359            self.show_context();
360        }
361        self.printer.print_ln();
362        // Observed: LuaTeX does not mention the default extension, and
363        // offers to retry only for input files.
364        if s == "input file name" {
365            self.printer
366                .print_str("(Press Enter to retry, or Control-D to exit)");
367        }
368        self.printer.print_nl("Please type another ");
369        self.printer.print_str(s);
370        self.fatal_error("*** (job aborted, file error in nonstop mode)");
371    }
372
373    /// Opens the log file and prints the banner (`open_log_file`).
374    pub fn open_log_file(&mut self) {
375        let old = self.printer.selector;
376        if self.job_name.is_none() {
377            self.job_name = Some(b"texput".to_vec());
378        }
379        let job = String::from_utf8_lossy(self.job_name.as_ref().unwrap()).into_owned();
380        let name = format!("{job}.log");
381        if !self.name_ok(&name, retex_kpath::Access::Write) {
382            self.prompt_file_name(name.as_bytes(), "transcript file name", ".log");
383        }
384        let mut path = PathBuf::from(&name);
385        if let Some(dir) = &self.files.output_dir {
386            path = dir.join(path);
387        }
388        let mut file = std::fs::File::create(&path);
389        // Without an output directory, a log that cannot be written in the
390        // current directory goes to TEXMFOUTPUT, and so do the files
391        // written after it (web2c manual, "Output file location").
392        if file.is_err()
393            && self.files.output_dir.is_none()
394            && let Some(dir) = self
395                .kpse
396                .as_ref()
397                .and_then(|k| k.borrow().var_value("TEXMFOUTPUT"))
398                .filter(|d| !d.is_empty())
399        {
400            let dir = PathBuf::from(dir);
401            file = std::fs::File::create(dir.join(&name));
402            if file.is_ok() {
403                path = dir.join(&name);
404                self.files.output_dir = Some(dir);
405                self.files.log_in_texmfoutput = true;
406            }
407        }
408        match file {
409            Ok(f) => self
410                .printer
411                .set_log(Box::new(std::io::BufWriter::with_capacity(1 << 16, f))),
412            Err(_) => self.prompt_file_name(name.as_bytes(), "transcript file name", ".log"),
413        }
414        self.files.log_name = Some(path);
415        // Print the banner line, including the date and time.
416        let banner = self.log_banner();
417        self.printer.log_raw(banner.as_bytes());
418        let shell = self.shell_escape_banner();
419        if shell.is_empty() {
420            self.printer.file_offset = banner.len();
421        } else {
422            self.printer.log_raw(b"\n");
423            self.printer.log_raw(shell.as_bytes());
424            self.printer.file_offset = shell.len();
425        }
426        self.printer.selector = Selector::LogOnly;
427        self.printer.print_nl("**");
428        let line = self.files.command_line.clone();
429        self.printer.print_bytes(&line);
430        self.printer.print_ln();
431        self.printer.selector = match old {
432            Selector::NoPrint => Selector::LogOnly,
433            Selector::TermOnly => Selector::TermAndLog,
434            s => s,
435        };
436    }
437
438    /// The first line of the log file.
439    /// The line after the banner that tells the shell escape mode (none
440    /// when it is disabled).
441    pub fn shell_escape_banner(&self) -> &'static str {
442        match self.shell_escape {
443            0 => "",
444            1 => " system commands enabled.",
445            _ => " restricted system commands enabled.",
446        }
447    }
448
449    fn log_banner(&self) -> String {
450        let c = self.start_time.civil();
451        const MONTHS: [&str; 12] = [
452            "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
453        ];
454        format!(
455            "This is retex, Version {} (LuaTeX 1.22.0 compatible)  ({})  {} {} {} {:02}:{:02}",
456            env!("CARGO_PKG_VERSION"),
457            if self.ini_version {
458                "INITEX".to_owned()
459            } else {
460                self.format_ident
461                    .trim()
462                    .trim_start_matches('(')
463                    .trim_end_matches(')')
464                    .to_owned()
465            },
466            c.day,
467            MONTHS[(c.month as usize).saturating_sub(1).min(11)],
468            c.year,
469            c.hour,
470            c.minute
471        )
472    }
473
474    /// The terminal line is exhausted: in `\errorstopmode`/`\scrollmode`
475    /// ask for more input (§360), otherwise stop.
476    pub fn terminal_exhausted(&mut self) {
477        if self.printer.selector == Selector::NoPrint || self.printer.selector == Selector::TermOnly
478        {
479            self.open_log_file();
480        }
481        if self.err.interaction > crate::error::Interaction::NonStop {
482            let empty = matches!(self.input.stack.last(), Some(Level::Text(t)) if t.buf.is_empty());
483            if empty {
484                self.printer
485                    .print_nl("(Please type a command or say `\\end')");
486            }
487            self.printer.print_ln();
488            let mut line = self.prompt_input("*");
489            let elc = self.eqtb.int(pg::assign_int::ENDLINECHAR);
490            if (0..=0x10_FFFF).contains(&elc) {
491                line.push(elc as u32);
492            }
493            if let Some(Level::Text(t)) = self.input.stack.last_mut() {
494                t.buf = line;
495                t.loc = 0;
496                t.state = crate::input::LexState::NewLine;
497            }
498            return;
499        }
500        self.fatal_error("*** (job aborted, no legal \\end found)");
501    }
502
503    /// `\openin` and `\closein` (`open_or_close_in`).
504    pub fn open_or_close_in(&mut self) {
505        let c = self.cur_chr;
506        self.scan_four_bit_int();
507        let n = self.cur_val as usize;
508        self.files.read_files[n] = ReadFile::Closed;
509        if c != 0 {
510            self.scan_optional_equals();
511            let name = self.scan_file_name();
512            if name.first() == Some(&b'|') {
513                if let Some(out) = self.read_pipe(&name) {
514                    self.files.read_files[n] = ReadFile::JustOpen(LineReader::new(out));
515                }
516                return;
517            }
518            let name = String::from_utf8_lossy(&name).into_owned();
519            if !self.name_ok(&name, retex_kpath::Access::Read) {
520                return;
521            }
522            if let Some(p) = self.find_input_file(&name, retex_kpath::FileFormat::Tex)
523                && let Some(data) = self.read_file_recorded(&p)
524            {
525                self.files.read_files[n] = ReadFile::JustOpen(LineReader::new(data));
526            } else {
527                self.record_absent_file(&name);
528            }
529        }
530    }
531
532    /// Reads a line from a `\read` stream into a token list (`read_toks`,
533    /// §482). `j` is 1 for `\readline` (e-TeX). The result starts with the
534    /// `END_MATCH` token, as a macro body without parameters.
535    pub fn read_toks(&mut self, n: i32, r: crate::hash::CsId, j: u32) -> Vec<Token> {
536        self.input.scanner_status = ScannerStatus::Defining;
537        self.input.warning_index = r;
538        let mut out: Vec<Token> = vec![Token::END_MATCH];
539        let m: usize = if (0..=15).contains(&n) {
540            n as usize
541        } else {
542            16
543        };
544        let s = self.input.align_state;
545        self.input.align_state = 1_000_000;
546        loop {
547            // Input and store tokens from the next line of the file.
548            let line: Vec<u8> = match self.files.read_files.get_mut(m) {
549                None | Some(ReadFile::Closed) => {
550                    // The (empty) line being read shows in the error
551                    // context as `<read *>` (§483).
552                    self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
553                    // Interactive terminal reading is not supported.
554                    self.fatal_error("*** (cannot \\read from terminal in nonstop modes)");
555                }
556                Some(ReadFile::JustOpen(lines)) => {
557                    let first = lines.pop_front();
558                    match first {
559                        Some(l) => {
560                            let rest = std::mem::take(lines);
561                            self.files.read_files[m] = ReadFile::Normal(rest);
562                            l
563                        }
564                        None => {
565                            self.files.read_files[m] = ReadFile::Closed;
566                            Vec::new()
567                        }
568                    }
569                }
570                Some(ReadFile::Normal(lines)) => match lines.pop_front() {
571                    Some(l) => l,
572                    None => {
573                        self.files.read_files[m] = ReadFile::Closed;
574                        if self.input.align_state != 1_000_000 {
575                            self.runaway();
576                            self.print_err("File ended within ");
577                            self.print_esc_str("read");
578                            self.help(&["This \\read has unbalanced braces."]);
579                            self.input.align_state = 1_000_000;
580                            self.error();
581                        }
582                        Vec::new()
583                    }
584                },
585            };
586            let end_line_char = self.eqtb.int(pg::assign_int::ENDLINECHAR);
587            let mut buf = crate::input::decode_line(&line);
588            if (0..=0x10_FFFF).contains(&end_line_char) {
589                buf.push(end_line_char as u32);
590            }
591            if j == 1 {
592                // \readline: spaces become space tokens, the rest other.
593                for &c in &buf {
594                    out.push(if c == ' ' as u32 {
595                        Token::SPACE
596                    } else {
597                        Token::other(c)
598                    });
599                }
600                break;
601            }
602            self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
603            if let Some(Level::Text(t)) = self.input.stack.last_mut() {
604                t.buf = buf;
605                t.loc = 0;
606                t.state = crate::input::LexState::NewLine;
607            }
608            loop {
609                self.get_token();
610                if self.cur_tok.0 == 0 {
611                    break;
612                }
613                if self.input.align_state < 1_000_000 {
614                    // An unmatched `}` aborts the line.
615                    loop {
616                        self.get_token();
617                        if self.cur_tok.0 == 0 {
618                            break;
619                        }
620                    }
621                    self.input.align_state = 1_000_000;
622                    break;
623                }
624                out.push(self.cur_tok);
625            }
626            self.end_file_reading();
627            if self.input.align_state == 1_000_000 {
628                break;
629            }
630        }
631        self.input.scanner_status = ScannerStatus::Normal;
632        self.input.align_state = s;
633        out
634    }
635
636    /// `\scantokens` and `\scantextokens` (e-TeX `pseudo_start`).
637    pub fn pseudo_start(&mut self, text_tokens: bool) {
638        let list = self.scan_general_text();
639        let s = self.print_to_string(|e| e.show_token_list(&list, 0, list.len(), i64::MAX));
640        // Split at the new-line character. As in etex.ch, each line ends
641        // at a new-line character or at the end of the string, so a final
642        // new-line character starts no further line and an empty string
643        // has no line at all.
644        let nl = self.eqtb.int(pg::assign_int::NEWLINECHAR);
645        let mut lines: VecDeque<Vec<u8>> = VecDeque::new();
646        let mut rest = s.as_slice();
647        while !rest.is_empty() {
648            let end = if (0..128).contains(&nl) {
649                rest.iter().position(|&b| i32::from(b) == nl)
650            } else {
651                None
652            };
653            let (line, tail) = match end {
654                Some(i) => (&rest[..i], &rest[i + 1..]),
655                None => (rest, &rest[rest.len()..]),
656            };
657            lines.push_back(line.to_vec());
658            rest = tail;
659        }
660        let tracing = self.eqtb.int(pg::assign_int::TRACINGSCANTOKENS) > 0 && !text_tokens;
661        self.begin_file_reading(
662            LineSource::Lines {
663                lines,
664                catcodes: None,
665                pseudo: !text_tokens,
666            },
667            Vec::new(),
668            tracing,
669        );
670        if let Some(Level::Text(t)) = self.input.stack.last_mut() {
671            t.line = 0;
672            t.buf.clear();
673            t.loc = 0;
674            t.eof_seen = text_tokens;
675        }
676        if tracing {
677            if self.printer.term_offset > self.printer.max_print_line.saturating_sub(3) {
678                self.printer.print_ln();
679            } else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
680                self.printer.print_char(' ' as u32);
681            }
682            self.printer.print_str("( ");
683            self.input.open_parens += 1;
684            self.printer.update_terminal();
685        }
686    }
687
688    /// e-TeX's check when a file ends inside groups or conditionals.
689    pub fn file_closed_checks(&mut self) {}
690
691    /// Closes everything at the end of the run
692    /// (`close_files_and_terminate`).
693    pub fn close_files_and_terminate(&mut self) {
694        for n in 0..128u32 {
695            if self.printer.write_open(n) {
696                self.printer.close_write(n);
697            }
698        }
699        self.print_memory_stats();
700        // Observed after a fatal error (an emergency stop): in PDF mode
701        // there is no PDF file and the log ends with LuaTeX's notice; in
702        // DVI mode the notice comes before the usual last lines.
703        let pdf_mode = self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0;
704        let fatal = self.err.history == crate::error::History::FatalErrorStop && !self.pdf.fatal;
705        if fatal && pdf_mode {
706            self.pdf.fatal = true;
707            self.finish_output();
708            self.printer
709                .print_nl("!  ==> Fatal error occurred, no output PDF file produced!");
710            // Observed: without a log (it could not be written) the line
711            // is left open.
712            if self.printer.log_opened() {
713                self.printer.print_ln();
714            }
715            let _ = std::fs::remove_file(self.output_path("pdf"));
716            self.finish_log();
717            return;
718        }
719        if fatal {
720            self.printer
721                .print_nl("!  ==> Fatal error occurred, bad output DVI file produced!");
722            self.printer.print_ln();
723        }
724        self.finish_output();
725        if self.total_pages == 0 {
726            self.printer.print_nl(
727                if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0 {
728                    "warning  (pdf backend): no pages of output."
729                } else {
730                    "No pages of output."
731                },
732            );
733            self.printer.print_ln();
734            if self.pdf.stream_written
735                && self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0
736            {
737                self.pdf_fatal("already written content discarded, no output file produced.");
738            }
739        }
740        // LuaTeX reports the objects it allocated (log only).
741        if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0
742            && self.pdf.max_obj() > 0
743            && !self.pdf.fatal
744            && self.printer.log_opened()
745        {
746            if self.printer.file_offset > 0 {
747                self.printer.print_ln();
748            }
749            let n = self.pdf.max_obj();
750            let names = self.pdf.named_dests();
751            // The extra memory is not modelled exactly: LuaTeX uses 8
752            // words per outline entry, ten per box resource and five per
753            // object (observed), and more for threads.
754            let extra = 1
755                + 8 * self.pdf.outlines.len()
756                + 10 * self.box_resources.len()
757                + self.pdf.obj_words;
758            let (objs, streams) = self.pdf.objstm_stats;
759            let compressed = if streams > 0 {
760                format!(
761                    " {objs} compressed objects within {streams} object stream{}\n",
762                    if streams == 1 { "" } else { "s" }
763                )
764            } else {
765                String::new()
766            };
767            self.printer.log_raw(
768                format!(
769                    "\nPDF statistics: {n} PDF objects out of {} (max. 8388607)\n{compressed} \
770                     {names} named destinations out of {} (max. 131072)\n \
771                     {extra} words of extra memory for PDF output out of {} (max. 100000000)\n",
772                    n.max(1000),
773                    names.max(1000),
774                    extra.max(10000),
775                )
776                .as_bytes(),
777            );
778        }
779        if self.printer.log_opened() {
780            self.printer.log_raw(b"\n");
781        }
782        self.finish_synctex();
783        self.finish_log();
784    }
785
786    /// LuaTeX's memory usage report, which ends the log when
787    /// `\tracingstats` is positive (compatibility mode only; just the
788    /// counts retex keeps).
789    fn print_memory_stats(&mut self) {
790        if self.diagnostics.is_some()
791            || !self.printer.log_opened()
792            || self
793                .eqtb
794                .int(crate::primitives_gen::assign_int::TRACINGSTATS)
795                <= 0
796        {
797            return;
798        }
799        let sel = self.printer.selector;
800        if sel == Selector::TermAndLog {
801            self.printer.selector = Selector::LogOnly;
802        }
803        self.printer
804            .print_nl("Here is how much of LuaTeX's memory you used:");
805        let cs = self.hash.len();
806        self.printer.print_nl(&format!(
807            " {cs} multiletter control sequences out of 65536+600000"
808        ));
809        let fonts = self.fonts.fonts.len();
810        self.printer.print_nl(&format!(" {fonts} fonts"));
811        self.printer.print_ln();
812        self.printer.selector = sel;
813    }
814
815    /// Closes the log and names it on the terminal.
816    pub(crate) fn finish_log(&mut self) {
817        if self.printer.log_opened() {
818            self.printer.close_log();
819            self.printer.selector = Selector::TermOnly;
820            if let Some(name) = self.files.log_name.clone() {
821                // The name as opened: in an -output-directory, LuaTeX shows
822                // it without the directory (observed).
823                let shown = match &self.files.output_dir {
824                    Some(dir) if !self.files.log_in_texmfoutput => {
825                        name.strip_prefix(dir).map(PathBuf::from).unwrap_or(name)
826                    }
827                    _ => name,
828                };
829                self.printer.print_nl("Transcript written on ");
830                self.printer.print_bytes(shown.to_string_lossy().as_bytes());
831                self.printer.print_char('.' as u32);
832            }
833        }
834        self.printer.print_ln();
835        self.printer.flush();
836    }
837}
838
839/// Shared record of the contents written to files during a pass.
840pub type WrittenSink = std::rc::Rc<std::cell::RefCell<HashMap<PathBuf, Vec<u8>>>>;
841
842/// A file writer that also records everything written, so that the pass
843/// driver can compare it with what the next pass reads.
844pub struct RecordingWriter {
845    inner: std::io::BufWriter<std::fs::File>,
846    path: PathBuf,
847    sink: WrittenSink,
848}
849
850impl RecordingWriter {
851    /// Wraps `f`, recording its content under `path`.
852    pub fn new(f: std::fs::File, path: PathBuf, sink: WrittenSink) -> Self {
853        sink.borrow_mut().insert(path.clone(), Vec::new());
854        RecordingWriter {
855            inner: std::io::BufWriter::new(f),
856            path,
857            sink,
858        }
859    }
860}
861
862impl RecordingWriter {
863    /// Wraps `f`, appending to what is already recorded under `path` (a
864    /// file reopened after resuming from a snapshot).
865    pub fn resume(f: std::fs::File, path: PathBuf, sink: WrittenSink) -> Self {
866        sink.borrow_mut().entry(path.clone()).or_default();
867        RecordingWriter {
868            inner: std::io::BufWriter::new(f),
869            path,
870            sink,
871        }
872    }
873}
874
875impl std::io::Write for RecordingWriter {
876    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
877        if let Some(v) = self.sink.borrow_mut().get_mut(&self.path) {
878            v.extend_from_slice(buf);
879        }
880        self.inner.write(buf)
881    }
882
883    fn flush(&mut self) -> std::io::Result<()> {
884        self.inner.flush()
885    }
886}