Compare commits

...

12 Commits

Author SHA1 Message Date
Neven Villani
e0de716926
Merge 5d792c46ca8a5006ecc19de515fe5bd952797da0 into e9f1b5825a9d37ca0c173a7b2830ba36a27ca9e0 2025-07-24 13:48:52 +02:00
Laurenz
e9f1b5825a
Lint for iterations over hash types (#6652) 2025-07-24 11:34:08 +00:00
Neven Villani
5d792c46ca Fix local issue with Rust version 2025-07-24 10:10:39 +02:00
Neven Villani
1617578a06
Merge branch 'main' into main 2025-07-24 10:02:30 +02:00
Neven Villani
26f0a1358c Move parts of the messages to hints 2025-07-24 09:30:00 +02:00
Neven Villani
3b84ce91e3 One comment out of date 2025-07-24 09:28:22 +02:00
Neven Villani
ddaec8aa85 Update in tests/ too the PathBuf in FileError 2025-07-24 09:28:22 +02:00
Neven Villani
3846955ce6 Report path on a FileError 2025-07-24 09:28:22 +02:00
Neven Villani
d148f3ebe6 those were temporary files 2025-07-24 09:28:16 +02:00
Neven Villani
3292aeaaa1 What's with the namespacing on the unit tests ? 2025-07-24 09:26:50 +02:00
Neven Villani
407b006dc6 first attempt 2025-07-24 09:26:50 +02:00
Neven Villani
7089a79b1e I want to get all of this working 2025-07-24 09:26:50 +02:00
9 changed files with 192 additions and 88 deletions

View File

@ -159,6 +159,7 @@ strip = true
[workspace.lints.clippy]
blocks_in_conditions = "allow"
comparison_chain = "allow"
iter_over_hash_type = "warn"
manual_range_contains = "allow"
mutable_key_type = "allow"
uninlined_format_args = "warn"

View File

@ -29,8 +29,9 @@ pub fn init(command: &InitCommand) -> StrResult<()> {
})?;
// Find or download the package.
let package_path =
package_storage.prepare_package(&spec, &mut PrintDownload(&spec))?;
let package_path = package_storage
.prepare_package(&spec, &mut PrintDownload(&spec))
.map_err(|e| eco_format!("{e}"))?;
// Parse the manifest.
let manifest = parse_manifest(&package_path)?;

View File

@ -139,6 +139,7 @@ impl Watcher {
fn update(&mut self, iter: impl IntoIterator<Item = PathBuf>) -> StrResult<()> {
// Mark all files as not "seen" so that we may unwatch them if they
// aren't in the dependency list.
#[allow(clippy::iter_over_hash_type, reason = "order does not matter")]
for seen in self.watched.values_mut() {
*seen = false;
}

View File

@ -173,6 +173,7 @@ impl SystemWorld {
/// Reset the compilation state in preparation of a new compilation.
pub fn reset(&mut self) {
#[allow(clippy::iter_over_hash_type, reason = "order does not matter")]
for slot in self.slots.get_mut().values_mut() {
slot.reset();
}
@ -404,7 +405,9 @@ fn system_path(
// Join the path to the root. If it tries to escape, deny
// 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`.
@ -427,7 +430,7 @@ fn read(
fn read_from_disk(path: &Path) -> FileResult<Vec<u8>> {
let f = |e| FileError::from_io(e, path);
if fs::metadata(path).map_err(f)?.is_dir() {
Err(FileError::IsDirectory)
Err(FileError::IsDirectory(path.into()))
} else {
fs::read(path).map_err(f)
}

View File

@ -87,6 +87,7 @@ impl PackageStorage {
) -> PackageResult<PathBuf> {
let subdir = format!("{}/{}/{}", spec.namespace, spec.name, spec.version);
// By default, search for the package locally
if let Some(packages_dir) = &self.package_path {
let dir = packages_dir.join(&subdir);
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 {
let dir = cache_dir.join(&subdir);
if dir.exists() {
@ -102,14 +104,42 @@ impl PackageStorage {
// Download from network if it doesn't exist yet.
if spec.namespace == DEFAULT_NAMESPACE {
self.download_package(spec, cache_dir, progress)?;
if dir.exists() {
return Ok(dir);
}
return self.download_package(spec, cache_dir, progress);
}
}
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!(
"the 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!(
"the registry at {} 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.
@ -171,21 +201,29 @@ impl PackageStorage {
spec: &PackageSpec,
cache_dir: &Path,
progress: &mut dyn Progress,
) -> PackageResult<()> {
) -> PackageResult<PathBuf> {
assert_eq!(spec.namespace, DEFAULT_NAMESPACE);
let url = format!(
"{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/{}-{}.tar.gz",
spec.name, spec.version
);
let namespace_url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}");
let url = format!("{namespace_url}/{}-{}.tar.gz", spec.name, spec.version);
let data = match self.downloader.download_with_progress(&url, progress) {
Ok(data) => data,
Err(ureq::Error::Status(404, _)) => {
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 {
return Err(PackageError::NotFound(spec.clone()));
return Err(PackageError::NotFound(
spec.clone(),
eco_format!(
"the registry at {namespace_url} does not have package '{}'",
spec.name
),
));
}
}
Err(err) => {
@ -235,8 +273,8 @@ impl PackageStorage {
// broken packages still occur even with the rename safeguard, we might
// consider more complex solutions like file locking or checksums.
match fs::rename(&tempdir, &package_dir) {
Ok(()) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(()),
Ok(()) => Ok(package_dir),
Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(package_dir),
Err(err) => Err(error("failed to move downloaded package directory", err)),
}
}

View File

@ -314,6 +314,24 @@ impl<T> Trace<T> for SourceResult<T> {
/// A result type with a string error message.
pub type StrResult<T> = Result<T, EcoString>;
/// A common trait to add a span to any error
pub trait ErrAt {
/// Attach a span to a more specialized error and turn it into
/// a generic error, optionally with hints.
fn err_at(self, span: Span) -> SourceDiagnostic;
}
/// Blanket implementation for anything that doesn't implement its own
/// convertion to a SourceDiagnostic.
impl<S> ErrAt for S
where
S: Into<EcoString>,
{
fn err_at(self, span: Span) -> SourceDiagnostic {
SourceDiagnostic::error(span, self)
}
}
/// Convert a [`StrResult`] or [`HintedStrResult`] to a [`SourceResult`] by
/// adding span information.
pub trait At<T> {
@ -323,18 +341,10 @@ pub trait At<T> {
impl<T, S> At<T> for Result<T, S>
where
S: Into<EcoString>,
S: ErrAt,
{
fn at(self, span: Span) -> SourceResult<T> {
self.map_err(|message| {
let mut diagnostic = SourceDiagnostic::error(span, message);
if diagnostic.message.contains("(access denied)") {
diagnostic.hint("cannot read file outside of project root");
diagnostic
.hint("you can adjust the project root with the --root argument");
}
eco_vec![diagnostic]
})
self.map_err(|e| eco_vec![e.err_at(span)])
}
}
@ -439,13 +449,13 @@ pub enum FileError {
/// A file was not found at this path.
NotFound(PathBuf),
/// A file could not be accessed.
AccessDenied,
AccessDenied(PathBuf),
/// A directory was found, but a file was expected.
IsDirectory,
IsDirectory(PathBuf),
/// 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.
InvalidUtf8,
InvalidUtf8(Option<PathBuf>),
/// The package the file is part of could not be loaded.
Package(PackageError),
/// Another error.
@ -459,15 +469,38 @@ impl FileError {
pub fn from_io(err: io::Error, path: &Path) -> Self {
match err.kind() {
io::ErrorKind::NotFound => Self::NotFound(path.into()),
io::ErrorKind::PermissionDenied => Self::AccessDenied,
io::ErrorKind::PermissionDenied => Self::AccessDenied(path.into()),
io::ErrorKind::InvalidData
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}"))),
}
}
fn write_hints(&self) -> EcoVec<EcoString> {
match self {
Self::NotFound(_) => eco_vec![],
Self::AccessDenied(_) => {
eco_vec![
eco_format!("cannot read file outside of project root"),
eco_format!(
"you can adjust the project root with the --root argument"
),
]
}
Self::IsDirectory(_) => eco_vec![],
Self::NotSource(_) => eco_vec![],
Self::InvalidUtf8(Some(path)) => {
eco_vec![eco_format!("tried to read {}", path.display())]
}
Self::InvalidUtf8(None) => eco_vec![],
Self::Package(error) => error.write_hints(),
Self::Other(Some(err)) => eco_vec![eco_format!("{err}")],
Self::Other(None) => eco_vec![],
}
}
}
impl std::error::Error for FileError {}
@ -478,26 +511,40 @@ impl Display for FileError {
Self::NotFound(path) => {
write!(f, "file not found (searched at {})", path.display())
}
Self::AccessDenied => f.pad("failed to load file (access denied)"),
Self::IsDirectory => f.pad("failed to load file (is a directory)"),
Self::NotSource => f.pad("not a typst source file"),
Self::InvalidUtf8 => f.pad("file is not valid utf-8"),
Self::Package(error) => error.fmt(f),
Self::Other(Some(err)) => write!(f, "failed to load file ({err})"),
Self::Other(None) => f.pad("failed to load file"),
Self::NotSource(path) => {
write!(f, "{} is not a typst source file", path.display())
}
Self::InvalidUtf8(_) => write!(f, "file is not valid utf-8"),
Self::IsDirectory(path) => {
write!(f, "failed to load file {} (is a directory)", path.display())
}
Self::AccessDenied(path) => {
write!(f, "failed to load file {} (access denied)", path.display())
}
Self::Other(_) => {
write!(f, "failed to load file")
}
Self::Package(error) => write!(f, "{error}"),
}
}
}
impl ErrAt for FileError {
fn err_at(self, span: Span) -> SourceDiagnostic {
let hints = self.write_hints();
SourceDiagnostic::error(span, eco_format!("{}", self)).with_hints(hints)
}
}
impl From<Utf8Error> for FileError {
fn from(_: Utf8Error) -> Self {
Self::InvalidUtf8
Self::InvalidUtf8(None)
}
}
impl From<FromUtf8Error> for FileError {
fn from(_: FromUtf8Error) -> Self {
Self::InvalidUtf8
Self::InvalidUtf8(None)
}
}
@ -507,24 +554,20 @@ impl From<PackageError> for FileError {
}
}
impl From<FileError> for EcoString {
fn from(err: FileError) -> Self {
eco_format!("{err}")
}
}
/// A result type with a package-related error.
pub type PackageResult<T> = Result<T, PackageError>;
/// 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)]
pub enum PackageError {
/// 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.
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.
NetworkFailed(Option<EcoString>),
/// The package archive was malformed.
@ -533,40 +576,55 @@ pub enum PackageError {
Other(Option<EcoString>),
}
impl PackageError {
fn write_hints(&self) -> EcoVec<EcoString> {
match self {
Self::NotFound(_, detail) => {
eco_vec![eco_format!("{detail}")]
}
Self::VersionNotFound(spec, latest, registry) => {
if let Some(version) = latest {
eco_vec![
eco_format!(
"the package {} exists, but not with version {}",
spec.name,
spec.version
),
eco_format!(
"the registry at {registry} provides up to version {version}"
),
]
} else {
eco_vec![eco_format!(
"the registry at {registry} contains no versions for this package"
)]
}
}
Self::NetworkFailed(Some(err)) => eco_vec![eco_format!("{err}")],
Self::NetworkFailed(None) => eco_vec![],
Self::MalformedArchive(Some(err)) => eco_vec![eco_format!("{err}")],
Self::MalformedArchive(None) => {
eco_vec![eco_format!("the archive is malformed")]
}
Self::Other(Some(err)) => eco_vec![eco_format!("{err}")],
Self::Other(None) => eco_vec![],
}
}
}
impl std::error::Error for PackageError {}
impl Display for PackageError {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Self::NotFound(spec) => {
write!(f, "package not found (searched for {spec})",)
Self::NotFound(spec, _) => write!(f, "package {spec} not found"),
Self::VersionNotFound(spec, _, _) => {
write!(f, "package version {spec} not found")
}
Self::VersionNotFound(spec, latest) => {
write!(
f,
"package found, but version {} does not exist (latest is {})",
spec.version, latest,
)
Self::NetworkFailed(_) => write!(f, "failed to download package"),
Self::MalformedArchive(_) => write!(f, "failed to decompress package"),
Self::Other(_) => f.pad("failed to load package"),
}
Self::NetworkFailed(Some(err)) => {
write!(f, "failed to download package ({err})")
}
Self::NetworkFailed(None) => f.pad("failed to download package"),
Self::MalformedArchive(Some(err)) => {
write!(f, "failed to decompress package ({err})")
}
Self::MalformedArchive(None) => {
f.pad("failed to decompress package (archive malformed)")
}
Self::Other(Some(err)) => write!(f, "failed to load package ({err})"),
Self::Other(None) => f.pad("failed to load package"),
}
}
}
impl From<PackageError> for EcoString {
fn from(err: PackageError) -> Self {
eco_format!("{err}")
}
}

View File

@ -42,10 +42,10 @@ use std::collections::HashSet;
use std::sync::LazyLock;
use comemo::{Track, Tracked, Validate};
use ecow::{EcoString, EcoVec, eco_format, eco_vec};
use ecow::{EcoVec, eco_format, eco_vec};
use typst_html::HtmlDocument;
use typst_library::diag::{
FileError, SourceDiagnostic, SourceResult, Warned, bail, warning,
ErrAt, FileError, SourceDiagnostic, SourceResult, Warned, bail, warning,
};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{NativeRuleMap, StyleChain, Styles, Value};
@ -191,9 +191,8 @@ fn hint_invalid_main_file(
file_error: FileError,
input: FileId,
) -> EcoVec<SourceDiagnostic> {
let is_utf8_error = matches!(file_error, FileError::InvalidUtf8);
let mut diagnostic =
SourceDiagnostic::error(Span::detached(), EcoString::from(file_error));
let is_utf8_error = matches!(file_error, FileError::InvalidUtf8(_));
let mut diagnostic = file_error.err_at(Span::detached());
// Attempt to provide helpful hints for UTF-8 errors. Perhaps the user
// mistyped the filename. For example, they could have written "file.pdf"

View File

@ -172,7 +172,9 @@ pub(crate) fn system_path(id: FileId) -> FileResult<PathBuf> {
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.
@ -186,7 +188,7 @@ pub(crate) fn read(path: &Path) -> FileResult<Cow<'static, [u8]>> {
let f = |e| FileError::from_io(e, path);
if fs::metadata(path).map_err(f)?.is_dir() {
Err(FileError::IsDirectory)
Err(FileError::IsDirectory(path.into()))
} else {
fs::read(path).map(Cow::Owned).map_err(f)
}

View File

@ -302,11 +302,11 @@
#import 5 as x
--- 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-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-file-not-found-invalid ---
@ -483,3 +483,4 @@ This is never reached.
--- import-from-file-package-lookalike ---
// Error: 9-28 file not found (searched at tests/suite/scripting/#test/mypkg:1.0.0)
#import "#test/mypkg:1.0.0": *