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:
parent
c71c576c29
commit
89d40109e5
7 changed files with 107 additions and 1 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
18
src/retry.rs
18
src/retry.rs
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue