use ecow::{EcoString, eco_format};
use typst_syntax::{FileId, PathError, RootedPath, Spanned, VirtualPath, VirtualRoot};
use crate::diag::{
At, HintedStrResult, HintedString, SourceResult, StrResult, bail, error,
};
use crate::foundations::{Repr, Str, cast, func, scope, ty};
#[ty(scope, name = "path")]
#[derive(Debug, Clone, PartialEq, Hash)]
type RootedPath;
#[scope(ext)]
impl RootedPath {
#[func(constructor)]
pub fn construct(
path: Spanned<PathOrStr>,
) -> SourceResult<RootedPath> {
path.v.resolve_if_some(path.span.id()).at(path.span)
}
}
impl Repr for RootedPath {
fn repr(&self) -> EcoString {
eco_format!("path({})", self.vpath().get_with_slash().repr())
}
}
#[derive(Debug, Clone, PartialEq, Hash)]
pub enum PathOrStr {
Path(RootedPath),
Str(Str),
}
impl PathOrStr {
pub fn resolve(&self, within: FileId) -> HintedStrResult<RootedPath> {
Ok(match self {
PathOrStr::Path(v) => v.clone(),
PathOrStr::Str(v) => {
let root = within.root();
let base = within.vpath();
let resolved = match base.parent() {
Some(parent) => parent.join(v),
None => base.join(v),
}
.map_err(|err| format_resolve_error(err, root, v))?;
RootedPath::new(root.clone(), resolved)
}
})
}
pub fn resolve_if_some(&self, within: Option<FileId>) -> HintedStrResult<RootedPath> {
self.resolve(within.ok_or("cannot access file system from here")?)
}
}
cast! {
PathOrStr,
self => match self {
Self::Path(v) => v.into_value(),
Self::Str(v) => v.into_value(),
},
v: RootedPath => Self::Path(v),
v: Str => Self::Str(v),
}
fn format_resolve_error(err: PathError, root: &VirtualRoot, path: &str) -> HintedString {
match err {
PathError::Escapes => {
let kind = match root {
VirtualRoot::Project => "project",
VirtualRoot::Package(_) => "package",
};
let mut diag = error!(
"path `{}` would escape the {kind} root", path.repr();
hint: "cannot access files outside of the {kind} sandbox";
);
if *root == VirtualRoot::Project {
diag.hint("you can adjust the project root with the `--root` argument");
}
diag
}
PathError::Backslash => error!(
"path must not contain a backslash";
hint: "use forward slashes instead: `{}`",
path.replace("\\", "/").repr();
hint: "in earlier Typst versions, backslashes indicated path separators on Windows";
hint: "this behavior is no longer supported as it is not portable";
),
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct BundlePath(VirtualPath);
impl BundlePath {
pub fn new(path: VirtualPath) -> StrResult<Self> {
if path.is_root() {
bail!("path must have at least one component");
}
Ok(Self(path))
}
pub fn into_inner(self) -> VirtualPath {
self.0
}
}
impl AsRef<VirtualPath> for BundlePath {
fn as_ref(&self) -> &VirtualPath {
&self.0
}
}
cast! {
BundlePath,
self => self.0.into_with_slash().into_value(),
v: Str => Self::new(
VirtualPath::new(&v).map_err(|err| format_bundle_error(err, &v))?
)?
}
fn format_bundle_error(err: PathError, path: &str) -> HintedString {
match err {
PathError::Escapes => {
error!("path `{}` would escape the bundle root", path.repr())
}
PathError::Backslash => error!(
"path must not contain a backslash";
hint: "use forward slashes instead: `{}`",
path.replace("\\", "/").repr();
),
}
}
#[cfg(test)]
mod tests {
use typst_syntax::{VirtualPath, VirtualRoot};
use super::*;
#[test]
fn test_resolve() {
let path =
|p| RootedPath::new(VirtualRoot::Project, VirtualPath::new(p).unwrap());
let id = |p| path(p).intern();
let id1 = id("src/main.typ");
let resolve = |s: &str| {
PathOrStr::Str(s.into())
.resolve(id1)
.map_err(|err| err.message().clone())
};
assert_eq!(resolve("works.bib"), Ok(path("src/works.bib")));
assert_eq!(resolve(""), Ok(path("/src")));
assert_eq!(resolve("."), Ok(path("/src")));
assert_eq!(resolve(".."), Ok(path("/")));
assert_eq!(
resolve("../.."),
Err("path `\"../..\"` would escape the project root".into())
);
assert_eq!(resolve("a\\b"), Err("path must not contain a backslash".into()));
}
}