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};
#[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<()> {
if self.index.is_initialized() {
return Ok(());
}
self.connect_reader();
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 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),
};
let mut v = Vec::with_capacity(info.length);
tt_note!(status, "downloading {}", info.name);
if info.length == 0 {
return OpenResult::Ok(InputHandle::new_read_only(
info.name.to_owned(),
Cursor::new(v),
InputOrigin::Other,
));
}
for i in 0..NET_RETRY_ATTEMPTS {
let mut stream = match self
.reader
.as_mut()
.unwrap()
.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;
}
};
match stream.read_to_end(&mut v) {
Ok(_) => {}
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;
}
};
return OpenResult::Ok(InputHandle::new_read_only(
info.name.to_owned(),
Cursor::new(v),
InputOrigin::Other,
));
}
OpenResult::Err(anyhow!(
"failed to download \"{}\"; please check your network connection.",
info.name
))
}
}