1use std::error;
4use std::ffi::OsStr;
5use std::fmt::{self, Debug, Formatter};
6use std::num::NonZeroU16;
7use std::ops::Deref;
8use std::path::{self, Path, PathBuf};
9use std::sync::{LazyLock, RwLock};
10
11use ecow::{EcoString, eco_format};
12use rustc_hash::FxHashMap;
13
14use crate::package::PackageSpec;
15
16#[derive(Clone, Eq, PartialEq, Hash)]
20pub struct RootedPath {
21 root: VirtualRoot,
22 vpath: VirtualPath,
23}
24
25impl RootedPath {
26 pub fn new(root: VirtualRoot, vpath: VirtualPath) -> Self {
28 Self { root, vpath }
29 }
30
31 pub fn intern(self) -> FileId {
33 FileId::new(self)
34 }
35
36 pub fn root(&self) -> &VirtualRoot {
38 &self.root
39 }
40
41 #[deprecated = "use `root` instead"]
43 pub fn package(&self) -> Option<&PackageSpec> {
44 match self.root() {
45 VirtualRoot::Project => None,
46 VirtualRoot::Package(package) => Some(package),
47 }
48 }
49
50 pub fn vpath(&self) -> &VirtualPath {
53 &self.vpath
54 }
55
56 pub fn map(&self, f: impl FnOnce(&VirtualPath) -> VirtualPath) -> Self {
58 Self::new(self.root.clone(), f(&self.vpath))
59 }
60}
61
62impl Debug for RootedPath {
63 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
64 let vpath = self.vpath();
65 match self.root() {
66 VirtualRoot::Project => Debug::fmt(vpath, f),
67 VirtualRoot::Package(package) => write!(f, "{package:?}{vpath:?}"),
68 }
69 }
70}
71
72#[derive(Debug, Clone, Eq, PartialEq, Hash)]
74pub enum VirtualRoot {
75 Project,
79 Package(PackageSpec),
81}
82
83static INTERNER: LazyLock<RwLock<Interner>> = LazyLock::new(|| {
85 RwLock::new(Interner { to_id: FxHashMap::default(), from_id: Vec::new() })
86});
87
88struct Interner {
90 to_id: FxHashMap<&'static RootedPath, FileId>,
91 from_id: Vec<&'static RootedPath>,
92}
93
94#[derive(Copy, Clone, Eq, PartialEq, Hash)]
98pub struct FileId(NonZeroU16);
99
100impl FileId {
101 #[track_caller]
105 pub fn new(path: RootedPath) -> Self {
106 let mut interner = INTERNER.write().unwrap();
112 if let Some(&id) = interner.to_id.get(&path) {
113 return id;
114 }
115
116 let num = u16::try_from(interner.from_id.len() + 1)
120 .and_then(NonZeroU16::try_from)
121 .expect("out of file ids");
122
123 let id = FileId(num);
124 let leaked = Box::leak(Box::new(path));
125 interner.to_id.insert(leaked, id);
126 interner.from_id.push(leaked);
127 id
128 }
129
130 #[track_caller]
143 pub fn unique(path: RootedPath) -> Self {
144 let mut interner = INTERNER.write().unwrap();
145 let num = u16::try_from(interner.from_id.len() + 1)
146 .and_then(NonZeroU16::try_from)
147 .expect("out of file ids");
148
149 let id = FileId(num);
150 let leaked = Box::leak(Box::new(path));
151 interner.from_id.push(leaked);
152 id
153 }
154
155 pub const fn from_raw(v: NonZeroU16) -> Self {
161 Self(v)
162 }
163
164 pub const fn into_raw(self) -> NonZeroU16 {
166 self.0
167 }
168
169 pub fn get(&self) -> &'static RootedPath {
171 INTERNER.read().unwrap().from_id[usize::from(self.0.get() - 1)]
172 }
173}
174
175impl Deref for FileId {
176 type Target = RootedPath;
177
178 fn deref(&self) -> &Self::Target {
179 self.get()
180 }
181}
182
183impl Debug for FileId {
184 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
185 self.get().fmt(f)
186 }
187}
188
189#[derive(Clone, Eq, PartialEq, Hash)]
191pub struct VirtualPath(Segments);
192
193impl VirtualPath {
194 pub fn new(path: impl AsRef<str>) -> Result<Self, PathError> {
196 let segments = Segments::normalize(components(path.as_ref()))?;
197 Ok(Self(segments))
198 }
199
200 pub fn virtualize(root_path: &Path, path: &Path) -> Result<Self, VirtualizeError> {
209 let path = path.strip_prefix(root_path).map_err(|_| PathError::Escapes)?;
210 let mut segments = Segments::new();
211 for c in path.components() {
212 let comp = match c {
213 path::Component::RootDir => Component::Root,
214 path::Component::CurDir => Component::Current,
215 path::Component::ParentDir => Component::Parent,
216 path::Component::Normal(s) => {
217 let string = s.to_str().ok_or(VirtualizeError::Utf8)?;
218 let segment = Segment::new(string)
219 .map_err(|s| VirtualizeError::Invalid(s.into()))?;
220 Component::Normal(segment)
221 }
222 path::Component::Prefix(_) => return Err(PathError::Escapes.into()),
223 };
224 segments.push_component(comp)?;
225 }
226 Ok(Self(segments))
227 }
228
229 pub fn realize(&self, root: &Path) -> Result<PathBuf, RealizeError> {
244 let mut out = root.to_path_buf();
245 for s in self.0.iter() {
246 out.push(s.realize()?);
247 }
248 Ok(out)
249 }
250
251 pub fn into_with_slash(self) -> EcoString {
253 self.0.into_with_slash()
254 }
255
256 pub fn get_with_slash(&self) -> &str {
258 self.0.get_with_slash()
259 }
260
261 pub fn get_without_slash(&self) -> &str {
263 self.0.get_without_slash()
264 }
265
266 pub fn is_root(&self) -> bool {
268 self.0.is_empty()
269 }
270
271 pub fn file_name(&self) -> Option<&str> {
273 self.0.last().map(Segment::get)
274 }
275
276 pub fn file_stem(&self) -> Option<&str> {
278 let last = self.0.last()?;
279 let (before, after) = last.split_dot();
280 before.or(after)
281 }
282
283 pub fn extension(&self) -> Option<&str> {
285 let last = self.0.last()?;
286 let (before, after) = last.split_dot();
287 before.and(after)
288 }
289
290 #[track_caller]
296 pub fn with_extension(&self, ext: &str) -> Self {
297 let Some(stem) = self.file_stem() else { return self.clone() };
298 let buf = eco_format!("{stem}.{ext}");
299 let segment = Segment::new(&buf).expect("extension is invalid");
300
301 let mut segments = self.0.clone();
302 segments.pop();
303 segments.push(segment);
304 Self(segments)
305 }
306
307 pub fn parent(&self) -> Option<Self> {
311 let mut segments = self.0.clone();
312 if !segments.pop() {
313 return None;
314 }
315 Some(Self(segments))
316 }
317
318 pub fn join(&self, path: &str) -> Result<Self, PathError> {
320 let combined = self
321 .0
322 .iter()
323 .map(|c| Ok(Component::Normal(c)))
324 .chain(components(path));
325 let segments = Segments::normalize(combined)?;
326 Ok(Self(segments))
327 }
328
329 pub fn relative_from(&self, base: &Self) -> EcoString {
331 let mut ita = self.0.iter();
335 let mut itb = base.0.iter();
336 let mut buf: Vec<&str> = vec![];
337 loop {
338 match (ita.next(), itb.next()) {
339 (None, None) => break,
340 (Some(a), None) => {
341 buf.push(a.get());
342 buf.extend(ita.map(Segment::get));
343 break;
344 }
345 (None, Some(_)) => buf.push(".."),
346 (Some(a), Some(b)) if buf.is_empty() && a == b => (),
347 (Some(a), Some(_)) => {
348 buf.extend(std::iter::repeat_n("..", 1 + itb.count()));
349 buf.push(a.get());
350 buf.extend(ita.map(Segment::get));
351 break;
352 }
353 }
354 }
355 buf.join("/").into()
356 }
357}
358
359impl VirtualPath {
360 #[deprecated = "use `virtualize` with swapped arguments instead"]
362 pub fn within_root(path: &Path, root: &Path) -> Option<Self> {
363 Self::virtualize(root, path).ok()
364 }
365
366 #[deprecated = "use `realize` instead"]
369 pub fn resolve(&self, root: &Path) -> Option<PathBuf> {
370 self.realize(root).ok()
371 }
372
373 #[deprecated = "use `get_without_slash` instead"]
375 pub fn as_rootless_path(&self) -> &Path {
376 Path::new(self.get_without_slash())
377 }
378
379 #[deprecated = "use `get_with_slash` instead"]
381 pub fn as_rooted_path(&self) -> &Path {
382 Path::new(self.get_with_slash())
383 }
384}
385
386impl Debug for VirtualPath {
387 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
388 self.get_with_slash().fmt(f)
389 }
390}
391
392#[derive(Debug, Copy, Clone, Eq, PartialEq)]
394enum Component<'a> {
395 Root,
396 Current,
397 Parent,
398 Normal(Segment<'a>),
399}
400
401const SEPARATOR: char = '/';
403const CURRENT: &str = ".";
404const PARENT: &str = "..";
405
406fn components(path: &str) -> impl Iterator<Item = Result<Component<'_>, PathError>> {
411 path.split(SEPARATOR).enumerate().map(|(i, s)| {
412 match s {
413 "" if i == 0 && !path.is_empty() => Ok(Component::Root),
415 "" => Ok(Component::Current),
417 CURRENT => Ok(Component::Current),
418 PARENT => Ok(Component::Parent),
419 other => match Segment::new(other) {
420 Ok(segment) => Ok(Component::Normal(segment)),
421 Err("\\") => Err(PathError::Backslash),
422 Err(_) => unreachable!(),
423 },
424 }
425 })
426}
427
428#[derive(Debug, Copy, Clone, Eq, PartialEq)]
433struct Segment<'a>(&'a str);
434
435impl<'a> Segment<'a> {
436 fn new(segment: &'a str) -> Result<Self, &'a str> {
437 if matches!(segment, "" | CURRENT | PARENT) {
440 return Err(segment);
441 }
442
443 if let Some(m) = segment.matches([SEPARATOR, '\\']).next() {
445 return Err(m);
446 }
447
448 Ok(Self(segment))
449 }
450
451 fn new_unchecked(segment: &'a str) -> Self {
452 debug_assert!(Self::new(segment).is_ok());
453 Self(segment)
454 }
455
456 fn get(self) -> &'a str {
457 self.0
458 }
459
460 fn split_dot(self) -> (Option<&'a str>, Option<&'a str>) {
461 let mut iter = self.0.rsplitn(2, '.');
462 let after = iter.next();
463 let before = iter.next();
464 if before == Some("") { (Some(self.0), None) } else { (before, after) }
465 }
466
467 fn realize(self) -> Result<&'a OsStr, RealizeError> {
472 let mut iter = Path::new(self.get()).components();
476 match (iter.next(), iter.next()) {
477 (Some(path::Component::Normal(s)), None) => {
479 #[cfg(windows)]
482 if is_windows_reserved(self.get()) {
483 return Err(RealizeError::Invalid(self.get().into()));
484 }
485 Ok(s)
486 }
487 (None | Some(path::Component::Normal(_)), _) => {
489 Err(RealizeError::Invalid(self.get().into()))
490 }
491 (Some(other), _) => {
493 Err(RealizeError::Invalid(other.as_os_str().to_string_lossy().into()))
494 }
495 }
496 }
497}
498
499#[cfg(windows)]
504fn is_windows_reserved(name: &str) -> bool {
505 #[rustfmt::skip]
506 fn is_reserved_base(basename: &str) -> bool {
507 matches!(
508 basename,
509 "CON" | "PRN" | "AUX" | "NUL"
510 | "COM0" | "COM1" | "COM2" | "COM3" | "COM4" | "COM5" | "COM6"
511 | "COM7" | "COM8" | "COM9" | "COM¹" | "COM²" | "COM³"
512 | "LPT0" | "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6"
513 | "LPT7" | "LPT8" | "LPT9" | "LPT¹" | "LPT²" | "LPT³"
514 | "CONIN$" | "CONOUT$"
515 )
516 }
517
518 let base = name.split(['.', ':']).next().unwrap_or(name).trim_end_matches(' ');
521 base.len() <= 7 && is_reserved_base(&EcoString::from(base).to_ascii_uppercase())
522}
523
524#[derive(Clone, Eq, PartialEq, Hash)]
530struct Segments(EcoString);
531
532impl Segments {
533 fn new() -> Self {
534 Self(EcoString::from(SEPARATOR))
535 }
536
537 fn normalize<'a>(
538 comps: impl IntoIterator<Item = Result<Component<'a>, PathError>>,
539 ) -> Result<Segments, PathError> {
540 let mut out = Segments::new();
541 for component in comps {
542 out.push_component(component?)?;
543 }
544 Ok(out)
545 }
546
547 fn is_empty(&self) -> bool {
548 self.0.len() == 1
549 }
550
551 fn into_with_slash(self) -> EcoString {
552 self.0
553 }
554
555 fn get_with_slash(&self) -> &str {
556 &self.0
557 }
558
559 fn get_without_slash(&self) -> &str {
560 self.0.strip_prefix(SEPARATOR).expect("path to start with slash")
561 }
562
563 fn clear(&mut self) {
564 self.0.truncate(1);
565 }
566
567 fn push_component(&mut self, component: Component) -> Result<(), PathError> {
568 match component {
569 Component::Root => self.clear(),
571 Component::Current => {}
573 Component::Parent => {
577 if !self.pop() {
578 return Err(PathError::Escapes);
579 }
580 }
581 Component::Normal(segment) => self.push(segment),
582 }
583 Ok(())
584 }
585
586 fn push<'a>(&mut self, segment: Segment<'a>) {
587 if !self.is_empty() {
588 self.0.push(SEPARATOR);
589 }
590 self.0.push_str(segment.0);
591 }
592
593 fn pop(&mut self) -> bool {
594 if self.is_empty() {
595 return false;
596 }
597 let i = self.0.rfind(SEPARATOR).expect("to contain a slash");
598 self.0.truncate(std::cmp::max(1, i));
599 true
600 }
601
602 fn last(&self) -> Option<Segment<'_>> {
603 self.iter().next_back()
604 }
605
606 fn iter(&self) -> impl DoubleEndedIterator<Item = Segment<'_>> {
607 let mut iter = self.0[1..].split(SEPARATOR);
608 if self.is_empty() {
609 iter.next();
610 }
611 iter.map(Segment::new_unchecked)
612 }
613}
614
615#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
618pub enum PathError {
619 Escapes,
624 Backslash,
628}
629
630impl fmt::Display for PathError {
631 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
632 match self {
633 Self::Escapes => write!(f, "path escapes project root"),
634 Self::Backslash => write!(f, "path contains backslash"),
635 }
636 }
637}
638
639impl error::Error for PathError {}
640
641#[derive(Debug, Clone, Eq, PartialEq, Hash)]
643pub enum VirtualizeError {
644 Path(PathError),
646 Invalid(EcoString),
650 Utf8,
652}
653
654impl fmt::Display for VirtualizeError {
655 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
656 match self {
657 Self::Path(inner) => fmt::Display::fmt(inner, f),
658 Self::Invalid(component) => {
659 write!(f, "path contains invalid component `{component:?}`")
660 }
661 Self::Utf8 => write!(f, "path contains non-UTF-8 bytes"),
662 }
663 }
664}
665
666impl error::Error for VirtualizeError {}
670
671impl From<PathError> for VirtualizeError {
672 fn from(err: PathError) -> Self {
673 Self::Path(err)
674 }
675}
676
677#[derive(Debug, Clone, Eq, PartialEq, Hash)]
679pub enum RealizeError {
680 Invalid(EcoString),
690}
691
692impl fmt::Display for RealizeError {
693 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
694 match self {
695 Self::Invalid(component) => {
696 write!(f, "path contains invalid component `{component:?}`")
697 }
698 }
699 }
700}
701
702impl error::Error for RealizeError {}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707
708 #[track_caller]
709 fn path(p: &str) -> VirtualPath {
710 VirtualPath::new(p).unwrap()
711 }
712
713 #[test]
714 fn test_new() {
715 #[track_caller]
716 fn test(path: &str, expected: Result<&str, PathError>) {
717 let path = VirtualPath::new(path);
718 assert_eq!(
719 path.as_ref().map(|s| s.get_with_slash()).map_err(Clone::clone),
720 expected
721 );
722 }
723
724 test("", Ok("/"));
725 test("a/./file.txt", Ok("/a/file.txt"));
726 test("file.txt", Ok("/file.txt"));
727 test("/file.txt", Ok("/file.txt"));
728 test("hello/world", Ok("/hello/world"));
729 test("hello/world/", Ok("/hello/world"));
730 test("a///b", Ok("/a/b"));
731 test("/a///b", Ok("/a/b"));
732 test("./world.txt", Ok("/world.txt"));
733 test("./world.txt/", Ok("/world.txt"));
734 test("hello/.././/wor/ld.typ.extra", Ok("/wor/ld.typ.extra"));
735 test("hello/.../world", Ok("/hello/.../world"));
736 test("\u{200b}..", Ok("/\u{200b}.."));
737 test("..", Err(PathError::Escapes));
738 test("../world.txt", Err(PathError::Escapes));
739 test("a\\world.txt", Err(PathError::Backslash));
740 }
741
742 #[test]
743 #[cfg(unix)]
744 fn test_virtualize_unix() {
745 test_virtualize("/", "/main.typ", Ok("/main.typ"));
746 test_virtualize("//a/b", "/a//b///c//d", Ok("/c/d"));
747 test_virtualize(
748 "/home/typst/desktop/",
749 "/home/typst/desktop/src/main.typ",
750 Ok("/src/main.typ"),
751 );
752 test_virtualize(
753 "/home/typst/desktop/",
754 "/home/typst/main.typ",
755 Err(PathError::Escapes.into()),
756 );
757 }
758
759 #[test]
760 #[cfg(windows)]
761 fn test_virtualize_windows() {
762 test_virtualize(
763 "C:\\Users\\typst\\Desktop",
764 "C:\\Users\\typst\\Desktop\\src\\main.typ",
765 Ok("/src/main.typ"),
766 );
767 test_virtualize(
768 "C:\\Users\\typst\\Desktop",
769 "C:\\Users\\typst\\main.typ",
770 Err(PathError::Escapes.into()),
771 );
772 }
773
774 #[track_caller]
775 fn test_virtualize(
776 root_path: impl AsRef<Path>,
777 path: impl AsRef<Path>,
778 expected: Result<&str, VirtualizeError>,
779 ) {
780 assert_eq!(
781 VirtualPath::virtualize(root_path.as_ref(), path.as_ref(),)
782 .as_ref()
783 .map(|v| v.get_with_slash())
784 .map_err(Clone::clone),
785 expected,
786 );
787 }
788
789 #[test]
790 fn test_realize() {
791 let p = path("src/text/main.typ");
792 assert_eq!(
793 p.realize(Path::new("/home/users/typst")),
794 Ok(PathBuf::from("/home/users/typst/src/text/main.typ")),
795 );
796 }
797
798 #[test]
799 #[cfg(windows)]
800 fn test_realize_windows() {
801 let root = Path::new("C:\\Users\\typst");
802 let invalid = |s: &str| RealizeError::Invalid(s.into());
803 assert_eq!(path("C:System32").realize(root), Err(invalid("C:")));
804 assert_eq!(path("D:Stuff").realize(root), Err(invalid("D:")));
805 assert_eq!(path("C:/System32").realize(root), Err(invalid("C:")));
806 assert_eq!(path("Foo/E:System").realize(root), Err(invalid("E:")));
807 assert_eq!(path("Foo/E:/System").realize(root), Err(invalid("E:")));
808 assert_eq!(path("F:").realize(root), Err(invalid("F:")));
809 assert_eq!(path("CON").realize(root), Err(invalid("CON")));
810 assert_eq!(path("A/CON .txt").realize(root), Err(invalid("CON .txt")));
811 assert_eq!(path("A/CON:foo/bar").realize(root), Err(invalid("CON:foo")));
812 assert_eq!(path("a/LPT\u{00b2} .baz/b").realize(root), Err(invalid("LPT² .baz")));
813 }
814
815 #[test]
816 fn test_file_ops() {
817 let p1 = path("src/text/file.typ");
818 assert_eq!(p1.file_name(), Some("file.typ"));
819 assert_eq!(p1.file_stem(), Some("file"));
820 assert_eq!(p1.extension(), Some("typ"));
821 assert_eq!(p1.with_extension("txt"), path("src/text/file.txt"));
822 assert_eq!(p1.parent(), Some(path("src/text")));
823
824 let p2 = path("src");
825 assert_eq!(p2.file_name(), Some("src"));
826 assert_eq!(p2.file_stem(), Some("src"));
827 assert_eq!(p2.extension(), None);
828 assert_eq!(p2.with_extension("txt"), path("src.txt"));
829 assert_eq!(p2.parent(), Some(path("/")));
830
831 let p3 = path("");
832 assert_eq!(p3.file_name(), None);
833 assert_eq!(p3.file_stem(), None);
834 assert_eq!(p3.extension(), None);
835 assert_eq!(p3.with_extension("txt"), p3);
836 assert_eq!(p3.parent(), None);
837 }
838
839 #[test]
840 fn test_join() {
841 let p1 = path("src");
842 assert_eq!(p1.join("a\\b"), Err(PathError::Backslash));
843 let p2 = p1.join("text").unwrap();
844 assert_eq!(p2.get_with_slash(), "/src/text");
845 let p3 = p2.join("..").unwrap();
846 assert_eq!(p1, p3);
847 assert_eq!(p3.get_with_slash(), "/src");
848 let p4 = p3.join("..").unwrap();
849 assert_eq!(p4.get_with_slash(), "/");
850 assert_eq!(p4.join(".."), Err(PathError::Escapes));
851 }
852
853 #[test]
854 fn test_relative_from() {
855 let p1 = path("src/text/main.typ");
856 assert_eq!(p1.relative_from(&path("/src/text")), "main.typ");
857 assert_eq!(p1.relative_from(&path("/src/data")), "../text/main.typ");
858 assert_eq!(p1.relative_from(&path("src/")), "text/main.typ");
859 assert_eq!(p1.relative_from(&path("/")), "src/text/main.typ");
860
861 let p2 = path("src");
862 assert_eq!(p2.relative_from(&path("src")), "");
863 assert_eq!(p2.relative_from(&path("src/data")), "..");
864 }
865
866 #[test]
867 fn test_segments() {
868 let mut s = Segments::new();
869 assert_eq!(s.get_with_slash(), "/");
870 assert_eq!(s.get_without_slash(), "");
871 s.push(Segment::new("to").unwrap());
872 assert_eq!(s.get_with_slash(), "/to");
873 s.push(Segment::new("hi.txt").unwrap());
874 assert_eq!(s.get_with_slash(), "/to/hi.txt");
875 assert_eq!(s.get_without_slash(), "to/hi.txt");
876 assert_eq!(s.last().map(Segment::get), Some("hi.txt"));
877 assert!(s.pop());
878 assert_eq!(s.get_with_slash(), "/to");
879 assert!(s.pop());
880 assert_eq!(s.get_with_slash(), "/");
881 assert!(!s.pop());
882 assert_eq!(s.get_with_slash(), "/");
883 assert_eq!(s.last(), None);
884 }
885
886 #[test]
887 fn test_segment() {
888 assert_eq!(Segment::new("\\b"), Err("\\"));
889 assert_eq!(Segment::new("a/b"), Err("/"));
890 assert_eq!(Segment::new(""), Err(""));
891 assert_eq!(Segment::new("."), Err("."));
892 assert_eq!(Segment::new(".."), Err(".."));
893 }
894}