Calculate download checksums

This commit is contained in:
2026-05-07 22:15:56 -04:00
parent 4302d0817c
commit a6e1482648
8 changed files with 186 additions and 10 deletions
Generated
+1
View File
@@ -2202,6 +2202,7 @@ dependencies = [
"gpui", "gpui",
"gpui-component", "gpui-component",
"gpui-component-assets", "gpui-component-assets",
"sha2",
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
+1
View File
@@ -19,6 +19,7 @@ gpui = "0.2.2"
gpui-component = "0.5.1" gpui-component = "0.5.1"
gpui-component-assets = "0.5.1" gpui-component-assets = "0.5.1"
servo = "0.1.0" servo = "0.1.0"
sha2 = "0.10.9"
thiserror = "2.0.12" thiserror = "2.0.12"
url = "2.5.4" url = "2.5.4"
uuid = { version = "1.12.1", features = ["v7"] } uuid = { version = "1.12.1", features = ["v7"] }
+1
View File
@@ -12,6 +12,7 @@ ely_domain = { path = "../ely_domain" }
gpui.workspace = true gpui.workspace = true
gpui-component.workspace = true gpui-component.workspace = true
gpui-component-assets.workspace = true gpui-component-assets.workspace = true
sha2.workspace = true
thiserror.workspace = true thiserror.workspace = true
[lints] [lints]
@@ -0,0 +1,117 @@
use std::{
fs::{self, File},
io::{self, Read},
path::{Path, PathBuf},
};
use ely_domain::{DomainError, DownloadChecksum};
use sha2::{Digest, Sha256};
use thiserror::Error;
const CHECKSUM_BUFFER_BYTES: usize = 64 * 1024;
pub struct DownloadChecksumCalculator;
#[derive(Debug, Error)]
pub enum DownloadChecksumError {
#[error("download file is unavailable: {path}")]
FileUnavailable {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("download path is not a file: {path}")]
NotAFile { path: PathBuf },
#[error(transparent)]
Domain(#[from] DomainError),
}
impl DownloadChecksumCalculator {
pub fn sha256(path: &Path) -> Result<DownloadChecksum, DownloadChecksumError> {
require_file(path)?;
let mut file = File::open(path).map_err(|source| {
DownloadChecksumError::FileUnavailable { path: path.to_path_buf(), source }
})?;
let mut hasher = Sha256::new();
let mut buffer = [0; CHECKSUM_BUFFER_BYTES];
loop {
let bytes_read = file.read(&mut buffer).map_err(|source| {
DownloadChecksumError::FileUnavailable { path: path.to_path_buf(), source }
})?;
if bytes_read == 0 {
break;
}
hasher.update(&buffer[..bytes_read]);
}
DownloadChecksum::sha256_hex(encode_lower_hex(&hasher.finalize())).map_err(Into::into)
}
}
fn require_file(path: &Path) -> Result<(), DownloadChecksumError> {
match fs::metadata(path) {
Ok(metadata) if metadata.is_file() => Ok(()),
Ok(_) => Err(DownloadChecksumError::NotAFile { path: path.to_path_buf() }),
Err(source) => {
Err(DownloadChecksumError::FileUnavailable { path: path.to_path_buf(), source })
}
}
}
fn encode_lower_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let byte = *byte;
output.push(HEX[(byte >> 4) as usize] as char);
output.push(HEX[(byte & 0x0f) as usize] as char);
}
output
}
#[cfg(test)]
mod tests {
use std::{
error::Error,
fs,
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
use super::{DownloadChecksumCalculator, DownloadChecksumError};
#[test]
fn computes_sha256_for_download_file() -> Result<(), Box<dyn Error>> {
let path = temp_file_path("checksum")?;
fs::write(&path, [])?;
let checksum = DownloadChecksumCalculator::sha256(&path)?;
let value = checksum.value().to_string();
fs::remove_file(path)?;
assert_eq!(value, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
Ok(())
}
#[test]
fn rejects_missing_download_file() -> Result<(), Box<dyn Error>> {
let path = temp_file_path("missing")?;
let error = match DownloadChecksumCalculator::sha256(&path) {
Ok(_) => return Err("missing download file should be rejected".into()),
Err(error) => error,
};
assert!(matches!(error, DownloadChecksumError::FileUnavailable { .. }));
Ok(())
}
fn temp_file_path(name: &str) -> Result<PathBuf, Box<dyn Error>> {
let nanos = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
Ok(std::env::temp_dir().join(format!("ely-download-{name}-{nanos}.bin")))
}
}
+1
View File
@@ -1 +1,2 @@
pub mod download_checksums;
pub mod download_files; pub mod download_files;
@@ -16,7 +16,7 @@ impl ElyShell {
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> AnyElement { ) -> AnyElement {
div() div()
.w(px(64.0)) .w(px(96.0))
.flex() .flex()
.items_center() .items_center()
.justify_end() .justify_end()
@@ -84,6 +84,7 @@ impl ElyShell {
|this| { |this| {
let open_id = entry.id().clone(); let open_id = entry.id().clone();
let reveal_id = entry.id().clone(); let reveal_id = entry.id().clone();
let checksum_id = entry.id().clone();
this.child( this.child(
download_action_button("open", index, IconName::ExternalLink, "Open File") download_action_button("open", index, IconName::ExternalLink, "Open File")
@@ -104,6 +105,20 @@ impl ElyShell {
})) }))
.into_any_element(), .into_any_element(),
) )
.when(entry.checksum().is_none(), |this| {
this.child(
download_action_button(
"checksum",
index,
IconName::CircleCheck,
"Calculate SHA-256",
)
.on_click(cx.listener(move |shell, _, _, cx| {
shell.calculate_download_checksum(&checksum_id, cx);
}))
.into_any_element(),
)
})
}, },
) )
.into_any_element() .into_any_element()
@@ -1,6 +1,6 @@
use ely_browser_core::BrowserSnapshot; use ely_browser_core::BrowserSnapshot;
use ely_design_system::colors; use ely_design_system::colors;
use ely_domain::{DownloadEntry, DownloadSecurity}; use ely_domain::{DownloadChecksum, DownloadEntry, DownloadSecurity};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div, AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div,
@@ -106,8 +106,8 @@ impl ElyShell {
), ),
), ),
) )
.when_some(self.download_file_error.clone(), |this, message| { .when_some(self.download_action_error.clone(), |this, message| {
this.child(render_download_file_error(message)) this.child(render_download_action_error(message))
}) })
.when( .when(
self.download_clear_confirmation && !snapshot.download_entries.is_empty(), self.download_clear_confirmation && !snapshot.download_entries.is_empty(),
@@ -259,6 +259,9 @@ impl ElyShell {
) )
.when(entry.security().requires_prompt(), |this| { .when(entry.security().requires_prompt(), |this| {
this.child(render_security_prompt(entry.security())) this.child(render_security_prompt(entry.security()))
})
.when_some(entry.checksum(), |this, checksum| {
this.child(render_checksum_label(checksum))
}), }),
), ),
), ),
@@ -296,7 +299,7 @@ impl ElyShell {
} }
} }
fn render_download_file_error(message: String) -> AnyElement { fn render_download_action_error(message: String) -> AnyElement {
div() div()
.rounded_md() .rounded_md()
.border_1() .border_1()
@@ -323,3 +326,18 @@ fn render_security_prompt(security: &DownloadSecurity) -> AnyElement {
.child(download_security_label(security)) .child(download_security_label(security))
.into_any_element() .into_any_element()
} }
fn render_checksum_label(checksum: &DownloadChecksum) -> AnyElement {
div()
.flex()
.items_center()
.gap_1()
.text_color(rgb(colors::SUCCESS))
.child(IconName::CircleCheck)
.child(format!("SHA-256 {}", short_checksum(checksum.value())))
.into_any_element()
}
fn short_checksum(value: &str) -> &str {
value.get(..12).unwrap_or(value)
}
+27 -5
View File
@@ -9,7 +9,10 @@ use gpui_component::input::{InputEvent, InputState, SelectAll};
use crate::{ use crate::{
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab, CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
OpenSettings, RestoreClosedTab, SelectNextTab, SelectPreviousTab, ToggleFavoriteTab, OpenSettings, RestoreClosedTab, SelectNextTab, SelectPreviousTab, ToggleFavoriteTab,
TogglePinnedTab, services::download_files::DownloadFileAction, TogglePinnedTab,
services::{
download_checksums::DownloadChecksumCalculator, download_files::DownloadFileAction,
},
}; };
enum ShellState { enum ShellState {
@@ -22,7 +25,7 @@ pub struct ElyShell {
focus_handle: FocusHandle, focus_handle: FocusHandle,
command_input: Entity<InputState>, command_input: Entity<InputState>,
last_intent: Option<CommandIntent>, last_intent: Option<CommandIntent>,
download_file_error: Option<String>, download_action_error: Option<String>,
download_clear_confirmation: bool, download_clear_confirmation: bool,
_command_subscription: Subscription, _command_subscription: Subscription,
} }
@@ -79,7 +82,7 @@ impl ElyShell {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
command_input, command_input,
last_intent: None, last_intent: None,
download_file_error: None, download_action_error: None,
download_clear_confirmation: false, download_clear_confirmation: false,
_command_subscription: command_subscription, _command_subscription: command_subscription,
} }
@@ -266,7 +269,26 @@ impl ElyShell {
core.clear_downloads_for_active_profile(); core.clear_downloads_for_active_profile();
} }
self.download_clear_confirmation = false; self.download_clear_confirmation = false;
self.download_file_error = None; self.download_action_error = None;
cx.notify();
}
fn calculate_download_checksum(&mut self, download_id: &DownloadId, cx: &mut Context<Self>) {
let result = match &mut self.state {
ShellState::Ready(core) => core
.download_target_file_path(download_id)
.map_err(|error| error.to_string())
.and_then(|path| {
DownloadChecksumCalculator::sha256(&path).map_err(|error| error.to_string())
})
.and_then(|checksum| {
core.record_download_checksum(download_id, checksum)
.map_err(|error| error.to_string())
}),
ShellState::StartupError(message) => Err(message.clone()),
};
self.download_action_error = result.err();
cx.notify(); cx.notify();
} }
@@ -292,7 +314,7 @@ impl ElyShell {
ShellState::StartupError(message) => Err(message.clone()), ShellState::StartupError(message) => Err(message.clone()),
}; };
self.download_file_error = result.err(); self.download_action_error = result.err();
cx.notify(); cx.notify();
} }