use std::{fmt::Debug, io::Read, path::PathBuf};
use tectonic_errors::{prelude::bail, Result};
use tectonic_io_base::{digest::DigestData, InputHandle, IoProvider, OpenResult};
use tectonic_status_base::StatusBackend;
pub mod cache;
pub mod dir;
pub mod itar;
mod ttb;
pub mod ttb_fs;
pub mod ttb_net;
pub mod zip;
use cache::BundleCache;
use dir::DirBundle;
use itar::ItarBundle;
use ttb_fs::TTBFsBundle;
use ttb_net::TTBNetBundle;
use zip::ZipBundle;
const TECTONIC_BUNDLE_PREFIX_DEFAULT: &str = "https://relay.fullyjustified.net";
const NET_RETRY_ATTEMPTS: usize = 3;
const NET_RETRY_SLEEP_MS: u64 = 500;
pub trait FileInfo: Clone + Debug {
fn path(&self) -> &str;
fn name(&self) -> &str;
}
pub trait FileIndex<'this>
where
Self: Sized + 'this + Debug,
{
type InfoType: FileInfo;
fn iter(&'this self) -> Box<dyn Iterator<Item = &'this Self::InfoType> + 'this>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn is_initialized(&self) -> bool {
!self.is_empty()
}
fn initialize(&mut self, reader: &mut dyn Read) -> Result<()>;
fn search(&'this mut self, name: &str) -> Option<Self::InfoType>;
}
pub trait Bundle: IoProvider {
fn get_digest(&mut self) -> Result<DigestData>;
fn all_files(&self) -> Vec<String>;
}
impl<B: Bundle + ?Sized> Bundle for Box<B> {
fn get_digest(&mut self) -> Result<DigestData> {
(**self).get_digest()
}
fn all_files(&self) -> Vec<String> {
(**self).all_files()
}
}
pub trait CachableBundle<'this, T>
where
Self: Bundle + 'this,
T: FileIndex<'this>,
{
fn initialize_index(&mut self, _source: &mut dyn Read) -> Result<()> {
Ok(())
}
fn get_index_reader(&mut self) -> Result<Box<dyn Read>>;
fn index(&mut self) -> &mut T;
fn open_fileinfo(
&mut self,
info: &T::InfoType,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle>;
fn search(&mut self, name: &str) -> Option<T::InfoType>;
fn get_location(&mut self) -> String;
}
impl<'this, T: FileIndex<'this>, B: CachableBundle<'this, T> + ?Sized> CachableBundle<'this, T>
for Box<B>
{
fn initialize_index(&mut self, source: &mut dyn Read) -> Result<()> {
(**self).initialize_index(source)
}
fn get_location(&mut self) -> String {
(**self).get_location()
}
fn get_index_reader(&mut self) -> Result<Box<dyn Read>> {
(**self).get_index_reader()
}
fn index(&mut self) -> &mut T {
(**self).index()
}
fn open_fileinfo(
&mut self,
info: &T::InfoType,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
(**self).open_fileinfo(info, status)
}
fn search(&mut self, name: &str) -> Option<T::InfoType> {
(**self).search(name)
}
}
pub fn detect_bundle(
source: String,
only_cached: bool,
custom_cache_dir: Option<PathBuf>,
) -> Result<Option<Box<dyn Bundle>>> {
use url::Url;
if let Ok(url) = Url::parse(&source) {
if url.scheme() == "https" || url.scheme() == "http" {
if source.ends_with("ttb") {
let bundle = BundleCache::new(
Box::new(TTBNetBundle::new(source)?),
only_cached,
custom_cache_dir,
)?;
return Ok(Some(Box::new(bundle)));
} else {
let bundle = BundleCache::new(
Box::new(ItarBundle::new(source)?),
only_cached,
custom_cache_dir,
)?;
return Ok(Some(Box::new(bundle)));
}
} else if url.scheme() == "file" {
let file_path = url.to_file_path().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"failed to parse local path",
)
})?;
return bundle_from_path(file_path);
} else {
return Ok(None);
}
} else {
return bundle_from_path(PathBuf::from(source));
}
fn bundle_from_path(p: PathBuf) -> Result<Option<Box<dyn Bundle>>> {
let ext = p.extension().map_or("", |x| x.to_str().unwrap_or(""));
if p.is_dir() {
Ok(Some(Box::new(DirBundle::new(p))))
} else if ext == "zip" {
Ok(Some(Box::new(ZipBundle::open(p)?)))
} else if ext == "ttb" {
Ok(Some(Box::new(TTBFsBundle::open(p)?)))
} else {
Ok(None)
}
}
}
pub fn get_fallback_bundle_url(format_version: u32) -> String {
let bundle_locked = option_env!("TECTONIC_BUNDLE_LOCKED").unwrap_or("");
let bundle_prefix =
option_env!("TECTONIC_BUNDLE_PREFIX").unwrap_or(TECTONIC_BUNDLE_PREFIX_DEFAULT);
if !bundle_locked.is_empty() {
return bundle_locked.to_owned();
}
if format_version < 32 {
format!("{bundle_prefix}/default_bundle.tar")
} else {
format!("{bundle_prefix}/default_bundle_v{format_version}.tar")
}
}
pub fn get_fallback_bundle(format_version: u32, only_cached: bool) -> Result<Box<dyn Bundle>> {
let url = get_fallback_bundle_url(format_version);
let bundle = detect_bundle(url, only_cached, None)?;
if bundle.is_none() {
bail!("could not open default bundle")
}
Ok(bundle.unwrap())
}