diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5763c00e..a4ae50fc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -210,6 +210,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -454,6 +463,12 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "cookie" version = "0.18.1" @@ -523,6 +538,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -557,6 +581,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cssparser" version = "0.31.2" @@ -753,8 +786,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -1721,6 +1765,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -4042,8 +4095,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -4480,7 +4544,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "syn 2.0.117", "tauri-utils", "thiserror 2.0.18", @@ -4809,7 +4873,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", + "sha2 0.11.0", "tauri", "tauri-build", "tauri-nspanel", @@ -5162,9 +5226,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "tz-rs" @@ -6232,7 +6296,7 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.18", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 26a28841..45a685b6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -47,7 +47,7 @@ time = { version = "0.3", features = ["std", "local-offset", "macros", "parsing" tz-rs = "0.7" async-trait = "0.1" semver = "1" -sha2 = "0.10" +sha2 = "0.11" libc = "0.2" keyring = { version = "3", features = ["apple-native"] } diff --git a/src-tauri/src/models/download.rs b/src-tauri/src/models/download.rs index b307a934..ca8b748b 100644 --- a/src-tauri/src/models/download.rs +++ b/src-tauri/src/models/download.rs @@ -28,7 +28,7 @@ use futures_util::StreamExt; use tokio::sync::Semaphore; use tokio_util::sync::CancellationToken; -use super::storage::{ModelStore, StorageError}; +use super::storage::{hex_digest, HashWriter, ModelStore, StorageError}; use crate::config::defaults::DOWNLOAD_PROGRESS_MIN_INTERVAL_MS; /// Progress events streamed to the frontend while a model downloads. @@ -628,7 +628,9 @@ async fn fetch_into_partial( file: spec.file.clone(), }); store - .feed_partial(&spec.sha256, &mut hasher, &|| cancel.is_cancelled()) + .feed_partial(&spec.sha256, &mut HashWriter(&mut hasher), &|| { + cancel.is_cancelled() + }) .map_err(DownloadIoError::Write)?; } } @@ -698,7 +700,7 @@ async fn fetch_into_partial( } file.flush().map_err(DownloadIoError::Write)?; Ok(FetchOutcome::Done { - sha256: format!("{:x}", hasher.finalize()), + sha256: hex_digest(&hasher.finalize()), }) } @@ -824,7 +826,7 @@ mod tests { /// Compute the hex SHA-256 of `data`. fn sha256_of(data: &[u8]) -> String { - format!("{:x}", Sha256::digest(data)) + hex_digest(&Sha256::digest(data)) } /// Event sink: returns the shared event log and an `emit` closure. diff --git a/src-tauri/src/models/storage.rs b/src-tauri/src/models/storage.rs index cc6ec6c1..8adf6339 100644 --- a/src-tauri/src/models/storage.rs +++ b/src-tauri/src/models/storage.rs @@ -25,6 +25,27 @@ use sha2::{Digest, Sha256}; use super::HfGgufPart; use crate::config::defaults::BLOB_HASH_BUFFER_BYTES; +/// Renders a SHA-256 digest as a lowercase hex string. +pub(crate) fn hex_digest(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Adapts a [`Digest`] hasher to [`io::Write`] so it can be used as a +/// [`ModelStore::feed_partial`] sink. sha2 0.11 dropped hashers' own `Write` +/// impl; this forwards each write straight into [`Digest::update`]. +pub(crate) struct HashWriter<'a, D: Digest>(pub &'a mut D); + +impl io::Write for HashWriter<'_, D> { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.update(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + /// Errors returned by [`ModelStore`] operations. #[derive(Debug, thiserror::Error)] pub enum StorageError { @@ -165,8 +186,8 @@ impl ModelStore { let mut hasher = Sha256::new(); // A full-length-partial verify always runs to completion: there is no // pause surface for it, so it never cancels. - self.feed_partial(sha256, &mut hasher, &|| false)?; - let actual = format!("{:x}", hasher.finalize()); + self.feed_partial(sha256, &mut HashWriter(&mut hasher), &|| false)?; + let actual = hex_digest(&hasher.finalize()); self.install_if_matches(sha256, &actual) } @@ -327,8 +348,20 @@ pub fn free_disk_bytes(path: &std::path::Path) -> Option { mod tests { use super::*; use sha2::{Digest, Sha256}; + use std::io::Write as _; use tempfile::TempDir; + #[test] + fn hash_writer_forwards_writes_and_flush_is_a_no_op() { + let mut hasher = Sha256::new(); + { + let mut writer = HashWriter(&mut hasher); + writer.write_all(b"abc").unwrap(); + writer.flush().unwrap(); + } + assert_eq!(hex_digest(&hasher.finalize()), sha256_of(b"abc")); + } + /// Build a fresh store rooted at a temporary directory. fn make_store() -> (TempDir, ModelStore) { let dir = TempDir::new().unwrap(); @@ -338,7 +371,7 @@ mod tests { /// Compute the hex SHA-256 of `data`. fn sha256_of(data: &[u8]) -> String { - format!("{:x}", Sha256::digest(data)) + hex_digest(&Sha256::digest(data)) } /// Write `data` into the store's partial slot for `sha256`. @@ -467,7 +500,7 @@ mod tests { // result must equal hashing the whole stream in one pass. let mut taken = store.take_suspended_hash("aa", 3).unwrap(); taken.update(b"def"); - assert_eq!(format!("{:x}", taken.finalize()), sha256_of(b"abcdef")); + assert_eq!(hex_digest(&taken.finalize()), sha256_of(b"abcdef")); } #[test]