use crate::{Bundle, CachableBundle, FileIndex, FileInfo, NET_RETRY_ATTEMPTS, NET_RETRY_SLEEP_MS};
use flate2::read::GzDecoder;
use std::{
collections::HashMap,
io::{BufRead, BufReader, Cursor, Read},
str::FromStr,
thread,
time::Duration,
};
use tectonic_errors::prelude::*;
use tectonic_geturl::{DefaultBackend, DefaultRangeReader, GetUrlBackend, RangeReader};
use tectonic_io_base::{digest, InputHandle, InputOrigin, IoProvider, OpenResult};
use tectonic_status_base::{tt_note, tt_warning, NoopStatusBackend, StatusBackend};
mod batch;
fn read_file_with_retries(
reader: &mut DefaultRangeReader,
info: &ItarFileInfo,
status: &mut dyn StatusBackend,
) -> OpenResult<Vec<u8>> {
if info.length == 0 {
return OpenResult::Ok(Vec::new());
}
for i in 0..NET_RETRY_ATTEMPTS {
let mut stream = match reader.read_range(info.offset, info.length) {
Ok(r) => r,
Err(e) => {
tt_warning!(status,
"failure fetching \"{}\" from network ({}/{NET_RETRY_ATTEMPTS})",
info.name, i+1; e
);
thread::sleep(Duration::from_millis(NET_RETRY_SLEEP_MS));
continue;
}
};
let mut v = Vec::with_capacity(info.length);
match stream.read_to_end(&mut v) {
Ok(_) => return OpenResult::Ok(v),
Err(e) => {
tt_warning!(status,
"failure downloading \"{}\" from network ({}/{NET_RETRY_ATTEMPTS})",
info.name, i+1; e.into()
);
thread::sleep(Duration::from_millis(NET_RETRY_SLEEP_MS));
continue;
}
};
}
OpenResult::Err(anyhow!(
"failed to download \"{}\"; please check your network connection.",
info.name
))
}
#[derive(Clone, Debug)]
pub struct ItarFileInfo {
name: String,
offset: u64,
length: usize,
}
impl FileInfo for ItarFileInfo {
fn name(&self) -> &str {
&self.name
}
fn path(&self) -> &str {
&self.name
}
}
#[derive(Default, Debug)]
pub struct ItarFileIndex {
content: HashMap<String, ItarFileInfo>,
}
impl<'this> FileIndex<'this> for ItarFileIndex {
type InfoType = ItarFileInfo;
fn iter(&'this self) -> Box<dyn Iterator<Item = &'this ItarFileInfo> + 'this> {
Box::new(self.content.values())
}
fn len(&self) -> usize {
self.content.len()
}
fn initialize(&mut self, reader: &mut dyn Read) -> Result<()> {
self.content.clear();
for line in BufReader::new(reader).lines() {
let line = line?;
let mut bits = line.split_whitespace();
if let (Some(name), Some(offset), Some(length)) =
(bits.next(), bits.next(), bits.next())
{
self.content.insert(
name.to_owned(),
ItarFileInfo {
name: name.to_owned(),
offset: offset.parse::<u64>()?,
length: length.parse::<usize>()?,
},
);
} else {
bail!("malformed index line");
}
}
Ok(())
}
fn search(&'this mut self, name: &str) -> Option<ItarFileInfo> {
self.content.get(name).cloned()
}
}
pub struct ItarBundle {
url: String,
index: ItarFileIndex,
reader: Option<DefaultRangeReader>,
}
impl ItarBundle {
pub fn new(url: String) -> Result<ItarBundle> {
Ok(ItarBundle {
index: ItarFileIndex::default(),
reader: None,
url,
})
}
fn connect_reader(&mut self) {
let geturl_backend = DefaultBackend::default();
if self.reader.is_none() {
self.reader = Some(geturl_backend.open_range_reader(&self.url));
}
}
fn ensure_index(&mut self) -> Result<()> {
self.connect_reader();
if self.index.is_initialized() {
return Ok(());
}
let mut reader = self.get_index_reader()?;
self.index.initialize(&mut reader)?;
Ok(())
}
}
impl IoProvider for ItarBundle {
fn input_open_name(
&mut self,
name: &str,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
if let Err(e) = self.ensure_index() {
return OpenResult::Err(e);
};
let info = match self.index.search(name) {
Some(a) => a,
None => return OpenResult::NotAvailable,
};
self.open_fileinfo(&info, status)
}
}
impl Bundle for ItarBundle {
fn all_files(&self) -> Vec<String> {
self.index.iter().map(|x| x.path().to_owned()).collect()
}
fn get_digest(&mut self) -> Result<tectonic_io_base::digest::DigestData> {
let digest_text = match self.input_open_name(digest::DIGEST_NAME, &mut NoopStatusBackend {})
{
OpenResult::Ok(h) => {
let mut text = String::new();
h.take(64).read_to_string(&mut text)?;
text
}
OpenResult::NotAvailable => {
bail!("bundle does not provide needed SHA256SUM file");
}
OpenResult::Err(e) => {
return Err(e);
}
};
Ok(atry!(digest::DigestData::from_str(&digest_text); ["corrupted SHA256 digest data"]))
}
}
impl CachableBundle<'_, ItarFileIndex> for ItarBundle {
fn supports_batch_open(&self) -> bool {
true
}
fn get_location(&mut self) -> String {
self.url.clone()
}
fn initialize_index(&mut self, source: &mut dyn Read) -> Result<()> {
self.index.initialize(source)?;
Ok(())
}
fn index(&mut self) -> &mut ItarFileIndex {
&mut self.index
}
fn search(&mut self, name: &str) -> Option<ItarFileInfo> {
self.index.search(name)
}
fn get_index_reader(&mut self) -> Result<Box<dyn Read>> {
let mut geturl_backend = DefaultBackend::default();
let index_url = format!("{}.index.gz", self.url);
let reader = GzDecoder::new(geturl_backend.get_url(&index_url)?);
Ok(Box::new(reader))
}
fn open_fileinfo(
&mut self,
info: &ItarFileInfo,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
match self.ensure_index() {
Ok(_) => {}
Err(e) => return OpenResult::Err(e),
};
tt_note!(status, "downloading {}", info.name);
match read_file_with_retries(self.reader.as_mut().unwrap(), info, status) {
OpenResult::Ok(v) => OpenResult::Ok(InputHandle::new_read_only(
info.name.to_owned(),
Cursor::new(v),
InputOrigin::Other,
)),
OpenResult::NotAvailable => OpenResult::NotAvailable,
OpenResult::Err(e) => OpenResult::Err(e),
}
}
fn batch_open(
&mut self,
infos: &[ItarFileInfo],
status: &mut dyn StatusBackend,
) -> Vec<OpenResult<Vec<u8>>> {
batch::open(self, infos, status)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cached_index_still_connects_range_reader() {
let mut bundle = ItarBundle::new("https://example.invalid/bundle.tar".into()).unwrap();
bundle
.index
.initialize(&mut Cursor::new(b"plain.tex 0 1\n"))
.unwrap();
assert!(bundle.reader.is_none());
bundle.ensure_index().unwrap();
assert!(bundle.reader.is_some());
}
}