Skip to main content

tectonic_bundles/
ttb_net.rs

1// Copyright 2023-2024 the Tectonic Project
2// Licensed under the MIT License.
3
4//! Read ttb v1 bundles on the internet.
5//!
6//! The main type offered by this module is the [`TTBNetBundle`] struct,
7//! which can (but should not) be used directly as a [`tectonic_io_base::IoProvider`].
8//!
9//! Instead, wrap it in a [`crate::BundleCache`] for filesystem-backed caching.
10
11use crate::{
12    ttb::{TTBFileIndex, TTBFileInfo, TTBv1Header},
13    Bundle, CachableBundle, FileIndex, FileInfo, NET_RETRY_ATTEMPTS, NET_RETRY_SLEEP_MS,
14};
15use flate2::read::GzDecoder;
16use std::{
17    convert::TryFrom,
18    io::{Cursor, Read},
19    thread,
20    time::Duration,
21};
22use tectonic_errors::prelude::*;
23use tectonic_geturl::{DefaultBackend, DefaultRangeReader, GetUrlBackend, RangeReader};
24use tectonic_io_base::{InputHandle, InputOrigin, IoProvider, OpenResult};
25use tectonic_status_base::{tt_note, tt_warning, StatusBackend};
26
27/// Read a [`TTBFileInfo`] from this bundle.
28/// We assume that `fileinfo` points to a valid file in this bundle.
29fn read_fileinfo(fileinfo: &TTBFileInfo, reader: &mut DefaultRangeReader) -> Result<Box<dyn Read>> {
30    // fileinfo.length is a u32, so it must fit inside a usize (assuming 32/64-bit machine).
31    let stream = reader.read_range(fileinfo.start, fileinfo.gzip_len as usize)?;
32    Ok(Box::new(GzDecoder::new(stream)))
33}
34
35/// Access ttbv1 bundle hosted on the internet.
36/// This struct provides NO caching. All files
37/// are downloaded.
38///
39/// As such, this bundle should probably be wrapped in a [`crate::BundleCache`].
40pub struct TTBNetBundle<T>
41where
42    for<'a> T: FileIndex<'a>,
43{
44    url: String,
45    index: T,
46
47    // We need the network to load these.
48    // They're None until absolutely necessary.
49    reader: Option<DefaultRangeReader>,
50}
51
52/// The internal file-information struct used by the [`TTBNetBundle`].
53impl TTBNetBundle<TTBFileIndex> {
54    /// Create a new ZIP bundle for a generic readable and seekable stream.
55    /// This method does not require network access.
56    /// It will succeed even in we can't connect to the bundle, or if we're given a bad url.
57    pub fn new(url: String) -> Result<Self> {
58        Ok(TTBNetBundle {
59            reader: None,
60            index: TTBFileIndex::default(),
61            url,
62        })
63    }
64
65    fn connect_reader(&mut self) -> Result<()> {
66        if self.reader.is_some() {
67            return Ok(());
68        }
69        let geturl_backend = DefaultBackend::default();
70        self.reader = Some(geturl_backend.open_range_reader(&self.url));
71        Ok(())
72    }
73
74    fn get_header(&mut self) -> Result<TTBv1Header> {
75        self.connect_reader()?;
76        let mut header: [u8; 70] = [0u8; 70];
77        self.reader
78            .as_mut()
79            .unwrap()
80            .read_range(0, 70)?
81            .read_exact(&mut header)?;
82        let header = TTBv1Header::try_from(header)?;
83        Ok(header)
84    }
85
86    // Fill this bundle's index if it is empty.
87    fn ensure_index(&mut self) -> Result<()> {
88        if self.index.is_initialized() {
89            return Ok(());
90        }
91
92        let mut reader = self.get_index_reader()?;
93        self.index.initialize(&mut reader)?;
94        Ok(())
95    }
96}
97
98impl IoProvider for TTBNetBundle<TTBFileIndex> {
99    fn input_open_name(
100        &mut self,
101        name: &str,
102        status: &mut dyn StatusBackend,
103    ) -> OpenResult<InputHandle> {
104        if let Err(e) = self.ensure_index() {
105            return OpenResult::Err(e);
106        };
107
108        let info = match self.search(name) {
109            None => return OpenResult::NotAvailable,
110            Some(s) => s,
111        };
112
113        // Retries are handled in open_fileinfo,
114        // since BundleCache never calls input_open_name.
115        self.open_fileinfo(&info, status)
116    }
117}
118
119impl Bundle for TTBNetBundle<TTBFileIndex> {
120    fn all_files(&self) -> Vec<String> {
121        self.index.iter().map(|x| x.path().to_owned()).collect()
122    }
123
124    fn get_digest(&mut self) -> Result<tectonic_io_base::digest::DigestData> {
125        let header = self.get_header()?;
126        Ok(header.digest)
127    }
128}
129
130impl CachableBundle<'_, TTBFileIndex> for TTBNetBundle<TTBFileIndex> {
131    fn get_location(&mut self) -> String {
132        self.url.clone()
133    }
134
135    fn initialize_index(&mut self, source: &mut dyn Read) -> Result<()> {
136        self.index.initialize(source)?;
137        Ok(())
138    }
139
140    fn index(&mut self) -> &mut TTBFileIndex {
141        &mut self.index
142    }
143
144    fn search(&mut self, name: &str) -> Option<TTBFileInfo> {
145        self.index.search(name)
146    }
147
148    fn get_index_reader(&mut self) -> Result<Box<dyn Read>> {
149        self.connect_reader()?;
150        let header = self.get_header()?;
151
152        read_fileinfo(
153            &TTBFileInfo {
154                start: header.index_start,
155                gzip_len: header.index_gzip_len,
156                real_len: header.index_real_len,
157                path: "".to_owned(),
158                name: "".to_owned(),
159                hash: None,
160            },
161            self.reader.as_mut().unwrap(),
162        )
163    }
164
165    fn open_fileinfo(
166        &mut self,
167        info: &TTBFileInfo,
168        status: &mut dyn StatusBackend,
169    ) -> OpenResult<InputHandle> {
170        let mut v: Vec<u8> = Vec::with_capacity(info.real_len as usize);
171        tt_note!(status, "downloading {}", info.name);
172
173        // Edge case for zero-sized reads
174        // (these cause errors on some web hosts)
175        if info.gzip_len == 0 {
176            return OpenResult::Ok(InputHandle::new_read_only(
177                info.name.to_owned(),
178                Cursor::new(v),
179                InputOrigin::Other,
180            ));
181        }
182
183        // Get file with retries
184        for i in 0..NET_RETRY_ATTEMPTS {
185            let mut reader = match read_fileinfo(info, self.reader.as_mut().unwrap()) {
186                Ok(r) => r,
187                Err(e) => {
188                    tt_warning!(status,
189                        "failure fetching \"{}\" from network ({}/{NET_RETRY_ATTEMPTS})",
190                        info.name, i+1; e
191                    );
192                    thread::sleep(Duration::from_millis(NET_RETRY_SLEEP_MS));
193                    continue;
194                }
195            };
196
197            match reader.read_to_end(&mut v) {
198                Ok(_) => {}
199                Err(e) => {
200                    tt_warning!(status,
201                        "failure downloading \"{}\" from network ({}/{NET_RETRY_ATTEMPTS})",
202                        info.name, i+1; e.into()
203                    );
204                    thread::sleep(Duration::from_millis(NET_RETRY_SLEEP_MS));
205                    continue;
206                }
207            };
208
209            return OpenResult::Ok(InputHandle::new_read_only(
210                info.name.to_owned(),
211                Cursor::new(v),
212                InputOrigin::Other,
213            ));
214        }
215
216        OpenResult::Err(anyhow!(
217            "failed to download \"{}\"; please check your network connection.",
218            info.name
219        ))
220    }
221}