Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions storages/chat-store/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ impl EventHandler for ChatStoreHandler {
EventKind::ServerAck,
EventKind::UndecryptableMessage,
EventKind::HistorySync,
EventKind::PushNameUpdate,
EventKind::ContactUpdate,
EventKind::PinUpdate,
EventKind::MuteUpdate,
Expand Down Expand Up @@ -978,16 +977,6 @@ fn apply_event(
Ok(())
}
Event::HistorySync(lazy) => apply_history_sync(conn, device_id, lazy, cs),
Event::PushNameUpdate(update) => {
upsert_contact_push_name(
conn,
device_id,
&update.jid.to_string(),
&update.new_push_name,
)?;
cs.contacts = true;
Ok(())
}
Event::ContactUpdate(update) => {
upsert_contact_names(
conn,
Expand Down
158 changes: 143 additions & 15 deletions tools/whatspec-codegen/src/emit/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@
//! by concatenating the variant values, so adding one variant upstream renames
//! the entry; names also repeat across modules (`ACK`, `ENUM_LID_PN`,
//! `EventType`), and 17 are proto-nested names `waproto` already generates from
//! the `.proto`. None of those can carry a stable Rust type identity. So the
//! split is: [`WANTED`] binds a name, a shape and any variant spellings, and
//! the `.proto`. None of those can carry a stable Rust type identity.
//!
//! What makes an entry bindable is that its module owns the wire format we
//! parse, not that its variants match ours. Two enums can agree on every value
//! and still be unrelated -- the catalog's only `audio`/`video` pair belongs to
//! the status composer, and binding it to the call-link media type would let a
//! kind added for composing a status arrive in `<call_link>`. A variant set is
//! how a candidate is found; the module is what decides.
//!
//! So the split is: [`WANTED`] binds a name, a shape and any variant spellings, and
//! the IR owns everything that can drift -- which variants exist, what they
//! carry, and whether integers are bit positions. A variant added upstream
//! lands here on the next sync, and `--check` fails if the tree disagrees.
Expand All @@ -30,22 +38,55 @@ const HEADER: &str = "\

";

/// Which variant carries `#[wire_default]`.
///
/// Stated per enum rather than inferred, because `WireEnum` emits `Default`
/// whether or not we ask for one and falls back to the variant declared first.
/// Variant order here is the catalog's, so inferring would hand upstream the
/// power to change a default by reordering: `DecryptFailType` is listed
/// `hide, show` and would have flipped this client's `Show` to `Hide` with no
/// compile error and no failing test.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WireDefault {
/// The protocol's default for this attribute, named by its wire value.
/// Checked against the catalog, so a value that is renamed or dropped
/// upstream fails generation rather than moving the default.
Wire(&'static str),
/// The protocol defines no default. `Default` still exists, since the
/// derive emits it unconditionally, and resolves to whichever variant the
/// catalog happens to list first; no meaning may be read into that value.
Unspecified,
}

/// How a catalog entry is bound to Rust.
///
/// The default rides on the shape rather than sitting beside it so that the
/// combinations that do not exist cannot be written down: an enum always
/// answers the question, and a mask set is never asked it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shape {
/// A `WireEnum` over the variant values, closed: a wire value outside the
/// set is not representable. Mirrors an `attrEnumOrNullIfUnknown` field,
/// where the official parser nulls what it does not recognize.
Closed,
Closed(WireDefault),
/// The same, plus a `#[wire_fallback] Unknown(String)` arm keeping the wire
/// bytes of a value this build does not model.
Open,
Open(WireDefault),
/// Integer variants emitted as `pub const` masks named `<rust>_<VARIANT>`.
/// `bitPosition` entries are shifted here so a caller never repeats the
/// shift.
/// shift. Constants have no `Default` to pin.
Masks,
}

impl Shape {
fn default_value(self) -> Option<&'static str> {
match self {
Self::Closed(WireDefault::Wire(v)) | Self::Open(WireDefault::Wire(v)) => Some(v),
_ => None,
}
}
}

/// One catalog entry this repository binds, keyed the way the catalog is:
/// module first, because the name alone is not unique.
pub struct Wanted {
Expand All @@ -67,7 +108,7 @@ pub const WANTED: &[Wanted] = &[
module: "WAWebHandleMsgCommon",
name: "STANZA_MSG_TYPES",
rust: "StanzaMessageType",
shape: Shape::Open,
shape: Shape::Open(WireDefault::Unspecified),
renames: &[("medianotify", "MediaNotify")],
doc: "The `type` attribute of an incoming `<message>` envelope.\n\
///\n\
Expand All @@ -80,7 +121,7 @@ pub const WANTED: &[Wanted] = &[
module: "WAWebHandleMsgCommon",
name: "POLL_TYPES",
rust: "PollType",
shape: Shape::Closed,
shape: Shape::Closed(WireDefault::Unspecified),
renames: &[],
doc: "The `polltype` attribute of an incoming `<message><meta>` node.\n\
///\n\
Expand All @@ -92,7 +133,7 @@ pub const WANTED: &[Wanted] = &[
module: "WAWebBackendJobs.flow",
name: "EncMediaType",
rust: "EncMediaType",
shape: Shape::Open,
shape: Shape::Open(WireDefault::Unspecified),
renames: &[("livelocation", "LiveLocation")],
doc: "The `mediatype` attribute of an `<enc>` node.\n\
///\n\
Expand All @@ -108,6 +149,49 @@ pub const WANTED: &[Wanted] = &[
renames: &[],
doc: "Bits of a receipt's `<meta mode>` bitmask.",
},
Wanted {
module: "WAWebBackendJobs.flow",
name: "DecryptFailType",
rust: "DecryptFailMode",
shape: Shape::Closed(WireDefault::Wire("show")),
renames: &[],
doc: "The `decrypt-fail` attribute of an `<enc>` node.\n\
///\n\
/// `Hide` is the server asking that a failure to decrypt this\n\
/// stanza not be surfaced to the user.",
},
Wanted {
module: "WAWebSchemaGroupMetadata",
name: "MemberAddMode",
rust: "MemberAddMode",
shape: Shape::Closed(WireDefault::Wire("admin_add")),
renames: &[],
doc: "Who may add participants to a group.",
},
Wanted {
module: "WAWebGroupHistoryShareMode",
name: "MemberShareGroupHistoryMode",
rust: "MemberShareHistoryMode",
shape: Shape::Closed(WireDefault::Wire("admin_share")),
renames: &[],
doc: "Who may share a group's history with a new participant.",
},
Wanted {
module: "WAWebSetPrivacyJob",
name: "PrivacyUserAction",
rust: "DisallowedListAction",
shape: Shape::Closed(WireDefault::Wire("add")),
renames: &[],
doc: "Whether a privacy disallowed-list entry is being added or removed.",
},
Wanted {
module: "WAWebGroupApiConst",
name: "GROUP_PARTICIPANT_TYPES",
rust: "GroupParticipantType",
shape: Shape::Closed(WireDefault::Wire("participant")),
renames: &[("superadmin", "SuperAdmin")],
doc: "A participant's role in a group.",
},
];

pub fn generate(ir: &EnumsIr) -> Result<String> {
Expand All @@ -117,7 +201,7 @@ pub fn generate(ir: &EnumsIr) -> Result<String> {
for wanted in WANTED {
let def = lookup(ir, wanted)?;
match wanted.shape {
Shape::Closed | Shape::Open => out.push_str(&wire_enum(wanted, def)?),
Shape::Closed(_) | Shape::Open(_) => out.push_str(&wire_enum(wanted, def)?),
Shape::Masks => out.push_str(&masks(wanted, def)?),
}
}
Expand Down Expand Up @@ -174,16 +258,15 @@ fn wire_enum(wanted: &Wanted, def: &EnumDef) -> Result<String> {
wanted.name
);

let copy = if wanted.shape == Shape::Closed {
", Copy"
} else {
""
};
let open = matches!(wanted.shape, Shape::Open(_));
let copy = if open { "" } else { ", Copy" };
let mut out = format!(
"/// {}\n///\n/// Generated from `{}` in `{}`.\n#[derive(Debug, Clone{copy}, PartialEq, Eq, crate::WireEnum)]\npub enum {} {{\n",
wanted.doc, wanted.name, def.module, wanted.rust
);

let declared_default = wanted.shape.default_value();
let mut marked = 0usize;
let mut used = BTreeSet::new();
for variant in &def.variants {
let Scalar::Str(wire) = &variant.value else {
Expand All @@ -201,10 +284,26 @@ fn wire_enum(wanted: &Wanted, def: &EnumDef) -> Result<String> {
wanted.module,
wanted.name
);
if declared_default == Some(wire.as_str()) {
out.push_str(" #[wire_default]\n");
marked += 1;
}
out.push_str(&format!(" #[wire = {}]\n {ident},\n", rust_str(wire)));
}

if wanted.shape == Shape::Open {
// Checked against what was emitted rather than against the catalog, so the
// assertion covers the write as well as the declaration: a default the
// catalog dropped and a default the loop failed to place both land here.
let expected = usize::from(declared_default.is_some());
ensure!(
marked == expected,
"{}::{} expected {expected} variant(s) to carry #[wire_default] and {marked} did; \
the declared default {declared_default:?} is not a wire value this catalog entry carries",
wanted.module,
wanted.name
);

if open {
out.push_str(
" /// A value this build does not model, kept verbatim.\n #[wire_fallback]\n Unknown(String),\n",
);
Expand Down Expand Up @@ -328,6 +427,35 @@ mod tests {
assert!(closed.contains(", Copy,"));
}

/// The failure this guards is silent by nature: the derive always produces
/// a `Default`, so a declared default the catalog stopped carrying would
/// otherwise leave the first variant standing in for it.
#[test]
fn a_declared_default_the_catalog_dropped_stops_the_generator() {
let entry = wanted("DecryptFailType");
assert_eq!(entry.shape.default_value(), Some("show"));

let ok = wire_enum(entry, &def("DecryptFailType", "m", &["hide", "show"])).expect("emit");
// Placed on the declared value, not on whichever the catalog lists first.
assert!(ok.contains("#[wire_default]\n #[wire = \"show\"]"));

let err = wire_enum(entry, &def("DecryptFailType", "m", &["hide", "suppress"]))
.expect_err("the declared default is gone");
assert!(err.to_string().contains("#[wire_default]"), "{err}");
}

/// The mirror case: an enum that declares no default must not pick one up
/// from a variant that happens to share the spelling of another's.
#[test]
fn an_unspecified_default_marks_nothing() {
let out = wire_enum(
wanted("POLL_TYPES"),
&def("POLL_TYPES", "m", &["vote", "show", "add"]),
)
.expect("emit");
assert!(!out.contains("#[wire_default]"));
}

/// The catalog repeats names across modules, so binding by name alone would
/// pick whichever entry came first.
#[test]
Expand Down
74 changes: 57 additions & 17 deletions wacore/src/iq/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,7 @@ pub enum MemberLinkMode {
AllMemberLink,
}

/// Member add mode for who can add participants.
#[derive(Debug, Clone, Copy, PartialEq, Eq, WireEnum)]
pub enum MemberAddMode {
#[wire = "admin_add"]
AdminAdd,
#[wire = "all_member_add"]
AllMemberAdd,
}
pub use crate::types::wire_enums::MemberAddMode;

/// Membership approval mode for join requests.
#[derive(Debug, Clone, Copy, PartialEq, Eq, WireEnum)]
Expand All @@ -71,15 +64,7 @@ pub enum MembershipApprovalMode {
On,
}

/// Who can share message history with new members.
#[derive(Debug, Clone, Copy, PartialEq, Eq, WireEnum)]
pub enum MemberShareHistoryMode {
#[wire_default]
#[wire = "admin_share"]
AdminShare,
#[wire = "all_member_share"]
AllMemberShare,
}
pub use crate::types::wire_enums::MemberShareHistoryMode;

/// Review state for an appeal on a suspended group.
#[derive(Debug, Clone, Copy, PartialEq, Eq, WireEnum)]
Expand Down Expand Up @@ -3690,6 +3675,61 @@ mod tests {
use super::*;
use crate::request::InfoQueryType;

/// Whether a `w:g2` request is addressed to the group server or to one
/// group's own JID, checked against what the whatspec IR resolves for each
/// upstream builder.
///
/// The IR only started answering this in schema 4: before it, every `w:g2`
/// stanza reported the namespace's base target of `s.whatsapp.net`, so a
/// request sent to the wrong one of the two looked exactly like a request
/// sent to the right one. It now splits them (26 `group_jid`, 6 `g.us`),
/// and a mistake here is otherwise silent -- the server ignores the IQ and
/// the caller waits out its timeout.
///
/// This is a regression lock, not a derivation: the expectations below were
/// read off the IR by hand, since the IR is not available at test time.
#[test]
fn group_requests_are_addressed_the_way_the_ir_resolves_them() {
let group: Jid = "120363000000000001@g.us".parse().unwrap();
let server = Jid::new("", Server::Group);

// `g.us`: the group server answers these, not any one group.
assert_eq!(LeaveGroupIq::new(&group).build_iq().to, server);
assert_eq!(GroupParticipatingIq::new().build_iq().to, server);
assert_eq!(
BatchGetGroupInfoIq::new(std::slice::from_ref(&group))
.build_iq()
.to,
server
);
assert_eq!(GetGroupInviteInfoIq::new("ABC123").build_iq().to, server);

// `group_jid`: addressed to the one group they act on.
assert_eq!(
SetGroupSubjectIq::new(&group, GroupSubject::new("x").unwrap())
.build_iq()
.to,
group
);
assert_eq!(
GetGroupInviteLinkIq::new(&group, false).build_iq().to,
group
);
assert_eq!(
GetGroupInviteLinkIq::new(&group, true).build_iq().to,
group,
"the reset overload the IR resolves to group_jid is the one with an \
empty <invite/>; the g.us overload carries a code and is not this"
);
assert_eq!(
ReportGroupMessagesIq::new(&group, &["M1".to_string()])
.build_iq()
.to,
group
);
assert_eq!(GetReportedGroupMessagesIq::new(&group).build_iq().to, group);
}

#[test]
fn report_messages_iq_matches_the_group_report_shape() {
let jid: Jid = "120363000000000001@g.us".parse().unwrap();
Expand Down
9 changes: 1 addition & 8 deletions wacore/src/iq/privacy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,14 +220,7 @@ impl IqSpec for PrivacySettingsSpec {
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, WireEnum)]
pub enum DisallowedListAction {
#[wire_default]
#[wire = "add"]
Add,
#[wire = "remove"]
Remove,
}
pub use crate::types::wire_enums::DisallowedListAction;

#[derive(Debug, Clone)]
pub struct DisallowedListUserEntry {
Expand Down
Loading
Loading