Record download checksums
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use std::{path::PathBuf, time::SystemTime};
|
||||
|
||||
use ely_domain::{DownloadEntry, DownloadId, UrlText};
|
||||
use ely_domain::{DownloadChecksum, DownloadEntry, DownloadId, UrlText};
|
||||
|
||||
use crate::CoreError;
|
||||
|
||||
@@ -70,6 +70,15 @@ impl BrowserCore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn record_download_checksum(
|
||||
&mut self,
|
||||
download_id: &DownloadId,
|
||||
checksum: DownloadChecksum,
|
||||
) -> Result<(), CoreError> {
|
||||
self.download_entry_mut(download_id)?.record_checksum(checksum)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn download_target_file_path(
|
||||
&self,
|
||||
download_id: &DownloadId,
|
||||
|
||||
@@ -2,10 +2,12 @@ use std::{error::Error, path::Path};
|
||||
|
||||
use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig};
|
||||
use ely_domain::{
|
||||
DomainError, DownloadDestination, DownloadId, DownloadPolicy, DownloadSecurity, DownloadState,
|
||||
ProfileKind, UrlText,
|
||||
DomainError, DownloadChecksum, DownloadDestination, DownloadId, DownloadPolicy,
|
||||
DownloadSecurity, DownloadState, ProfileKind, UrlText,
|
||||
};
|
||||
|
||||
const REPORT_SHA256: &str = "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855";
|
||||
|
||||
#[test]
|
||||
fn download_entries_stay_with_active_profile() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
@@ -66,6 +68,67 @@ fn controls_download_lifecycle() -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_checksum_after_download_completion() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let download_id = core.record_download_started(
|
||||
UrlText::parse("https://example.com/report.pdf")?,
|
||||
"report.pdf",
|
||||
Some(2048),
|
||||
)?;
|
||||
|
||||
core.complete_download(&download_id, 2048)?;
|
||||
core.record_download_checksum(&download_id, DownloadChecksum::sha256_hex(REPORT_SHA256)?)?;
|
||||
|
||||
let checksum =
|
||||
active_download(&core)?.checksum().ok_or("download checksum should exist")?.clone();
|
||||
assert_eq!(checksum.value(), REPORT_SHA256.to_ascii_lowercase());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_checksum_before_download_completion() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
let download_id = core.record_download_started(
|
||||
UrlText::parse("https://example.com/report.pdf")?,
|
||||
"report.pdf",
|
||||
Some(2048),
|
||||
)?;
|
||||
|
||||
let error = match core
|
||||
.record_download_checksum(&download_id, DownloadChecksum::sha256_hex(REPORT_SHA256)?)
|
||||
{
|
||||
Ok(()) => return Err("checksum should require completed download".into()),
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
error,
|
||||
CoreError::Domain(DomainError::InvalidDownloadTransition {
|
||||
action: "record checksum",
|
||||
state: "in_progress"
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_sha256_checksum() -> Result<(), Box<dyn Error>> {
|
||||
let error = match DownloadChecksum::sha256_hex("not-a-sha256") {
|
||||
Ok(_) => return Err("checksum should require 64 hex characters".into()),
|
||||
Err(error) => error,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
error,
|
||||
DomainError::InvalidDownloadChecksum {
|
||||
algorithm: "sha256",
|
||||
value: "not-a-sha256".to_string()
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn records_active_profile_download_policy_on_started_entry() -> Result<(), Box<dyn Error>> {
|
||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||
|
||||
@@ -31,6 +31,17 @@ pub enum DownloadSecurity {
|
||||
DangerousExtension,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum DownloadChecksumAlgorithm {
|
||||
Sha256,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct DownloadChecksum {
|
||||
algorithm: DownloadChecksumAlgorithm,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct DownloadEntry {
|
||||
id: DownloadId,
|
||||
@@ -43,6 +54,7 @@ pub struct DownloadEntry {
|
||||
state: DownloadState,
|
||||
received_bytes: u64,
|
||||
total_bytes: Option<u64>,
|
||||
checksum: Option<DownloadChecksum>,
|
||||
started_at: SystemTime,
|
||||
}
|
||||
|
||||
@@ -137,6 +149,40 @@ impl DownloadSecurity {
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadChecksumAlgorithm {
|
||||
#[must_use]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Sha256 => "sha256",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadChecksum {
|
||||
pub fn sha256_hex(value: impl Into<String>) -> Result<Self, DomainError> {
|
||||
let value = value.into();
|
||||
let value = value.trim();
|
||||
if !is_sha256_hex(value) {
|
||||
return Err(DomainError::InvalidDownloadChecksum {
|
||||
algorithm: DownloadChecksumAlgorithm::Sha256.as_str(),
|
||||
value: value.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self { algorithm: DownloadChecksumAlgorithm::Sha256, value: value.to_ascii_lowercase() })
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn algorithm(&self) -> &DownloadChecksumAlgorithm {
|
||||
&self.algorithm
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn value(&self) -> &str {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl DownloadEntry {
|
||||
pub fn started(
|
||||
profile_id: ProfileId,
|
||||
@@ -165,6 +211,7 @@ impl DownloadEntry {
|
||||
state: DownloadState::InProgress,
|
||||
received_bytes: 0,
|
||||
total_bytes,
|
||||
checksum: None,
|
||||
started_at,
|
||||
})
|
||||
}
|
||||
@@ -215,6 +262,12 @@ impl DownloadEntry {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn record_checksum(&mut self, checksum: DownloadChecksum) -> Result<(), DomainError> {
|
||||
self.require_state("record checksum", &[DownloadState::Completed])?;
|
||||
self.checksum = Some(checksum);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn id(&self) -> &DownloadId {
|
||||
&self.id
|
||||
@@ -265,6 +318,11 @@ impl DownloadEntry {
|
||||
self.total_bytes
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn checksum(&self) -> Option<&DownloadChecksum> {
|
||||
self.checksum.as_ref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn started_at(&self) -> SystemTime {
|
||||
self.started_at
|
||||
@@ -330,3 +388,7 @@ fn download_extension(file_name: &str) -> Option<&str> {
|
||||
fn is_dangerous_extension(extension: &str) -> bool {
|
||||
DANGEROUS_DOWNLOAD_EXTENSIONS.iter().any(|candidate| extension.eq_ignore_ascii_case(candidate))
|
||||
}
|
||||
|
||||
fn is_sha256_hex(value: &str) -> bool {
|
||||
value.len() == 64 && value.as_bytes().iter().all(u8::is_ascii_hexdigit)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,9 @@ pub enum DomainError {
|
||||
#[error("invalid file name: {value}")]
|
||||
InvalidFileName { value: String },
|
||||
|
||||
#[error("invalid {algorithm} download checksum: {value}")]
|
||||
InvalidDownloadChecksum { algorithm: &'static str, value: String },
|
||||
|
||||
#[error("invalid download directory: {path}")]
|
||||
InvalidDownloadDirectory { path: String },
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ mod url_text;
|
||||
pub use archive::{ArchiveSource, ArchivedTab};
|
||||
pub use command::{CommandIntent, CommandScope};
|
||||
pub use download::{
|
||||
DownloadDestination, DownloadEntry, DownloadPolicy, DownloadSecurity, DownloadState,
|
||||
DownloadChecksum, DownloadChecksumAlgorithm, DownloadDestination, DownloadEntry,
|
||||
DownloadPolicy, DownloadSecurity, DownloadState,
|
||||
};
|
||||
pub use error::DomainError;
|
||||
pub use history::HistoryEntry;
|
||||
|
||||
Reference in New Issue
Block a user