chore: initial sanitized public snapshot

This commit is contained in:
Aria2 Rust Pro Contributors
2026-07-18 14:51:59 +08:00
commit 14dcf8c9bf
321 changed files with 76893 additions and 0 deletions
@@ -0,0 +1,110 @@
use adler2::Adler32;
use crc32fast::Hasher as Crc32Hasher;
use md5::Md5;
use sha1::Sha1;
use sha2::{Digest, Sha224, Sha256, Sha384, Sha512};
use super::model::{ChecksumSpec, ResponseBody};
impl ChecksumSpec {
#[must_use]
/// Returns whether the observed and expected digests match.
pub fn is_verified(&self) -> bool {
self.actual_hex
.as_deref()
.is_some_and(|actual| actual.eq_ignore_ascii_case(&self.expected_hex))
}
#[must_use]
/// Computes the payload digest using the configured algorithm.
pub fn compute_actual_hex(&self, payload: &[u8]) -> Option<String> {
checksum_hex(&self.algorithm, payload)
}
#[must_use]
/// Returns whether `payload` matches the expected digest when supported.
pub fn verify_payload(&self, payload: &[u8]) -> Option<bool> {
self.compute_actual_hex(payload)
.map(|actual| actual.eq_ignore_ascii_case(&self.expected_hex))
}
}
/// Compares a streamed body's observed digest with the expected checksum.
pub(super) fn streamed_checksum_verification(
checksum: &ChecksumSpec,
body: &ResponseBody,
) -> Option<bool> {
match body {
ResponseBody::Streamed {
observed_digest: Some(actual),
..
} => Some(actual.eq_ignore_ascii_case(&checksum.expected_hex)),
_ => None,
}
}
/// Computes a lowercase hexadecimal digest for the requested checksum algorithm.
#[must_use]
pub(super) fn checksum_hex(algorithm: &str, payload: &[u8]) -> Option<String> {
let algorithm = algorithm.trim();
let digest = if matches_checksum_algorithm(algorithm, &["sha1", "sha-1", "sha"]) {
let mut hasher = Sha1::new();
hasher.update(payload);
hasher.finalize().to_vec()
} else if matches_checksum_algorithm(algorithm, &["sha224", "sha-224"]) {
let mut hasher = Sha224::new();
hasher.update(payload);
hasher.finalize().to_vec()
} else if matches_checksum_algorithm(algorithm, &["sha256", "sha-256"]) {
let mut hasher = Sha256::new();
hasher.update(payload);
hasher.finalize().to_vec()
} else if matches_checksum_algorithm(algorithm, &["sha384", "sha-384"]) {
let mut hasher = Sha384::new();
hasher.update(payload);
hasher.finalize().to_vec()
} else if matches_checksum_algorithm(algorithm, &["sha512", "sha-512"]) {
let mut hasher = Sha512::new();
hasher.update(payload);
hasher.finalize().to_vec()
} else if algorithm.eq_ignore_ascii_case("md5") {
let mut hasher = Md5::new();
hasher.update(payload);
hasher.finalize().to_vec()
} else if algorithm.eq_ignore_ascii_case("adler32") {
let mut hasher = Adler32::new();
hasher.write_slice(payload);
hasher.checksum().to_be_bytes().to_vec()
} else if algorithm.eq_ignore_ascii_case("crc32") {
let mut hasher = Crc32Hasher::new();
hasher.update(payload);
hasher.finalize().to_be_bytes().to_vec()
} else {
return None;
};
Some(bytes_to_hex(&digest))
}
/// Returns whether a checksum algorithm matches any accepted spelling.
fn matches_checksum_algorithm(algorithm: &str, accepted: &[&str]) -> bool {
accepted
.iter()
.any(|candidate| algorithm.eq_ignore_ascii_case(candidate))
}
/// Hex-encodes digest bytes using lowercase hexadecimal.
#[must_use]
fn bytes_to_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(char::from(HEX[usize::from(byte >> 4)]));
out.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
out
}
/// Saturates a `usize` length into `u64`.
pub(super) fn usize_to_u64(value: usize) -> u64 {
u64::try_from(value).unwrap_or(u64::MAX)
}
@@ -0,0 +1,509 @@
use std::{collections::HashMap, path::PathBuf};
pub use crate::auth::{
AuthChallengeModel as AuthChallenge, AuthCredentialModel as AuthCredential, AuthScheme,
};
/// Classifies how one header participates in an HTTP exchange.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HeaderKind {
/// Header belongs to the request.
Request,
/// Header belongs to the response.
Response,
/// Header is valid for both directions.
General,
}
/// One normalized HTTP header field.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpHeader {
/// Lower-level header name.
pub name: String,
/// Raw header value.
pub value: String,
/// Header classification within the exchange.
pub kind: HeaderKind,
}
/// HTTP methods supported by the protocol layer.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HttpMethod {
/// `GET`
Get,
/// `HEAD`
Head,
/// `POST`
Post,
/// `PUT`
Put,
/// `DELETE`
Delete,
}
/// HTTP versions surfaced by the transport layer.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HttpVersion {
/// HTTP/1.0
Http10,
/// HTTP/1.1
Http11,
/// HTTP/2
Http2,
/// HTTP/3
Http3,
}
/// One request byte or piece range.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RangeSpec {
/// Inclusive start offset.
pub start: u64,
/// Optional inclusive end offset.
pub end_inclusive: Option<u64>,
/// Unit used by the range.
pub unit: RangeUnit,
}
impl RangeSpec {
#[must_use]
/// Returns whether the range omits an explicit end bound.
pub const fn is_open_ended(&self) -> bool {
self.end_inclusive.is_none()
}
#[must_use]
/// Returns the requested length when the end bound is known.
pub fn length_hint(&self) -> Option<u64> {
self.end_inclusive
.map(|end| end.saturating_sub(self.start).saturating_add(1))
}
}
/// Units supported by HTTP-style range models.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RangeUnit {
/// Byte-oriented ranges.
Bytes,
/// Piece-oriented ranges used by higher-level scheduling.
Pieces,
}
/// Parsed `Content-Range` response metadata.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ContentRangeSpec {
/// Unit reported by the server.
pub unit: RangeUnit,
/// Inclusive start offset returned by the server.
pub start: u64,
/// Inclusive end offset returned by the server.
pub end_inclusive: u64,
/// Total object size when known.
pub total_size: Option<u64>,
/// Whether the response represents an unsatisfied range.
pub unsatisfied: bool,
}
impl ContentRangeSpec {
#[must_use]
/// Returns the completed length implied by the range payload.
pub const fn completed_length(&self) -> u64 {
if self.unsatisfied {
0
} else {
self.end_inclusive.saturating_add(1)
}
}
#[must_use]
/// Returns whether the range is explicitly unsatisfied.
pub const fn is_unsatisfied(&self) -> bool {
self.unsatisfied
}
}
/// Resume metadata carried into one HTTP transfer attempt.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ResumeState {
/// Requested starting offset for the retry or resumed request.
pub requested_offset: u64,
/// Offset actually accepted by the remote server.
pub accepted_offset: Option<u64>,
/// Whether the attempt truly resumed instead of restarting from zero.
pub resumed: bool,
}
/// Retry policy knobs applied to HTTP work.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RetryPolicy {
/// Maximum number of attempts.
pub max_attempts: u32,
/// Initial backoff delay in milliseconds.
pub initial_backoff_ms: u64,
/// Maximum backoff delay in milliseconds.
pub max_backoff_ms: u64,
/// Whether `3xx` responses are retryable.
pub retry_on_3xx: bool,
/// Whether `4xx` responses are retryable.
pub retry_on_4xx: bool,
/// Whether `5xx` responses are retryable.
pub retry_on_5xx: bool,
/// Whether transport-level network errors are retryable.
pub retry_on_network_error: bool,
/// Whether timeout failures are retryable.
pub retry_on_timeout: bool,
}
/// Fully-resolved retry behavior for one request.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RetryStrategy {
/// Base retry policy.
pub policy: RetryPolicy,
/// Optional jitter value in milliseconds.
pub jitter: Option<u64>,
/// Optional upper bound on total retry elapsed time in milliseconds.
pub max_elapsed_ms: Option<u64>,
}
/// Normalized reasons for retrying one transfer attempt.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum RetryReason {
/// A transport-level network failure occurred.
NetworkError,
/// The request timed out.
Timeout,
/// The server responded with a retryable `3xx`.
Http3xx,
/// The server responded with a retryable `4xx`.
Http4xx,
/// The server responded with a retryable `5xx`.
Http5xx,
/// Partial-content semantics did not match the requested resume state.
PartialContentMismatch,
/// Another retryable condition occurred.
Other,
}
/// One recorded retry attempt.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct RetryAttempt {
/// Attempt number starting at one.
pub attempt: u32,
/// Retry reason for the attempt.
pub reason: RetryReason,
/// Optional HTTP status observed during the attempt.
pub status: Option<u16>,
/// Optional backoff delay in milliseconds before the next attempt.
pub backoff_ms: Option<u64>,
}
/// Final or intermediate completion state for one HTTP response.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HttpCompletionState {
/// Response is not yet complete.
Incomplete,
/// Response is usable but only partial.
Partial,
/// Response is complete without checksum verification.
Complete,
/// Response is complete and checksum-verified.
Verified,
}
/// Derived completion summary for one HTTP response.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HttpCompletionModel {
/// Overall completion state.
pub state: HttpCompletionState,
/// Total payload length when known.
pub total_length: Option<u64>,
/// Number of completed bytes.
pub completed_length: u64,
/// Whether the response used partial-content semantics.
pub partial_content: bool,
/// Whether the status code indicates terminal success.
pub terminal_success: bool,
/// Whether checksum metadata was present.
pub checksum_seen: bool,
/// Whether the checksum could be verified successfully.
pub checksum_verified: bool,
}
/// Segment-level progress view for one transfer snapshot.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HttpSegmentProgressModel {
/// Range originally requested from the server.
pub requested_range: Option<RangeSpec>,
/// Requested starting offset.
pub requested_offset: u64,
/// Offset accepted by the server when present.
pub accepted_offset: Option<u64>,
/// Completed offset derived from the current response.
pub completed_offset: Option<u64>,
/// Whether the transfer is actively resuming instead of restarting.
pub resumed: bool,
}
/// Retry-attempt detail enriched with segment and resume context.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct HttpRetryAttemptDetailModel {
/// Base retry-attempt data.
pub base: RetryAttempt,
/// Range requested for the attempt.
pub requested_range: Option<RangeSpec>,
/// Requested starting offset for the attempt.
pub requested_offset: u64,
/// Offset accepted by the server when present.
pub accepted_offset: Option<u64>,
/// Resume metadata captured for the attempt.
pub resume_state: Option<ResumeState>,
}
/// Snapshot of one in-flight or completed HTTP transfer.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpTransferProgressModel {
/// Stable task identifier.
pub task_id: String,
/// Request model associated with the transfer.
pub request: HttpRequestModel,
/// Segment-level progress details.
pub segment: HttpSegmentProgressModel,
/// Retry-attempt history with contextual detail.
pub retry_attempts: Vec<HttpRetryAttemptDetailModel>,
/// Maximum number of concurrent connections permitted for the task.
pub max_connections: u16,
/// Optional checksum hook attached to the transfer.
pub checksum_hook: Option<ChecksumHookModel>,
/// Derived completion summary when a response exists.
pub completion: Option<HttpCompletionModel>,
}
/// Proxy configuration projected into HTTP requests.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProxyConfig {
/// Proxy scheme such as `http` or `socks5`.
pub scheme: String,
/// Proxy host name or IP.
pub host: String,
/// Proxy port.
pub port: u16,
/// Optional proxy username.
pub username: Option<String>,
/// Optional proxy password.
pub password: Option<String>,
/// Hosts that should bypass the proxy.
pub bypass_hosts: Vec<String>,
/// Whether proxying is disabled for the request.
pub no_proxy: bool,
}
/// One normalized HTTP cookie.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Cookie {
/// Cookie name.
pub name: String,
/// Cookie value.
pub value: String,
/// Optional domain constraint.
pub domain: Option<String>,
/// Optional path constraint.
pub path: Option<String>,
/// Whether the cookie requires a secure transport.
pub secure: bool,
/// Whether the cookie is `HttpOnly`.
pub http_only: bool,
/// Optional same-site policy marker.
pub same_site: Option<String>,
/// Expiration time as a Unix timestamp when present.
pub expires_unix_epoch: Option<u64>,
}
/// TLS behavior attached to one HTTP session.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TlsConfig {
/// Whether peer certificates must be verified.
pub verify_peer: bool,
/// Whether host name verification is enabled.
pub verify_host: bool,
/// Minimum TLS version when constrained.
pub min_version: Option<String>,
/// Maximum TLS version when constrained.
pub max_version: Option<String>,
/// Optional CA bundle path.
pub ca_file: Option<String>,
/// Optional client certificate path.
pub cert_file: Option<String>,
/// Optional client key path.
pub key_file: Option<String>,
}
/// Ordered collection of request headers.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpRequestHeaders {
/// Stored request headers.
pub headers: Vec<HttpHeader>,
}
/// Ordered collection of response headers.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpResponseHeaders {
/// Stored response headers.
pub headers: Vec<HttpHeader>,
}
/// Request-body representation for HTTP transfers.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum HttpBody {
/// No request body.
Empty,
/// UTF-8 text request body.
Text(String),
/// Arbitrary binary request body.
Binary(Vec<u8>),
/// Streaming body with an optional declared length.
Stream {
/// Declared body length when the caller knows it.
expected_len: Option<u64>,
},
}
/// Expected and observed checksum metadata for one payload.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ChecksumSpec {
/// Hash algorithm name.
pub algorithm: String,
/// Expected digest hex string.
pub expected_hex: String,
/// Observed digest hex string when known.
pub actual_hex: Option<String>,
}
/// Optional checksum hook attached to a transfer.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ChecksumHookModel {
/// Checksum specification to evaluate.
pub spec: ChecksumSpec,
/// Whether the hook is enabled.
pub enabled: bool,
}
/// Optional direct-write target for one live HTTP response body.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpResponseSinkTarget {
/// Final output path that should receive the response body directly.
pub target_path: PathBuf,
}
/// Fully normalized HTTP request model.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpRequestModel {
/// HTTP method.
pub method: HttpMethod,
/// Fully-qualified request URL.
pub url: String,
/// Requested HTTP version.
pub version: HttpVersion,
/// Explicit request headers.
pub headers: HttpRequestHeaders,
/// Query parameters to attach to the URL.
pub query: HashMap<String, String>,
/// Optional range metadata.
pub range: Option<RangeSpec>,
/// Request body.
pub body: HttpBody,
/// Retry strategy for the request.
pub retry: RetryStrategy,
/// Optional origin credential.
pub auth: Option<AuthCredential>,
/// Optional proxy configuration.
pub proxy: Option<ProxyConfig>,
/// Optional direct-write sink for live response persistence.
pub response_sink: Option<HttpResponseSinkTarget>,
}
/// Session-scoped defaults that shape HTTP execution.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpSessionModel {
/// Stable session identifier.
pub session_id: String,
/// Optional user-agent string.
pub user_agent: Option<String>,
/// Default headers applied to requests.
pub default_headers: Vec<HttpHeader>,
/// Cookies carried by the session.
pub cookies: Vec<Cookie>,
/// Optional default credential.
pub auth: Option<AuthCredential>,
/// Optional default proxy configuration.
pub proxy: Option<ProxyConfig>,
/// Optional TLS behavior for the session.
pub tls: Option<TlsConfig>,
/// Default retry strategy for the session.
pub retry: RetryStrategy,
}
/// Response-body representation used by the protocol layer.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ResponseBody {
/// No response payload.
Empty,
/// Inline retained payload bytes.
Inline(Vec<u8>),
/// Streamed payload metadata with optional retained artifacts.
Streamed {
/// Declared content length when known.
expected_len: Option<u64>,
/// Observed byte count written through the sink.
observed_len: Option<u64>,
/// Observed digest when computed by the sink.
observed_digest: Option<String>,
/// Optional temporary file path holding the streamed body.
temp_path: Option<PathBuf>,
},
}
/// Executable HTTP transfer task passed into downloaders.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpTransferTaskModel {
/// Stable task identifier.
pub task_id: String,
/// Request model for the task.
pub request: HttpRequestModel,
/// Response headers already associated with the task.
pub response_headers: HttpResponseHeaders,
/// Current response body state.
pub body: ResponseBody,
/// Resume metadata when resuming is in play.
pub resume_state: Option<ResumeState>,
/// Retry-attempt history.
pub retry_attempts: Vec<RetryAttempt>,
/// Optional checksum hook.
pub checksum_hook: Option<ChecksumHookModel>,
/// Maximum allowed concurrent connections.
pub max_connections: u16,
/// Retry strategy for the task.
pub retry: RetryStrategy,
}
/// Normalized HTTP response model produced by connectors and fixtures.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HttpResponseModel {
/// Numeric HTTP status code.
pub status: u16,
/// Human-readable reason phrase.
pub reason: String,
/// Negotiated HTTP version.
pub version: HttpVersion,
/// Response headers.
pub headers: HttpResponseHeaders,
/// Response body representation.
pub body: ResponseBody,
/// Parsed `Content-Range` metadata when present.
pub content_range: Option<ContentRangeSpec>,
/// Whether the response used partial-content semantics.
pub partial_content: bool,
/// Optional checksum metadata.
pub checksum: Option<ChecksumSpec>,
/// Original URL before redirects when one occurred.
pub redirected_from: Option<String>,
}
@@ -0,0 +1,177 @@
use super::{
checksum::{streamed_checksum_verification, usize_to_u64},
model::{
HttpCompletionModel, HttpCompletionState, HttpResponseModel, HttpRetryAttemptDetailModel,
HttpSegmentProgressModel, HttpTransferProgressModel, HttpTransferTaskModel, ResponseBody,
},
};
impl HttpTransferTaskModel {
#[must_use]
/// Returns the effective requested offset for the task.
pub fn requested_offset(&self) -> u64 {
self.resume_state
.as_ref()
.map(|state| state.requested_offset)
.or_else(|| self.request.range.as_ref().map(|range| range.start))
.unwrap_or_default()
}
#[must_use]
/// Returns retry attempts enriched with resume and range context.
pub fn retry_attempt_details(&self) -> Vec<HttpRetryAttemptDetailModel> {
let requested_offset = self.requested_offset();
let requested_range = self.request.range;
let accepted_offset = self
.resume_state
.as_ref()
.and_then(|state| state.accepted_offset);
let resume_state = self.resume_state;
self.retry_attempts
.iter()
.copied()
.map(|base| HttpRetryAttemptDetailModel {
base,
requested_range,
requested_offset,
accepted_offset,
resume_state,
})
.collect()
}
#[must_use]
/// Builds a progress snapshot from the current task and optional response.
pub fn progress_snapshot(
&self,
response: Option<&HttpResponseModel>,
) -> HttpTransferProgressModel {
let completion = response.map(HttpResponseModel::completion_model);
let completed_offset = response.map(HttpResponseModel::completed_length);
let (accepted_offset, resumed) = response.map_or_else(
|| {
(
self.resume_state
.as_ref()
.and_then(|state| state.accepted_offset),
self.resume_state
.as_ref()
.is_some_and(|state| state.resumed),
)
},
|response| {
if response.status == 206 {
let accepted_offset = response
.content_range
.as_ref()
.and_then(|range| (!range.is_unsatisfied()).then_some(range.start));
(
accepted_offset,
accepted_offset.is_some()
|| self
.resume_state
.as_ref()
.is_some_and(|state| state.resumed),
)
} else {
(None, false)
}
},
);
HttpTransferProgressModel {
task_id: self.task_id.clone(),
request: self.request.clone(),
segment: HttpSegmentProgressModel {
requested_range: self.request.range,
requested_offset: self.requested_offset(),
accepted_offset,
completed_offset,
resumed,
},
retry_attempts: self.retry_attempt_details(),
max_connections: self.max_connections,
checksum_hook: self.checksum_hook.clone(),
completion,
}
}
}
impl HttpResponseModel {
#[must_use]
/// Returns the total payload length when the response exposes it.
pub fn total_length(&self) -> Option<u64> {
self.content_range
.as_ref()
.and_then(|range| range.total_size)
.or_else(|| match &self.body {
ResponseBody::Inline(bytes) => Some(usize_to_u64(bytes.len())),
ResponseBody::Streamed {
expected_len,
observed_len,
..
} => expected_len.or(*observed_len),
ResponseBody::Empty => None,
})
}
#[must_use]
/// Returns the completed payload length represented by the response.
pub fn completed_length(&self) -> u64 {
self.content_range
.as_ref()
.map(super::model::ContentRangeSpec::completed_length)
.or_else(|| match &self.body {
ResponseBody::Inline(bytes) => Some(usize_to_u64(bytes.len())),
ResponseBody::Streamed { observed_len, .. } => *observed_len,
ResponseBody::Empty => Some(0),
})
.unwrap_or_default()
}
#[must_use]
/// Returns inline body bytes when they are retained in memory.
pub fn body_bytes(&self) -> Option<&[u8]> {
match &self.body {
ResponseBody::Empty => Some(&[]),
ResponseBody::Inline(bytes) => Some(bytes),
ResponseBody::Streamed { .. } => None,
}
}
#[must_use]
/// Derives a completion summary from the response payload and metadata.
pub fn completion_model(&self) -> HttpCompletionModel {
let total_length = self.total_length();
let completed_length = self.completed_length();
let checksum_seen = self.checksum.is_some();
let checksum_verified = self.checksum.as_ref().is_some_and(|checksum| {
self.body_bytes()
.and_then(|bytes| checksum.verify_payload(bytes))
.or_else(|| streamed_checksum_verification(checksum, &self.body))
.unwrap_or_else(|| checksum.is_verified())
});
let terminal_success = (200..300).contains(&self.status);
let complete_enough = total_length.is_none_or(|total| completed_length >= total);
let state = if !terminal_success {
HttpCompletionState::Incomplete
} else if checksum_verified && complete_enough {
HttpCompletionState::Verified
} else if complete_enough {
HttpCompletionState::Complete
} else {
HttpCompletionState::Partial
};
HttpCompletionModel {
state,
total_length,
completed_length,
partial_content: self.partial_content,
terminal_success,
checksum_seen,
checksum_verified,
}
}
}
@@ -0,0 +1,489 @@
use std::collections::HashMap;
use super::*;
fn sample_retry_strategy() -> RetryStrategy {
RetryStrategy {
policy: RetryPolicy {
max_attempts: 5,
initial_backoff_ms: 100,
max_backoff_ms: 5_000,
retry_on_3xx: false,
retry_on_4xx: false,
retry_on_5xx: true,
retry_on_network_error: true,
retry_on_timeout: true,
},
jitter: Some(25),
max_elapsed_ms: Some(60_000),
}
}
fn sample_request() -> HttpRequestModel {
HttpRequestModel {
method: HttpMethod::Get,
url: "https://example.invalid/file.bin".to_string(),
version: HttpVersion::Http11,
headers: HttpRequestHeaders { headers: vec![] },
query: HashMap::new(),
range: Some(RangeSpec {
start: 4096,
end_inclusive: None,
unit: RangeUnit::Bytes,
}),
body: HttpBody::Empty,
retry: sample_retry_strategy(),
auth: None,
proxy: None,
response_sink: None,
}
}
#[test]
fn response_model_carries_partial_content_and_content_range() {
let response = HttpResponseModel {
status: 206,
reason: "Partial Content".to_string(),
version: HttpVersion::Http11,
headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Streamed {
expected_len: Some(1024),
observed_len: Some(1024),
observed_digest: None,
temp_path: None,
},
content_range: Some(ContentRangeSpec {
unit: RangeUnit::Bytes,
start: 4096,
end_inclusive: 5119,
total_size: Some(10_000),
unsatisfied: false,
}),
partial_content: true,
checksum: None,
redirected_from: None,
};
assert!(response.partial_content);
assert_eq!(
response.content_range,
Some(ContentRangeSpec {
unit: RangeUnit::Bytes,
start: 4096,
end_inclusive: 5119,
total_size: Some(10_000),
unsatisfied: false,
})
);
}
#[test]
fn response_completion_model_distinguishes_partial_and_verified() {
let partial = HttpResponseModel {
status: 206,
reason: "Partial Content".to_string(),
version: HttpVersion::Http11,
headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Inline(b"12345".to_vec()),
content_range: Some(ContentRangeSpec {
unit: RangeUnit::Bytes,
start: 0,
end_inclusive: 4,
total_size: Some(10),
unsatisfied: false,
}),
partial_content: true,
checksum: None,
redirected_from: None,
};
let verified = HttpResponseModel {
status: 200,
reason: "OK".to_string(),
version: HttpVersion::Http11,
headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Inline(b"abc".to_vec()),
content_range: None,
partial_content: false,
checksum: Some(ChecksumSpec {
algorithm: "sha-1".to_string(),
expected_hex: "a9993e364706816aba3e25717850c26c9cd0d89d".to_string(),
actual_hex: None,
}),
redirected_from: None,
};
assert_eq!(
partial.completion_model().state,
HttpCompletionState::Partial
);
assert_eq!(partial.completion_model().completed_length, 5);
assert_eq!(
verified.completion_model().state,
HttpCompletionState::Verified
);
assert!(verified.completion_model().checksum_verified);
}
#[test]
fn range_rejection_keeps_total_length_truth_without_reporting_progress() {
let response = HttpResponseModel {
status: 416,
reason: "Range Not Satisfiable".to_string(),
version: HttpVersion::Http11,
headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Empty,
content_range: Some(ContentRangeSpec {
unit: RangeUnit::Bytes,
start: 0,
end_inclusive: 0,
total_size: Some(8192),
unsatisfied: true,
}),
partial_content: false,
checksum: None,
redirected_from: None,
};
let completion = response.completion_model();
assert_eq!(completion.total_length, Some(8192));
assert_eq!(completion.completed_length, 0);
assert_eq!(completion.state, HttpCompletionState::Incomplete);
}
#[test]
fn checksum_verification_uses_inline_payload_bytes() {
let checksum = ChecksumSpec {
algorithm: "sha-256".to_string(),
expected_hex: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
.to_string(),
actual_hex: None,
};
assert_eq!(checksum.verify_payload(b"abc"), Some(true));
assert_eq!(checksum.verify_payload(b"abcd"), Some(false));
}
#[test]
fn streamed_response_completion_uses_observed_length_and_digest() {
let response = HttpResponseModel {
status: 200,
reason: "OK".to_string(),
version: HttpVersion::Http11,
headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Streamed {
expected_len: Some(12),
observed_len: Some(12),
observed_digest: Some("9251ad9cddb52f55d2c6b96280c781e7".to_string()),
temp_path: None,
},
content_range: None,
partial_content: false,
checksum: Some(ChecksumSpec {
algorithm: "md5".to_string(),
expected_hex: "9251ad9cddb52f55d2c6b96280c781e7".to_string(),
actual_hex: None,
}),
redirected_from: None,
};
let completion = response.completion_model();
assert_eq!(completion.completed_length, 12);
assert_eq!(completion.total_length, Some(12));
assert_eq!(completion.state, HttpCompletionState::Verified);
assert!(completion.checksum_verified);
}
#[test]
fn streamed_completion_uses_observed_len_without_expected_len() {
let response = HttpResponseModel {
status: 200,
reason: "OK".to_string(),
version: HttpVersion::Http11,
headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Streamed {
expected_len: None,
observed_len: Some(4096),
observed_digest: None,
temp_path: None,
},
content_range: None,
partial_content: false,
checksum: None,
redirected_from: None,
};
let completion = response.completion_model();
assert_eq!(completion.total_length, Some(4096));
assert_eq!(completion.completed_length, 4096);
assert_eq!(completion.state, HttpCompletionState::Complete);
}
#[test]
fn streamed_completion_does_not_treat_expected_len_as_completed_len() {
let response = HttpResponseModel {
status: 200,
reason: "OK".to_string(),
version: HttpVersion::Http11,
headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Streamed {
expected_len: Some(4096),
observed_len: None,
observed_digest: None,
temp_path: None,
},
content_range: None,
partial_content: false,
checksum: None,
redirected_from: None,
};
let completion = response.completion_model();
assert_eq!(completion.total_length, Some(4096));
assert_eq!(completion.completed_length, 0);
assert_eq!(completion.state, HttpCompletionState::Partial);
}
#[test]
fn streamed_checksum_verifies_from_observed_digest_without_inline_body() {
let response = HttpResponseModel {
status: 200,
reason: "OK".to_string(),
version: HttpVersion::Http11,
headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Streamed {
expected_len: None,
observed_len: Some(128),
observed_digest: Some(
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad".to_string(),
),
temp_path: None,
},
content_range: None,
partial_content: false,
checksum: Some(ChecksumSpec {
algorithm: "sha-256".to_string(),
expected_hex: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
.to_string(),
actual_hex: None,
}),
redirected_from: None,
};
let completion = response.completion_model();
assert!(completion.checksum_seen);
assert!(completion.checksum_verified);
assert_eq!(completion.state, HttpCompletionState::Verified);
}
#[test]
fn transfer_task_tracks_resume_offsets() {
let task = HttpTransferTaskModel {
task_id: "task-resume-1".to_string(),
request: sample_request(),
response_headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Streamed {
expected_len: None,
observed_len: None,
observed_digest: None,
temp_path: None,
},
resume_state: Some(ResumeState {
requested_offset: 8192,
accepted_offset: Some(8192),
resumed: true,
}),
retry_attempts: vec![],
checksum_hook: None,
max_connections: 4,
retry: sample_retry_strategy(),
};
let resume_state = task.resume_state.expect("resume state should exist");
assert!(resume_state.resumed);
assert_eq!(resume_state.requested_offset, 8192);
assert_eq!(resume_state.accepted_offset, Some(8192));
}
#[test]
fn transfer_task_tracks_retry_attempt_history() {
let task = HttpTransferTaskModel {
task_id: "task-retry-1".to_string(),
request: sample_request(),
response_headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Streamed {
expected_len: None,
observed_len: None,
observed_digest: None,
temp_path: None,
},
resume_state: None,
retry_attempts: vec![
RetryAttempt {
attempt: 1,
reason: RetryReason::Timeout,
status: None,
backoff_ms: Some(100),
},
RetryAttempt {
attempt: 2,
reason: RetryReason::Http5xx,
status: Some(503),
backoff_ms: Some(250),
},
],
checksum_hook: None,
max_connections: 4,
retry: sample_retry_strategy(),
};
assert_eq!(task.retry_attempts.len(), 2);
assert_eq!(task.retry_attempts[0].reason, RetryReason::Timeout);
assert_eq!(task.retry_attempts[1].status, Some(503));
assert_eq!(task.retry_attempts[1].backoff_ms, Some(250));
}
#[test]
fn transfer_task_progress_snapshot_tracks_segment_progress_and_retry_context() {
let request = HttpRequestModel {
method: HttpMethod::Get,
url: "https://example.invalid/segment.bin".to_string(),
version: HttpVersion::Http11,
headers: HttpRequestHeaders { headers: vec![] },
query: HashMap::new(),
range: Some(RangeSpec {
start: 8192,
end_inclusive: Some(12_287),
unit: RangeUnit::Bytes,
}),
body: HttpBody::Empty,
retry: sample_retry_strategy(),
auth: None,
proxy: None,
response_sink: None,
};
let task = HttpTransferTaskModel {
task_id: "task-progress-1".to_string(),
request,
response_headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Streamed {
expected_len: None,
observed_len: None,
observed_digest: None,
temp_path: None,
},
resume_state: Some(ResumeState {
requested_offset: 8192,
accepted_offset: Some(8192),
resumed: true,
}),
retry_attempts: vec![
RetryAttempt {
attempt: 1,
reason: RetryReason::Timeout,
status: None,
backoff_ms: Some(100),
},
RetryAttempt {
attempt: 2,
reason: RetryReason::Http5xx,
status: Some(503),
backoff_ms: Some(250),
},
],
checksum_hook: Some(ChecksumHookModel {
spec: ChecksumSpec {
algorithm: "sha-256".to_string(),
expected_hex: "abc123".to_string(),
actual_hex: None,
},
enabled: true,
}),
max_connections: 4,
retry: sample_retry_strategy(),
};
let response = HttpResponseModel {
status: 206,
reason: "Partial Content".to_string(),
version: HttpVersion::Http11,
headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Inline(b"abcdefghijkl".to_vec()),
content_range: Some(ContentRangeSpec {
unit: RangeUnit::Bytes,
start: 8192,
end_inclusive: 12_203,
total_size: Some(16_384),
unsatisfied: false,
}),
partial_content: true,
checksum: Some(ChecksumSpec {
algorithm: "sha-256".to_string(),
expected_hex: "abc123".to_string(),
actual_hex: Some("abc123".to_string()),
}),
redirected_from: None,
};
let progress = task.progress_snapshot(Some(&response));
assert_eq!(progress.task_id, "task-progress-1");
assert_eq!(progress.segment.requested_offset, 8192);
assert_eq!(progress.segment.accepted_offset, Some(8192));
assert_eq!(progress.segment.completed_offset, Some(12_204));
assert!(progress.segment.resumed);
assert_eq!(progress.retry_attempts.len(), 2);
assert_eq!(progress.retry_attempts[0].requested_offset, 8192);
assert_eq!(progress.retry_attempts[1].accepted_offset, Some(8192));
assert_eq!(
progress
.completion
.as_ref()
.expect("completion should be present")
.state,
HttpCompletionState::Partial
);
}
#[test]
fn progress_snapshot_clears_resume_truth_when_server_ignores_requested_range() {
let task = HttpTransferTaskModel {
task_id: "task-progress-range-ignored".to_string(),
request: sample_request(),
response_headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Empty,
resume_state: Some(ResumeState {
requested_offset: 4096,
accepted_offset: Some(4096),
resumed: true,
}),
retry_attempts: vec![],
checksum_hook: None,
max_connections: 1,
retry: sample_retry_strategy(),
};
let response = HttpResponseModel {
status: 200,
reason: "OK".to_string(),
version: HttpVersion::Http11,
headers: HttpResponseHeaders { headers: vec![] },
body: ResponseBody::Inline(vec![b'x'; 16_384]),
content_range: None,
partial_content: false,
checksum: None,
redirected_from: None,
};
let progress = task.progress_snapshot(Some(&response));
assert_eq!(progress.segment.requested_offset, 4096);
assert_eq!(progress.segment.accepted_offset, None);
assert_eq!(progress.segment.completed_offset, Some(16_384));
assert!(!progress.segment.resumed);
assert_eq!(
progress
.completion
.as_ref()
.expect("completion should exist")
.state,
HttpCompletionState::Complete
);
}