Replace anyhow with custom RepositoryError for improved error specificity and consistency. Remove anyhow dependency.

This commit is contained in:
Till Wegmueller 2025-07-26 15:33:39 +02:00
parent 40560e5960
commit 4649608408
No known key found for this signature in database
15 changed files with 518 additions and 868 deletions

2
.gitignore vendored
View file

@ -88,7 +88,7 @@ Cargo.lock
**/*.rs.bk
# Prototype directory
sample_data/**/build/prototype/i386
sample_data
.vagrant
/.output.txt

847
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -17,8 +17,9 @@ keywords = ["packaging", "illumos"]
[dependencies]
regex = "1.5.5"
anyhow = "1.0.56"
thiserror = "1.0.30"
thiserror = "1.0.50"
miette = "7.6.0"
tracing = "0.1.37"
maplit = "0.1.6"
object = "0.23.0"
sha2 = "0.9.3"
@ -32,6 +33,5 @@ flate2 = "1.0.28"
lz4 = "1.24.0"
semver = { version = "1.0.20", features = ["serde"] }
diff-struct = "0.5.3"
searchy = "0.5.0"
tantivy = { version = "0.24.2", features = ["mmap"] }
chrono = "0.4.41"
tempfile = "3.20.0"

View file

@ -9,6 +9,7 @@ use crate::digest::Digest;
use crate::fmri::Fmri;
use crate::payload::{Payload, PayloadError};
use diff::Diff;
use miette::Diagnostic;
use pest::Parser;
use pest_derive::Parser;
use serde::{Deserialize, Serialize};
@ -22,21 +23,29 @@ use thiserror::Error;
type Result<T> = StdResult<T, ActionError>;
#[derive(Debug, Error)]
#[derive(Debug, Error, Diagnostic)]
pub enum ActionError {
#[error(transparent)]
#[diagnostic(transparent)]
PayloadError(#[from] PayloadError),
#[error(transparent)]
#[diagnostic(transparent)]
FileError(#[from] FileError),
#[error("value {0} is not a boolean")]
#[diagnostic(
code(ips::action_error::invalid_boolean),
help("Boolean values must be 'true', 'false', 't', or 'f'.")
)]
NotBooleanValue(String),
#[error(transparent)]
#[diagnostic(code(ips::action_error::io))]
IOError(#[from] std::io::Error),
#[error(transparent)]
#[diagnostic(code(ips::action_error::parser))]
ParserError(#[from] pest::error::Error<Rule>),
}
@ -256,9 +265,13 @@ impl FacetedAction for File {
}
}
#[derive(Debug, Error)]
#[derive(Debug, Error, Diagnostic)]
pub enum FileError {
#[error("file path is not a string")]
#[diagnostic(
code(ips::action_error::file::invalid_path),
help("The file path must be convertible to a valid string.")
)]
FilePathIsNoStringError,
}
@ -538,7 +551,15 @@ impl Manifest {
pub fn parse_file<P: AsRef<Path>>(f: P) -> Result<Manifest> {
let content = read_to_string(f)?;
Manifest::parse_string(content)
// Try to parse as JSON first
match serde_json::from_str::<Manifest>(&content) {
Ok(manifest) => Ok(manifest),
Err(_) => {
// If JSON parsing fails, fall back to string format
Manifest::parse_string(content)
}
}
}
pub fn parse_string(content: String) -> Result<Manifest> {
@ -630,11 +651,20 @@ impl Default for ActionKind {
}
//TODO Multierror and no failure for these cases
#[derive(Debug, Error)]
#[derive(Debug, Error, Diagnostic)]
pub enum ManifestError {
#[error("unknown action {action:?} at line {line:?}")]
#[diagnostic(
code(ips::action_error::manifest::unknown_action),
help("Check the action name and make sure it's one of the supported action types.")
)]
UnknownAction { line: usize, action: String },
#[error("action string \"{action:?}\" at line {line:?} is invalid: {message:?}")]
#[diagnostic(
code(ips::action_error::manifest::invalid_action),
help("Check the action format and fix the syntax error.")
)]
InvalidAction {
line: usize,
action: String,

View file

@ -4,6 +4,7 @@
// obtain one at https://mozilla.org/MPL/2.0/.
use diff::Diff;
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use sha2::Digest as Sha2Digest;
#[allow(unused_imports)]
@ -146,10 +147,19 @@ impl Display for Digest {
}
}
#[derive(Debug, Error)]
#[derive(Debug, Error, Diagnostic)]
pub enum DigestError {
#[error("hashing algorithm {algorithm:?} is not known by this library")]
#[diagnostic(
code(ips::digest_error::unknown_algorithm),
help("Use one of the supported algorithms: sha1, sha256t, sha512t, sha512t_256, sha3256t, sha3512t_256, sha3512t")
)]
UnknownAlgorithm { algorithm: String },
#[error("digest {digest:?} is not formatted properly: {details:?}")]
#[diagnostic(
code(ips::digest_error::invalid_format),
help("Digest should be in the format: source:algorithm:hash")
)]
InvalidDigestFormat { digest: String, details: String },
}

View file

@ -58,25 +58,55 @@
//! ```
use diff::Diff;
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
/// Errors that can occur when parsing an FMRI
#[derive(Debug, Error, PartialEq)]
#[derive(Debug, Error, Diagnostic, PartialEq)]
pub enum FmriError {
#[error("invalid FMRI format")]
#[diagnostic(
code(ips::fmri_error::invalid_format),
help("FMRI should be in the format: [scheme://][publisher/]name[@version]")
)]
InvalidFormat,
#[error("invalid version format")]
#[diagnostic(
code(ips::fmri_error::invalid_version_format),
help("Version should be in the format: release[,branch][-build][:timestamp]")
)]
InvalidVersionFormat,
#[error("invalid release format")]
#[diagnostic(
code(ips::fmri_error::invalid_release_format),
help("Release should be a dot-separated vector of digits (e.g., 5.11)")
)]
InvalidReleaseFormat,
#[error("invalid branch format")]
#[diagnostic(
code(ips::fmri_error::invalid_branch_format),
help("Branch should be a dot-separated vector of digits (e.g., 1)")
)]
InvalidBranchFormat,
#[error("invalid build format")]
#[diagnostic(
code(ips::fmri_error::invalid_build_format),
help("Build should be a dot-separated vector of digits (e.g., 2020.0.1.0)")
)]
InvalidBuildFormat,
#[error("invalid timestamp format")]
#[diagnostic(
code(ips::fmri_error::invalid_timestamp_format),
help("Timestamp should be a hexadecimal string (e.g., 20200421T195136Z)")
)]
InvalidTimestampFormat,
}

View file

@ -1,18 +1,27 @@
mod properties;
use properties::*;
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
#[derive(Debug, Error, Diagnostic)]
pub enum ImageError {
//Implement derives for IO error and serde_json error
#[error(transparent)]
#[error("I/O error: {0}")]
#[diagnostic(
code(ips::image_error::io),
help("Check system resources and permissions")
)]
IO(#[from] std::io::Error),
#[error(transparent)]
#[error("JSON error: {0}")]
#[diagnostic(
code(ips::image_error::json),
help("Check the JSON format and try again")
)]
Json(#[from] serde_json::Error),
}

View file

@ -10,6 +10,7 @@ pub mod fmri;
pub mod image;
pub mod payload;
pub mod repository;
mod test_json_manifest;
#[cfg(test)]
mod tests {

View file

@ -5,6 +5,7 @@
use crate::digest::{Digest, DigestAlgorithm, DigestError, DigestSource};
use diff::Diff;
use miette::Diagnostic;
use object::Object;
use serde::{Deserialize, Serialize};
use std::io::Error as IOError;
@ -14,11 +15,17 @@ use thiserror::Error;
type Result<T> = StdResult<T, PayloadError>;
#[derive(Debug, Error)]
#[derive(Debug, Error, Diagnostic)]
pub enum PayloadError {
#[error("io error: {0}")]
#[error("I/O error: {0}")]
#[diagnostic(
code(ips::payload_error::io),
help("Check system resources and permissions")
)]
IOError(#[from] IOError),
#[error("digest error: {0}")]
#[error(transparent)]
#[diagnostic(transparent)]
DigestError(#[from] DigestError),
}

View file

@ -3,15 +3,74 @@
// MPL was not distributed with this file, You can
// obtain one at https://mozilla.org/MPL/2.0/.
use anyhow::Result;
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use thiserror::Error;
use crate::fmri::Fmri;
/// Errors that can occur in catalog operations
#[derive(Debug, Error, Diagnostic)]
pub enum CatalogError {
#[error("catalog part does not exist: {name}")]
#[diagnostic(
code(ips::repository_error::catalog::part_not_found),
help("Check that the catalog part exists and is accessible")
)]
CatalogPartNotFound {
name: String,
},
#[error("catalog part not loaded: {name}")]
#[diagnostic(
code(ips::repository_error::catalog::part_not_loaded),
help("Load the catalog part before attempting to save it")
)]
CatalogPartNotLoaded {
name: String,
},
#[error("update log not loaded: {name}")]
#[diagnostic(
code(ips::repository_error::catalog::update_log_not_loaded),
help("Load the update log before attempting to save it")
)]
UpdateLogNotLoaded {
name: String,
},
#[error("update log does not exist: {name}")]
#[diagnostic(
code(ips::repository_error::catalog::update_log_not_found),
help("Check that the update log exists and is accessible")
)]
UpdateLogNotFound {
name: String,
},
#[error("failed to serialize JSON: {0}")]
#[diagnostic(
code(ips::repository_error::catalog::json_serialize),
help("This is likely a bug in the code")
)]
JsonSerializationError(#[from] serde_json::Error),
#[error("I/O error: {0}")]
#[diagnostic(
code(ips::repository_error::catalog::io),
help("Check system resources and permissions")
)]
IoError(#[from] io::Error),
}
/// Result type for catalog operations
pub type Result<T> = std::result::Result<T, CatalogError>;
/// Format a SystemTime as an ISO-8601 'basic format' date in UTC
fn format_iso8601_basic(time: &SystemTime) -> String {
let datetime = convert_system_time_to_datetime(time);
@ -393,7 +452,9 @@ impl CatalogManager {
self.parts.insert(name.to_string(), part);
Ok(())
} else {
Err(anyhow::anyhow!("Catalog part does not exist: {}", name))
Err(CatalogError::CatalogPartNotFound {
name: name.to_string(),
})
}
}
@ -404,7 +465,9 @@ impl CatalogManager {
part.save(&part_path)?;
Ok(())
} else {
Err(anyhow::anyhow!("Catalog part not loaded: {}", name))
Err(CatalogError::CatalogPartNotLoaded {
name: name.to_string(),
})
}
}
@ -453,7 +516,9 @@ impl CatalogManager {
Ok(())
} else {
Err(anyhow::anyhow!("Update log not loaded: {}", name))
Err(CatalogError::UpdateLogNotLoaded {
name: name.to_string(),
})
}
}
@ -465,7 +530,9 @@ impl CatalogManager {
self.update_logs.insert(name.to_string(), log);
Ok(())
} else {
Err(anyhow::anyhow!("Update log does not exist: {}", name))
Err(CatalogError::UpdateLogNotFound {
name: name.to_string(),
})
}
}

View file

@ -3,7 +3,7 @@
// MPL was not distributed with this file, You can
// obtain one at https://mozilla.org/MPL/2.0/.
use anyhow::{anyhow, Result};
use super::{Result, RepositoryError};
use flate2::write::GzEncoder;
use flate2::Compression as GzipCompression;
use lz4::EncoderBuilder;
@ -315,12 +315,12 @@ impl Transaction {
if temp_file_path.exists() {
// If it exists, remove it to avoid any issues with existing content
fs::remove_file(&temp_file_path)
.map_err(|e| anyhow!("Failed to remove existing temp file: {}", e))?;
.map_err(|e| RepositoryError::FileWriteError(format!("Failed to remove existing temp file: {}", e)))?;
}
// Read the file content
let file_content = fs::read(file_path)
.map_err(|e| anyhow!("Failed to read file {}: {}", file_path.display(), e))?;
.map_err(|e| RepositoryError::FileReadError(format!("Failed to read file {}: {}", file_path.display(), e)))?;
// Create a payload with the hash information if it doesn't exist
let mut updated_file_action = file_action;
@ -338,16 +338,16 @@ impl Transaction {
// Write the file content to the encoder
encoder
.write_all(&file_content)
.map_err(|e| anyhow!("Failed to write data to Gzip encoder: {}", e))?;
.map_err(|e| RepositoryError::Other(format!("Failed to write data to Gzip encoder: {}", e)))?;
// Finish the compression and get the compressed data
let compressed_data = encoder
.finish()
.map_err(|e| anyhow!("Failed to finish Gzip compression: {}", e))?;
.map_err(|e| RepositoryError::Other(format!("Failed to finish Gzip compression: {}", e)))?;
// Write the compressed data to the temp file
fs::write(&temp_file_path, &compressed_data)
.map_err(|e| anyhow!("Failed to write compressed data to temp file: {}", e))?;
.map_err(|e| RepositoryError::FileWriteError(format!("Failed to write compressed data to temp file: {}", e)))?;
// Calculate hash of the compressed data
let mut hasher = Sha256::new();
@ -358,19 +358,19 @@ impl Transaction {
// Create an LZ4 encoder with default compression level
let mut encoder = EncoderBuilder::new()
.build(Vec::new())
.map_err(|e| anyhow!("Failed to create LZ4 encoder: {}", e))?;
.map_err(|e| RepositoryError::Other(format!("Failed to create LZ4 encoder: {}", e)))?;
// Write the file content to the encoder
encoder
.write_all(&file_content)
.map_err(|e| anyhow!("Failed to write data to LZ4 encoder: {}", e))?;
.map_err(|e| RepositoryError::Other(format!("Failed to write data to LZ4 encoder: {}", e)))?;
// Finish the compression and get the compressed data
let (compressed_data, _) = encoder.finish();
// Write the compressed data to the temp file
fs::write(&temp_file_path, &compressed_data).map_err(|e| {
anyhow!("Failed to write LZ4 compressed data to temp file: {}", e)
RepositoryError::FileWriteError(format!("Failed to write LZ4 compressed data to temp file: {}", e))
})?;
// Calculate hash of the compressed data
@ -510,7 +510,7 @@ impl ReadableRepository for FileBackend {
// Check if the repository directory exists
if !path.exists() {
return Err(anyhow!("Repository does not exist: {}", path.display()));
return Err(RepositoryError::NotFound(path.display().to_string()));
}
// Load the repository configuration
@ -595,7 +595,7 @@ impl ReadableRepository for FileBackend {
// Filter publishers if specified
let publishers = if let Some(pub_name) = publisher {
if !self.config.publishers.contains(&pub_name.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", pub_name));
return Err(RepositoryError::PublisherNotFound(pub_name.to_string()));
}
vec![pub_name.to_string()]
} else {
@ -611,9 +611,8 @@ impl ReadableRepository for FileBackend {
if publisher_pkg_dir.exists() {
// Verify that the publisher is in the config
if !self.config.publishers.contains(&pub_name) {
return Err(anyhow!(
"Publisher directory exists but is not in the repository configuration: {}",
pub_name
return Err(RepositoryError::Other(
format!("Publisher directory exists but is not in the repository configuration: {}", pub_name)
));
}
@ -746,7 +745,7 @@ impl ReadableRepository for FileBackend {
// Filter publishers if specified
let publishers = if let Some(pub_name) = publisher {
if !self.config.publishers.contains(&pub_name.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", pub_name));
return Err(RepositoryError::PublisherNotFound(pub_name.to_string()));
}
vec![pub_name.to_string()]
} else {
@ -987,8 +986,13 @@ impl ReadableRepository for FileBackend {
publisher: Option<&str>,
limit: Option<usize>,
) -> Result<Vec<PackageInfo>> {
println!("Searching for packages with query: {}", query);
println!("Publisher: {:?}", publisher);
println!("Limit: {:?}", limit);
// If no publisher is specified, use the default publisher if available
let publisher = publisher.or_else(|| self.config.default_publisher.as_deref());
println!("Effective publisher: {:?}", publisher);
// If still no publisher, we need to search all publishers
let publishers = if let Some(pub_name) = publisher {
@ -996,34 +1000,55 @@ impl ReadableRepository for FileBackend {
} else {
self.config.publishers.clone()
};
println!("Publishers to search: {:?}", publishers);
let mut results = Vec::new();
// For each publisher, search the index
for pub_name in publishers {
println!("Searching publisher: {}", pub_name);
// Check if the index exists
let index_path = self.path.join("index").join(&pub_name).join("search.json");
println!("Index path: {}", index_path.display());
println!("Index exists: {}", index_path.exists());
if let Ok(Some(index)) = self.get_search_index(&pub_name) {
println!("Got search index for publisher: {}", pub_name);
println!("Index terms: {:?}", index.terms.keys().collect::<Vec<_>>());
// Search the index
let fmris = index.search(query, limit);
println!("Search results (FMRIs): {:?}", fmris);
// Convert FMRIs to PackageInfo
for fmri_str in fmris {
if let Ok(fmri) = Fmri::parse(&fmri_str) {
println!("Adding package to results: {}", fmri);
results.push(PackageInfo { fmri });
} else {
println!("Failed to parse FMRI: {}", fmri_str);
}
}
} else {
println!("No search index found for publisher: {}", pub_name);
println!("Falling back to simple search");
// If the index doesn't exist, fall back to the simple search
let all_packages = self.list_packages(Some(&pub_name), None)?;
println!("All packages: {:?}", all_packages);
// Filter packages by the query string
let matching_packages: Vec<PackageInfo> = all_packages
.into_iter()
.filter(|pkg| {
// Match against package name
pkg.fmri.stem().contains(query)
let matches = pkg.fmri.stem().contains(query);
println!("Package: {}, Matches: {}", pkg.fmri.stem(), matches);
matches
})
.collect();
println!("Matching packages: {:?}", matching_packages);
// Add matching packages to the results
results.extend(matching_packages);
@ -1035,6 +1060,7 @@ impl ReadableRepository for FileBackend {
results.truncate(max_results);
}
println!("Final search results: {:?}", results);
Ok(results)
}
}
@ -1111,13 +1137,13 @@ impl WritableRepository for FileBackend {
// Remove the catalog directory if it exists
if catalog_dir.exists() {
fs::remove_dir_all(&catalog_dir)
.map_err(|e| anyhow!("Failed to remove catalog directory: {}", e))?;
.map_err(|e| RepositoryError::Other(format!("Failed to remove catalog directory: {}", e)))?;
}
// Remove the package directory if it exists
if pkg_dir.exists() {
fs::remove_dir_all(&pkg_dir)
.map_err(|e| anyhow!("Failed to remove package directory: {}", e))?;
.map_err(|e| RepositoryError::Other(format!("Failed to remove package directory: {}", e)))?;
}
// Save the updated configuration
@ -1146,7 +1172,7 @@ impl WritableRepository for FileBackend {
) -> Result<()> {
// Check if the publisher exists
if !self.config.publishers.contains(&publisher.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", publisher));
return Err(RepositoryError::PublisherNotFound(publisher.to_string()));
}
// Create the property key in the format "publisher/property"
@ -1166,7 +1192,7 @@ impl WritableRepository for FileBackend {
// Filter publishers if specified
let publishers = if let Some(pub_name) = publisher {
if !self.config.publishers.contains(&pub_name.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", pub_name));
return Err(RepositoryError::PublisherNotFound(pub_name.to_string()));
}
vec![pub_name.to_string()]
} else {
@ -1196,7 +1222,7 @@ impl WritableRepository for FileBackend {
// Filter publishers if specified
let publishers = if let Some(pub_name) = publisher {
if !self.config.publishers.contains(&pub_name.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", pub_name));
return Err(RepositoryError::PublisherNotFound(pub_name.to_string()));
}
vec![pub_name.to_string()]
} else {
@ -1235,7 +1261,7 @@ impl WritableRepository for FileBackend {
fn set_default_publisher(&mut self, publisher: &str) -> Result<()> {
// Check if the publisher exists
if !self.config.publishers.contains(&publisher.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", publisher));
return Err(RepositoryError::PublisherNotFound(publisher.to_string()));
}
// Set the default publisher
@ -1729,10 +1755,10 @@ impl FileBackend {
let actual_path = &transaction.manifest.files[0].path;
if actual_path != expected_path {
return Err(anyhow!(
"Path in FileAction is incorrect. Expected: {}, Actual: {}",
return Err(RepositoryError::Other(
format!("Path in FileAction is incorrect. Expected: {}, Actual: {}",
expected_path,
actual_path
actual_path)
));
}
@ -1744,14 +1770,14 @@ impl FileBackend {
let stored_file_path = self.path.join("file").join(&hash);
if !stored_file_path.exists() {
return Err(anyhow!("File was not stored correctly"));
return Err(RepositoryError::Other("File was not stored correctly".to_string()));
}
// Verify the manifest was updated
let manifest_path = self.path.join("pkg").join("manifest");
if !manifest_path.exists() {
return Err(anyhow!("Manifest was not created"));
return Err(RepositoryError::Other("Manifest was not created".to_string()));
}
println!("File publishing test passed!");
@ -1770,15 +1796,15 @@ impl FileBackend {
// Check if the prototype directory exists
if !proto_dir.exists() {
return Err(anyhow!(
return Err(RepositoryError::NotFound(format!(
"Prototype directory does not exist: {}",
proto_dir.display()
));
)));
}
// Check if the publisher exists
if !self.config.publishers.contains(&publisher.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", publisher));
return Err(RepositoryError::PublisherNotFound(publisher.to_string()));
}
// Begin a transaction

View file

@ -3,9 +3,159 @@
// MPL was not distributed with this file, You can
// obtain one at https://mozilla.org/MPL/2.0/.
use anyhow::Result;
use std::collections::HashMap;
use std::path::Path;
use std::io;
use std::path::{Path, StripPrefixError};
use miette::Diagnostic;
use thiserror::Error;
/// Result type for repository operations
pub type Result<T> = std::result::Result<T, RepositoryError>;
/// Errors that can occur in repository operations
#[derive(Debug, Error, Diagnostic)]
pub enum RepositoryError {
#[error("unsupported repository version: {0}")]
#[diagnostic(
code(ips::repository_error::unsupported_version),
help("Supported repository versions: 4")
)]
UnsupportedVersion(u32),
#[error("repository not found at {0}")]
#[diagnostic(
code(ips::repository_error::not_found),
help("Check that the repository path exists and is accessible")
)]
NotFound(String),
#[error("publisher {0} not found")]
#[diagnostic(
code(ips::repository_error::publisher_not_found),
help("Check that the publisher name is correct and exists in the repository")
)]
PublisherNotFound(String),
#[error("publisher {0} already exists")]
#[diagnostic(
code(ips::repository_error::publisher_exists),
help("Use a different publisher name or remove the existing publisher first")
)]
PublisherExists(String),
#[error("failed to read repository configuration: {0}")]
#[diagnostic(
code(ips::repository_error::config_read),
help("Check that the repository configuration file exists and is valid")
)]
ConfigReadError(String),
#[error("failed to write repository configuration: {0}")]
#[diagnostic(
code(ips::repository_error::config_write),
help("Check that the repository directory is writable")
)]
ConfigWriteError(String),
#[error("failed to create directory: {0}")]
#[diagnostic(
code(ips::repository_error::directory_create),
help("Check that the parent directory exists and is writable")
)]
DirectoryCreateError(String),
#[error("failed to read file: {0}")]
#[diagnostic(
code(ips::repository_error::file_read),
help("Check that the file exists and is readable")
)]
FileReadError(String),
#[error("failed to write file: {0}")]
#[diagnostic(
code(ips::repository_error::file_write),
help("Check that the directory is writable")
)]
FileWriteError(String),
#[error("failed to parse JSON: {0}")]
#[diagnostic(
code(ips::repository_error::json_parse),
help("Check that the JSON file is valid")
)]
JsonParseError(String),
#[error("failed to serialize JSON: {0}")]
#[diagnostic(
code(ips::repository_error::json_serialize),
help("This is likely a bug in the code")
)]
JsonSerializeError(String),
#[error("I/O error: {0}")]
#[diagnostic(
code(ips::repository_error::io),
help("Check system resources and permissions")
)]
IoError(#[from] io::Error),
#[error("other error: {0}")]
#[diagnostic(
code(ips::repository_error::other),
help("See error message for details")
)]
Other(String),
#[error("JSON error: {0}")]
#[diagnostic(
code(ips::repository_error::json_error),
help("Check the JSON format and try again")
)]
JsonError(String),
#[error("digest error: {0}")]
#[diagnostic(
code(ips::repository_error::digest_error),
help("Check the digest format and try again")
)]
DigestError(String),
#[error(transparent)]
#[diagnostic(transparent)]
ActionError(#[from] ActionError),
#[error(transparent)]
#[diagnostic(transparent)]
CatalogError(#[from] catalog::CatalogError),
#[error("path prefix error: {0}")]
#[diagnostic(
code(ips::repository_error::path_prefix),
help("Check that the path is valid and within the expected directory")
)]
PathPrefixError(String),
}
// Implement From for common error types
impl From<serde_json::Error> for RepositoryError {
fn from(err: serde_json::Error) -> Self {
RepositoryError::JsonError(err.to_string())
}
}
impl From<DigestError> for RepositoryError {
fn from(err: DigestError) -> Self {
RepositoryError::DigestError(err.to_string())
}
}
impl From<StripPrefixError> for RepositoryError {
fn from(err: StripPrefixError) -> Self {
RepositoryError::PathPrefixError(err.to_string())
}
}
mod catalog;
mod file_backend;
@ -13,9 +163,11 @@ mod rest_backend;
#[cfg(test)]
mod tests;
pub use catalog::{CatalogAttrs, CatalogManager, CatalogOperationType, CatalogPart, UpdateLog};
pub use catalog::{CatalogAttrs, CatalogError, CatalogManager, CatalogOperationType, CatalogPart, UpdateLog};
pub use file_backend::FileBackend;
pub use rest_backend::RestBackend;
use crate::actions::ActionError;
use crate::digest::DigestError;
/// Repository configuration filename
pub const REPOSITORY_CONFIG_FILENAME: &str = "pkg6.repository";
@ -77,12 +229,12 @@ impl Default for RepositoryVersion {
}
impl std::convert::TryFrom<u32> for RepositoryVersion {
type Error = anyhow::Error;
type Error = RepositoryError;
fn try_from(value: u32) -> Result<Self, Self::Error> {
fn try_from(value: u32) -> std::result::Result<Self, Self::Error> {
match value {
4 => Ok(RepositoryVersion::V4),
_ => Err(anyhow::anyhow!("Unsupported repository version: {}", value)),
_ => Err(RepositoryError::UnsupportedVersion(value)),
}
}
}

View file

@ -3,12 +3,11 @@
// MPL was not distributed with this file, You can
// obtain one at https://mozilla.org/MPL/2.0/.
use anyhow::{anyhow, Result};
use std::path::{Path, PathBuf};
use super::{
PackageContents, PackageInfo, PublisherInfo, ReadableRepository, RepositoryConfig,
RepositoryInfo, RepositoryVersion, WritableRepository,
RepositoryError, RepositoryInfo, RepositoryVersion, Result, WritableRepository,
};
/// Repository implementation that uses a REST API
@ -97,7 +96,7 @@ impl WritableRepository for RestBackend {
// Filter publishers if specified
let publishers = if let Some(pub_name) = publisher {
if !self.config.publishers.contains(&pub_name.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", pub_name));
return Err(RepositoryError::PublisherNotFound(pub_name.to_string()));
}
vec![pub_name.to_string()]
} else {
@ -130,7 +129,7 @@ impl WritableRepository for RestBackend {
// Filter publishers if specified
let publishers = if let Some(pub_name) = publisher {
if !self.config.publishers.contains(&pub_name.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", pub_name));
return Err(RepositoryError::PublisherNotFound(pub_name.to_string()));
}
vec![pub_name.to_string()]
} else {
@ -162,7 +161,7 @@ impl WritableRepository for RestBackend {
// Check if the publisher exists
if !self.config.publishers.contains(&publisher.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", publisher));
return Err(RepositoryError::PublisherNotFound(publisher.to_string()));
}
// Set the default publisher
@ -199,7 +198,7 @@ impl WritableRepository for RestBackend {
// Check if the publisher exists
if !self.config.publishers.contains(&publisher.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", publisher));
return Err(RepositoryError::PublisherNotFound(publisher.to_string()));
}
// Create the property key in the format "publisher/property"
@ -274,7 +273,7 @@ impl ReadableRepository for RestBackend {
// Filter publishers if specified
let publishers = if let Some(pub_name) = publisher {
if !self.config.publishers.contains(&pub_name.to_string()) {
return Err(anyhow!("Publisher does not exist: {}", pub_name));
return Err(RepositoryError::PublisherNotFound(pub_name.to_string()));
}
vec![pub_name.to_string()]
} else {

View file

@ -8,8 +8,8 @@ mod tests {
use crate::actions::Manifest;
use crate::fmri::Fmri;
use crate::repository::{
CatalogManager, FileBackend, ReadableRepository, RepositoryVersion, WritableRepository,
REPOSITORY_CONFIG_FILENAME,
CatalogManager, FileBackend, ReadableRepository, RepositoryError, RepositoryVersion, Result,
WritableRepository, REPOSITORY_CONFIG_FILENAME,
};
use std::fs;
use std::path::PathBuf;
@ -72,7 +72,7 @@ mod tests {
manifest_path: &PathBuf,
prototype_dir: &PathBuf,
publisher: &str,
) -> Result<(), anyhow::Error> {
) -> Result<()> {
println!(
"Publishing package from manifest: {}",
manifest_path.display()
@ -83,18 +83,16 @@ mod tests {
// Check if the manifest file exists
if !manifest_path.exists() {
println!("Error: Manifest file does not exist");
return Err(anyhow::anyhow!(
"Manifest file does not exist: {}",
manifest_path.display()
return Err(RepositoryError::FileReadError(
format!("Manifest file does not exist: {}", manifest_path.display())
));
}
// Check if the prototype directory exists
if !prototype_dir.exists() {
println!("Error: Prototype directory does not exist");
return Err(anyhow::anyhow!(
"Prototype directory does not exist: {}",
prototype_dir.display()
return Err(RepositoryError::NotFound(
format!("Prototype directory does not exist: {}", prototype_dir.display())
));
}

View file

@ -0,0 +1,60 @@
#[cfg(test)]
mod tests {
use crate::actions::Manifest;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_parse_json_manifest() {
// Create a temporary directory for the test
let temp_dir = tempdir().unwrap();
let manifest_path = temp_dir.path().join("test_manifest.json");
// Create a simple manifest
let mut manifest = Manifest::new();
// Add some attributes
let mut attr = crate::actions::Attr::default();
attr.key = "pkg.fmri".to_string();
attr.values = vec!["pkg://test/example@1.0.0".to_string()];
manifest.attributes.push(attr);
// Serialize the manifest to JSON
let manifest_json = serde_json::to_string_pretty(&manifest).unwrap();
// Write the JSON to a file
let mut file = File::create(&manifest_path).unwrap();
file.write_all(manifest_json.as_bytes()).unwrap();
// Parse the JSON manifest
let parsed_manifest = Manifest::parse_file(&manifest_path).unwrap();
// Verify that the parsed manifest matches the original
assert_eq!(parsed_manifest.attributes.len(), 1);
assert_eq!(parsed_manifest.attributes[0].key, "pkg.fmri");
assert_eq!(parsed_manifest.attributes[0].values[0], "pkg://test/example@1.0.0");
}
#[test]
fn test_parse_string_manifest() {
// Create a temporary directory for the test
let temp_dir = tempdir().unwrap();
let manifest_path = temp_dir.path().join("test_manifest.p5m");
// Create a simple string-formatted manifest
let manifest_string = "set name=pkg.fmri value=pkg://test/example@1.0.0\n";
// Write the string to a file
let mut file = File::create(&manifest_path).unwrap();
file.write_all(manifest_string.as_bytes()).unwrap();
// Parse the string manifest
let parsed_manifest = Manifest::parse_file(&manifest_path).unwrap();
// Verify that the parsed manifest has the expected attributes
assert_eq!(parsed_manifest.attributes.len(), 1);
assert_eq!(parsed_manifest.attributes[0].key, "name");
assert_eq!(parsed_manifest.attributes[0].values[0], "pkg.fmri");
}
}