-
Notifications
You must be signed in to change notification settings - Fork 165
/
Copy pathlib.rs
303 lines (264 loc) · 10.2 KB
/
lib.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
// Copyright 2016-2021 the Tectonic Project
// Licensed under the MIT License.
#![deny(missing_docs)]
//! Implementations of Tectonic bundle formats.
//!
//! A Tectonic "bundle" is a collection of TeX support files. In code, bundles
//! implement the [`Bundle`] trait defined here, although most of the action in
//! a bundle will be in its implementation of [`tectonic_io_base::IoProvider`].
//!
//! This crate provides the following bundle implementations:
//!
//! - [`cache::BundleCache`] provides filesystem-backed caching for any bundle
//! - [`itar::ItarBundle`] provides filesystem-backed caching for any bundle
//! - [`dir::DirBundle`] turns a directory full of files into a bundle; it is
//! useful for testing and lightweight usage.
//! - [`zip::ZipBundle`] for a ZIP-format bundle.
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;
// How many times network bundles should retry
// a download, and how long they should wait
// between attempts.
const NET_RETRY_ATTEMPTS: usize = 3;
const NET_RETRY_SLEEP_MS: u64 = 500;
/// Uniquely identifies a file in a bundle.
pub trait FileInfo: Clone + Debug {
/// Return a path to this file, relative to the bundle.
fn path(&self) -> &str;
/// Return the name of this file
fn name(&self) -> &str;
}
/// Keeps track of
pub trait FileIndex<'this>
where
Self: Sized + 'this + Debug,
{
/// The FileInfo this index handles
type InfoType: FileInfo;
/// Iterate over all [`FileInfo`]s in this index
fn iter(&'this self) -> Box<dyn Iterator<Item = &'this Self::InfoType> + 'this>;
/// Get the number of [`FileInfo`]s in this index
fn len(&self) -> usize;
/// Returns true if this index is empty
fn is_empty(&self) -> bool {
self.len() == 0
}
/// Has this index been filled with bundle data?
/// This is always false until we call [`self.initialize()`],
/// and is always true afterwards.
fn is_initialized(&self) -> bool {
!self.is_empty()
}
/// Fill this index from a file
fn initialize(&mut self, reader: &mut dyn Read) -> Result<()>;
/// Search for a file in this index, obeying search order.
///
/// Returns a `Some(FileInfo)` if a file was found, and `None` otherwise.
fn search(&'this mut self, name: &str) -> Option<Self::InfoType>;
}
/// A trait for bundles of Tectonic support files.
///
/// A "bundle" is an [`IoProvider`] with a few special properties. Bundles are
/// read-only, and their contents can be enumerated In principle a bundle is
/// completely defined by its file contents, which can be summarized by a
/// cryptographic digest, obtainable using the [`Self::get_digest`] method: two
/// bundles with the same digest should contain exactly the same set of files,
/// and if any aspect of a bundle’s file contents change, so should its digest.
/// Finally, it is generally expected that a bundle will contain a large number
/// of TeX support files, and that you can generate one or more TeX format files
/// using only the files contained in a bundle.
pub trait Bundle: IoProvider {
/// Get a cryptographic digest summarizing this bundle’s contents,
/// which summarizes the exact contents of every file in the bundle.
fn get_digest(&mut self) -> Result<DigestData>;
/// Iterate over all file paths in this bundle.
/// This is used for the `bundle search` command
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()
}
}
/// A bundle that may be cached.
///
/// These methods do not implement any new features.
/// Instead, they give the [`cache::BundleCache`] wrapper
/// more direct access to existing bundle functionality.
pub trait CachableBundle<'this, T>
where
Self: Bundle + 'this,
T: FileIndex<'this>,
{
/// Initialize this bundle's file index from an external reader
/// This allows us to retrieve the FileIndex from the cache WITHOUT
/// touching the network.
fn initialize_index(&mut self, _source: &mut dyn Read) -> Result<()> {
Ok(())
}
/// Get a `Read` instance to this bundle's index,
/// reading directly from the backend.
fn get_index_reader(&mut self) -> Result<Box<dyn Read>>;
/// Return a reference to this bundle's FileIndex.
fn index(&mut self) -> &mut T;
/// Open the file that `info` points to.
fn open_fileinfo(
&mut self,
info: &T::InfoType,
status: &mut dyn StatusBackend,
) -> OpenResult<InputHandle>;
/// Search for a file in this bundle.
/// This should foward the call to `self.index`
fn search(&mut self, name: &str) -> Option<T::InfoType>;
/// Return a string that corresponds to this bundle's location, probably a URL.
/// We should NOT need to do any network IO to get this value.
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)
}
}
/// Try to open a bundle from a string,
/// detecting its type.
///
/// Returns None if auto-detection fails.
pub fn detect_bundle(
source: String,
only_cached: bool,
custom_cache_dir: Option<PathBuf>,
) -> Result<Option<Box<dyn Bundle>>> {
use url::Url;
// Parse URL and detect bundle type
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 {
// If we couldn't parse the URL, this is probably a local path.
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)
}
}
}
/// Get the URL of the default bundle.
///
/// This is a mostly-hardcoded URL of a default bundle that will provide some
/// "sensible" set of TeX support files. The higher-level `tectonic` crate
/// provides a configuration mechanism to allow the user to override this
/// setting, so you should use that if you are in a position to do so.
///
/// The URL depends on the format version supported by the engine, since that
/// roughly corresponds to a TeXLive version, and the engine and TeXLive files
/// are fairly closely coupled.
///
/// The URL template used in this function will be embedded in the binaries that
/// you create, which may be used for years into the future, so it needs to be
/// durable and reliable. We used `archive.org` for a while, but it had
/// low-level reliability problems and was blocked in China. We now use a custom
/// webservice.
pub fn get_fallback_bundle_url(format_version: u32) -> String {
// Build time environment variables declared in `bundles/build.rs`:
let bundle_locked = option_env!("TECTONIC_BUNDLE_LOCKED").unwrap_or("");
let bundle_prefix = option_env!("TECTONIC_BUNDLE_PREFIX")
.filter(|x| !x.is_empty())
.expect("TECTONIC_BUNDLE_PREFIX must be defined at compile time");
// Simply return the locked url when it is specified:
if !bundle_locked.is_empty() {
return bundle_locked.to_owned();
}
// Format version 32 (TeXLive 2021) was when we introduced versioning to the
// URL.
if format_version < 32 {
format!("{bundle_prefix}/default_bundle.tar")
} else {
format!("{bundle_prefix}/default_bundle_v{format_version}.tar")
}
}
/// Open the fallback bundle.
///
/// This is essentially the default Tectonic bundle, but the higher-level
/// `tectonic` crate provides a configuration mechanism to allow the user to
/// override the bundle URL setting, and that should be preferred if you’re in a
/// position to use it.
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())
}