Nach den ersten Beispielskripten mit Rust migriere ich meinen Python-basierten Integritätsmonitor. Einen signifikanten Geschwindigkeitszuwachs erwarte ich nicht, weil das Skript im Wesentlichen aus I/O-Operationen und der Berechnung von Hash-Werten besteht (und Python sich diesbezüglich auf eine C-Bibliothek stützt). Allerdings beansprucht die Rust-Version rund dreimal so viel Zeit (4 vs. 12 Sekunden), was nicht gut zum Produktversprechen der Sprache passt. Es zeigt sich, dass der Wrapper sha256
mit der Methode try_digest()
sehr komfortabel, aber auch enorm langsam ist:
use sha256::try_digest; use std::path::Path; fn get_hash(filepath: &str) -> String { let input = Path::new(filepath); let hasher = try_digest(input).unwrap(); hasher }
Meine Anfrage im Rust-Forum führt binnen Minuten zu Analysen (fixed buffer size
als Wettbewerbsvorteil für Python) und Lösungsvorschlägen (Verwendung von sha2
in Verbindung mit buffer streaming). Den Durchbruch bringen der Verweis auf die Assembly-Implementierung von Hash-Funktionen in sha2
(sha2 = { version = "0.10.6", features = ["asm", "asm-aarch64"] }
bzw. in ring
:
use ring::digest; use sha2::{Digest, Sha256}; use hex; fn get_hash_sha2(filepath: &str) -> String { let bytes = std::fs::read(filepath).unwrap(); let mut h = Sha256::new(); h.update(&bytes[..]); hex::encode(h.finalize()) } fn get_hash_ring(filepath: &str) -> String { let bytes = std::fs::read(filepath).unwrap(); let hasher = digest::digest(&digest::SHA256, &bytes); hex::encode(hasher) }
Ich entscheide mich für die (minimal schnellere) ring
-Variante:
use ring::digest; use hex; use walkdir::{DirEntry, WalkDir}; use std::fs; use std::collections::HashMap; use serde_json; use std::io; const HASHPATH: &str = "/Users/snafu/scripts/hashes_rust.json"; const DOCPATH: &str = "/Users/snafu/Documents"; fn get_hash(filepath: &str) -> String { let bytes = std::fs::read(filepath).unwrap(); let hasher = digest::digest(&digest::SHA256, &bytes); hex::encode(hasher) } fn is_hidden(entry: &DirEntry) -> bool { entry.file_name() .to_str() .map(|s| s.starts_with(".")) .unwrap_or(false) } fn create_baseline() { let file_content = fs::read_to_string(HASHPATH).unwrap(); let mut hash_storage: HashMap<String, String> = serde_json::from_str(&file_content).unwrap(); for entry in WalkDir::new(DOCPATH).follow_links(true).into_iter().filter_entry(|e| !is_hidden(e)) { let file = entry.unwrap(); let metadata = match fs::metadata(&file.path()) { Ok(value) => value, Err(_) => continue, }; if metadata.is_file() { let filepath = file.path().display().to_string(); let hash = get_hash(&filepath); if hash_storage.contains_key(&filepath) { if hash_storage[&filepath] == hash { continue; } else { println!("Hash for file '{filepath}' does not match baseline. Update hash and proceed (y) or cancel (n)? "); let mut overwrite = String::new(); io::stdin().read_line(&mut overwrite).expect("Failed to read line"); match overwrite.trim() { "y" => (), _ => continue, }; }; }; println!("Updating hash/creating new hash for file '{filepath}'..."); *hash_storage.entry(filepath).or_insert(hash) = hash.clone(); }; }; let mut to_remove = Vec::new(); for absolute_path in hash_storage.keys() { match fs::metadata(&absolute_path) { Ok(_) => continue, Err(_) => { println!("Removing hash for deleted file {absolute_path}..."); to_remove.push(absolute_path.to_owned()); }, }; } for key in to_remove.iter() { hash_storage.remove(key); } let serialized = serde_json::to_string(&hash_storage).unwrap(); fs::write(HASHPATH, serialized).expect("Unable to write file"); } fn main() { create_baseline(); }
Das Skript ist mit der Python-Version nahezu identisch, abgesehen von der Entfernung nicht mehr benötigter Hashes aus hash_storage
. Weil die Methode keys()
eine mutable reference auf hash_storage
erzeugt, kann das Objekt nicht innerhalb der for
-Schleife modifiziert werden. Stattdessen wird ein Hilfsobjekt vom Typ Vec<String>
mit den zu entfernenden Einträgen gefüllt und in einer zweiten for
-Schleife abgearbeitet.
Der Aufwand hat sich jedenfalls gelohnt:
snafu@local hasher % /usr/bin/time -h /usr/local/bin/hasher.py 3.95s real 1.33s user 0.78s sys snafu@local hasher % /usr/bin/time -h target/release/hasher 2.13s real 1.23s user 0.57s sys
Darüber hinaus verleiht mir die freundliche Rust-Gemeinschaft den New User of the Month award for February 2023
:
This badge is granted to congratulate two new users each month for their excellent overall contributions, as measured by how often their posts were liked, and by whom.
Ich bin sehr gerührt.