Compare commits

..

14 Commits

Author SHA1 Message Date
iequidoo
12fdfc96a3 fix: Don't resend webxdc status updates in OutBroadcast-s (#7679)
We don't remember which status updates are "initial" (sent initially by the broadcast owner) and
which were sent by subscribers, so don't resend any of them to avoid accidental sharing of
confidential data.
2026-02-23 12:14:32 -03:00
iequidoo
de0b23d063 feat: Send webxdc status updates in InBroadcast-s (#7679)
They are already applied by the broadcast owner and other subscriber's devices, no changes are
needed on the receiver side.
2026-02-22 16:18:33 -03:00
iequidoo
b0ffe81415 feat: Don't send Chat-Group-Name* headers for InBroadcast-s
Broadcast subscribers can't change the chat name, so sending the "Chat-Group-Name{,-Timestamp}"
headers looks unnecessary. That could be useful for other subscriber's devices, but having only the
chat name isn't enough anyway, at least knowing the secret is necessary which is sent by the
broadcast owner.
2026-02-22 16:18:33 -03:00
B. Petersen
58d457140e fix: add cffi type for "Description changed" info message 2026-02-21 23:11:30 +01:00
biörn
b531a3c012 fix: chat-description-changed text in old clients (#7870)
instead of Alice saying to Bob "You changed the chat description",
we now say "[Chat description changed, please update ...]

i was also considering to say "[Chat description changed to:\n\n...]"
but then there is no incentive for ppl to update, and chat descriptions
for chat creation would still be missing. and this is probably far more
often used.

successor of https://github.com/chatmail/core/pull/7829
2026-02-21 21:07:41 +00:00
link2xt
f055f6226c feat: add context to message loading failures 2026-02-21 11:48:38 +00:00
link2xt
e95dca87bd feat: add backup versions to the importing error message
This would have helped debugging the problem reported at
<https://support.delta.chat/t/backup-too-new-please-update-delta-chat-message/4761>
2026-02-19 15:28:41 +00:00
B. Petersen
0d9442458a fix: add missing group description strings to cffi 2026-02-18 20:28:47 +01:00
link2xt
60cf483270 refactor(http): saturating addition to calculate cache expiration timestamp 2026-02-17 16:01:16 +00:00
link2xt
598d759b8d refactor(imex): check for overflow when adding blob size
Cannot happen without custom filesystem or sparse files,
but removes clippy lint.
2026-02-17 16:01:16 +00:00
link2xt
10b93b3943 refactor: enable clippy::arithmetic_side_effects lint 2026-02-17 16:01:16 +00:00
link2xt
5a06d08613 fix(imex): do not call set_config before running SQL migrations (#7851)
`set_config` expects that migrations have already been run and fails
if backup is old and e.g. does not have `transports` table.
2026-02-17 15:39:42 +00:00
link2xt
85de4bf678 build(git-cliff): do not capitalize the first letter of commit message
Some commit messages start with the function names
for additional context and these should not be capitalized.
2026-02-17 15:21:08 +00:00
link2xt
624fc394d9 feat: improve logging of connection failures
Previously it was not always clear whether IMAP or SMTP connection
failed and what was the endpoint used.
2026-02-17 15:14:36 +00:00
51 changed files with 268 additions and 52 deletions

View File

@@ -403,7 +403,7 @@ that failed to be published for 2.31.0 due to not configured "trusted publishers
### Features / Changes
- Lookup_or_create_adhoc_group(): Add context to SQL errors ([#7554](https://github.com/chatmail/core/pull/7554)).
- `lookup_or_create_adhoc_group()`: Add context to SQL errors ([#7554](https://github.com/chatmail/core/pull/7554)).
## [2.31.0] - 2025-12-04

View File

@@ -66,7 +66,7 @@ body = """
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}\
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
{{ commit.message | upper_first }}.\
{{ commit.message }}.\
{% if commit.footers is defined %}\
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
{% raw %} {% endraw %}- {{ footer.value }}\

View File

@@ -4612,6 +4612,7 @@ int dc_msg_is_info (const dc_msg_t* msg);
* and also offer a way to fix the encryption, eg. by a button offering a QR scan
* - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info`
* - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted"
* - DC_INFO_GROUP_NAME_CHANGED (70) - Info-message "Description changed", UI should open the profile with the description
*
* For the messages that refer to a CONTACT,
* dc_msg_get_info_contact_id() returns the contact ID.
@@ -4667,6 +4668,7 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg);
#define DC_INFO_INVALID_UNENCRYPTED_MAIL 13
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
#define DC_INFO_CHAT_E2EE 50
#define DC_INFO_GROUP_DESCRIPTION_CHANGED 70
/**
@@ -7609,6 +7611,12 @@ void dc_event_unref(dc_event_t* event);
/// "Incoming video call"
#define DC_STR_INCOMING_VIDEO_CALL 235
/// "You changed the chat description."
#define DC_STR_GROUP_DESCRIPTION_CHANGED_BY_YOU 240
/// "Chat description changed by %1$s."
#define DC_STR_GROUP_DESCRIPTION_CHANGED_BY_OTHER 241
/**
* @}
*/

View File

@@ -586,6 +586,7 @@ impl Config {
}
#[cfg(not(target_os = "ios"))]
#[expect(clippy::arithmetic_side_effects)]
async fn create_lock_task(dir: PathBuf) -> Result<Option<JoinHandle<anyhow::Result<()>>>> {
let lockfile = dir.join(LOCKFILE_NAME);
let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?);
@@ -752,6 +753,7 @@ impl Config {
}
/// Creates a new account in the account manager directory.
#[expect(clippy::arithmetic_side_effects)]
async fn new_account(&mut self) -> Result<AccountConfig> {
let id = {
let id = self.inner.next_id;
@@ -841,6 +843,7 @@ impl Config {
///
/// Without this workaround removing account may fail on Windows with an error
/// "The process cannot access the file because it is being used by another process. (os error 32)".
#[expect(clippy::arithmetic_side_effects)]
async fn try_many_times<F, Fut, T>(f: F) -> std::result::Result<(), T>
where
F: Fn() -> Fut,

View File

@@ -73,6 +73,7 @@ impl fmt::Display for Aheader {
let keydata = self.public_key.to_base64().chars().enumerate().fold(
String::new(),
|mut res, (i, c)| {
#[expect(clippy::arithmetic_side_effects)]
if i % 78 == 78 - "keydata=".len() {
res.push(' ')
}

View File

@@ -321,6 +321,7 @@ impl<'a> BlobObject<'a> {
/// then the updated user-visible filename will be returned;
/// this may be necessary because the format may be changed to JPG,
/// i.e. "image.png" -> "image.jpg".
#[expect(clippy::arithmetic_side_effects)]
fn check_or_recode_to_size(
&mut self,
context: &Context,

View File

@@ -79,6 +79,7 @@ impl CallInfo {
}
fn remaining_ring_seconds(&self) -> i64 {
#[expect(clippy::arithmetic_side_effects)]
let remaining_seconds = self.msg.timestamp_sent + RINGING_SECONDS - time();
remaining_seconds.clamp(0, RINGING_SECONDS)
}
@@ -175,6 +176,7 @@ impl CallInfo {
}
/// Returns call duration in seconds.
#[expect(clippy::arithmetic_side_effects)]
pub fn duration_seconds(&self) -> i64 {
if let (Some(start), Some(end)) = (
self.msg.param.get_i64(CALL_ACCEPTED_TIMESTAMP),

View File

@@ -941,6 +941,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
/// Jaccard similarity coefficient is used to estimate similarity of chat member sets.
///
/// Chat is considered active if something was posted there within the last 42 days.
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_similar_chat_ids(self, context: &Context) -> Result<Vec<(ChatId, f64)>> {
// Count number of common members in this and other chats.
let intersection = context
@@ -1145,6 +1146,7 @@ SELECT id, rfc724_mid, pre_rfc724_mid, timestamp, ?, 1 FROM msgs WHERE chat_id=?
/// prefer plaintext emails.
///
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
let chat = Chat::load_from_db(context, self).await?;
if !chat.is_encrypted(context).await? {
@@ -1730,6 +1732,7 @@ impl Chat {
///
/// If `update_msg_id` is set, that record is reused;
/// if `update_msg_id` is None, a new record is created.
#[expect(clippy::arithmetic_side_effects)]
async fn prepare_msg_raw(
&mut self,
context: &Context,
@@ -2694,7 +2697,9 @@ async fn prepare_send_msg(
.unwrap_or_default(),
_ => false,
};
if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? {
if msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate {
// Already checked in `send_webxdc_status_update_struct()`.
} else if let Some(reason) = chat.why_cant_send_ex(context, &skip_fn).await? {
bail!("Cannot send to {chat_id}: {reason}");
}
@@ -2995,6 +3000,7 @@ pub async fn send_text_msg(
}
/// Sends chat members a request to edit the given message's text.
#[expect(clippy::arithmetic_side_effects)]
pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: String) -> Result<()> {
let mut original_msg = Message::load_from_db(context, msg_id).await?;
ensure!(
@@ -3100,6 +3106,7 @@ pub async fn get_chat_msgs(context: &Context, chat_id: ChatId) -> Result<Vec<Cha
}
/// Returns messages belonging to the chat according to the given options.
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_chat_msgs_ex(
context: &Context,
chat_id: ChatId,
@@ -3978,6 +3985,7 @@ pub(crate) async fn add_contact_to_chat_ex(
/// This function does not check if the avatar is set.
/// If avatar is not set and this function returns `true`,
/// a `Chat-User-Avatar: 0` header should be sent to reset the avatar.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId) -> Result<bool> {
let timestamp_some_days_ago = time() - DC_RESEND_USER_AVATAR_DAYS * 24 * 60 * 60;
let needs_attach = context
@@ -4258,7 +4266,9 @@ async fn set_chat_description_ex(
if chat.is_promoted() {
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_str::msg_chat_description_changed(context, ContactId::SELF).await;
msg.text =
"[Chat description changed. To see this and other new features, please update the app]"
.to_string();
msg.param.set_cmd(SystemMessage::GroupDescriptionChanged);
msg.id = send_msg(context, chat_id, &mut msg).await?;
@@ -4433,6 +4443,7 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
}
/// Forwards multiple messages to a chat in another context.
#[expect(clippy::arithmetic_side_effects)]
pub async fn forward_msgs_2ctx(
ctx_src: &Context,
msg_ids: &[MsgId],
@@ -4563,6 +4574,7 @@ pub async fn save_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
/// the copy contains a reference to the original message
/// as well as to the original chat in case the original message gets deleted.
/// Returns data needed to add a `SaveMessage` sync item.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn save_copy_in_self_talk(
context: &Context,
src_msg_id: MsgId,
@@ -4670,7 +4682,7 @@ pub async fn resend_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
// note(treefit): only matters if it is the last message in chat (but probably to expensive to check, debounce also solves it)
chatlist_events::emit_chatlist_item_changed(context, msg.chat_id);
if msg.viewtype == Viewtype::Webxdc {
if msg.viewtype == Viewtype::Webxdc && msg.chat_typ != Chattype::OutBroadcast {
let conn_fn = |conn: &mut rusqlite::Connection| {
let range = conn.query_row(
"SELECT IFNULL(min(id), 1), IFNULL(max(id), 0) \
@@ -4741,6 +4753,7 @@ pub(crate) async fn get_chat_id_by_grpid(
///
/// Optional `label` can be provided to ensure that message is added only once.
/// If `important` is true, a notification will be sent.
#[expect(clippy::arithmetic_side_effects)]
pub async fn add_device_msg_with_importance(
context: &Context,
label: Option<&str>,

View File

@@ -3231,7 +3231,7 @@ async fn test_chat_description(initial_description: &str, join_via_qr: bool) ->
let sent = alice.pop_sent_msg().await;
assert_eq!(
sent.load_from_db().await.text,
"You changed the chat description."
"[Chat description changed. To see this and other new features, please update the app]"
);
tcm.section("Bob receives the description change");

View File

@@ -7,6 +7,7 @@ use colorutils_rs::{Oklch, Rgb, TransferFunction};
use sha1::{Digest, Sha1};
/// Converts an identifier to Hue angle.
#[expect(clippy::arithmetic_side_effects)]
fn str_to_angle(s: &str) -> f32 {
let bytes = s.as_bytes();
let result = Sha1::digest(bytes);
@@ -19,6 +20,7 @@ fn str_to_angle(s: &str) -> f32 {
///
/// Returns a 24-bit number with 8 least significant bits corresponding to the blue color and 8
/// most significant bits corresponding to the red color.
#[expect(clippy::arithmetic_side_effects)]
fn rgb_to_u32(rgb: Rgb<u8>) -> u32 {
65536 * u32::from(rgb.r) + 256 * u32::from(rgb.g) + u32::from(rgb.b)
}

View File

@@ -107,12 +107,14 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9);
IntoStaticStr,
Serialize,
Deserialize,
Default,
)]
#[repr(u32)]
pub enum Chattype {
/// A 1:1 chat, i.e. a normal chat with a single contact.
///
/// Created by [`ChatId::create_for_contact`].
#[default]
Single = 100,
/// Group chat.

View File

@@ -673,6 +673,7 @@ impl Contact {
}
/// Returns `true` if this contact was seen recently.
#[expect(clippy::arithmetic_side_effects)]
pub fn was_seen_recently(&self) -> bool {
time() - self.last_seen <= SEEN_RECENTLY_SECONDS
}
@@ -1071,6 +1072,7 @@ VALUES (?, ?, ?, ?, ?, ?)
/// The `addr_book` is a multiline string in the format `Name one\nAddress one\nName two\nAddress two`.
///
/// Returns the number of modified contacts.
#[expect(clippy::arithmetic_side_effects)]
pub async fn add_address_book(context: &Context, addr_book: &str) -> Result<usize> {
let mut modify_cnt = 0;
@@ -1909,6 +1911,7 @@ pub(crate) async fn set_status(
}
/// Updates last seen timestamp of the contact if it is earlier than the given `timestamp`.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn update_last_seen(
context: &Context,
contact_id: ContactId,
@@ -2000,6 +2003,7 @@ pub(crate) async fn mark_contact_id_as_verified(
Ok(())
}
#[expect(clippy::arithmetic_side_effects)]
fn cat_fingerprint(ret: &mut String, name: &str, addr: &str, fingerprint: &str) {
*ret += &format!("\n\n{name} ({addr}):\n{fingerprint}");
}
@@ -2041,6 +2045,7 @@ impl RecentlySeenLoop {
}
}
#[expect(clippy::arithmetic_side_effects)]
async fn run(context: Context, interrupt: Receiver<RecentlySeenInterrupt>) {
type MyHeapElem = (Reverse<i64>, ContactId);

View File

@@ -342,6 +342,7 @@ enum RunningState {
/// actual keys and their values which will be present are not
/// guaranteed. Calling [Context::get_info] also includes information
/// about the context on top of the information here.
#[expect(clippy::arithmetic_side_effects)]
pub fn get_info() -> BTreeMap<&'static str, String> {
let mut res = BTreeMap::new();

View File

@@ -235,6 +235,7 @@ fn str_cb(event_str: &str, dehtml: &mut Dehtml) {
}
}
#[expect(clippy::arithmetic_side_effects)]
fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
let tag = String::from_utf8_lossy(event.name().as_ref())
.trim()
@@ -280,6 +281,7 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
}
}
#[expect(clippy::arithmetic_side_effects)]
fn dehtml_starttag_cb<B: std::io::BufRead>(
event: &BytesStart,
dehtml: &mut Dehtml,
@@ -356,6 +358,7 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
/// In order to know when a specific tag is closed, we need to count the opening and closing tags.
/// The `counts`s are stored in the `Dehtml` struct.
#[expect(clippy::arithmetic_side_effects)]
fn pop_tag(count: &mut u32) {
if *count > 0 {
*count -= 1;
@@ -364,6 +367,7 @@ fn pop_tag(count: &mut u32) {
/// In order to know when a specific tag is closed, we need to count the opening and closing tags.
/// The `counts`s are stored in the `Dehtml` struct.
#[expect(clippy::arithmetic_side_effects)]
fn maybe_push_tag(
event: &BytesStart,
reader: &Reader<impl BufRead>,

View File

@@ -593,6 +593,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option<i64> {
.min()
}
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiver<()>) {
loop {
let ephemeral_timestamp = next_expiration_timestamp(context).await;
@@ -650,6 +651,7 @@ pub(crate) async fn ephemeral_loop(context: &Context, interrupt_receiver: Receiv
}
/// Schedules expired IMAP messages for deletion.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn delete_expired_imap_messages(context: &Context) -> Result<()> {
let now = time();

View File

@@ -86,6 +86,7 @@ impl HtmlMsgParser {
/// Function takes a raw mime-message string,
/// searches for the main-text part
/// and returns that as parser.html
#[expect(clippy::arithmetic_side_effects)]
pub async fn from_bytes<'a>(
context: &Context,
rawmime: &'a [u8],
@@ -119,6 +120,7 @@ impl HtmlMsgParser {
/// Usually, there is at most one plain-text and one HTML-text part,
/// multiple plain-text parts might be used for mailinglist-footers,
/// therefore we use the first one.
#[expect(clippy::arithmetic_side_effects)]
async fn collect_texts_recursive<'a>(
&'a mut self,
context: &'a Context,

View File

@@ -208,6 +208,7 @@ impl<T: Iterator<Item = (i64, u32, String)>> Iterator for UidGrouper<T> {
// Tuple of folder, row IDs, and UID range as a string.
type Item = (String, Vec<i64>, String);
#[expect(clippy::arithmetic_side_effects)]
fn next(&mut self) -> Option<Self::Item> {
let (_, _, folder) = self.inner.peek().cloned()?;
@@ -356,10 +357,10 @@ impl Imap {
context,
self.proxy_config.clone(),
self.strict_tls,
connection_candidate,
&connection_candidate,
)
.await
.context("IMAP failed to connect")
.with_context(|| format!("IMAP failed to connect to {connection_candidate}"))
{
Ok(client) => client,
Err(err) => {
@@ -543,6 +544,7 @@ impl Imap {
/// Fetches new messages.
///
/// Returns true if at least one message was fetched.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn fetch_new_messages(
&mut self,
context: &Context,
@@ -583,6 +585,7 @@ impl Imap {
}
/// Returns number of messages processed and whether the function should be called again.
#[expect(clippy::arithmetic_side_effects)]
async fn fetch_new_msg_batch(
&mut self,
context: &Context,
@@ -1265,6 +1268,7 @@ impl Session {
///
/// If the message is incorrect or there is a failure to write a message to the database,
/// it is skipped and the error is logged.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn fetch_many_msgs(
&mut self,
context: &Context,
@@ -1429,6 +1433,7 @@ impl Session {
/// We get [`/shared/comment`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.1)
/// and [`/shared/admin`](https://www.rfc-editor.org/rfc/rfc5464#section-6.2.2)
/// metadata.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn update_metadata(&mut self, context: &Context) -> Result<()> {
let mut lock = context.metadata.write().await;
@@ -2364,6 +2369,7 @@ async fn should_ignore_folder(
/// Builds a list of sequence/uid sets. The returned sets have each no more than around 1000
/// characters because according to <https://tools.ietf.org/html/rfc2683#section-3.2.1.5>
/// command lines should not be much more than 1000 chars (servers should allow at least 8000 chars)
#[expect(clippy::arithmetic_side_effects)]
fn build_sequence_sets(uids: &[u32]) -> Result<Vec<(Vec<u32>, String)>> {
// first, try to find consecutive ranges:
let mut ranges: Vec<UidRange> = vec![];

View File

@@ -150,7 +150,7 @@ impl Client {
Err(err) => {
warn!(
context,
"Failed to connect to {host} ({resolved_addr}): {err:#}."
"IMAP failed to connect to {host} ({resolved_addr}): {err:#}."
);
Err(err)
}
@@ -161,7 +161,7 @@ impl Client {
context: &Context,
proxy_config: Option<ProxyConfig>,
strict_tls: bool,
candidate: ConnectionCandidate,
candidate: &ConnectionCandidate,
) -> Result<Self> {
let host = &candidate.host;
let port = candidate.port;

View File

@@ -127,6 +127,7 @@ impl Session {
/// Prefetch `n_uids` messages starting from `uid_next`. Returns a list of fetch results in the
/// order of ascending delivery time to the server (INTERNALDATE).
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn prefetch(
&mut self,
uid_next: u32,

View File

@@ -293,6 +293,7 @@ impl<R> AsyncRead for ProgressReader<R>
where
R: AsyncRead,
{
#[expect(clippy::arithmetic_side_effects)]
fn poll_read(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
@@ -379,17 +380,6 @@ async fn import_backup_stream_inner<R: tokio::io::AsyncRead + Unpin>(
if res.is_ok() {
res = check_backup_version(context).await;
}
if res.is_ok() {
// All recent backups have `bcc_self` set to "1" before export.
//
// Setting `bcc_self` to "1" on export was introduced on 2024-12-17
// in commit 21664125d798021be75f47d5b0d5006d338b4531
//
// We additionally try to set `bcc_self` to "1" after import here
// for compatibility with older backups,
// but eventually this code can be removed.
res = context.set_config(Config::BccSelf, Some("1")).await;
}
fs::remove_file(unpacked_database)
.await
.context("cannot remove unpacked database")
@@ -449,6 +439,7 @@ fn get_next_backup_path(
/// Exports the database to a separate file with the given passphrase.
///
/// Set passphrase to empty string to export the database unencrypted.
#[expect(clippy::arithmetic_side_effects)]
async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Result<()> {
// get a fine backup file name (the name includes the date so that multiple backup instances are possible)
let now = time();
@@ -522,6 +513,7 @@ impl<W> AsyncWrite for ProgressWriter<W>
where
W: AsyncWrite,
{
#[expect(clippy::arithmetic_side_effects)]
fn poll_write(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
@@ -601,6 +593,7 @@ async fn import_secret_key(context: &Context, path: &Path) -> Result<()> {
/// containing secret keys are imported and the last successfully
/// imported which does not contain "legacy" in its filename
/// is set as the default.
#[expect(clippy::arithmetic_side_effects)]
async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
let attr = tokio::fs::metadata(path).await?;
@@ -654,6 +647,7 @@ async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
Ok(())
}
#[expect(clippy::arithmetic_side_effects)]
async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
let mut export_errors = 0;
@@ -800,7 +794,7 @@ async fn check_backup_version(context: &Context) -> Result<()> {
let version = (context.sql.get_raw_config_int("backup_version").await?).unwrap_or(2);
ensure!(
version <= DCBACKUP_VERSION,
"Backup too new, please update Delta Chat"
"This profile is from a newer version of Delta Chat. Please update Delta Chat and try again (profile version is v{version}, the latest supported is v{DCBACKUP_VERSION})"
);
Ok(())
}

View File

@@ -129,6 +129,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result<St
}
/// Creates a new setup code for Autocrypt Setup Message.
#[expect(clippy::arithmetic_side_effects)]
fn create_setup_code(_context: &Context) -> String {
let mut random_val: u16;
let mut ret = String::new();

View File

@@ -189,10 +189,11 @@ impl BackupProvider {
let blobdir = BlobDirContents::new(&context).await?;
let mut file_size = 0;
file_size += dbfile.metadata()?.len();
let mut file_size = dbfile.metadata()?.len();
for blob in blobdir.iter() {
file_size += blob.to_abs_path().metadata()?.len()
file_size = file_size
.checked_add(blob.to_abs_path().metadata()?.len())
.context("File size overflow")?;
}
send_stream.write_all(&file_size.to_be_bytes()).await?;

View File

@@ -17,6 +17,7 @@
clippy::cloned_instead_of_copied,
clippy::manual_is_variant_and
)]
#![cfg_attr(not(test), warn(clippy::arithmetic_side_effects))]
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
#![cfg_attr(not(test), forbid(clippy::string_slice))]
#![allow(

View File

@@ -263,6 +263,7 @@ impl Kml {
}
/// Enables location streaming in chat identified by `chat_id` for `seconds` seconds.
#[expect(clippy::arithmetic_side_effects)]
pub async fn send_locations_to_chat(
context: &Context,
chat_id: ChatId,
@@ -385,6 +386,7 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
}
/// Searches for locations in the given time range, optionally filtering by chat and contact IDs.
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_range(
context: &Context,
chat_id: Option<ChatId>,
@@ -517,6 +519,7 @@ pub(crate) async fn delete_orphaned_poi_locations(context: &Context) -> Result<(
}
/// Returns `location.kml` contents.
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(String, u32)>> {
let mut last_added_location_id = 0;
@@ -752,6 +755,7 @@ pub(crate) async fn location_loop(context: &Context, interrupt_receiver: Receive
/// Returns number of seconds until the next time location streaming for some chat ends
/// automatically.
#[expect(clippy::arithmetic_side_effects)]
async fn maybe_send_locations(context: &Context) -> Result<Option<u64>> {
let mut next_event: Option<u64> = None;

View File

@@ -76,6 +76,7 @@ impl<S: SessionStream> LoggingStream<S> {
}
impl<S: SessionStream> AsyncRead for LoggingStream<S> {
#[expect(clippy::arithmetic_side_effects)]
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,

View File

@@ -201,6 +201,7 @@ SELECT ?1, rfc724_mid, pre_rfc724_mid, timestamp, ?, ? FROM msgs WHERE id=?1
}
/// Returns detailed message information in a multi-line text form.
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_info(self, context: &Context) -> Result<String> {
let msg = Message::load_from_db(context, self).await?;
@@ -452,6 +453,7 @@ pub struct Message {
pub(crate) is_dc_message: MessengerMessage,
pub(crate) original_msg_id: MsgId,
pub(crate) mime_modified: bool,
pub(crate) chat_typ: Chattype,
pub(crate) chat_visibility: ChatVisibility,
pub(crate) chat_blocked: Blocked,
pub(crate) location_id: u32,
@@ -526,6 +528,7 @@ impl Message {
m.param AS param,
m.hidden AS hidden,
m.location_id AS location,
c.type AS chat_typ,
c.archived AS visibility,
c.blocked AS blocked
FROM msgs m
@@ -585,6 +588,10 @@ impl Message {
param: row.get::<_, String>("param")?.parse().unwrap_or_default(),
hidden: row.get("hidden")?,
location_id: row.get("location")?,
// This is safe: see `ChatId::delete_ex()`, `None` chat type can't happen.
chat_typ: row
.get::<_, Option<_>>("chat_typ")?
.unwrap_or(Chattype::Single),
chat_visibility: row.get::<_, Option<_>>("visibility")?.unwrap_or_default(),
chat_blocked: row
.get::<_, Option<Blocked>>("blocked")?
@@ -822,6 +829,7 @@ impl Message {
///
/// Currently this includes `additional_text`, but this may change in future, when the UIs show
/// the necessary info themselves.
#[expect(clippy::arithmetic_side_effects)]
pub fn get_text(&self) -> String {
self.text.clone() + &self.additional_text
}
@@ -964,6 +972,7 @@ impl Message {
///
/// A message has a deviating timestamp when it is sent on
/// another day as received/sorted by.
#[expect(clippy::arithmetic_side_effects)]
pub fn has_deviating_timestamp(&self) -> bool {
let cnv_to_local = gm2local_offset();
let sort_timestamp = self.get_sort_timestamp() + cnv_to_local;
@@ -2119,6 +2128,7 @@ pub async fn get_request_msg_cnt(context: &Context) -> usize {
/// Returns the number of messages that are older than the given number of seconds.
/// This includes e-mails downloaded due to the `show_emails` option.
/// Messages in the "saved messages" folder are not counted as they will not be deleted automatically.
#[expect(clippy::arithmetic_side_effects)]
pub async fn estimate_deletion_cnt(
context: &Context,
from_server: bool,

View File

@@ -195,6 +195,7 @@ fn new_address_with_name(name: &str, address: String) -> Address<'static> {
}
impl MimeFactory {
#[expect(clippy::arithmetic_side_effects)]
pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
let now = time();
let chat = Chat::load_from_db(context, msg.chat_id).await?;
@@ -726,6 +727,7 @@ impl MimeFactory {
/// Consumes a `MimeFactory` and renders it into a message which is then stored in
/// `smtp`-table to be used by the SMTP loop
#[expect(clippy::arithmetic_side_effects)]
pub async fn render(mut self, context: &Context) -> Result<RenderedEmail> {
let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new();
@@ -1587,10 +1589,7 @@ impl MimeFactory {
}
}
if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
{
if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
headers.push((
"Chat-Group-Name",
mail_builder::headers::text::Text::new(chat.name.to_string()).into(),
@@ -1601,7 +1600,11 @@ impl MimeFactory {
mail_builder::headers::text::Text::new(ts.to_string()).into(),
));
}
}
if chat.typ == Chattype::Group
|| chat.typ == Chattype::OutBroadcast
|| chat.typ == Chattype::InBroadcast
{
match command {
SystemMessage::MemberRemovedFromGroup => {
let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default();
@@ -2034,14 +2037,15 @@ impl MimeFactory {
HeaderDef::IrohGossipTopic.get_headername(),
mail_builder::headers::raw::Raw::new(topic).into(),
));
if let (Some(json), _) = context
.render_webxdc_status_update_object(
msg.id,
StatusUpdateSerial::MIN,
StatusUpdateSerial::MAX,
None,
)
.await?
if msg.chat_typ != Chattype::OutBroadcast
&& let (Some(json), _) = context
.render_webxdc_status_update_object(
msg.id,
StatusUpdateSerial::MIN,
StatusUpdateSerial::MAX,
None,
)
.await?
{
parts.push(context.build_status_update_part(&json));
}
@@ -2069,6 +2073,7 @@ impl MimeFactory {
}
/// Render an MDN
#[expect(clippy::arithmetic_side_effects)]
fn render_mdn(&mut self) -> Result<MimePart<'static>> {
// RFC 6522, this also requires the `report-type` parameter which is equal
// to the MIME subtype of the second body part of the multipart/report

View File

@@ -266,6 +266,7 @@ impl MimeMessage {
///
/// This method has some side-effects,
/// such as saving blobs and saving found public keys to the database.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
let mail = mailparse::parse_mail(body)?;
@@ -728,6 +729,7 @@ impl MimeMessage {
Ok(parser)
}
#[expect(clippy::arithmetic_side_effects)]
fn get_timestamp_sent(
hdrs: &[mailparse::MailHeader<'_>],
default: i64,
@@ -1005,6 +1007,7 @@ impl MimeMessage {
Ok(())
}
#[expect(clippy::arithmetic_side_effects)]
fn avatar_action_from_header(
&mut self,
context: &Context,
@@ -1506,6 +1509,7 @@ impl MimeMessage {
}
#[expect(clippy::too_many_arguments)]
#[expect(clippy::arithmetic_side_effects)]
async fn do_add_single_file_part(
&mut self,
context: &Context,
@@ -2057,6 +2061,7 @@ impl MimeMessage {
/// Returns parsed `Chat-Group-Member-Timestamps` header contents.
///
/// Returns `None` if there is no such header.
#[expect(clippy::arithmetic_side_effects)]
pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
self.get_header(HeaderDef::ChatGroupMemberTimestamps)

View File

@@ -109,8 +109,8 @@ pub(crate) async fn connect_tcp_inner(
) -> Result<Pin<Box<TimeoutStream<TcpStream>>>> {
let tcp_stream = timeout(TIMEOUT, TcpStream::connect(addr))
.await
.context("connection timeout")?
.context("connection failure")?;
.context("Connection timeout")?
.context("Connection failure")?;
// Disable Nagle's algorithm.
tcp_stream.set_nodelay(true)?;

View File

@@ -118,7 +118,7 @@ where
fn http_url_cache_timestamps(url: &str, mimetype: Option<&str>) -> (i64, i64) {
let now = time();
let expires = now + 3600 * 24 * 35;
let expires = now.saturating_add(3600 * 24 * 35);
let stale = if url.ends_with(".xdc") {
// WebXDCs are never stale, they just expire.
expires
@@ -128,19 +128,19 @@ fn http_url_cache_timestamps(url: &str, mimetype: Option<&str>) -> (i64, i64) {
// Policy at <https://operations.osmfoundation.org/policies/tiles/>
// requires that we cache tiles for at least 7 days.
// Do not revalidate earlier than that.
now + 3600 * 24 * 7
now.saturating_add(3600 * 24 * 7)
} else if mimetype.is_some_and(|s| s.starts_with("image/")) {
// Cache images for 1 day.
//
// As of 2024-12-12 WebXDC icons at <https://webxdc.org/apps/>
// use the same path for all app versions,
// so may change, but it is not critical if outdated icon is displayed.
now + 3600 * 24
now.saturating_add(3600 * 24)
} else {
// Revalidate everything else after 1 hour.
//
// This includes HTML, CSS and JS.
now + 3600
now.saturating_add(3600)
};
(expires, stale)
}
@@ -173,6 +173,7 @@ async fn http_cache_put(context: &Context, url: &str, response: &Response) -> Re
/// Retrieves the binary from HTTP cache.
///
/// Also returns if the response is stale and should be revalidated in the background.
#[expect(clippy::arithmetic_side_effects)]
async fn http_cache_get(context: &Context, url: &str) -> Result<Option<(Response, bool)>> {
let now = time();
let Some((blob_name, mimetype, encoding, stale_timestamp)) = context

View File

@@ -174,6 +174,7 @@ pub enum ProxyConfig {
}
/// Constructs HTTP/1.1 `CONNECT` request for HTTP(S) proxy.
#[expect(clippy::arithmetic_side_effects)]
fn http_connect_request(host: &str, port: u16, auth: Option<(&str, &str)>) -> String {
// According to <https://datatracker.ietf.org/doc/html/rfc7230#section-5.4>
// clients MUST send `Host:` header in HTTP/1.1 requests,
@@ -322,6 +323,7 @@ impl ProxyConfig {
/// config into `proxy_url` if `proxy_url` is unset or empty.
///
/// Unsets `socks5_host`, `socks5_port`, `socks5_user` and `socks5_password` in any case.
#[expect(clippy::arithmetic_side_effects)]
async fn migrate_socks_config(sql: &Sql) -> Result<()> {
if sql.get_raw_config("proxy_url").await?.is_none() {
// Load legacy SOCKS5 settings.

View File

@@ -67,6 +67,7 @@ pub async fn get_oauth2_url(
}
}
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn get_oauth2_access_token(
context: &Context,
addr: &str,
@@ -256,6 +257,7 @@ pub(crate) async fn get_oauth2_addr(
}
impl Oauth2 {
#[expect(clippy::arithmetic_side_effects)]
fn from_address(addr: &str) -> Option<Self> {
let addr_normalized = normalize_addr(addr);
if let Some(domain) = addr_normalized

View File

@@ -533,6 +533,7 @@ pub(crate) fn iroh_topic_from_str(topic: &str) -> Result<TopicId> {
Ok(topic)
}
#[expect(clippy::arithmetic_side_effects)]
async fn subscribe_loop(
context: &Context,
mut stream: iroh_gossip::net::GossipReceiver,

View File

@@ -170,6 +170,7 @@ pub enum SeipdVersion {
/// Encrypts `plain` text using `public_keys_for_encryption`
/// and signs it using `private_key_for_signing`.
#[expect(clippy::arithmetic_side_effects)]
pub async fn pk_encrypt(
plain: Vec<u8>,
public_keys_for_encryption: Vec<SignedPublicKey>,

View File

@@ -24,6 +24,7 @@ pub struct PlainText {
impl PlainText {
/// Convert plain text to HTML.
/// The function handles quotes, links, fixed and floating text paragraphs.
#[expect(clippy::arithmetic_side_effects)]
pub fn to_html(&self) -> String {
static LINKIFY_MAIL_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r"\b([\w.\-+]+@[\w.\-]+)\b").unwrap());

View File

@@ -680,6 +680,7 @@ fn decode_account(qr: &str) -> Result<Qr> {
}
/// scheme: `https://t.me/socks?server=foo&port=123` or `https://t.me/socks?server=1.2.3.4&port=123`
#[expect(clippy::arithmetic_side_effects)]
fn decode_tg_socks_proxy(_context: &Context, qr: &str) -> Result<Qr> {
let url = url::Url::parse(qr).context("Invalid t.me/socks url")?;
@@ -1021,6 +1022,7 @@ async fn decode_smtp(context: &Context, qr: &str) -> Result<Qr> {
/// Scheme: `MATMSG:TO:addr...;SUB:subject...;BODY:body...;`
///
/// There may or may not be linebreaks after the fields.
#[expect(clippy::arithmetic_side_effects)]
async fn decode_matmsg(context: &Context, qr: &str) -> Result<Qr> {
// Does not work when the text `TO:` is used in subject/body _and_ TO: is not the first field.
// we ignore this case.

View File

@@ -15,6 +15,7 @@ use crate::securejoin;
use crate::stock_str::{self, backup_transfer_qr};
/// Create a QR code from any input data.
#[expect(clippy::arithmetic_side_effects)]
pub fn create_qr_svg(qrcode_content: &str) -> Result<String> {
let all_size = 512.0;
let qr_code_size = 416.0;
@@ -175,6 +176,7 @@ async fn self_info(context: &Context) -> Result<(Option<Vec<u8>>, String, String
Ok((avatar, displayname, addr, color))
}
#[expect(clippy::arithmetic_side_effects)]
fn inner_generate_secure_join_qr_code(
qrcode_description: &str,
qrcode_content: &str,

View File

@@ -122,6 +122,7 @@ impl Reactions {
}
/// Returns a map from emojis to their frequencies.
#[expect(clippy::arithmetic_side_effects)]
pub fn emoji_frequencies(&self) -> BTreeMap<String, usize> {
let mut emoji_frequencies: BTreeMap<String, usize> = BTreeMap::new();
for reaction in self.reactions.values() {

View File

@@ -887,7 +887,11 @@ UPDATE config SET value=? WHERE keyname='configured_addr' AND value!=?1
.is_some()
{
can_info_msg = false;
Some(Message::load_from_db(context, insert_msg_id).await?)
Some(
Message::load_from_db(context, insert_msg_id)
.await
.context("Failed to load just created webxdc instance")?,
)
} else if let Some(field) = mime_parser.get_header(HeaderDef::InReplyTo) {
if let Some(instance) =
message::get_by_rfc724_mids(context, &parse_message_ids(field)).await?
@@ -1185,6 +1189,7 @@ pub async fn from_field_to_contact_id(
}
}
#[expect(clippy::arithmetic_side_effects)]
async fn decide_chat_assignment(
context: &Context,
mime_parser: &MimeMessage,
@@ -2136,7 +2141,9 @@ async fn add_parts(
}
if let Some(replace_msg_id) = replace_msg_id {
let placeholder = Message::load_from_db(context, replace_msg_id).await?;
let placeholder = Message::load_from_db(context, replace_msg_id)
.await
.context("Failed to load placeholder message")?;
for key in [
Param::WebxdcSummary,
Param::WebxdcSummaryTimestamp,
@@ -2919,6 +2926,7 @@ async fn create_group(
}
}
#[expect(clippy::arithmetic_side_effects)]
async fn update_chats_contacts_timestamps(
context: &Context,
chat_id: ChatId,
@@ -3418,6 +3426,7 @@ async fn apply_chat_name_avatar_and_description_changes(
}
/// Returns a list of strings that should be shown as info messages, informing about group membership changes.
#[expect(clippy::arithmetic_side_effects)]
async fn group_changes_msgs(
context: &Context,
added_ids: &HashSet<ContactId>,

View File

@@ -303,6 +303,7 @@ impl Context {
///
/// This comes as an HTML from the core so that we can easily improve it
/// and the improvement instantly reaches all UIs.
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_connectivity_html(&self) -> Result<String> {
let mut ret = r#"<!DOCTYPE html>
<html>

View File

@@ -410,6 +410,7 @@ pub(crate) fn get_secure_join_step(mime_message: &MimeMessage) -> Option<SecureJ
///
/// When `handle_securejoin_handshake()` is called, the message is not yet filed in the
/// database; this is done by `receive_imf()` later on as needed.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn handle_securejoin_handshake(
context: &Context,
mime_message: &mut MimeMessage,

View File

@@ -9,6 +9,7 @@ use crate::tools::IsNoneOrEmpty;
/// This escapes a bit more than actually needed by delta (e.g. also lines as "-- footer"),
/// but for non-delta-compatibility, that seems to be better.
/// (to be only compatible with delta, only "[\r\n|\n]-- {0,2}[\r\n|\n]" needs to be replaced)
#[expect(clippy::arithmetic_side_effects)]
pub fn escape_message_footer_marks(text: &str) -> String {
if let Some(text) = text.strip_prefix("--") {
"-\u{200B}-".to_string() + &text.replace("\n--", "\n-\u{200B}-")
@@ -21,6 +22,7 @@ pub fn escape_message_footer_marks(text: &str) -> String {
/// Returns `(lines, footer_lines)` tuple;
/// `footer_lines` is set to `Some` if the footer was actually removed from `lines`
/// (which is equal to the input array otherwise).
#[expect(clippy::arithmetic_side_effects)]
pub(crate) fn remove_message_footer<'a>(
lines: &'a [&str],
) -> (&'a [&'a str], Option<&'a [&'a str]>) {
@@ -175,6 +177,7 @@ fn skip_forward_header<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
}
}
#[expect(clippy::arithmetic_side_effects)]
fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>) {
let mut first_quoted_line = lines.len();
let mut last_quoted_line = None;
@@ -217,6 +220,7 @@ fn remove_bottom_quote<'a>(lines: &'a [&str]) -> (&'a [&'a str], Option<String>)
}
}
#[expect(clippy::arithmetic_side_effects)]
fn remove_top_quote<'a>(
lines: &'a [&str],
is_chat_message: bool,
@@ -262,6 +266,7 @@ fn remove_top_quote<'a>(
}
}
#[expect(clippy::arithmetic_side_effects)]
fn render_message(lines: &[&str], is_cut_at_end: bool) -> String {
let mut ret = String::new();
/* we write empty lines only in case and non-empty line follows */

View File

@@ -136,7 +136,7 @@ async fn connection_attempt(
Err(err) => {
warn!(
context,
"Failed to connect to {host} ({resolved_addr}): {err:#}."
"SMTP failed to connect to {host} ({resolved_addr}): {err:#}."
);
Err(err)
}

View File

@@ -798,6 +798,7 @@ fn new_connection(path: &Path, passphrase: &str) -> Result<Connection> {
// Tries to clear the freelist to free some space on the disk.
//
// This only works if auto_vacuum is enabled.
#[expect(clippy::arithmetic_side_effects)]
async fn incremental_vacuum(context: &Context) -> Result<()> {
context
.sql
@@ -956,6 +957,7 @@ pub fn row_get_vec(row: &Row, idx: usize) -> rusqlite::Result<Vec<u8>> {
}
/// Enumerates used files in the blobdir and removes unused ones.
#[expect(clippy::arithmetic_side_effects)]
pub async fn remove_unused_files(context: &Context) -> Result<()> {
let mut files_in_use = HashSet::new();
let mut unreferenced_count = 0;

View File

@@ -31,6 +31,7 @@ tokio::task_local! {
static STOP_MIGRATIONS_AT: i32;
}
#[expect(clippy::arithmetic_side_effects)]
pub async fn run(context: &Context, sql: &Sql) -> Result<bool> {
let mut exists_before_update = false;
let mut dbversion_before_update = DBVERSION;
@@ -2202,6 +2203,7 @@ fn migrate_key_contacts(
}
/// Rewrite `from_id`, `to_id` in >= 1000 messages starting from the newest ones, to key-contacts.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn msgs_to_key_contacts(context: &Context) -> Result<()> {
let sql = &context.sql;
if sql

View File

@@ -50,6 +50,7 @@ impl std::fmt::Display for StorageUsage {
}
/// Get storage usage information for the Context's database
#[expect(clippy::arithmetic_side_effects)]
pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
let context_clone = ctx.clone();
let blobdir_size =
@@ -121,6 +122,7 @@ pub async fn get_storage_usage(ctx: &Context) -> Result<StorageUsage> {
}
/// Returns storage usage of the blob directory
#[expect(clippy::arithmetic_side_effects)]
pub fn get_blobdir_storage_usage(ctx: &Context) -> u64 {
WalkDir::new(ctx.get_blobdir())
.max_depth(2)

View File

@@ -37,6 +37,7 @@ impl SmearedTimestamp {
/// Allocates `count` unique timestamps.
///
/// Returns the first allocated timestamp.
#[expect(clippy::arithmetic_side_effects)]
pub fn create_n(&self, now: i64, count: i64) -> i64 {
let mut prev = self.smeared_timestamp.load(Ordering::Relaxed);
loop {

View File

@@ -47,6 +47,7 @@ use crate::stock_str;
/// Shortens a string to a specified length and adds "[...]" to the
/// end of the shortened string.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<'_, str> {
let count = buf.chars().count();
if count <= approx_chars + DC_ELLIPSIS.len() {
@@ -77,6 +78,7 @@ pub(crate) fn truncate(buf: &str, approx_chars: usize) -> Cow<'_, str> {
/// end of the shortened string.
///
/// returns tuple with the String and a boolean whether is was truncated
#[expect(clippy::arithmetic_side_effects)]
pub(crate) fn truncate_by_lines(
buf: String,
max_lines: usize,
@@ -256,6 +258,7 @@ async fn maybe_warn_on_bad_time(context: &Context, now: i64, known_past_timestam
false
}
#[expect(clippy::arithmetic_side_effects)]
async fn maybe_warn_on_outdated(context: &Context, now: i64, approx_compile_time: i64) {
if now > approx_compile_time + DC_OUTDATED_WARNING_DAYS * 24 * 60 * 60 {
let mut msg = Message::new_text(stock_str::update_reminder_msg_body(context).await);
@@ -649,6 +652,7 @@ impl ToOption<String> for Option<i32> {
}
}
#[expect(clippy::arithmetic_side_effects)]
pub fn remove_subject_prefix(last_subject: &str) -> String {
let subject_start = if last_subject.starts_with("Chat:") {
0
@@ -671,6 +675,7 @@ pub fn remove_subject_prefix(last_subject: &str) -> String {
// Types and methods to create hop-info for message-info
#[expect(clippy::arithmetic_side_effects)]
fn extract_address_from_receive_header<'a>(header: &'a str, start: &str) -> Option<&'a str> {
let header_len = header.len();
header.find(start).and_then(|mut begin| {
@@ -683,6 +688,7 @@ fn extract_address_from_receive_header<'a>(header: &'a str, start: &str) -> Opti
})
}
#[expect(clippy::arithmetic_side_effects)]
pub(crate) fn parse_receive_header(header: &str) -> String {
let header = header.replace(&['\r', '\n'][..], "");
let mut hop_info = String::from("Hop: ");
@@ -789,6 +795,7 @@ pub(crate) fn normalize_text(text: &str) -> Option<String> {
}
/// Increments `*t` and checks that it equals to `expected` after that.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) fn inc_and_check<T: PrimInt + AddAssign + std::fmt::Debug>(
t: &mut T,
expected: T,

View File

@@ -34,7 +34,7 @@ use serde_json::Value;
use sha2::{Digest, Sha256};
use tokio::{fs::File, io::BufReader};
use crate::chat::{self, Chat};
use crate::chat::{self, CantSendReason, Chat};
use crate::constants::Chattype;
use crate::contact::ContactId;
use crate::context::Context;
@@ -389,7 +389,9 @@ impl Context {
}
if let Some(ref href) = status_update_item.href {
let mut notify_msg = Message::load_from_db(self, notify_msg_id).await?;
let mut notify_msg = Message::load_from_db(self, notify_msg_id)
.await
.context("Failed to load just created notification message")?;
notify_msg.param.set(Param::Arg, href);
notify_msg.update_param(self).await?;
}
@@ -534,9 +536,14 @@ impl Context {
let chat = Chat::load_from_db(self, chat_id)
.await
.with_context(|| format!("Failed to load chat {chat_id} from the database"))?;
if let Some(reason) = chat.why_cant_send(self).await.with_context(|| {
format!("Failed to check if webxdc update can be sent to chat {chat_id}")
})? {
let skip_fn = |reason: &CantSendReason| *reason == CantSendReason::InBroadcast;
if let Some(reason) = chat
.why_cant_send_ex(self, &skip_fn)
.await
.with_context(|| {
format!("Failed to check if webxdc update can be sent to chat {chat_id}")
})?
{
bail!("Cannot send to {chat_id}: {reason}.");
}
@@ -782,6 +789,7 @@ impl Context {
/// {"payload":"another update data"}]}`
///
/// * `(first, last)`: range of status update serials to send.
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn render_webxdc_status_update_object(
&self,
instance_msg_id: MsgId,

View File

@@ -96,6 +96,7 @@ pub(crate) async fn intercept_send_update(
Ok(())
}
#[expect(clippy::arithmetic_side_effects)]
pub(crate) async fn intercept_get_updates(
context: &Context,
chat_id: Option<ChatId>,

View File

@@ -11,7 +11,9 @@ use crate::chat::{
use crate::chatlist::Chatlist;
use crate::config::Config;
use crate::ephemeral;
use crate::imex::{BackupProvider, get_backup};
use crate::receive_imf::receive_imf;
use crate::securejoin::get_securejoin_qr;
use crate::test_utils::{E2EE_INFO_MSGS, TestContext, TestContextManager};
use crate::tools::{self, SystemTime};
use crate::{message, sql};
@@ -1588,6 +1590,93 @@ async fn test_webxdc_no_internet_access() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_in_broadcast_send_status_update() -> Result<()> {
let mut tcm = TestContextManager::new();
let alice = &tcm.alice().await;
let bob = &tcm.bob().await;
let alice_chat_id = create_broadcast(alice, "bc".to_string()).await?;
let qr = get_securejoin_qr(alice, Some(alice_chat_id)).await?;
let bob_chat_id = tcm.exec_securejoin_qr(bob, alice, &qr).await;
let mut alice_instance = create_webxdc_instance(
alice,
"minimal.xdc",
include_bytes!("../../test-data/webxdc/minimal.xdc"),
)?;
alice_chat_id
.set_draft(alice, Some(&mut alice_instance))
.await?;
alice_instance = alice_chat_id.get_draft(alice).await?.unwrap();
alice
.send_webxdc_status_update(alice_instance.id, r#"{"payload":41}"#)
.await?;
send_msg(alice, alice_chat_id, &mut alice_instance).await?;
let sent_msg = alice.pop_sent_msg().await;
let bob_instance = bob.recv_msg(&sent_msg).await;
assert_eq!(bob_instance.chat_id, bob_chat_id);
let provider = BackupProvider::prepare(bob).await?;
let bob2 = &tcm.unconfigured().await;
get_backup(bob2, provider.qr()).await?;
bob.send_webxdc_status_update(bob_instance.id, r#"{"payload":42}"#)
.await?;
bob.flush_status_updates().await?;
// Don't wait to make sure that status updates are sent immediately, otherwise the check for
// Alice below isn't reliable.
let sent_msg = bob.pop_sent_msg_opt(Duration::ZERO).await.unwrap();
alice.recv_msg_trash(&sent_msg).await;
assert_eq!(
alice
.get_webxdc_status_updates(alice_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":41,"serial":1,"max_serial":2},
{"payload":42,"serial":2,"max_serial":2}]"#
);
bob2.recv_msg_trash(&sent_msg).await;
assert_eq!(
bob2.get_webxdc_status_updates(bob_instance.id, StatusUpdateSerial(0))
.await?,
r#"[{"payload":41,"serial":1,"max_serial":2},
{"payload":42,"serial":2,"max_serial":2}]"#
);
// Non-subscriber's status updates are rejected.
let alice_bob_id = alice.add_or_lookup_contact_id(bob).await;
remove_contact_from_chat(alice, alice_chat_id, alice_bob_id).await?;
alice.pop_sent_msg().await;
let status =
helper_send_receive_status_update(bob, alice, &bob_instance, &alice_instance).await?;
assert_eq!(
status,
r#"[{"payload":41,"serial":1,"max_serial":2},
{"payload":42,"serial":2,"max_serial":2}]"#
);
// Subscribers' status updates are confidential and shalln't be re-sent. So initial status
// updates aren't re-sent too.
let fiona = &tcm.fiona().await;
let fiona_chat_id = tcm.exec_securejoin_qr(fiona, alice, &qr).await;
resend_msgs(alice, &[alice_instance.id]).await?;
let sent1 = alice.pop_sent_msg().await;
alice.flush_status_updates().await?;
assert!(alice.pop_sent_msg_opt(Duration::ZERO).await.is_none());
let fiona_instance = fiona.recv_msg(&sent1).await;
assert_eq!(fiona_instance.chat_id, fiona_chat_id);
assert_eq!(fiona_instance.chat_typ, Chattype::InBroadcast);
assert_eq!(
fiona
.get_webxdc_status_updates(fiona_instance.id, StatusUpdateSerial(0))
.await?,
r#"[]"#
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_webxdc_chatlist_summary() -> Result<()> {
let t = TestContext::new_alice().await;