Skip to main content

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}