tectonic_io_base/lib.rs
1// Copyright 2016-2021 the Tectonic Project
2// Licensed under the MIT License.
3
4#![deny(missing_docs)]
5
6//! Tectonic’s pluggable I/O backend.
7//!
8//! This crates defines the core traits and types used by Tectonic’s I/O
9//! subsystem, and provides implementations for common stdlib types. It provides
10//! a simplified I/O model compatible with TeX’s usage patterns, as encapsulated
11//! in the [`IoProvider`] trait. Files are exposed as [`InputHandle`] or
12//! [`OutputHandle`] structs, which add a layer of bookkeeping to allow the
13//! higher levels of Tectonic to determine when the engine needs to be re-run.
14
15use sha2::Digest;
16use std::{
17 borrow::Cow,
18 fs::File,
19 io::{self, Cursor, Read, Seek, SeekFrom, Write},
20 path::{Path, PathBuf},
21};
22use tectonic_errors::{
23 anyhow::{bail, ensure},
24 Error, Result,
25};
26use tectonic_status_base::StatusBackend;
27use thiserror::Error as ThisError;
28
29use crate::digest::DigestData;
30
31pub mod app_dirs;
32pub mod digest;
33pub mod filesystem;
34pub mod flate2;
35pub mod stack;
36pub mod stdstreams;
37
38/// Errors that are generic to Tectonic's framework, but not capturable as
39/// IoErrors.
40#[derive(ThisError, Debug)]
41pub enum TectonicIoError {
42 /// The stream is not seekable.
43 #[error("cannot seek within this stream")]
44 NotSeekable,
45
46 /// The stream does not have a fixed size.
47 #[error("cannot obtain the size of this stream")]
48 NotSizeable,
49
50 /// Access to this path is forbidden.
51 #[error("access to the path `{}` is forbidden", .0.display())]
52 PathForbidden(PathBuf),
53}
54
55/// An extension to the basic Read trait supporting additional features
56/// needed for Tectonic's I/O system.
57pub trait InputFeatures: Read {
58 /// Get the size of the stream. Return `TectonicIoError::NotSizeable`
59 /// if the operation is not well-defined for this stream.
60 fn get_size(&mut self) -> Result<usize>;
61
62 /// Try to seek within the stream. Return `TectonicIoError::NotSeekable`
63 /// if the operation is not possible for this stream.
64 fn try_seek(&mut self, pos: SeekFrom) -> Result<u64>;
65
66 /// Get the modification time of this file as a Unix time. If that quantity
67 /// is not meaningfully defined for this input, return `Ok(None)`. This is
68 /// what the default implementation does.
69 fn get_unix_mtime(&mut self) -> Result<Option<i64>> {
70 Ok(None)
71 }
72}
73
74/// What kind of source an input file ultimately came from. We keep track of
75/// this in order to be able to emit Makefile-style dependencies for input
76/// files. Right now, we only provide enough options to achieve this goal; we
77/// could add more.
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum InputOrigin {
80 /// This file lives on the filesystem and might change under us. (That is,
81 /// it is not a cached bundle file.)
82 Filesystem,
83
84 /// This file was never used as an input.
85 NotInput,
86
87 /// This file is none of the above.
88 Other,
89}
90
91/// Input handles are basically Read objects with a few extras. We don't
92/// require the standard io::Seek because we need to provide a dummy
93/// implementation for GZip streams, which we wouldn't be allowed to do
94/// because both the trait and the target struct are outside of our crate.
95///
96/// An important role for the InputHandle struct is computing a cryptographic
97/// digest of the input file. The driver uses this information in order to
98/// figure out if the TeX engine needs rerunning. TeX makes our life more
99/// difficult, though, since it has somewhat funky file access patterns. LaTeX
100/// file opens work by opening a file and immediately closing it, which tests
101/// whether the file exists, and then by opening it again for real. Under the
102/// hood, XeTeX reads a couple of bytes from each file upon open to sniff its
103/// encoding. So we can't just stream data from `read()` calls into the SHA2
104/// computer, since we end up seeking and reading redundant data.
105///
106/// The current system maintains some internal state that, so far, helps us Do
107/// The Right Thing given all this. If there's a seek on the file, we give up
108/// on our digest computation. But if there's a seek back to the file
109/// beginning, we are open to the possibility of restarting the computation.
110/// But if nothing is ever read from the file, we once again give up on the
111/// computation. The `ExecutionState` code then has further pieces that track
112/// access to nonexistent files, which we treat as being equivalent to an
113/// existing empty file for these purposes.
114pub struct InputHandle {
115 name: String,
116 inner: Box<dyn InputFeatures>,
117 /// Indicates that the file cannot be written to (provided by a read-only IoProvider) and
118 /// therefore it is useless to compute the digest.
119 read_only: bool,
120 digest: digest::DigestComputer,
121 origin: InputOrigin,
122 ever_read: bool,
123 did_unhandled_seek: bool,
124 ungetc_char: Option<u8>,
125}
126
127impl InputHandle {
128 /// Create a new InputHandle wrapping an underlying type that implements
129 /// `InputFeatures`.
130 pub fn new<T: 'static + InputFeatures>(
131 name: impl Into<String>,
132 inner: T,
133 origin: InputOrigin,
134 ) -> InputHandle {
135 InputHandle {
136 name: name.into(),
137 inner: Box::new(inner),
138 read_only: false,
139 digest: Default::default(),
140 origin,
141 ever_read: false,
142 did_unhandled_seek: false,
143 ungetc_char: None,
144 }
145 }
146
147 /// Create a new InputHandle in read-only mode.
148 pub fn new_read_only<T: 'static + InputFeatures>(
149 name: impl Into<String>,
150 inner: T,
151 origin: InputOrigin,
152 ) -> InputHandle {
153 InputHandle {
154 name: name.into(),
155 inner: Box::new(inner),
156 read_only: true,
157 digest: Default::default(),
158 origin,
159 ever_read: false,
160 did_unhandled_seek: false,
161 ungetc_char: None,
162 }
163 }
164
165 /// Get the name associated with this handle.
166 pub fn name(&self) -> &str {
167 &self.name
168 }
169
170 /// Get the "origin" associated with this handle.
171 pub fn origin(&self) -> InputOrigin {
172 self.origin
173 }
174
175 /// Consumes the object and returns the underlying readable handle that
176 /// it references.
177 pub fn into_inner(self) -> Box<dyn InputFeatures> {
178 self.inner
179 }
180
181 /// Read any remaining data in the file and incorporate them into the
182 /// digest. This helps the rerun detection logic work correctly in
183 /// the somewhat-unusual case that a file is read then written, but
184 /// only part of the file is read, not the entire thing. This seems
185 /// to happen with biblatex XML state files.
186 pub fn scan_remainder(&mut self) -> Result<()> {
187 const BUFSIZE: usize = 1024;
188 let mut buf: [u8; BUFSIZE] = [0; BUFSIZE];
189
190 loop {
191 let n = match self.inner.read(&mut buf[..]) {
192 Ok(n) => n,
193
194 // There are times when the engine tries to open and read
195 // directories. When closing out such a handle, we'll get this
196 // error, but we should ignore it.
197 Err(ref ioe) if ioe.raw_os_error() == Some(libc::EISDIR) => return Ok(()),
198
199 Err(e) => return Err(e.into()),
200 };
201
202 if n == 0 {
203 break;
204 }
205
206 self.digest.update(&buf[..n]);
207 }
208
209 Ok(())
210 }
211
212 /// Consumes the object and returns the SHA256 sum of the content that was
213 /// read. No digest is returned if there was ever a seek on the input
214 /// stream, since in that case the results will not be reliable. We also
215 /// return None if the stream was never read, which is another common
216 /// TeX access pattern: files are opened, immediately closed, and then
217 /// opened again. Finally, no digest is returned if the file is marked read-only.
218 pub fn into_name_digest(self) -> (String, Option<DigestData>) {
219 if self.did_unhandled_seek || !self.ever_read || self.read_only {
220 (self.name, None)
221 } else {
222 (self.name, Some(DigestData::from(self.digest)))
223 }
224 }
225
226 /// Various piece of TeX want to use the libc `ungetc()` function a lot.
227 /// It's kind of gross, but happens often enough that we provide special
228 /// support for it. Here's `getc()` emulation that can return a previously
229 /// `ungetc()`-ed character.
230 pub fn getc(&mut self) -> Result<u8> {
231 if let Some(c) = self.ungetc_char {
232 self.ungetc_char = None;
233 return Ok(c);
234 }
235
236 let mut byte = [0u8; 1];
237
238 if self.read(&mut byte[..1])? == 0 {
239 // EOF
240 return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "EOF in getc").into());
241 }
242
243 Ok(byte[0])
244 }
245
246 /// Here's the `ungetc()` emulation.
247 pub fn ungetc(&mut self, byte: u8) -> Result<()> {
248 ensure!(
249 self.ungetc_char.is_none(),
250 "internal problem: cannot ungetc() more than once in a row"
251 );
252 self.ungetc_char = Some(byte);
253 Ok(())
254 }
255}
256
257impl Read for InputHandle {
258 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
259 if !buf.is_empty() {
260 if let Some(c) = self.ungetc_char {
261 // This does sometimes happen, so we need to deal with it. It's not that bad really.
262 buf[0] = c;
263 self.ungetc_char = None;
264 return Ok(self.read(&mut buf[1..])? + 1);
265 }
266 }
267
268 self.ever_read = true;
269 let n = self.inner.read(buf)?;
270 if !self.read_only {
271 self.digest.update(&buf[..n]);
272 }
273 Ok(n)
274 }
275}
276
277impl Seek for InputHandle {
278 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
279 self.try_seek(pos)
280 .map_err(|e| io::Error::new(io::ErrorKind::Other, e))
281 }
282}
283
284impl InputFeatures for InputHandle {
285 fn get_size(&mut self) -> Result<usize> {
286 self.inner.get_size()
287 }
288
289 fn get_unix_mtime(&mut self) -> Result<Option<i64>> {
290 self.inner.get_unix_mtime()
291 }
292
293 fn try_seek(&mut self, pos: SeekFrom) -> Result<u64> {
294 match pos {
295 SeekFrom::Start(0) => {
296 // As described above, there is a common pattern in TeX file
297 // accesses: read a few bytes to sniff, then go back to the
298 // beginning. We should tidy up the I/O to just buffer instead
299 // of seeking, but in the meantime, we can handle this.
300 self.digest = Default::default();
301 self.ever_read = false;
302 self.ungetc_char = None;
303 }
304 SeekFrom::Current(0) => {
305 // Noop. This must *not* clear the ungetc buffer for our
306 // current PDF startxref/xref parsing code to work.
307 }
308 _ => {
309 self.did_unhandled_seek = true;
310 self.ungetc_char = None;
311 }
312 }
313
314 let mut offset = self.inner.try_seek(pos)?;
315
316 // If there was an ungetc, the effective position in the stream is one
317 // byte before that of the underlying handle. Some of the code does
318 // noop seeks to get the current offset for various file parsing
319 // needs, so it's important that we return the right value. It should
320 // never happen that the underlying stream thinks that the offset is
321 // zero after we've ungetc'ed -- famous last words?
322
323 if self.ungetc_char.is_some() {
324 offset -= 1;
325 }
326
327 Ok(offset)
328 }
329}
330
331/// A handle for Tectonic output streams.
332pub struct OutputHandle {
333 name: String,
334 inner: Box<dyn Write>,
335 digest: digest::DigestComputer,
336}
337
338impl OutputHandle {
339 /// Create a new output handle.
340 pub fn new<T: 'static + Write>(name: impl Into<String>, inner: T) -> OutputHandle {
341 OutputHandle {
342 name: name.into(),
343 inner: Box::new(inner),
344 digest: digest::create(),
345 }
346 }
347
348 /// Get the name associated with this handle.
349 pub fn name(&self) -> &str {
350 &self.name
351 }
352
353 /// Consumes the object and returns the underlying writable handle that
354 /// it references.
355 pub fn into_inner(self) -> Box<dyn Write> {
356 self.inner
357 }
358
359 /// Consumes the object and returns the SHA256 sum of the content that was
360 /// written.
361 pub fn into_name_digest(self) -> (String, DigestData) {
362 (self.name, DigestData::from(self.digest))
363 }
364}
365
366impl Write for OutputHandle {
367 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
368 let n = self.inner.write(buf)?;
369 self.digest.update(&buf[..n]);
370 Ok(n)
371 }
372
373 fn flush(&mut self) -> io::Result<()> {
374 self.inner.flush()
375 }
376}
377
378/// A convenience type for file-open operations when "not found" needs to be
379/// handled specially.
380#[derive(Debug)]
381pub enum OpenResult<T> {
382 /// The open operation succeeded.
383 Ok(T),
384
385 /// The file was not available.
386 NotAvailable,
387
388 /// Something else went wrong.
389 Err(Error),
390}
391
392impl<T> OpenResult<T> {
393 /// Obtain the underlying file object, or panic.
394 pub fn unwrap(self) -> T {
395 match self {
396 OpenResult::Ok(t) => t,
397 _ => panic!("expected an open file"),
398 }
399 }
400
401 /// Returns true if this result is of the NotAvailable variant.
402 pub fn is_not_available(&self) -> bool {
403 matches!(*self, OpenResult::NotAvailable)
404 }
405
406 /// Convert this object into a plain Result, erroring if the item was not available.
407 pub fn must_exist(self) -> Result<T> {
408 match self {
409 OpenResult::Ok(t) => Ok(t),
410 OpenResult::Err(e) => Err(e),
411 OpenResult::NotAvailable => {
412 Err(io::Error::new(io::ErrorKind::NotFound, "not found").into())
413 }
414 }
415 }
416}
417
418/// A hack to allow casting of Bundles to IoProviders.
419///
420/// The code that sets up the I/O stack is handed a reference to a Bundle
421/// trait object. For the actual I/O, it needs to convert this to an
422/// IoProvider trait object. [According to
423/// StackExchange](https://stackoverflow.com/a/28664881/3760486), the
424/// following pattern is the least-bad way to achieve the necessary upcasting.
425pub trait AsIoProviderMut {
426 /// Represent this value as an IoProvider trait object.
427 fn as_ioprovider_mut(&mut self) -> &mut dyn IoProvider;
428}
429
430impl<T: IoProvider> AsIoProviderMut for T {
431 fn as_ioprovider_mut(&mut self) -> &mut dyn IoProvider {
432 self
433 }
434}
435
436/// A trait for types that can read or write files needed by the TeX engine.
437///
438/// An IO provider is a source of handles. One wrinkle is that it's good to be
439/// able to distinguish between unavailability of a given name and error
440/// accessing it.
441pub trait IoProvider: AsIoProviderMut {
442 /// Open the named file for output.
443 fn output_open_name(&mut self, _name: &str) -> OpenResult<OutputHandle> {
444 OpenResult::NotAvailable
445 }
446
447 /// Open the standard output stream.
448 fn output_open_stdout(&mut self) -> OpenResult<OutputHandle> {
449 OpenResult::NotAvailable
450 }
451
452 /// Open the named file for input.
453 fn input_open_name(
454 &mut self,
455 _name: &str,
456 _status: &mut dyn StatusBackend,
457 ) -> OpenResult<InputHandle> {
458 OpenResult::NotAvailable
459 }
460
461 /// Open the named file for input and return filesystem path information.
462 ///
463 /// This method extends [`Self::input_open_name`] to help support SyncTeX output.
464 /// While SyncTeX output files should contain absolute source file paths,
465 /// Tectonic’s pluggable I/O system makes it so that the mapping between
466 /// input names and filesystem paths is not well-defined. This optional
467 /// interface enables backends to provide filesystem information at the time
468 /// of opening.
469 ///
470 /// The default implementation returns None for the path information, to
471 /// preserve backwards compatibility. If you are implementing a new backend
472 /// that might provide path information, or you are implementing an I/O
473 /// provider that delegates to other I/O providers, you should implement
474 /// this function fully, and then provide a simple implementation of
475 /// [`Self::input_open_name`] that drops the pathing information.
476 fn input_open_name_with_abspath(
477 &mut self,
478 name: &str,
479 status: &mut dyn StatusBackend,
480 ) -> OpenResult<(InputHandle, Option<PathBuf>)> {
481 match self.input_open_name(name, status) {
482 OpenResult::Ok(h) => OpenResult::Ok((h, None)),
483 OpenResult::Err(e) => OpenResult::Err(e),
484 OpenResult::NotAvailable => OpenResult::NotAvailable,
485 }
486 }
487
488 /// Open the "primary" input file, which in the context of TeX is the main
489 /// input that it's given. When the build is being done using the
490 /// filesystem and the input is a file on the filesystem, this function
491 /// isn't necesssarily that important, but those conditions don't always
492 /// hold.
493 fn input_open_primary(&mut self, _status: &mut dyn StatusBackend) -> OpenResult<InputHandle> {
494 OpenResult::NotAvailable
495 }
496
497 /// Open the primary input and return filesystem path information.
498 ///
499 /// This method is as to [`Self::input_open_primary`] as
500 /// [`Self::input_open_name_with_abspath`] is to [`Self::input_open_name`].
501 fn input_open_primary_with_abspath(
502 &mut self,
503 status: &mut dyn StatusBackend,
504 ) -> OpenResult<(InputHandle, Option<PathBuf>)> {
505 match self.input_open_primary(status) {
506 OpenResult::Ok(h) => OpenResult::Ok((h, None)),
507 OpenResult::Err(e) => OpenResult::Err(e),
508 OpenResult::NotAvailable => OpenResult::NotAvailable,
509 }
510 }
511
512 /// Open a format file with the specified name. Format files have a
513 /// specialized entry point because IOProviders may wish to handle them
514 /// specially: namely, to munge the filename to one that includes the
515 /// current version of the Tectonic engine, since the format contents
516 /// depend sensitively on the engine internals.
517 fn input_open_format(
518 &mut self,
519 name: &str,
520 status: &mut dyn StatusBackend,
521 ) -> OpenResult<InputHandle> {
522 self.input_open_name(name, status)
523 }
524
525 /// Save an a format dump in some way that this provider may be able to
526 /// recover in the future. This awkward interface is needed to write
527 /// formats with their special munged file names.
528 fn write_format(
529 &mut self,
530 _name: &str,
531 _data: &[u8],
532 _status: &mut dyn StatusBackend,
533 ) -> Result<()> {
534 bail!("this I/O layer cannot save format files");
535 }
536}
537
538impl<P: IoProvider + ?Sized> IoProvider for Box<P> {
539 fn output_open_name(&mut self, name: &str) -> OpenResult<OutputHandle> {
540 (**self).output_open_name(name)
541 }
542
543 fn output_open_stdout(&mut self) -> OpenResult<OutputHandle> {
544 (**self).output_open_stdout()
545 }
546
547 fn input_open_name(
548 &mut self,
549 name: &str,
550 status: &mut dyn StatusBackend,
551 ) -> OpenResult<InputHandle> {
552 (**self).input_open_name(name, status)
553 }
554
555 fn input_open_name_with_abspath(
556 &mut self,
557 name: &str,
558 status: &mut dyn StatusBackend,
559 ) -> OpenResult<(InputHandle, Option<PathBuf>)> {
560 (**self).input_open_name_with_abspath(name, status)
561 }
562
563 fn input_open_primary(&mut self, status: &mut dyn StatusBackend) -> OpenResult<InputHandle> {
564 (**self).input_open_primary(status)
565 }
566
567 fn input_open_primary_with_abspath(
568 &mut self,
569 status: &mut dyn StatusBackend,
570 ) -> OpenResult<(InputHandle, Option<PathBuf>)> {
571 (**self).input_open_primary_with_abspath(status)
572 }
573
574 fn input_open_format(
575 &mut self,
576 name: &str,
577 status: &mut dyn StatusBackend,
578 ) -> OpenResult<InputHandle> {
579 (**self).input_open_format(name, status)
580 }
581
582 fn write_format(
583 &mut self,
584 name: &str,
585 data: &[u8],
586 status: &mut dyn StatusBackend,
587 ) -> Result<()> {
588 (**self).write_format(name, data, status)
589 }
590}
591
592// Some generically helpful InputFeatures impls
593
594impl InputFeatures for Cursor<Vec<u8>> {
595 fn get_size(&mut self) -> Result<usize> {
596 Ok(self.get_ref().len())
597 }
598
599 fn get_unix_mtime(&mut self) -> Result<Option<i64>> {
600 Ok(None)
601 }
602
603 fn try_seek(&mut self, pos: SeekFrom) -> Result<u64> {
604 Ok(self.seek(pos)?)
605 }
606}
607
608// Helpful.
609
610/// Try to open a file on the fileystem, returning an `OpenResult` type
611/// allowing easy handling if the file was not found.
612pub fn try_open_file<P: AsRef<Path>>(path: P) -> OpenResult<File> {
613 use std::io::ErrorKind::NotFound;
614
615 match File::open(path) {
616 Ok(f) => OpenResult::Ok(f),
617 Err(e) => {
618 if e.kind() == NotFound {
619 OpenResult::NotAvailable
620 } else {
621 OpenResult::Err(e.into())
622 }
623 }
624 }
625}
626
627// TeX path normalization:
628
629/// Normalize a path from a TeX build.
630///
631/// We attempt to do this in a system independent™ way by stripping any `.`,
632/// `..`, or extra separators '/' so that it is of the form.
633///
634/// ```text
635/// path/to/my/file.txt
636/// ../../path/to/parent/dir/file.txt
637/// /absolute/path/to/file.txt
638/// ```
639///
640/// Does not strip whitespace.
641///
642/// Returns `None` if the path refers to a parent of the root.
643fn try_normalize_tex_path(path: &str) -> Option<String> {
644 // TODO: we should normalize directory separators to "/".
645 // And do we need to handle Windows drive prefixes, etc?
646 use std::iter::repeat;
647
648 if path.is_empty() {
649 return Some("".into());
650 }
651
652 let mut r = Vec::new();
653 let mut parent_level = 0;
654 let mut has_root = false;
655
656 for (i, c) in path.split('/').enumerate() {
657 match c {
658 "" if i == 0 => {
659 has_root = true;
660 r.push("");
661 }
662 "" | "." => {}
663 ".." => {
664 match r.pop() {
665 // about to pop the root
666 Some("") => return None,
667 None => parent_level += 1,
668 _ => {}
669 }
670 }
671 _ => r.push(c),
672 }
673 }
674
675 let r = repeat("..")
676 .take(parent_level)
677 .chain(r)
678 // No `join` on `Iterator`.
679 .collect::<Vec<_>>()
680 .join("/");
681
682 if r.is_empty() {
683 if has_root {
684 Some("/".into())
685 } else {
686 Some(".".into())
687 }
688 } else {
689 Some(r)
690 }
691}
692
693/// Normalize a TeX path if possible, otherwise return the original path.
694///
695/// A _TeX path_ is a path that obeys simplified semantics: Unix-like syntax (`/`
696/// for separators, etc.), must be Unicode-able, no symlinks allowed such that
697/// `..` can be stripped lexically.
698pub fn normalize_tex_path(path: &str) -> Cow<str> {
699 if let Some(t) = try_normalize_tex_path(path).map(String::from) {
700 Cow::Owned(t)
701 } else {
702 Cow::Borrowed(path)
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709
710 #[test]
711 fn test_try_normalize_tex_path() {
712 // edge cases
713 assert_eq!(try_normalize_tex_path(""), Some("".into()));
714 assert_eq!(try_normalize_tex_path("/"), Some("/".into()));
715 assert_eq!(try_normalize_tex_path("//"), Some("/".into()));
716 assert_eq!(try_normalize_tex_path("."), Some(".".into()));
717 assert_eq!(try_normalize_tex_path("./"), Some(".".into()));
718 assert_eq!(try_normalize_tex_path(".."), Some("..".into()));
719 assert_eq!(try_normalize_tex_path("././/./"), Some(".".into()));
720 assert_eq!(try_normalize_tex_path("/././/."), Some("/".into()));
721
722 assert_eq!(
723 try_normalize_tex_path("my/path/file.txt"),
724 Some("my/path/file.txt".into())
725 );
726 // preserve spaces
727 assert_eq!(
728 try_normalize_tex_path(" my/pa th/file .txt "),
729 Some(" my/pa th/file .txt ".into())
730 );
731 assert_eq!(
732 try_normalize_tex_path("/my/path/file.txt"),
733 Some("/my/path/file.txt".into())
734 );
735 assert_eq!(
736 try_normalize_tex_path("./my///path/././file.txt"),
737 Some("my/path/file.txt".into())
738 );
739 assert_eq!(
740 try_normalize_tex_path("./../my/../../../file.txt"),
741 Some("../../../file.txt".into())
742 );
743 assert_eq!(
744 try_normalize_tex_path("././my//../path/../here/file.txt"),
745 Some("here/file.txt".into())
746 );
747 assert_eq!(
748 try_normalize_tex_path("./my/.././/path/../../here//file.txt"),
749 Some("../here/file.txt".into())
750 );
751
752 assert_eq!(try_normalize_tex_path("/my/../../file.txt"), None);
753 assert_eq!(
754 try_normalize_tex_path("/my/./.././path//../../file.txt"),
755 None
756 );
757 }
758}