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-component",
"gpui-component-assets",
"sha2",
"thiserror 2.0.18",
]
+1
View File
@@ -19,6 +19,7 @@ gpui = "0.2.2"
gpui-component = "0.5.1"
gpui-component-assets = "0.5.1"
servo = "0.1.0"
sha2 = "0.10.9"
thiserror = "2.0.12"
url = "2.5.4"
uuid = { version = "1.12.1", features = ["v7"] }
+1
View File
@@ -12,6 +12,7 @@ ely_domain = { path = "../ely_domain" }
gpui.workspace = true
gpui-component.workspace = true
gpui-component-assets.workspace = true
sha2.workspace = true
thiserror.workspace = true
[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;
@@ -16,7 +16,7 @@ impl ElyShell {
cx: &mut Context<Self>,
) -> AnyElement {
div()
.w(px(64.0))
.w(px(96.0))
.flex()
.items_center()
.justify_end()
@@ -84,6 +84,7 @@ impl ElyShell {
|this| {
let open_id = entry.id().clone();
let reveal_id = entry.id().clone();
let checksum_id = entry.id().clone();
this.child(
download_action_button("open", index, IconName::ExternalLink, "Open File")
@@ -104,6 +105,20 @@ impl ElyShell {
}))
.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()
@@ -1,6 +1,6 @@
use ely_browser_core::BrowserSnapshot;
use ely_design_system::colors;
use ely_domain::{DownloadEntry, DownloadSecurity};
use ely_domain::{DownloadChecksum, DownloadEntry, DownloadSecurity};
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, Context, InteractiveElement, IntoElement, ParentElement, SharedString, Styled, div,
@@ -106,8 +106,8 @@ impl ElyShell {
),
),
)
.when_some(self.download_file_error.clone(), |this, message| {
this.child(render_download_file_error(message))
.when_some(self.download_action_error.clone(), |this, message| {
this.child(render_download_action_error(message))
})
.when(
self.download_clear_confirmation && !snapshot.download_entries.is_empty(),
@@ -259,6 +259,9 @@ impl ElyShell {
)
.when(entry.security().requires_prompt(), |this| {
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()
.rounded_md()
.border_1()
@@ -323,3 +326,18 @@ fn render_security_prompt(security: &DownloadSecurity) -> AnyElement {
.child(download_security_label(security))
.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::{
CloseCurrentTab, FocusAddressBar, FocusCommandMode, OpenDownloads, OpenHistory, OpenNewTab,
OpenSettings, RestoreClosedTab, SelectNextTab, SelectPreviousTab, ToggleFavoriteTab,
TogglePinnedTab, services::download_files::DownloadFileAction,
TogglePinnedTab,
services::{
download_checksums::DownloadChecksumCalculator, download_files::DownloadFileAction,
},
};
enum ShellState {
@@ -22,7 +25,7 @@ pub struct ElyShell {
focus_handle: FocusHandle,
command_input: Entity<InputState>,
last_intent: Option<CommandIntent>,
download_file_error: Option<String>,
download_action_error: Option<String>,
download_clear_confirmation: bool,
_command_subscription: Subscription,
}
@@ -79,7 +82,7 @@ impl ElyShell {
focus_handle: cx.focus_handle(),
command_input,
last_intent: None,
download_file_error: None,
download_action_error: None,
download_clear_confirmation: false,
_command_subscription: command_subscription,
}
@@ -266,7 +269,26 @@ impl ElyShell {
core.clear_downloads_for_active_profile();
}
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();
}
@@ -292,7 +314,7 @@ impl ElyShell {
ShellState::StartupError(message) => Err(message.clone()),
};
self.download_file_error = result.err();
self.download_action_error = result.err();
cx.notify();
}