Skip to content

Commit

Permalink
feat: Use random filename suffixes for blobstorage (#4309)
Browse files Browse the repository at this point in the history
Thanks to the added Param::Filename these random suffixes aren't sent over the network.
  • Loading branch information
iequidoo committed Jul 27, 2023
1 parent ceca5bf commit 3c26ede
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 53 deletions.
109 changes: 62 additions & 47 deletions src/blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use tokio::{fs, io};
use tokio_stream::wrappers::ReadDirStream;

use crate::config::Config;
use crate::constants::{self, MediaQuality};
use crate::constants::{self, MediaQuality, BLOB_CREATE_ATTEMPTS};
use crate::context::Context;
use crate::events::EventType;
use crate::log::LogExt;
Expand Down Expand Up @@ -71,10 +71,9 @@ impl<'a> BlobObject<'a> {
stem: &str,
ext: &str,
) -> Result<(String, fs::File)> {
const MAX_ATTEMPT: u32 = 16;
let mut attempt = 0;
let mut name = format!("{stem}{ext}");
loop {
let name = format!("{}-{:016x}{}", stem, rand::random::<u64>(), ext);
attempt += 1;
let path = dir.join(&name);
match fs::OpenOptions::new()
Expand All @@ -85,12 +84,10 @@ impl<'a> BlobObject<'a> {
{
Ok(file) => return Ok((name, file)),
Err(err) => {
if attempt >= MAX_ATTEMPT {
if attempt >= BLOB_CREATE_ATTEMPTS {
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);
}
}
}
Expand Down Expand Up @@ -621,6 +618,7 @@ mod tests {
use anyhow::Result;
use fs::File;
use image::{GenericImageView, Pixel};
use regex::Regex;

use super::*;
use crate::chat::{self, create_group_chat, ProtectionStatus};
Expand All @@ -640,32 +638,43 @@ mod tests {
async fn test_create() {
let t = TestContext::new().await;
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();
assert_eq!(data, b"hello");
assert_eq!(blob.as_name(), "$BLOBDIR/foo");
assert_eq!(blob.to_abs_path(), t.get_blobdir().join("foo"));
assert_eq!(
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)]
async fn test_lowercase_ext() {
let t = TestContext::new().await;
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)]
async fn test_as_file_name() {
let t = TestContext::new().await;
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)]
async fn test_as_rel_path() {
let t = TestContext::new().await;
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)]
Expand All @@ -680,30 +689,30 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_create_dup() {
let t = TestContext::new().await;
BlobObject::create(&t, "foo.txt", b"hello").await.unwrap();
let foo_path = t.get_blobdir().join("foo.txt");
let re = Regex::new("^foo-[[:xdigit:]]{16}.txt$").unwrap();

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());
BlobObject::create(&t, "foo.txt", b"world").await.unwrap();
let mut dir = fs::read_dir(t.get_blobdir()).await.unwrap();
while let Ok(Some(dirent)) = dir.next_entry().await {
let fname = dirent.file_name();
if fname == foo_path.file_name().unwrap() {
assert_eq!(fs::read(&foo_path).await.unwrap(), b"hello");
} else {
let name = fname.to_str().unwrap();
assert!(name.starts_with("foo"));
assert!(name.ends_with(".txt"));
}
}

let blob = BlobObject::create(&t, "foo.txt", b"world").await.unwrap();
assert!(re.is_match(blob.as_rel_path().to_str().unwrap()));
let foo_path2 = t.get_blobdir().join(blob.as_file_name());
assert!(foo_path2.exists());

assert!(foo_path != foo_path2);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_double_ext_preserved() {
let t = TestContext::new().await;
BlobObject::create(&t, "foo.tar.gz", b"hello")
let blob = BlobObject::create(&t, "foo.tar.gz", b"hello")
.await
.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());
BlobObject::create(&t, "foo.tar.gz", b"world")
.await
Expand Down Expand Up @@ -737,7 +746,8 @@ mod tests {
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 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();
assert_eq!(data, b"boo");

Expand All @@ -758,7 +768,8 @@ mod tests {
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
.await
.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();
assert_eq!(data, b"boo");

Expand All @@ -769,6 +780,7 @@ mod tests {
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;
Expand All @@ -777,10 +789,10 @@ mod tests {
let blob = BlobObject::new_from_path(&t, src_ext.as_ref())
.await
.unwrap();
assert_eq!(
blob.as_name(),
"$BLOBDIR/autocrypt-setup-message-4137848473.html"
);
let re =
Regex::new("^\\$BLOBDIR/autocrypt-setup-message-4137848473-[[:xdigit:]]{16}.html$")
.unwrap();
assert!(re.is_match(blob.as_name()));
}

#[test]
Expand Down Expand Up @@ -838,19 +850,21 @@ mod tests {
let avatar_src = t.dir.path().join("avatar.jpg");
let avatar_bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
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()))
.await
.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!(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_blob,
avatar_blob,
constants::BALANCED_AVATAR_SIZE,
constants::BALANCED_AVATAR_SIZE,
);
Expand All @@ -860,12 +874,12 @@ mod tests {
file.metadata().await.unwrap().len()
}

let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
let blob = BlobObject::new_from_path(&t, avatar_blob).await.unwrap();
let strict_limits = true;
blob.recode_to_size(&t, blob.to_abs_path(), 1000, 3000, strict_limits)
.unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);
assert!(file_size(avatar_blob).await <= 3000);
assert!(file_size(avatar_blob).await > 2000);
tokio::task::block_in_place(move || {
let img = image::open(avatar_blob).unwrap();
assert!(img.width() > 130);
Expand Down Expand Up @@ -905,18 +919,19 @@ mod tests {
let avatar_src = t.dir.path().join("avatar.png");
let avatar_bytes = include_bytes!("../test-data/image/avatar64x64.png");
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()))
.await
.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!(
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()));
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
Expand Down
7 changes: 5 additions & 2 deletions src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3848,6 +3848,7 @@ mod tests {
use crate::message::delete_msgs;
use crate::receive_imf::receive_imf;
use crate::test_utils::{TestContext, TestContextManager};
use regex::Regex;
use tokio::fs;

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
Expand Down Expand Up @@ -6244,9 +6245,11 @@ mod tests {

// the file bob receives should not contain BIDI-control characters
assert_eq!(
Some("$BLOBDIR/harmless_file.txt.exe"),
msg.param.get(Param::File),
msg.param.get(Param::Filename).unwrap(),
"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(())
}
}
3 changes: 3 additions & 0 deletions src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,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
pub(crate) const DC_FOLDERS_CONFIGURED_VERSION: i32 = 4;

// Maximum attemps to create a blob file.
pub(crate) const BLOB_CREATE_ATTEMPTS: u32 = 2;

#[cfg(test)]
mod tests {
use num_traits::FromPrimitive;
Expand Down
7 changes: 3 additions & 4 deletions src/mimeparser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2192,6 +2192,7 @@ mod tests {
#![allow(clippy::indexing_slicing)]

use mailparse::ParsedMail;
use regex::Regex;

use super::*;
use crate::{
Expand Down Expand Up @@ -3711,10 +3712,8 @@ Message.
mime_message.parts[0].msg,
"this is a classic email – I attached the .EML file".to_string()
);
assert_eq!(
mime_message.parts[0].param.get(Param::File),
Some("$BLOBDIR/.eml")
);
let re = Regex::new("^\\$BLOBDIR/-[[:xdigit:]]{16}.eml$").unwrap();
assert!(re.is_match(mime_message.parts[0].param.get(Param::File).unwrap()));

assert_eq!(mime_message.parts[0].org_filename, Some(".eml".to_string()));

Expand Down

0 comments on commit 3c26ede

Please sign in to comment.