Compare commits

...

9 Commits

Author SHA1 Message Date
Neven Villani
65907aa5ca
Merge 143732d2a82c057f9efe48895fc798e422894fec into af2253ba16dfdc731e787e3a43a6f6a63ea65e0a 2025-07-22 13:18:45 +02:00
Neven Villani
143732d2a8 One comment out of date 2025-06-24 14:42:23 +02:00
Neven Villani
efe81a1aae
Merge branch 'main' into main 2025-06-12 16:06:33 +02:00
Neven Villani
3b5ee3c488 Update in tests/ too the PathBuf in FileError 2025-06-12 15:54:09 +02:00
Neven Villani
f3d5cdc6a5 Report path on a FileError 2025-06-12 15:47:47 +02:00
Neven Villani
85e03abe45 Improve messages
- restore ability to override a package locally
- cut down on some repeated logic
- more detailed errors
2025-06-12 13:36:13 +02:00
Neven Villani
c354369b05 What's with the namespacing on the unit tests ? 2025-06-11 22:43:14 +02:00
Neven Villani
91ec946283 first attempt 2025-06-11 22:32:10 +02:00
Neven Villani
a5ef75a9c2 I want to get all of this working 2025-06-11 22:31:56 +02:00
6 changed files with 101 additions and 42 deletions

View File

@ -404,7 +404,9 @@ fn system_path(
// Join the path to the root. If it tries to escape, deny // Join the path to the root. If it tries to escape, deny
// access. Note: It can still escape via symlinks. // access. Note: It can still escape via symlinks.
id.vpath().resolve(root).ok_or(FileError::AccessDenied) id.vpath()
.resolve(root)
.ok_or_else(|| FileError::AccessDenied(id.vpath().as_rootless_path().into()))
} }
/// Reads a file from a `FileId`. /// Reads a file from a `FileId`.
@ -427,7 +429,7 @@ fn read(
fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> { fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> {
let f = |e| FileError::from_io(e, path); let f = |e| FileError::from_io(e, path);
if fs::metadata(path).map_err(f)?.is_dir() { if fs::metadata(path).map_err(f)?.is_dir() {
Err(FileError::IsDirectory) Err(FileError::IsDirectory(path.into()))
} else { } else {
fs::read(path).map_err(f) fs::read(path).map_err(f)
} }

View File

@ -87,6 +87,7 @@ impl PackageStorage {
) -> PackageResult<PathBuf> { ) -> PackageResult<PathBuf> {
let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version); let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
// By default, search for the package locally
if let Some(packages_dir) = &self.package_path { if let Some(packages_dir) = &self.package_path {
let dir = packages_dir.join(&subdir); let dir = packages_dir.join(&subdir);
if dir.exists() { if dir.exists() {
@ -94,6 +95,7 @@ impl PackageStorage {
} }
} }
// As a fallback, look into the cache and possibly download from network.
if let Some(cache_dir) = &self.package_cache_path { if let Some(cache_dir) = &self.package_cache_path {
let dir = cache_dir.join(&subdir); let dir = cache_dir.join(&subdir);
if dir.exists() { if dir.exists() {
@ -102,14 +104,42 @@ impl PackageStorage {
// Download from network if it doesn't exist yet. // Download from network if it doesn't exist yet.
if spec.namespace == DEFAULT_NAMESPACE { if spec.namespace == DEFAULT_NAMESPACE {
self.download_package(spec, cache_dir, progress)?; return self.download_package(spec, cache_dir, progress);
if dir.exists() {
return Ok(dir);
}
} }
} }
Err(PackageError::NotFound(spec.clone())) // None of the strategies above found the package, so all code paths
// from now on fail. The rest of the function is only to determine the
// cause of the failure.
// We try `namespace/` then `namespace/name/` then `namespace/name/version/`
// and see where the first error occurs.
let not_found = |msg| Err(PackageError::NotFound(spec.clone(), msg));
let Some(packages_dir) = &self.package_path else {
return not_found(eco_format!("cannot access local package storage"));
};
let namespace_dir = packages_dir.join(format!("{}", spec.namespace));
if !namespace_dir.exists() {
return not_found(eco_format!(
"namespace @{} should be located at {}",
spec.namespace,
namespace_dir.display()
));
}
let package_dir = namespace_dir.join(format!("{}", spec.name));
if !package_dir.exists() {
return not_found(eco_format!(
"{} does not have package '{}'",
namespace_dir.display(),
spec.name
));
}
let latest = self.determine_latest_version(&spec.versionless()).ok();
Err(PackageError::VersionNotFound(
spec.clone(),
latest,
eco_format!("{}", namespace_dir.display()),
))
} }
/// Tries to determine the latest version of a package. /// Tries to determine the latest version of a package.
@ -171,21 +201,29 @@ impl PackageStorage {
spec: &PackageSpec, spec: &PackageSpec,
cache_dir: &Path, cache_dir: &Path,
progress: &mut dyn Progress, progress: &mut dyn Progress,
) -> PackageResult<()> { ) -> PackageResult<PathBuf> {
assert_eq!(spec.namespace, DEFAULT_NAMESPACE); assert_eq!(spec.namespace, DEFAULT_NAMESPACE);
let url = format!( let namespace_url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}");
"{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/{}-{}.tar.gz", let url = format!("{namespace_url}/{}-{}.tar.gz", spec.name, spec.version);
spec.name, spec.version
);
let data = match self.downloader.download_with_progress(&url, progress) { let data = match self.downloader.download_with_progress(&url, progress) {
Ok(data) => data, Ok(data) => data,
Err(ureq::Error::Status(404, _)) => { Err(ureq::Error::Status(404, _)) => {
if let Ok(version) = self.determine_latest_version(&spec.versionless()) { if let Ok(version) = self.determine_latest_version(&spec.versionless()) {
return Err(PackageError::VersionNotFound(spec.clone(), version)); return Err(PackageError::VersionNotFound(
spec.clone(),
Some(version),
eco_format!("{namespace_url}"),
));
} else { } else {
return Err(PackageError::NotFound(spec.clone())); return Err(PackageError::NotFound(
spec.clone(),
eco_format!(
"{namespace_url} does not have package '{}'",
spec.name
),
));
} }
} }
Err(err) => { Err(err) => {
@ -235,8 +273,8 @@ impl PackageStorage {
// broken packages still occur even with the rename safeguard, we might // broken packages still occur even with the rename safeguard, we might
// consider more complex solutions like file locking or checksums. // consider more complex solutions like file locking or checksums.
match fs::rename(&tempdir, &package_dir) { match fs::rename(&tempdir, &package_dir) {
Ok(()) => Ok(()), Ok(()) => Ok(package_dir),
Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(()), Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(package_dir),
Err(err) => Err(error("failed to move downloaded package directory", err)), Err(err) => Err(error("failed to move downloaded package directory", err)),
} }
} }

View File

@ -439,13 +439,13 @@ pub enum FileError {
/// A file was not found at this path. /// A file was not found at this path.
NotFound(PathBuf), NotFound(PathBuf),
/// A file could not be accessed. /// A file could not be accessed.
AccessDenied, AccessDenied(PathBuf),
/// A directory was found, but a file was expected. /// A directory was found, but a file was expected.
IsDirectory, IsDirectory(PathBuf),
/// The file is not a Typst source file, but should have been. /// The file is not a Typst source file, but should have been.
NotSource, NotSource(PathBuf),
/// The file was not valid UTF-8, but should have been. /// The file was not valid UTF-8, but should have been.
InvalidUtf8, InvalidUtf8(Option<PathBuf>),
/// The package the file is part of could not be loaded. /// The package the file is part of could not be loaded.
Package(PackageError), Package(PackageError),
/// Another error. /// Another error.
@ -459,11 +459,11 @@ impl FileError {
pub fn from_io(err: io::Error, path: &Path) -> Self { pub fn from_io(err: io::Error, path: &Path) -> Self {
match err.kind() { match err.kind() {
io::ErrorKind::NotFound => Self::NotFound(path.into()), io::ErrorKind::NotFound => Self::NotFound(path.into()),
io::ErrorKind::PermissionDenied => Self::AccessDenied, io::ErrorKind::PermissionDenied => Self::AccessDenied(path.into()),
io::ErrorKind::InvalidData io::ErrorKind::InvalidData
if err.to_string().contains("stream did not contain valid UTF-8") => if err.to_string().contains("stream did not contain valid UTF-8") =>
{ {
Self::InvalidUtf8 Self::InvalidUtf8(Some(path.into()))
} }
_ => Self::Other(Some(eco_format!("{err}"))), _ => Self::Other(Some(eco_format!("{err}"))),
} }
@ -478,10 +478,19 @@ impl Display for FileError {
Self::NotFound(path) => { Self::NotFound(path) => {
write!(f, "file not found (searched at {})", path.display()) write!(f, "file not found (searched at {})", path.display())
} }
Self::AccessDenied => f.pad("failed to load file (access denied)"), Self::AccessDenied(path) => {
Self::IsDirectory => f.pad("failed to load file (is a directory)"), write!(f, "failed to load file {} (access denied)", path.display())
Self::NotSource => f.pad("not a typst source file"), }
Self::InvalidUtf8 => f.pad("file is not valid utf-8"), Self::IsDirectory(path) => {
write!(f, "failed to load file {} (is a directory)", path.display())
}
Self::NotSource(path) => {
write!(f, "{} is not a typst source file", path.display())
}
Self::InvalidUtf8(Some(path)) => {
write!(f, "file {} is not valid utf-8", path.display())
}
Self::InvalidUtf8(None) => f.pad("file is not valid utf-8"),
Self::Package(error) => error.fmt(f), Self::Package(error) => error.fmt(f),
Self::Other(Some(err)) => write!(f, "failed to load file ({err})"), Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
Self::Other(None) => f.pad("failed to load file"), Self::Other(None) => f.pad("failed to load file"),
@ -491,13 +500,13 @@ impl Display for FileError {
impl From<Utf8Error> for FileError { impl From<Utf8Error> for FileError {
fn from(_: Utf8Error) -> Self { fn from(_: Utf8Error) -> Self {
Self::InvalidUtf8 Self::InvalidUtf8(None)
} }
} }
impl From<FromUtf8Error> for FileError { impl From<FromUtf8Error> for FileError {
fn from(_: FromUtf8Error) -> Self { fn from(_: FromUtf8Error) -> Self {
Self::InvalidUtf8 Self::InvalidUtf8(None)
} }
} }
@ -518,13 +527,15 @@ pub type PackageResult<T> = Result<T, PackageError>;
/// An error that occurred while trying to load a package. /// An error that occurred while trying to load a package.
/// ///
/// Some variants have an optional string can give more details, if available. /// Some variants have an optional string that can give more details, if available.
#[derive(Debug, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum PackageError { pub enum PackageError {
/// The specified package does not exist. /// The specified package does not exist.
NotFound(PackageSpec), /// Additionally provides information on where we tried to find the package.
NotFound(PackageSpec, EcoString),
/// The specified package found, but the version does not exist. /// The specified package found, but the version does not exist.
VersionNotFound(PackageSpec, PackageVersion), /// TODO: make the registry part of the error better typed
VersionNotFound(PackageSpec, Option<PackageVersion>, EcoString),
/// Failed to retrieve the package through the network. /// Failed to retrieve the package through the network.
NetworkFailed(Option<EcoString>), NetworkFailed(Option<EcoString>),
/// The package archive was malformed. /// The package archive was malformed.
@ -538,15 +549,20 @@ impl std::error::Error for PackageError {}
impl Display for PackageError { impl Display for PackageError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Self::NotFound(spec) => { Self::NotFound(spec, detail) => {
write!(f, "package not found (searched for {spec})",) write!(f, "package not found: {detail} (searching for {spec})",)
} }
Self::VersionNotFound(spec, latest) => { Self::VersionNotFound(spec, latest, registry) => {
write!( write!(
f, f,
"package found, but version {} does not exist (latest is {})", "package '{}' found, but version {} does not exist",
spec.version, latest, spec.name, spec.version
) )?;
if let Some(version) = latest {
write!(f, " (latest version provided by {registry} is {version})")
} else {
write!(f, " ({registry} contains no versions for this package)")
}
} }
Self::NetworkFailed(Some(err)) => { Self::NetworkFailed(Some(err)) => {
write!(f, "failed to download package ({err})") write!(f, "failed to download package ({err})")

View File

@ -191,7 +191,7 @@ fn hint_invalid_main_file(
file_error: FileError, file_error: FileError,
input: FileId, input: FileId,
) -> EcoVec<SourceDiagnostic> { ) -> EcoVec<SourceDiagnostic> {
let is_utf8_error = matches!(file_error, FileError::InvalidUtf8); let is_utf8_error = matches!(file_error, FileError::InvalidUtf8(_));
let mut diagnostic = let mut diagnostic =
SourceDiagnostic::error(Span::detached(), EcoString::from(file_error)); SourceDiagnostic::error(Span::detached(), EcoString::from(file_error));

View File

@ -172,7 +172,9 @@ pub(crate) fn system_path(id: FileId) -> FileResult<PathBuf> {
None => PathBuf::new(), None => PathBuf::new(),
}; };
id.vpath().resolve(&root).ok_or(FileError::AccessDenied) id.vpath()
.resolve(&root)
.ok_or_else(|| FileError::AccessDenied(id.vpath().as_rootless_path().into()))
} }
/// Read a file. /// Read a file.
@ -186,7 +188,7 @@ pub(crate) fn read(path: &Path) -> FileResult<Cow<'static, [u8]>> {
let f = |e| FileError::from_io(e, path); let f = |e| FileError::from_io(e, path);
if fs::metadata(path).map_err(f)?.is_dir() { if fs::metadata(path).map_err(f)?.is_dir() {
Err(FileError::IsDirectory) Err(FileError::IsDirectory(path.into()))
} else { } else {
fs::read(path).map(Cow::Owned).map_err(f) fs::read(path).map(Cow::Owned).map_err(f)
} }

View File

@ -302,11 +302,11 @@
#import 5 as x #import 5 as x
--- import-from-string-invalid --- --- import-from-string-invalid ---
// Error: 9-11 failed to load file (is a directory) // Error: 9-11 failed to load file tests/suite/scripting (is a directory)
#import "": name #import "": name
--- import-from-string-renamed-invalid --- --- import-from-string-renamed-invalid ---
// Error: 9-11 failed to load file (is a directory) // Error: 9-11 failed to load file tests/suite/scripting (is a directory)
#import "" as x #import "" as x
--- import-file-not-found-invalid --- --- import-file-not-found-invalid ---
@ -483,3 +483,4 @@ This is never reached.
--- import-from-file-package-lookalike --- --- import-from-file-package-lookalike ---
// Error: 9-28 file not found (searched at tests/suite/scripting/#test/mypkg:1.0.0) // Error: 9-28 file not found (searched at tests/suite/scripting/#test/mypkg:1.0.0)
#import "#test/mypkg:1.0.0": * #import "#test/mypkg:1.0.0": *