tectonic_bundles 0.3.0

Tectonic "bundle" (support file collection) implementations.
Documentation
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
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
// Copyright 2017-2021 the Tectonic Project
// Licensed under the MIT License.

//! Local caching of bundle data.
//!
//! This module implements Tectonic’s local filesystem caching mechanism for TeX
//! support files. To enable efficient caching with proper invalidation
//! semantics, the caching layer does *not* merely wrap [`IoProvider`]
//! implementations. Instead, a cacheable bundle must implement the
//! [`CacheBackend`] trait defined in this module. An example of such a bundle
//! is the [`crate::itar::IndexedTarBackend`] for bundles served over HTTP.
//!
//! In order to access a cacheable bundle, you need a handle to a local
//! [`Cache`], probably obtained with [`Cache::get_user_default()`], and a URL,
//! which you’ll pass to [`Cache::open()`]. When using this function, you must
//! explicitly specify the concrete [`CacheBackend`] type that will service
//! backend requests.

use fs2::FileExt;
use std::{
    collections::HashMap,
    env,
    fs::{self, File},
    io::{BufRead, BufReader, Error as IoError, ErrorKind as IoErrorKind, Read, Write},
    path::{Path, PathBuf},
    str::FromStr,
};
use tectonic_errors::prelude::*;
use tectonic_io_base::{
    app_dirs,
    digest::{self, Digest, DigestData},
    try_open_file, InputHandle, InputOrigin, IoProvider, OpenResult,
};
use tectonic_status_base::{tt_warning, StatusBackend};

use crate::Bundle;

/// A cache of data from one or more bundles using the local filesystem.
#[derive(Debug)]
pub struct Cache {
    root: PathBuf,
}

impl Cache {
    /// Get a handle to a bundle cache, using default per-user settings.
    ///
    /// The cache location defaults to the `AppDataType::UserCache`
    /// provided by `app_dirs2` but can be overwritten using the
    /// `TECTONIC_CACHE_DIR` environment variable.
    ///
    /// This method may perform I/O to create the user cache directory, so it is
    /// fallible. (Due to its `app_dirs2` implementation, it would have to be
    /// fallible even if it didn't perform I/O.)
    pub fn get_user_default() -> Result<Self> {
        let env_cache_path = env::var_os("TECTONIC_CACHE_DIR");

        let cache_path = match env_cache_path {
            Some(env_cache_path) => {
                let env_cache_path = env_cache_path.into();
                fs::create_dir_all(&env_cache_path)?;
                env_cache_path
            }
            None => app_dirs::ensure_user_cache_dir("")?,
        };

        Ok(Cache { root: cache_path })
    }

    /// Get a handle to a bundle cache, using a custom cache directory.
    pub fn get_for_custom_directory<P: Into<PathBuf>>(root: P) -> Self {
        Cache { root: root.into() }
    }

    /// Get the root directory of this cache.
    pub fn root(&self) -> &Path {
        &self.root
    }

    /// Open a bundle through the cache layer.
    ///
    /// The URL specifies where the backend data live; it must be understood by,
    /// and contain data appropriate for, the [`CacheBackend`] type associated
    /// with the bundle that you’re creating. If *only_cached* is true, this
    /// instance will never actually connect to the backend; if any uncached
    /// files are requested, they will be represented as “not found”.
    pub fn open<CB: CacheBackend>(
        &mut self,
        url: &str,
        only_cached: bool,
        status: &mut dyn StatusBackend,
    ) -> Result<CachingBundle<CB>> {
        CachingBundle::new(url, only_cached, status, &self.root)
    }
}

/// Information describing a cache backend.
///
/// This type is returned by a [`CacheBackend`] on a "pull", a first-time
/// connection to the backend. It contains the detailed information that needs
/// to be saved in the cache to provide for efficient operation in subsequent
/// uses.
#[derive(Clone, Debug)]
pub struct BackendPullData {
    /// The final, "resolved" URL pointing to the backing content, in the case
    /// that the starting URL redirects.
    pub resolved_url: String,

    /// The digest of the overall bundle content.
    pub digest: DigestData,

    /// The bundle indexing data, allowing efficient retrieval of files from the
    /// backend.
    ///
    /// This is a multi-line string, where each line is an entry for a file.
    /// These lines will be parsed by [`CacheBackend::parse_index_line`]. This
    /// string will potentially contain several megabytes of data.
    pub index: String,
}

/// A source of files that can supply a cache-based bundle.
///
/// This trait is combined with [`CachingBundle`] to implement a caching bundle
/// interface.
pub trait CacheBackend: Sized {
    /// Information about a file stored in the backend.
    ///
    /// This information should be serializable to a single line of text. It is
    /// parsed out of the contents of [`BackendPullData::index`] by
    /// [`Self::parse_index_line`], and later passed to [`Self::get_file`] to
    /// enable the backend to efficiently retrieve the file in question. For
    /// instance, it might contain offset information informing the backend how
    /// to efficiently retrieve the file in question.
    type FileInfo: Clone;

    /// Connect to the backend and download its key information.
    ///
    /// This method is used the first time that the cache connects to a backend.
    /// The return value includes a package of information ([`BackendPullData`])
    /// that the cache will store to enable efficient operation on subsequent
    /// requests.
    fn open_with_pull(
        start_url: &str,
        status: &mut dyn StatusBackend,
    ) -> Result<(Self, BackendPullData)>;

    /// Connect to the backend and fetch validation information.
    ///
    /// This method is used when this backend has already been accessed by the
    /// cache during a previous execution. If we need to download more data from
    /// the backend, we first need to verify that the cached data still look
    /// valid. This method asks the backend to pull its “digest file” (currently
    /// named `SHA256SUM`) and return its contents for validate. The method
    /// should return `Err` on actual errors, and `Ok(None)` if there are any
    /// indications that the cached indexing data should be thrown out and
    /// re-fetched.
    fn open_with_quick_check(
        resolved_url: &str,
        digest_file_info: &Self::FileInfo,
        status: &mut dyn StatusBackend,
    ) -> Result<Option<(Self, DigestData)>>;

    /// Parse a line of the indexing data.
    ///
    /// The returned tuple should give the file name and an opaque
    /// [`Self::FileInfo`] that may help the backend retrieve the file in the
    /// future. The indexing data are originally obtained from
    /// [`BackendPullData::index`], but are stored in a file locally. This
    /// method should return an error if this particular line of index data
    /// seems to be malformatted. Such lines will probably just be silently
    /// ignored.
    fn parse_index_line(line: &str) -> Result<(String, Self::FileInfo)>;

    /// Obtain a file from the backend.
    ///
    /// Backend-specific retrieval information can be passed in the
    /// [`Self::FileInfo`] item, which is constructed from the backend’s index
    /// information. The file should be returned as one large byte vector.
    fn get_file(
        &mut self,
        name: &str,
        info: &Self::FileInfo,
        status: &mut dyn StatusBackend,
    ) -> Result<Vec<u8>>;
}

/// Information about a cached file.
#[derive(Clone, Copy, Debug)]
struct CachedFileInfo {
    /// The length of the file in bytes.
    ///
    /// This field isn't currently used, but seems handy to keep around.
    _length: u64,

    /// The digest of the file contents.
    ///
    /// This digest is used to locate the cached data on disk.
    digest: DigestData,
}

/// A caching bundle that obtains files from some a backend.
///
/// This bundle implementation is the key to Tectonic’s ability to download TeX
/// support files on the fly. The cache backend is generally expected to be some
/// kind of network-based resource, and the caching scheme is designed so that a
/// document build can avoid touching the network altogether if no new files
/// need to be downloaded.
#[derive(Debug)]
pub struct CachingBundle<CB: CacheBackend> {
    /// The URL specifying where to start looking for the bundle data.
    ///
    /// The caching layer maintains two URLs: the "start" URL and the "resolved"
    /// URL. The goal here is to be able to store a single URL for fetching
    /// data, but maintain the capability to update the bundle data behind that
    /// URL. Requests to the start URL may get redirected (one or more times)
    /// until eventually we arrive at the "resolved" URL. While the redirection
    /// of the start URL might change, the contents of a resolved URL should
    /// never change once published.
    start_url: String,

    /// The "resolved" URL for the backing data.
    ///
    /// The bundle data located at this URL should never change.
    resolved_url: String,

    /// The cached value of the backend’s content digest.
    ///
    /// This is stored in a file at [`Self::digest_path`]. This value may be
    /// inaccurate, if the backing bundle has been updated (or if the cache is
    /// corrupt, etc.) and we haven't yet synchronized with the backend and
    /// discovered that fact.
    cached_digest: DigestData,

    /// Information about all of the files that have been cached locally.
    ///
    /// This maps filenames to summary information that can then be used to
    /// retrieve file data from [`Self::data_base`]. The contents are loaded
    /// from the manifest file if the cache is non-empty.
    contents: HashMap<String, CachedFileInfo>,

    /// Information about all of the files known to the backend.
    ///
    /// This maps filenames to [`CacheBackend::FileInfo`] data that can be used
    /// to retrieve a file from the backend if needed.
    index: HashMap<String, CB::FileInfo>,

    /// If true, only use cached files -- never connect to the backend.
    ///
    /// This option can be useful if we are operating disconnected from the
    /// network (e.g., on an airplane). If you add a new figure to your
    /// document, the engine will inquire about several related files that it
    /// thinks might exist. Without this option, such an inquiry might require
    /// Tectonic to hit the network, when the user knows for sure that the
    /// bundle is not going to contain these files.
    only_cached: bool,

    /// The connection to the cache backend, maybe.
    ///
    /// This field will be `None` if there are locally cached data present and
    /// there has not yet been a need to connect to the backend. If it becomes
    /// necessary to "pull" and/or download a new file from the backend, this
    /// value will become `Some` — it represents something like an open network
    /// connection.
    backend: Option<CB>,

    /// The path to a file containing a cached copy of the backend's content
    /// digest.
    ///
    /// This file path is based on [`Self::start_url`].
    digest_path: PathBuf,

    /// A directory where we will save [`Self::resolved_url`].
    ///
    /// We need to cache `resolved_url` to enable the "quick check" backend
    /// reconnection path. The actual cache file path is based on the backend’s
    /// content digest.
    resolved_base: PathBuf,

    /// A directory where we will save the cache manifest.
    ///
    /// The manifest file contains information about the files that have
    /// actually been fetched from the backend and saved locally. The actual
    /// manifest file path is based on the backend’s content digest.
    manifest_path: PathBuf,

    /// A directory where we will save cached file data.
    ///
    /// This directory contains the actual cached file contents, in a directory
    /// structured based on the digest of each file’s content.
    data_base: PathBuf,
}

/// A locally-cached analogue of [`BackendPullData`].
///
/// This data structure is what we try to recover from the cache to see if we
/// can avoid connecting to the backend.
#[derive(Clone, Debug)]
struct CachedPullData<FI> {
    /// The saved backend content digest.
    pub digest: DigestData,

    /// The saved "resolved URL" for the backend.
    pub resolved_url: String,

    /// The saved indexing information for the backend.
    pub index: HashMap<String, FI>,
}

impl<CB: CacheBackend> CachingBundle<CB> {
    fn new(
        start_url: &str,
        only_cached: bool,
        status: &mut dyn StatusBackend,
        cache_root: &Path,
    ) -> Result<Self> {
        // Set up our paths.
        let digest_path =
            ensure_cache_dir(cache_root, "urls")?.join(app_dirs::app_dirs2::sanitized(start_url));
        let resolved_base = ensure_cache_dir(cache_root, "redirects")?;
        let index_base = ensure_cache_dir(cache_root, "indexes")?;
        let manifest_base = ensure_cache_dir(cache_root, "manifests")?;
        let data_base = ensure_cache_dir(cache_root, "files")?;

        // The whole point of this cache is to avoid connecting to the backend
        // if at all possible. So we first see if we have cached the "pull data"
        // that describe the overall backend contents.

        let mut backend = None;

        let cached_pull_data =
            match load_cached_pull_data::<CB>(&digest_path, &resolved_base, &index_base)? {
                Some(c) => c,
                None => {
                    // Some portion of the required cached data is missing. We need to
                    // do a complete pull and then cache the results.

                    let (new_backend, pull_data) = CB::open_with_pull(start_url, status)?;
                    backend = Some(new_backend);

                    let digest_text = pull_data.digest.to_string();
                    file_create_write(&digest_path, |f| writeln!(f, "{}", &digest_text))?;
                    file_create_write(make_txt_path(&resolved_base, &digest_text), |f| {
                        f.write_all(pull_data.resolved_url.as_bytes())
                    })?;
                    file_create_write(make_txt_path(&index_base, &digest_text), |f| {
                        f.write_all(pull_data.index.as_bytes())
                    })?;

                    // Now that we've done that, load_cached_pull_data() really ought to succeed ...
                    atry!(
                        load_cached_pull_data::<CB>(&digest_path, &resolved_base, &index_base)?;
                        ["cache files missing even after they were created"]
                    )
                }
            };

        // We call this `cached_digest`, but if `backend` is Some, it is a
        // validated, fresh digest.

        let cached_digest = cached_pull_data.digest;

        // Now that we have the backend content digest, we know which manifest
        // to use. Read it in, if it exists.

        let manifest_path = make_txt_path(&manifest_base, &cached_digest.to_string());
        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,
                        CachedFileInfo {
                            _length: length,
                            digest,
                        },
                    );
                }
            }
        }

        // All set.

        Ok(CachingBundle {
            start_url: start_url.to_owned(),
            resolved_url: cached_pull_data.resolved_url,
            digest_path,
            cached_digest,
            manifest_path,
            data_base,
            resolved_base,
            contents,
            only_cached,
            backend,
            index: cached_pull_data.index,
        })
    }

    /// Save data about a file to our local cache manifest.
    fn save_to_manifest(&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 a filename contains newline characters, it will mess up our
        // line-based manifest format. Be paranoid and refuse to record such
        // filenames.
        if !name.contains(|c| c == '\n' || c == '\r') {
            writeln!(man, "{} {} {}", name, length, digest_text)?;
        }

        self.contents.insert(
            name.to_owned(),
            CachedFileInfo {
                _length: length,
                digest,
            },
        );

        Ok(())
    }

    /// Ensure that the backend is connected and valid.
    ///
    /// Here we do a "quick check" to see if the backend's 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!
    ///
    /// After this function has been called, you can assume that `self.backend`
    /// is Some.
    fn ensure_backend_validity(&mut self, status: &mut dyn StatusBackend) -> Result<()> {
        // If backend is Some, we already have a validated connection to it.
        if self.backend.is_some() {
            return Ok(());
        }

        // Do the quick check. If anything goes wrong, eat the error and try a
        // fresh pull.
        if let Some(info) = self.index.get(digest::DIGEST_NAME) {
            if let Ok(Some((backend, digest))) =
                CB::open_with_quick_check(&self.resolved_url, info, status)
            {
                if self.cached_digest == digest {
                    // 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.backend = Some(backend);
                    return Ok(());
                }
            }
        }

        // The quick check failed. Try to pull all data to make sure that it
        // wasn't a network error or that the resolved URL hasn't been updated.
        let (new_backend, pull_data) = CB::open_with_pull(&self.start_url, status)?;

        if self.cached_digest != pull_data.digest {
            // Crap! The backend isn't what we thought it was. We may have been
            // giving incorrect results if we pulled files out of the cache
            // before this invocation. Rewrite the digest file so that next time
            // we'll start afresh, then bail.
            file_create_write(&self.digest_path, |f| {
                writeln!(f, "{}", pull_data.digest.to_string())
            })?;
            bail!("backend digest changed; rerun tectonic to use updated information");
        }

        if self.resolved_url != pull_data.resolved_url {
            // The resolved URL has changed, but the digest is the same. So
            // let's just update the URL and keep going.
            let resolved_path = make_txt_path(&self.resolved_base, &pull_data.digest.to_string());
            file_create_write(&resolved_path, |f| {
                f.write_all(pull_data.resolved_url.as_bytes())
            })?;

            self.resolved_url = pull_data.resolved_url;
        }

        // OK, it seems that everything is in order.
        self.backend = Some(new_backend);
        Ok(())
    }

    /// Make sure that a file is available, and return its filesystem path.
    ///
    /// If the file is already cached, just pull it out. Otherwise, fetch it
    /// from the backend.
    fn ensure_file_availability(
        &mut self,
        name: &str,
        status: &mut dyn StatusBackend,
    ) -> OpenResult<PathBuf> {
        // Already in the cache?
        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),
            };
        }

        // No, it's not. Are we in cache-only mode?
        if self.only_cached {
            return OpenResult::NotAvailable;
        }

        // Is the file in the backend at all?
        let info = match self.index.get(name).cloned() {
            Some(info) => info,
            None => return OpenResult::NotAvailable,
        };

        // Yes, it is. Time to fetch it! In order to do that, we need to ensure
        // that we have a valid backend connection.
        if let Err(e) = self.ensure_backend_validity(status) {
            return OpenResult::Err(e);
        }

        // Cool, we're connected to the backend now. Get the file. Note that we
        // don't need to check for updates to the index after the
        // ensure-validity, because we require that the contents of the bundle
        // are unchanged (as expressed in the content digest): if they did
        // change, ensure_backend_validity() would have bailed, because we might
        // have returned incorrect data for previous requests that hit the
        // cache.

        let content = match self.backend.as_mut().unwrap().get_file(name, &info, status) {
            Ok(c) => c,
            Err(e) => return OpenResult::Err(e),
        };

        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 the manifest every time we cache a new
        // file; not so efficient, but whatever.

        if let Err(e) = self.save_to_manifest(name, length as u64, digest) {
            return OpenResult::Err(e);
        }

        OpenResult::Ok(final_path)
    }
}

impl<CB: CacheBackend> IoProvider for CachingBundle<CB> {
    fn input_open_name(
        &mut self,
        name: &str,
        status: &mut dyn StatusBackend,
    ) -> OpenResult<InputHandle> {
        let path = match self.ensure_file_availability(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<CB: CacheBackend> Bundle for CachingBundle<CB> {
    fn get_digest(&mut self, _status: &mut dyn StatusBackend) -> Result<DigestData> {
        Ok(self.cached_digest)
    }

    fn all_files(&mut self, status: &mut dyn StatusBackend) -> Result<Vec<String>> {
        if !self.only_cached {
            self.ensure_backend_validity(status)?;
        }
        Ok(self.index.keys().cloned().collect())
    }
}

/// Load the cached "pull" data for a backend.
///
/// If any of the files are not found or otherwise have issues, return None.
fn load_cached_pull_data<CB: CacheBackend>(
    digest_path: &Path,
    resolved_base: &Path,
    index_base: &Path,
) -> Result<Option<CachedPullData<CB::FileInfo>>> {
    // Convert file-not-found errors into None.
    return match inner::<CB>(digest_path, resolved_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)
        }
    };

    fn inner<CB: CacheBackend>(
        digest_path: &Path,
        resolved_base: &Path,
        index_base: &Path,
    ) -> Result<CachedPullData<CB::FileInfo>> {
        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 resolved_path = make_txt_path(resolved_base, &digest_text);
        let resolved_url = fs::read_to_string(resolved_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 Ok((name, info)) = CB::parse_index_line(&line?) {
                    index.insert(name, info);
                }
            }
            index
        };

        Ok(CachedPullData {
            digest: DigestData::from_str(&digest_text)?,
            resolved_url,
            index,
        })
    }
}

/// 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(())
}

/// Ensure that a directory exists.
fn ensure_cache_dir(root: &Path, path: &str) -> Result<PathBuf> {
    let full_path = root.join(path);
    atry!(
        fs::create_dir_all(&full_path);
        ["failed to create directory `{}` or one of its parents", full_path.display()]
    );
    Ok(full_path)
}

/// Convenience to generate a text filename
fn make_txt_path(base: &Path, name: &str) -> PathBuf {
    base.join(&name).with_extension("txt")
}