mirror of
https://github.com/chatmail/core.git
synced 2026-04-17 21:46:35 +03:00
fix: Assume file extensions are 32 chars max and don't contain whitespace (#5338)
Before file extensions were also limited to 32 chars, but extra chars in the beginning were just cut off, e.g. "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" was considered to have an extension "d_point_and_double_ending.tar.gz". Better to take only "tar.gz" then. Also don't include whitespace-containing parts in extensions. File extensions generally don't contain whitespaces.
This commit is contained in:
29
src/blob.rs
29
src/blob.rs
@@ -232,41 +232,44 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
/// Get a file extension if any, including the dot, in lower case, otherwise an empty string.
|
||||
fn get_extension(name: &str) -> String {
|
||||
let mut name = name.to_string();
|
||||
let mut name = name;
|
||||
for part in name.rsplit('/') {
|
||||
if !part.is_empty() {
|
||||
name = part.to_string();
|
||||
name = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for part in name.rsplit('\\') {
|
||||
if !part.is_empty() {
|
||||
name = part.to_string();
|
||||
name = part;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Let's take the tricky filename
|
||||
// "file.with_lots_of_characters_behind_point_and_double_ending.tar.gz" as an example.
|
||||
// Split it into "file" and "with_lots_of_characters_behind_point_and_double_ending.tar.gz":
|
||||
let mut iter = name.splitn(2, '.');
|
||||
iter.next();
|
||||
|
||||
let ext_chars = iter.next().unwrap_or_default().chars();
|
||||
let ext: String = ext_chars
|
||||
// Assume that the extension is 32 chars maximum.
|
||||
let ext: String = name
|
||||
.chars()
|
||||
.rev()
|
||||
.take(32)
|
||||
.take_while(|c| !c.is_whitespace())
|
||||
.take(33)
|
||||
.collect::<Vec<_>>()
|
||||
.iter()
|
||||
.rev()
|
||||
.collect();
|
||||
// ext == "d_point_and_double_ending.tar.gz"
|
||||
// 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();
|
||||
if ext.is_empty() {
|
||||
ext
|
||||
String::new()
|
||||
} else {
|
||||
format!(".{ext}").to_lowercase()
|
||||
// Return ".d_point_and_double_ending.tar.gz", which is not perfect but acceptable.
|
||||
// Return ".tar.gz".
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use regex::Regex;
|
||||
use tokio::fs;
|
||||
|
||||
use super::*;
|
||||
@@ -2960,20 +2961,27 @@ Reply from different address
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_long_and_duplicated_filenames() -> Result<()> {
|
||||
async fn test_weird_and_duplicated_filenames() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
for filename_sent in &[
|
||||
"foo.bar very long file name test baz.tar.gz",
|
||||
"foobarabababababababbababababverylongfilenametestbaz.tar.gz",
|
||||
"fooo...tar.gz",
|
||||
"foo. .tar.gz",
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz",
|
||||
"a.tar.gz",
|
||||
"a.tar.gz",
|
||||
"a.a..a.a.a.a.tar.gz",
|
||||
for (filename_sent, expected_ext) in &[
|
||||
("foo.bar very long file name test baz.tar.gz", "tar.gz"),
|
||||
(
|
||||
"foo.barabababababababbababababverylongfilenametestbaz.tar.gz",
|
||||
"tar.gz",
|
||||
),
|
||||
("fooo...tar.gz", "..tar.gz"),
|
||||
("foo. .tar.gz", "tar.gz"),
|
||||
(
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.tar.gz",
|
||||
"tar.gz",
|
||||
),
|
||||
("a.tar.gz", "tar.gz"),
|
||||
("a.tar.gz", "tar.gz"),
|
||||
("a.a..a.a.a.a.tar.gz", "a..a.a.a.a.tar.gz"),
|
||||
("a. tar.tar.gz", "tar.gz"),
|
||||
] {
|
||||
let attachment = alice.blobdir.join(filename_sent);
|
||||
let content = format!("File content of {filename_sent}");
|
||||
@@ -2987,23 +2995,33 @@ async fn test_long_and_duplicated_filenames() -> Result<()> {
|
||||
|
||||
let msg_bob = bob.recv_msg(&sent).await;
|
||||
|
||||
async fn check_message(msg: &Message, t: &TestContext, filename: &str, content: &str) {
|
||||
async fn check_message(
|
||||
msg: &Message,
|
||||
t: &TestContext,
|
||||
filename: &str,
|
||||
expected_ext: &str,
|
||||
content: &str,
|
||||
) {
|
||||
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
||||
let resulting_filename = msg.get_filename().unwrap();
|
||||
assert_eq!(resulting_filename, filename);
|
||||
let path = msg.get_file(t).unwrap();
|
||||
if !msg.get_state().is_outgoing() {
|
||||
let re =
|
||||
Regex::new(&("^[[:xdigit:]]{16}.".to_string() + expected_ext + "$")).unwrap();
|
||||
assert!(
|
||||
re.is_match(path.file_name().unwrap().to_str().unwrap()),
|
||||
"invalid path {path:?}"
|
||||
);
|
||||
}
|
||||
let path2 = path.with_file_name("saved.txt");
|
||||
msg.save_file(t, &path2).await.unwrap();
|
||||
assert!(
|
||||
path.to_str().unwrap().ends_with(".tar.gz"),
|
||||
"path {path:?} doesn't end with .tar.gz"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(&path).await.unwrap(), content);
|
||||
assert_eq!(fs::read_to_string(&path2).await.unwrap(), content);
|
||||
fs::remove_file(path2).await.unwrap();
|
||||
}
|
||||
check_message(&msg_alice, &alice, filename_sent, &content).await;
|
||||
check_message(&msg_bob, &bob, filename_sent, &content).await;
|
||||
check_message(&msg_alice, &alice, filename_sent, expected_ext, &content).await;
|
||||
check_message(&msg_bob, &bob, filename_sent, expected_ext, &content).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user