1use 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#[derive(Clone, Eq, PartialEq, Hash)]
19pub struct RootedPath {
20 root: VirtualRoot,
21 vpath: VirtualPath,
22}
23
24impl RootedPath {
25 pub fn new(root: VirtualRoot, vpath: VirtualPath) -> Self {
27 Self { root, vpath }
28 }
29
30 pub fn intern(self) -> FileId {
32 FileId::new(self)
33 }
34
35 pub fn root(&self) -> &VirtualRoot {
37 &self.root
38 }
39
40 #[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 pub fn vpath(&self) -> &VirtualPath {
52 &self.vpath
53 }
54
55 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#[derive(Debug, Clone, Eq, PartialEq, Hash)]
73pub enum VirtualRoot {
74 Project,
78 Package(PackageSpec),
80}
81
82static INTERNER: LazyLock<RwLock<Interner>> = LazyLock::new(|| {
84 RwLock::new(Interner { to_id: FxHashMap::default(), from_id: Vec::new() })
85});
86
87struct Interner {
89 to_id: FxHashMap<&'static RootedPath, FileId>,
90 from_id: Vec<&'static RootedPath>,
91}
92
93#[derive(Copy, Clone, Eq, PartialEq, Hash)]
97pub struct FileId(NonZeroU16);
98
99impl FileId {
100 #[track_caller]
104 pub fn new(path: RootedPath) -> Self {
105 let mut interner = INTERNER.write().unwrap();
111 if let Some(&id) = interner.to_id.get(&path) {
112 return id;
113 }
114
115 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 #[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 pub const fn from_raw(v: NonZeroU16) -> Self {
160 Self(v)
161 }
162
163 pub const fn into_raw(self) -> NonZeroU16 {
165 self.0
166 }
167
168 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#[derive(Clone, Eq, PartialEq, Hash)]
190pub struct VirtualPath(Segments);
191
192impl VirtualPath {
193 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 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 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 pub fn into_with_slash(self) -> EcoString {
247 self.0.into_with_slash()
248 }
249
250 pub fn get_with_slash(&self) -> &str {
252 self.0.get_with_slash()
253 }
254
255 pub fn get_without_slash(&self) -> &str {
257 self.0.get_without_slash()
258 }
259
260 pub fn is_root(&self) -> bool {
262 self.0.is_empty()
263 }
264
265 pub fn file_name(&self) -> Option<&str> {
267 self.0.last().map(Segment::get)
268 }
269
270 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 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 #[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 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 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 pub fn relative_from(&self, base: &Self) -> EcoString {
325 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 #[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 #[deprecated = "use `realize` instead"]
363 pub fn resolve(&self, root: &Path) -> Option<PathBuf> {
364 Some(self.realize(root))
365 }
366
367 #[deprecated = "use `get_without_slash` instead"]
369 pub fn as_rootless_path(&self) -> &Path {
370 Path::new(self.get_without_slash())
371 }
372
373 #[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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
388enum Component<'a> {
389 Root,
390 Current,
391 Parent,
392 Normal(Segment<'a>),
393}
394
395const SEPARATOR: char = '/';
397const CURRENT: &str = ".";
398const PARENT: &str = "..";
399
400fn components(path: &str) -> impl Iterator<Item = Result<Component<'_>, PathError>> {
405 path.split(SEPARATOR).enumerate().map(|(i, s)| {
406 match s {
407 "" if i == 0 && !path.is_empty() => Ok(Component::Root),
409 "" => 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#[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 if matches!(segment, "" | CURRENT | PARENT) {
434 return Err(segment);
435 }
436
437 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#[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 Component::Root => self.clear(),
509 Component::Current => {}
511 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#[derive(Debug, Copy, Clone, Eq, PartialEq)]
556pub enum PathError {
557 Escapes,
562 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#[derive(Debug, Clone, Eq, PartialEq)]
581pub enum VirtualizeError {
582 Path(PathError),
584 Invalid(EcoString),
588 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
606impl 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}