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    /// Further files the job depends on that the pass driver does not
77    /// compare between passes (images), for `--watch`.
78    pub watched: Vec<PathBuf>,
79}
80
81impl Default for FileState {
82    fn default() -> Self {
83        FileState {
84            read_files: vec![ReadFile::Closed; 16],
85            name_in_progress: false,
86            output_dir: None,
87            read_log: Vec::new(),
88            written: HashMap::new(),
89            command_line: Vec::new(),
90            log_name: None,
91            watched: Vec::new(),
92        }
93    }
94}
95
96impl FileState {
97    /// Whether `\read` stream `n` is open.
98    pub fn read_open(&self, n: u32) -> bool {
99        !matches!(
100            self.read_files.get(n as usize),
101            None | Some(ReadFile::Closed)
102        )
103    }
104}
105
106/// FNV-1a hash used to detect content changes between passes.
107pub fn content_hash(data: &[u8]) -> u64 {
108    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
109    for &b in data {
110        h ^= u64::from(b);
111        h = h.wrapping_mul(0x0100_0000_01b3);
112    }
113    h
114}
115
116impl Engine {
117    /// Reads a whole file, recording it for the pass driver.
118    pub fn read_file_recorded(&mut self, path: &Path) -> Option<Vec<u8>> {
119        let data = std::fs::read(path).ok();
120        if !self.files.read_log.iter().any(|r| r.path == path) {
121            self.files.read_log.push(FileRecord {
122                path: path.to_path_buf(),
123                hash: data.as_deref().map(content_hash),
124            });
125        }
126        data
127    }
128
129    /// Records a file lookup from Lua (`kpse.find_file`, which expl3 uses to
130    /// test whether a file exists) for the pass driver: files of the job
131    /// (relative names) are recorded with their content hash, or as absent.
132    pub fn record_probe(&mut self, name: &str, found: Option<&Path>) {
133        let path = match found {
134            Some(p) if p.is_relative() => p.to_path_buf(),
135            Some(_) => return,
136            None if !Path::new(name).is_absolute() && !name.contains("//") => {
137                self.absent_path(name)
138            }
139            None => return,
140        };
141        if self.files.read_log.iter().any(|r| r.path == path) {
142            return;
143        }
144        let hash = found
145            .and_then(|p| std::fs::read(p).ok())
146            .map(|d| content_hash(&d));
147        self.files.read_log.push(FileRecord { path, hash });
148    }
149
150    /// Where a job file `name` that was not found would appear: in the
151    /// output directory, where the job writes its files, if there is one.
152    fn absent_path(&self, name: &str) -> PathBuf {
153        match &self.files.output_dir {
154            Some(d) if Path::new(name).is_relative() => d.join(name),
155            _ => PathBuf::from(name),
156        }
157    }
158
159    /// Records that file `name` was looked for and not found, so that the
160    /// pass driver reruns when this pass writes it (e.g. the `.aux` file
161    /// LaTeX tests for with `\filesize`).
162    pub fn record_absent_file(&mut self, name: &str) {
163        let p = self.absent_path(name);
164        if !self.files.read_log.iter().any(|r| r.path == p) {
165            self.files.read_log.push(FileRecord {
166                path: p,
167                hash: None,
168            });
169        }
170    }
171
172    /// Looks a TeX input file up: in the output directory first, then via
173    /// kpathsea.
174    pub fn find_input_file(&mut self, name: &str, fmt: retex_kpath::FileFormat) -> Option<PathBuf> {
175        if let Some(dir) = &self.files.output_dir {
176            for cand in [format!("{name}.tex"), name.to_owned()] {
177                let p = dir.join(&cand);
178                if p.is_file() {
179                    return Some(p);
180                }
181            }
182        }
183        if let Some(k) = &self.kpse {
184            return k.borrow_mut().find_file(name, fmt);
185        }
186        let p = PathBuf::from(name);
187        if p.is_file() {
188            return Some(p);
189        }
190        let p = PathBuf::from(format!("{name}.tex"));
191        p.is_file().then_some(p)
192    }
193
194    /// Scans a file name (`scan_file_name`). LuaTeX also accepts a name in
195    /// braces and names in double quotes.
196    pub fn scan_file_name(&mut self) -> Vec<u8> {
197        self.files.name_in_progress = true;
198        let mut name: Vec<u8> = Vec::new();
199        self.get_nonblank_noncall();
200        if self.cur_cmd == Cmd::LeftBrace {
201            self.back_input();
202            let toks = self.scan_toks_expanded();
203            let s = self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
204            name = s.into_iter().filter(|&b| b != b'"').collect();
205            self.files.name_in_progress = false;
206            return name;
207        }
208        let mut quoted = false;
209        loop {
210            if self.cur_cmd > Cmd::OtherChar || self.cur_cs != crate::hash::NO_CS {
211                self.back_input();
212                break;
213            }
214            let c = self.cur_chr;
215            if c == ' ' as u32 && !quoted {
216                break;
217            }
218            if c == '"' as u32 {
219                quoted = !quoted;
220            } else {
221                crate::utf8x::push(&mut name, c);
222            }
223            self.get_x_token();
224        }
225        self.files.name_in_progress = false;
226        name
227    }
228
229    /// `\input` (`start_input`).
230    pub fn start_input(&mut self) {
231        let name = self.scan_file_name();
232        let name_str = String::from_utf8_lossy(&name).into_owned();
233        let found = match self.find_input_file(&name_str, retex_kpath::FileFormat::Tex) {
234            Some(p) => p,
235            None => self.prompt_file_name(&name, "input file name", ".tex"),
236        };
237        let data = self.read_file_recorded(&found).unwrap_or_default();
238        if !self.printer.log_opened() && self.job_name.is_some() {
239            self.open_log_file();
240        }
241        if self.job_name.is_none() {
242            let base = Path::new(&name_str)
243                .file_stem()
244                .map(|s| s.to_string_lossy().into_owned())
245                .unwrap_or_else(|| name_str.clone());
246            self.job_name = Some(base.into_bytes());
247            self.open_log_file();
248        }
249        let display = found.to_string_lossy().into_owned().into_bytes();
250        if self.printer.term_offset + display.len() > self.printer.max_print_line.saturating_sub(2)
251        {
252            self.printer.print_ln();
253        } else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
254            self.printer.print_char(' ' as u32);
255        }
256        self.printer.print_char('(' as u32);
257        self.input.open_parens += 1;
258        self.printer.print_bytes(&display);
259        self.printer.update_terminal();
260        let source = LineSource::File {
261            data: Rc::from(data),
262            pos: 0,
263        };
264        self.begin_file_reading(source, display, true);
265        // Read the first line of the new file.
266        self.read_first_line();
267    }
268
269    fn read_first_line(&mut self) {
270        let end_line_char = self.eqtb.int(pg::assign_int::ENDLINECHAR);
271        if let Some(Level::Text(t)) = self.input.stack.last_mut() {
272            t.line = 0;
273            let line = match &mut t.source {
274                LineSource::File { data, pos } => match crate::input::next_line(data, *pos) {
275                    Some((bytes, next)) => {
276                        let l = crate::input::decode_line(bytes);
277                        *pos = next;
278                        Some(l)
279                    }
280                    None => None,
281                },
282                _ => None,
283            };
284            t.line = 1;
285            t.buf = line.unwrap_or_default();
286            if (0..=0x10_FFFF).contains(&end_line_char) {
287                t.buf.push(end_line_char as u32);
288            }
289            t.loc = 0;
290            t.state = crate::input::LexState::NewLine;
291        }
292    }
293
294    /// Asks for a file name after a lookup failure (`prompt_file_name`). In
295    /// non-interactive modes this is fatal.
296    pub fn prompt_file_name(&mut self, name: &[u8], s: &str, e: &str) -> ! {
297        if s == "input file name" {
298            self.print_err("I can't find file `");
299        } else {
300            self.print_err("I can't write on file `");
301        }
302        self.printer.print_bytes(name);
303        self.printer.print_str("'.");
304        if e == ".tex" {
305            self.show_context();
306        }
307        self.printer.print_ln();
308        self.printer
309            .print_str("(Press Enter to retry, or Control-D to exit");
310        if !e.is_empty() {
311            self.printer.print_str("; default file extension is `");
312            self.printer.print_str(e);
313            self.printer.print_str("'");
314        }
315        self.printer.print_str(")");
316        self.printer.print_nl("Please type another ");
317        self.printer.print_str(s);
318        self.fatal_error("*** (job aborted, file error in nonstop mode)");
319    }
320
321    /// Opens the log file and prints the banner (`open_log_file`).
322    pub fn open_log_file(&mut self) {
323        let old = self.printer.selector;
324        if self.job_name.is_none() {
325            self.job_name = Some(b"texput".to_vec());
326        }
327        let job = String::from_utf8_lossy(self.job_name.as_ref().unwrap()).into_owned();
328        let mut path = PathBuf::from(format!("{job}.log"));
329        if let Some(dir) = &self.files.output_dir {
330            path = dir.join(path);
331        }
332        match std::fs::File::create(&path) {
333            Ok(f) => self
334                .printer
335                .set_log(Box::new(std::io::BufWriter::with_capacity(1 << 16, f))),
336            Err(_) => {
337                // TeX would prompt; without a log we continue silently.
338                self.printer.set_log(Box::new(std::io::sink()));
339            }
340        }
341        self.files.log_name = Some(path);
342        // Print the banner line, including the date and time.
343        let banner = self.log_banner();
344        self.printer.log_raw(banner.as_bytes());
345        self.printer
346            .log_raw(b"\n restricted system commands enabled.");
347        self.printer.file_offset = " restricted system commands enabled.".len();
348        self.printer.selector = Selector::LogOnly;
349        self.printer.print_nl("**");
350        let line = self.files.command_line.clone();
351        self.printer.print_bytes(&line);
352        self.printer.print_ln();
353        self.printer.selector = match old {
354            Selector::NoPrint => Selector::LogOnly,
355            Selector::TermOnly => Selector::TermAndLog,
356            s => s,
357        };
358    }
359
360    /// The first line of the log file.
361    fn log_banner(&self) -> String {
362        let c = self.start_time.civil();
363        const MONTHS: [&str; 12] = [
364            "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
365        ];
366        format!(
367            "This is retex, Version {} (LuaTeX 1.22.0 compatible)  ({})  {} {} {} {:02}:{:02}",
368            env!("CARGO_PKG_VERSION"),
369            if self.ini_version {
370                "INITEX".to_owned()
371            } else {
372                self.format_ident
373                    .trim()
374                    .trim_start_matches('(')
375                    .trim_end_matches(')')
376                    .to_owned()
377            },
378            c.day,
379            MONTHS[(c.month as usize).saturating_sub(1).min(11)],
380            c.year,
381            c.hour,
382            c.minute
383        )
384    }
385
386    /// The terminal line is exhausted: in `\errorstopmode`/`\scrollmode`
387    /// ask for more input (§360), otherwise stop.
388    pub fn terminal_exhausted(&mut self) {
389        if self.printer.selector == Selector::NoPrint || self.printer.selector == Selector::TermOnly
390        {
391            self.open_log_file();
392        }
393        if self.err.interaction > crate::error::Interaction::NonStop {
394            let empty = matches!(self.input.stack.last(), Some(Level::Text(t)) if t.buf.is_empty());
395            if empty {
396                self.printer
397                    .print_nl("(Please type a command or say `\\end')");
398            }
399            self.printer.print_ln();
400            let mut line = self.prompt_input("*");
401            let elc = self.eqtb.int(pg::assign_int::ENDLINECHAR);
402            if (0..=0x10_FFFF).contains(&elc) {
403                line.push(elc as u32);
404            }
405            if let Some(Level::Text(t)) = self.input.stack.last_mut() {
406                t.buf = line;
407                t.loc = 0;
408                t.state = crate::input::LexState::NewLine;
409            }
410            return;
411        }
412        self.fatal_error("*** (job aborted, no legal \\end found)");
413    }
414
415    /// `\openin` and `\closein` (`open_or_close_in`).
416    pub fn open_or_close_in(&mut self) {
417        let c = self.cur_chr;
418        self.scan_four_bit_int();
419        let n = self.cur_val as usize;
420        self.files.read_files[n] = ReadFile::Closed;
421        if c != 0 {
422            self.scan_optional_equals();
423            let name = self.scan_file_name();
424            let name = String::from_utf8_lossy(&name).into_owned();
425            if let Some(p) = self.find_input_file(&name, retex_kpath::FileFormat::Tex)
426                && let Some(data) = self.read_file_recorded(&p)
427            {
428                self.files.read_files[n] = ReadFile::JustOpen(LineReader::new(data));
429            } else {
430                self.record_absent_file(&name);
431            }
432        }
433    }
434
435    /// Reads a line from a `\read` stream into a token list (`read_toks`,
436    /// §482). `j` is 1 for `\readline` (e-TeX). The result starts with the
437    /// `END_MATCH` token, as a macro body without parameters.
438    pub fn read_toks(&mut self, n: i32, r: crate::hash::CsId, j: u32) -> Vec<Token> {
439        self.input.scanner_status = ScannerStatus::Defining;
440        self.input.warning_index = r;
441        let mut out: Vec<Token> = vec![Token::END_MATCH];
442        let m: usize = if (0..=15).contains(&n) {
443            n as usize
444        } else {
445            16
446        };
447        let s = self.input.align_state;
448        self.input.align_state = 1_000_000;
449        loop {
450            // Input and store tokens from the next line of the file.
451            let line: Vec<u8> = match self.files.read_files.get_mut(m) {
452                None | Some(ReadFile::Closed) => {
453                    if self.err.interaction > crate::error::Interaction::NonStop {
454                        // Interactive terminal reading is not supported.
455                        self.fatal_error("*** (cannot \\read from terminal in nonstop modes)");
456                    }
457                    self.fatal_error("*** (cannot \\read from terminal in nonstop modes)");
458                }
459                Some(ReadFile::JustOpen(lines)) => {
460                    let first = lines.pop_front();
461                    match first {
462                        Some(l) => {
463                            let rest = std::mem::take(lines);
464                            self.files.read_files[m] = ReadFile::Normal(rest);
465                            l
466                        }
467                        None => {
468                            self.files.read_files[m] = ReadFile::Closed;
469                            Vec::new()
470                        }
471                    }
472                }
473                Some(ReadFile::Normal(lines)) => match lines.pop_front() {
474                    Some(l) => l,
475                    None => {
476                        self.files.read_files[m] = ReadFile::Closed;
477                        if self.input.align_state != 1_000_000 {
478                            self.runaway();
479                            self.print_err("File ended within ");
480                            self.print_esc_str("read");
481                            self.help(&["This \\read has unbalanced braces."]);
482                            self.input.align_state = 1_000_000;
483                            self.error();
484                        }
485                        Vec::new()
486                    }
487                },
488            };
489            let end_line_char = self.eqtb.int(pg::assign_int::ENDLINECHAR);
490            let mut buf = crate::input::decode_line(&line);
491            if (0..=0x10_FFFF).contains(&end_line_char) {
492                buf.push(end_line_char as u32);
493            }
494            if j == 1 {
495                // \readline: spaces become space tokens, the rest other.
496                for &c in &buf {
497                    out.push(if c == ' ' as u32 {
498                        Token::SPACE
499                    } else {
500                        Token::other(c)
501                    });
502                }
503                break;
504            }
505            self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
506            if let Some(Level::Text(t)) = self.input.stack.last_mut() {
507                t.buf = buf;
508                t.loc = 0;
509                t.state = crate::input::LexState::NewLine;
510            }
511            loop {
512                self.get_token();
513                if self.cur_tok.0 == 0 {
514                    break;
515                }
516                if self.input.align_state < 1_000_000 {
517                    // An unmatched `}` aborts the line.
518                    loop {
519                        self.get_token();
520                        if self.cur_tok.0 == 0 {
521                            break;
522                        }
523                    }
524                    self.input.align_state = 1_000_000;
525                    break;
526                }
527                out.push(self.cur_tok);
528            }
529            self.end_file_reading();
530            if self.input.align_state == 1_000_000 {
531                break;
532            }
533        }
534        self.input.scanner_status = ScannerStatus::Normal;
535        self.input.align_state = s;
536        out
537    }
538
539    /// `\scantokens` and `\scantextokens` (e-TeX `pseudo_start`).
540    pub fn pseudo_start(&mut self, text_tokens: bool) {
541        let list = self.scan_general_text();
542        let s = self.print_to_string(|e| e.show_token_list(&list, 0, list.len(), i64::MAX));
543        // Split at the new-line character.
544        let nl = self.eqtb.int(pg::assign_int::NEWLINECHAR);
545        let mut lines: VecDeque<Vec<u8>> = VecDeque::new();
546        if (0..128).contains(&nl) {
547            for part in s.split(|&b| i32::from(b) == nl) {
548                lines.push_back(part.to_vec());
549            }
550        } else {
551            lines.push_back(s);
552        }
553        let tracing = self.eqtb.int(pg::assign_int::TRACINGSCANTOKENS) > 0 && !text_tokens;
554        self.begin_file_reading(
555            LineSource::Lines {
556                lines,
557                catcodes: None,
558                pseudo: !text_tokens,
559            },
560            Vec::new(),
561            tracing,
562        );
563        if let Some(Level::Text(t)) = self.input.stack.last_mut() {
564            t.line = 0;
565            t.buf.clear();
566            t.loc = 0;
567            t.eof_seen = text_tokens;
568        }
569        if tracing {
570            if self.printer.term_offset > self.printer.max_print_line.saturating_sub(3) {
571                self.printer.print_ln();
572            } else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
573                self.printer.print_char(' ' as u32);
574            }
575            self.printer.print_str("( ");
576            self.input.open_parens += 1;
577            self.printer.update_terminal();
578        }
579    }
580
581    /// e-TeX's check when a file ends inside groups or conditionals.
582    pub fn file_closed_checks(&mut self) {}
583
584    /// Closes everything at the end of the run
585    /// (`close_files_and_terminate`).
586    pub fn close_files_and_terminate(&mut self) {
587        for n in 0..128u32 {
588            if self.printer.write_open(n) {
589                self.printer.close_write(n);
590            }
591        }
592        self.finish_output();
593        if self.total_pages == 0 {
594            self.printer.print_nl(
595                if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0 {
596                    "warning  (pdf backend): no pages of output."
597                } else {
598                    "No pages of output."
599                },
600            );
601            self.printer.print_ln();
602            if self.pdf.stream_written
603                && self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0
604            {
605                self.pdf_fatal("already written content discarded, no output file produced.");
606            }
607        }
608        // LuaTeX reports the objects it allocated (log only).
609        if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0
610            && self.pdf.max_obj() > 0
611            && !self.pdf.fatal
612            && self.printer.log_opened()
613        {
614            if self.printer.file_offset > 0 {
615                self.printer.print_ln();
616            }
617            let n = self.pdf.max_obj();
618            let names = self.pdf.named_dests();
619            // The extra memory is not modelled exactly: LuaTeX uses 8
620            // words per outline entry, ten per box resource and five per
621            // object (observed), and more for threads.
622            let extra = 1
623                + 8 * self.pdf.outlines.len()
624                + 10 * self.box_resources.len()
625                + self.pdf.obj_words;
626            let (objs, streams) = self.pdf.objstm_stats;
627            let compressed = if streams > 0 {
628                format!(
629                    " {objs} compressed objects within {streams} object stream{}\n",
630                    if streams == 1 { "" } else { "s" }
631                )
632            } else {
633                String::new()
634            };
635            self.printer.log_raw(
636                format!(
637                    "\nPDF statistics: {n} PDF objects out of {} (max. 8388607)\n{compressed} \
638                     {names} named destinations out of {} (max. 131072)\n \
639                     {extra} words of extra memory for PDF output out of {} (max. 100000000)\n",
640                    n.max(1000),
641                    names.max(1000),
642                    extra.max(10000),
643                )
644                .as_bytes(),
645            );
646        }
647        if self.printer.log_opened() {
648            self.printer.log_raw(b"\n");
649            self.printer.close_log();
650            self.printer.selector = Selector::TermOnly;
651            if let Some(name) = self.files.log_name.clone() {
652                self.printer.print_nl("Transcript written on ");
653                self.printer.print_bytes(name.to_string_lossy().as_bytes());
654                self.printer.print_char('.' as u32);
655            }
656        }
657        self.printer.print_ln();
658        self.printer.flush();
659    }
660}
661
662/// Shared record of the contents written to files during a pass.
663pub type WrittenSink = std::rc::Rc<std::cell::RefCell<HashMap<PathBuf, Vec<u8>>>>;
664
665/// A file writer that also records everything written, so that the pass
666/// driver can compare it with what the next pass reads.
667pub struct RecordingWriter {
668    inner: std::io::BufWriter<std::fs::File>,
669    path: PathBuf,
670    sink: WrittenSink,
671}
672
673impl RecordingWriter {
674    /// Wraps `f`, recording its content under `path`.
675    pub fn new(f: std::fs::File, path: PathBuf, sink: WrittenSink) -> Self {
676        sink.borrow_mut().insert(path.clone(), Vec::new());
677        RecordingWriter {
678            inner: std::io::BufWriter::new(f),
679            path,
680            sink,
681        }
682    }
683}
684
685impl RecordingWriter {
686    /// Wraps `f`, appending to what is already recorded under `path` (a
687    /// file reopened after resuming from a snapshot).
688    pub fn resume(f: std::fs::File, path: PathBuf, sink: WrittenSink) -> Self {
689        sink.borrow_mut().entry(path.clone()).or_default();
690        RecordingWriter {
691            inner: std::io::BufWriter::new(f),
692            path,
693            sink,
694        }
695    }
696}
697
698impl std::io::Write for RecordingWriter {
699    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
700        if let Some(v) = self.sink.borrow_mut().get_mut(&self.path) {
701            v.extend_from_slice(buf);
702        }
703        self.inner.write(buf)
704    }
705
706    fn flush(&mut self) -> std::io::Result<()> {
707        self.inner.flush()
708    }
709}