mirror of
https://github.com/chatmail/core.git
synced 2026-05-11 19:06:29 +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::ImageReader;
|
||||||
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
use image::{DynamicImage, GenericImage, GenericImageView, ImageFormat, Pixel, Rgba};
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use tokio::io::AsyncWriteExt;
|
use tokio::{fs, task};
|
||||||
use tokio::{fs, io, task};
|
|
||||||
use tokio_stream::wrappers::ReadDirStream;
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
@@ -48,73 +47,6 @@ enum ImageOutputFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> BlobObject<'a> {
|
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.
|
/// Creates a blob object by copying or renaming an existing file.
|
||||||
/// If the source file is already in the blobdir, it will be renamed,
|
/// If the source file is already in the blobdir, it will be renamed,
|
||||||
/// otherwise it will be copied to the blobdir first.
|
/// 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.
|
/// Returns a [BlobObject] for an existing blob from a path.
|
||||||
///
|
///
|
||||||
/// The path must designate a file directly in the blobdir and
|
/// 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.
|
/// Checks whether a name is a valid blob name.
|
||||||
///
|
///
|
||||||
/// This is slightly less strict than stanitise_name, presumably
|
/// This is slightly less strict than stanitise_name, presumably
|
||||||
|
|||||||
@@ -104,55 +104,15 @@ async fn test_create_long_names() {
|
|||||||
assert!(blobname.len() < 70);
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_create_from_name_long() {
|
async fn test_create_from_name_long() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
||||||
fs::write(&src_ext, b"boo").await.unwrap();
|
fs::write(&src_ext, b"boo").await.unwrap();
|
||||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
let blob = BlobObject::create_and_deduplicate(&t, &src_ext, &src_ext).unwrap();
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
blob.as_name(),
|
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"));
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_add_white_bg() {
|
async fn test_add_white_bg() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
@@ -236,7 +138,7 @@ async fn test_add_white_bg() {
|
|||||||
let avatar_src = t.dir.path().join("avatar.png");
|
let avatar_src = t.dir.path().join("avatar.png");
|
||||||
fs::write(&avatar_src, bytes).await.unwrap();
|
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 img_wh = 128;
|
||||||
let maybe_sticker = &mut false;
|
let maybe_sticker = &mut false;
|
||||||
let strict_limits = true;
|
let strict_limits = true;
|
||||||
@@ -285,7 +187,7 @@ async fn test_selfavatar_outside_blobdir() {
|
|||||||
constants::BALANCED_AVATAR_SIZE,
|
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 maybe_sticker = &mut false;
|
||||||
let strict_limits = true;
|
let strict_limits = true;
|
||||||
blob.recode_to_size(&t, None, maybe_sticker, 1000, 3000, strict_limits)
|
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);
|
check_image_size(file_saved, compressed_width, compressed_height);
|
||||||
|
|
||||||
if original_width == compressed_width {
|
if original_width == compressed_width {
|
||||||
assert_extension(&alice, alice_msg, extension).await;
|
assert_extension(&alice, alice_msg, extension);
|
||||||
} else {
|
} else {
|
||||||
assert_extension(&alice, alice_msg, "jpg").await;
|
assert_extension(&alice, alice_msg, "jpg");
|
||||||
}
|
}
|
||||||
|
|
||||||
let bob_msg = bob.recv_msg(&sent).await;
|
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);
|
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
||||||
|
|
||||||
if original_width == compressed_width {
|
if original_width == compressed_width {
|
||||||
assert_extension(&bob, bob_msg, extension).await;
|
assert_extension(&bob, bob_msg, extension);
|
||||||
} else {
|
} else {
|
||||||
assert_extension(&bob, bob_msg, "jpg").await;
|
assert_extension(&bob, bob_msg, "jpg");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(img)
|
Ok(img)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn assert_extension(context: &TestContext, msg: Message, extension: &str) {
|
fn assert_extension(context: &TestContext, msg: Message, extension: &str) {
|
||||||
assert!(msg
|
assert!(msg
|
||||||
.param
|
.param
|
||||||
.get(Param::File)
|
.get(Param::File)
|
||||||
@@ -703,8 +605,7 @@ async fn assert_extension(context: &TestContext, msg: Message, extension: &str)
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
msg.param
|
msg.param
|
||||||
.get_blob(Param::File, context)
|
.get_file_blob(context)
|
||||||
.await
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.suffix()
|
.suffix()
|
||||||
|
|||||||
@@ -904,8 +904,7 @@ impl ChatId {
|
|||||||
if msg.viewtype == Viewtype::Vcard {
|
if msg.viewtype == Viewtype::Vcard {
|
||||||
let blob = msg
|
let blob = msg
|
||||||
.param
|
.param
|
||||||
.get_blob(Param::File, context)
|
.get_file_blob(context)?
|
||||||
.await?
|
|
||||||
.context("no file stored in params")?;
|
.context("no file stored in params")?;
|
||||||
msg.try_set_vcard(context, &blob.to_abs_path()).await?;
|
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() {
|
} else if msg.viewtype.has_file() {
|
||||||
let mut blob = msg
|
let mut blob = msg
|
||||||
.param
|
.param
|
||||||
.get_blob(Param::File, context)
|
.get_file_blob(context)?
|
||||||
.await?
|
|
||||||
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
.with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?;
|
||||||
let send_as_is = msg.viewtype == Viewtype::File;
|
let send_as_is = msg.viewtype == Viewtype::File;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ use crate::tools::time;
|
|||||||
use crate::webxdc::StatusUpdateItem;
|
use crate::webxdc::StatusUpdateItem;
|
||||||
use async_channel::{self as channel, Receiver, Sender};
|
use async_channel::{self as channel, Receiver, Sender};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::PathBuf;
|
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -100,9 +99,7 @@ pub async fn maybe_set_logging_xdc(
|
|||||||
context,
|
context,
|
||||||
msg.get_viewtype(),
|
msg.get_viewtype(),
|
||||||
chat_id,
|
chat_id,
|
||||||
msg.param
|
msg.param.get(Param::Filename),
|
||||||
.get_path(Param::Filename, context)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
msg.get_id(),
|
msg.get_id(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -115,18 +112,16 @@ pub async fn maybe_set_logging_xdc_inner(
|
|||||||
context: &Context,
|
context: &Context,
|
||||||
viewtype: Viewtype,
|
viewtype: Viewtype,
|
||||||
chat_id: ChatId,
|
chat_id: ChatId,
|
||||||
filename: Option<PathBuf>,
|
filename: Option<&str>,
|
||||||
msg_id: MsgId,
|
msg_id: MsgId,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if viewtype == Viewtype::Webxdc {
|
if viewtype == Viewtype::Webxdc {
|
||||||
if let Some(file) = filename {
|
if let Some(filename) = filename {
|
||||||
if let Some(file_name) = file.file_name().and_then(|name| name.to_str()) {
|
if filename.starts_with("debug_logging")
|
||||||
if file_name.starts_with("debug_logging")
|
&& filename.ends_with(".xdc")
|
||||||
&& file_name.ends_with(".xdc")
|
&& chat_id.is_self_talk(context).await?
|
||||||
&& chat_id.is_self_talk(context).await?
|
{
|
||||||
{
|
set_debug_logging_xdc(context, Some(msg_id)).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.
|
/// Returns the full path to the file associated with a message.
|
||||||
pub fn get_file(&self, context: &Context) -> Option<PathBuf> {
|
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.
|
/// 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
|
/// 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<()> {
|
pub(crate) async fn try_calc_and_set_dimensions(&mut self, context: &Context) -> Result<()> {
|
||||||
if self.viewtype.has_file() {
|
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 let Some(path_and_filename) = file_param {
|
||||||
if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
|
if (self.viewtype == Viewtype::Image || self.viewtype == Viewtype::Gif)
|
||||||
&& !self.param.exists(Param::Width)
|
&& !self.param.exists(Param::Width)
|
||||||
@@ -817,7 +817,7 @@ impl Message {
|
|||||||
|
|
||||||
/// Returns the size of the file in bytes, if applicable.
|
/// Returns the size of the file in bytes, if applicable.
|
||||||
pub async fn get_filebytes(&self, context: &Context) -> Result<Option<u64>> {
|
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(
|
Ok(Some(get_filebytes(context, &path).await.with_context(
|
||||||
|| format!("failed to get {} size in bytes", path.display()),
|
|| 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)?;
|
msg.set_file_from_bytes(t, "/\\:ee.tx*T ", b"hallo", None)?;
|
||||||
assert_eq!(msg.get_filename().unwrap(), "ee.txT");
|
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");
|
assert_eq!(blob.suffix().unwrap(), "txt");
|
||||||
|
|
||||||
// The filename shouldn't be empty if there were only illegal characters:
|
// 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
|
let blob = msg
|
||||||
.param
|
.param
|
||||||
.get_blob(Param::File, context)
|
.get_file_blob(context)?
|
||||||
.await?
|
|
||||||
.context("msg has no file")?;
|
.context("msg has no file")?;
|
||||||
|
|
||||||
// Get file name to use for sending. For privacy purposes, we do
|
// 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:
|
// Make sure the file is there even though the html is wrong:
|
||||||
let param = &message.parts[0].param;
|
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 f = tokio::fs::File::open(blob.to_abs_path()).await.unwrap();
|
||||||
let size = f.metadata().await.unwrap().len();
|
let size = f.metadata().await.unwrap().len();
|
||||||
assert_eq!(size, 154);
|
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);
|
assert_eq!(mimeparser.parts[0].typ, Viewtype::File);
|
||||||
let blob: BlobObject = mimeparser.parts[0]
|
let blob: BlobObject = mimeparser.parts[0]
|
||||||
.param
|
.param
|
||||||
.get_blob(Param::File, &context)
|
.get_file_blob(&context)
|
||||||
.await
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1883,8 +1882,7 @@ This is the epilogue. It is also to be ignored.";
|
|||||||
assert_eq!(mimeparser.parts[1].typ, Viewtype::File);
|
assert_eq!(mimeparser.parts[1].typ, Viewtype::File);
|
||||||
let blob: BlobObject = mimeparser.parts[1]
|
let blob: BlobObject = mimeparser.parts[1]
|
||||||
.param
|
.param
|
||||||
.get_blob(Param::File, &context)
|
.get_file_blob(&context)
|
||||||
.await
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
153
src/param.rs
153
src/param.rs
@@ -3,6 +3,7 @@ use std::fmt;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str;
|
use std::str;
|
||||||
|
|
||||||
|
use anyhow::ensure;
|
||||||
use anyhow::{bail, Error, Result};
|
use anyhow::{bail, Error, Result};
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@@ -295,6 +296,9 @@ impl Params {
|
|||||||
|
|
||||||
/// Set the given key to the passed in value.
|
/// Set the given key to the passed in value.
|
||||||
pub fn set(&mut self, key: Param, value: impl ToString) -> &mut Self {
|
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.inner.insert(key, value.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -357,59 +361,20 @@ impl Params {
|
|||||||
self.get(key).and_then(|s| s.parse().ok())
|
self.get(key).and_then(|s| s.parse().ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the given parameter and parse as [ParamsFile].
|
/// Returns a [BlobObject] for the [Param::File] parameter.
|
||||||
///
|
pub fn get_file_blob<'a>(&self, context: &'a Context) -> Result<Option<BlobObject<'a>>> {
|
||||||
/// See also [Params::get_blob] and [Params::get_path] which may
|
let Some(val) = self.get(Param::File) else {
|
||||||
/// be more convenient.
|
return Ok(None);
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
ensure!(val.starts_with("$BLOBDIR/"));
|
||||||
|
let blob = BlobObject::from_name(context, val.to_string())?;
|
||||||
Ok(Some(blob))
|
Ok(Some(blob))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the parameter and returns a [PathBuf] for it.
|
/// Returns a [PathBuf] for the [Param::File] parameter.
|
||||||
///
|
pub fn get_file_path(&self, context: &Context) -> Result<Option<PathBuf>> {
|
||||||
/// This parses the parameter value as a [ParamsFile] and returns
|
let blob = self.get_file_blob(context)?;
|
||||||
/// a [PathBuf] to the file.
|
Ok(blob.map(|p| p.to_abs_path()))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the given parameter to the passed in `i32`.
|
/// 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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::path::Path;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use tokio::fs;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::test_utils::TestContext;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dc_param() {
|
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::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_eq!(p1.get_int(Param::Height), None);
|
||||||
assert!(!p1.exists(Param::Height));
|
assert!(!p1.exists(Param::Height));
|
||||||
|
|
||||||
@@ -483,13 +418,13 @@ mod tests {
|
|||||||
let mut p1 = Params::new();
|
let mut p1 = Params::new();
|
||||||
|
|
||||||
p1.set(Param::Forwarded, "foo")
|
p1.set(Param::Forwarded, "foo")
|
||||||
.set_int(Param::File, 2)
|
.set_int(Param::Width, 2)
|
||||||
.remove(Param::GuaranteeE2ee)
|
.remove(Param::GuaranteeE2ee)
|
||||||
.set_int(Param::Duration, 4);
|
.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.to_string(), "a=foo\nd=4",);
|
||||||
assert_eq!(p1.len(), 2);
|
assert_eq!(p1.len(), 2);
|
||||||
@@ -511,56 +446,6 @@ mod tests {
|
|||||||
assert_eq!(params.to_string().parse::<Params>().unwrap(), params);
|
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)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_params_unknown_key() -> Result<()> {
|
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.
|
// '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,
|
context,
|
||||||
part.typ,
|
part.typ,
|
||||||
chat_id,
|
chat_id,
|
||||||
part.param
|
part.param.get(Param::Filename),
|
||||||
.get_path(Param::File, context)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
*msg_id,
|
*msg_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -5612,7 +5612,7 @@ PGh0bWw+PGJvZHk+dGV4dDwvYm9keT5kYXRh
|
|||||||
|
|
||||||
assert_eq!(msg.get_filename().unwrap(), "test.HTML");
|
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");
|
assert_eq!(blob.suffix().unwrap(), "html");
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ impl Sql {
|
|||||||
|
|
||||||
if recode_avatar {
|
if recode_avatar {
|
||||||
if let Some(avatar) = context.get_config(Config::Selfavatar).await? {
|
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 {
|
match blob.recode_to_avatar_size(context).await {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Some(path) = blob.to_abs_path().to_str() {
|
if let Some(path) = blob.to_abs_path().to_str() {
|
||||||
|
|||||||
@@ -469,7 +469,8 @@ mod tests {
|
|||||||
|
|
||||||
let mut msg = Message::new(Viewtype::File);
|
let mut msg = Message::new(Viewtype::File);
|
||||||
msg.set_text(some_text.clone());
|
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);
|
msg.param.set_cmd(SystemMessage::AutocryptSetupMessage);
|
||||||
assert_summary_texts(&msg, ctx, "Autocrypt Setup Message").await; // file name is not added for autocrypt setup messages
|
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
|
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