mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +03:00
feat: Use random filename suffixes for blobstorage (#4309)
Recently there was an accident with a chatbot that replaced its avatar set from the command line with an unrelated avatar of a contact. Both the `selfavatar` setting and the contact avatar `i` param pointed to `$BLOBDIR/avatar.png` at the time it was detected. How this happened is unclear, but it is possible that `avatar.png` was removed, unmounted or otherwise not detected by the core, and the core stored avatar received from the contact as `avatar.png`, while `selfavatar` config still pointed to `$BLOBDIR/avatar.png`. Such bugs are unavoidable even if the core itself has no bugs as we cannot rely on blobdir not reside on the faulty network filesystem, being incorrectly backed up and restored etc., so we should assume that files may be randomly removed. Then there may be dangling `$BLOBDIR/...` references in the database which may accidentally point to unrelated files, could even be an `avatar.png` file sent to the bot in private. To prevent such bugs, we add random filename suffixes for the blobdir objects. Thanks to the added Param::Filename these random suffixes aren't sent over the network.
This commit is contained in:
@@ -107,8 +107,8 @@ class Message:
|
|||||||
lib.dc_msg_set_html(self._dc_msg, as_dc_charpointer(html_text))
|
lib.dc_msg_set_html(self._dc_msg, as_dc_charpointer(html_text))
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
def filename(self):
|
def file_path(self):
|
||||||
"""filename if there was an attachment, otherwise empty string."""
|
"""file path if there was an attachment, otherwise empty string."""
|
||||||
return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg))
|
return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg))
|
||||||
|
|
||||||
def set_file(self, path, mime_type=None):
|
def set_file(self, path, mime_type=None):
|
||||||
@@ -119,9 +119,8 @@ class Message:
|
|||||||
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
def basename(self) -> str:
|
def filename(self) -> str:
|
||||||
"""basename of the attachment if it exists, otherwise empty string."""
|
"""filename of the attachment if any, otherwise empty string."""
|
||||||
# FIXME, it does not return basename
|
|
||||||
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
|
|||||||
@@ -181,16 +181,16 @@ def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
|
|||||||
|
|
||||||
msg = send_and_receive_message()
|
msg = send_and_receive_message()
|
||||||
assert msg.text == "withfile"
|
assert msg.text == "withfile"
|
||||||
assert open(msg.filename).read() == "some data"
|
assert open(msg.file_path).read() == "some data"
|
||||||
msg.filename.index(basename)
|
msg.file_path.index(basename)
|
||||||
assert msg.filename.endswith(ext)
|
assert msg.file_path.endswith(ext)
|
||||||
|
|
||||||
msg2 = send_and_receive_message()
|
msg2 = send_and_receive_message()
|
||||||
assert msg2.text == "withfile"
|
assert msg2.text == "withfile"
|
||||||
assert open(msg2.filename).read() == "some data"
|
assert open(msg2.file_path).read() == "some data"
|
||||||
msg2.filename.index(basename)
|
msg2.file_path.index(basename)
|
||||||
assert msg2.filename.endswith(ext)
|
assert msg2.file_path.endswith(ext)
|
||||||
assert msg.filename != msg2.filename
|
assert msg.file_path != msg2.file_path
|
||||||
|
|
||||||
|
|
||||||
def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
||||||
@@ -214,9 +214,9 @@ def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
|||||||
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
msg = ac2.get_message_by_id(ev.data2)
|
msg = ac2.get_message_by_id(ev.data2)
|
||||||
|
|
||||||
assert open(msg.filename).read() == content
|
assert open(msg.file_path).read() == content
|
||||||
msg.filename.index(basename)
|
msg.file_path.index(basename)
|
||||||
assert msg.filename.endswith(ext)
|
assert msg.file_path.endswith(ext)
|
||||||
|
|
||||||
|
|
||||||
def test_html_message(acfactory, lp):
|
def test_html_message(acfactory, lp):
|
||||||
@@ -310,7 +310,7 @@ def test_webxdc_message(acfactory, data, lp):
|
|||||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||||
msg1 = chat.send_msg(msg1)
|
msg1 = chat.send_msg(msg1)
|
||||||
assert msg1.is_webxdc()
|
assert msg1.is_webxdc()
|
||||||
assert msg1.filename
|
assert msg1.file_path
|
||||||
|
|
||||||
assert msg1.send_status_update({"payload": "test1"}, "some test data")
|
assert msg1.send_status_update({"payload": "test1"}, "some test data")
|
||||||
assert msg1.send_status_update({"payload": "test2"}, "more test data")
|
assert msg1.send_status_update({"payload": "test2"}, "more test data")
|
||||||
@@ -323,7 +323,7 @@ def test_webxdc_message(acfactory, data, lp):
|
|||||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||||
assert msg2.text == "message1"
|
assert msg2.text == "message1"
|
||||||
assert msg2.is_webxdc()
|
assert msg2.is_webxdc()
|
||||||
assert msg2.filename
|
assert msg2.file_path
|
||||||
ac2._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
ac2._evtracker.get_info_contains("Marked messages [0-9]+ in folder INBOX as seen.")
|
||||||
ac2.direct_imap.select_folder("Inbox")
|
ac2.direct_imap.select_folder("Inbox")
|
||||||
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||||
@@ -338,7 +338,7 @@ def test_webxdc_huge_update(acfactory, data, lp):
|
|||||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||||
msg1 = chat.send_msg(msg1)
|
msg1 = chat.send_msg(msg1)
|
||||||
assert msg1.is_webxdc()
|
assert msg1.is_webxdc()
|
||||||
assert msg1.filename
|
assert msg1.file_path
|
||||||
|
|
||||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||||
assert msg2.is_webxdc()
|
assert msg2.is_webxdc()
|
||||||
@@ -360,7 +360,7 @@ def test_webxdc_download_on_demand(acfactory, data, lp):
|
|||||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||||
msg1 = chat.send_msg(msg1)
|
msg1 = chat.send_msg(msg1)
|
||||||
assert msg1.is_webxdc()
|
assert msg1.is_webxdc()
|
||||||
assert msg1.filename
|
assert msg1.file_path
|
||||||
|
|
||||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||||
assert msg2.is_webxdc()
|
assert msg2.is_webxdc()
|
||||||
@@ -1350,7 +1350,7 @@ def test_quote_attachment(tmp_path, acfactory, lp):
|
|||||||
received_reply = ac1._evtracker.wait_next_incoming_message()
|
received_reply = ac1._evtracker.wait_next_incoming_message()
|
||||||
assert received_reply.text == "message reply"
|
assert received_reply.text == "message reply"
|
||||||
assert received_reply.quoted_text == received_message.text
|
assert received_reply.quoted_text == received_message.text
|
||||||
assert open(received_reply.filename).read() == "data to send"
|
assert open(received_reply.file_path).read() == "data to send"
|
||||||
|
|
||||||
|
|
||||||
def test_saved_mime_on_received_message(acfactory, lp):
|
def test_saved_mime_on_received_message(acfactory, lp):
|
||||||
@@ -1443,8 +1443,8 @@ def test_send_and_receive_image(acfactory, lp, data):
|
|||||||
assert ev.data2 == msg_out.id
|
assert ev.data2 == msg_out.id
|
||||||
msg_in = ac2.get_message_by_id(msg_out.id)
|
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||||
assert msg_in.is_image()
|
assert msg_in.is_image()
|
||||||
assert os.path.exists(msg_in.filename)
|
assert os.path.exists(msg_in.file_path)
|
||||||
assert os.stat(msg_in.filename).st_size == os.stat(path).st_size
|
assert os.stat(msg_in.file_path).st_size == os.stat(path).st_size
|
||||||
m = message_queue.get()
|
m = message_queue.get()
|
||||||
assert m == msg_in
|
assert m == msg_in
|
||||||
|
|
||||||
@@ -1577,7 +1577,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
|||||||
assert len(messages) == 3
|
assert len(messages) == 3
|
||||||
assert messages[0].text == "msg1"
|
assert messages[0].text == "msg1"
|
||||||
assert messages[1].filemime == "image/png"
|
assert messages[1].filemime == "image/png"
|
||||||
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size
|
assert os.stat(messages[1].file_path).st_size == os.stat(original_image_path).st_size
|
||||||
ac.set_config("displayname", "new displayname")
|
ac.set_config("displayname", "new displayname")
|
||||||
assert ac.get_config("displayname") == "new displayname"
|
assert ac.get_config("displayname") == "new displayname"
|
||||||
|
|
||||||
@@ -1650,7 +1650,7 @@ def test_ac_setup_message(acfactory, lp):
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
msg.continue_key_transfer(str(reversed(setup_code)))
|
msg.continue_key_transfer(str(reversed(setup_code)))
|
||||||
lp.sec("try a good setup code")
|
lp.sec("try a good setup code")
|
||||||
print("*************** Incoming ASM File at: ", msg.filename)
|
print("*************** Incoming ASM File at: ", msg.file_path)
|
||||||
print("*************** Setup Code: ", setup_code)
|
print("*************** Setup Code: ", setup_code)
|
||||||
msg.continue_key_transfer(setup_code)
|
msg.continue_key_transfer(setup_code)
|
||||||
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
assert ac1.get_info()["fingerprint"] == ac2.get_info()["fingerprint"]
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ class TestOnlineInCreation:
|
|||||||
src = tmp_path / "file.txt"
|
src = tmp_path / "file.txt"
|
||||||
src.write_text("hello there\n")
|
src.write_text("hello there\n")
|
||||||
msg = chat.send_file(str(src))
|
msg = chat.send_file(str(src))
|
||||||
assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file"))
|
assert msg.file_path.startswith(os.path.join(ac1.get_blobdir(), "file"))
|
||||||
assert msg.filename.endswith(".txt")
|
assert msg.file_path.endswith(".txt")
|
||||||
|
|
||||||
def test_forward_increation(self, acfactory, data, lp):
|
def test_forward_increation(self, acfactory, data, lp):
|
||||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||||
@@ -99,9 +99,9 @@ class TestOnlineInCreation:
|
|||||||
|
|
||||||
lp.sec("wait1 for original or forwarded messages to arrive")
|
lp.sec("wait1 for original or forwarded messages to arrive")
|
||||||
received_original = ac2._evtracker.wait_next_incoming_message()
|
received_original = ac2._evtracker.wait_next_incoming_message()
|
||||||
assert cmp(received_original.filename, orig, shallow=False)
|
assert cmp(received_original.file_path, orig, shallow=False)
|
||||||
|
|
||||||
lp.sec("wait2 for original or forwarded messages to arrive")
|
lp.sec("wait2 for original or forwarded messages to arrive")
|
||||||
received_copy = ac2._evtracker.wait_next_incoming_message()
|
received_copy = ac2._evtracker.wait_next_incoming_message()
|
||||||
assert received_copy.id != received_original.id
|
assert received_copy.id != received_original.id
|
||||||
assert cmp(received_copy.filename, orig, shallow=False)
|
assert cmp(received_copy.file_path, orig, shallow=False)
|
||||||
|
|||||||
@@ -440,31 +440,34 @@ class TestOfflineChat:
|
|||||||
assert msg.is_image()
|
assert msg.is_image()
|
||||||
assert msg
|
assert msg
|
||||||
assert msg.id > 0
|
assert msg.id > 0
|
||||||
assert os.path.exists(msg.filename)
|
assert os.path.exists(msg.file_path)
|
||||||
assert msg.filemime == "image/png"
|
assert msg.filemime == "image/png"
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("fn", "typein", "typeout"),
|
("stem", "ext", "typein", "typeout"),
|
||||||
[
|
[
|
||||||
("r", None, "application/octet-stream"),
|
("r", "", None, "application/octet-stream"),
|
||||||
("r.txt", None, "text/plain"),
|
("r", ".txt", None, "text/plain"),
|
||||||
("r.txt", "text/plain", "text/plain"),
|
("r", ".txt", "text/plain", "text/plain"),
|
||||||
("r.txt", "image/png", "image/png"),
|
("r", ".txt", "image/png", "image/png"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_message_file(self, chat1, data, lp, fn, typein, typeout):
|
def test_message_file(self, chat1, data, lp, stem, ext, typein, typeout):
|
||||||
lp.sec("sending file")
|
lp.sec("sending file")
|
||||||
|
fn = stem + ext
|
||||||
fp = data.get_path(fn)
|
fp = data.get_path(fn)
|
||||||
msg = chat1.send_file(fp, typein)
|
msg = chat1.send_file(fp, typein)
|
||||||
assert msg
|
assert msg
|
||||||
assert msg.id > 0
|
assert msg.id > 0
|
||||||
assert msg.is_file()
|
assert msg.is_file()
|
||||||
assert os.path.exists(msg.filename)
|
assert os.path.exists(msg.file_path)
|
||||||
assert msg.filename.endswith(msg.basename)
|
assert msg.file_path.endswith(ext)
|
||||||
|
assert msg.filename == fn
|
||||||
assert msg.filemime == typeout
|
assert msg.filemime == typeout
|
||||||
msg2 = chat1.send_file(fp, typein)
|
msg2 = chat1.send_file(fp, typein)
|
||||||
assert msg2 != msg
|
assert msg2 != msg
|
||||||
assert msg2.filename != msg.filename
|
assert msg2.file_path != msg.file_path
|
||||||
|
assert msg2.filename == fn
|
||||||
|
|
||||||
def test_create_contact(self, acfactory):
|
def test_create_contact(self, acfactory):
|
||||||
ac1 = acfactory.get_pseudo_configured_account()
|
ac1 = acfactory.get_pseudo_configured_account()
|
||||||
@@ -532,7 +535,7 @@ class TestOfflineChat:
|
|||||||
messages = chat2.get_messages()
|
messages = chat2.get_messages()
|
||||||
assert len(messages) == 2
|
assert len(messages) == 2
|
||||||
assert messages[0].text == "msg1"
|
assert messages[0].text == "msg1"
|
||||||
assert os.path.exists(messages[1].filename)
|
assert os.path.exists(messages[1].file_path)
|
||||||
|
|
||||||
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
|
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
|
||||||
passphrase1 = "passphrase1"
|
passphrase1 = "passphrase1"
|
||||||
@@ -570,7 +573,7 @@ class TestOfflineChat:
|
|||||||
messages = chat2.get_messages()
|
messages = chat2.get_messages()
|
||||||
assert len(messages) == 2
|
assert len(messages) == 2
|
||||||
assert messages[0].text == "msg1"
|
assert messages[0].text == "msg1"
|
||||||
assert os.path.exists(messages[1].filename)
|
assert os.path.exists(messages[1].file_path)
|
||||||
|
|
||||||
ac2.shutdown()
|
ac2.shutdown()
|
||||||
|
|
||||||
@@ -587,7 +590,7 @@ class TestOfflineChat:
|
|||||||
messages = chat2.get_messages()
|
messages = chat2.get_messages()
|
||||||
assert len(messages) == 2
|
assert len(messages) == 2
|
||||||
assert messages[0].text == "msg1"
|
assert messages[0].text == "msg1"
|
||||||
assert os.path.exists(messages[1].filename)
|
assert os.path.exists(messages[1].file_path)
|
||||||
|
|
||||||
def test_import_export_with_passphrase(self, acfactory, tmp_path):
|
def test_import_export_with_passphrase(self, acfactory, tmp_path):
|
||||||
passphrase = "test_passphrase"
|
passphrase = "test_passphrase"
|
||||||
@@ -626,7 +629,7 @@ class TestOfflineChat:
|
|||||||
messages = chat2.get_messages()
|
messages = chat2.get_messages()
|
||||||
assert len(messages) == 2
|
assert len(messages) == 2
|
||||||
assert messages[0].text == "msg1"
|
assert messages[0].text == "msg1"
|
||||||
assert os.path.exists(messages[1].filename)
|
assert os.path.exists(messages[1].file_path)
|
||||||
|
|
||||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
||||||
"""
|
"""
|
||||||
@@ -671,7 +674,7 @@ class TestOfflineChat:
|
|||||||
messages = chat2.get_messages()
|
messages = chat2.get_messages()
|
||||||
assert len(messages) == 2
|
assert len(messages) == 2
|
||||||
assert messages[0].text == "msg1"
|
assert messages[0].text == "msg1"
|
||||||
assert os.path.exists(messages[1].filename)
|
assert os.path.exists(messages[1].file_path)
|
||||||
|
|
||||||
ac2.shutdown()
|
ac2.shutdown()
|
||||||
|
|
||||||
@@ -688,7 +691,7 @@ class TestOfflineChat:
|
|||||||
messages = chat2.get_messages()
|
messages = chat2.get_messages()
|
||||||
assert len(messages) == 2
|
assert len(messages) == 2
|
||||||
assert messages[0].text == "msg1"
|
assert messages[0].text == "msg1"
|
||||||
assert os.path.exists(messages[1].filename)
|
assert os.path.exists(messages[1].file_path)
|
||||||
|
|
||||||
def test_set_get_draft(self, chat1):
|
def test_set_get_draft(self, chat1):
|
||||||
msg = Message.new_empty(chat1.account, "text")
|
msg = Message.new_empty(chat1.account, "text")
|
||||||
|
|||||||
109
src/blob.rs
109
src/blob.rs
@@ -16,7 +16,7 @@ use tokio::{fs, io};
|
|||||||
use tokio_stream::wrappers::ReadDirStream;
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::constants::{self, MediaQuality};
|
use crate::constants::{self, MediaQuality, BLOB_CREATE_ATTEMPTS};
|
||||||
use crate::context::Context;
|
use crate::context::Context;
|
||||||
use crate::events::EventType;
|
use crate::events::EventType;
|
||||||
use crate::log::LogExt;
|
use crate::log::LogExt;
|
||||||
@@ -71,10 +71,9 @@ impl<'a> BlobObject<'a> {
|
|||||||
stem: &str,
|
stem: &str,
|
||||||
ext: &str,
|
ext: &str,
|
||||||
) -> Result<(String, fs::File)> {
|
) -> Result<(String, fs::File)> {
|
||||||
const MAX_ATTEMPT: u32 = 16;
|
|
||||||
let mut attempt = 0;
|
let mut attempt = 0;
|
||||||
let mut name = format!("{stem}{ext}");
|
|
||||||
loop {
|
loop {
|
||||||
|
let name = format!("{}-{:016x}{}", stem, rand::random::<u64>(), ext);
|
||||||
attempt += 1;
|
attempt += 1;
|
||||||
let path = dir.join(&name);
|
let path = dir.join(&name);
|
||||||
match fs::OpenOptions::new()
|
match fs::OpenOptions::new()
|
||||||
@@ -85,12 +84,10 @@ impl<'a> BlobObject<'a> {
|
|||||||
{
|
{
|
||||||
Ok(file) => return Ok((name, file)),
|
Ok(file) => return Ok((name, file)),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
if attempt >= MAX_ATTEMPT {
|
if attempt >= BLOB_CREATE_ATTEMPTS {
|
||||||
return Err(err).context("failed to create file");
|
return Err(err).context("failed to create file");
|
||||||
} else if attempt == 1 && !dir.exists() {
|
} else if attempt == 1 && !dir.exists() {
|
||||||
fs::create_dir_all(dir).await.log_err(context).ok();
|
fs::create_dir_all(dir).await.log_err(context).ok();
|
||||||
} else {
|
|
||||||
name = format!("{}-{}{}", stem, rand::random::<u32>(), ext);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -674,6 +671,7 @@ mod tests {
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use fs::File;
|
use fs::File;
|
||||||
use image::{GenericImageView, Pixel};
|
use image::{GenericImageView, Pixel};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
use crate::chat::{self, create_group_chat, ProtectionStatus};
|
||||||
@@ -693,32 +691,43 @@ mod tests {
|
|||||||
async fn test_create() {
|
async fn test_create() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let blob = BlobObject::create(&t, "foo", b"hello").await.unwrap();
|
let blob = BlobObject::create(&t, "foo", b"hello").await.unwrap();
|
||||||
let fname = t.get_blobdir().join("foo");
|
let re = Regex::new("^foo-[[:xdigit:]]{16}$").unwrap();
|
||||||
|
assert!(re.is_match(blob.as_file_name()));
|
||||||
|
let fname = t.get_blobdir().join(blob.as_file_name());
|
||||||
let data = fs::read(fname).await.unwrap();
|
let data = fs::read(fname).await.unwrap();
|
||||||
assert_eq!(data, b"hello");
|
assert_eq!(data, b"hello");
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/foo");
|
assert_eq!(
|
||||||
assert_eq!(blob.to_abs_path(), t.get_blobdir().join("foo"));
|
blob.as_name(),
|
||||||
|
"$BLOBDIR/".to_string() + blob.as_file_name()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
blob.to_abs_path(),
|
||||||
|
t.get_blobdir().join(blob.as_file_name())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_lowercase_ext() {
|
async fn test_lowercase_ext() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let blob = BlobObject::create(&t, "foo.TXT", b"hello").await.unwrap();
|
let blob = BlobObject::create(&t, "foo.TXT", b"hello").await.unwrap();
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/foo.txt");
|
let re = Regex::new("^\\$BLOBDIR/foo-[[:xdigit:]]{16}.txt$").unwrap();
|
||||||
|
assert!(re.is_match(blob.as_name()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_as_file_name() {
|
async fn test_as_file_name() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
|
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
|
||||||
assert_eq!(blob.as_file_name(), "foo.txt");
|
let re = Regex::new("^foo-[[:xdigit:]]{16}.txt$").unwrap();
|
||||||
|
assert!(re.is_match(blob.as_file_name()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_as_rel_path() {
|
async fn test_as_rel_path() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
|
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
|
||||||
assert_eq!(blob.as_rel_path(), Path::new("foo.txt"));
|
let re = Regex::new("^foo-[[:xdigit:]]{16}.txt$").unwrap();
|
||||||
|
assert!(re.is_match(blob.as_rel_path().to_str().unwrap()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
@@ -733,30 +742,30 @@ mod tests {
|
|||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_create_dup() {
|
async fn test_create_dup() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
|
let re = Regex::new("^foo-[[:xdigit:]]{16}.txt$").unwrap();
|
||||||
let foo_path = t.get_blobdir().join("foo.txt");
|
|
||||||
|
let blob = BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
|
||||||
|
assert!(re.is_match(blob.as_rel_path().to_str().unwrap()));
|
||||||
|
let foo_path = t.get_blobdir().join(blob.as_file_name());
|
||||||
assert!(foo_path.exists());
|
assert!(foo_path.exists());
|
||||||
BlobObject::create(&t, "foo.txt", b"world").await.unwrap();
|
|
||||||
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
|
let blob = BlobObject::create(&t, "foo.txt", b"world").await.unwrap();
|
||||||
while let Ok(Some(dirent)) = dir.next_entry().await {
|
assert!(re.is_match(blob.as_rel_path().to_str().unwrap()));
|
||||||
let fname = dirent.file_name();
|
let foo_path2 = t.get_blobdir().join(blob.as_file_name());
|
||||||
if fname == foo_path.file_name().unwrap() {
|
assert!(foo_path2.exists());
|
||||||
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
|
|
||||||
} else {
|
assert!(foo_path != foo_path2);
|
||||||
let name = fname.to_str().unwrap();
|
|
||||||
assert!(name.starts_with("foo"));
|
|
||||||
assert!(name.ends_with(".txt"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
async fn test_double_ext_preserved() {
|
async fn test_double_ext_preserved() {
|
||||||
let t = TestContext::new().await;
|
let t = TestContext::new().await;
|
||||||
BlobObject::create(&t, "foo.tar.gz", b"hello")
|
let blob = BlobObject::create(&t, "foo.tar.gz", b"hello")
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let foo_path = t.get_blobdir().join("foo.tar.gz");
|
let re = Regex::new("^foo-[[:xdigit:]]{16}.tar.gz$").unwrap();
|
||||||
|
assert!(re.is_match(blob.as_file_name()));
|
||||||
|
let foo_path = t.get_blobdir().join(blob.as_file_name());
|
||||||
assert!(foo_path.exists());
|
assert!(foo_path.exists());
|
||||||
BlobObject::create(&t, "foo.tar.gz", b"world")
|
BlobObject::create(&t, "foo.tar.gz", b"world")
|
||||||
.await
|
.await
|
||||||
@@ -790,7 +799,8 @@ mod tests {
|
|||||||
let src = t.dir.path().join("src");
|
let src = t.dir.path().join("src");
|
||||||
fs::write(&src, b"boo").await.unwrap();
|
fs::write(&src, b"boo").await.unwrap();
|
||||||
let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
|
let blob = BlobObject::create_and_copy(&t, src.as_ref()).await.unwrap();
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/src");
|
let re = Regex::new("^\\$BLOBDIR/src-[[:xdigit:]]{16}$").unwrap();
|
||||||
|
assert!(re.is_match(blob.as_name()));
|
||||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||||
assert_eq!(data, b"boo");
|
assert_eq!(data, b"boo");
|
||||||
|
|
||||||
@@ -811,7 +821,8 @@ mod tests {
|
|||||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(blob.as_name(), "$BLOBDIR/external");
|
let re = Regex::new("^\\$BLOBDIR/external-[[:xdigit:]]{16}$").unwrap();
|
||||||
|
assert!(re.is_match(blob.as_name()));
|
||||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||||
assert_eq!(data, b"boo");
|
assert_eq!(data, b"boo");
|
||||||
|
|
||||||
@@ -822,6 +833,7 @@ mod tests {
|
|||||||
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
let data = fs::read(blob.to_abs_path()).await.unwrap();
|
||||||
assert_eq!(data, b"boo");
|
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;
|
||||||
@@ -830,10 +842,10 @@ mod tests {
|
|||||||
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
let re =
|
||||||
blob.as_name(),
|
Regex::new("^\\$BLOBDIR/autocrypt-setup-message-4137848473-[[:xdigit:]]{16}.html$")
|
||||||
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
|
.unwrap();
|
||||||
);
|
assert!(re.is_match(blob.as_name()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -891,19 +903,21 @@ mod tests {
|
|||||||
let avatar_src = t.dir.path().join("avatar.jpg");
|
let avatar_src = t.dir.path().join("avatar.jpg");
|
||||||
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
|
||||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||||
let avatar_blob = t.get_blobdir().join("avatar.jpg");
|
|
||||||
assert!(!avatar_blob.exists());
|
|
||||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||||
|
let blobdir = t.get_blobdir().to_str().unwrap();
|
||||||
|
assert!(avatar_blob.starts_with(blobdir));
|
||||||
|
let re = Regex::new("avatar-[[:xdigit:]]{16}.jpg$").unwrap();
|
||||||
|
assert!(re.is_match(&avatar_blob));
|
||||||
|
let avatar_blob = Path::new(&avatar_blob);
|
||||||
assert!(avatar_blob.exists());
|
assert!(avatar_blob.exists());
|
||||||
assert!(fs::metadata(&avatar_blob).await.unwrap().len() < avatar_bytes.len() as u64);
|
assert!(fs::metadata(&avatar_blob).await.unwrap().len() < avatar_bytes.len() as u64);
|
||||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
|
||||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
|
||||||
|
|
||||||
check_image_size(avatar_src, 1000, 1000);
|
check_image_size(avatar_src, 1000, 1000);
|
||||||
check_image_size(
|
check_image_size(
|
||||||
&avatar_blob,
|
avatar_blob,
|
||||||
constants::BALANCED_AVATAR_SIZE,
|
constants::BALANCED_AVATAR_SIZE,
|
||||||
constants::BALANCED_AVATAR_SIZE,
|
constants::BALANCED_AVATAR_SIZE,
|
||||||
);
|
);
|
||||||
@@ -913,7 +927,7 @@ mod tests {
|
|||||||
file.metadata().await.unwrap().len()
|
file.metadata().await.unwrap().len()
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
|
let mut blob = BlobObject::new_from_path(&t, avatar_blob).await.unwrap();
|
||||||
let maybe_sticker = &mut false;
|
let maybe_sticker = &mut false;
|
||||||
let strict_limits = true;
|
let strict_limits = true;
|
||||||
blob.recode_to_size(
|
blob.recode_to_size(
|
||||||
@@ -925,8 +939,8 @@ mod tests {
|
|||||||
strict_limits,
|
strict_limits,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(file_size(&avatar_blob).await <= 3000);
|
assert!(file_size(avatar_blob).await <= 3000);
|
||||||
assert!(file_size(&avatar_blob).await > 2000);
|
assert!(file_size(avatar_blob).await > 2000);
|
||||||
tokio::task::block_in_place(move || {
|
tokio::task::block_in_place(move || {
|
||||||
let img = image::open(avatar_blob).unwrap();
|
let img = image::open(avatar_blob).unwrap();
|
||||||
assert!(img.width() > 130);
|
assert!(img.width() > 130);
|
||||||
@@ -966,18 +980,19 @@ mod tests {
|
|||||||
let avatar_src = t.dir.path().join("avatar.png");
|
let avatar_src = t.dir.path().join("avatar.png");
|
||||||
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
|
||||||
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
fs::write(&avatar_src, avatar_bytes).await.unwrap();
|
||||||
let avatar_blob = t.get_blobdir().join("avatar.png");
|
|
||||||
assert!(!avatar_blob.exists());
|
|
||||||
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(avatar_blob.exists());
|
let avatar_blob = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
||||||
|
let blobdir = t.get_blobdir().to_str().unwrap();
|
||||||
|
assert!(avatar_blob.starts_with(blobdir));
|
||||||
|
let re = Regex::new("avatar-[[:xdigit:]]{16}.png$").unwrap();
|
||||||
|
assert!(re.is_match(&avatar_blob));
|
||||||
|
assert!(Path::new(&avatar_blob).exists());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fs::metadata(&avatar_blob).await.unwrap().len(),
|
fs::metadata(&avatar_blob).await.unwrap().len(),
|
||||||
avatar_bytes.len() as u64
|
avatar_bytes.len() as u64
|
||||||
);
|
);
|
||||||
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap();
|
|
||||||
assert_eq!(avatar_cfg, avatar_blob.to_str().map(|s| s.to_string()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
|||||||
@@ -4473,6 +4473,7 @@ mod tests {
|
|||||||
use crate::message::delete_msgs;
|
use crate::message::delete_msgs;
|
||||||
use crate::receive_imf::receive_imf;
|
use crate::receive_imf::receive_imf;
|
||||||
use crate::test_utils::{sync, TestContext, TestContextManager};
|
use crate::test_utils::{sync, TestContext, TestContextManager};
|
||||||
|
use regex::Regex;
|
||||||
use strum::IntoEnumIterator;
|
use strum::IntoEnumIterator;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
|
|
||||||
@@ -7019,9 +7020,11 @@ mod tests {
|
|||||||
|
|
||||||
// the file bob receives should not contain BIDI-control characters
|
// the file bob receives should not contain BIDI-control characters
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some("$BLOBDIR/harmless_file.txt.exe"),
|
msg.param.get(Param::Filename).unwrap(),
|
||||||
msg.param.get(Param::File),
|
"harmless_file.txt.exe"
|
||||||
);
|
);
|
||||||
|
let re = Regex::new("^\\$BLOBDIR/harmless_file-[[:xdigit:]]{16}.txt.exe$").unwrap();
|
||||||
|
assert!(re.is_match(msg.param.get(Param::File).unwrap()));
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,9 @@ pub const WORSE_IMAGE_SIZE: u32 = 640;
|
|||||||
// this value can be increased if the folder configuration is changed and must be redone on next program start
|
// this value can be increased if the folder configuration is changed and must be redone on next program start
|
||||||
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
|
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;
|
||||||
|
|
||||||
|
// Maximum attemps to create a blob file.
|
||||||
|
pub(crate) const BLOB_CREATE_ATTEMPTS: u32 = 2;
|
||||||
|
|
||||||
// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
|
// If more recipients are needed in SMTP's `RCPT TO:` header, the recipient list is split into
|
||||||
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting
|
// chunks. This does not affect MIME's `To:` header. Can be overwritten by setting
|
||||||
// `max_smtp_rcpt_to` in the provider db.
|
// `max_smtp_rcpt_to` in the provider db.
|
||||||
|
|||||||
@@ -2221,6 +2221,7 @@ mod tests {
|
|||||||
#![allow(clippy::indexing_slicing)]
|
#![allow(clippy::indexing_slicing)]
|
||||||
|
|
||||||
use mailparse::ParsedMail;
|
use mailparse::ParsedMail;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -3740,10 +3741,8 @@ Message.
|
|||||||
mime_message.parts[0].msg,
|
mime_message.parts[0].msg,
|
||||||
"this is a classic email – I attached the .EML file".to_string()
|
"this is a classic email – I attached the .EML file".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
let re = Regex::new("^\\$BLOBDIR/-[[:xdigit:]]{16}.eml$").unwrap();
|
||||||
mime_message.parts[0].param.get(Param::File),
|
assert!(re.is_match(mime_message.parts[0].param.get(Param::File).unwrap()));
|
||||||
Some("$BLOBDIR/.eml")
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(mime_message.parts[0].org_filename, Some(".eml".to_string()));
|
assert_eq!(mime_message.parts[0].org_filename, Some(".eml".to_string()));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user