//! Blob utilities which do not require RPC calls. //! //! Offers the following functions: //! //! - [`get_blob_path()`] //! - [`hash_blob()`] use base64; use sha2::{Digest, Sha256}; use crate::error::GolgiError; /// Lookup the filepath for a blob. /// /// # Example /// /// ```rust /// use golgi::{blobs, GolgiError}; /// /// fn lookup_blob_path() -> Result<(), GolgiError> { /// let blob_ref = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256"; /// /// let blob_path = blobs::get_blob_path(blob_ref)?; /// /// println!("{}", blob_path); /// /// // 26/524773dc9e1b5129640f5f20f18bcd42831f415e477f408b9edeba1293d46f /// /// Ok(()) /// } /// ``` pub fn get_blob_path(blob_id: &str) -> Result { // ensure the id starts with the correct sigil link if !blob_id.starts_with('&') { return Err(GolgiError::SigilLink(format!( "incorrect first character, expected '&' sigil: {}", blob_id ))); } // find the dot index denoting the start of the algorithm definition tag let dot_index = blob_id .rfind('.') .ok_or_else(|| GolgiError::SigilLink(format!("no dot index was found: {}", blob_id)))?; // obtain the base64 portion (substring) of the blob id let base64_str = &blob_id[1..dot_index]; // decode blob substring from base64 (to bytes) let blob_bytes = base64::decode_config(base64_str, base64::STANDARD)?; // represent the blob bytes as hex, removing all unnecessary characters let blob_hex = format!("{:02x?}", blob_bytes) .replace('[', "") .replace(']', "") .replace(',', "") .replace(' ', ""); // split the hex representation of the decoded base64 // this is how paths are formatted for the blobstore // e.g. 26/524773dc9e1b5129640f5f20f18bcd42831f415e477f408b9edeba1293d46f // full path would be: `/home/user/.ssb-go/blobs/sha256/26/524773dc...` let blob_path = format!("{}/{}", &blob_hex[..2], &blob_hex[2..]); Ok(blob_path) } /// Hash the given blob byte slice and return the hex representation and /// blob ID (sigil-link). /// /// This function can be used when adding a blob to the local blobstore. pub fn hash_blob(buffer: &[u8]) -> Result<(String, String), GolgiError> { // hash the bytes let hash = Sha256::digest(buffer); // generate a hex representation of the hash let hex_hash = format!("{:02x?}", hash) .replace('[', "") .replace(']', "") .replace(',', "") .replace(' ', ""); // encode the hash let b64_hash = base64::encode(&hash[..]); // format the base64 encoding as a blob sigil-link (blob id) let blob_id = format!("&{}.sha256", b64_hash); Ok((hex_hash, blob_id)) } #[cfg(test)] mod tests { use crate::blobs; /* HAPPY PATH TESTS */ #[test] fn blob_path() { let blob_ref = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256"; let blob_path = blobs::get_blob_path(blob_ref); assert!(blob_path.is_ok()); assert_eq!( blob_path.unwrap(), "26/524773dc9e1b5129640f5f20f18bcd42831f415e477f408b9edeba1293d46f" ); } #[test] fn hashed_blob() { // pretend this represents a file which has been written to a buffer let buffer = vec![7; 128]; let hash_result = blobs::hash_blob(&buffer); assert!(hash_result.is_ok()); let (hex_hash, blob_id) = hash_result.unwrap(); assert_eq!( hex_hash, "4c1398e54d53e925cff04da532f4bbaf15f75b5981fc76c2072dfdc6491a9fb1" ); assert_eq!( blob_id, "&TBOY5U1T6SXP8E2lMvS7rxX3W1mB/HbCBy39xkkan7E=.sha256" ); } /* SAD PATH TESTS */ #[test] fn blob_id_without_sigil() { let blob_ref = "JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256"; match blobs::get_blob_path(blob_ref) { Err(e) => assert_eq!(e.to_string(), "SSB blob ID error: incorrect first character, expected '&' sigil: JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8=.sha256"), _ => panic!(), } } #[test] fn blob_id_without_algo() { let blob_ref = "&JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8="; match blobs::get_blob_path(blob_ref) { Err(e) => assert_eq!(e.to_string(), "SSB blob ID error: no dot index was found: &JlJHc9yeG1EpZA9fIPGLzUKDH0FeR39Ai57euhKT1G8="), _ => panic!(), } } }