tectonic(!): switch to using `tectonic_bundles`

Start using the separated-out bundle implementation crate. Now you can
work with bundles, and the cache, without having to link to XeTeX and
everything! This is a BREAKING CHANGE because the original bundle
implementations have been removed, and the Bundle trait has gained a new
required method.
This commit is contained in:
Peter Williams 2021-06-13 11:47:54 -04:00
parent a070d2b4f8
commit 51ee421466
11 changed files with 60 additions and 796 deletions

View File

@ -57,8 +57,10 @@ sub-crates:
- [`tectonic_bridge_graphite2`](https://crates.io/crates/tectonic_bridge_graphite2)
- [`tectonic_bridge_harfbuzz`](https://crates.io/crates/tectonic_bridge_harfbuzz)
- [`tectonic_bridge_icu`](https://crates.io/crates/tectonic_bridge_icu)
- [`tectonic_bundles`](https://crates.io/crates/tectonic_bundles)
- [`tectonic_cfg_support`](https://crates.io/crates/tectonic_cfg_support)
- [`tectonic_dep_support`](https://crates.io/crates/tectonic_dep_support)
- [`tectonic_docmodel`](https://crates.io/crates/tectonic_docmodel)
- [`tectonic_engine_bibtex`](https://crates.io/crates/tectonic_engine_bibtex)
- [`tectonic_engine_xdvipdfmx`](https://crates.io/crates/tectonic_engine_xdvipdfmx)
- [`tectonic_engine_xetex`](https://crates.io/crates/tectonic_engine_xetex)

14
Cargo.lock generated
View File

@ -2122,6 +2122,7 @@ dependencies = [
"sha2",
"structopt",
"tectonic_bridge_core",
"tectonic_bundles",
"tectonic_docmodel",
"tectonic_engine_bibtex",
"tectonic_engine_xdvipdfmx",
@ -2196,6 +2197,19 @@ dependencies = [
"tectonic_dep_support",
]
[[package]]
name = "tectonic_bundles"
version = "0.0.0-dev.0"
dependencies = [
"flate2",
"fs2",
"tectonic_errors",
"tectonic_geturl",
"tectonic_io_base",
"tectonic_status_base",
"zip",
]
[[package]]
name = "tectonic_cfg_support"
version = "0.0.0-dev.0"

View File

@ -67,6 +67,7 @@ serde = { version = "^1.0", features = ["derive"], optional = true }
sha2 = "^0.9"
structopt = "0.3"
tectonic_bridge_core = { path = "crates/bridge_core", version = "0.0.0-dev.0" }
tectonic_bundles = { path = "crates/bundles", version = "0.0.0-dev.0", default-features = false }
tectonic_docmodel = { path = "crates/docmodel", version = "0.0.0-dev.0", optional = true }
tectonic_engine_bibtex = { path = "crates/engine_bibtex", version = "0.0.0-dev.0" }
tectonic_engine_xdvipdfmx = { path = "crates/engine_xdvipdfmx", version = "0.0.0-dev.0" }
@ -97,10 +98,10 @@ serialization = ["serde", "tectonic_docmodel", "toml"]
external-harfbuzz = ["tectonic_engine_xetex/external-harfbuzz"]
geturl-curl = ["tectonic_geturl/curl"]
geturl-reqwest = ["tectonic_geturl/reqwest"]
geturl-curl = ["tectonic_bundles/geturl-curl", "tectonic_geturl/curl"]
geturl-reqwest = ["tectonic_bundles/geturl-reqwest", "tectonic_geturl/reqwest"]
native-tls-vendored = ["tectonic_geturl/native-tls-vendored"]
native-tls-vendored = ["tectonic_bundles/native-tls-vendored", "tectonic_geturl/native-tls-vendored"]
# developer feature to compile with the necessary flags for profiling tectonic.
profile = []
@ -129,6 +130,7 @@ tectonic_bridge_flate = "thiscommit:2021-01-01:eer4ahL4"
tectonic_bridge_graphite2 = "2c1ffcd702a662c003bd3d7d0ca4d169784cb6ad"
tectonic_bridge_harfbuzz = "2c1ffcd702a662c003bd3d7d0ca4d169784cb6ad"
tectonic_bridge_icu = "2c1ffcd702a662c003bd3d7d0ca4d169784cb6ad"
tectonic_bundles = "thiscommit:2021-06-13:Q0esYor"
tectonic_cfg_support = "thiscommit:aeRoo7oa"
tectonic_dep_support = "5faf4205bdd3d31101b749fc32857dd746f9e5bc"
tectonic_docmodel = "cd77b60d48b1ae3ef80d708e6858ea91cd9fa812"

View File

@ -15,15 +15,14 @@ use std::{
path::{Path, PathBuf},
sync::atomic::{AtomicBool, Ordering},
};
use tectonic_bundles::{
cache::Cache, dir::DirBundle, itar::IndexedTarBackend, zip::ZipBundle, Bundle,
};
use tectonic_io_base::app_dirs;
use url::Url;
use crate::{
app_dirs,
errors::{ErrorKind, Result},
io::cached_itarbundle::CachedITarBundle,
io::dirbundle::DirBundle,
io::zipbundle::ZipBundle,
io::Bundle,
status::StatusBackend,
};
@ -123,8 +122,13 @@ impl PersistentConfig {
custom_cache_root: Option<&Path>,
status: &mut dyn StatusBackend,
) -> Result<Box<dyn Bundle>> {
let bundle = CachedITarBundle::new(url, only_cached, custom_cache_root, status)?;
let mut cache = if let Some(root) = custom_cache_root {
Cache::get_for_custom_directory(root)
} else {
Cache::get_user_default()?
};
let bundle = cache.open::<IndexedTarBackend>(url, only_cached, status)?;
Ok(Box::new(bundle) as _)
}
@ -190,7 +194,7 @@ impl Default for PersistentConfig {
fn default() -> Self {
PersistentConfig {
default_bundles: vec![BundleInfo {
url: String::from("https://archive.org/services/purl/net/pkgwpub/tectonic-default"),
url: String::from(tectonic_bundles::FALLBACK_BUNDLE_URL),
}],
}
}

View File

@ -12,6 +12,9 @@ use std::{
fs, io,
path::{Path, PathBuf},
};
use tectonic_bundles::{
cache::Cache, dir::DirBundle, itar::IndexedTarBackend, zip::ZipBundle, Bundle,
};
use tectonic_docmodel::{
document::{BuildTargetType, Document},
workspace::{Workspace, WorkspaceCreator},
@ -23,7 +26,6 @@ use crate::{
config, ctry,
driver::{OutputFormat, PassSetting, ProcessingSessionBuilder},
errors::{ErrorKind, Result},
io::{cached_itarbundle::CachedITarBundle, dirbundle::DirBundle, zipbundle::ZipBundle, Bundle},
status::StatusBackend,
test_util, tt_note,
};
@ -109,10 +111,10 @@ impl DocumentExt for Document {
Ok(Box::new(test_util::TestBundle::default()))
} else if let Ok(url) = Url::parse(&self.bundle_loc) {
if url.scheme() != "file" {
let bundle = CachedITarBundle::new(
let mut cache = Cache::get_user_default()?;
let bundle = cache.open::<IndexedTarBackend>(
&self.bundle_loc,
setup_options.only_cached,
None,
status,
)?;
Ok(Box::new(bundle))

View File

@ -27,6 +27,7 @@ use std::{
time::SystemTime,
};
use tectonic_bridge_core::{CoreBridgeLauncher, DriverHooks, SystemRequestError};
use tectonic_bundles::Bundle;
use tectonic_io_base::{
digest::DigestData,
filesystem::{FilesystemIo, FilesystemPrimaryInputIo},
@ -40,7 +41,7 @@ use crate::{
io::{
format_cache::FormatCache,
memory::{MemoryFileCollection, MemoryIo},
Bundle, InputOrigin,
InputOrigin,
},
status::StatusBackend,
tt_error, tt_note, tt_warning,

View File

@ -1,619 +0,0 @@
// Copyright 2017-2020 the Tectonic Project
// Licensed under the MIT License.
use flate2::read::GzDecoder;
use fs2::FileExt;
use std::{
collections::HashMap,
fs::{self, File},
io::{BufRead, BufReader, Error as IoError, ErrorKind as IoErrorKind, Read, Write},
path::{Path, PathBuf},
str::FromStr,
};
use tectonic_errors::{anyhow::bail, atry, Result};
use tectonic_geturl::{DefaultBackend, DefaultRangeReader, GetUrlBackend, RangeReader};
use super::{try_open_file, Bundle, InputHandle, InputOrigin, IoProvider, OpenResult};
use crate::app_dirs;
use crate::digest::{self, Digest, DigestData};
use crate::errors::SyncError;
use crate::status::StatusBackend;
use crate::{tt_note, tt_warning};
const MAX_HTTP_ATTEMPTS: usize = 4;
#[derive(Clone, Copy, Debug)]
struct FileInfo {
offset: u64,
length: u64,
}
#[derive(Clone, Copy, Debug)]
struct LocalCacheItem {
_length: u64,
digest: DigestData,
}
/// Attempts to download a file from the bundle.
fn get_file(
data: &mut DefaultRangeReader,
name: &str,
offset: u64,
length: usize,
status: &mut dyn StatusBackend,
) -> Result<Vec<u8>> {
// In principle it'd be cool to return a handle right to the HTTP
// response, but those can't be seekable, and doing so introduces
// lifetime-related issues. So for now we just slurp the whole thing
// into RAM.
tt_note!(status, "downloading {}", name);
// When fetching a bunch of resource files (i.e., on the first
// invocation), bintray will sometimes drop connections. The error
// manifests itself in a way that has a not-so-nice user experience.
// Our solution: retry the HTTP a few times in case it was a transient
// problem.
let mut buf = Vec::with_capacity(length);
let mut overall_failed = true;
let mut any_failed = false;
for _ in 0..MAX_HTTP_ATTEMPTS {
let mut stream = match data.read_range(offset, length) {
Ok(r) => r,
Err(e) => {
tt_warning!(status, "failure requesting \"{}\" from network", name; e);
any_failed = true;
continue;
}
};
if let Err(e) = stream.read_to_end(&mut buf) {
tt_warning!(status, "failure downloading \"{}\" from network", name; e.into());
any_failed = true;
continue;
}
overall_failed = false;
break;
}
if overall_failed {
bail!(
"failed to retrieve \"{}\" from the network; \
this most probably is not Tectonic's fault \
-- please check your network connection.",
name
);
} else if any_failed {
tt_note!(status, "download succeeded after retry");
}
Ok(buf)
}
fn parse_index_line(line: &str) -> Result<Option<(String, FileInfo)>> {
let mut bits = line.split_whitespace();
if let (Some(name), Some(offset), Some(length)) = (bits.next(), bits.next(), bits.next()) {
Ok(Some((
name.to_owned(),
FileInfo {
offset: offset.parse::<u64>()?,
length: length.parse::<u64>()?,
},
)))
} else {
// TODO: preserve the warning info or something!
Ok(None)
}
}
/// Attempts to find the redirected url, download the index and digest.
fn get_everything(
backend: &mut DefaultBackend,
url: &str,
status: &mut dyn StatusBackend,
) -> Result<(String, String, String)> {
let url = backend.resolve_url(url, status)?;
let index = {
let mut index = String::new();
let index_url = format!("{}.index.gz", &url);
tt_note!(status, "downloading index {}", index_url);
GzDecoder::new(backend.get_url(&index_url, status)?).read_to_string(&mut index)?;
index
};
let digest_text = {
// Find the location of the digest file.
let digest_info = {
let mut digest_info = None;
for line in index.lines() {
if let Some((name, info)) = parse_index_line(line)? {
if name == digest::DIGEST_NAME {
digest_info = Some(info);
break;
}
}
}
atry!(digest_info; ["backend does not provide needed {} file", digest::DIGEST_NAME])
};
let mut range_reader = backend.open_range_reader(&url);
String::from_utf8(get_file(
&mut range_reader,
digest::DIGEST_NAME,
digest_info.offset,
digest_info.length as usize,
status,
)?)
.map_err(|e| e.utf8_error())?
};
Ok((digest_text, index, url))
}
#[derive(Clone, Debug)]
struct CacheContent {
digest_text: String,
redirect_url: String,
index: HashMap<String, FileInfo>,
}
/// Load cached data.
///
/// If any of the files is not found return None.
fn load_cache(
digest_path: &Path,
redirect_base: &Path,
index_base: &Path,
) -> Result<Option<CacheContent>> {
// Convert file-not-found errors into None.
match load_cache_inner(digest_path, redirect_base, index_base) {
Ok(r) => Ok(Some(r)),
Err(e) => {
if let Some(ioe) = e.downcast_ref::<IoError>() {
if ioe.kind() == IoErrorKind::NotFound {
return Ok(None);
}
}
Err(e)
}
}
}
/// See `load_cache`.
fn load_cache_inner(
digest_path: &Path,
redirect_base: &Path,
index_base: &Path,
) -> Result<CacheContent> {
let digest_text = {
let f = File::open(digest_path)?;
let mut digest_text = String::with_capacity(digest::DIGEST_LEN);
f.take(digest::DIGEST_LEN as u64)
.read_to_string(&mut digest_text)?;
digest_text
};
let redirect_path = make_txt_path(redirect_base, &digest_text);
let redirect_url = fs::read_to_string(redirect_path)?;
let index_path = make_txt_path(index_base, &digest_text);
let index = {
let f = File::open(index_path)?;
let mut index = HashMap::new();
for line in BufReader::new(f).lines() {
if let Some((name, info)) = parse_index_line(&line?)? {
index.insert(name, info);
}
}
index
};
Ok(CacheContent {
digest_text,
redirect_url,
index,
})
}
fn make_txt_path(base: &Path, digest_text: &str) -> PathBuf {
base.join(&digest_text).with_extension("txt")
}
/// Bundle provided by an indexed tar file over http with a local cache.
#[derive(Debug)]
pub struct CachedITarBundle {
url: String,
redirect_url: String,
digest_path: PathBuf,
cached_digest: DigestData,
checked_digest: bool,
redirect_base: PathBuf,
manifest_path: PathBuf,
data_base: PathBuf,
contents: HashMap<String, LocalCacheItem>,
only_cached: bool,
tar_data: DefaultRangeReader,
index: HashMap<String, FileInfo>,
}
impl CachedITarBundle {
pub fn new(
url: &str,
only_cached: bool,
custom_cache_root: Option<&Path>,
status: &mut dyn StatusBackend,
) -> Result<CachedITarBundle> {
let mut backend = DefaultBackend::default();
let digest_path = cache_dir("urls", custom_cache_root)?.join(app_dirs::sanitized(url));
let redirect_base = &cache_dir("redirects", custom_cache_root)?;
let index_base = &cache_dir("indexes", custom_cache_root)?;
let manifest_base = &cache_dir("manifests", custom_cache_root)?;
let data_base = &cache_dir("files", custom_cache_root)?;
let mut checked_digest = false;
let CacheContent {digest_text, redirect_url, index} =
// Try loading the cached files.
match load_cache(&digest_path, &redirect_base, &index_base)? {
Some(c) => c,
None => {
// At least one of the cached files does not exists. We fetch everything from
// scratch and save the files.
let (digest_text, index, redirect_url) = get_everything(&mut backend, url, status)?;
let _ = DigestData::from_str(&digest_text)?;
checked_digest = true;
file_create_write(&digest_path, |f| writeln!(f, "{}", digest_text))?;
file_create_write(make_txt_path(&redirect_base, &digest_text), |f| f.write_all(redirect_url.as_bytes()))?;
file_create_write(make_txt_path(&index_base, &digest_text), |f| f.write_all(index.as_bytes()))?;
// Reload the cached files now when they were saved.
atry!(load_cache(&digest_path, &redirect_base, &index_base)?; ["cache files missing even after they were created"])
}
};
let cached_digest = DigestData::from_str(&digest_text)?;
// We can now figure out which manifest to use.
let manifest_path = make_txt_path(manifest_base, &digest_text);
// Read it in, if it exists.
let mut contents = HashMap::new();
match try_open_file(&manifest_path) {
OpenResult::NotAvailable => {}
OpenResult::Err(e) => {
return Err(e);
}
OpenResult::Ok(mfile) => {
// Note that the lock is released when the file is closed,
// which is good since BufReader::new() and BufReader::lines()
// consume their objects.
if let Err(e) = mfile.lock_shared() {
tt_warning!(status, "failed to lock manifest file \"{}\" for reading; this might be fine",
manifest_path.display(); e.into());
}
let f = BufReader::new(mfile);
for res in f.lines() {
let line = res?;
let mut bits = line.rsplitn(3, ' ');
let (original_name, length, digest) =
match (bits.next(), bits.next(), bits.next(), bits.next()) {
(Some(s), Some(t), Some(r), None) => (r, t, s),
_ => continue,
};
let name = original_name.to_owned();
let length = match length.parse::<u64>() {
Ok(l) => l,
Err(_) => continue,
};
let digest = if digest == "-" {
continue;
} else {
match DigestData::from_str(&digest) {
Ok(d) => d,
Err(e) => {
tt_warning!(status, "ignoring bad digest data \"{}\" for \"{}\" in \"{}\"",
&digest, original_name, manifest_path.display() ; e);
continue;
}
}
};
contents.insert(
name,
LocalCacheItem {
_length: length,
digest,
},
);
}
}
}
// All set.
let tar_data = backend.open_range_reader(&redirect_url);
Ok(CachedITarBundle {
url: url.to_owned(),
redirect_url,
digest_path,
cached_digest,
checked_digest,
manifest_path,
data_base: data_base.to_owned(),
redirect_base: redirect_base.to_owned(),
contents,
only_cached,
tar_data,
index,
})
}
fn record_cache_result(&mut self, name: &str, length: u64, digest: DigestData) -> Result<()> {
let digest_text = digest.to_string();
// Due to a quirk about permissions for file locking on Windows, we
// need to add `.read(true)` to be able to lock a file opened in
// append mode.
let mut man = fs::OpenOptions::new()
.append(true)
.create(true)
.read(true)
.open(&self.manifest_path)?;
// Lock will be released when file is closed at the end of this function.
atry!(man.lock_exclusive(); ["failed to lock manifest file \"{}\" for writing", self.manifest_path.display()]);
if !name.contains(|c| c == '\n' || c == '\r') {
writeln!(man, "{} {} {}", name, length, digest_text)?;
}
self.contents.insert(
name.to_owned(),
LocalCacheItem {
_length: length,
digest,
},
);
Ok(())
}
/// If we're going to make a request of the backend, we should check that
/// its digest is what we expect. If not, we do a lame thing where we
/// error out but set things up so that things should succeed if the
/// program is re-run. Exactly the lame TeX user experience that I've been
/// trying to avoid!
fn check_digest(&mut self, status: &mut dyn StatusBackend) -> Result<()> {
if self.checked_digest {
return Ok(());
}
// Do a quick and dirty check first and ignore errors.
if let Some(info) = self.index.get(digest::DIGEST_NAME) {
if let Ok(d) = get_file(
&mut self.tar_data,
digest::DIGEST_NAME,
info.offset,
info.length as usize,
status,
) {
if let Ok(d) = String::from_utf8(d) {
if let Ok(d) = DigestData::from_str(&d) {
if self.cached_digest == d {
// We managed to pull some data that match the digest.
// We can be quite confident that the bundle is what we expect it to be.
self.checked_digest = true;
return Ok(());
}
}
}
}
}
// The quick check failed. Try to pull all data to make sure that it wasn't a network
// error or that the redirect url hasn't been updated.
let mut backend = DefaultBackend::default();
let (digest_text, _index, redirect_url) = get_everything(&mut backend, &self.url, status)?;
let current_digest =
atry!(DigestData::from_str(&digest_text); ["bad SHA256 digest from bundle"]);
if self.cached_digest != current_digest {
// Crap! The backend isn't what we thought it was. Rewrite the
// digest file so that next time we'll start afresh.
file_create_write(&self.digest_path, |f| {
writeln!(f, "{}", current_digest.to_string())
})?;
bail!("backend digest changed; rerun tectonic to use updated information");
}
if self.redirect_url != redirect_url {
// The redirect url has changed, let's update it.
let redirect_path = make_txt_path(&self.redirect_base, &digest_text);
file_create_write(&redirect_path, |f| f.write_all(redirect_url.as_bytes()))?;
self.redirect_url = redirect_url;
}
// Index should've changed as the digest hasn't.
// Phew, the backend hasn't changed. Don't check again.
self.checked_digest = true;
Ok(())
}
/// Find the path in the local cache for the provided file. Download the file first if it is
/// not in the local cache already.
fn path_for_name(&mut self, name: &str, status: &mut dyn StatusBackend) -> OpenResult<PathBuf> {
if let Some(info) = self.contents.get(name) {
return match info.digest.create_two_part_path(&self.data_base) {
Ok(p) => OpenResult::Ok(p),
Err(e) => OpenResult::Err(e),
};
}
// The file is not in the cache and we are asked not to try to fetch it.
if self.only_cached {
return OpenResult::NotAvailable;
}
let info = match self.index.get(name).cloned() {
Some(info) => info,
None => return OpenResult::NotAvailable,
};
// Bummer, we haven't seen this file before. We need to (try to) fetch
// the item from the backend, saving it to disk and calculating its
// digest ourselves, then enter it in the cache and in our manifest.
// Fun times. Because we're touching the backend, we need to verify that
// its digest is what we think.
if let Err(e) = self.check_digest(status) {
return OpenResult::Err(e);
}
// The bundle's overall digest is OK. Now try open the file. If it's
// not available, cache that result, since LaTeX compilations commonly
// touch nonexistent files. If we didn't maintain the negative cache,
// we'd have to touch the network for virtually every compilation.
let content = match get_file(
&mut self.tar_data,
name,
info.offset,
info.length as usize,
status,
) {
Ok(c) => c,
Err(e) => return OpenResult::Err(e),
};
// OK, we can stream the file to a temporary location on disk,
// computing its SHA256 as we go.
let length = content.len();
let mut digest_builder = digest::create();
digest_builder.update(&content);
let digest = DigestData::from(digest_builder);
let final_path = match digest.create_two_part_path(&self.data_base) {
Ok(p) => p,
Err(e) => return OpenResult::Err(e),
};
// Perform a racy check for the destination existing, because this
// matters on Windows: if the destination is already there, we'll get
// an error because the destination is marked read-only. Assuming
// non-pathological filesystem manipulation, though, we'll only be
// subject to the race once.
if !final_path.exists() {
if let Err(e) = file_create_write(&final_path, |f| f.write_all(&content)) {
return OpenResult::Err(e);
}
// Now we can make the file readonly. It would be nice to set the
// permissions using the already-open file handle owned by the
// tempfile, but mkstemp doesn't give us access.
let mut perms = match fs::metadata(&final_path) {
Ok(p) => p,
Err(e) => {
return OpenResult::Err(e.into());
}
}
.permissions();
perms.set_readonly(true);
if let Err(e) = fs::set_permissions(&final_path, perms) {
return OpenResult::Err(e.into());
}
}
// And finally add a record of this file to our manifest. Note that
// we're opening and closing this file every time we load a new file;
// not so efficient, but whatever.
if let Err(e) = self.record_cache_result(name, length as u64, digest) {
return OpenResult::Err(e);
}
OpenResult::Ok(final_path)
}
}
impl IoProvider for CachedITarBundle {
fn input_open_name(
&mut self,
name: &str,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
let path = match self.path_for_name(name, status) {
OpenResult::Ok(p) => p,
OpenResult::NotAvailable => return OpenResult::NotAvailable,
OpenResult::Err(e) => return OpenResult::Err(e),
};
let f = match File::open(&path) {
Ok(f) => f,
Err(e) => return OpenResult::Err(e.into()),
};
OpenResult::Ok(InputHandle::new_read_only(
name,
BufReader::new(f),
InputOrigin::Other,
))
}
}
impl Bundle for CachedITarBundle {
fn get_digest(&mut self, _status: &mut dyn StatusBackend) -> Result<DigestData> {
Ok(self.cached_digest)
}
}
/// A convenience method to provide a better error message when writing to a created file.
fn file_create_write<P, F, E>(path: P, write_fn: F) -> Result<()>
where
P: AsRef<Path>,
F: FnOnce(&mut File) -> std::result::Result<(), E>,
E: std::error::Error + 'static + Sync + Send,
{
let path = path.as_ref();
let mut f = atry!(File::create(path); ["couldn't open {} for writing",
path.display()]);
atry!(write_fn(&mut f); ["couldn't write to {}", path.display()]);
Ok(())
}
fn cache_dir(path: &str, custom_cache_root: Option<&Path>) -> Result<PathBuf> {
if let Some(root) = custom_cache_root {
if !root.is_dir() {
bail!("Custom cache path {} is not a directory", root.display());
}
let full_path = root.join(path);
atry!(fs::create_dir_all(&full_path); ["failed to create directory {}", full_path.display()]);
Ok(full_path)
} else {
Ok(app_dirs::user_cache_dir(path).map_err(SyncError::new)?)
}
}

View File

@ -1,40 +0,0 @@
use std::{fs::File, io::BufReader, path::PathBuf};
use super::{Bundle, InputHandle, InputOrigin, IoProvider, OpenResult};
use crate::status::StatusBackend;
pub struct DirBundle {
dir: PathBuf,
}
impl DirBundle {
pub fn new(dir: PathBuf) -> DirBundle {
DirBundle { dir }
}
}
impl IoProvider for DirBundle {
fn input_open_name(
&mut self,
name: &str,
_status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
let mut path = self.dir.clone();
path.push(name);
if path.is_file() {
match File::open(path) {
Err(e) => OpenResult::Err(e.into()),
Ok(f) => OpenResult::Ok(InputHandle::new(
name,
BufReader::new(f),
InputOrigin::Filesystem,
)),
}
} else {
OpenResult::NotAvailable
}
}
}
impl Bundle for DirBundle {}

View File

@ -3,15 +3,10 @@
//! Extensions to Tectonics pluggable I/O backend.
use std::{io::Read, str::FromStr};
use tectonic_errors::{anyhow::bail, atry, Result};
use tectonic_status_base::StatusBackend;
pub mod cached_itarbundle;
pub mod dirbundle;
pub mod format_cache;
pub mod memory;
pub mod zipbundle;
// Convenience re-exports.
@ -28,53 +23,6 @@ pub use tectonic_io_base::{
pub use self::memory::MemoryIo;
/// A special IoProvider that can make TeX format files.
///
/// A “bundle” is expected to contain a large number of TeX support files —
/// for instance, a compilation of a TeXLive distribution. In terms of the
/// software architecture, though, what is special about a bundle is that one
/// can generate one or more TeX format files from its contents without
/// reference to any other I/O resources.
pub trait Bundle: IoProvider {
/// Get a cryptographic digest summarizing this bundles contents.
///
/// The digest summarizes the exact contents of every file in the bundle.
/// It is computed from the sorted names and SHA256 digests of the
/// component files [as implemented in the script
/// builder/make-zipfile.py](https://github.com/tectonic-typesetting/tectonic-staging/blob/master/builder/make-zipfile.py#L138)
/// in the `tectonic-staging` module.
///
/// The default implementation gets the digest from a file name
/// `SHA256SUM`, which is expected to contain the digest in hex-encoded
/// format.
fn get_digest(&mut self, status: &mut dyn StatusBackend) -> Result<DigestData> {
let digest_text = match self.input_open_name(digest::DIGEST_NAME, status) {
OpenResult::Ok(h) => {
let mut text = String::new();
h.take(64).read_to_string(&mut text)?;
text
}
OpenResult::NotAvailable => {
// Broken or un-cacheable backend.
bail!("bundle does not provide needed SHA256SUM file");
}
OpenResult::Err(e) => {
return Err(e);
}
};
Ok(atry!(DigestData::from_str(&digest_text); ["corrupted SHA256 digest data"]))
}
}
impl<B: Bundle + ?Sized> Bundle for Box<B> {
fn get_digest(&mut self, status: &mut dyn StatusBackend) -> Result<DigestData> {
(**self).get_digest(status)
}
}
// Helper for testing. FIXME: I want this to be conditionally compiled with
// #[cfg(test)] but things break if I do that.

View File

@ -1,69 +0,0 @@
// src/io/zipbundle.rs -- I/O on files in a Zipped-up "bundle"
// Copyright 2016-2020 the Tectonic Project
// Licensed under the MIT License.
use std::{
fs::File,
io::{Cursor, Read, Seek},
path::Path,
};
use zip::{result::ZipError, ZipArchive};
use super::{Bundle, InputHandle, InputOrigin, IoProvider, OpenResult};
use crate::errors::Result;
use crate::status::StatusBackend;
pub struct ZipBundle<R: Read + Seek> {
zip: ZipArchive<R>,
}
impl<R: Read + Seek> ZipBundle<R> {
pub fn new(reader: R) -> Result<ZipBundle<R>> {
Ok(ZipBundle {
zip: ZipArchive::new(reader)?,
})
}
}
impl ZipBundle<File> {
pub fn open<P: AsRef<Path>>(path: P) -> Result<ZipBundle<File>> {
Self::new(File::open(path)?)
}
}
impl<R: Read + Seek> IoProvider for ZipBundle<R> {
fn input_open_name(
&mut self,
name: &str,
_status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle> {
// We need to be able to look at other items in the Zip file while
// reading this one, so the only path forward is to read the entire
// contents into a buffer right now. RAM is cheap these days.
let mut zipitem = match self.zip.by_name(name) {
Ok(f) => f,
Err(e) => {
return match e {
ZipError::Io(sube) => OpenResult::Err(sube.into()),
ZipError::FileNotFound => OpenResult::NotAvailable,
_ => OpenResult::Err(e.into()),
};
}
};
let mut buf = Vec::with_capacity(zipitem.size() as usize);
if let Err(e) = zipitem.read_to_end(&mut buf) {
return OpenResult::Err(e.into());
}
OpenResult::Ok(InputHandle::new_read_only(
name,
Cursor::new(buf),
InputOrigin::Other,
))
}
}
impl<R: Read + Seek> Bundle for ZipBundle<R> {}

View File

@ -35,12 +35,13 @@
//! That call simultaneously tells this module where to find the test assets,
//! and also activates the test mode.
use std::{collections::HashSet, env, ffi::OsStr, path::PathBuf};
use std::{collections::HashSet, env, ffi::OsStr, fs, path::PathBuf};
use tectonic_bundles::Bundle;
use tectonic_errors::Result;
use crate::{
digest::DigestData,
io::{Bundle, FilesystemIo, InputHandle, IoProvider, OpenResult},
io::{FilesystemIo, InputHandle, IoProvider, OpenResult},
status::StatusBackend,
};
@ -129,4 +130,22 @@ impl Bundle for TestBundle {
fn get_digest(&mut self, _status: &mut dyn StatusBackend) -> Result<DigestData> {
Ok(DigestData::zeros())
}
fn all_files(&mut self, _status: &mut dyn StatusBackend) -> Result<Vec<String>> {
// XXX: this is copy/paste of DirBundle.
let mut files = Vec::new();
for entry in fs::read_dir(&self.0.root())? {
let entry = entry?;
// This catches both regular files and symlinks:`
if !entry.file_type()?.is_dir() {
if let Some(s) = entry.file_name().to_str() {
files.push(s.to_owned());
}
}
}
Ok(files)
}
}