use std::error;
use std::ffi::OsStr;
use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroU16;
use std::ops::Deref;
use std::path::{self, Path, PathBuf};
use std::sync::{LazyLock, RwLock};
use ecow::{EcoString, eco_format};
use rustc_hash::FxHashMap;
use crate::package::PackageSpec;
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct RootedPath {
root: VirtualRoot,
vpath: VirtualPath,
}
impl RootedPath {
pub fn new(root: VirtualRoot, vpath: VirtualPath) -> Self {
Self { root, vpath }
}
pub fn intern(self) -> FileId {
FileId::new(self)
}
pub fn root(&self) -> &VirtualRoot {
&self.root
}
#[deprecated = "use `root` instead"]
pub fn package(&self) -> Option<&PackageSpec> {
match self.root() {
VirtualRoot::Project => None,
VirtualRoot::Package(package) => Some(package),
}
}
pub fn vpath(&self) -> &VirtualPath {
&self.vpath
}
pub fn map(&self, f: impl FnOnce(&VirtualPath) -> VirtualPath) -> Self {
Self::new(self.root.clone(), f(&self.vpath))
}
}
impl Debug for RootedPath {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
let vpath = self.vpath();
match self.root() {
VirtualRoot::Project => Debug::fmt(vpath, f),
VirtualRoot::Package(package) => write!(f, "{package:?}{vpath:?}"),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum VirtualRoot {
Project,
Package(PackageSpec),
}
static INTERNER: LazyLock<RwLock<Interner>> = LazyLock::new(|| {
RwLock::new(Interner { to_id: FxHashMap::default(), from_id: Vec::new() })
});
struct Interner {
to_id: FxHashMap<&'static RootedPath, FileId>,
from_id: Vec<&'static RootedPath>,
}
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct FileId(NonZeroU16);
impl FileId {
#[track_caller]
pub fn new(path: RootedPath) -> Self {
let mut interner = INTERNER.write().unwrap();
if let Some(&id) = interner.to_id.get(&path) {
return id;
}
let num = u16::try_from(interner.from_id.len() + 1)
.and_then(NonZeroU16::try_from)
.expect("out of file ids");
let id = FileId(num);
let leaked = Box::leak(Box::new(path));
interner.to_id.insert(leaked, id);
interner.from_id.push(leaked);
id
}
#[track_caller]
pub fn unique(path: RootedPath) -> Self {
let mut interner = INTERNER.write().unwrap();
let num = u16::try_from(interner.from_id.len() + 1)
.and_then(NonZeroU16::try_from)
.expect("out of file ids");
let id = FileId(num);
let leaked = Box::leak(Box::new(path));
interner.from_id.push(leaked);
id
}
pub const fn from_raw(v: NonZeroU16) -> Self {
Self(v)
}
pub const fn into_raw(self) -> NonZeroU16 {
self.0
}
pub fn get(&self) -> &'static RootedPath {
INTERNER.read().unwrap().from_id[usize::from(self.0.get() - 1)]
}
}
impl Deref for FileId {
type Target = RootedPath;
fn deref(&self) -> &Self::Target {
self.get()
}
}
impl Debug for FileId {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.get().fmt(f)
}
}
#[derive(Clone, Eq, PartialEq, Hash)]
pub struct VirtualPath(Segments);
impl VirtualPath {
pub fn new(path: impl AsRef<str>) -> Result<Self, PathError> {
let segments = Segments::normalize(components(path.as_ref()))?;
Ok(Self(segments))
}
pub fn virtualize(root_path: &Path, path: &Path) -> Result<Self, VirtualizeError> {
let path = path.strip_prefix(root_path).map_err(|_| PathError::Escapes)?;
let mut segments = Segments::new();
for c in path.components() {
let comp = match c {
path::Component::RootDir => Component::Root,
path::Component::CurDir => Component::Current,
path::Component::ParentDir => Component::Parent,
path::Component::Normal(s) => {
let string = s.to_str().ok_or(VirtualizeError::Utf8)?;
let segment = Segment::new(string)
.map_err(|s| VirtualizeError::Invalid(s.into()))?;
Component::Normal(segment)
}
path::Component::Prefix(_) => return Err(PathError::Escapes.into()),
};
segments.push_component(comp)?;
}
Ok(Self(segments))
}
pub fn realize(&self, root: &Path) -> Result<PathBuf, RealizeError> {
let mut out = root.to_path_buf();
for s in self.0.iter() {
out.push(s.realize()?);
}
Ok(out)
}
pub fn into_with_slash(self) -> EcoString {
self.0.into_with_slash()
}
pub fn get_with_slash(&self) -> &str {
self.0.get_with_slash()
}
pub fn get_without_slash(&self) -> &str {
self.0.get_without_slash()
}
pub fn is_root(&self) -> bool {
self.0.is_empty()
}
pub fn file_name(&self) -> Option<&str> {
self.0.last().map(Segment::get)
}
pub fn file_stem(&self) -> Option<&str> {
let last = self.0.last()?;
let (before, after) = last.split_dot();
before.or(after)
}
pub fn extension(&self) -> Option<&str> {
let last = self.0.last()?;
let (before, after) = last.split_dot();
before.and(after)
}
#[track_caller]
pub fn with_extension(&self, ext: &str) -> Self {
let Some(stem) = self.file_stem() else { return self.clone() };
let buf = eco_format!("{stem}.{ext}");
let segment = Segment::new(&buf).expect("extension is invalid");
let mut segments = self.0.clone();
segments.pop();
segments.push(segment);
Self(segments)
}
pub fn parent(&self) -> Option<Self> {
let mut segments = self.0.clone();
if !segments.pop() {
return None;
}
Some(Self(segments))
}
pub fn join(&self, path: &str) -> Result<Self, PathError> {
let combined = self
.0
.iter()
.map(|c| Ok(Component::Normal(c)))
.chain(components(path));
let segments = Segments::normalize(combined)?;
Ok(Self(segments))
}
pub fn relative_from(&self, base: &Self) -> EcoString {
let mut ita = self.0.iter();
let mut itb = base.0.iter();
let mut buf: Vec<&str> = vec![];
loop {
match (ita.next(), itb.next()) {
(None, None) => break,
(Some(a), None) => {
buf.push(a.get());
buf.extend(ita.map(Segment::get));
break;
}
(None, Some(_)) => buf.push(".."),
(Some(a), Some(b)) if buf.is_empty() && a == b => (),
(Some(a), Some(_)) => {
buf.extend(std::iter::repeat_n("..", 1 + itb.count()));
buf.push(a.get());
buf.extend(ita.map(Segment::get));
break;
}
}
}
buf.join("/").into()
}
}
impl VirtualPath {
#[deprecated = "use `virtualize` with swapped arguments instead"]
pub fn within_root(path: &Path, root: &Path) -> Option<Self> {
Self::virtualize(root, path).ok()
}
#[deprecated = "use `realize` instead"]
pub fn resolve(&self, root: &Path) -> Option<PathBuf> {
self.realize(root).ok()
}
#[deprecated = "use `get_without_slash` instead"]
pub fn as_rootless_path(&self) -> &Path {
Path::new(self.get_without_slash())
}
#[deprecated = "use `get_with_slash` instead"]
pub fn as_rooted_path(&self) -> &Path {
Path::new(self.get_with_slash())
}
}
impl Debug for VirtualPath {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.get_with_slash().fmt(f)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
enum Component<'a> {
Root,
Current,
Parent,
Normal(Segment<'a>),
}
const SEPARATOR: char = '/';
const CURRENT: &str = ".";
const PARENT: &str = "..";
fn components(path: &str) -> impl Iterator<Item = Result<Component<'_>, PathError>> {
path.split(SEPARATOR).enumerate().map(|(i, s)| {
match s {
"" if i == 0 && !path.is_empty() => Ok(Component::Root),
"" => Ok(Component::Current),
CURRENT => Ok(Component::Current),
PARENT => Ok(Component::Parent),
other => match Segment::new(other) {
Ok(segment) => Ok(Component::Normal(segment)),
Err("\\") => Err(PathError::Backslash),
Err(_) => unreachable!(),
},
}
})
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
struct Segment<'a>(&'a str);
impl<'a> Segment<'a> {
fn new(segment: &'a str) -> Result<Self, &'a str> {
if matches!(segment, "" | CURRENT | PARENT) {
return Err(segment);
}
if let Some(m) = segment.matches([SEPARATOR, '\\']).next() {
return Err(m);
}
Ok(Self(segment))
}
fn new_unchecked(segment: &'a str) -> Self {
debug_assert!(Self::new(segment).is_ok());
Self(segment)
}
fn get(self) -> &'a str {
self.0
}
fn split_dot(self) -> (Option<&'a str>, Option<&'a str>) {
let mut iter = self.0.rsplitn(2, '.');
let after = iter.next();
let before = iter.next();
if before == Some("") { (Some(self.0), None) } else { (before, after) }
}
fn realize(self) -> Result<&'a OsStr, RealizeError> {
let mut iter = Path::new(self.get()).components();
match (iter.next(), iter.next()) {
(Some(path::Component::Normal(s)), None) => {
#[cfg(windows)]
if is_windows_reserved(self.get()) {
return Err(RealizeError::Invalid(self.get().into()));
}
Ok(s)
}
(None | Some(path::Component::Normal(_)), _) => {
Err(RealizeError::Invalid(self.get().into()))
}
(Some(other), _) => {
Err(RealizeError::Invalid(other.as_os_str().to_string_lossy().into()))
}
}
}
}
#[cfg(windows)]
fn is_windows_reserved(name: &str) -> bool {
#[rustfmt::skip]
fn is_reserved_base(basename: &str) -> bool {
matches!(
basename,
"CON" | "PRN" | "AUX" | "NUL"
| "COM0" | "COM1" | "COM2" | "COM3" | "COM4" | "COM5" | "COM6"
| "COM7" | "COM8" | "COM9" | "COM¹" | "COM²" | "COM³"
| "LPT0" | "LPT1" | "LPT2" | "LPT3" | "LPT4" | "LPT5" | "LPT6"
| "LPT7" | "LPT8" | "LPT9" | "LPT¹" | "LPT²" | "LPT³"
| "CONIN$" | "CONOUT$"
)
}
let base = name.split(['.', ':']).next().unwrap_or(name).trim_end_matches(' ');
base.len() <= 7 && is_reserved_base(&EcoString::from(base).to_ascii_uppercase())
}
#[derive(Clone, Eq, PartialEq, Hash)]
struct Segments(EcoString);
impl Segments {
fn new() -> Self {
Self(EcoString::from(SEPARATOR))
}
fn normalize<'a>(
comps: impl IntoIterator<Item = Result<Component<'a>, PathError>>,
) -> Result<Segments, PathError> {
let mut out = Segments::new();
for component in comps {
out.push_component(component?)?;
}
Ok(out)
}
fn is_empty(&self) -> bool {
self.0.len() == 1
}
fn into_with_slash(self) -> EcoString {
self.0
}
fn get_with_slash(&self) -> &str {
&self.0
}
fn get_without_slash(&self) -> &str {
self.0.strip_prefix(SEPARATOR).expect("path to start with slash")
}
fn clear(&mut self) {
self.0.truncate(1);
}
fn push_component(&mut self, component: Component) -> Result<(), PathError> {
match component {
Component::Root => self.clear(),
Component::Current => {}
Component::Parent => {
if !self.pop() {
return Err(PathError::Escapes);
}
}
Component::Normal(segment) => self.push(segment),
}
Ok(())
}
fn push<'a>(&mut self, segment: Segment<'a>) {
if !self.is_empty() {
self.0.push(SEPARATOR);
}
self.0.push_str(segment.0);
}
fn pop(&mut self) -> bool {
if self.is_empty() {
return false;
}
let i = self.0.rfind(SEPARATOR).expect("to contain a slash");
self.0.truncate(std::cmp::max(1, i));
true
}
fn last(&self) -> Option<Segment<'_>> {
self.iter().next_back()
}
fn iter(&self) -> impl DoubleEndedIterator<Item = Segment<'_>> {
let mut iter = self.0[1..].split(SEPARATOR);
if self.is_empty() {
iter.next();
}
iter.map(Segment::new_unchecked)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum PathError {
Escapes,
Backslash,
}
impl fmt::Display for PathError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Escapes => write!(f, "path escapes project root"),
Self::Backslash => write!(f, "path contains backslash"),
}
}
}
impl error::Error for PathError {}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum VirtualizeError {
Path(PathError),
Invalid(EcoString),
Utf8,
}
impl fmt::Display for VirtualizeError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Path(inner) => fmt::Display::fmt(inner, f),
Self::Invalid(component) => {
write!(f, "path contains invalid component `{component:?}`")
}
Self::Utf8 => write!(f, "path contains non-UTF-8 bytes"),
}
}
}
impl error::Error for VirtualizeError {}
impl From<PathError> for VirtualizeError {
fn from(err: PathError) -> Self {
Self::Path(err)
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum RealizeError {
Invalid(EcoString),
}
impl fmt::Display for RealizeError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Invalid(component) => {
write!(f, "path contains invalid component `{component:?}`")
}
}
}
}
impl error::Error for RealizeError {}
#[cfg(test)]
mod tests {
use super::*;
#[track_caller]
fn path(p: &str) -> VirtualPath {
VirtualPath::new(p).unwrap()
}
#[test]
fn test_new() {
#[track_caller]
fn test(path: &str, expected: Result<&str, PathError>) {
let path = VirtualPath::new(path);
assert_eq!(
path.as_ref().map(|s| s.get_with_slash()).map_err(Clone::clone),
expected
);
}
test("", Ok("/"));
test("a/./file.txt", Ok("/a/file.txt"));
test("file.txt", Ok("/file.txt"));
test("/file.txt", Ok("/file.txt"));
test("hello/world", Ok("/hello/world"));
test("hello/world/", Ok("/hello/world"));
test("a///b", Ok("/a/b"));
test("/a///b", Ok("/a/b"));
test("./world.txt", Ok("/world.txt"));
test("./world.txt/", Ok("/world.txt"));
test("hello/.././/wor/ld.typ.extra", Ok("/wor/ld.typ.extra"));
test("hello/.../world", Ok("/hello/.../world"));
test("\u{200b}..", Ok("/\u{200b}.."));
test("..", Err(PathError::Escapes));
test("../world.txt", Err(PathError::Escapes));
test("a\\world.txt", Err(PathError::Backslash));
}
#[test]
#[cfg(unix)]
fn test_virtualize_unix() {
test_virtualize("/", "/main.typ", Ok("/main.typ"));
test_virtualize("//a/b", "/a//b///c//d", Ok("/c/d"));
test_virtualize(
"/home/typst/desktop/",
"/home/typst/desktop/src/main.typ",
Ok("/src/main.typ"),
);
test_virtualize(
"/home/typst/desktop/",
"/home/typst/main.typ",
Err(PathError::Escapes.into()),
);
}
#[test]
#[cfg(windows)]
fn test_virtualize_windows() {
test_virtualize(
"C:\\Users\\typst\\Desktop",
"C:\\Users\\typst\\Desktop\\src\\main.typ",
Ok("/src/main.typ"),
);
test_virtualize(
"C:\\Users\\typst\\Desktop",
"C:\\Users\\typst\\main.typ",
Err(PathError::Escapes.into()),
);
}
#[track_caller]
fn test_virtualize(
root_path: impl AsRef<Path>,
path: impl AsRef<Path>,
expected: Result<&str, VirtualizeError>,
) {
assert_eq!(
VirtualPath::virtualize(root_path.as_ref(), path.as_ref(),)
.as_ref()
.map(|v| v.get_with_slash())
.map_err(Clone::clone),
expected,
);
}
#[test]
fn test_realize() {
let p = path("src/text/main.typ");
assert_eq!(
p.realize(Path::new("/home/users/typst")),
Ok(PathBuf::from("/home/users/typst/src/text/main.typ")),
);
}
#[test]
#[cfg(windows)]
fn test_realize_windows() {
let root = Path::new("C:\\Users\\typst");
let invalid = |s: &str| RealizeError::Invalid(s.into());
assert_eq!(path("C:System32").realize(root), Err(invalid("C:")));
assert_eq!(path("D:Stuff").realize(root), Err(invalid("D:")));
assert_eq!(path("C:/System32").realize(root), Err(invalid("C:")));
assert_eq!(path("Foo/E:System").realize(root), Err(invalid("E:")));
assert_eq!(path("Foo/E:/System").realize(root), Err(invalid("E:")));
assert_eq!(path("F:").realize(root), Err(invalid("F:")));
assert_eq!(path("CON").realize(root), Err(invalid("CON")));
assert_eq!(path("A/CON .txt").realize(root), Err(invalid("CON .txt")));
assert_eq!(path("A/CON:foo/bar").realize(root), Err(invalid("CON:foo")));
assert_eq!(path("a/LPT\u{00b2} .baz/b").realize(root), Err(invalid("LPT² .baz")));
}
#[test]
fn test_file_ops() {
let p1 = path("src/text/file.typ");
assert_eq!(p1.file_name(), Some("file.typ"));
assert_eq!(p1.file_stem(), Some("file"));
assert_eq!(p1.extension(), Some("typ"));
assert_eq!(p1.with_extension("txt"), path("src/text/file.txt"));
assert_eq!(p1.parent(), Some(path("src/text")));
let p2 = path("src");
assert_eq!(p2.file_name(), Some("src"));
assert_eq!(p2.file_stem(), Some("src"));
assert_eq!(p2.extension(), None);
assert_eq!(p2.with_extension("txt"), path("src.txt"));
assert_eq!(p2.parent(), Some(path("/")));
let p3 = path("");
assert_eq!(p3.file_name(), None);
assert_eq!(p3.file_stem(), None);
assert_eq!(p3.extension(), None);
assert_eq!(p3.with_extension("txt"), p3);
assert_eq!(p3.parent(), None);
}
#[test]
fn test_join() {
let p1 = path("src");
assert_eq!(p1.join("a\\b"), Err(PathError::Backslash));
let p2 = p1.join("text").unwrap();
assert_eq!(p2.get_with_slash(), "/src/text");
let p3 = p2.join("..").unwrap();
assert_eq!(p1, p3);
assert_eq!(p3.get_with_slash(), "/src");
let p4 = p3.join("..").unwrap();
assert_eq!(p4.get_with_slash(), "/");
assert_eq!(p4.join(".."), Err(PathError::Escapes));
}
#[test]
fn test_relative_from() {
let p1 = path("src/text/main.typ");
assert_eq!(p1.relative_from(&path("/src/text")), "main.typ");
assert_eq!(p1.relative_from(&path("/src/data")), "../text/main.typ");
assert_eq!(p1.relative_from(&path("src/")), "text/main.typ");
assert_eq!(p1.relative_from(&path("/")), "src/text/main.typ");
let p2 = path("src");
assert_eq!(p2.relative_from(&path("src")), "");
assert_eq!(p2.relative_from(&path("src/data")), "..");
}
#[test]
fn test_segments() {
let mut s = Segments::new();
assert_eq!(s.get_with_slash(), "/");
assert_eq!(s.get_without_slash(), "");
s.push(Segment::new("to").unwrap());
assert_eq!(s.get_with_slash(), "/to");
s.push(Segment::new("hi.txt").unwrap());
assert_eq!(s.get_with_slash(), "/to/hi.txt");
assert_eq!(s.get_without_slash(), "to/hi.txt");
assert_eq!(s.last().map(Segment::get), Some("hi.txt"));
assert!(s.pop());
assert_eq!(s.get_with_slash(), "/to");
assert!(s.pop());
assert_eq!(s.get_with_slash(), "/");
assert!(!s.pop());
assert_eq!(s.get_with_slash(), "/");
assert_eq!(s.last(), None);
}
#[test]
fn test_segment() {
assert_eq!(Segment::new("\\b"), Err("\\"));
assert_eq!(Segment::new("a/b"), Err("/"));
assert_eq!(Segment::new(""), Err(""));
assert_eq!(Segment::new("."), Err("."));
assert_eq!(Segment::new(".."), Err(".."));
}
}