Skip to main content

typst_syntax/
path.rs

1//! Virtual, cross-platform reproducible path handling.
2
3use std::error;
4use std::fmt::{self, Debug, Formatter};
5use std::num::NonZeroU16;
6use std::ops::Deref;
7use std::path::{self, Path, PathBuf};
8use std::sync::{LazyLock, RwLock};
9
10use ecow::{EcoString, eco_format};
11use rustc_hash::FxHashMap;
12
13use crate::package::PackageSpec;
14
15/// A path in a specific virtual file system root.
16///
17/// This identifies a location in a project or package.
18#[derive(Clone, Eq, PartialEq, Hash)]
19pub struct RootedPath {
20    root: VirtualRoot,
21    vpath: VirtualPath,
22}
23
24impl RootedPath {
25    /// Create a rooted path from a root and a virtual path within the root.
26    pub fn new(root: VirtualRoot, vpath: VirtualPath) -> Self {
27        Self { root, vpath }
28    }
29
30    /// Turns this path into a `FileId`.
31    pub fn intern(self) -> FileId {
32        FileId::new(self)
33    }
34
35    /// The root this path resides in.
36    pub fn root(&self) -> &VirtualRoot {
37        &self.root
38    }
39
40    /// The package the path resides in, if any.
41    #[deprecated = "use `root` instead"]
42    pub fn package(&self) -> Option<&PackageSpec> {
43        match self.root() {
44            VirtualRoot::Project => None,
45            VirtualRoot::Package(package) => Some(package),
46        }
47    }
48
49    /// The absolute and normalized path to the file _within_ the project or
50    /// package.
51    pub fn vpath(&self) -> &VirtualPath {
52        &self.vpath
53    }
54
55    /// Maps the virtual path while retaining the root.
56    pub fn map(&self, f: impl FnOnce(&VirtualPath) -> VirtualPath) -> Self {
57        Self::new(self.root.clone(), f(&self.vpath))
58    }
59}
60
61impl Debug for RootedPath {
62    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
63        let vpath = self.vpath();
64        match self.root() {
65            VirtualRoot::Project => Debug::fmt(vpath, f),
66            VirtualRoot::Package(package) => write!(f, "{package:?}{vpath:?}"),
67        }
68    }
69}
70
71/// The root of a virtual file system.
72#[derive(Debug, Clone, Eq, PartialEq, Hash)]
73pub enum VirtualRoot {
74    /// The canonical root of the Typst project.
75    ///
76    /// This is what `TYPST_ROOT` defines.
77    Project,
78    /// A root in a package.
79    Package(PackageSpec),
80}
81
82/// The global interner for rooted paths.
83static INTERNER: LazyLock<RwLock<Interner>> = LazyLock::new(|| {
84    RwLock::new(Interner { to_id: FxHashMap::default(), from_id: Vec::new() })
85});
86
87/// An interner for rooted paths.
88struct Interner {
89    to_id: FxHashMap<&'static RootedPath, FileId>,
90    from_id: Vec<&'static RootedPath>,
91}
92
93/// An interned version of [`RootedPath`].
94///
95/// This type is globally interned and thus cheap to copy, compare, and hash.
96#[derive(Copy, Clone, Eq, PartialEq, Hash)]
97pub struct FileId(NonZeroU16);
98
99impl FileId {
100    /// Create a new interned file specification.
101    ///
102    /// This is the same as [`RootedPath::intern`].
103    #[track_caller]
104    pub fn new(path: RootedPath) -> Self {
105        // Try to find an existing entry that we can reuse.
106        //
107        // We could check with just a read lock, but if the pair is not yet
108        // present, we would then need to recheck after acquiring a write lock,
109        // which is probably not worth it.
110        let mut interner = INTERNER.write().unwrap();
111        if let Some(&id) = interner.to_id.get(&path) {
112            return id;
113        }
114
115        // Create a new entry forever by leaking the pair. We can't leak more
116        // than 2^16 pair (and typically will leak a lot less), so its not a
117        // big deal.
118        let num = u16::try_from(interner.from_id.len() + 1)
119            .and_then(NonZeroU16::try_from)
120            .expect("out of file ids");
121
122        let id = FileId(num);
123        let leaked = Box::leak(Box::new(path));
124        interner.to_id.insert(leaked, id);
125        interner.from_id.push(leaked);
126        id
127    }
128
129    /// Create a new unique ("fake") file specification, which is not accessible
130    /// by path.
131    ///
132    /// Caution: the ID returned by this method is the *only* identifier of the
133    /// file, constructing a file ID with a path will *not* reuse the ID even if
134    /// the path is the same. This method should only be used for generating
135    /// "virtual" file ids such as content read from stdin.
136    ///
137    /// While the returned ID is not otherwise accessible, the provided root and
138    /// path still matter as they define how paths within the identified file
139    /// will resolve. For the example of content read from stdin, it will define
140    /// how a relative path in the stdin content is resolved.
141    #[track_caller]
142    pub fn unique(path: RootedPath) -> Self {
143        let mut interner = INTERNER.write().unwrap();
144        let num = u16::try_from(interner.from_id.len() + 1)
145            .and_then(NonZeroU16::try_from)
146            .expect("out of file ids");
147
148        let id = FileId(num);
149        let leaked = Box::leak(Box::new(path));
150        interner.from_id.push(leaked);
151        id
152    }
153
154    /// Construct from a raw number.
155    ///
156    /// Should only be used with numbers retrieved via
157    /// [`into_raw`](Self::into_raw). Misuse may results in panics, but no
158    /// unsafety.
159    pub const fn from_raw(v: NonZeroU16) -> Self {
160        Self(v)
161    }
162
163    /// Extract the raw underlying number.
164    pub const fn into_raw(self) -> NonZeroU16 {
165        self.0
166    }
167
168    /// Get the static, interned rooted path.
169    pub fn get(&self) -> &'static RootedPath {
170        INTERNER.read().unwrap().from_id[usize::from(self.0.get() - 1)]
171    }
172}
173
174impl Deref for FileId {
175    type Target = RootedPath;
176
177    fn deref(&self) -> &Self::Target {
178        self.get()
179    }
180}
181
182impl Debug for FileId {
183    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
184        self.get().fmt(f)
185    }
186}
187
188/// A path in a virtual file system.
189#[derive(Clone, Eq, PartialEq, Hash)]
190pub struct VirtualPath(Segments);
191
192impl VirtualPath {
193    /// Creates a new virtual path.
194    pub fn new(path: impl AsRef<str>) -> Result<Self, PathError> {
195        let segments = Segments::normalize(components(path.as_ref()))?;
196        Ok(Self(segments))
197    }
198
199    /// Creates a virtual path from a real path and a real root.
200    ///
201    /// Returns `None` if the file path is not contained in the root (i.e. if
202    /// `root_path` is not a lexical prefix of `path`). No file system
203    /// operations are performed.
204    ///
205    /// This is the single function that translates from a real path to a
206    /// virtual path. Its counterpart is [`VirtualPath::realize`].
207    pub fn virtualize(root_path: &Path, path: &Path) -> Result<Self, VirtualizeError> {
208        let path = path.strip_prefix(root_path).map_err(|_| PathError::Escapes)?;
209        let mut segments = Segments::new();
210        for c in path.components() {
211            let comp = match c {
212                path::Component::RootDir => Component::Root,
213                path::Component::CurDir => Component::Current,
214                path::Component::ParentDir => Component::Parent,
215                path::Component::Normal(s) => {
216                    let string = s.to_str().ok_or(VirtualizeError::Utf8)?;
217                    let segment = Segment::new(string)
218                        .map_err(|s| VirtualizeError::Invalid(s.into()))?;
219                    Component::Normal(segment)
220                }
221                path::Component::Prefix(_) => return Err(PathError::Escapes.into()),
222            };
223            segments.push_component(comp)?;
224        }
225        Ok(Self(segments))
226    }
227
228    /// Turns the virtual path into an actual file system path (where the
229    /// project or package resides). You need to provide the appropriate `root`
230    /// path, relative to which this path will be resolved.
231    ///
232    /// This can be used in the implementations of `World::source` and
233    /// `World::file`.
234    ///
235    /// This is the single function that translates from a virtual path to a
236    /// real path. Its counterpart is [`VirtualPath::virtualize`].
237    pub fn realize(&self, root: &Path) -> PathBuf {
238        let mut out = root.to_path_buf();
239        for s in self.0.iter() {
240            out.push(s.get());
241        }
242        out
243    }
244
245    /// Extracts the path with a leading slash.
246    pub fn into_with_slash(self) -> EcoString {
247        self.0.into_with_slash()
248    }
249
250    /// Returns the path with a leading slash.
251    pub fn get_with_slash(&self) -> &str {
252        self.0.get_with_slash()
253    }
254
255    /// Returns the path without a leading slash.
256    pub fn get_without_slash(&self) -> &str {
257        self.0.get_without_slash()
258    }
259
260    /// Whether this is the path `/`.
261    pub fn is_root(&self) -> bool {
262        self.0.is_empty()
263    }
264
265    /// Returns the file name portion of the path.
266    pub fn file_name(&self) -> Option<&str> {
267        self.0.last().map(Segment::get)
268    }
269
270    /// Returns the file name portion of the path without the extension.
271    pub fn file_stem(&self) -> Option<&str> {
272        let last = self.0.last()?;
273        let (before, after) = last.split_dot();
274        before.or(after)
275    }
276
277    /// Returns the file extension of the path.
278    pub fn extension(&self) -> Option<&str> {
279        let last = self.0.last()?;
280        let (before, after) = last.split_dot();
281        before.and(after)
282    }
283
284    /// Returns a modified path with an adjusted extension.
285    ///
286    /// # Panics
287    /// Panics if the resulting path segment would be invalid, e.g. because the
288    /// extension contains a forward or backslash.
289    #[track_caller]
290    pub fn with_extension(&self, ext: &str) -> Self {
291        let Some(stem) = self.file_stem() else { return self.clone() };
292        let buf = eco_format!("{stem}.{ext}");
293        let segment = Segment::new(&buf).expect("extension is invalid");
294
295        let mut segments = self.0.clone();
296        segments.pop();
297        segments.push(segment);
298        Self(segments)
299    }
300
301    /// Returns the path with its final component removed.
302    ///
303    /// Returns `None` if the path is already at the root.
304    pub fn parent(&self) -> Option<Self> {
305        let mut segments = self.0.clone();
306        if !segments.pop() {
307            return None;
308        }
309        Some(Self(segments))
310    }
311
312    /// Joins the given `path` to `self`.
313    pub fn join(&self, path: &str) -> Result<Self, PathError> {
314        let combined = self
315            .0
316            .iter()
317            .map(|c| Ok(Component::Normal(c)))
318            .chain(components(path));
319        let segments = Segments::normalize(combined)?;
320        Ok(Self(segments))
321    }
322
323    /// Expresses this path as a relative path from the given base path.
324    pub fn relative_from(&self, base: &Self) -> EcoString {
325        // Adapted from rustc's `path_relative_from` function (MIT).
326        // Copyright 2012-2015 The Rust Project Developers.
327        // See NOTICE for full attribution.
328        let mut ita = self.0.iter();
329        let mut itb = base.0.iter();
330        let mut buf: Vec<&str> = vec![];
331        loop {
332            match (ita.next(), itb.next()) {
333                (None, None) => break,
334                (Some(a), None) => {
335                    buf.push(a.get());
336                    buf.extend(ita.map(Segment::get));
337                    break;
338                }
339                (None, Some(_)) => buf.push(".."),
340                (Some(a), Some(b)) if buf.is_empty() && a == b => (),
341                (Some(a), Some(_)) => {
342                    buf.extend(std::iter::repeat_n("..", 1 + itb.count()));
343                    buf.push(a.get());
344                    buf.extend(ita.map(Segment::get));
345                    break;
346                }
347            }
348        }
349        buf.join("/").into()
350    }
351}
352
353impl VirtualPath {
354    /// Create a virtual path from a real path and a real root.
355    #[deprecated = "use `virtualize` with swapped arguments instead"]
356    pub fn within_root(path: &Path, root: &Path) -> Option<Self> {
357        Self::virtualize(root, path).ok()
358    }
359
360    /// Resolve the virtual path relative to an actual file system root
361    /// (where the project or package resides).
362    #[deprecated = "use `realize` instead"]
363    pub fn resolve(&self, root: &Path) -> Option<PathBuf> {
364        Some(self.realize(root))
365    }
366
367    /// Get the underlying path without a leading `/` or `\`.
368    #[deprecated = "use `get_without_slash` instead"]
369    pub fn as_rootless_path(&self) -> &Path {
370        Path::new(self.get_without_slash())
371    }
372
373    /// Get the underlying path with a leading `/` or `\`.
374    #[deprecated = "use `get_with_slash` instead"]
375    pub fn as_rooted_path(&self) -> &Path {
376        Path::new(self.get_with_slash())
377    }
378}
379
380impl Debug for VirtualPath {
381    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
382        self.get_with_slash().fmt(f)
383    }
384}
385
386/// A component in a virtual path.
387#[derive(Debug, Copy, Clone, Eq, PartialEq)]
388enum Component<'a> {
389    Root,
390    Current,
391    Parent,
392    Normal(Segment<'a>),
393}
394
395// Special symbols in virtual paths.
396const SEPARATOR: char = '/';
397const CURRENT: &str = ".";
398const PARENT: &str = "..";
399
400/// Splits a user-supplied path into its constituent parts.
401///
402/// This only splits and recognizes special segments. It does not check the
403/// validity of normal segments. This is done in [`Segments::push`].
404fn components(path: &str) -> impl Iterator<Item = Result<Component<'_>, PathError>> {
405    path.split(SEPARATOR).enumerate().map(|(i, s)| {
406        match s {
407            // A leading separator indicates an absolute path.
408            "" if i == 0 && !path.is_empty() => Ok(Component::Root),
409            // Consecutive separators have no effect.
410            "" => Ok(Component::Current),
411            CURRENT => Ok(Component::Current),
412            PARENT => Ok(Component::Parent),
413            other => match Segment::new(other) {
414                Ok(segment) => Ok(Component::Normal(segment)),
415                Err("\\") => Err(PathError::Backslash),
416                Err(_) => unreachable!(),
417            },
418        }
419    })
420}
421
422/// A segment in a normalized path.
423///
424/// A segments is never empty, `.`, or `..` and it never contains back- or
425/// forward slashes.
426#[derive(Debug, Copy, Clone, Eq, PartialEq)]
427struct Segment<'a>(&'a str);
428
429impl<'a> Segment<'a> {
430    fn new(segment: &'a str) -> Result<Self, &'a str> {
431        // These invariants are important to avoid the path from escaping the
432        // root after being realized, in particular the `..` part.
433        if matches!(segment, "" | CURRENT | PARENT) {
434            return Err(segment);
435        }
436
437        // Interior separators or backslashes are not allowed.
438        if let Some(m) = segment.matches([SEPARATOR, '\\']).next() {
439            return Err(m);
440        }
441
442        Ok(Self(segment))
443    }
444
445    fn new_unchecked(segment: &'a str) -> Self {
446        debug_assert!(Self::new(segment).is_ok());
447        Self(segment)
448    }
449
450    fn get(self) -> &'a str {
451        self.0
452    }
453
454    fn split_dot(self) -> (Option<&'a str>, Option<&'a str>) {
455        let mut iter = self.0.rsplitn(2, '.');
456        let after = iter.next();
457        let before = iter.next();
458        if before == Some("") { (Some(self.0), None) } else { (before, after) }
459    }
460}
461
462/// Stores a sequence of path segments as a string.
463///
464/// The underlying string always represents a normalized absolute path and is
465/// guaranteed to start with a slash. Segments are never empty, `.`, or `..` and
466/// they never contain back- or forward slashes.
467#[derive(Clone, Eq, PartialEq, Hash)]
468struct Segments(EcoString);
469
470impl Segments {
471    fn new() -> Self {
472        Self(EcoString::from(SEPARATOR))
473    }
474
475    fn normalize<'a>(
476        comps: impl IntoIterator<Item = Result<Component<'a>, PathError>>,
477    ) -> Result<Segments, PathError> {
478        let mut out = Segments::new();
479        for component in comps {
480            out.push_component(component?)?;
481        }
482        Ok(out)
483    }
484
485    fn is_empty(&self) -> bool {
486        self.0.len() == 1
487    }
488
489    fn into_with_slash(self) -> EcoString {
490        self.0
491    }
492
493    fn get_with_slash(&self) -> &str {
494        &self.0
495    }
496
497    fn get_without_slash(&self) -> &str {
498        self.0.strip_prefix(SEPARATOR).expect("path to start with slash")
499    }
500
501    fn clear(&mut self) {
502        self.0.truncate(1);
503    }
504
505    fn push_component(&mut self, component: Component) -> Result<(), PathError> {
506        match component {
507            // Root component resets the path.
508            Component::Root => self.clear(),
509            // Current component has no effect.
510            Component::Current => {}
511            // Parent component removes the last segment. If there is no
512            // segment, this indicates that the path would escape the root.
513            // In this case, we return an error.
514            Component::Parent => {
515                if !self.pop() {
516                    return Err(PathError::Escapes);
517                }
518            }
519            Component::Normal(segment) => self.push(segment),
520        }
521        Ok(())
522    }
523
524    fn push<'a>(&mut self, segment: Segment<'a>) {
525        if !self.is_empty() {
526            self.0.push(SEPARATOR);
527        }
528        self.0.push_str(segment.0);
529    }
530
531    fn pop(&mut self) -> bool {
532        if self.is_empty() {
533            return false;
534        }
535        let i = self.0.rfind(SEPARATOR).expect("to contain a slash");
536        self.0.truncate(std::cmp::max(1, i));
537        true
538    }
539
540    fn last(&self) -> Option<Segment<'_>> {
541        self.iter().next_back()
542    }
543
544    fn iter(&self) -> impl DoubleEndedIterator<Item = Segment<'_>> {
545        let mut iter = self.0[1..].split(SEPARATOR);
546        if self.is_empty() {
547            iter.next();
548        }
549        iter.map(Segment::new_unchecked)
550    }
551}
552
553/// An error that can occur on construction or modification of a
554/// [`VirtualPath`].
555#[derive(Debug, Copy, Clone, Eq, PartialEq)]
556pub enum PathError {
557    /// The constructed or modified path would escape the root. This would
558    /// for instance, when trying to join `..` to the path `/`.
559    ///
560    /// Note that a path might still escape through symlinks.
561    Escapes,
562    /// The path contains a backslash. This is not allowed as it leads to
563    /// cross-platform compatibility hazards (since Windows uses backslashes as
564    /// a path separator).
565    Backslash,
566}
567
568impl fmt::Display for PathError {
569    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
570        match self {
571            Self::Escapes => write!(f, "path escapes project root"),
572            Self::Backslash => write!(f, "path contains backslash"),
573        }
574    }
575}
576
577impl error::Error for PathError {}
578
579/// An error that can occur in [`VirtualPath::virtualize`].
580#[derive(Debug, Clone, Eq, PartialEq)]
581pub enum VirtualizeError {
582    /// A normal path error.
583    Path(PathError),
584    /// A path component contained an invalid string. This should almost never
585    /// occur under normal circumstances, but it could happen if some OS allows
586    /// forward slashes or dots in path components.
587    Invalid(EcoString),
588    /// The file path contains non-UTF-8 encodable bytes.
589    Utf8,
590}
591
592impl fmt::Display for VirtualizeError {
593    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
594        write!(f, "could not virtualize path, ")?;
595
596        match self {
597            Self::Path(inner) => fmt::Display::fmt(inner, f),
598            Self::Invalid(component) => {
599                write!(f, "path contains invalid component `{component:?}`")
600            }
601            Self::Utf8 => write!(f, "path contains non-UTF-8 bytes"),
602        }
603    }
604}
605
606// NOTE: Because we opt to inline the formatting of the PathError we cannot
607// also return it as the error source, else it will be displayed twice in error
608// backtraces.
609impl error::Error for VirtualizeError {}
610
611impl From<PathError> for VirtualizeError {
612    fn from(err: PathError) -> Self {
613        Self::Path(err)
614    }
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620
621    #[track_caller]
622    fn path(p: &str) -> VirtualPath {
623        VirtualPath::new(p).unwrap()
624    }
625
626    #[test]
627    fn test_new() {
628        #[track_caller]
629        fn test(path: &str, expected: Result<&str, PathError>) {
630            let path = VirtualPath::new(path);
631            assert_eq!(
632                path.as_ref().map(|s| s.get_with_slash()).map_err(Clone::clone),
633                expected
634            );
635        }
636
637        test("", Ok("/"));
638        test("a/./file.txt", Ok("/a/file.txt"));
639        test("file.txt", Ok("/file.txt"));
640        test("/file.txt", Ok("/file.txt"));
641        test("hello/world", Ok("/hello/world"));
642        test("hello/world/", Ok("/hello/world"));
643        test("a///b", Ok("/a/b"));
644        test("/a///b", Ok("/a/b"));
645        test("./world.txt", Ok("/world.txt"));
646        test("./world.txt/", Ok("/world.txt"));
647        test("hello/.././/wor/ld.typ.extra", Ok("/wor/ld.typ.extra"));
648        test("hello/.../world", Ok("/hello/.../world"));
649        test("\u{200b}..", Ok("/\u{200b}.."));
650        test("..", Err(PathError::Escapes));
651        test("../world.txt", Err(PathError::Escapes));
652        test("a\\world.txt", Err(PathError::Backslash));
653    }
654
655    #[test]
656    #[cfg(unix)]
657    fn test_virtualize_unix() {
658        test_virtualize("/", "/main.typ", Ok("/main.typ"));
659        test_virtualize("//a/b", "/a//b///c//d", Ok("/c/d"));
660        test_virtualize(
661            "/home/typst/desktop/",
662            "/home/typst/desktop/src/main.typ",
663            Ok("/src/main.typ"),
664        );
665        test_virtualize(
666            "/home/typst/desktop/",
667            "/home/typst/main.typ",
668            Err(PathError::Escapes.into()),
669        );
670    }
671
672    #[test]
673    #[cfg(windows)]
674    fn test_virtualize_windows() {
675        test_virtualize(
676            "C:\\Users\\typst\\Desktop",
677            "C:\\Users\\typst\\Desktop\\src\\main.typ",
678            Ok("/src/main.typ"),
679        );
680        test_virtualize(
681            "C:\\Users\\typst\\Desktop",
682            "C:\\Users\\typst\\main.typ",
683            Err(PathError::Escapes.into()),
684        );
685    }
686
687    #[track_caller]
688    fn test_virtualize(
689        root_path: impl AsRef<Path>,
690        path: impl AsRef<Path>,
691        expected: Result<&str, VirtualizeError>,
692    ) {
693        assert_eq!(
694            VirtualPath::virtualize(root_path.as_ref(), path.as_ref(),)
695                .as_ref()
696                .map(|v| v.get_with_slash())
697                .map_err(Clone::clone),
698            expected,
699        );
700    }
701
702    #[test]
703    fn test_realize() {
704        let p = path("src/text/main.typ");
705        assert_eq!(
706            p.realize(Path::new("/home/users/typst")),
707            Path::new("/home/users/typst/src/text/main.typ")
708        );
709    }
710
711    #[test]
712    fn test_file_ops() {
713        let p1 = path("src/text/file.typ");
714        assert_eq!(p1.file_name(), Some("file.typ"));
715        assert_eq!(p1.file_stem(), Some("file"));
716        assert_eq!(p1.extension(), Some("typ"));
717        assert_eq!(p1.with_extension("txt"), path("src/text/file.txt"));
718        assert_eq!(p1.parent(), Some(path("src/text")));
719
720        let p2 = path("src");
721        assert_eq!(p2.file_name(), Some("src"));
722        assert_eq!(p2.file_stem(), Some("src"));
723        assert_eq!(p2.extension(), None);
724        assert_eq!(p2.with_extension("txt"), path("src.txt"));
725        assert_eq!(p2.parent(), Some(path("/")));
726
727        let p3 = path("");
728        assert_eq!(p3.file_name(), None);
729        assert_eq!(p3.file_stem(), None);
730        assert_eq!(p3.extension(), None);
731        assert_eq!(p3.with_extension("txt"), p3);
732        assert_eq!(p3.parent(), None);
733    }
734
735    #[test]
736    fn test_join() {
737        let p1 = path("src");
738        assert_eq!(p1.join("a\\b"), Err(PathError::Backslash));
739        let p2 = p1.join("text").unwrap();
740        assert_eq!(p2.get_with_slash(), "/src/text");
741        let p3 = p2.join("..").unwrap();
742        assert_eq!(p1, p3);
743        assert_eq!(p3.get_with_slash(), "/src");
744        let p4 = p3.join("..").unwrap();
745        assert_eq!(p4.get_with_slash(), "/");
746        assert_eq!(p4.join(".."), Err(PathError::Escapes));
747    }
748
749    #[test]
750    fn test_relative_from() {
751        let p1 = path("src/text/main.typ");
752        assert_eq!(p1.relative_from(&path("/src/text")), "main.typ");
753        assert_eq!(p1.relative_from(&path("/src/data")), "../text/main.typ");
754        assert_eq!(p1.relative_from(&path("src/")), "text/main.typ");
755        assert_eq!(p1.relative_from(&path("/")), "src/text/main.typ");
756
757        let p2 = path("src");
758        assert_eq!(p2.relative_from(&path("src")), "");
759        assert_eq!(p2.relative_from(&path("src/data")), "..");
760    }
761
762    #[test]
763    fn test_segments() {
764        let mut s = Segments::new();
765        assert_eq!(s.get_with_slash(), "/");
766        assert_eq!(s.get_without_slash(), "");
767        s.push(Segment::new("to").unwrap());
768        assert_eq!(s.get_with_slash(), "/to");
769        s.push(Segment::new("hi.txt").unwrap());
770        assert_eq!(s.get_with_slash(), "/to/hi.txt");
771        assert_eq!(s.get_without_slash(), "to/hi.txt");
772        assert_eq!(s.last().map(Segment::get), Some("hi.txt"));
773        assert!(s.pop());
774        assert_eq!(s.get_with_slash(), "/to");
775        assert!(s.pop());
776        assert_eq!(s.get_with_slash(), "/");
777        assert!(!s.pop());
778        assert_eq!(s.get_with_slash(), "/");
779        assert_eq!(s.last(), None);
780    }
781
782    #[test]
783    fn test_segment() {
784        assert_eq!(Segment::new("\\b"), Err("\\"));
785        assert_eq!(Segment::new("a/b"), Err("/"));
786        assert_eq!(Segment::new(""), Err(""));
787        assert_eq!(Segment::new("."), Err("."));
788        assert_eq!(Segment::new(".."), Err(".."));
789    }
790}