treewide: improve rustdoc for common functions; add must_use annotations

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I380a36e7007aa8b59cf6c730f4474cd66a6a6964
This commit is contained in:
raf 2026-03-17 21:37:07 +03:00
commit 89d40109e5
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
7 changed files with 107 additions and 1 deletions

View file

@ -4,9 +4,12 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Deserialize, Serialize)] #[derive(Clone, Deserialize, Serialize)]
#[serde(transparent)] #[serde(transparent)]
#[must_use]
pub struct Secret(String); pub struct Secret(String);
impl Secret { impl Secret {
/// Returns the secret value.
#[must_use]
pub fn expose(&self) -> &str { pub fn expose(&self) -> &str {
&self.0 &self.0
} }
@ -32,6 +35,18 @@ pub struct Config {
} }
impl 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<Path>) -> anyhow::Result<Self> { pub fn from_file(path: impl AsRef<Path>) -> anyhow::Result<Self> {
let path = path.as_ref(); let path = path.as_ref();
let contents = std::fs::read_to_string(path).map_err(|e| { let contents = std::fs::read_to_string(path).map_err(|e| {
@ -203,6 +218,8 @@ pub enum SinkConfig {
} }
impl SinkConfig { impl SinkConfig {
/// Returns the sink identifier.
#[must_use]
pub fn id(&self) -> &str { pub fn id(&self) -> &str {
match self { match self {
Self::Filesystem(cfg) => &cfg.id, Self::Filesystem(cfg) => &cfg.id,

View file

@ -3,6 +3,10 @@ use blake3::Hasher;
use crate::core::types::{ContentHash, Manifest}; use crate::core::types::{ContentHash, Manifest};
/// Converts a hex string to a 32-byte array. /// 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]> { fn hex_to_bytes(hex_str: &str) -> anyhow::Result<[u8; 32]> {
let bytes = hex::decode(hex_str) let bytes = hex::decode(hex_str)
.map_err(|e| anyhow::anyhow!("Invalid hex string '{hex_str}': {e}"))?; .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 /// - Deterministic root hash for the manifest
/// - Efficient inclusion proofs (log n verification) /// - Efficient inclusion proofs (log n verification)
/// - Cryptographic integrity guarantees /// - Cryptographic integrity guarantees
///
/// # Errors
///
/// Returns an error if any leaf hash is not a valid 32-byte hex string.
pub fn compute_merkle_root( pub fn compute_merkle_root(
leaf_hashes: &[ContentHash], leaf_hashes: &[ContentHash],
) -> anyhow::Result<ContentHash> { ) -> anyhow::Result<ContentHash> {
@ -75,6 +83,9 @@ pub fn compute_merkle_root(
/// The sibling hashes needed to verify the leaf is in the tree. /// The sibling hashes needed to verify the leaf is in the tree.
/// To verify: starting with the leaf hash, iteratively hash with siblings /// To verify: starting with the leaf hash, iteratively hash with siblings
/// and compare against the known root. /// 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( pub fn get_inclusion_proof(
leaf_hashes: &[ContentHash], leaf_hashes: &[ContentHash],
leaf_index: usize, leaf_index: usize,
@ -142,6 +153,7 @@ pub fn get_inclusion_proof(
/// # Returns /// # Returns
/// ///
/// `true` if the proof is valid for the given root and leaf hash. /// `true` if the proof is valid for the given root and leaf hash.
#[must_use]
pub fn verify_inclusion_proof( pub fn verify_inclusion_proof(
root: &ContentHash, root: &ContentHash,
leaf_hash: &ContentHash, leaf_hash: &ContentHash,
@ -177,6 +189,7 @@ pub fn verify_inclusion_proof(
/// A node in a Merkle inclusion proof. /// A node in a Merkle inclusion proof.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[must_use]
pub enum MerkleProofNode { pub enum MerkleProofNode {
/// Sibling is to the left of the target. /// Sibling is to the left of the target.
Left([u8; 32]), Left([u8; 32]),
@ -186,12 +199,18 @@ pub enum MerkleProofNode {
/// A Merkle inclusion proof. /// A Merkle inclusion proof.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[must_use]
pub struct MerkleProof { pub struct MerkleProof {
pub leaf_index: usize, pub leaf_index: usize,
pub siblings: Vec<MerkleProofNode>, pub siblings: Vec<MerkleProofNode>,
} }
/// Finalizes the manifest by computing its Merkle root hash. /// 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<()> { pub fn finalize_manifest(manifest: &mut Manifest) -> anyhow::Result<()> {
if manifest.root_hash.is_none() { if manifest.root_hash.is_none() {
let leaf_hashes: Vec<ContentHash> = manifest let leaf_hashes: Vec<ContentHash> = manifest

View file

@ -5,6 +5,11 @@ use std::path::Path;
use blake3::Hasher; use blake3::Hasher;
use tokio::io::AsyncReadExt; 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<String> { pub async fn hash_file(path: &Path) -> anyhow::Result<String> {
let mut file = tokio::fs::File::open(path).await?; let mut file = tokio::fs::File::open(path).await?;
let mut hasher = Hasher::new(); let mut hasher = Hasher::new();
@ -27,6 +32,8 @@ pub struct StreamingHasher {
} }
impl StreamingHasher { impl StreamingHasher {
/// Creates a new streaming hasher.
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
hasher: Hasher::new(), hasher: Hasher::new(),
@ -39,6 +46,8 @@ impl StreamingHasher {
self.bytes_hashed += data.len() as u64; 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) { pub fn finalize(self) -> (String, u64) {
let hash = self.hasher.finalize().to_hex().to_string(); let hash = self.hasher.finalize().to_hex().to_string();
(hash, self.bytes_hashed) (hash, self.bytes_hashed)

View file

@ -6,6 +6,7 @@ use crate::config::RetryConfig;
/// Classification of an error for retry purposes. /// Classification of an error for retry purposes.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use]
pub enum ErrorKind { pub enum ErrorKind {
/// Transient error - safe to retry (network failure, rate limit, 5xx). /// Transient error - safe to retry (network failure, rate limit, 5xx).
Transient, Transient,
@ -14,6 +15,13 @@ pub enum ErrorKind {
} }
/// Classify a `reqwest::Error` for retry purposes. /// 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 { pub fn classify_reqwest_error(err: &reqwest::Error) -> ErrorKind {
if err.is_connect() || err.is_timeout() || err.is_request() { if err.is_connect() || err.is_timeout() || err.is_request() {
return ErrorKind::Transient; return ErrorKind::Transient;
@ -28,6 +36,10 @@ pub fn classify_reqwest_error(err: &reqwest::Error) -> ErrorKind {
} }
/// Classify an HTTP status code for retry purposes. /// 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 { pub const fn classify_status_code(status: u16) -> ErrorKind {
match status { match status {
// Rate limit and server-side errors - back off and retry. // 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`] /// [`ErrorKind::Transient`] errors are retried. A [`ErrorKind::Permanent`]
/// error is returned immediately without further attempts. /// 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<F, Fut, T, E, C>( pub async fn execute_with_retry<F, Fut, T, E, C>(
config: &RetryConfig, config: &RetryConfig,
operation_name: &str, operation_name: &str,

View file

@ -11,11 +11,26 @@ pub struct FilesystemSink {
} }
impl 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<Self> { pub fn new(config: FilesystemSinkConfig) -> anyhow::Result<Self> {
std::fs::create_dir_all(&config.path)?; std::fs::create_dir_all(&config.path)?;
Ok(Self { config }) 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( pub async fn write(
&self, &self,
artifact: &Artifact, artifact: &Artifact,

View file

@ -27,6 +27,12 @@ pub struct Repository {
} }
impl ForgejoSource { 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( pub fn new(
config: ForgejoSourceConfig, config: ForgejoSourceConfig,
retry: RetryConfig, 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<Vec<Repository>> { pub async fn list_repositories(&self) -> anyhow::Result<Vec<Repository>> {
let mut repos = Vec::new(); let mut repos = Vec::new();
@ -142,6 +154,15 @@ impl ForgejoSource {
Ok((repos, has_more)) 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( pub async fn download_archive(
&self, &self,
repo: &Repository, repo: &Repository,

View file

@ -20,7 +20,9 @@ impl Storage {
/// Creates a new storage connection and runs migrations. /// Creates a new storage connection and runs migrations.
/// ///
/// # Errors /// # Errors
///
/// Returns an error if: /// Returns an error if:
///
/// - The database connection cannot be established /// - The database connection cannot be established
/// - WAL mode cannot be enabled /// - WAL mode cannot be enabled
/// - Migrations fail /// - Migrations fail
@ -89,6 +91,7 @@ impl Storage {
/// Creates a new job in the database. /// Creates a new job in the database.
/// ///
/// # Errors /// # Errors
///
/// Returns an error if the database insert fails. /// Returns an error if the database insert fails.
pub async fn create_job( pub async fn create_job(
&self, &self,
@ -125,7 +128,9 @@ impl Storage {
/// Updates the status of an existing job. /// Updates the status of an existing job.
/// ///
/// # Errors /// # Errors
///
/// Returns an error if: /// Returns an error if:
///
/// - The database update fails /// - The database update fails
/// - The job is not found /// - The job is not found
pub async fn update_job_status( pub async fn update_job_status(
@ -161,6 +166,7 @@ impl Storage {
/// Lists all jobs for a given run. /// Lists all jobs for a given run.
/// ///
/// # Errors /// # Errors
///
/// Returns an error if the database query fails or if row data is invalid. /// Returns an error if the database query fails or if row data is invalid.
pub async fn list_jobs_by_run( pub async fn list_jobs_by_run(
&self, &self,
@ -186,7 +192,9 @@ impl Storage {
/// Saves a manifest with its root hash for a backup run. /// Saves a manifest with its root hash for a backup run.
/// ///
/// # Errors /// # Errors
///
/// Returns an error if: /// Returns an error if:
///
/// - The artifact count exceeds i64 max /// - The artifact count exceeds i64 max
/// - The database insert fails /// - The database insert fails
pub async fn save_manifest( pub async fn save_manifest(
@ -221,6 +229,7 @@ impl Storage {
/// Retrieves the stored root hash for a backup run. /// Retrieves the stored root hash for a backup run.
/// ///
/// # Errors /// # Errors
///
/// Returns an error if the database query fails. /// Returns an error if the database query fails.
pub async fn get_manifest_root( pub async fn get_manifest_root(
&self, &self,