diff --git a/src/config.rs b/src/config.rs index 4edf8bf..ee94e91 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,9 +4,12 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Deserialize, Serialize)] #[serde(transparent)] +#[must_use] pub struct Secret(String); impl Secret { + /// Returns the secret value. + #[must_use] pub fn expose(&self) -> &str { &self.0 } @@ -32,6 +35,18 @@ pub struct Config { } impl Config { + /// Loads configuration from a TOML file with environment variable + /// interpolation. + /// + /// # Errors + /// + /// Returns an error if: + /// + /// - The file cannot be read + /// - Environment variable interpolation fails + /// - The TOML parsing fails + /// - Configuration validation fails (no sources, no sinks, invalid + /// `sample_ratio`) pub fn from_file(path: impl AsRef) -> anyhow::Result { let path = path.as_ref(); let contents = std::fs::read_to_string(path).map_err(|e| { @@ -203,6 +218,8 @@ pub enum SinkConfig { } impl SinkConfig { + /// Returns the sink identifier. + #[must_use] pub fn id(&self) -> &str { match self { Self::Filesystem(cfg) => &cfg.id, diff --git a/src/crypto/manifest.rs b/src/crypto/manifest.rs index 56fd1b0..d99175b 100644 --- a/src/crypto/manifest.rs +++ b/src/crypto/manifest.rs @@ -3,6 +3,10 @@ use blake3::Hasher; use crate::core::types::{ContentHash, Manifest}; /// Converts a hex string to a 32-byte array. +/// +/// # Errors +/// +/// Returns an error if the hex string is invalid or not exactly 32 bytes. fn hex_to_bytes(hex_str: &str) -> anyhow::Result<[u8; 32]> { let bytes = hex::decode(hex_str) .map_err(|e| anyhow::anyhow!("Invalid hex string '{hex_str}': {e}"))?; @@ -30,6 +34,10 @@ fn hex_to_bytes(hex_str: &str) -> anyhow::Result<[u8; 32]> { /// - Deterministic root hash for the manifest /// - Efficient inclusion proofs (log n verification) /// - Cryptographic integrity guarantees +/// +/// # Errors +/// +/// Returns an error if any leaf hash is not a valid 32-byte hex string. pub fn compute_merkle_root( leaf_hashes: &[ContentHash], ) -> anyhow::Result { @@ -75,6 +83,9 @@ pub fn compute_merkle_root( /// The sibling hashes needed to verify the leaf is in the tree. /// To verify: starting with the leaf hash, iteratively hash with siblings /// and compare against the known root. +/// +/// Returns `None` if `leaf_index` is out of bounds or if any hash is invalid. +#[must_use] pub fn get_inclusion_proof( leaf_hashes: &[ContentHash], leaf_index: usize, @@ -142,6 +153,7 @@ pub fn get_inclusion_proof( /// # Returns /// /// `true` if the proof is valid for the given root and leaf hash. +#[must_use] pub fn verify_inclusion_proof( root: &ContentHash, leaf_hash: &ContentHash, @@ -177,6 +189,7 @@ pub fn verify_inclusion_proof( /// A node in a Merkle inclusion proof. #[derive(Debug, Clone)] +#[must_use] pub enum MerkleProofNode { /// Sibling is to the left of the target. Left([u8; 32]), @@ -186,12 +199,18 @@ pub enum MerkleProofNode { /// A Merkle inclusion proof. #[derive(Debug, Clone)] +#[must_use] pub struct MerkleProof { pub leaf_index: usize, pub siblings: Vec, } /// Finalizes the manifest by computing its Merkle root hash. +/// +/// # Errors +/// +/// Returns an error if any artifact hash in the manifest is not a valid +/// 32-byte hex string. pub fn finalize_manifest(manifest: &mut Manifest) -> anyhow::Result<()> { if manifest.root_hash.is_none() { let leaf_hashes: Vec = manifest diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 3e013cf..637d94e 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -5,6 +5,11 @@ use std::path::Path; use blake3::Hasher; use tokio::io::AsyncReadExt; +/// Computes the BLAKE3 hash of a file. +/// +/// # Errors +/// +/// Returns an error if the file cannot be opened or read. pub async fn hash_file(path: &Path) -> anyhow::Result { let mut file = tokio::fs::File::open(path).await?; let mut hasher = Hasher::new(); @@ -27,6 +32,8 @@ pub struct StreamingHasher { } impl StreamingHasher { + /// Creates a new streaming hasher. + #[must_use] pub fn new() -> Self { Self { hasher: Hasher::new(), @@ -39,6 +46,8 @@ impl StreamingHasher { self.bytes_hashed += data.len() as u64; } + /// Finalizes the hash and returns the hash string and byte count. + #[must_use] pub fn finalize(self) -> (String, u64) { let hash = self.hasher.finalize().to_hex().to_string(); (hash, self.bytes_hashed) diff --git a/src/retry.rs b/src/retry.rs index 233e848..16de52e 100644 --- a/src/retry.rs +++ b/src/retry.rs @@ -6,6 +6,7 @@ use crate::config::RetryConfig; /// Classification of an error for retry purposes. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[must_use] pub enum ErrorKind { /// Transient error - safe to retry (network failure, rate limit, 5xx). Transient, @@ -14,6 +15,13 @@ pub enum ErrorKind { } /// Classify a `reqwest::Error` for retry purposes. +/// +/// # Errors +/// +/// This function does not return a `Result`, but returns +/// `ErrorKind::Transient` for network-level errors (connection, timeout, +/// request) and HTTP 5xx/429 status codes, or `ErrorKind::Permanent` for +/// HTTP 4xx client errors. pub fn classify_reqwest_error(err: &reqwest::Error) -> ErrorKind { if err.is_connect() || err.is_timeout() || err.is_request() { return ErrorKind::Transient; @@ -28,6 +36,10 @@ pub fn classify_reqwest_error(err: &reqwest::Error) -> ErrorKind { } /// Classify an HTTP status code for retry purposes. +/// +/// Returns `ErrorKind::Transient` for status codes 429 and 5xx (server errors), +/// `ErrorKind::Permanent` for 4xx (client errors), and `ErrorKind::Transient` +/// as a safe default for all other status codes. pub const fn classify_status_code(status: u16) -> ErrorKind { match status { // Rate limit and server-side errors - back off and retry. @@ -60,7 +72,11 @@ fn scale_backoff(backoff_ms: u64, multiplier: f64) -> u64 { /// [`ErrorKind::Transient`] errors are retried. A [`ErrorKind::Permanent`] /// error is returned immediately without further attempts. /// -/// Returns the last error after all attempts are exhausted. +/// # Errors +/// +/// Returns the last error from `operation` after all retry attempts are +/// exhausted, or the first `ErrorKind::Permanent` error encountered (which is +/// not retried). The error type `E` is determined by the `operation` closure. pub async fn execute_with_retry( config: &RetryConfig, operation_name: &str, diff --git a/src/sinks/filesystem.rs b/src/sinks/filesystem.rs index a972ca8..c791cc0 100644 --- a/src/sinks/filesystem.rs +++ b/src/sinks/filesystem.rs @@ -11,11 +11,26 @@ pub struct FilesystemSink { } impl FilesystemSink { + /// Creates a new filesystem sink. + /// + /// # Errors + /// + /// Returns an error if the sink directory cannot be created. pub fn new(config: FilesystemSinkConfig) -> anyhow::Result { std::fs::create_dir_all(&config.path)?; Ok(Self { config }) } + /// Writes an artifact to the filesystem sink. + /// + /// # Errors + /// + /// Returns an error if: + /// + /// - The content hash is invalid (too short or non-ASCII) + /// - The target directory cannot be created + /// - The artifact cannot be copied + /// - Hash verification fails (if enabled) pub async fn write( &self, artifact: &Artifact, diff --git a/src/sources/forgejo.rs b/src/sources/forgejo.rs index 2166118..5200d55 100644 --- a/src/sources/forgejo.rs +++ b/src/sources/forgejo.rs @@ -27,6 +27,12 @@ pub struct Repository { } impl ForgejoSource { + /// Creates a new Forgejo source client. + /// + /// # Errors + /// + /// Returns an error if the HTTP client cannot be built (e.g., invalid + /// headers). pub fn new( config: ForgejoSourceConfig, retry: RetryConfig, @@ -51,6 +57,12 @@ impl ForgejoSource { }) } + /// Lists all repositories from the configured organizations. + /// + /// # Errors + /// + /// Returns an error if the API request fails or if the response cannot be + /// parsed. pub async fn list_repositories(&self) -> anyhow::Result> { let mut repos = Vec::new(); @@ -142,6 +154,15 @@ impl ForgejoSource { Ok((repos, has_more)) } + /// Downloads a repository archive and computes its content hash. + /// + /// # Errors + /// + /// Returns an error if: + /// + /// - The download request fails + /// - The response status is not successful + /// - The temporary file cannot be created or written pub async fn download_archive( &self, repo: &Repository, diff --git a/src/storage/mod.rs b/src/storage/mod.rs index f210a70..00e139b 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -20,7 +20,9 @@ impl Storage { /// Creates a new storage connection and runs migrations. /// /// # Errors + /// /// Returns an error if: + /// /// - The database connection cannot be established /// - WAL mode cannot be enabled /// - Migrations fail @@ -89,6 +91,7 @@ impl Storage { /// Creates a new job in the database. /// /// # Errors + /// /// Returns an error if the database insert fails. pub async fn create_job( &self, @@ -125,7 +128,9 @@ impl Storage { /// Updates the status of an existing job. /// /// # Errors + /// /// Returns an error if: + /// /// - The database update fails /// - The job is not found pub async fn update_job_status( @@ -161,6 +166,7 @@ impl Storage { /// Lists all jobs for a given run. /// /// # Errors + /// /// Returns an error if the database query fails or if row data is invalid. pub async fn list_jobs_by_run( &self, @@ -186,7 +192,9 @@ impl Storage { /// Saves a manifest with its root hash for a backup run. /// /// # Errors + /// /// Returns an error if: + /// /// - The artifact count exceeds i64 max /// - The database insert fails pub async fn save_manifest( @@ -221,6 +229,7 @@ impl Storage { /// Retrieves the stored root hash for a backup run. /// /// # Errors + /// /// Returns an error if the database query fails. pub async fn get_manifest_root( &self,