use sha2::Digest;
use std::{
borrow::Cow,
fs::File,
io::{self, Cursor, Read, Seek, SeekFrom, Write},
path::{Path, PathBuf},
};
use tectonic_errors::{
anyhow::{bail, ensure},
Error, Result,
};
use tectonic_status_base::StatusBackend;
use thiserror::Error as ThisError;
use crate::digest::DigestData;
pub mod app_dirs;
pub mod digest;
pub mod filesystem;
pub mod flate2;
pub mod stack;
pub mod stdstreams;
#[derive(ThisError, Debug)]
pub enum TectonicIoError {
#[error("cannot seek within this stream")]
NotSeekable,
#[error("cannot obtain the size of this stream")]
NotSizeable,
#[error("access to the path `{}` is forbidden", .0.display())]
PathForbidden(PathBuf),
}
pub trait InputFeatures: Read {
fn get_size(&mut self) -> Result<usize>;
fn try_seek(&mut self, pos: SeekFrom) -> Result<u64>;
fn get_unix_mtime(&mut self) -> Result<Option<i64>> {
Ok(None)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum InputOrigin {
Filesystem,
NotInput,
Other,
}
pub struct InputHandle {
name: String,
inner: Box<dyn InputFeatures>,
read_only: bool,
digest: digest::DigestComputer,
origin: InputOrigin,
ever_read: bool,
did_unhandled_seek: bool,
ungetc_char: Option<u8>,
}
impl InputHandle {
pub fn new<T: 'static + InputFeatures>(
name: impl Into<String>,
inner: T,
origin: InputOrigin,
) -> InputHandle {
InputHandle {
name: name.into(),
inner: Box::new(inner),
read_only: false,
digest: Default::default(),
origin,
ever_read: false,
did_unhandled_seek: false,
ungetc_char: None,
}
}
pub fn new_read_only<T: 'static + InputFeatures>(
name: impl Into<String>,
inner: T,
origin: InputOrigin,
) -> InputHandle {
InputHandle {
name: name.into(),
inner: Box::new(inner),
read_only: true,
digest: Default::default(),
origin,
ever_read: false,
did_unhandled_seek: false,
ungetc_char: None,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn origin(&self) -> InputOrigin {
self.origin
}
pub fn into_inner(self) -> Box<dyn InputFeatures> {
self.inner
}
pub fn scan_remainder(&mut self) -> Result<()> {
const BUFSIZE: usize = 1024;
let mut buf: [u8; BUFSIZE] = [0; BUFSIZE];
loop {
let n = match self.inner.read(&mut buf[..]) {
Ok(n) => n,
Err(ref ioe) if ioe.raw_os_error() == Some(libc::EISDIR) => return Ok(()),
Err(e) => return Err(e.into()),
};
if n == 0 {
break;
}
self.digest.update(&buf[..n]);
}
Ok(())
}
pub fn into_name_digest(self) -> (String, Option<DigestData>) {
if self.did_unhandled_seek || !self.ever_read || self.read_only {
(self.name, None)
} else {
(self.name, Some(DigestData::from(self.digest)))
}
}
pub fn getc(&mut self) -> Result<u8> {
if let Some(c) = self.ungetc_char {
self.ungetc_char = None;
return Ok(c);
}
let mut byte = [0u8; 1];
if self.read(&mut byte[..1])? == 0 {
return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF in getc").into());
}
Ok(byte[0])
}
pub fn ungetc(&mut self, byte: u8) -> Result<()> {
ensure!(
self.ungetc_char.is_none(),
"internal problem: cannot ungetc() more than once in a row"
);
self.ungetc_char = Some(byte);
Ok(())
}
}
impl Read for InputHandle {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if !buf.is_empty() {
if let Some(c) = self.ungetc_char {
buf[0] = c;
self.ungetc_char = None;
return Ok(self.read(&mut buf[1..])? + 1);
}
}
self.ever_read = true;
let n = self.inner.read(buf)?;
if !self.read_only {
self.digest.update(&buf[..n]);
}
Ok(n)
}
}
impl Seek for InputHandle {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.try_seek(pos).map_err(io::Error::other)
}
}
impl InputFeatures for InputHandle {
fn get_size(&mut self) -> Result<usize> {
self.inner.get_size()
}
fn get_unix_mtime(&mut self) -> Result<Option<i64>> {
self.inner.get_unix_mtime()
}
fn try_seek(&mut self, pos: SeekFrom) -> Result<u64> {
match pos {
SeekFrom::Start(0) => {
self.digest = Default::default();
self.ever_read = false;
self.ungetc_char = None;
}
SeekFrom::Current(0) => {
}
_ => {
self.did_unhandled_seek = true;
self.ungetc_char = None;
}
}
let mut offset = self.inner.try_seek(pos)?;
if self.ungetc_char.is_some() {
offset -= 1;
}
Ok(offset)
}
}
pub struct OutputHandle {
name: String,
inner: Box<dyn Write>,
digest: digest::DigestComputer,
}
impl OutputHandle {
pub fn new<T: 'static + Write>(name: impl Into<String>, inner: T) -> OutputHandle {
OutputHandle {
name: name.into(),
inner: Box::new(inner),
digest: digest::create(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn into_inner(self) -> Box<dyn Write> {
self.inner
}
pub fn into_name_digest(self) -> (String, DigestData) {
(self.name, DigestData::from(self.digest))
}
}
impl Write for OutputHandle {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let n = self.inner.write(buf)?;
self.digest.update(&buf[..n]);
Ok(n)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
#[derive(Debug)]
pub enum OpenResult<T> {
Ok(T),
NotAvailable,
Err(Error),
}
impl<T> OpenResult<T> {
pub fn unwrap(self) -> T {
match self {
OpenResult::Ok(t) => t,
_ => panic!("expected an open file"),
}
}
pub fn is_not_available(&self) -> bool {
matches!(*self, OpenResult::NotAvailable)
}
pub fn must_exist(self) -> Result<T> {
match self {
OpenResult::Ok(t) => Ok(t),
OpenResult::Err(e) => Err(e),
OpenResult::NotAvailable => {
Err(io::Error::new(io::ErrorKind::NotFound, "not found").into())
}
}
}
}
pub trait AsIoProviderMut {
fn as_ioprovider_mut(&mut self) -> &mut dyn IoProvider;
}
impl<T: IoProvider> AsIoProviderMut for T {
fn as_ioprovider_mut(&mut self) -> &mut dyn IoProvider {
self
}
}
pub trait IoProvider: AsIoProviderMut {
fn output_open_name(&mut self, _name: &str) -> OpenResult<OutputHandle> {
OpenResult::NotAvailable
}
fn output_open_stdout(&mut self) -> OpenResult<OutputHandle> {
OpenResult::NotAvailable
}
fn input_open_name(
&mut self,
_name: &str,
_status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
OpenResult::NotAvailable
}
fn input_open_name_with_abspath(
&mut self,
name: &str,
status: &mut dyn StatusBackend,
) -> OpenResult<(InputHandle, Option<PathBuf>)> {
match self.input_open_name(name, status) {
OpenResult::Ok(h) => OpenResult::Ok((h, None)),
OpenResult::Err(e) => OpenResult::Err(e),
OpenResult::NotAvailable => OpenResult::NotAvailable,
}
}
fn input_open_primary(&mut self, _status: &mut dyn StatusBackend) -> OpenResult<InputHandle> {
OpenResult::NotAvailable
}
fn input_open_primary_with_abspath(
&mut self,
status: &mut dyn StatusBackend,
) -> OpenResult<(InputHandle, Option<PathBuf>)> {
match self.input_open_primary(status) {
OpenResult::Ok(h) => OpenResult::Ok((h, None)),
OpenResult::Err(e) => OpenResult::Err(e),
OpenResult::NotAvailable => OpenResult::NotAvailable,
}
}
fn input_open_format(
&mut self,
name: &str,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
self.input_open_name(name, status)
}
fn write_format(
&mut self,
_name: &str,
_data: &[u8],
_status: &mut dyn StatusBackend,
) -> Result<()> {
bail!("this I/O layer cannot save format files");
}
}
impl<P: IoProvider + ?Sized> IoProvider for Box<P> {
fn output_open_name(&mut self, name: &str) -> OpenResult<OutputHandle> {
(**self).output_open_name(name)
}
fn output_open_stdout(&mut self) -> OpenResult<OutputHandle> {
(**self).output_open_stdout()
}
fn input_open_name(
&mut self,
name: &str,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
(**self).input_open_name(name, status)
}
fn input_open_name_with_abspath(
&mut self,
name: &str,
status: &mut dyn StatusBackend,
) -> OpenResult<(InputHandle, Option<PathBuf>)> {
(**self).input_open_name_with_abspath(name, status)
}
fn input_open_primary(&mut self, status: &mut dyn StatusBackend) -> OpenResult<InputHandle> {
(**self).input_open_primary(status)
}
fn input_open_primary_with_abspath(
&mut self,
status: &mut dyn StatusBackend,
) -> OpenResult<(InputHandle, Option<PathBuf>)> {
(**self).input_open_primary_with_abspath(status)
}
fn input_open_format(
&mut self,
name: &str,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
(**self).input_open_format(name, status)
}
fn write_format(
&mut self,
name: &str,
data: &[u8],
status: &mut dyn StatusBackend,
) -> Result<()> {
(**self).write_format(name, data, status)
}
}
impl InputFeatures for Cursor<Vec<u8>> {
fn get_size(&mut self) -> Result<usize> {
Ok(self.get_ref().len())
}
fn get_unix_mtime(&mut self) -> Result<Option<i64>> {
Ok(None)
}
fn try_seek(&mut self, pos: SeekFrom) -> Result<u64> {
Ok(self.seek(pos)?)
}
}
pub fn try_open_file<P: AsRef<Path>>(path: P) -> OpenResult<File> {
use std::io::ErrorKind::NotFound;
match File::open(path) {
Ok(f) => OpenResult::Ok(f),
Err(e) => {
if e.kind() == NotFound {
OpenResult::NotAvailable
} else {
OpenResult::Err(e.into())
}
}
}
}
fn try_normalize_tex_path(path: &str) -> Option<String> {
use std::iter::repeat_n;
if path.is_empty() {
return Some("".into());
}
let mut r = Vec::new();
let mut parent_level = 0;
let mut has_root = false;
for (i, c) in path.split('/').enumerate() {
match c {
"" if i == 0 => {
has_root = true;
r.push("");
}
"" | "." => {}
".." => {
match r.pop() {
Some("") => return None,
None => parent_level += 1,
_ => {}
}
}
_ => r.push(c),
}
}
let r = repeat_n("..", parent_level)
.chain(r)
.collect::<Vec<_>>()
.join("/");
if r.is_empty() {
if has_root {
Some("/".into())
} else {
Some(".".into())
}
} else {
Some(r)
}
}
pub fn normalize_tex_path(path: &str) -> Cow<'_, str> {
if let Some(t) = try_normalize_tex_path(path) {
Cow::Owned(t)
} else {
Cow::Borrowed(path)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_try_normalize_tex_path() {
assert_eq!(try_normalize_tex_path(""), Some("".into()));
assert_eq!(try_normalize_tex_path("/"), Some("/".into()));
assert_eq!(try_normalize_tex_path("//"), Some("/".into()));
assert_eq!(try_normalize_tex_path("."), Some(".".into()));
assert_eq!(try_normalize_tex_path("./"), Some(".".into()));
assert_eq!(try_normalize_tex_path(".."), Some("..".into()));
assert_eq!(try_normalize_tex_path("././/./"), Some(".".into()));
assert_eq!(try_normalize_tex_path("/././/."), Some("/".into()));
assert_eq!(
try_normalize_tex_path("my/path/file.txt"),
Some("my/path/file.txt".into())
);
assert_eq!(
try_normalize_tex_path(" my/pa th/file .txt "),
Some(" my/pa th/file .txt ".into())
);
assert_eq!(
try_normalize_tex_path("/my/path/file.txt"),
Some("/my/path/file.txt".into())
);
assert_eq!(
try_normalize_tex_path("./my///path/././file.txt"),
Some("my/path/file.txt".into())
);
assert_eq!(
try_normalize_tex_path("./../my/../../../file.txt"),
Some("../../../file.txt".into())
);
assert_eq!(
try_normalize_tex_path("././my//../path/../here/file.txt"),
Some("here/file.txt".into())
);
assert_eq!(
try_normalize_tex_path("./my/.././/path/../../here//file.txt"),
Some("../here/file.txt".into())
);
assert_eq!(try_normalize_tex_path("/my/../../file.txt"), None);
assert_eq!(
try_normalize_tex_path("/my/./.././path//../../file.txt"),
None
);
}
}