// // lib.rs // Copyright (C) 2023-2024 Óscar García Amor // Distributed under terms of the GNU GPLv3 license. // #[macro_use] extern crate rocket; use std::fs::{ copy, create_dir, create_dir_all, read_to_string, remove_dir_all, remove_file, rename, write, File, }; use std::io::{self, Error, ErrorKind, Read}; use std::path::{Path, PathBuf}; use rand::seq::SliceRandom; use rocket::data::Capped; use rocket::fs::TempFile; use rocket::http::uri::Host; use rocket::serde::json; use syntect::easy::HighlightLines; use syntect::highlighting::{Color, Style, ThemeSet}; use syntect::html::{styled_line_to_highlighted_html, IncludeBackground}; use syntect::parsing::SyntaxSet; use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings}; use rocket::tokio::fs::File as TokioFile; use time::{Duration, OffsetDateTime}; pub mod models; use crate::models::LesmaMeta; /// Defines clients requesting information in plain text. pub const PLAIN_TEXT_AGENTS: [&str; 5] = ["curl", "httpie", "lwp-request", "wget", "python-requests"]; /// Simple struct to store an incremental hasher for BLAKE3. struct B3Hasher(blake3::Hasher); impl B3Hasher { /// Add input to the hash. fn update(&mut self, input: &[u8]) { self.0.update(input); } /// Finalize the state and return a hash string. fn finalize(&mut self) -> String { self.0.finalize().to_hex().to_string() } } /// Calculates BLAKE3 hash of a file fn hasher(mut reader: R, state: &mut B3Hasher) -> io::Result<()> { let mut buf = [0; 32768]; loop { match reader.read(&mut buf) { Ok(0) => return Ok(()), Ok(n) => state.update(&buf[..n]), Err(e) => return Err(e) } } } /// Struct for read binary file with its name pub struct BinaryFile(pub PathBuf, pub TokioFile, pub String); impl BinaryFile { pub async fn open>(path: P, name: String) -> io::Result { let file = TokioFile::open(path.as_ref()).await?; Ok(BinaryFile(path.as_ref().to_path_buf(), file, name)) } } /// Struct for lesma manager pub struct Lesma { pub id: String, pub hash: String, pub storage: PathBuf, } /// lesma manager implementation /// /// # Example /// /// ``` /// use lesma::Lesma; /// /// // To create a new lesma with random ID and hash /// let lesma = Lesma::new(std::env::temp_dir()); /// /// // To create a new lesma from a given ID /// let lesma = Lesma::from(std::env::temp_dir(), "lesma"); /// ``` impl Lesma { /// Returns new lesma manager /// /// # Example /// /// ``` /// use lesma::Lesma; /// /// let temp_dir = std::env::temp_dir(); /// let lesma = Lesma::new(temp_dir); /// assert!(lesma.id.len() >= 4); /// assert_eq!(64, lesma.hash.len()); /// ``` pub fn new(storage: PathBuf) -> Lesma { // Get new ID and hash let (id, hash) = new_hash(&storage); Lesma { id, hash, storage } } /// Returns new lesma manager from given ID /// /// # Example /// /// ``` /// use lesma::Lesma; /// /// let temp_dir = std::env::temp_dir(); /// let id = "lesma"; /// let hash = "d887e6b1413f5dc4892bb390438e033eaf5dab94f9064eee73f7ba64f702f1ec"; /// let lesma = Lesma::from(temp_dir, id); /// assert_eq!(id.to_string(), lesma.id); /// assert_eq!(hash.to_string(), lesma.hash); /// ``` pub fn from>(storage: PathBuf, id: S) -> Lesma { let id = id.into(); let hash = blake3::hash(id.as_bytes()).to_hex().to_string(); Lesma { id, hash, storage } } /// Returns new lesma manager from given ID and hash /// /// # Example /// /// ``` /// use lesma::Lesma; /// /// let temp_dir = std::env::temp_dir(); /// let id = "lesma"; /// let hash = "d887e6b1413f5dc4892bb390438e033eaf5dab94f9064eee73f7ba64f702f1ec"; /// let lesma = Lesma::from_id_hash(temp_dir, id, hash); /// assert_eq!(id.to_string(), lesma.id); /// assert_eq!(hash.to_string(), lesma.hash); /// ``` pub fn from_id_hash>(storage: PathBuf, id: S, hash: S) -> Lesma { let id = id.into(); let hash = hash.into(); Lesma { id, hash, storage } } /// Saves a binary data lesma pub async fn save_data(&self, temp: &Path, limit: u8, expire: u32, password: Option, data: &mut Capped>) -> Result<(), Error> { // Get the raw name of the uploaded file (safe to unwrap since it has checked before) let raw_name = data.raw_name().unwrap().dangerous_unsafe_unsanitized_raw().as_str(); let file_path_buf = PathBuf::from(raw_name); // Get sanitized file name let file_name = file_path_buf.file_name().ok_or(Error::new(ErrorKind::InvalidData, "Invalid filename"))?.to_str().unwrap(); // A temporary file is needed to store the data received in the request, the new_hash function // can be used for this purpose let (_, temp_file_name) = new_hash(temp); let temp_file = temp.join(&temp_file_name); if data.is_complete() { // Data are persisted to the temporary file data.persist_to(&temp_file).await?; } else { // The file is larger than allowed in the configuration return Err(Error::new(ErrorKind::BrokenPipe, "File too large")) } // Calculate BLAKE3 hash of file let mut file = File::open(&temp_file)?; let mut b3hasher = B3Hasher(blake3::Hasher::new()); hasher(&mut file, &mut b3hasher)?; let lesma_hash = b3hasher.finalize(); // Get data directory and counter, data and meta files let data_dir = self.storage.join("data").join(&lesma_hash); let counter_file = data_dir.join("_counter"); let data_file = data_dir.join("_data"); let meta_file = self.storage.join("meta").join(&self.hash); // Fill in the metadata let lesma_meta = LesmaMeta { hash: lesma_hash, password: password.map(|p| blake3::hash(p.as_bytes()).to_hex().to_string()), name: file_name.to_string(), binary: true, limit, current: 0, create: OffsetDateTime::now_utc(), expire: OffsetDateTime::now_utc() + Duration::hours(expire.into()) }; // Serialize metadata in JSON let meta_json = json::to_string(&lesma_meta)?; // Stores the metadata in the metadata file write(meta_file, meta_json)?; // Check if the data already exists if data_dir.is_dir() { // Add +1 to counter file let mut counter = read_to_string(&counter_file)?.trim().parse::().map_err(|e| Error::new(ErrorKind::InvalidData, e))?; counter += 1; write(counter_file, counter.to_string())?; } else { // Create new data directory create_dir(&data_dir)?; // Try to move the file from the temporary location to the final location if this is not // possible copy it. rename(&temp_file, &data_file).or_else(|_| { copy(&temp_file, data_file)?; remove_file(temp_file) })?; write(counter_file, "1")?; } Ok(()) } /// Saves a plain text lesma pub fn save_plain(&self, limit: u8, expire: u32, password: Option, lesma: &String) -> Result<(), Error> { // Calculate BLAKE3 hash of lesma let lesma_hash = blake3::hash(lesma.as_bytes()).to_hex().to_string(); // Get data directory and counter, data and meta files let data_dir = self.storage.join("data").join(&lesma_hash); let counter_file = data_dir.join("_counter"); let data_file = data_dir.join("_data"); let meta_file = self.storage.join("meta").join(&self.hash); // Fill in the metadata let lesma_meta = LesmaMeta { hash: lesma_hash, password: password.and_then(|p| if p.is_empty() { None } else { Some(blake3::hash(p.as_bytes()).to_hex().to_string()) }), name: "lesma".to_string(), binary: false, limit, current: 0, create: OffsetDateTime::now_utc(), expire: OffsetDateTime::now_utc() + Duration::hours(expire.into()) }; // Serialize metadata in JSON let meta_json = json::to_string(&lesma_meta)?; // Stores the metadata in the metadata file write(meta_file, meta_json)?; // Check if the data already exists if data_dir.is_dir() { // Add +1 to counter file let mut counter = read_to_string(&counter_file)?.trim().parse::().map_err(|e| Error::new(ErrorKind::InvalidData, e))?; counter += 1; write(counter_file, counter.to_string())?; } else { // Create new data directory create_dir(&data_dir)?; // Save data write(data_file, lesma)?; write(counter_file, "1")?; } Ok(()) } /// Reads lesma metadata pub fn read_meta(&self) -> Result { // Get metadata file let meta_file = self.storage.join("meta").join(&self.hash); // Read file and deserialize JSON metadata let meta_json = read_to_string(meta_file)?; let lesma_meta = json::from_str::(&meta_json).map_err(|e| Error::new(ErrorKind::InvalidData, e))?; Ok(lesma_meta) } /// Reads plain lesma data pub fn read_plain_data(&self, lesma_meta: &LesmaMeta, password: Option) -> Result { // Check if lesma has expired or if has reached max views if lesma_has_expired(lesma_meta) || lesma_reached_max_views(lesma_meta) { trace!("Deleting expired plain lesma: {} / {}", self.id, self.hash); self.delete(lesma_meta)?; debug!("Deleted expired plain lesma: {} / {}", self.id, self.hash); return Err(Error::new(ErrorKind::InvalidData, "lesma expired")) } // Check for password if lesma_meta.password.is_none() || lesma_meta.password == password.map(|p| blake3::hash(p.as_bytes()).to_hex().to_string()) { // Updates metadata trace!("Updating plain lesma metadata: {} / {}", self.id, self.hash); self.update_meta(lesma_meta)?; debug!("Updated plain lesma metadata: {} / {}", self.id, self.hash); // Get and read data file let data_file = self.storage.join("data").join(&lesma_meta.hash).join("_data"); read_to_string(data_file) } else { trace!("lesma is password protected and the user has not entered the magic word"); Err(Error::new(ErrorKind::InvalidData, "invalid password")) } } /// Reads binary lesma data pub async fn read_binary_data(&self, lesma_meta: &LesmaMeta, password: Option) -> io::Result { // Check if lesma has expired or if has reached max views if lesma_has_expired(lesma_meta) || lesma_reached_max_views(lesma_meta) { trace!("Deleting expired binary lesma: {} / {}", self.id, self.hash); self.delete(lesma_meta)?; debug!("Deleted expired binary lesma: {} / {}", self.id, self.hash); return Err(Error::new(ErrorKind::InvalidData, "lesma expired")) } // Check for password if lesma_meta.password.is_none() || lesma_meta.password == password.map(|p| blake3::hash(p.as_bytes()).to_hex().to_string()) { // Updates metadata trace!("Updating binary lesma metadata: {} / {}", self.id, self.hash); self.update_meta(lesma_meta)?; debug!("Updated binary lesma metadata: {} / {}", self.id, self.hash); // Get and read data file let data_file = self.storage.join("data").join(&lesma_meta.hash).join("_data"); if ! data_file.is_file() { error!("Binary lesma {} retains metafile but its data has vanished (HASH: {})", &self.id, &self.hash); } BinaryFile::open(data_file, lesma_meta.name.clone()).await } else { trace!("lesma is password protected and the user has not entered the magic word"); Err(Error::new(ErrorKind::InvalidData, "invalid password")) } } /// Updates lesma metadata pub fn update_meta(&self, lesma_meta: &LesmaMeta) -> Result<(), Error> { // Only if it has a download limit if lesma_meta.limit > 0 { // Make lesma meta struct mutable let mut mut_lesma_meta = lesma_meta.clone(); // Update download counter mut_lesma_meta.current += 1; // Serialize metadata in JSON let meta_json = json::to_string(&mut_lesma_meta)?; // Get and metadata file let meta_file = self.storage.join("meta").join(&self.hash); // Stores the metadata in the metadata file debug!("Updating the download counter from {} to {}", lesma_meta.current, mut_lesma_meta.current); write(meta_file, meta_json) } else { trace!("No download limit, no counter update is required"); Ok(()) } } /// Deletes an existing lesma pub fn delete(&self, lesma_meta: &LesmaMeta) -> Result<(), Error> { // Get data directory and counter and meta files let data_dir = self.storage.join("data").join(&lesma_meta.hash); let counter_file = data_dir.join("_counter"); let meta_file = self.storage.join("meta").join(&self.hash); // Reads counter file let mut counter = read_to_string(&counter_file)?.trim().parse::().map_err(|e| Error::new(ErrorKind::InvalidData, e))?; if counter > 1 { // Removes -1 to counter counter -= 1; write(counter_file, counter.to_string())?; trace!("Counter updated to: {}", counter); } else { // Removes lesma data directory debug!("Deleting data directory: {}", &lesma_meta.hash); remove_dir_all(data_dir)?; trace!("Data directory deleted: {}", &lesma_meta.hash); } // Removes lesma meta file debug!("Deleting metafile: {}", &self.hash); remove_file(meta_file) } } /// Returns a pseudo ramdom string with pattern consonant-vowel. /// /// # Example /// /// ``` /// use lesma::new_id; /// /// // Generate new ID string with four consonants and four vowels /// let nid = new_id(4); /// assert_eq!(8, nid.len()); /// ``` pub fn new_id(length: u8) -> String { let consonants: Vec = "bcdfghjklmnpqrstvwxyz".chars().collect(); let vowels: Vec = "aeiou".chars().collect(); let mut id = String::new(); let mut rng = rand::thread_rng(); for _ in 0..length { id.push(*consonants.choose(&mut rng).unwrap()); id.push(*vowels.choose(&mut rng).unwrap()); } debug!("New ID generated: {}", &id); id } /// Returns a tuple with a pseudo ramdom string with pattern consonant-vowel and its BLAKE3 hash. /// /// # Example /// /// ``` /// use lesma::new_hash; /// /// // Directory where it checks that the new ID has not already been generated /// let temp_dir = std::env::temp_dir(); /// /// // Generate new ID string and its BLAKE3 hash /// let (nid, hash) = new_hash(&temp_dir); /// ``` pub fn new_hash(meta_dir: &Path) -> (String, String) { let mut len = 4; let mut nid = new_id(len); let mut hash = blake3::hash(nid.as_bytes()).to_hex().to_string(); while meta_dir.join(&hash).is_file() { nid = new_id(len); hash = blake3::hash(nid.as_bytes()).to_hex().to_string(); len += 1; } debug!("New ID / BLAKE3 hash generated: {} / {}", &nid, &hash); (nid, hash) } /// Creates the lesma storage directory pub fn create_lesma_storage(storage: &PathBuf) -> Result<(), String> { let data_dir = storage.join("data"); let meta_dir = storage.join("meta"); // Create base directory if ! storage.is_dir() { create_dir_all(storage).map_err(|e| {format!("Unable to create lesma storage directory: {}", e)})?; info!("lesma storage directory created"); } // Create data directory if ! data_dir.is_dir() { create_dir(&data_dir).map_err(|e| {format!("Unable to create data directory inside lesma storage directory: {}", e)})?; info!("lesma data directory created"); } // Create meta directory if ! meta_dir.is_dir() { create_dir(&meta_dir).map_err(|e| {format!("Unable to create meta directory inside lesma storage directory: {}", e)})?; info!("lesma meta directory created"); } Ok(()) } /// Creates the sample lesma pub fn create_sample_lesma(storage: &Path) -> Result<(), String> { let id = "lesma"; let hash = "d887e6b1413f5dc4892bb390438e033eaf5dab94f9064eee73f7ba64f702f1ec"; if ! storage.join("meta").join(hash).is_file() { let sample = include_str!("lib.rs"); let lesma = Lesma::from_id_hash(storage.to_path_buf(), id, hash); lesma.save_plain(0, 876000, None, &sample.to_string()).map_err(|e| {format!("Unable to create sample lesma: {}", e)})?; info!("Sample lesma created") } Ok(()) } /// Reads lesma metadata from meta file pub fn read_meta_from_file(meta_file: PathBuf) -> Result { // Read file and deserialize JSON metadata let meta_json = read_to_string(meta_file)?; let lesma_meta = json::from_str::(&meta_json).map_err(|e| Error::new(ErrorKind::InvalidData, e))?; Ok(lesma_meta) } /// Returns limit default value in human readable format. /// /// # Example /// /// ``` /// use lesma::human_default_limit; /// /// let default_limit = human_default_limit(0); /// assert_eq!("unlimited".to_string(), default_limit); /// /// let default_limit = human_default_limit(2); /// assert_eq!("2".to_string(), default_limit); /// ``` pub fn human_default_limit(default_limit: u8) -> String { if default_limit > 0 { default_limit.to_string() } else { "unlimited".to_string() } } /// Returns expire default value in human readable format. Limit is defined in hours, this function /// returns the values in hours and days as long as they are more than 24 hours. /// /// # Example /// /// ``` /// use lesma::human_default_expire; /// /// let default_expire = human_default_expire(24); /// assert_eq!("24 hour(s)", default_expire); /// /// let default_expire = human_default_expire(50); /// assert_eq!("50 hour(s) / 2.08 day(s)", default_expire); /// /// let default_expire = human_default_expire(72); /// assert_eq!("72 hour(s) / 3 day(s)", default_expire); /// ``` pub fn human_default_expire(default_expire: u32) -> String { if default_expire > 24 { let default_expire_days = if default_expire % 24 == 0 { format!("{} day(s)", default_expire / 24) } else { format!("{:.2} day(s)", default_expire as f32 / 24.0) }; format!("{} hour(s) / {}", default_expire, default_expire_days) } else { format!("{} hour(s)", default_expire) } } /// Returns maximum expiration value in human readable format. It is defined in hours, this /// function returns the values in hours and days as long as they are more than 24 hours. /// /// # Example /// /// ``` /// use lesma::human_max_expire; /// /// let max_expire = human_max_expire(24); /// assert_eq!("24h", max_expire); /// /// let max_expire = human_max_expire(50); /// assert_eq!("50h/2.08d", max_expire); /// /// let max_expire = human_max_expire(72); /// assert_eq!("72h/3d", max_expire); /// ``` pub fn human_max_expire(max_expire: u32) -> String { if max_expire > 24 { let max_expire_days = if max_expire % 24 == 0 { format!("{}d", max_expire / 24) } else { format!("{:.2}d", max_expire as f32 / 24.0) }; format!("{}h/{}", max_expire, max_expire_days) } else { format!("{}h", max_expire) } } /// Returns a sanitized URI string. /// /// This function checks if the host value is included in the whitelist. If it is, it returns its /// value by adding the protocol (http or https), otherwise it returns a empty string. pub fn get_uri(https: bool, host: &Host<'_>, domains: &Vec>) -> String { let protocol = if https { "https" } else { "http" }; let sanitized_host = host.to_absolute(protocol, domains); match sanitized_host { Some(sanitized_host) => sanitized_host.to_string(), None => "".to_string() } } /// Checks if a lesma has expired pub fn lesma_has_expired(lesma_meta: &LesmaMeta) -> bool { lesma_meta.expire < OffsetDateTime::now_utc() } /// Checks if a lesma reach max views pub fn lesma_reached_max_views(lesma_meta: &LesmaMeta) -> bool { lesma_meta.limit != 0 && lesma_meta.current >= lesma_meta.limit } /// Highligh plain text pub fn highligh_plain(plain: &String, extension: &std::ffi::OsStr) -> String { // Load syntax set and theme set let ps = SyntaxSet::load_defaults_newlines(); let mut ts = ThemeSet::load_defaults(); ts.themes.get_mut("base16-ocean.dark").unwrap().settings.background = Some(Color {r: 0x00, g: 0x00, b: 0x00, a: 0x00}); match ps.find_syntax_by_extension(extension.to_str().unwrap()) { Some(syntax) => { let mut h = HighlightLines::new(syntax, &ts.themes["base16-ocean.dark"]); let mut highlighted_plain = String::new(); for line in LinesWithEndings::from(plain) { let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ps).unwrap(); highlighted_plain.push_str(&as_24_bit_terminal_escaped(&ranges[..], true)); } highlighted_plain }, None => plain.to_string() // Return uncolorized } } /// Convert lesma into HTML and highligh pub fn into_html(plain: &str, extension: Option<&std::ffi::OsStr>) -> String { // Load syntax set and theme set let ps = SyntaxSet::load_defaults_newlines(); let ts = ThemeSet::load_defaults(); // Try to determine syntax let syntax = if extension.is_none() { // Safe to unwrap since it is a valid syntax ps.find_syntax_by_extension("txt").unwrap() } else { match ps.find_syntax_by_extension(extension.unwrap().to_str().unwrap()) { Some(syntax) => syntax, None => ps.find_syntax_by_extension("txt").unwrap() } }; let mut h = HighlightLines::new(syntax, &ts.themes["InspiredGitHub"]); let mut html_lesma_data = String::new(); let mut line_numbers = String::new(); let mut lines = String::new(); let mut counter = 1; for line in LinesWithEndings::from(plain) { let regions = h.highlight_line(line, &ps).unwrap(); let html = styled_line_to_highlighted_html(®ions[..], IncludeBackground::No).unwrap(); line_numbers.push_str(&format!("{}\n", counter)); lines.push_str(&format!("{}", counter, counter, html)); counter +=1; } html_lesma_data.push_str("
");
    html_lesma_data.push_str(&line_numbers);
    html_lesma_data.push_str("
");
    html_lesma_data.push_str(&lines);
    html_lesma_data.push_str("
"); html_lesma_data } #[cfg(test)] mod tests { use super::*; fn new_temp_dir() -> PathBuf { use rand::distributions::{Distribution, Alphanumeric}; let mut rnd_name: String = Alphanumeric .sample_iter(&mut rand::thread_rng()) .take(64) .map(char::from) .collect(); while std::env::temp_dir().join(&rnd_name).is_dir() { rnd_name = Alphanumeric .sample_iter(&mut rand::thread_rng()) .take(64) .map(char::from) .collect(); } let temp_dir = std::env::temp_dir().join(&rnd_name); create_dir_all(&temp_dir).expect("The temporary directory is expected to be created"); temp_dir } #[test] fn test_new_id() { let nid = new_id(6); assert_eq!(12, nid.len()); } #[test] fn test_new_hash() { let temp_dir = std::env::temp_dir(); let (nid, hash) = new_hash(&temp_dir); assert!(nid.len() >= 4); assert_eq!(64, hash.len()); } #[test] fn test_b3hasher() { let mut b3hasher = B3Hasher(blake3::Hasher::new()); b3hasher.update(b"some text"); assert_eq!( "a0a1c159952a5c5fef1fe3ac3dca48da36ee61ede3e8b5c79fea5458d4db1c7d", b3hasher.finalize() ); } #[test] fn test_create_lesma_storage() { let temp_dir = new_temp_dir(); assert!(create_lesma_storage(&temp_dir).is_ok()); remove_dir_all(&temp_dir).expect("The temporary directory is expected to be deleted"); } #[test] fn test_create_sample_lesma() { let sample = include_str!("lib.rs"); let lesma_hash = blake3::hash(sample.as_bytes()).to_hex().to_string(); let hash = "d887e6b1413f5dc4892bb390438e033eaf5dab94f9064eee73f7ba64f702f1ec"; let temp_dir = new_temp_dir(); let counter_file = temp_dir.join("data").join(&lesma_hash).join("_counter"); let data_file = temp_dir.join("data").join(&lesma_hash).join("_data"); let meta_file = temp_dir.join("meta").join(hash); let expected_meta = format!("{{\"hash\":\"{}\",\"password\":null,\"name\":\"lesma\",\"binary\":false,\"limit\":0,\"current\":0", lesma_hash); // This error occurs because there is no directory structure created assert!(create_sample_lesma(&temp_dir).is_err()); create_lesma_storage(&temp_dir).expect("The lesma storage directory structure is expected to be created"); assert!(create_sample_lesma(&temp_dir).is_ok()); assert!(temp_dir.join("data").join(&lesma_hash).is_dir()); assert!(counter_file.is_file()); assert!(data_file.is_file()); assert!(meta_file.is_file()); let readed_counter = read_to_string(&counter_file).expect("The lesma counter is expected to be readed"); let readed_data = read_to_string(&data_file).expect("The lesma data is expected to be readed"); let readed_meta = read_to_string(&meta_file).expect("The lesma meta is expected to be readed"); assert_eq!("1", readed_counter); assert_eq!(sample.to_string(), readed_data); assert!(readed_meta.starts_with(&expected_meta)); remove_dir_all(&temp_dir).expect("The temporary directory is expected to be deleted"); } #[test] fn test_get_uri() { let domains = [Host::new(uri!("localhost:8000")), Host::new(uri!("127.0.0.1:8000")), Host::new(uri!("lesma.eu"))].to_vec(); let host = Host::new(uri!("localhost:8000")); assert_eq!("http://localhost:8000", get_uri(false, &host, &domains)); let host = Host::new(uri!("127.0.0.1:8000")); assert_eq!("https://127.0.0.1:8000", get_uri(true, &host, &domains)); let host = Host::new(uri!("lesma.eu")); assert_eq!("https://lesma.eu", get_uri(true, &host, &domains)); let host = Host::new(uri!("false.host")); assert_eq!("", get_uri(true, &host, &domains)); } #[test] fn test_save_plain() { let lesma_hash = "d887e6b1413f5dc4892bb390438e033eaf5dab94f9064eee73f7ba64f702f1ec"; let sample = "lesma".to_string(); let temp_dir = new_temp_dir(); let lesma = Lesma::new(temp_dir.clone()); let counter_file = temp_dir.join("data").join(&lesma_hash).join("_counter"); let data_file = temp_dir.join("data").join(&lesma_hash).join("_data"); let meta_file = temp_dir.join("meta").join(&lesma.hash); create_lesma_storage(&temp_dir).expect("The lesma storage directory structure is expected to be created"); // Saves a new lesma with password let save_plain_result = lesma.save_plain(0, 1, Some(sample.clone()), &sample); assert!(save_plain_result.is_ok()); assert!(temp_dir.join("data").join(&lesma_hash).is_dir()); assert!(counter_file.is_file()); assert!(data_file.is_file()); assert!(meta_file.is_file()); let expected_meta = format!("{{\"hash\":\"{}\",\"password\":\"{}\",\"name\":\"lesma\",\"binary\":false,\"limit\":0,\"current\":0", lesma_hash, lesma_hash); let readed_counter = read_to_string(&counter_file).expect("The lesma counter is expected to be readed"); let readed_data = read_to_string(&data_file).expect("The lesma data is expected to be readed"); let readed_meta = read_to_string(&meta_file).expect("The lesma meta is expected to be readed"); assert_eq!("1", readed_counter); assert_eq!(sample, readed_data); assert!(readed_meta.starts_with(&expected_meta)); // Saves a new file without a password let lesma = Lesma::new(temp_dir.clone()); let meta_file = temp_dir.join("meta").join(&lesma.hash); let save_plain_result = lesma.save_plain(0, 1, None, &sample); assert!(save_plain_result.is_ok()); let expected_meta = format!("{{\"hash\":\"{}\",\"password\":null,\"name\":\"lesma\",\"binary\":false,\"limit\":0,\"current\":0", lesma_hash); let readed_counter = read_to_string(&counter_file).expect("The lesma counter is expected to be readed"); let readed_meta = read_to_string(&meta_file).expect("The lesma meta is expected to be readed"); assert_eq!("2", readed_counter); assert!(readed_meta.starts_with(&expected_meta)); remove_dir_all(&temp_dir).expect("The temporary directory is expected to be deleted"); } #[test] fn test_read_meta() { // Hash and lesma hash are the same because the word lesma is used as the origin of both let hash = "d887e6b1413f5dc4892bb390438e033eaf5dab94f9064eee73f7ba64f702f1ec"; let sample = "lesma".to_string(); let temp_dir = new_temp_dir(); create_lesma_storage(&temp_dir).expect("The lesma storage directory structure is expected to be created"); let lesma = Lesma::from_id_hash(temp_dir.clone(), &sample, &hash.to_string()); lesma.save_plain(0, 1, None, &sample).expect("A plain lesma is expected to be created"); let lesma_meta = lesma.read_meta().expect("The lesma meta is expected to be readed"); assert_eq!(hash, lesma_meta.hash); assert!(lesma_meta.password.is_none()); assert_eq!(sample, lesma_meta.name); assert!(!lesma_meta.binary); assert_eq!(0, lesma_meta.limit); assert_eq!(0, lesma_meta.current); // Saves same data but with new hash and adding a password let new_id = "new lesma"; let new_hash = "e533f5b6b43ecfe60c28d44caf1696d82ebff44633882d80fd8fc5b6d4f8e32f"; let lesma = Lesma::from_id_hash(temp_dir.clone(), new_id, new_hash); lesma.save_plain(0, 1, Some(sample.clone()), &sample).expect("A plain lesma is expected to be created"); let lesma_meta = lesma.read_meta().expect("The lesma meta is expected to be readed"); assert_eq!(hash, lesma_meta.hash); assert_eq!(Some(hash.to_string()), lesma_meta.password); assert_eq!(sample, lesma_meta.name); assert!(!lesma_meta.binary); assert_eq!(0, lesma_meta.limit); assert_eq!(0, lesma_meta.current); remove_dir_all(&temp_dir).expect("The temporary directory is expected to be deleted"); } #[test] fn test_read_plain_data() { // Hash and lesma hash are the same because the word lesma is used as the origin of both let hash = "d887e6b1413f5dc4892bb390438e033eaf5dab94f9064eee73f7ba64f702f1ec".to_string(); let sample = "lesma".to_string(); let temp_dir = new_temp_dir(); create_lesma_storage(&temp_dir).expect("The lesma storage directory structure is expected to be created"); let lesma = Lesma::from_id_hash(temp_dir.clone(), &sample, &hash); lesma.save_plain(0, 1, None, &sample).expect("A plain lesma is expected to be created"); let lesma_meta = lesma.read_meta().expect("The lesma meta is expected to be readed"); let lesma_data = lesma.read_plain_data(&lesma_meta, None).expect("The lesma data is expected to be readed"); assert_eq!(sample, lesma_data); lesma.delete(&lesma_meta).expect("A plain lesma is expected to be deleted"); lesma.save_plain(1, 1, Some(sample.clone()), &sample).expect("A plain lesma is expected to be created"); let lesma_meta = lesma.read_meta().expect("The lesma meta is expected to be readed"); let lesma_data = lesma.read_plain_data(&lesma_meta, None); assert!(lesma_data.is_err()); assert_eq!("invalid password".to_string(), lesma_data.unwrap_err().to_string()); let lesma_data = lesma.read_plain_data(&lesma_meta, Some(sample.clone())).expect("The lesma data is expected to be readed"); assert_eq!(sample, lesma_data); // Reached the read limit let lesma_meta = lesma.read_meta().expect("The lesma meta is expected to be readed"); let lesma_data = lesma.read_plain_data(&lesma_meta, Some(sample.clone())); assert!(lesma_data.is_err()); assert_eq!("lesma expired".to_string(), lesma_data.unwrap_err().to_string()); remove_dir_all(&temp_dir).expect("The temporary directory is expected to be deleted"); } #[test] fn test_update_meta() { let lesma_hash = "d887e6b1413f5dc4892bb390438e033eaf5dab94f9064eee73f7ba64f702f1ec"; let sample = "lesma".to_string(); let temp_dir = new_temp_dir(); let lesma = Lesma::new(temp_dir.clone()); let meta_file = temp_dir.join("meta").join(&lesma.hash); create_lesma_storage(&temp_dir).expect("The lesma storage directory structure is expected to be created"); lesma.save_plain(0, 1, None, &sample).expect("A plain lesma is expected to be created"); let lesma_meta = lesma.read_meta().expect("The lesma meta is expected to be readed"); let update_lesma_meta_result = lesma.update_meta(&lesma_meta); assert!(update_lesma_meta_result.is_ok()); let expected_meta = format!("{{\"hash\":\"{}\",\"password\":null,\"name\":\"lesma\",\"binary\":false,\"limit\":0,\"current\":0", lesma_hash); let readed_meta = read_to_string(&meta_file).expect("The lesma meta is expected to be readed"); assert!(readed_meta.starts_with(&expected_meta)); // Saves a new lesma with limit and using same content let lesma = Lesma::new(temp_dir.clone()); let meta_file = temp_dir.join("meta").join(&lesma.hash); lesma.save_plain(2, 1, None, &sample).expect("A plain lesma is expected to be created"); let lesma_meta = lesma.read_meta().expect("The lesma meta is expected to be readed"); lesma.update_meta(&lesma_meta).expect("The lesma meta is expected to be updated"); let expected_meta = format!("{{\"hash\":\"{}\",\"password\":null,\"name\":\"lesma\",\"binary\":false,\"limit\":2,\"current\":1", lesma_hash); let readed_meta = read_to_string(&meta_file).expect("The lesma meta is expected to be readed"); assert!(readed_meta.starts_with(&expected_meta)); remove_dir_all(&temp_dir).expect("The temporary directory is expected to be deleted"); } #[test] fn test_delete() { let lesma_hash = "d887e6b1413f5dc4892bb390438e033eaf5dab94f9064eee73f7ba64f702f1ec"; let sample = "lesma".to_string(); let temp_dir = new_temp_dir(); let lesma = Lesma::new(temp_dir.clone()); let data_dir = temp_dir.join("data").join(&lesma_hash); let counter_file = data_dir.join("_counter"); let data_file = data_dir.join("_data"); let meta_file = temp_dir.join("meta").join(&lesma.hash); create_lesma_storage(&temp_dir).expect("The lesma storage directory structure is expected to be created"); lesma.save_plain(0, 1, None, &sample).expect("A plain lesma is expected to be created"); let lesma_meta = lesma.read_meta().expect("The lesma meta is expected to be readed"); let delete_lesma_result = lesma.delete(&lesma_meta); assert!(delete_lesma_result.is_ok()); assert!(!data_dir.is_dir()); assert!(!counter_file.is_file()); assert!(!data_file.is_file()); assert!(!meta_file.is_file()); // Save sample lesma again lesma.save_plain(0, 1, None, &sample).expect("A plain lesma is expected to be created"); // Create new lesma with same contents let lesma = Lesma::new(temp_dir.clone()); lesma.save_plain(0, 1, None, &sample).expect("A plain lesma is expected to be created"); let meta_file = temp_dir.join("meta").join(&lesma.hash); let readed_counter = read_to_string(&counter_file).expect("The lesma counter is expected to be readed"); assert_eq!("2", readed_counter); let delete_lesma_result = lesma.delete(&lesma_meta); assert!(delete_lesma_result.is_ok()); assert!(data_dir.is_dir()); assert!(counter_file.is_file()); assert!(data_file.is_file()); assert!(!meta_file.is_file()); let readed_counter = read_to_string(&counter_file).expect("The lesma counter is expected to be readed"); assert_eq!("1", readed_counter); remove_dir_all(&temp_dir).expect("The temporary directory is expected to be deleted"); } #[test] fn test_lesma_has_expired() { let mut lesma_meta = LesmaMeta { hash: "".to_string(), password: None, name: "".to_string(), binary: false, limit: 0, current: 0, create: OffsetDateTime::now_utc(), expire: OffsetDateTime::now_utc() + Duration::hours(1) }; assert!(!lesma_has_expired(&lesma_meta)); lesma_meta.expire = OffsetDateTime::now_utc() - Duration::hours(1); assert!(lesma_has_expired(&lesma_meta)); } #[test] fn test_lesma_reached_max_views() { let mut lesma_meta = LesmaMeta { hash: "".to_string(), password: None, name: "".to_string(), binary: false, limit: 0, current: 0, create: OffsetDateTime::now_utc(), expire: OffsetDateTime::now_utc() + Duration::hours(1) }; assert!(!lesma_reached_max_views(&lesma_meta)); lesma_meta.current = 10; assert!(!lesma_reached_max_views(&lesma_meta)); lesma_meta.current = 0; lesma_meta.limit = 2; assert!(!lesma_reached_max_views(&lesma_meta)); lesma_meta.current += 1; assert!(!lesma_reached_max_views(&lesma_meta)); lesma_meta.current += 1; assert!(lesma_reached_max_views(&lesma_meta)); lesma_meta.current += 1; assert!(lesma_reached_max_views(&lesma_meta)); } }