Record download checksums
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use std::{path::PathBuf, time::SystemTime};
|
use std::{path::PathBuf, time::SystemTime};
|
||||||
|
|
||||||
use ely_domain::{DownloadEntry, DownloadId, UrlText};
|
use ely_domain::{DownloadChecksum, DownloadEntry, DownloadId, UrlText};
|
||||||
|
|
||||||
use crate::CoreError;
|
use crate::CoreError;
|
||||||
|
|
||||||
@@ -70,6 +70,15 @@ impl BrowserCore {
|
|||||||
Ok(())
|
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(
|
pub fn download_target_file_path(
|
||||||
&self,
|
&self,
|
||||||
download_id: &DownloadId,
|
download_id: &DownloadId,
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ use std::{error::Error, path::Path};
|
|||||||
|
|
||||||
use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig};
|
use ely_browser_core::{BrowserCore, CoreError, InitialBrowserConfig};
|
||||||
use ely_domain::{
|
use ely_domain::{
|
||||||
DomainError, DownloadDestination, DownloadId, DownloadPolicy, DownloadSecurity, DownloadState,
|
DomainError, DownloadChecksum, DownloadDestination, DownloadId, DownloadPolicy,
|
||||||
ProfileKind, UrlText,
|
DownloadSecurity, DownloadState, ProfileKind, UrlText,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const REPORT_SHA256: &str = "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855";
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn download_entries_stay_with_active_profile() -> Result<(), Box<dyn Error>> {
|
fn download_entries_stay_with_active_profile() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
@@ -66,6 +68,67 @@ fn controls_download_lifecycle() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn records_active_profile_download_policy_on_started_entry() -> Result<(), Box<dyn Error>> {
|
fn records_active_profile_download_policy_on_started_entry() -> Result<(), Box<dyn Error>> {
|
||||||
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
let mut core = BrowserCore::new(InitialBrowserConfig::ely_defaults()?)?;
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ pub enum DownloadSecurity {
|
|||||||
DangerousExtension,
|
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)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct DownloadEntry {
|
pub struct DownloadEntry {
|
||||||
id: DownloadId,
|
id: DownloadId,
|
||||||
@@ -43,6 +54,7 @@ pub struct DownloadEntry {
|
|||||||
state: DownloadState,
|
state: DownloadState,
|
||||||
received_bytes: u64,
|
received_bytes: u64,
|
||||||
total_bytes: Option<u64>,
|
total_bytes: Option<u64>,
|
||||||
|
checksum: Option<DownloadChecksum>,
|
||||||
started_at: SystemTime,
|
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 {
|
impl DownloadEntry {
|
||||||
pub fn started(
|
pub fn started(
|
||||||
profile_id: ProfileId,
|
profile_id: ProfileId,
|
||||||
@@ -165,6 +211,7 @@ impl DownloadEntry {
|
|||||||
state: DownloadState::InProgress,
|
state: DownloadState::InProgress,
|
||||||
received_bytes: 0,
|
received_bytes: 0,
|
||||||
total_bytes,
|
total_bytes,
|
||||||
|
checksum: None,
|
||||||
started_at,
|
started_at,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -215,6 +262,12 @@ impl DownloadEntry {
|
|||||||
Ok(())
|
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]
|
#[must_use]
|
||||||
pub fn id(&self) -> &DownloadId {
|
pub fn id(&self) -> &DownloadId {
|
||||||
&self.id
|
&self.id
|
||||||
@@ -265,6 +318,11 @@ impl DownloadEntry {
|
|||||||
self.total_bytes
|
self.total_bytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn checksum(&self) -> Option<&DownloadChecksum> {
|
||||||
|
self.checksum.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn started_at(&self) -> SystemTime {
|
pub fn started_at(&self) -> SystemTime {
|
||||||
self.started_at
|
self.started_at
|
||||||
@@ -330,3 +388,7 @@ fn download_extension(file_name: &str) -> Option<&str> {
|
|||||||
fn is_dangerous_extension(extension: &str) -> bool {
|
fn is_dangerous_extension(extension: &str) -> bool {
|
||||||
DANGEROUS_DOWNLOAD_EXTENSIONS.iter().any(|candidate| extension.eq_ignore_ascii_case(candidate))
|
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}")]
|
#[error("invalid file name: {value}")]
|
||||||
InvalidFileName { value: String },
|
InvalidFileName { value: String },
|
||||||
|
|
||||||
|
#[error("invalid {algorithm} download checksum: {value}")]
|
||||||
|
InvalidDownloadChecksum { algorithm: &'static str, value: String },
|
||||||
|
|
||||||
#[error("invalid download directory: {path}")]
|
#[error("invalid download directory: {path}")]
|
||||||
InvalidDownloadDirectory { path: String },
|
InvalidDownloadDirectory { path: String },
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ mod url_text;
|
|||||||
pub use archive::{ArchiveSource, ArchivedTab};
|
pub use archive::{ArchiveSource, ArchivedTab};
|
||||||
pub use command::{CommandIntent, CommandScope};
|
pub use command::{CommandIntent, CommandScope};
|
||||||
pub use download::{
|
pub use download::{
|
||||||
DownloadDestination, DownloadEntry, DownloadPolicy, DownloadSecurity, DownloadState,
|
DownloadChecksum, DownloadChecksumAlgorithm, DownloadDestination, DownloadEntry,
|
||||||
|
DownloadPolicy, DownloadSecurity, DownloadState,
|
||||||
};
|
};
|
||||||
pub use error::DomainError;
|
pub use error::DomainError;
|
||||||
pub use history::HistoryEntry;
|
pub use history::HistoryEntry;
|
||||||
|
|||||||
Reference in New Issue
Block a user