Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 73 additions & 9 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
10 changes: 6 additions & 4 deletions src-tauri/src/models/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)?;
}
}
Expand Down Expand Up @@ -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()),
})
}

Expand Down Expand Up @@ -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.
Expand Down
41 changes: 37 additions & 4 deletions src-tauri/src/models/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<D: Digest> io::Write for HashWriter<'_, D> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
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 {
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -327,8 +348,20 @@ pub fn free_disk_bytes(path: &std::path::Path) -> Option<u64> {
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();
Expand All @@ -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`.
Expand Down Expand Up @@ -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]
Expand Down