diff --git a/src/blob.rs b/src/blob.rs index c4ff4e3e40..5e74adffc0 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -14,8 +14,7 @@ use image::codecs::jpeg::JpegEncoder; use image::ImageReader; use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba}; use num_traits::FromPrimitive; -use tokio::io::AsyncWriteExt; -use tokio::{fs, io, task}; +use tokio::{fs, task}; use tokio_stream::wrappers::ReadDirStream; use crate::config::Config; @@ -48,73 +47,6 @@ enum ImageOutputFormat { } impl<'a> BlobObject<'a> { - /// Creates a new file, returning a tuple of the name and the handle. - async fn create_new_file( - context: &Context, - dir: &Path, - stem: &str, - ext: &str, - ) -> Result<(String, fs::File)> { - const MAX_ATTEMPT: u32 = 16; - let mut attempt = 0; - let mut name = format!("{stem}{ext}"); - loop { - attempt += 1; - let path = dir.join(&name); - match fs::OpenOptions::new() - // Using `create_new(true)` in order to avoid race conditions - // when creating multiple files with the same name. - .create_new(true) - .write(true) - .open(&path) - .await - { - Ok(file) => return Ok((name, file)), - Err(err) => { - if attempt >= MAX_ATTEMPT { - return Err(err).context("failed to create file"); - } else if attempt == 1 && !dir.exists() { - fs::create_dir_all(dir).await.log_err(context).ok(); - } else { - name = format!("{}-{}{}", stem, rand::random::(), ext); - } - } - } - } - } - - /// Creates a new blob object with unique name by copying an existing file. - /// - /// This creates a new blob - /// and copies an existing file into it. This is done in a - /// in way which avoids race-conditions when multiple files are - /// concurrently created. - pub async fn create_and_copy(context: &'a Context, src: &Path) -> Result> { - let mut src_file = fs::File::open(src) - .await - .with_context(|| format!("failed to open file {}", src.display()))?; - let (stem, ext) = BlobObject::sanitize_name_and_split_extension(&src.to_string_lossy()); - let (name, mut dst_file) = - BlobObject::create_new_file(context, context.get_blobdir(), &stem, &ext).await?; - let name_for_err = name.clone(); - if let Err(err) = io::copy(&mut src_file, &mut dst_file).await { - // Attempt to remove the failed file, swallow errors resulting from that. - let path = context.get_blobdir().join(&name_for_err); - fs::remove_file(path).await.ok(); - return Err(err).context("failed to copy file"); - } - - // Ensure that all buffered bytes are written - dst_file.flush().await?; - - let blob = BlobObject { - blobdir: context.get_blobdir(), - name: format!("$BLOBDIR/{name}"), - }; - context.emit_event(EventType::NewBlobFile(blob.as_name().to_string())); - Ok(blob) - } - /// Creates a blob object by copying or renaming an existing file. /// If the source file is already in the blobdir, it will be renamed, /// otherwise it will be copied to the blobdir first. @@ -209,27 +141,6 @@ impl<'a> BlobObject<'a> { }) } - /// Creates a blob from a file, possibly copying it to the blobdir. - /// - /// If the source file is not a path to into the blob directory - /// the file will be copied into the blob directory first. If the - /// source file is already in the blobdir it will not be copied - /// and only be created if it is a valid blobname, that is no - /// subdirectory is used and [BlobObject::sanitize_name_and_split_extension] does not - /// modify the filename. - /// - /// Paths into the blob directory may be either defined by an absolute path - /// or by the relative prefix `$BLOBDIR`. - pub async fn new_from_path(context: &'a Context, src: &Path) -> Result> { - if src.starts_with(context.get_blobdir()) { - BlobObject::from_path(context, src) - } else if src.starts_with("$BLOBDIR/") { - BlobObject::from_name(context, src.to_str().unwrap_or_default().to_string()) - } else { - BlobObject::create_and_copy(context, src).await - } - } - /// Returns a [BlobObject] for an existing blob from a path. /// /// The path must designate a file directly in the blobdir and @@ -301,50 +212,6 @@ impl<'a> BlobObject<'a> { } } - /// The name is returned as a tuple, the first part - /// being the stem or basename and the second being an extension, - /// including the dot. E.g. "foo.txt" is returned as `("foo", - /// ".txt")` while "bar" is returned as `("bar", "")`. - /// - /// The extension part will always be lowercased. - fn sanitize_name_and_split_extension(name: &str) -> (String, String) { - let name = sanitize_filename(name); - // Let's take a tricky filename, - // "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example. - // Assume that the extension is 32 chars maximum. - let ext: String = name - .chars() - .rev() - .take_while(|c| { - (!c.is_ascii_punctuation() || *c == '.') && !c.is_whitespace() && !c.is_control() - }) - .take(33) - .collect::>() - .iter() - .rev() - .collect(); - // ext == "nd_point_and_double_ending.tar.gz" - - // Split it into "nd_point_and_double_ending" and "tar.gz": - let mut iter = ext.splitn(2, '.'); - iter.next(); - - let ext = iter.next().unwrap_or_default(); - let ext = if ext.is_empty() { - String::new() - } else { - format!(".{ext}") - // ".tar.gz" - }; - let stem = name - .strip_suffix(&ext) - .unwrap_or_default() - .chars() - .take(64) - .collect(); - (stem, ext.to_lowercase()) - } - /// Checks whether a name is a valid blob name. /// /// This is slightly less strict than stanitise_name, presumably diff --git a/src/blob/blob_tests.rs b/src/blob/blob_tests.rs index ad911d091f..a4ce824863 100644 --- a/src/blob/blob_tests.rs +++ b/src/blob/blob_tests.rs @@ -104,55 +104,15 @@ async fn test_create_long_names() { assert!(blobname.len() < 70); } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_create_and_copy() { - let t = TestContext::new().await; - let src = t.dir.path().join("src"); - fs::write(&src, b"boo").await.unwrap(); - let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap(); - assert_eq!(blob.as_name(), "$BLOBDIR/src"); - let data = fs::read(blob.to_abs_path()).await.unwrap(); - assert_eq!(data, b"boo"); - - let whoops = t.dir.path().join("whoops"); - assert!(BlobObject::create_and_copy(&t, whoops.as_ref()) - .await - .is_err()); - let whoops = t.get_blobdir().join("whoops"); - assert!(!whoops.exists()); -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn test_create_from_path() { - let t = TestContext::new().await; - - let src_ext = t.dir.path().join("external"); - fs::write(&src_ext, b"boo").await.unwrap(); - let blob = BlobObject::new_from_path(&t, src_ext.as_ref()) - .await - .unwrap(); - assert_eq!(blob.as_name(), "$BLOBDIR/external"); - let data = fs::read(blob.to_abs_path()).await.unwrap(); - assert_eq!(data, b"boo"); - - let src_int = t.get_blobdir().join("internal"); - fs::write(&src_int, b"boo").await.unwrap(); - let blob = BlobObject::new_from_path(&t, &src_int).await.unwrap(); - assert_eq!(blob.as_name(), "$BLOBDIR/internal"); - let data = fs::read(blob.to_abs_path()).await.unwrap(); - assert_eq!(data, b"boo"); -} #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_create_from_name_long() { let t = TestContext::new().await; let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html"); fs::write(&src_ext, b"boo").await.unwrap(); - let blob = BlobObject::new_from_path(&t, src_ext.as_ref()) - .await - .unwrap(); + let blob = BlobObject::create_and_deduplicate(&t, &src_ext, &src_ext).unwrap(); assert_eq!( blob.as_name(), - "$BLOBDIR/autocrypt-setup-message-4137848473.html" + "$BLOBDIR/06f010b24d1efe57ffab44a8ad20c54.html" ); } @@ -166,64 +126,6 @@ fn test_is_blob_name() { assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar")); } -#[test] -fn test_sanitise_name() { - let (stem, ext) = BlobObject::sanitize_name_and_split_extension( - "Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt", - ); - assert_eq!(ext, ".txt"); - assert!(!stem.is_empty()); - - // the extensions are kept together as between stem and extension a number may be added - - // and `foo.tar.gz` should become `foo-1234.tar.gz` and not `foo.tar-1234.gz` - let (stem, ext) = BlobObject::sanitize_name_and_split_extension("wot.tar.gz"); - assert_eq!(stem, "wot"); - assert_eq!(ext, ".tar.gz"); - - let (stem, ext) = BlobObject::sanitize_name_and_split_extension(".foo.bar"); - assert_eq!(stem, "file"); - assert_eq!(ext, ".foo.bar"); - - let (stem, ext) = BlobObject::sanitize_name_and_split_extension("foo?.bar"); - assert!(stem.contains("foo")); - assert!(!stem.contains('?')); - assert_eq!(ext, ".bar"); - - let (stem, ext) = BlobObject::sanitize_name_and_split_extension("no-extension"); - assert_eq!(stem, "no-extension"); - assert_eq!(ext, ""); - - let (stem, ext) = - BlobObject::sanitize_name_and_split_extension("path/ignored\\this: is* forbidden?.c"); - assert_eq!(ext, ".c"); - assert!(!stem.contains("path")); - assert!(!stem.contains("ignored")); - assert!(stem.contains("this")); - assert!(stem.contains("forbidden")); - assert!(!stem.contains('/')); - assert!(!stem.contains('\\')); - assert!(!stem.contains(':')); - assert!(!stem.contains('*')); - assert!(!stem.contains('?')); - - let (stem, ext) = BlobObject::sanitize_name_and_split_extension( - "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz", - ); - assert_eq!( - stem, - "file.with_lots_of_characters_behind_point_and_double_ending" - ); - assert_eq!(ext, ".tar.gz"); - - let (stem, ext) = BlobObject::sanitize_name_and_split_extension("a. tar.tar.gz"); - assert_eq!(stem, "a. tar"); - assert_eq!(ext, ".tar.gz"); - - let (stem, ext) = BlobObject::sanitize_name_and_split_extension("Guia_uso_GNB (v0.8).pdf"); - assert_eq!(stem, "Guia_uso_GNB (v0.8)"); - assert_eq!(ext, ".pdf"); -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_add_white_bg() { let t = TestContext::new().await; @@ -236,7 +138,7 @@ async fn test_add_white_bg() { let avatar_src = t.dir.path().join("avatar.png"); fs::write(&avatar_src, bytes).await.unwrap(); - let mut blob = BlobObject::new_from_path(&t, &avatar_src).await.unwrap(); + let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap(); let img_wh = 128; let maybe_sticker = &mut false; let strict_limits = true; @@ -285,7 +187,7 @@ async fn test_selfavatar_outside_blobdir() { constants::BALANCED_AVATAR_SIZE, ); - let mut blob = BlobObject::new_from_path(&t, avatar_path).await.unwrap(); + let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap(); let maybe_sticker = &mut false; let strict_limits = true; blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits) @@ -643,9 +545,9 @@ impl SendImageCheckMediaquality<'_> { check_image_size(file_saved, compressed_width, compressed_height); if original_width == compressed_width { - assert_extension(&alice, alice_msg, extension).await; + assert_extension(&alice, alice_msg, extension); } else { - assert_extension(&alice, alice_msg, "jpg").await; + assert_extension(&alice, alice_msg, "jpg"); } let bob_msg = bob.recv_msg(&sent).await; @@ -668,16 +570,16 @@ impl SendImageCheckMediaquality<'_> { let img = check_image_size(file_saved, compressed_width, compressed_height); if original_width == compressed_width { - assert_extension(&bob, bob_msg, extension).await; + assert_extension(&bob, bob_msg, extension); } else { - assert_extension(&bob, bob_msg, "jpg").await; + assert_extension(&bob, bob_msg, "jpg"); } Ok(img) } } -async fn assert_extension(context: &TestContext, msg: Message, extension: &str) { +fn assert_extension(context: &TestContext, msg: Message, extension: &str) { assert!(msg .param .get(Param::File) @@ -703,8 +605,7 @@ async fn assert_extension(context: &TestContext, msg: Message, extension: &str) ); assert_eq!( msg.param - .get_blob(Param::File, context) - .await + .get_file_blob(context) .unwrap() .unwrap() .suffix() diff --git a/src/chat.rs b/src/chat.rs index 86765da8ec..04896a0d37 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -904,8 +904,7 @@ impl ChatId { if msg.viewtype == Viewtype::Vcard { let blob = msg .param - .get_blob(Param::File, context) - .await? + .get_file_blob(context)? .context("no file stored in params")?; msg.try_set_vcard(context, &blob.to_abs_path()).await?; } @@ -2751,8 +2750,7 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { } else if msg.viewtype.has_file() { let mut blob = msg .param - .get_blob(Param::File, context) - .await? + .get_file_blob(context)? .with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?; let send_as_is = msg.viewtype == Viewtype::File; diff --git a/src/debug_logging.rs b/src/debug_logging.rs index 4b0ce441f4..9b437e8cc1 100644 --- a/src/debug_logging.rs +++ b/src/debug_logging.rs @@ -9,7 +9,6 @@ use crate::tools::time; use crate::webxdc::StatusUpdateItem; use async_channel::{self as channel, Receiver, Sender}; use serde_json::json; -use std::path::PathBuf; use tokio::task; #[derive(Debug)] @@ -100,9 +99,7 @@ pub async fn maybe_set_logging_xdc( context, msg.get_viewtype(), chat_id, - msg.param - .get_path(Param::Filename, context) - .unwrap_or_default(), + msg.param.get(Param::Filename), msg.get_id(), ) .await?; @@ -115,18 +112,16 @@ pub async fn maybe_set_logging_xdc_inner( context: &Context, viewtype: Viewtype, chat_id: ChatId, - filename: Option, + filename: Option<&str>, msg_id: MsgId, ) -> anyhow::Result<()> { if viewtype == Viewtype::Webxdc { - if let Some(file) = filename { - if let Some(file_name) = file.file_name().and_then(|name| name.to_str()) { - if file_name.starts_with("debug_logging") - && file_name.ends_with(".xdc") - && chat_id.is_self_talk(context).await? - { - set_debug_logging_xdc(context, Some(msg_id)).await?; - } + if let Some(filename) = filename { + if filename.starts_with("debug_logging") + && filename.ends_with(".xdc") + && chat_id.is_self_talk(context).await? + { + set_debug_logging_xdc(context, Some(msg_id)).await?; } } } diff --git a/src/message.rs b/src/message.rs index 9ee538540d..0841c74209 100644 --- a/src/message.rs +++ b/src/message.rs @@ -636,7 +636,7 @@ impl Message { /// Returns the full path to the file associated with a message. pub fn get_file(&self, context: &Context) -> Option { - self.param.get_path(Param::File, context).unwrap_or(None) + self.param.get_file_path(context).unwrap_or(None) } /// Returns vector of vcards if the file has a vCard attachment. @@ -669,7 +669,7 @@ impl Message { /// If message is an image or gif, set Param::Width and Param::Height pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> { if self.viewtype.has_file() { - let file_param = self.param.get_path(Param::File, context)?; + let file_param = self.param.get_file_path(context)?; if let Some(path_and_filename) = file_param { if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif) && !self.param.exists(Param::Width) @@ -817,7 +817,7 @@ impl Message { /// Returns the size of the file in bytes, if applicable. pub async fn get_filebytes(&self, context: &Context) -> Result> { - if let Some(path) = self.param.get_path(Param::File, context)? { + if let Some(path) = self.param.get_file_path(context)? { Ok(Some(get_filebytes(context, &path).await.with_context( || format!("failed to get {} size in bytes", path.display()), )?)) diff --git a/src/message/message_tests.rs b/src/message/message_tests.rs index 2f6da4a763..27a2f6953e 100644 --- a/src/message/message_tests.rs +++ b/src/message/message_tests.rs @@ -768,7 +768,7 @@ async fn test_sanitize_filename_message() -> Result<()> { msg.set_file_from_bytes(t, "/\\:ee.tx*T ", b"hallo", None)?; assert_eq!(msg.get_filename().unwrap(), "ee.txT"); - let blob = msg.param.get_blob(Param::File, t).await?.unwrap(); + let blob = msg.param.get_file_blob(t)?.unwrap(); assert_eq!(blob.suffix().unwrap(), "txt"); // The filename shouldn't be empty if there were only illegal characters: diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 57b76df949..def21ce03e 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -1628,8 +1628,7 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result &mut Self { + if key == Param::File { + debug_assert!(value.to_string().starts_with("$BLOBDIR/")); + } self.inner.insert(key, value.to_string()); self } @@ -357,59 +361,20 @@ impl Params { self.get(key).and_then(|s| s.parse().ok()) } - /// Gets the given parameter and parse as [ParamsFile]. - /// - /// See also [Params::get_blob] and [Params::get_path] which may - /// be more convenient. - pub fn get_file<'a>(&self, key: Param, context: &'a Context) -> Result>> { - let val = match self.get(key) { - Some(val) => val, - None => return Ok(None), - }; - ParamsFile::from_param(context, val).map(Some) - } - - /// Gets the parameter and returns a [BlobObject] for it. - /// - /// This parses the parameter value as a [ParamsFile] and than - /// tries to return a [BlobObject] for that file. If the file is - /// not yet a valid blob, one will be created by copying the file. - /// - /// Note that in the [ParamsFile::FsPath] case the blob can be - /// created without copying if the path already refers to a valid - /// blob. If so a [BlobObject] will be returned. - pub async fn get_blob<'a>( - &self, - key: Param, - context: &'a Context, - ) -> Result>> { - let val = match self.get(key) { - Some(val) => val, - None => return Ok(None), - }; - let file = ParamsFile::from_param(context, val)?; - let blob = match file { - ParamsFile::FsPath(path) => BlobObject::new_from_path(context, &path).await?, - ParamsFile::Blob(blob) => blob, + /// Returns a [BlobObject] for the [Param::File] parameter. + pub fn get_file_blob<'a>(&self, context: &'a Context) -> Result>> { + let Some(val) = self.get(Param::File) else { + return Ok(None); }; + ensure!(val.starts_with("$BLOBDIR/")); + let blob = BlobObject::from_name(context, val.to_string())?; Ok(Some(blob)) } - /// Gets the parameter and returns a [PathBuf] for it. - /// - /// This parses the parameter value as a [ParamsFile] and returns - /// a [PathBuf] to the file. - pub fn get_path(&self, key: Param, context: &Context) -> Result> { - let val = match self.get(key) { - Some(val) => val, - None => return Ok(None), - }; - let file = ParamsFile::from_param(context, val)?; - let path = match file { - ParamsFile::FsPath(path) => path, - ParamsFile::Blob(blob) => blob.to_abs_path(), - }; - Ok(Some(path)) + /// Returns a [PathBuf] for the [Param::File] parameter. + pub fn get_file_path(&self, context: &Context) -> Result> { + let blob = self.get_file_blob(context)?; + Ok(blob.map(|p| p.to_abs_path())) } /// Set the given parameter to the passed in `i32`. @@ -431,48 +396,18 @@ impl Params { } } -/// The value contained in [Param::File]. -/// -/// Because the only way to construct this object is from a valid -/// UTF-8 string it is always safe to convert the value contained -/// within the [ParamsFile::FsPath] back to a [String] or [&str]. -/// Despite the type itself does not guarantee this. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ParamsFile<'a> { - FsPath(PathBuf), - Blob(BlobObject<'a>), -} - -impl<'a> ParamsFile<'a> { - /// Parse the [Param::File] value into an object. - /// - /// If the value was stored into the [Params] correctly this - /// should not fail. - pub fn from_param(context: &'a Context, src: &str) -> Result> { - let param = match src.starts_with("$BLOBDIR/") { - true => ParamsFile::Blob(BlobObject::from_name(context, src.to_string())?), - false => ParamsFile::FsPath(PathBuf::from(src)), - }; - Ok(param) - } -} - #[cfg(test)] mod tests { - use std::path::Path; use std::str::FromStr; - use tokio::fs; - use super::*; - use crate::test_utils::TestContext; #[test] fn test_dc_param() { - let mut p1: Params = "a=1\nf=2\nc=3".parse().unwrap(); + let mut p1: Params = "a=1\nw=2\nc=3".parse().unwrap(); assert_eq!(p1.get_int(Param::Forwarded), Some(1)); - assert_eq!(p1.get_int(Param::File), Some(2)); + assert_eq!(p1.get_int(Param::Width), Some(2)); assert_eq!(p1.get_int(Param::Height), None); assert!(!p1.exists(Param::Height)); @@ -483,13 +418,13 @@ mod tests { let mut p1 = Params::new(); p1.set(Param::Forwarded, "foo") - .set_int(Param::File, 2) + .set_int(Param::Width, 2) .remove(Param::GuaranteeE2ee) .set_int(Param::Duration, 4); - assert_eq!(p1.to_string(), "a=foo\nd=4\nf=2"); + assert_eq!(p1.to_string(), "a=foo\nd=4\nw=2"); - p1.remove(Param::File); + p1.remove(Param::Width); assert_eq!(p1.to_string(), "a=foo\nd=4",); assert_eq!(p1.len(), 2); @@ -511,56 +446,6 @@ mod tests { assert_eq!(params.to_string().parse::().unwrap(), params); } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_params_file_fs_path() { - let t = TestContext::new().await; - if let ParamsFile::FsPath(p) = ParamsFile::from_param(&t, "/foo/bar/baz").unwrap() { - assert_eq!(p, Path::new("/foo/bar/baz")); - } else { - panic!("Wrong enum variant"); - } - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_params_file_blob() { - let t = TestContext::new().await; - if let ParamsFile::Blob(b) = ParamsFile::from_param(&t, "$BLOBDIR/foo").unwrap() { - assert_eq!(b.as_name(), "$BLOBDIR/foo"); - } else { - panic!("Wrong enum variant"); - } - } - - // Tests for Params::get_file(), Params::get_path() and Params::get_blob(). - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_params_get_fileparam() { - let t = TestContext::new().await; - let fname = t.dir.path().join("foo"); - let mut p = Params::new(); - p.set(Param::File, fname.to_str().unwrap()); - - let file = p.get_file(Param::File, &t).unwrap().unwrap(); - assert_eq!(file, ParamsFile::FsPath(fname.clone())); - - let path: PathBuf = p.get_path(Param::File, &t).unwrap().unwrap(); - assert_eq!(path, fname); - - fs::write(fname, b"boo").await.unwrap(); - let blob = p.get_blob(Param::File, &t).await.unwrap().unwrap(); - assert!(blob.as_name().starts_with("$BLOBDIR/foo")); - - // Blob in blobdir, expect blob. - let bar_path = t.get_blobdir().join("bar"); - p.set(Param::File, bar_path.to_str().unwrap()); - let blob = p.get_blob(Param::File, &t).await.unwrap().unwrap(); - assert_eq!(blob, BlobObject::from_name(&t, "bar".to_string()).unwrap()); - - p.remove(Param::File); - assert!(p.get_file(Param::File, &t).unwrap().is_none()); - assert!(p.get_path(Param::File, &t).unwrap().is_none()); - assert!(p.get_blob(Param::File, &t).await.unwrap().is_none()); - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_params_unknown_key() -> Result<()> { // 'Z' is used as a key that is known to be unused; these keys should be ignored silently by definition. diff --git a/src/receive_imf.rs b/src/receive_imf.rs index dee7e1b27c..9d10177195 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1727,9 +1727,7 @@ RETURNING id context, part.typ, chat_id, - part.param - .get_path(Param::File, context) - .unwrap_or_default(), + part.param.get(Param::Filename), *msg_id, ) .await?; diff --git a/src/receive_imf/receive_imf_tests.rs b/src/receive_imf/receive_imf_tests.rs index 594bdc9fe8..d48c189325 100644 --- a/src/receive_imf/receive_imf_tests.rs +++ b/src/receive_imf/receive_imf_tests.rs @@ -5612,7 +5612,7 @@ PGh0bWw+PGJvZHk+dGV4dDwvYm9keT5kYXRh assert_eq!(msg.get_filename().unwrap(), "test.HTML"); - let blob = msg.param.get_blob(Param::File, alice).await?.unwrap(); + let blob = msg.param.get_file_blob(alice)?.unwrap(); assert_eq!(blob.suffix().unwrap(), "html"); Ok(()) diff --git a/src/sql.rs b/src/sql.rs index 9f0e791d2a..0a5be9e360 100644 --- a/src/sql.rs +++ b/src/sql.rs @@ -251,7 +251,7 @@ impl Sql { if recode_avatar { if let Some(avatar) = context.get_config(Config::Selfavatar).await? { - let mut blob = BlobObject::new_from_path(context, avatar.as_ref()).await?; + let mut blob = BlobObject::from_name(context, avatar)?; match blob.recode_to_avatar_size(context).await { Ok(()) => { if let Some(path) = blob.to_abs_path().to_str() { diff --git a/src/summary.rs b/src/summary.rs index d2f55f3053..2f0064c809 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -469,7 +469,8 @@ mod tests { let mut msg = Message::new(Viewtype::File); msg.set_text(some_text.clone()); - msg.param.set(Param::File, "foo.bar"); + msg.set_file_from_bytes(ctx, "foo.bar", b"data", None) + .unwrap(); msg.param.set_cmd(SystemMessage::AutocryptSetupMessage); assert_summary_texts(&msg, ctx, "Autocrypt Setup Message").await; // file name is not added for autocrypt setup messages } diff --git a/src/tools/tools_tests.rs b/src/tools/tools_tests.rs index 62c40bb752..119a400bd3 100644 --- a/src/tools/tools_tests.rs +++ b/src/tools/tools_tests.rs @@ -560,3 +560,38 @@ fn test_parse_mailto() { reps ); } + +#[test] +fn test_sanitize_filename() { + let name = sanitize_filename("Я ЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯЯ.txt"); + assert!(!name.is_empty()); + + let name = sanitize_filename("wot.tar.gz"); + assert_eq!(name, "wot.tar.gz"); + + let name = sanitize_filename(".foo.bar"); + assert_eq!(name, "file.foo.bar"); + + let name = sanitize_filename("foo?.bar"); + assert_eq!(name, "foo.bar"); + assert!(!name.contains('?')); + + let name = sanitize_filename("no-extension"); + assert_eq!(name, "no-extension"); + + let name = sanitize_filename("path/ignored\\this: is* forbidden?.c"); + assert_eq!(name, "this is forbidden.c"); + + let name = + sanitize_filename("file.with_lots_of_characters_behind_point_and_double_ending.tar.gz"); + assert_eq!( + name, + "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" + ); + + let name = sanitize_filename("a. tar.tar.gz"); + assert_eq!(name, "a. tar.tar.gz"); + + let name = sanitize_filename("Guia_uso_GNB (v0.8).pdf"); + assert_eq!(name, "Guia_uso_GNB (v0.8).pdf"); +}