Skip to content
Merged
3 changes: 1 addition & 2 deletions src/features/chat_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
use crate::appstate_sync::Mutation;
use crate::client::Client;
use anyhow::Result;
use chrono::DateTime;
use log::debug;
use wacore::appstate::patch_decode::WAPatchName;
use wacore::types::events::{
Expand Down Expand Up @@ -89,7 +88,7 @@ pub(crate) fn dispatch_chat_mutation(
.as_ref()
.and_then(|v| v.timestamp)
.unwrap_or(0);
let time = DateTime::from_timestamp_millis(ts).unwrap_or_else(wacore::time::now_utc);
let time = wacore::time::from_millis_or_now(ts);
let jid: Jid = if m.index.len() > 1 {
match m.index[1].parse() {
Ok(j) => j,
Expand Down
20 changes: 11 additions & 9 deletions src/handlers/notification.rs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ async fn handle_business_notification(client: &Arc<Client>, node: &NodeRef<'_>)
let event = Event::BusinessStatusUpdate(BusinessStatusUpdate {
jid: notification.from.clone(),
update_type,
timestamp: notification.timestamp,
timestamp: wacore::time::from_secs_or_now(notification.timestamp),
target_jid: notification.jid.clone(),
hash: notification.hash.clone(),
verified_name,
Expand Down Expand Up @@ -1045,8 +1045,8 @@ fn notification_timestamp(node: &NodeRef<'_>) -> chrono::DateTime<chrono::Utc> {
node.attrs()
.optional_u64("t")
.and_then(|t| i64::try_from(t).ok())
.and_then(|t| chrono::DateTime::from_timestamp(t, 0))
.unwrap_or_else(chrono::Utc::now)
.and_then(wacore::time::from_secs)
.unwrap_or_else(wacore::time::now_utc)
}

/// Learn LID-PN mappings from a contacts modify notification.
Expand Down Expand Up @@ -1171,7 +1171,7 @@ async fn handle_contacts_notification(client: &Arc<Client>, node: &NodeRef<'_>)
let after = child
.attrs()
.optional_u64("after")
.and_then(|after| chrono::DateTime::from_timestamp(after as i64, 0));
.and_then(|after| wacore::time::from_secs(after as i64));
Comment on lines 1171 to +1174

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Read the file around lines 1171-1174 to see the actual code context
head -n 1180 src/handlers/notification.rs | tail -n 20

Repository: jlucaso1/whatsapp-rust

Length of output: 689


🏁 Script executed:

# Search for the wacore::time module to understand from_secs
rg "from_secs" -A 3 --type rs

Repository: jlucaso1/whatsapp-rust

Length of output: 92


🏁 Script executed:

# Check if there's a wacore crate in the project and inspect time module
fd -e rs -x grep -l "wacore" | head -5

Repository: jlucaso1/whatsapp-rust

Length of output: 257


🏁 Script executed:

# Look for the definition of from_secs to understand what it accepts
rg "fn from_secs" -A 5 --type rs

Repository: jlucaso1/whatsapp-rust

Length of output: 92


🏁 Script executed:

# Search for from_secs definition without type flag
grep -r "fn from_secs" --include="*.rs"

Repository: jlucaso1/whatsapp-rust

Length of output: 240


🏁 Script executed:

# Check the wacore module structure
find wacore -name "*.rs" -type f | head -20

Repository: jlucaso1/whatsapp-rust

Length of output: 681


🏁 Script executed:

# Look at the time module in wacore
grep -r "from_secs" wacore --include="*.rs" -B 2 -A 10

Repository: jlucaso1/whatsapp-rust

Length of output: 7301


🏁 Script executed:

# Check if there are any tests or usages that show from_secs behavior
grep -r "from_secs" --include="*.rs" -B 2 -A 5 | head -60

Repository: jlucaso1/whatsapp-rust

Length of output: 3823


Avoid u64 as i64 for after to reject oversized timestamps.

Rust wraps on overflow, so a malformed after above i64::MAX becomes a negative timestamp (before 1970) instead of None, likely unintended for a sync request attribute.

💡 Suggested fix
             let after = child
                 .attrs()
                 .optional_u64("after")
-                .and_then(|after| wacore::time::from_secs(after as i64));
+                .and_then(|after| i64::try_from(after).ok())
+                .and_then(wacore::time::from_secs);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let after = child
.attrs()
.optional_u64("after")
.and_then(|after| chrono::DateTime::from_timestamp(after as i64, 0));
.and_then(|after| wacore::time::from_secs(after as i64));
let after = child
.attrs()
.optional_u64("after")
.and_then(|after| i64::try_from(after).ok())
.and_then(wacore::time::from_secs);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/handlers/notification.rs` around lines 1171 - 1174, The current
conversion uses `u64 as i64` which wraps on overflow; change the `after`
handling (starting from `child.attrs().optional_u64("after")`) to safely reject
values > i64::MAX by using a fallible conversion (e.g., `try_into()`/`checked_*`
or an explicit `if after <= i64::MAX as u64`) and only call
`wacore::time::from_secs` with the safely converted `i64`; return `None` for
oversized `after` values so malformed timestamps are rejected instead of
wrapping.


debug!(
target: "Client/Contacts",
Expand Down Expand Up @@ -1220,8 +1220,8 @@ async fn handle_group_notification(client: &Arc<Client>, node: Arc<OwnedNodeRef>

let timestamp = i64::try_from(notification.timestamp)
.ok()
.and_then(|t| chrono::DateTime::from_timestamp(t, 0))
.unwrap_or_else(chrono::Utc::now);
.and_then(wacore::time::from_secs)
.unwrap_or_else(wacore::time::now_utc);

for action in notification.actions {
// Granularly patch group cache instead of invalidating — matches WA Web's
Expand Down Expand Up @@ -1396,7 +1396,8 @@ fn handle_disappearing_mode_notification(client: &Arc<Client>, node: &NodeRef<'_
// WA Web: `t.attrTime("t")` — required, no default.
let Some(setting_timestamp) = dm_attrs
.optional_string("t")
.and_then(|s| s.parse::<u64>().ok())
.and_then(|s| s.parse::<i64>().ok())
.and_then(wacore::time::from_secs)
else {
warn!(
"disappearing_mode notification missing or invalid 't' attribute: {}",
Expand Down Expand Up @@ -1698,7 +1699,7 @@ mod tests {
/// Helper: parse a disappearing_mode notification node the same way
/// the handler does, returning `(duration, setting_timestamp)` or `None`
/// on validation failure.
fn parse_disappearing_mode(node: &Node) -> Option<(u32, u64)> {
fn parse_disappearing_mode(node: &Node) -> Option<(u32, i64)> {
let dm_node = node.get_optional_child("disappearing_mode")?;
let mut dm_attrs = dm_node.attrs();
let duration = dm_attrs
Expand All @@ -1707,7 +1708,8 @@ mod tests {
.unwrap_or(0);
let setting_timestamp = dm_attrs
.optional_string("t")
.and_then(|s| s.parse::<u64>().ok())?;
.and_then(|s| s.parse::<i64>().ok())
.filter(|&t| wacore::time::from_secs(t).is_some())?;
Some((duration, setting_timestamp))
}

Expand Down
2 changes: 1 addition & 1 deletion src/handlers/presence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ impl StanzaHandler for PresenceHandler {
.get_attr("last")
.map(|v| v.as_str())
.and_then(|s| s.parse::<i64>().ok())
.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0));
.and_then(wacore::time::from_secs);

debug!(
target: "PresenceHandler",
Expand Down
6 changes: 2 additions & 4 deletions src/pdo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -413,10 +413,8 @@ impl Client {

let timestamp = web_msg
.message_timestamp
.map(|ts| {
chrono::DateTime::from_timestamp(ts as i64, 0).unwrap_or_else(chrono::Utc::now)
})
.unwrap_or_else(chrono::Utc::now);
.map(|ts| wacore::time::from_secs_or_now(ts as i64))
.unwrap_or_else(wacore::time::now_utc);

Ok(MessageInfo {
id: key.id.clone().unwrap_or_default(),
Expand Down
2 changes: 1 addition & 1 deletion src/version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub async fn resolve_and_update_version(
let needs_fetch = if last_fetched_ms == 0 {
true
} else {
match chrono::DateTime::from_timestamp_millis(last_fetched_ms) {
match wacore::time::from_millis(last_fetched_ms) {
Some(last_fetched_dt) => {
wacore::time::now_utc().signed_duration_since(last_fetched_dt)
> chrono::Duration::hours(24)
Expand Down
3 changes: 1 addition & 2 deletions wacore/src/messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,7 @@ pub fn parse_message_info(
.optional_string("notify")
.map(|s| s.to_string())
.unwrap_or_default(),
timestamp: chrono::DateTime::from_timestamp(attrs.unix_time("t"), 0)
.unwrap_or_else(chrono::Utc::now),
timestamp: crate::time::from_secs_or_now(attrs.unix_time("t")),
category,
edit: attrs
.optional_string("edit")
Expand Down
2 changes: 1 addition & 1 deletion wacore/src/stanza/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use wacore_binary::Node;
pub fn notification_timestamp(node: &Node) -> chrono::DateTime<chrono::Utc> {
node.attrs()
.optional_u64("t")
.and_then(|t| chrono::DateTime::from_timestamp(t as i64, 0))
.and_then(|t| crate::time::from_secs(t as i64))
.unwrap_or_else(crate::time::now_utc)
Comment on lines 21 to 24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Rust, what happens when you convert u64toi64withasand the value is greater thani64::MAX? Does it wrap, saturate, or error?

💡 Result:

When converting a u64 to i64 using the as cast where the u64 value exceeds i64::MAX (9223372036854775807), it performs a reinterpretation of the bit pattern. Since both types are 64 bits and Rust uses two's complement for signed integers, casting u64 to i64 is a no-op on the bits—it reinterprets the unsigned bits as a signed value. For values <= i64::MAX, the numerical value is preserved. For values > i64::MAX, the result is a negative i64. Specifically, u64::MAX (all bits 1) becomes -1i64, as confirmed by the official documentation: u64::cast_signed example shows u64::MAX.cast_signed == -1i64, and this produces the same result as as. This is neither saturation (clamping to i64::MAX), nor does it error or panic. It "wraps" in the sense of bit reinterpretation, producing a negative number equivalent to the two's complement interpretation. Official Rust Reference confirms: "Casting between two integers of the same size (e.g. i32 -> u32) is a no-op (Rust uses 2's complement for negative numbers)." Rust By Example notes: "When casting to a signed type, the (bitwise) result is the same as first casting to the corresponding unsigned type." It does not panic on overflow like arithmetic operations in debug mode; casts are always defined and do not trigger overflow checks.

Citations:


🏁 Script executed:

cat -n wacore/src/stanza/notification.rs | head -50

Repository: jlucaso1/whatsapp-rust

Length of output: 2503


🏁 Script executed:

rg "parse_disappearing_mode" wacore/src/stanza/notification.rs -A 10

Repository: jlucaso1/whatsapp-rust

Length of output: 2290


🏁 Script executed:

rg "fn from_secs" wacore/src/time.rs -A 5

Repository: jlucaso1/whatsapp-rust

Length of output: 521


🏁 Script executed:

rg "notification_timestamp" wacore/src -B 2 -A 5

Repository: jlucaso1/whatsapp-rust

Length of output: 1924


🏁 Script executed:

rg "from_timestamp" wacore/src -B 2 -A 2

Repository: jlucaso1/whatsapp-rust

Length of output: 813


🏁 Script executed:

# Check if there are any tests for notification_timestamp that show the wrapping issue
rg "test.*notification_timestamp" wacore/src/stanza/notification.rs -A 15

Repository: jlucaso1/whatsapp-rust

Length of output: 48


Use signed parsing or checked conversion for the timestamp.

The current code uses optional_u64("t") followed by as i64. When the attribute contains a value larger than i64::MAX (e.g., 18446744073709551615), the cast reinterprets the bit pattern as a negative i64 (e.g., -1). Since chrono::DateTime::from_timestamp() accepts negative timestamps (valid pre-1970 dates), the conversion succeeds instead of returning None, and the invalid input produces a 1969 timestamp rather than falling back to now_utc().

Suggested change
 pub fn notification_timestamp(node: &Node) -> chrono::DateTime<chrono::Utc> {
     node.attrs()
-        .optional_u64("t")
-        .and_then(|t| crate::time::from_secs(t as i64))
+        .optional_string("t")
+        .and_then(|t| t.parse::<i64>().ok())
+        .and_then(crate::time::from_secs)
         .unwrap_or_else(crate::time::now_utc)
 }

Parsing as i64 will reject out-of-range values, triggering the fallback as intended. No test currently covers this edge case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wacore/src/stanza/notification.rs` around lines 21 - 24, Replace the
unsigned-to-signed unchecked cast that causes wraparound: instead of calling
node.attrs().optional_u64("t").and_then(|t| crate::time::from_secs(t as
i64)).unwrap_or_else(crate::time::now_utc), parse or convert the attribute into
a signed i64 safely (e.g., use an optional_i64 accessor or try_from/checked
conversion on the u64) so out-of-range u64 values fail the conversion and
trigger the fallback to crate::time::now_utc; locate the code around
node.attrs(), optional_u64("t"), crate::time::from_secs and crate::time::now_utc
and change the conversion to a checked/validated i64 path.

}

Expand Down
28 changes: 28 additions & 0 deletions wacore/src/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,34 @@ pub fn now_utc() -> chrono::DateTime<chrono::Utc> {
.expect("time provider returned out-of-range millisecond timestamp")
}

/// Convert a Unix timestamp (seconds) to `DateTime<Utc>`.
/// Returns `None` for out-of-range values.
#[inline]
pub fn from_secs(ts: i64) -> Option<chrono::DateTime<chrono::Utc>> {
chrono::DateTime::from_timestamp(ts, 0)
}

/// Convert a Unix timestamp (seconds) to `DateTime<Utc>`,
/// falling back to `now_utc()` for out-of-range values.
#[inline]
pub fn from_secs_or_now(ts: i64) -> chrono::DateTime<chrono::Utc> {
from_secs(ts).unwrap_or_else(now_utc)
}

/// Convert a Unix timestamp (milliseconds) to `DateTime<Utc>`.
/// Returns `None` for out-of-range values.
#[inline]
pub fn from_millis(ts: i64) -> Option<chrono::DateTime<chrono::Utc>> {
chrono::DateTime::from_timestamp_millis(ts)
}

/// Convert a Unix timestamp (milliseconds) to `DateTime<Utc>`,
/// falling back to `now_utc()` for out-of-range values.
#[inline]
pub fn from_millis_or_now(ts: i64) -> chrono::DateTime<chrono::Utc> {
from_millis(ts).unwrap_or_else(now_utc)
}

/// Portable monotonic instant, replacing `std::time::Instant` which is
/// unavailable on `wasm32-unknown-unknown`.
///
Expand Down
24 changes: 20 additions & 4 deletions wacore/src/types/events.rs
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -316,9 +316,11 @@ impl From<crate::stanza::business::BusinessNotificationType> for BusinessUpdateT
/// Business status update notification.
#[derive(Debug, Clone, Serialize)]
pub struct BusinessStatusUpdate {
/// The business account whose status changed.
pub jid: Jid,
pub update_type: BusinessUpdateType,
pub timestamp: i64,
#[serde(with = "chrono::serde::ts_seconds")]
pub timestamp: DateTime<Utc>,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
#[serde(skip_serializing_if = "Option::is_none")]
pub target_jid: Option<Jid>,
#[serde(skip_serializing_if = "Option::is_none")]
Expand All @@ -344,9 +346,10 @@ pub struct DisappearingModeChanged {
pub from: Jid,
/// New duration in seconds (0 = disabled, 86400 = 24h, etc.).
pub duration: u32,
/// Unix timestamp (seconds) when the setting was changed.
/// Consumers should only apply this if it's newer than their stored timestamp.
pub setting_timestamp: u64,
/// When the setting was changed.
/// Consumers should only apply this if it's newer than their stored value.
#[serde(with = "chrono::serde::ts_seconds")]
pub setting_timestamp: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize)]
Expand Down Expand Up @@ -451,6 +454,7 @@ impl Event {
/// reaction counts for one or more messages.
#[derive(Debug, Clone, Serialize)]
pub struct NewsletterLiveUpdate {
/// The newsletter channel this update belongs to.
pub newsletter_jid: Jid,
pub messages: Vec<NewsletterLiveUpdateMessage>,
}
Expand Down Expand Up @@ -706,6 +710,7 @@ pub struct ChatPresenceUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct PresenceUpdate {
/// The contact whose presence changed.
pub from: Jid,
pub unavailable: bool,
pub last_seen: Option<DateTime<Utc>>,
Expand All @@ -729,6 +734,7 @@ pub struct PictureUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct UserAboutUpdate {
/// The contact whose about text changed.
pub jid: Jid,
pub status: String,
pub timestamp: DateTime<Utc>,
Expand All @@ -744,6 +750,7 @@ pub struct UserAboutUpdate {
/// sync mutations (different source, different payload).
#[derive(Debug, Clone, Serialize)]
pub struct ContactUpdated {
/// The contact whose profile was updated.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
}
Expand Down Expand Up @@ -804,6 +811,7 @@ pub struct GroupUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct ContactUpdate {
/// The chat/contact this sync action applies to.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
pub action: Box<wa::sync_action_value::ContactAction>,
Expand All @@ -812,6 +820,7 @@ pub struct ContactUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct PushNameUpdate {
/// The contact who changed their push name.
pub jid: Jid,
pub message: Box<MessageInfo>,
pub old_push_name: String,
Expand All @@ -820,6 +829,7 @@ pub struct PushNameUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct PinUpdate {
/// The chat being pinned or unpinned.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
pub action: Box<wa::sync_action_value::PinAction>,
Expand All @@ -828,6 +838,7 @@ pub struct PinUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct MuteUpdate {
/// The chat being muted or unmuted.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
pub action: Box<wa::sync_action_value::MuteAction>,
Expand All @@ -836,6 +847,7 @@ pub struct MuteUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct ArchiveUpdate {
/// The chat being archived or unarchived.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
pub action: Box<wa::sync_action_value::ArchiveChatAction>,
Expand All @@ -844,6 +856,7 @@ pub struct ArchiveUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct StarUpdate {
/// The chat containing the starred or unstarred message.
pub chat_jid: Jid,
/// The participant who sent the message. `Some` for group messages from
/// others, `None` for self-authored or 1-on-1 messages (wire value `"0"`).
Expand All @@ -857,6 +870,7 @@ pub struct StarUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct MarkChatAsReadUpdate {
/// The chat being marked as read or unread.
pub jid: Jid,
pub timestamp: DateTime<Utc>,
pub action: Box<wa::sync_action_value::MarkChatAsReadAction>,
Expand All @@ -865,6 +879,7 @@ pub struct MarkChatAsReadUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct DeleteChatUpdate {
/// The chat being deleted.
pub jid: Jid,
/// From the index, not the proto — DeleteChatAction only has messageRange.
pub delete_media: bool,
Expand All @@ -875,6 +890,7 @@ pub struct DeleteChatUpdate {

#[derive(Debug, Clone, Serialize)]
pub struct DeleteMessageForMeUpdate {
/// The chat containing the deleted message.
pub chat_jid: Jid,
pub participant_jid: Option<Jid>,
pub message_id: String,
Expand Down
Loading