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