mirror of
https://github.com/chatmail/core.git
synced 2026-05-05 14:26:30 +03:00
This causes problems for Delta Chat Desktop at <https://github.com/deltachat/deltachat-desktop/pull/6278> even though the logic was originally introduced for iOS. If the problem remains on iOS, heuristics can be added into iOS UI.
874 lines
30 KiB
Rust
874 lines
30 KiB
Rust
use std::time::Duration;
|
|
|
|
use super::*;
|
|
use crate::message::{Message, Viewtype};
|
|
use crate::param::Param;
|
|
use crate::sql;
|
|
use crate::test_utils::{self, AVATAR_64x64_BYTES, AVATAR_64x64_DEDUPLICATED, TestContext};
|
|
use crate::tools::SystemTime;
|
|
|
|
fn check_image_size(path: impl AsRef<Path>, width: u32, height: u32) -> image::DynamicImage {
|
|
tokio::task::block_in_place(move || {
|
|
let img = ImageReader::open(path)
|
|
.expect("failed to open image")
|
|
.with_guessed_format()
|
|
.expect("failed to guess format")
|
|
.decode()
|
|
.expect("failed to decode image");
|
|
assert_eq!(img.width(), width, "invalid width");
|
|
assert_eq!(img.height(), height, "invalid height");
|
|
img
|
|
})
|
|
}
|
|
|
|
const FILE_BYTES: &[u8] = b"hello";
|
|
const FILE_DEDUPLICATED: &str = "ea8f163db38682925e4491c5e58d4bb.txt";
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_create() {
|
|
let t = TestContext::new().await;
|
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
|
let fname = t.get_blobdir().join(FILE_DEDUPLICATED);
|
|
let data = fs::read(fname).await.unwrap();
|
|
assert_eq!(data, FILE_BYTES);
|
|
assert_eq!(blob.as_name(), format!("$BLOBDIR/{FILE_DEDUPLICATED}"));
|
|
assert_eq!(blob.to_abs_path(), t.get_blobdir().join(FILE_DEDUPLICATED));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_lowercase_ext() {
|
|
let t = TestContext::new().await;
|
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.TXT").unwrap();
|
|
assert!(
|
|
blob.as_name().ends_with(".txt"),
|
|
"Blob {blob:?} should end with .txt"
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_suffix() {
|
|
let t = TestContext::new().await;
|
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
|
assert_eq!(blob.suffix(), Some("txt"));
|
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "bar").unwrap();
|
|
assert_eq!(blob.suffix(), None);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_create_dup() {
|
|
let t = TestContext::new().await;
|
|
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.txt").unwrap();
|
|
let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED);
|
|
assert!(foo_path.exists());
|
|
BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.txt").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(), FILE_BYTES);
|
|
} else {
|
|
let name = fname.to_str().unwrap();
|
|
assert!(name.ends_with(".txt"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_double_ext() {
|
|
let t = TestContext::new().await;
|
|
BlobObject::create_and_deduplicate_from_bytes(&t, FILE_BYTES, "foo.tar.gz").unwrap();
|
|
let foo_path = t.get_blobdir().join(FILE_DEDUPLICATED).with_extension("gz");
|
|
assert!(foo_path.exists());
|
|
BlobObject::create_and_deduplicate_from_bytes(&t, b"world", "foo.tar.gz").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(), FILE_BYTES);
|
|
} else {
|
|
let name = fname.to_str().unwrap();
|
|
println!("{name}");
|
|
assert_eq!(name.starts_with("foo"), false);
|
|
assert_eq!(name.ends_with(".tar.gz"), false);
|
|
assert!(name.ends_with(".gz"));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_create_long_names() {
|
|
let t = TestContext::new().await;
|
|
let s = format!("file.{}", "a".repeat(100));
|
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"data", &s).unwrap();
|
|
let blobname = blob.as_name().split('/').next_back().unwrap();
|
|
assert!(blobname.len() < 70);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_create_from_name_long() {
|
|
let t = TestContext::new().await;
|
|
let src_ext = t.dir.path().join("autocrypt-setup-message-4137848473.html");
|
|
fs::write(&src_ext, b"boo").await.unwrap();
|
|
let blob = BlobObject::create_and_deduplicate(&t, &src_ext, &src_ext).unwrap();
|
|
assert_eq!(
|
|
blob.as_name(),
|
|
"$BLOBDIR/06f010b24d1efe57ffab44a8ad20c54.html"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_is_blob_name() {
|
|
assert!(BlobObject::is_acceptible_blob_name("foo"));
|
|
assert!(BlobObject::is_acceptible_blob_name("foo.txt"));
|
|
assert!(BlobObject::is_acceptible_blob_name(&"f".repeat(128)));
|
|
assert!(!BlobObject::is_acceptible_blob_name("foo/bar"));
|
|
assert!(!BlobObject::is_acceptible_blob_name("foo\\bar"));
|
|
assert!(!BlobObject::is_acceptible_blob_name("foo\x00bar"));
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_add_white_bg() {
|
|
let t = TestContext::new().await;
|
|
let bytes0 = include_bytes!("../../test-data/image/logo.png").as_slice();
|
|
let bytes1 = include_bytes!("../../test-data/image/avatar900x900.png").as_slice();
|
|
for (bytes, color) in [
|
|
(bytes0, [255u8, 255, 255, 255]),
|
|
(bytes1, [253u8, 198, 0, 255]),
|
|
] {
|
|
let avatar_src = t.dir.path().join("avatar.png");
|
|
fs::write(&avatar_src, bytes).await.unwrap();
|
|
|
|
let mut blob = BlobObject::create_and_deduplicate(&t, &avatar_src, &avatar_src).unwrap();
|
|
let img_wh = 128;
|
|
let viewtype = &mut Viewtype::Image;
|
|
let strict_limits = true;
|
|
blob.check_or_recode_to_size(&t, None, viewtype, img_wh, 20_000, strict_limits)
|
|
.unwrap();
|
|
tokio::task::block_in_place(move || {
|
|
let img = ImageReader::open(blob.to_abs_path())
|
|
.unwrap()
|
|
.with_guessed_format()
|
|
.unwrap()
|
|
.decode()
|
|
.unwrap();
|
|
assert!(img.width() == img_wh);
|
|
assert!(img.height() == img_wh);
|
|
assert_eq!(img.get_pixel(0, 0), Rgba(color));
|
|
});
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_selfavatar_outside_blobdir() {
|
|
async fn file_size(path_buf: &Path) -> u64 {
|
|
fs::metadata(path_buf).await.unwrap().len()
|
|
}
|
|
|
|
let t = TestContext::new().await;
|
|
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();
|
|
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 avatar_path = Path::new(&avatar_blob);
|
|
let scaled_avatar_size = file_size(avatar_path).await;
|
|
info!(&t, "Scaled avatar size: {scaled_avatar_size}.");
|
|
assert!(scaled_avatar_size < avatar_bytes.len() as u64);
|
|
|
|
check_image_size(avatar_src, 1000, 1000);
|
|
check_image_size(
|
|
&avatar_blob,
|
|
constants::BALANCED_AVATAR_SIZE,
|
|
constants::BALANCED_AVATAR_SIZE,
|
|
);
|
|
|
|
assert!(
|
|
avatar_blob.ends_with("2a048b6fcd86448032b854ea1ad7608.jpg"),
|
|
"The avatar filename should be its hash, but instead it's {avatar_blob}"
|
|
);
|
|
|
|
let mut blob = BlobObject::create_and_deduplicate(&t, avatar_path, avatar_path).unwrap();
|
|
let viewtype = &mut Viewtype::Image;
|
|
let strict_limits = true;
|
|
blob.check_or_recode_to_size(&t, None, viewtype, 1000, 3000, strict_limits)
|
|
.unwrap();
|
|
let new_file_size = file_size(&blob.to_abs_path()).await;
|
|
assert!(new_file_size <= 3000);
|
|
assert!(new_file_size > 2000);
|
|
// The new file should be smaller:
|
|
assert!(new_file_size < scaled_avatar_size);
|
|
// And the original file should not be touched:
|
|
assert_eq!(file_size(avatar_path).await, scaled_avatar_size);
|
|
tokio::task::block_in_place(move || {
|
|
let img = ImageReader::open(blob.to_abs_path())
|
|
.unwrap()
|
|
.with_guessed_format()
|
|
.unwrap()
|
|
.decode()
|
|
.unwrap();
|
|
assert!(img.width() > 130);
|
|
assert_eq!(img.width(), img.height());
|
|
});
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_selfavatar_in_blobdir() {
|
|
let t = TestContext::new().await;
|
|
let avatar_src = t.get_blobdir().join("avatar.png");
|
|
fs::write(&avatar_src, test_utils::AVATAR_900x900_BYTES)
|
|
.await
|
|
.unwrap();
|
|
|
|
check_image_size(&avatar_src, 900, 900);
|
|
|
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
|
.await
|
|
.unwrap();
|
|
let avatar_cfg = t.get_config(Config::Selfavatar).await.unwrap().unwrap();
|
|
assert!(
|
|
avatar_cfg.ends_with("d57cb5ce5f371531b6e1fb17b6dd1af.png"),
|
|
"Avatar file name {avatar_cfg} should end with its hash"
|
|
);
|
|
|
|
check_image_size(
|
|
avatar_cfg,
|
|
constants::BALANCED_AVATAR_SIZE,
|
|
constants::BALANCED_AVATAR_SIZE,
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_selfavatar_copy_without_recode() {
|
|
let t = TestContext::new().await;
|
|
let avatar_src = t.dir.path().join("avatar.png");
|
|
fs::write(&avatar_src, AVATAR_64x64_BYTES).await.unwrap();
|
|
let avatar_blob = t.get_blobdir().join(AVATAR_64x64_DEDUPLICATED);
|
|
assert!(!avatar_blob.exists());
|
|
t.set_config(Config::Selfavatar, Some(avatar_src.to_str().unwrap()))
|
|
.await
|
|
.unwrap();
|
|
assert!(avatar_blob.exists());
|
|
assert_eq!(
|
|
fs::metadata(&avatar_blob).await.unwrap().len(),
|
|
AVATAR_64x64_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)]
|
|
async fn test_recode_image_1() {
|
|
let bytes = include_bytes!("../../test-data/image/avatar1000x1000.jpg");
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Image,
|
|
media_quality_config: "0",
|
|
bytes,
|
|
extension: "jpg",
|
|
has_exif: true,
|
|
original_width: 1000,
|
|
original_height: 1000,
|
|
compressed_width: 1000,
|
|
compressed_height: 1000,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Image,
|
|
media_quality_config: "1",
|
|
bytes,
|
|
extension: "jpg",
|
|
has_exif: true,
|
|
original_width: 1000,
|
|
original_height: 1000,
|
|
compressed_width: 1000,
|
|
compressed_height: 1000,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_recode_image_2() {
|
|
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
|
|
let bytes = include_bytes!("../../test-data/image/rectangle2000x1800-rotated.jpg");
|
|
let img_rotated = SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Image,
|
|
media_quality_config: "0",
|
|
bytes,
|
|
extension: "jpg",
|
|
has_exif: true,
|
|
original_width: 2000,
|
|
original_height: 1800,
|
|
orientation: Some(Orientation::Rotate270),
|
|
compressed_width: 1800,
|
|
compressed_height: 2000,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
assert_correct_rotation(&img_rotated);
|
|
|
|
let mut buf = Cursor::new(vec![]);
|
|
img_rotated.write_to(&mut buf, ImageFormat::Jpeg).unwrap();
|
|
let bytes = buf.into_inner();
|
|
|
|
let img_rotated = SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Image,
|
|
media_quality_config: "1",
|
|
bytes: &bytes,
|
|
extension: "jpg",
|
|
original_width: 1800,
|
|
original_height: 2000,
|
|
compressed_width: 1800,
|
|
compressed_height: 2000,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
assert_correct_rotation(&img_rotated);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_recode_image_vflipped() {
|
|
let bytes = include_bytes!("../../test-data/image/rectangle200x180-vflipped.jpg");
|
|
let img_rotated = SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Image,
|
|
media_quality_config: "0",
|
|
bytes,
|
|
extension: "jpg",
|
|
has_exif: true,
|
|
original_width: 200,
|
|
original_height: 180,
|
|
orientation: Some(Orientation::FlipVertical),
|
|
compressed_width: 200,
|
|
compressed_height: 180,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
assert_correct_rotation(&img_rotated);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_recode_image_bad_exif() {
|
|
// `exiftool` reports for this file "Bad offset for IFD0 XResolution", still Exif must be
|
|
// detected and removed.
|
|
let bytes = include_bytes!("../../test-data/image/1000x1000-bad-exif.jpg");
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Image,
|
|
media_quality_config: "0",
|
|
bytes,
|
|
extension: "jpg",
|
|
has_exif: true,
|
|
original_width: 1000,
|
|
original_height: 1000,
|
|
compressed_width: 1000,
|
|
compressed_height: 1000,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_recode_image_balanced_png() {
|
|
let bytes = include_bytes!("../../test-data/image/screenshot.png");
|
|
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Image,
|
|
media_quality_config: "0",
|
|
bytes,
|
|
extension: "png",
|
|
original_width: 1920,
|
|
original_height: 1080,
|
|
compressed_width: 1920,
|
|
compressed_height: 1080,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Image,
|
|
media_quality_config: "1",
|
|
bytes,
|
|
extension: "png",
|
|
original_width: 1920,
|
|
original_height: 1080,
|
|
compressed_width: constants::WORSE_IMAGE_SIZE,
|
|
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::File,
|
|
media_quality_config: "1",
|
|
bytes,
|
|
extension: "png",
|
|
original_width: 1920,
|
|
original_height: 1080,
|
|
compressed_width: 1920,
|
|
compressed_height: 1080,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::File,
|
|
media_quality_config: "1",
|
|
bytes,
|
|
extension: "png",
|
|
original_width: 1920,
|
|
original_height: 1080,
|
|
compressed_width: 1920,
|
|
compressed_height: 1080,
|
|
set_draft: true,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Sticker,
|
|
media_quality_config: "0",
|
|
bytes,
|
|
extension: "png",
|
|
original_width: 1920,
|
|
original_height: 1080,
|
|
res_viewtype: Some(Viewtype::Sticker),
|
|
compressed_width: 1920,
|
|
compressed_height: 1080,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_sticker_with_exif() {
|
|
let bytes = include_bytes!("../../test-data/image/logo-exif.png");
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Sticker,
|
|
bytes,
|
|
extension: "png",
|
|
// TODO: Pretend there's no Exif. Currently `exif` crate doesn't detect Exif in this image,
|
|
// so the test doesn't check all the logic it should.
|
|
has_exif: false,
|
|
original_width: 135,
|
|
original_height: 135,
|
|
res_viewtype: Some(Viewtype::Sticker),
|
|
compressed_width: 135,
|
|
compressed_height: 135,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
/// Tests that RGBA PNG can be recoded into JPEG
|
|
/// by dropping alpha channel.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_recode_image_rgba_png_to_jpeg() {
|
|
let bytes = include_bytes!("../../test-data/image/screenshot-rgba.png");
|
|
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Image,
|
|
media_quality_config: "1",
|
|
bytes,
|
|
extension: "png",
|
|
original_width: 1920,
|
|
original_height: 1080,
|
|
compressed_width: constants::WORSE_IMAGE_SIZE,
|
|
compressed_height: constants::WORSE_IMAGE_SIZE * 1080 / 1920,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_recode_image_huge_jpg() {
|
|
let bytes = include_bytes!("../../test-data/image/screenshot.jpg");
|
|
SendImageCheckMediaquality {
|
|
viewtype: Viewtype::Image,
|
|
media_quality_config: "0",
|
|
bytes,
|
|
extension: "jpg",
|
|
has_exif: true,
|
|
original_width: 1920,
|
|
original_height: 1080,
|
|
compressed_width: constants::BALANCED_IMAGE_SIZE,
|
|
compressed_height: constants::BALANCED_IMAGE_SIZE * 1080 / 1920,
|
|
..Default::default()
|
|
}
|
|
.test()
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
fn assert_correct_rotation(img: &DynamicImage) {
|
|
// The test images are black in the bottom left corner after correctly applying
|
|
// the EXIF orientation
|
|
|
|
let [luma] = img.get_pixel(10, 10).to_luma().0;
|
|
assert_eq!(luma, 255);
|
|
let [luma] = img.get_pixel(img.width() - 10, 10).to_luma().0;
|
|
assert_eq!(luma, 255);
|
|
let [luma] = img
|
|
.get_pixel(img.width() - 10, img.height() - 10)
|
|
.to_luma()
|
|
.0;
|
|
assert_eq!(luma, 255);
|
|
let [luma] = img.get_pixel(10, img.height() - 10).to_luma().0;
|
|
assert_eq!(luma, 0);
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct SendImageCheckMediaquality<'a> {
|
|
pub(crate) viewtype: Viewtype,
|
|
pub(crate) media_quality_config: &'a str,
|
|
pub(crate) bytes: &'a [u8],
|
|
pub(crate) extension: &'a str,
|
|
pub(crate) has_exif: bool,
|
|
pub(crate) original_width: u32,
|
|
pub(crate) original_height: u32,
|
|
pub(crate) orientation: Option<Orientation>,
|
|
pub(crate) res_viewtype: Option<Viewtype>,
|
|
pub(crate) compressed_width: u32,
|
|
pub(crate) compressed_height: u32,
|
|
pub(crate) set_draft: bool,
|
|
}
|
|
|
|
impl SendImageCheckMediaquality<'_> {
|
|
pub(crate) async fn test(self) -> anyhow::Result<DynamicImage> {
|
|
let viewtype = self.viewtype;
|
|
let media_quality_config = self.media_quality_config;
|
|
let bytes = self.bytes;
|
|
let extension = self.extension;
|
|
let has_exif = self.has_exif;
|
|
let original_width = self.original_width;
|
|
let original_height = self.original_height;
|
|
let orientation = self.orientation.unwrap_or(Orientation::NoTransforms);
|
|
let res_viewtype = self.res_viewtype.unwrap_or(Viewtype::Image);
|
|
let compressed_width = self.compressed_width;
|
|
let compressed_height = self.compressed_height;
|
|
let set_draft = self.set_draft;
|
|
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
alice
|
|
.set_config(Config::MediaQuality, Some(media_quality_config))
|
|
.await?;
|
|
let file = alice.get_blobdir().join("file").with_extension(extension);
|
|
let file_name = format!("file.{extension}");
|
|
|
|
fs::write(&file, &bytes)
|
|
.await
|
|
.context("failed to write file")?;
|
|
check_image_size(&file, original_width, original_height);
|
|
|
|
let (_, exif) = image_metadata(&std::fs::File::open(&file)?)?;
|
|
if has_exif {
|
|
let exif = exif.unwrap();
|
|
assert_eq!(exif_orientation(&exif, &alice), orientation);
|
|
} else {
|
|
assert!(exif.is_none());
|
|
}
|
|
|
|
let mut msg = Message::new(viewtype);
|
|
msg.set_file_and_deduplicate(&alice, &file, Some(&file_name), None)?;
|
|
let chat = alice.create_chat(&bob).await;
|
|
if set_draft {
|
|
chat.id.set_draft(&alice, Some(&mut msg)).await.unwrap();
|
|
msg = chat.id.get_draft(&alice).await.unwrap().unwrap();
|
|
assert_eq!(msg.get_viewtype(), Viewtype::File);
|
|
}
|
|
let sent = alice.send_msg(chat.id, &mut msg).await;
|
|
let alice_msg = alice.get_last_msg().await;
|
|
assert_eq!(alice_msg.get_width() as u32, compressed_width);
|
|
assert_eq!(alice_msg.get_height() as u32, compressed_height);
|
|
let file_saved = alice
|
|
.get_blobdir()
|
|
.join("saved-".to_string() + &alice_msg.get_filename().unwrap());
|
|
alice_msg.save_file(&alice, &file_saved).await?;
|
|
check_image_size(file_saved, compressed_width, compressed_height);
|
|
|
|
if original_width == compressed_width {
|
|
assert_extension(&alice, alice_msg, extension);
|
|
} else {
|
|
assert_extension(&alice, alice_msg, "jpg");
|
|
}
|
|
|
|
let bob_msg = bob.recv_msg(&sent).await;
|
|
assert_eq!(bob_msg.get_viewtype(), res_viewtype);
|
|
assert_eq!(bob_msg.get_width() as u32, compressed_width);
|
|
assert_eq!(bob_msg.get_height() as u32, compressed_height);
|
|
let file_saved = bob
|
|
.get_blobdir()
|
|
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
|
bob_msg.save_file(&bob, &file_saved).await?;
|
|
if viewtype == Viewtype::File {
|
|
assert_eq!(file_saved.extension().unwrap(), extension);
|
|
let bytes1 = fs::read(&file_saved).await?;
|
|
assert_eq!(&bytes1, bytes);
|
|
}
|
|
|
|
let (_, exif) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
|
assert!(res_viewtype != Viewtype::Image || exif.is_none());
|
|
|
|
let img = check_image_size(file_saved, compressed_width, compressed_height);
|
|
|
|
if original_width == compressed_width {
|
|
assert_extension(&bob, bob_msg, extension);
|
|
} else {
|
|
assert_extension(&bob, bob_msg, "jpg");
|
|
}
|
|
|
|
Ok(img)
|
|
}
|
|
}
|
|
|
|
fn assert_extension(context: &TestContext, msg: Message, extension: &str) {
|
|
assert!(
|
|
msg.param
|
|
.get(Param::File)
|
|
.unwrap()
|
|
.ends_with(&format!(".{extension}"))
|
|
);
|
|
assert!(
|
|
msg.param
|
|
.get(Param::Filename)
|
|
.unwrap()
|
|
.ends_with(&format!(".{extension}"))
|
|
);
|
|
assert!(
|
|
msg.get_filename()
|
|
.unwrap()
|
|
.ends_with(&format!(".{extension}"))
|
|
);
|
|
assert_eq!(
|
|
msg.get_file(context)
|
|
.unwrap()
|
|
.extension()
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap(),
|
|
extension
|
|
);
|
|
assert_eq!(
|
|
msg.param
|
|
.get_file_blob(context)
|
|
.unwrap()
|
|
.unwrap()
|
|
.suffix()
|
|
.unwrap(),
|
|
extension
|
|
);
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_send_big_gif_as_image() -> Result<()> {
|
|
let bytes = include_bytes!("../../test-data/image/screenshot.gif");
|
|
let (width, height) = (1920u32, 1080u32);
|
|
let alice = TestContext::new_alice().await;
|
|
let bob = TestContext::new_bob().await;
|
|
alice
|
|
.set_config(
|
|
Config::MediaQuality,
|
|
Some(&(MediaQuality::Worse as i32).to_string()),
|
|
)
|
|
.await?;
|
|
let file = alice.get_blobdir().join("file").with_extension("gif");
|
|
fs::write(&file, &bytes)
|
|
.await
|
|
.context("failed to write file")?;
|
|
let mut msg = Message::new(Viewtype::Image);
|
|
msg.set_file_and_deduplicate(&alice, &file, Some("file.gif"), None)?;
|
|
let chat = alice.create_chat(&bob).await;
|
|
let sent = alice.send_msg(chat.id, &mut msg).await;
|
|
let bob_msg = bob.recv_msg(&sent).await;
|
|
// DC must detect the image as GIF and send it w/o reencoding.
|
|
assert_eq!(bob_msg.get_viewtype(), Viewtype::Gif);
|
|
assert_eq!(bob_msg.get_width() as u32, width);
|
|
assert_eq!(bob_msg.get_height() as u32, height);
|
|
let file_saved = bob
|
|
.get_blobdir()
|
|
.join("saved-".to_string() + &bob_msg.get_filename().unwrap());
|
|
bob_msg.save_file(&bob, &file_saved).await?;
|
|
let (file_size, _) = image_metadata(&std::fs::File::open(&file_saved)?)?;
|
|
assert_eq!(file_size, bytes.len() as u64);
|
|
check_image_size(file_saved, width, height);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_send_gif_as_sticker() -> Result<()> {
|
|
let bytes = include_bytes!("../../test-data/image/image100x50.gif");
|
|
let alice = &TestContext::new_alice().await;
|
|
let file = alice.get_blobdir().join("file").with_extension("gif");
|
|
fs::write(&file, &bytes)
|
|
.await
|
|
.context("failed to write file")?;
|
|
let mut msg = Message::new(Viewtype::Sticker);
|
|
msg.set_file_and_deduplicate(alice, &file, None, None)?;
|
|
let chat = alice.get_self_chat().await;
|
|
let sent = alice.send_msg(chat.id, &mut msg).await;
|
|
let msg = Message::load_from_db(alice, sent.sender_msg_id).await?;
|
|
assert_eq!(msg.get_viewtype(), Viewtype::Sticker);
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_create_and_deduplicate() -> Result<()> {
|
|
let t = TestContext::new().await;
|
|
|
|
let path = t.get_blobdir().join("anyfile.dat");
|
|
fs::write(&path, b"bla").await?;
|
|
let blob = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
|
assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f.dat");
|
|
assert_eq!(path.exists(), false);
|
|
|
|
assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
|
|
|
|
fs::write(&path, b"bla").await?;
|
|
let blob2 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
|
assert_eq!(blob2.name, blob.name);
|
|
|
|
let path_outside_blobdir = t.dir.path().join("anyfile.dat");
|
|
fs::write(&path_outside_blobdir, b"bla").await?;
|
|
let blob3 =
|
|
BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
|
|
assert!(path_outside_blobdir.exists());
|
|
assert_eq!(blob3.name, blob.name);
|
|
|
|
fs::write(&path, b"blabla").await?;
|
|
let blob4 = BlobObject::create_and_deduplicate(&t, &path, &path)?;
|
|
assert_ne!(blob4.name, blob.name);
|
|
|
|
fs::remove_dir_all(t.get_blobdir()).await?;
|
|
let blob5 =
|
|
BlobObject::create_and_deduplicate(&t, &path_outside_blobdir, &path_outside_blobdir)?;
|
|
assert_eq!(blob5.name, blob.name);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_create_and_deduplicate_from_bytes() -> Result<()> {
|
|
let t = TestContext::new().await;
|
|
|
|
fs::remove_dir(t.get_blobdir()).await?;
|
|
let blob = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
|
|
assert_eq!(blob.name, "$BLOBDIR/ce940175885d7b78f7b7e9f1396611f");
|
|
|
|
assert_eq!(fs::read(&blob.to_abs_path()).await?, b"bla");
|
|
let modified1 = blob.to_abs_path().metadata()?.modified()?;
|
|
|
|
// Test that the modification time of the file is updated when a new file is created
|
|
// so that it's not deleted during housekeeping.
|
|
// We can't use SystemTime::shift() here because file creation uses the actual OS time,
|
|
// which we can't mock from our code.
|
|
tokio::time::sleep(Duration::from_millis(1100)).await;
|
|
|
|
let blob2 = BlobObject::create_and_deduplicate_from_bytes(&t, b"bla", "file")?;
|
|
assert_eq!(blob2.name, blob.name);
|
|
|
|
let modified2 = blob.to_abs_path().metadata()?.modified()?;
|
|
assert_ne!(modified1, modified2);
|
|
sql::housekeeping(&t).await?;
|
|
assert!(blob2.to_abs_path().exists());
|
|
|
|
// If we do shift the time by more than 1h, the blob file will be deleted during housekeeping:
|
|
SystemTime::shift(Duration::from_secs(65 * 60));
|
|
sql::housekeeping(&t).await?;
|
|
assert_eq!(blob2.to_abs_path().exists(), false);
|
|
|
|
let blob3 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
|
|
assert_ne!(blob3.name, blob.name);
|
|
|
|
{
|
|
// If something goes wrong and the blob file is overwritten,
|
|
// the correct content should be restored:
|
|
fs::write(blob3.to_abs_path(), b"bloblo").await?;
|
|
|
|
let blob4 = BlobObject::create_and_deduplicate_from_bytes(&t, b"blabla", "file")?;
|
|
let blob4_content = fs::read(blob4.to_abs_path()).await?;
|
|
assert_eq!(blob4_content, b"blabla");
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that an image that already fits into the width limit,
|
|
/// but not the bytes limit,
|
|
/// is compressed without changing the resolution.
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
|
async fn test_recode_without_downscaling() -> Result<()> {
|
|
let t = &TestContext::new().await;
|
|
|
|
let image = include_bytes!("../../test-data/image/screenshot120x120.jpg");
|
|
const { assert!(120 < constants::WORSE_AVATAR_SIZE) };
|
|
|
|
for is_avatar in [true, false] {
|
|
let mut blob =
|
|
BlobObject::create_and_deduplicate_from_bytes(t, image, "image.jpg").unwrap();
|
|
let image_path = blob.to_abs_path();
|
|
check_image_size(&image_path, 120, 120);
|
|
|
|
assert!(
|
|
fs::metadata(&image_path).await.unwrap().len() > constants::WORSE_AVATAR_BYTES as u64
|
|
);
|
|
|
|
// Repeat the check, because a second call to `check_or_recode_to_size()`
|
|
// is not supposed to change anything:
|
|
let mut imgs = vec![];
|
|
for _ in 0..2 {
|
|
let mut viewtype = Viewtype::Image;
|
|
let new_name = blob.check_or_recode_to_size(
|
|
t,
|
|
Some("image.jpg".to_string()),
|
|
&mut viewtype,
|
|
constants::WORSE_AVATAR_SIZE,
|
|
constants::WORSE_AVATAR_BYTES,
|
|
is_avatar,
|
|
)?;
|
|
let image_path = blob.to_abs_path();
|
|
assert_eq!(new_name, "image.jpg"); // The name shall not have changed
|
|
assert_eq!(viewtype, Viewtype::Image); // The viewtype shall not have changed
|
|
let img = check_image_size(&image_path, 120, 120); // The resolution shall not have changed
|
|
imgs.push(img);
|
|
|
|
let new_image_bytes = fs::metadata(&image_path).await.unwrap().len();
|
|
assert!(
|
|
new_image_bytes < constants::WORSE_AVATAR_BYTES as u64,
|
|
"The new image size, {new_image_bytes}, should be lower than {}, is_avatar={is_avatar}",
|
|
constants::WORSE_AVATAR_BYTES
|
|
);
|
|
}
|
|
|
|
assert_eq!(imgs[0], imgs[1]);
|
|
}
|
|
|
|
Ok(())
|
|
}
|