use std::collections::{HashMap, VecDeque};
use std::path::{Path, PathBuf};
use std::rc::Rc;
use crate::Engine;
use crate::cmd::Cmd;
use crate::input::{Level, LineSource, ScannerStatus};
use crate::primitives_gen as pg;
use crate::print::Selector;
use crate::token::Token;
#[derive(Clone, Debug)]
pub enum ReadFile {
Closed,
JustOpen(LineReader),
Normal(LineReader),
}
#[derive(Clone, Debug, Default)]
pub struct LineReader {
data: Vec<u8>,
pos: usize,
}
impl LineReader {
pub fn new(data: Vec<u8>) -> Self {
LineReader { data, pos: 0 }
}
pub fn pop_front(&mut self) -> Option<Vec<u8>> {
let (line, next) = crate::input::next_line(&self.data, self.pos)?;
let line = line.to_vec();
self.pos = next;
Some(line)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileRecord {
pub path: PathBuf,
pub hash: Option<u64>,
}
#[derive(Debug)]
pub struct FileState {
pub read_files: Vec<ReadFile>,
pub name_in_progress: bool,
pub output_dir: Option<PathBuf>,
pub read_log: Vec<FileRecord>,
pub written: HashMap<PathBuf, Vec<u8>>,
pub command_line: Vec<u8>,
pub log_name: Option<PathBuf>,
pub log_in_texmfoutput: bool,
pub watched: Vec<PathBuf>,
}
impl Default for FileState {
fn default() -> Self {
FileState {
read_files: vec![ReadFile::Closed; 16],
name_in_progress: false,
output_dir: None,
read_log: Vec::new(),
written: HashMap::new(),
command_line: Vec::new(),
log_name: None,
log_in_texmfoutput: false,
watched: Vec::new(),
}
}
}
impl FileState {
pub fn read_open(&self, n: u32) -> bool {
!matches!(
self.read_files.get(n as usize),
None | Some(ReadFile::Closed)
)
}
}
pub fn content_hash(data: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in data {
h ^= u64::from(b);
h = h.wrapping_mul(0x0100_0000_01b3);
}
h
}
impl Engine {
pub fn read_file_recorded(&mut self, path: &Path) -> Option<Vec<u8>> {
let data = std::fs::read(path).ok();
if !self.files.read_log.iter().any(|r| r.path == path) {
self.files.read_log.push(FileRecord {
path: path.to_path_buf(),
hash: data.as_deref().map(content_hash),
});
}
data
}
pub fn record_probe(&mut self, name: &str, found: Option<&Path>) {
let path = match found {
Some(p) if p.is_relative() => p.to_path_buf(),
Some(_) => return,
None if !Path::new(name).is_absolute() && !name.contains("//") => {
self.absent_path(name)
}
None => return,
};
if self.files.read_log.iter().any(|r| r.path == path) {
return;
}
let hash = found
.and_then(|p| std::fs::read(p).ok())
.map(|d| content_hash(&d));
self.files.read_log.push(FileRecord { path, hash });
}
fn absent_path(&self, name: &str) -> PathBuf {
match &self.files.output_dir {
Some(d) if Path::new(name).is_relative() => d.join(name),
_ => PathBuf::from(name),
}
}
pub fn record_absent_file(&mut self, name: &str) {
let p = self.absent_path(name);
if !self.files.read_log.iter().any(|r| r.path == p) {
self.files.read_log.push(FileRecord {
path: p,
hash: None,
});
}
}
pub fn name_ok(&self, name: &str, access: retex_kpath::Access) -> bool {
let Some(k) = &self.kpse else {
return true;
};
let refusal = match k.borrow().name_ok(name, access, false) {
Ok(()) => return true,
Err(r) => r,
};
let program = std::env::args().next().unwrap_or_else(|| "retex".into());
eprint!("\n{program}: {}\n", refusal.message(name));
false
}
pub fn find_input_file(&mut self, name: &str, fmt: retex_kpath::FileFormat) -> Option<PathBuf> {
if let Some(dir) = &self.files.output_dir {
for cand in [format!("{name}.tex"), name.to_owned()] {
let p = dir.join(&cand);
if p.is_file() {
return Some(p);
}
}
}
if let Some(k) = &self.kpse {
return k.borrow_mut().find_file(name, fmt);
}
let p = PathBuf::from(name);
if p.is_file() {
return Some(p);
}
let p = PathBuf::from(format!("{name}.tex"));
p.is_file().then_some(p)
}
pub fn scan_file_name(&mut self) -> Vec<u8> {
self.files.name_in_progress = true;
let mut name: Vec<u8> = Vec::new();
self.get_nonblank_noncall();
if self.cur_cmd == Cmd::LeftBrace {
self.back_input();
let toks = self.scan_toks_expanded();
let s = self.print_to_string(|e| e.show_token_list(&toks, 0, toks.len(), i64::MAX));
name = s.into_iter().filter(|&b| b != b'"').collect();
self.files.name_in_progress = false;
return name;
}
let mut quoted = false;
loop {
if self.cur_cmd > Cmd::OtherChar || self.cur_cs != crate::hash::NO_CS {
self.back_input();
break;
}
let c = self.cur_chr;
if c == ' ' as u32 && !quoted {
break;
}
if c == '"' as u32 {
quoted = !quoted;
} else {
crate::utf8x::push(&mut name, c);
}
self.get_x_token();
}
self.files.name_in_progress = false;
name
}
fn read_pipe(&mut self, name: &[u8]) -> Option<Vec<u8>> {
let cmd = name.strip_prefix(b"|")?;
if self.shell_escape == 0 {
return None;
}
let host = self.lua.clone()?;
host.pipe_read(self, cmd)
}
pub fn start_input(&mut self) {
let name = self.scan_file_name();
let name_str = String::from_utf8_lossy(&name).into_owned();
let (found, data) = if let Some(out) = self.read_pipe(&name) {
(PathBuf::from(&name_str), out)
} else {
let allowed = self.name_ok(&name_str, retex_kpath::Access::Read);
let found = match allowed
.then(|| self.find_input_file(&name_str, retex_kpath::FileFormat::Tex))
.flatten()
{
Some(p) => p,
None => self.prompt_file_name(&name, "input file name", ".tex"),
};
let data = self.read_file_recorded(&found).unwrap_or_default();
(found, data)
};
if let Some(f) = self.progress_hook {
f(self);
}
if !self.printer.log_opened() && self.job_name.is_some() {
self.open_log_file();
}
if self.job_name.is_none() {
let base = Path::new(&name_str)
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| name_str.clone());
self.job_name = Some(base.into_bytes());
self.open_log_file();
}
let display = found.to_string_lossy().into_owned().into_bytes();
let shown = if display.contains(&b' ') {
[&b"\""[..], &display, b"\""].concat()
} else {
display.clone()
};
self.check_in_open();
if self.printer.term_offset + shown.len() > self.printer.max_print_line.saturating_sub(2) {
self.printer.print_ln();
} else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
self.printer.print_char(' ' as u32);
}
self.printer.print_char('(' as u32);
self.input.open_parens += 1;
self.printer.print_bytes(&shown);
self.printer.update_terminal();
let source = LineSource::File {
data: Rc::from(data),
pos: 0,
};
self.begin_file_reading(source, display, true);
self.synctex_input_opened(&found);
self.read_first_line();
}
fn read_first_line(&mut self) {
let end_line_char = self.eqtb.int(pg::assign_int::ENDLINECHAR);
if let Some(Level::Text(t)) = self.input.stack.last_mut() {
t.line = 0;
let line = match &mut t.source {
LineSource::File { data, pos } => match crate::input::next_line(data, *pos) {
Some((bytes, next)) => {
let l = crate::input::decode_line(bytes);
*pos = next;
Some(l)
}
None => None,
},
_ => None,
};
t.line = 1;
t.sync.1 = 1;
t.buf = line.unwrap_or_default();
if (0..=0x10_FFFF).contains(&end_line_char) {
t.buf.push(end_line_char as u32);
}
t.loc = 0;
t.state = crate::input::LexState::NewLine;
}
self.sync_input_changed();
}
pub fn prompt_file_name(&mut self, name: &[u8], s: &str, e: &str) -> ! {
if s == "input file name" {
self.print_err("I can't find file `");
} else {
self.print_err("I can't write on file `");
}
self.printer.print_bytes(name);
self.printer.print_str("'.");
if e == ".tex" {
self.show_context();
}
self.printer.print_ln();
if s == "input file name" {
self.printer
.print_str("(Press Enter to retry, or Control-D to exit)");
}
self.printer.print_nl("Please type another ");
self.printer.print_str(s);
self.fatal_error("*** (job aborted, file error in nonstop mode)");
}
pub fn open_log_file(&mut self) {
let old = self.printer.selector;
if self.job_name.is_none() {
self.job_name = Some(b"texput".to_vec());
}
let job = String::from_utf8_lossy(self.job_name.as_ref().unwrap()).into_owned();
let name = format!("{job}.log");
if !self.name_ok(&name, retex_kpath::Access::Write) {
self.prompt_file_name(name.as_bytes(), "transcript file name", ".log");
}
let mut path = PathBuf::from(&name);
if let Some(dir) = &self.files.output_dir {
path = dir.join(path);
}
let mut file = std::fs::File::create(&path);
if file.is_err()
&& self.files.output_dir.is_none()
&& let Some(dir) = self
.kpse
.as_ref()
.and_then(|k| k.borrow().var_value("TEXMFOUTPUT"))
.filter(|d| !d.is_empty())
{
let dir = PathBuf::from(dir);
file = std::fs::File::create(dir.join(&name));
if file.is_ok() {
path = dir.join(&name);
self.files.output_dir = Some(dir);
self.files.log_in_texmfoutput = true;
}
}
match file {
Ok(f) => self
.printer
.set_log(Box::new(std::io::BufWriter::with_capacity(1 << 16, f))),
Err(_) => self.prompt_file_name(name.as_bytes(), "transcript file name", ".log"),
}
self.files.log_name = Some(path);
let banner = self.log_banner();
self.printer.log_raw(banner.as_bytes());
let shell = self.shell_escape_banner();
if shell.is_empty() {
self.printer.file_offset = banner.len();
} else {
self.printer.log_raw(b"\n");
self.printer.log_raw(shell.as_bytes());
self.printer.file_offset = shell.len();
}
self.printer.selector = Selector::LogOnly;
self.printer.print_nl("**");
let line = self.files.command_line.clone();
self.printer.print_bytes(&line);
self.printer.print_ln();
self.printer.selector = match old {
Selector::NoPrint => Selector::LogOnly,
Selector::TermOnly => Selector::TermAndLog,
s => s,
};
}
pub fn shell_escape_banner(&self) -> &'static str {
match self.shell_escape {
0 => "",
1 => " system commands enabled.",
_ => " restricted system commands enabled.",
}
}
fn log_banner(&self) -> String {
let c = self.start_time.civil();
const MONTHS: [&str; 12] = [
"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
];
format!(
"This is retex, Version {} (LuaTeX 1.22.0 compatible) ({}) {} {} {} {:02}:{:02}",
env!("CARGO_PKG_VERSION"),
if self.ini_version {
"INITEX".to_owned()
} else {
self.format_ident
.trim()
.trim_start_matches('(')
.trim_end_matches(')')
.to_owned()
},
c.day,
MONTHS[(c.month as usize).saturating_sub(1).min(11)],
c.year,
c.hour,
c.minute
)
}
pub fn terminal_exhausted(&mut self) {
if self.printer.selector == Selector::NoPrint || self.printer.selector == Selector::TermOnly
{
self.open_log_file();
}
if self.err.interaction > crate::error::Interaction::NonStop {
let empty = matches!(self.input.stack.last(), Some(Level::Text(t)) if t.buf.is_empty());
if empty {
self.printer
.print_nl("(Please type a command or say `\\end')");
}
self.printer.print_ln();
let mut line = self.prompt_input("*");
let elc = self.eqtb.int(pg::assign_int::ENDLINECHAR);
if (0..=0x10_FFFF).contains(&elc) {
line.push(elc as u32);
}
if let Some(Level::Text(t)) = self.input.stack.last_mut() {
t.buf = line;
t.loc = 0;
t.state = crate::input::LexState::NewLine;
}
return;
}
self.fatal_error("*** (job aborted, no legal \\end found)");
}
pub fn open_or_close_in(&mut self) {
let c = self.cur_chr;
self.scan_four_bit_int();
let n = self.cur_val as usize;
self.files.read_files[n] = ReadFile::Closed;
if c != 0 {
self.scan_optional_equals();
let name = self.scan_file_name();
if name.first() == Some(&b'|') {
if let Some(out) = self.read_pipe(&name) {
self.files.read_files[n] = ReadFile::JustOpen(LineReader::new(out));
}
return;
}
let name = String::from_utf8_lossy(&name).into_owned();
if !self.name_ok(&name, retex_kpath::Access::Read) {
return;
}
if let Some(p) = self.find_input_file(&name, retex_kpath::FileFormat::Tex)
&& let Some(data) = self.read_file_recorded(&p)
{
self.files.read_files[n] = ReadFile::JustOpen(LineReader::new(data));
} else {
self.record_absent_file(&name);
}
}
}
pub fn read_toks(&mut self, n: i32, r: crate::hash::CsId, j: u32) -> Vec<Token> {
self.input.scanner_status = ScannerStatus::Defining;
self.input.warning_index = r;
let mut out: Vec<Token> = vec![Token::END_MATCH];
let m: usize = if (0..=15).contains(&n) {
n as usize
} else {
16
};
let s = self.input.align_state;
self.input.align_state = 1_000_000;
loop {
let line: Vec<u8> = match self.files.read_files.get_mut(m) {
None | Some(ReadFile::Closed) => {
self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
self.fatal_error("*** (cannot \\read from terminal in nonstop modes)");
}
Some(ReadFile::JustOpen(lines)) => {
let first = lines.pop_front();
match first {
Some(l) => {
let rest = std::mem::take(lines);
self.files.read_files[m] = ReadFile::Normal(rest);
l
}
None => {
self.files.read_files[m] = ReadFile::Closed;
Vec::new()
}
}
}
Some(ReadFile::Normal(lines)) => match lines.pop_front() {
Some(l) => l,
None => {
self.files.read_files[m] = ReadFile::Closed;
if self.input.align_state != 1_000_000 {
self.runaway();
self.print_err("File ended within ");
self.print_esc_str("read");
self.help(&["This \\read has unbalanced braces."]);
self.input.align_state = 1_000_000;
self.error();
}
Vec::new()
}
},
};
let end_line_char = self.eqtb.int(pg::assign_int::ENDLINECHAR);
let mut buf = crate::input::decode_line(&line);
if (0..=0x10_FFFF).contains(&end_line_char) {
buf.push(end_line_char as u32);
}
if j == 1 {
for &c in &buf {
out.push(if c == ' ' as u32 {
Token::SPACE
} else {
Token::other(c)
});
}
break;
}
self.begin_file_reading(LineSource::Read(m as u32), Vec::new(), false);
if let Some(Level::Text(t)) = self.input.stack.last_mut() {
t.buf = buf;
t.loc = 0;
t.state = crate::input::LexState::NewLine;
}
loop {
self.get_token();
if self.cur_tok.0 == 0 {
break;
}
if self.input.align_state < 1_000_000 {
loop {
self.get_token();
if self.cur_tok.0 == 0 {
break;
}
}
self.input.align_state = 1_000_000;
break;
}
out.push(self.cur_tok);
}
self.end_file_reading();
if self.input.align_state == 1_000_000 {
break;
}
}
self.input.scanner_status = ScannerStatus::Normal;
self.input.align_state = s;
out
}
pub fn pseudo_start(&mut self, text_tokens: bool) {
let list = self.scan_general_text();
let s = self.print_to_string(|e| e.show_token_list(&list, 0, list.len(), i64::MAX));
let nl = self.eqtb.int(pg::assign_int::NEWLINECHAR);
let mut lines: VecDeque<Vec<u8>> = VecDeque::new();
let mut rest = s.as_slice();
while !rest.is_empty() {
let end = if (0..128).contains(&nl) {
rest.iter().position(|&b| i32::from(b) == nl)
} else {
None
};
let (line, tail) = match end {
Some(i) => (&rest[..i], &rest[i + 1..]),
None => (rest, &rest[rest.len()..]),
};
lines.push_back(line.to_vec());
rest = tail;
}
let tracing = self.eqtb.int(pg::assign_int::TRACINGSCANTOKENS) > 0 && !text_tokens;
self.begin_file_reading(
LineSource::Lines {
lines,
catcodes: None,
pseudo: !text_tokens,
},
Vec::new(),
tracing,
);
if let Some(Level::Text(t)) = self.input.stack.last_mut() {
t.line = 0;
t.buf.clear();
t.loc = 0;
t.eof_seen = text_tokens;
}
if tracing {
if self.printer.term_offset > self.printer.max_print_line.saturating_sub(3) {
self.printer.print_ln();
} else if self.printer.term_offset > 0 || self.printer.file_offset > 0 {
self.printer.print_char(' ' as u32);
}
self.printer.print_str("( ");
self.input.open_parens += 1;
self.printer.update_terminal();
}
}
pub fn file_closed_checks(&mut self) {}
pub fn close_files_and_terminate(&mut self) {
for n in 0..128u32 {
if self.printer.write_open(n) {
self.printer.close_write(n);
}
}
self.print_memory_stats();
let pdf_mode = self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0;
let fatal = self.err.history == crate::error::History::FatalErrorStop && !self.pdf.fatal;
if fatal && pdf_mode {
self.pdf.fatal = true;
self.finish_output();
self.printer
.print_nl("! ==> Fatal error occurred, no output PDF file produced!");
if self.printer.log_opened() {
self.printer.print_ln();
}
let _ = std::fs::remove_file(self.output_path("pdf"));
self.finish_log();
return;
}
if fatal {
self.printer
.print_nl("! ==> Fatal error occurred, bad output DVI file produced!");
self.printer.print_ln();
}
self.finish_output();
if self.total_pages == 0 {
self.printer.print_nl(
if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0 {
"warning (pdf backend): no pages of output."
} else {
"No pages of output."
},
);
self.printer.print_ln();
if self.pdf.stream_written
&& self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0
{
self.pdf_fatal("already written content discarded, no output file produced.");
}
}
if self.eqtb.int(crate::primitives_gen::assign_int::OUTPUTMODE) > 0
&& self.pdf.max_obj() > 0
&& !self.pdf.fatal
&& self.printer.log_opened()
{
if self.printer.file_offset > 0 {
self.printer.print_ln();
}
let n = self.pdf.max_obj();
let names = self.pdf.named_dests();
let extra = 1
+ 8 * self.pdf.outlines.len()
+ 10 * self.box_resources.len()
+ self.pdf.obj_words;
let (objs, streams) = self.pdf.objstm_stats;
let compressed = if streams > 0 {
format!(
" {objs} compressed objects within {streams} object stream{}\n",
if streams == 1 { "" } else { "s" }
)
} else {
String::new()
};
self.printer.log_raw(
format!(
"\nPDF statistics: {n} PDF objects out of {} (max. 8388607)\n{compressed} \
{names} named destinations out of {} (max. 131072)\n \
{extra} words of extra memory for PDF output out of {} (max. 100000000)\n",
n.max(1000),
names.max(1000),
extra.max(10000),
)
.as_bytes(),
);
}
if self.printer.log_opened() {
self.printer.log_raw(b"\n");
}
self.finish_synctex();
self.finish_log();
}
fn print_memory_stats(&mut self) {
if self.diagnostics.is_some()
|| !self.printer.log_opened()
|| self
.eqtb
.int(crate::primitives_gen::assign_int::TRACINGSTATS)
<= 0
{
return;
}
let sel = self.printer.selector;
if sel == Selector::TermAndLog {
self.printer.selector = Selector::LogOnly;
}
if self.printer.file_offset > 0 {
self.printer.print_ln();
}
self.printer.print_ln();
self.printer
.print_str("Here is how much of LuaTeX's memory you used:");
let cs = self.hash.len();
self.printer.print_nl(&format!(
" {cs} multiletter control sequences out of 65536+600000"
));
let fonts = self.fonts.fonts.len();
self.printer.print_nl(&format!(" {fonts} fonts"));
self.printer.print_ln();
self.printer.selector = sel;
}
pub(crate) fn finish_log(&mut self) {
if self.printer.log_opened() {
self.printer.close_log();
self.printer.selector = Selector::TermOnly;
if let Some(name) = self.files.log_name.clone() {
let shown = match &self.files.output_dir {
Some(dir) if !self.files.log_in_texmfoutput => {
name.strip_prefix(dir).map(PathBuf::from).unwrap_or(name)
}
_ => name,
};
self.printer.print_nl("Transcript written on ");
self.printer.print_bytes(shown.to_string_lossy().as_bytes());
self.printer.print_char('.' as u32);
}
}
self.printer.print_ln();
self.printer.flush();
}
}
pub type WrittenSink = std::rc::Rc<std::cell::RefCell<HashMap<PathBuf, Vec<u8>>>>;
pub struct RecordingWriter {
inner: std::io::BufWriter<std::fs::File>,
path: PathBuf,
sink: WrittenSink,
}
impl RecordingWriter {
pub fn new(f: std::fs::File, path: PathBuf, sink: WrittenSink) -> Self {
sink.borrow_mut().insert(path.clone(), Vec::new());
RecordingWriter {
inner: std::io::BufWriter::new(f),
path,
sink,
}
}
}
impl RecordingWriter {
pub fn resume(f: std::fs::File, path: PathBuf, sink: WrittenSink) -> Self {
sink.borrow_mut().entry(path.clone()).or_default();
RecordingWriter {
inner: std::io::BufWriter::new(f),
path,
sink,
}
}
}
impl std::io::Write for RecordingWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if let Some(v) = self.sink.borrow_mut().get_mut(&self.path) {
v.extend_from_slice(buf);
}
self.inner.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}