mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 13:36:30 +03:00
refactor: Remove unused blob functions (#6563)
This commit is contained in:
135
src/blob.rs
135
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::<u32>(), 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<BlobObject<'a>> {
|
||||
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<BlobObject<'a>> {
|
||||
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::<Vec<_>>()
|
||||
.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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<PathBuf>,
|
||||
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?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PathBuf> {
|
||||
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<Option<u64>> {
|
||||
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()),
|
||||
)?))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1628,8 +1628,7 @@ async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'s
|
||||
|
||||
let blob = msg
|
||||
.param
|
||||
.get_blob(Param::File, context)
|
||||
.await?
|
||||
.get_file_blob(context)?
|
||||
.context("msg has no file")?;
|
||||
|
||||
// Get file name to use for sending. For privacy purposes, we do
|
||||
|
||||
@@ -785,7 +785,7 @@ MDYyMDYxNTE1RTlDOEE4Cj4+CnN0YXJ0eHJlZgo4Mjc4CiUlRU9GCg==
|
||||
|
||||
// Make sure the file is there even though the html is wrong:
|
||||
let param = &message.parts[0].param;
|
||||
let blob: BlobObject = param.get_blob(Param::File, &t).await.unwrap().unwrap();
|
||||
let blob: BlobObject = param.get_file_blob(&t).unwrap().unwrap();
|
||||
let f = tokio::fs::File::open(blob.to_abs_path()).await.unwrap();
|
||||
let size = f.metadata().await.unwrap().len();
|
||||
assert_eq!(size, 154);
|
||||
@@ -1871,8 +1871,7 @@ This is the epilogue. It is also to be ignored.";
|
||||
assert_eq!(mimeparser.parts[0].typ, Viewtype::File);
|
||||
let blob: BlobObject = mimeparser.parts[0]
|
||||
.param
|
||||
.get_blob(Param::File, &context)
|
||||
.await
|
||||
.get_file_blob(&context)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1883,8 +1882,7 @@ This is the epilogue. It is also to be ignored.";
|
||||
assert_eq!(mimeparser.parts[1].typ, Viewtype::File);
|
||||
let blob: BlobObject = mimeparser.parts[1]
|
||||
.param
|
||||
.get_blob(Param::File, &context)
|
||||
.await
|
||||
.get_file_blob(&context)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
||||
153
src/param.rs
153
src/param.rs
@@ -3,6 +3,7 @@ use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::str;
|
||||
|
||||
use anyhow::ensure;
|
||||
use anyhow::{bail, Error, Result};
|
||||
use num_traits::FromPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -295,6 +296,9 @@ impl Params {
|
||||
|
||||
/// Set the given key to the passed in value.
|
||||
pub fn set(&mut self, key: Param, value: impl ToString) -> &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<Option<ParamsFile<'a>>> {
|
||||
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<Option<BlobObject<'a>>> {
|
||||
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<Option<BlobObject<'a>>> {
|
||||
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<Option<PathBuf>> {
|
||||
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<Option<PathBuf>> {
|
||||
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<ParamsFile<'a>> {
|
||||
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::<Params>().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.
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user