Skip to main content

max / audiofiles

Upgrade sha2 to 0.11 sha2 0.11 moves the digest output from generic-array to hybrid-array, which does not implement LowerHex, so every format!("{:x}", ..) over a digest is now hex::encode. Same lowercase hex, same content addresses. Pin the store's content address against an independent SHA-256 known answer, so a future hasher or encoding swap has to match a fixed string rather than only agree with itself. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-22 00:28 UTC
Commit: 178eff39a2b8db141100c0ba65e59dfb61db5dd7
Parent: cfcfe21
8 files changed, +101 insertions, -24 deletions
M Cargo.lock +69 -12
@@ -23,7 +23,7 @@ version = "0.5.2"
23 23 source = "registry+https://github.com/rust-lang/crates.io-index"
24 24 checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
25 25 dependencies = [
26 - "crypto-common",
26 + "crypto-common 0.1.7",
27 27 "generic-array",
28 28 ]
29 29
@@ -421,6 +421,7 @@ version = "0.5.0"
421 421 dependencies = [
422 422 "bs1770",
423 423 "dirs",
424 + "hex",
424 425 "hound",
425 426 "rayon",
426 427 "realfft",
@@ -428,7 +429,7 @@ dependencies = [
428 429 "rusqlite",
429 430 "serde",
430 431 "serde_json",
431 - "sha2",
432 + "sha2 0.11.0",
432 433 "stratum-dsp",
433 434 "symphonia",
434 435 "tagtree",
@@ -458,13 +459,14 @@ dependencies = [
458 459 "audiofiles-core",
459 460 "base64",
460 461 "chrono",
462 + "hex",
461 463 "open",
462 464 "parking_lot",
463 465 "rand",
464 466 "rusqlite",
465 467 "serde",
466 468 "serde_json",
467 - "sha2",
469 + "sha2 0.11.0",
468 470 "synckit-client",
469 471 "tempfile",
470 472 "thiserror 2.0.19",
@@ -527,7 +529,7 @@ version = "0.10.6"
527 529 source = "registry+https://github.com/rust-lang/crates.io-index"
528 530 checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
529 531 dependencies = [
530 - "digest",
532 + "digest 0.10.7",
531 533 ]
532 534
533 535 [[package]]
@@ -540,6 +542,15 @@ dependencies = [
540 542 ]
541 543
542 544 [[package]]
545 + name = "block-buffer"
546 + version = "0.12.1"
547 + source = "registry+https://github.com/rust-lang/crates.io-index"
548 + checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
549 + dependencies = [
550 + "hybrid-array",
551 + ]
552 +
553 + [[package]]
543 554 name = "block-padding"
544 555 version = "0.3.3"
545 556 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -818,7 +829,7 @@ version = "0.4.4"
818 829 source = "registry+https://github.com/rust-lang/crates.io-index"
819 830 checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
820 831 dependencies = [
821 - "crypto-common",
832 + "crypto-common 0.1.7",
822 833 "inout",
823 834 "zeroize",
824 835 ]
@@ -870,6 +881,12 @@ dependencies = [
870 881 ]
871 882
872 883 [[package]]
884 + name = "const-oid"
885 + version = "0.10.2"
886 + source = "registry+https://github.com/rust-lang/crates.io-index"
887 + checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
888 +
889 + [[package]]
873 890 name = "const-random"
874 891 version = "0.1.18"
875 892 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1083,6 +1100,15 @@ dependencies = [
1083 1100 ]
1084 1101
1085 1102 [[package]]
1103 + name = "crypto-common"
1104 + version = "0.2.2"
1105 + source = "registry+https://github.com/rust-lang/crates.io-index"
1106 + checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
1107 + dependencies = [
1108 + "hybrid-array",
1109 + ]
1110 +
1111 + [[package]]
1086 1112 name = "cursor-icon"
1087 1113 version = "1.2.0"
1088 1114 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1100,12 +1126,23 @@ version = "0.10.7"
1100 1126 source = "registry+https://github.com/rust-lang/crates.io-index"
1101 1127 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
1102 1128 dependencies = [
1103 - "block-buffer",
1104 - "crypto-common",
1129 + "block-buffer 0.10.4",
1130 + "crypto-common 0.1.7",
1105 1131 "subtle",
1106 1132 ]
1107 1133
1108 1134 [[package]]
1135 + name = "digest"
1136 + version = "0.11.3"
1137 + source = "registry+https://github.com/rust-lang/crates.io-index"
1138 + checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
1139 + dependencies = [
1140 + "block-buffer 0.12.1",
1141 + "const-oid",
1142 + "crypto-common 0.2.2",
1143 + ]
1144 +
1145 + [[package]]
1109 1146 name = "dirs"
1110 1147 version = "6.0.0"
1111 1148 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2150,7 +2187,7 @@ version = "0.12.1"
2150 2187 source = "registry+https://github.com/rust-lang/crates.io-index"
2151 2188 checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
2152 2189 dependencies = [
2153 - "digest",
2190 + "digest 0.10.7",
2154 2191 ]
2155 2192
2156 2193 [[package]]
@@ -2199,6 +2236,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
2199 2236 checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
2200 2237
2201 2238 [[package]]
2239 + name = "hybrid-array"
2240 + version = "0.4.13"
2241 + source = "registry+https://github.com/rust-lang/crates.io-index"
2242 + checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c"
2243 + dependencies = [
2244 + "typenum",
2245 + ]
2246 +
2247 + [[package]]
2202 2248 name = "hyper"
2203 2249 version = "1.11.0"
2204 2250 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4421,7 +4467,7 @@ dependencies = [
4421 4467 "num",
4422 4468 "once_cell",
4423 4469 "serde",
4424 - "sha2",
4470 + "sha2 0.10.9",
4425 4471 "zbus",
4426 4472 ]
4427 4473
@@ -4543,7 +4589,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
4543 4589 dependencies = [
4544 4590 "cfg-if",
4545 4591 "cpufeatures 0.2.17",
4546 - "digest",
4592 + "digest 0.10.7",
4593 + ]
4594 +
4595 + [[package]]
4596 + name = "sha2"
4597 + version = "0.11.0"
4598 + source = "registry+https://github.com/rust-lang/crates.io-index"
4599 + checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
4600 + dependencies = [
4601 + "cfg-if",
4602 + "cpufeatures 0.3.0",
4603 + "digest 0.11.3",
4547 4604 ]
4548 4605
4549 4606 [[package]]
@@ -5025,7 +5082,7 @@ dependencies = [
5025 5082 "rusqlite",
5026 5083 "serde",
5027 5084 "serde_json",
5028 - "sha2",
5085 + "sha2 0.10.9",
5029 5086 "thiserror 2.0.19",
5030 5087 "tokio",
5031 5088 "tokio-stream",
@@ -5573,7 +5630,7 @@ version = "0.5.1"
5573 5630 source = "registry+https://github.com/rust-lang/crates.io-index"
5574 5631 checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
5575 5632 dependencies = [
5576 - "crypto-common",
5633 + "crypto-common 0.1.7",
5577 5634 "subtle",
5578 5635 ]
5579 5636
M Cargo.toml +2 -1
@@ -18,7 +18,8 @@ eframe = { version = "0.34", default-features = false, features = ["default_font
18 18 cpal = "0.17"
19 19 rusqlite = { version = "0.39", features = ["bundled", "functions"] }
20 20 thiserror = "2.0.18"
21 - sha2 = "0.10.9"
21 + sha2 = "0.11.0"
22 + hex = "0.4.3"
22 23 symphonia = { version = "0.5.5", default-features = false, features = ["wav", "aiff", "mp3", "flac", "ogg", "vorbis", "pcm", "aac", "alac", "isomp4", "caf"] }
23 24 parking_lot = "0.12.5"
24 25 dirs = "6.0.0"
@@ -11,6 +11,7 @@ analysis = ["dep:bs1770", "dep:realfft"]
11 11 rusqlite = { workspace = true }
12 12 thiserror = { workspace = true }
13 13 sha2 = { workspace = true }
14 + hex = { workspace = true }
14 15 symphonia = { workspace = true }
15 16 stratum-dsp = { workspace = true }
16 17 serde = { workspace = true }
@@ -90,7 +90,7 @@ impl SampleHash {
90 90 ///
91 91 /// `store::validate_hash` delegates here, so there is one rule set rather than
92 92 /// two that can drift. Lowercase-only matches how the app produces hashes
93 - /// (`format!("{:x}")`), so every real hash passes and uppercase is rejected.
93 + /// (`hex::encode`), so every real hash passes and uppercase is rejected.
94 94 pub fn validated(s: &str) -> std::result::Result<Self, &'static str> {
95 95 if s.len() != 64
96 96 || !s
@@ -126,7 +126,7 @@ pub fn relocate_missing_loose_files(db: &Database, search_root: &Path) -> Result
126 126 if !ok {
127 127 continue;
128 128 }
129 - let computed = format!("{:x}", hasher.finalize());
129 + let computed = hex::encode(hasher.finalize());
130 130 if computed == *hash {
131 131 let abs = cand
132 132 .canonicalize()
@@ -442,7 +442,7 @@ impl SampleStore {
442 442 }
443 443 hasher.update(&buf[..n]);
444 444 }
445 - let computed = format!("{:x}", hasher.finalize());
445 + let computed = hex::encode(hasher.finalize());
446 446
447 447 Ok(computed == hash)
448 448 }
@@ -769,7 +769,7 @@ pub fn relocate_sample(
769 769 }
770 770 hasher.update(&buf[..n]);
771 771 }
772 - let computed = format!("{:x}", hasher.finalize());
772 + let computed = hex::encode(hasher.finalize());
773 773
774 774 if computed != hash {
775 775 return Err(CoreError::Internal(format!(
@@ -827,7 +827,7 @@ pub fn hash_file(path: &Path) -> Result<(String, i64)> {
827 827 }
828 828 hasher.update(&buf[..n]);
829 829 }
830 - Ok((format!("{:x}", hasher.finalize()), file_size))
830 + Ok((hex::encode(hasher.finalize()), file_size))
831 831 }
832 832
833 833 /// Copy `src` to `dst` while computing the SHA-256 of the bytes actually
@@ -850,7 +850,7 @@ fn copy_hashing(src: &Path, dst: &Path) -> Result<String> {
850 850 output.write_all(&buf[..n]).map_err(|e| io_err(dst, e))?;
851 851 }
852 852 output.sync_all().map_err(|e| io_err(dst, e))?;
853 - Ok(format!("{:x}", hasher.finalize()))
853 + Ok(hex::encode(hasher.finalize()))
854 854 }
855 855
856 856 /// Hash a batch of files in parallel (rayon). Each result is aligned to the
@@ -1101,6 +1101,23 @@ mod tests {
1101 1101 }
1102 1102
1103 1103 #[test]
1104 + fn hash_file_matches_the_reference_sha256_digest() {
1105 + // The content address is the library's primary key, so the exact hex
1106 + // string a given byte sequence produces is a compatibility guarantee:
1107 + // change it and every stored path and database row stops resolving.
1108 + // Pinned against an independent SHA-256 of the same bytes, so a hasher
1109 + // or hex-encoding swap has to survive a known answer, not just agree
1110 + // with itself.
1111 + let (dir, _db, _store) = setup();
1112 + let src = create_test_file(&dir, "kick.wav", b"fake audio data");
1113 + let (hash, _) = hash_file(&src).unwrap();
1114 + assert_eq!(
1115 + hash, "cec560f942befcb4e4a4d1161c5c03b3a787e2d525f650042641e62bf8773c69",
1116 + "SHA-256 of b\"fake audio data\", lowercase hex, no separators"
1117 + );
1118 + }
1119 +
1120 + #[test]
1104 1121 fn import_hashed_matches_serial_import() {
1105 1122 let (dir, db, store) = setup();
1106 1123 let src = create_test_file(&dir, "snare.wav", b"some audio bytes");
@@ -1221,7 +1238,7 @@ mod tests {
1221 1238 let src = create_test_file(&dir, "kick.wav", content);
1222 1239
1223 1240 // Pre-place a truncated blob at the canonical path the real import targets.
1224 - let hash = format!("{:x}", Sha256::digest(content));
1241 + let hash = hex::encode(Sha256::digest(content));
1225 1242 let dest = store.sample_path(&hash, "wav").unwrap();
1226 1243 fs::create_dir_all(dest.parent().unwrap()).unwrap();
1227 1244 fs::write(&dest, b"trunc").unwrap();
@@ -1233,7 +1250,7 @@ mod tests {
1233 1250 let stored = fs::read(&dest).unwrap();
1234 1251 assert_eq!(stored, content, "truncated blob must be repaired on import");
1235 1252 assert_eq!(
1236 - format!("{:x}", Sha256::digest(&stored)),
1253 + hex::encode(Sha256::digest(&stored)),
1237 1254 hash,
1238 1255 "repaired blob hashes back to its content address"
1239 1256 );
@@ -1377,7 +1394,7 @@ mod tests {
1377 1394 // committed to `{wrong_hash}.ext`.
1378 1395 let (dir, db, store) = setup();
1379 1396 let src = create_test_file(&dir, "kick.wav", b"the actual bytes on disk");
1380 - let wrong_hash = format!("{:x}", Sha256::digest(b"what we hashed earlier"));
1397 + let wrong_hash = hex::encode(Sha256::digest(b"what we hashed earlier"));
1381 1398
1382 1399 let result = store.import_hashed(&src, &wrong_hash, 24, &db);
1383 1400 assert!(
@@ -14,6 +14,7 @@ serde_json = { workspace = true }
14 14 tracing = { workspace = true }
15 15 uuid = { workspace = true }
16 16 sha2 = { workspace = true }
17 + hex = { workspace = true }
17 18 rand = { workspace = true }
18 19 base64 = { workspace = true }
19 20 thiserror = { workspace = true }
@@ -114,7 +114,7 @@ pub async fn download_missing_blobs(
114 114 };
115 115
116 116 // Verify SHA-256 before writing
117 - let computed = format!("{:x}", Sha256::digest(&data));
117 + let computed = hex::encode(Sha256::digest(&data));
118 118 if computed != *hash {
119 119 tracing::warn!(
120 120 expected = hash,
@@ -212,7 +212,7 @@ pub async fn download_one_blob(
212 212 .await
213 213 .map_err(|e| SyncError::Client(e.to_string()))?;
214 214
215 - let computed = format!("{:x}", Sha256::digest(&data));
215 + let computed = hex::encode(Sha256::digest(&data));
216 216 if computed != *hash {
217 217 return Err(SyncError::Other(format!(
218 218 "Downloaded blob hash mismatch (expected {hash}, got {computed})"