1use 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#[derive(Clone, Debug)]
18pub enum ReadFile {
19 Closed,
21 JustOpen(LineReader),
23 Normal(LineReader),
25}
26
27#[derive(Clone, Debug, Default)]
30pub struct LineReader {
31 data: Vec<u8>,
32 pos: usize,
33}
34
35impl LineReader {
36 pub fn new(data: Vec<u8>) -> Self {
38 LineReader { data, pos: 0 }
39 }
40
41 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#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct FileRecord {
53 pub path: PathBuf,
55 pub hash: Option<u64>,
57}
58
59#[derive(Debug)]
61pub struct FileState {
62 pub read_files: Vec<ReadFile>,
64 pub name_in_progress: bool,
66 pub output_dir: Option<PathBuf>,
68 pub read_log: Vec<FileRecord>,
70 pub written: HashMap<PathBuf, Vec<u8>>,
72 pub command_line: Vec<u8>,
74 pub log_name: Option<PathBuf>,
76 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 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
106pub 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 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 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 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 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 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 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 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 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 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 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 self.printer.set_log(Box::new(std::io::sink()));
339 }
340 }
341 self.files.log_name = Some(path);
342 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 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 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 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 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 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 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 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 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 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 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 pub fn file_closed_checks(&mut self) {}
583
584 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 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 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
662pub type WrittenSink = std::rc::Rc<std::cell::RefCell<HashMap<PathBuf, Vec<u8>>>>;
664
665pub struct RecordingWriter {
668 inner: std::io::BufWriter<std::fs::File>,
669 path: PathBuf,
670 sink: WrittenSink,
671}
672
673impl RecordingWriter {
674 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 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}