diff --git a/crates/wa-codegen/src/emit.rs b/crates/wa-codegen/src/emit.rs index 1eeef79..f75e3be 100644 --- a/crates/wa-codegen/src/emit.rs +++ b/crates/wa-codegen/src/emit.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use wa_ir::wap; -use wa_ir::{ParsedField, ParsedFieldType, WapAttrKind, WapChildNode}; +use wa_ir::{ParsedField, ParsedFieldType, WapAttrKind, WapChildNode, WapContentKind}; use crate::fields::{ child_content_type, flatten_same_node, is_attr_field, is_child_field, is_jid_kind, @@ -606,6 +606,7 @@ pub(crate) fn emit_child_builder( } } body.extend(emit_variant_groups(child, &var_name, indent, ctx)); + body.extend(emit_node_content(child, &var_name, indent, ctx)); if !nested_var_names.is_empty() { body.push(format!( "{indent}{var_name} = {var_name}.children([{}]);", @@ -722,6 +723,82 @@ fn emit_variant_groups( build } +/// Emit the leaf element content of a request node (``, ``, the +/// prekey ``) — the byte payload the node carries between +/// its tags. Without this a key-material node builds empty and is unusable on the +/// wire. Two shapes, both `bytes`: +/// - a compile-time constant ([`WapContent::const_bytes`], e.g. the one-byte `00` +/// nonce) → a literal `.bytes(vec![0x00])`; +/// - a caller-supplied buffer (a bare `bytes` content, e.g. a prekey `signature`) +/// → a `Vec` spec field pushed to `ctx.fields` (mirroring +/// [`emit_variant_groups`]) and threaded in as `.bytes(self..clone())`. +/// +/// The field name derives from the caller's collision-free `var_name` (`id_node` → +/// `id_content`, `id_node_2` → `id_content_2`) so repeated leaf tags across a +/// stanza (the three ``/`` nodes in a prekey `` tree) each get a +/// distinct field. +fn emit_node_content( + child: &WapChildNode, + var_name: &str, + indent: &str, + ctx: &mut VariantCtx, +) -> Vec { + let Some(content) = &child.content else { + return Vec::new(); + }; + // A fixed byte constant: emit the literal buffer, no caller input needed. Handled + // in isolation — a `const_bytes` that fails to decode degrades to no content, and + // must never fall through to the caller-supplied-field branch below (that would + // turn a fixed constant into an unwanted `Vec` builder argument). + if let Some(hex) = &content.const_bytes { + return decode_hex(hex) + .map(|bytes| { + let lits = bytes + .iter() + .map(|b| format!("0x{b:02x}")) + .collect::>() + .join(", "); + vec![format!( + "{indent}{var_name} = {var_name}.bytes(vec![{lits}]);" + )] + }) + .unwrap_or_default(); + } + // A caller-supplied byte buffer: thread a `Vec` spec field. Rename the + // *terminal* `_node` segment (the var is `{tag}_node` or `{tag}_node_{n}`) so a + // tag that itself contains `_node` isn't corrupted (`node_id_node` → `node_id_content`). + if content.kind == WapContentKind::Bytes { + let field = match var_name.rfind("_node") { + Some(pos) => format!( + "{}_content{}", + &var_name[..pos], + &var_name[pos + "_node".len()..] + ), + None => format!("{var_name}_content"), + }; + ctx.fields + .push((field.clone(), "Vec".to_string(), false)); + return vec![format!( + "{indent}{var_name} = {var_name}.bytes(self.{field}.clone());" + )]; + } + Vec::new() +} + +/// Decode an even-length hex string (`"00"`, `"0a1b"`) to its bytes. Returns `None` +/// for malformed input (odd length, non-ASCII, or a non-hex digit) so a bad +/// `const_bytes` degrades to no content rather than panicking codegen. The +/// `is_ascii` gate also keeps the byte-index slicing below on char boundaries. +fn decode_hex(s: &str) -> Option> { + if s.len() % 2 != 0 || !s.is_ascii() { + return None; + } + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()) + .collect() +} + /// A const attr present in every variant of a group with DISTINCT values is the /// discriminator (e.g. `type` = `jid`/`invite`); variants are named by its value. fn variant_discriminator(group: &wa_ir::WapVariantGroup) -> Option { @@ -1265,4 +1342,131 @@ mod tests { "{code}" ); } + + #[test] + fn const_bytes_content_emits_literal_byte_buffer() { + use wa_ir::{WapContent, WapContentKind}; + // The one-byte `00` link-code pairing nonce: a compile-time constant, so the + // builder writes the literal buffer directly (no caller input). Previously the + // node was built empty, making the request unusable on the wire. + let node = WapChildNode { + tag: "link_code_pairing_nonce".into(), + attrs: vec![], + children: vec![], + content: Some(WapContent { + kind: WapContentKind::Bytes, + byte_length: Some(1), + const_bytes: Some("00".into()), + ..Default::default() + }), + repeats: false, + variant_groups: vec![], + }; + let (lines, _) = build1(&node); + let code = lines.join("\n"); + assert!( + code.contains( + "link_code_pairing_nonce_node = link_code_pairing_nonce_node.bytes(vec![0x00]);" + ), + "{code}" + ); + } + + #[test] + fn dynamic_bytes_content_threads_a_vec_u8_spec_field() { + use wa_ir::{WapContent, WapContentKind}; + // A prekey `` carries caller-supplied key material, so the builder + // threads a `Vec` spec field named after the (collision-free) var and + // writes it as the node content. + let node = WapChildNode { + tag: "signature".into(), + attrs: vec![], + children: vec![], + content: Some(WapContent { + kind: WapContentKind::Bytes, + byte_length: Some(64), + ..Default::default() + }), + repeats: false, + variant_groups: vec![], + }; + let (mut enums, mut fields) = (Vec::new(), Vec::new()); + let mut ctx = VariantCtx { + spec_base: "T", + enum_defs: &mut enums, + fields: &mut fields, + }; + let (lines, _) = emit_child_builder(&node, "", &mut HashMap::new(), &mut ctx); + let code = lines.join("\n"); + assert!( + code.contains("signature_node = signature_node.bytes(self.signature_content.clone());"), + "{code}" + ); + assert_eq!( + fields, + vec![( + "signature_content".to_string(), + "Vec".to_string(), + false + )] + ); + } + + #[test] + fn dynamic_content_field_renames_only_the_terminal_node_segment() { + use wa_ir::{WapContent, WapContentKind}; + // A tag whose own name contains `_node` must not corrupt the field name: the + // var `node_id_node` yields `node_id_content`, not `node_content_node`. + let node = WapChildNode { + tag: "node_id".into(), + attrs: vec![], + children: vec![], + content: Some(WapContent { + kind: WapContentKind::Bytes, + byte_length: Some(8), + ..Default::default() + }), + repeats: false, + variant_groups: vec![], + }; + let (mut enums, mut fields) = (Vec::new(), Vec::new()); + let mut ctx = VariantCtx { + spec_base: "T", + enum_defs: &mut enums, + fields: &mut fields, + }; + emit_child_builder(&node, "", &mut HashMap::new(), &mut ctx); + assert_eq!(fields[0].0, "node_id_content"); + } + + #[test] + fn malformed_const_bytes_emits_nothing_and_adds_no_field() { + use wa_ir::{WapContent, WapContentKind}; + // A `const_bytes` that can't be decoded degrades to no content — it must not + // fall through and be turned into a caller-supplied `Vec` field. + let node = WapChildNode { + tag: "nonce".into(), + attrs: vec![], + children: vec![], + content: Some(WapContent { + kind: WapContentKind::Bytes, + const_bytes: Some("zz".into()), // not hex + ..Default::default() + }), + repeats: false, + variant_groups: vec![], + }; + let (mut enums, mut fields) = (Vec::new(), Vec::new()); + let mut ctx = VariantCtx { + spec_base: "T", + enum_defs: &mut enums, + fields: &mut fields, + }; + let (lines, _) = emit_child_builder(&node, "", &mut HashMap::new(), &mut ctx); + assert!(fields.is_empty(), "no spec field for a malformed constant"); + assert!( + !lines.join("\n").contains(".bytes("), + "no content emitted for a malformed constant" + ); + } } diff --git a/crates/wa-ir/src/iq.rs b/crates/wa-ir/src/iq.rs index 4654329..1c4b669 100644 --- a/crates/wa-ir/src/iq.rs +++ b/crates/wa-ir/src/iq.rs @@ -397,6 +397,19 @@ pub struct ParsedField { pub required: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub byte_length: Option, + /// Inclusive lower bound on the byte-content length, enforced via + /// `contentBytesRange(node, min, max)` when `min != max` — a payload-size *limit* + /// (a media buffer capped at 1 MiB, a token capped at 128 bytes), as opposed to a + /// fixed [`byte_length`] (the `min == max` case). Present only for a + /// [`ParsedFieldType::Bytes`] field with a true range; a consumer can enforce the + /// bound rather than guessing an unbounded buffer. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub byte_min: Option, + /// Inclusive upper bound from the same `contentBytesRange` check (see [`byte_min`]). + /// + /// [`byte_min`]: ParsedField::byte_min + #[serde(default, skip_serializing_if = "Option::is_none")] + pub byte_max: Option, /// Inclusive lower bound the parser enforces via `attrIntRange(node, name, min, /// max)`. Present only for a bounded [`ParsedFieldType::Integer`]; a consumer can /// validate or pick a narrower Rust width from these. The timestamp-marker range is diff --git a/crates/wa-notif/src/lib.rs b/crates/wa-notif/src/lib.rs index 3220843..5e69631 100644 --- a/crates/wa-notif/src/lib.rs +++ b/crates/wa-notif/src/lib.rs @@ -37,8 +37,8 @@ use oxc_allocator::Allocator; use oxc_ast::ast::{Expression, Statement, SwitchCase, SwitchStatement, VariableDeclaration}; use oxc_ast_visit::{Visit, walk}; use wa_ir::{ - AssertionKind, NotifIr, NotificationDef, ParsedResponse, StanzaTagDef, SubCase, - SubDiscriminant, SubDiscriminantOn, + AssertionKind, NotifIr, NotificationDef, ParsedResponse, ServerRequestTag, StanzaTagDef, + SubCase, SubDiscriminant, SubDiscriminantOn, }; use wa_oxc::{ arg_expr, as_call, as_identifier, as_int, as_member, as_string_lit, callee_method, @@ -123,6 +123,22 @@ pub fn extract_notif_from_modules( n.content = notification_content(slice, ¬if_type); } + // Phase 2b: a type still degraded here has a handler that delegates parsing to + // the smax receive-RPC path rather than an inline `WADeprecatedWapParser`, so its + // field tree lives in a `WASmaxIn...Request` module (the srvreq domain). + // Recover it by matching the server-request read-shape whose asserted `type` + // equals the notification's, keeping only unambiguous single-shape types. + if dispatch.notifications.iter().any(|n| n.content.is_none()) { + let srvreq_shapes = srvreq_notification_shapes(source, module_defs); + for n in &mut dispatch.notifications { + if n.content.is_none() + && let Some(shape) = srvreq_shapes.get(&n.notif_type) + { + n.content = Some(shape.clone()); + } + } + } + NotifIr { wa_version: wa_version.to_string(), dispatcher_modules, @@ -217,6 +233,41 @@ fn notification_content(handler_slice: &str, notif_type: &str) -> Option...Request` shape whose +/// parser asserts that `type`. Only types with a single matching shape are kept, so +/// a type parsed by several distinct request modules stays degraded rather than being +/// bound to one arbitrary shape (mirroring [`pick_notification_parser`]'s caution). +fn srvreq_notification_shapes( + source: &str, + module_defs: &[ModuleDefinition], +) -> std::collections::HashMap { + let asserted_type = |p: &ParsedResponse| -> Option { + p.assertions.iter().find_map(|a| { + (a.kind == AssertionKind::Attr && a.name.as_deref() == Some("type")) + .then(|| a.value.clone()) + .flatten() + }) + }; + let mut by_type: std::collections::HashMap> = + std::collections::HashMap::new(); + for def in wa_scan::scan_server_requests_from_modules(source, module_defs) { + if def.tag != ServerRequestTag::Notification { + continue; + } + if let Some(t) = asserted_type(&def.shape) { + by_type.entry(t).or_default().push(def.shape); + } + } + by_type + .into_iter() + .filter_map(|(t, mut shapes)| match shapes.len() { + 1 => Some((t, shapes.pop().expect("len checked"))), + _ => None, + }) + .collect() +} + /// Choose the parser for `notif_type` from a handler module's parsers. /// /// A module can hold parsers for several notification types (each asserting its diff --git a/crates/wa-notif/src/tests.rs b/crates/wa-notif/src/tests.rs index dbca005..bd9ded5 100644 --- a/crates/wa-notif/src/tests.rs +++ b/crates/wa-notif/src/tests.rs @@ -509,3 +509,58 @@ fn extraction_is_deterministic() { let b = serde_json::to_string(&ir()).unwrap(); assert_eq!(a, b); } + +/// A `` handler that delegates to the smax receive-RPC +/// path (no inline `WADeprecatedWapParser`) plus the matching `WASmaxIn*Request` +/// read-shape module, so Phase 2b recovers the content from the srvreq domain. +const SRVREQ_LINK_BUNDLE: &str = r#" +__d("WAWebCommsHandleLoggedInStanza",["WAWebAccountLinkingNotificationHandler"],function(g,r,d,o,e,i,l){ + l.handle = function(){ return (function*(e,t){ + var n = e.attrs; + switch (e.tag) { + case "notification": + switch (n.type) { + case "waffle": return yield r("WAWebAccountLinkingNotificationHandler")(e); + } + } + }); }; +}, 1); +__d("WASmaxInWaffleWFNotificationRequest",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"notification");if(!t.success)return t;var n=o("WASmaxParseUtils").assertAttr(e,"type","waffle");if(!n.success)return n;var s=o("WASmaxParseUtils").attrString(e,"id");return s.success?o("WAResultOrError").makeResult({id:s.value}):s}l.parseWFNotificationRequest=e}),1); +"#; + +#[test] +fn srvreq_read_shape_recovers_degraded_notification_content() { + let ir = extract_notif(SRVREQ_LINK_BUNDLE, "2.3000.test"); + let content = notif(&ir, "waffle") + .content + .as_ref() + .expect("waffle content recovered from the srvreq read-shape"); + assert_eq!(content.parser_name, "parseWFNotificationRequest"); + assert!(content.fields.iter().any(|f| f.name == "id")); +} + +#[test] +fn ambiguous_srvreq_type_leaves_notification_degraded() { + // Two request modules assert the same `type="hosted"`; without a unique shape the + // notification stays degraded rather than binding to an arbitrary one. + let bundle = r#" +__d("WAWebCommsHandleLoggedInStanza",["WAWebHandleHostedNotification"],function(g,r,d,o,e,i,l){ + l.handle = function(){ return (function*(e,t){ + var n = e.attrs; + switch (e.tag) { + case "notification": + switch (n.type) { + case "hosted": return yield r("WAWebHandleHostedNotification")(e); + } + } + }); }; +}, 1); +__d("WASmaxInCoexistenceOnboardingStatusNotificationRequest",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"notification");if(!t.success)return t;var n=o("WASmaxParseUtils").assertAttr(e,"type","hosted");if(!n.success)return n;var s=o("WASmaxParseUtils").attrString(e,"a");return s.success?o("WAResultOrError").makeResult({a:s.value}):s}l.parseOnboardingStatusNotificationRequest=e}),1); +__d("WASmaxInCoexistenceOffboardingNotificationRequest",["WAResultOrError","WASmaxParseUtils"],(function(t,n,r,o,a,i,l){function e(e){var t=o("WASmaxParseUtils").assertTag(e,"notification");if(!t.success)return t;var n=o("WASmaxParseUtils").assertAttr(e,"type","hosted");if(!n.success)return n;var s=o("WASmaxParseUtils").attrString(e,"b");return s.success?o("WAResultOrError").makeResult({b:s.value}):s}l.parseOffboardingNotificationRequest=e}),1); +"#; + let ir = extract_notif(bundle, "2.3000.test"); + assert!( + notif(&ir, "hosted").content.is_none(), + "an ambiguous type (two read-shapes) must stay degraded" + ); +} diff --git a/crates/wa-scan/src/content_length_index.rs b/crates/wa-scan/src/content_length_index.rs index 73c5026..6e6974a 100644 --- a/crates/wa-scan/src/content_length_index.rs +++ b/crates/wa-scan/src/content_length_index.rs @@ -86,8 +86,18 @@ pub(crate) fn build_pass(defs: &[ModuleDefinition], source: &str) -> ContentLeng } for (parent, tag, len) in collect_module(slice) { let key = (parent, tag); + // Keep the lexicographically-smallest module name as the provenance, not + // the first one *seen* — the scan order follows bundle file order, so a + // first-wins tie-break makes `byteLengthSource` depend on input ordering + // and breaks the byte-identical-output guarantee when two modules pin the + // same (parent,tag,len). source_module .entry(key.clone()) + .and_modify(|existing| { + if m.name < *existing { + *existing = m.name.clone(); + } + }) .or_insert_with(|| m.name.clone()); seen_lengths.entry(key).or_default().insert(len); } @@ -348,6 +358,20 @@ mod tests { assert_eq!(idx.get("product_list", "value"), None); } + #[test] + fn byte_length_source_is_order_independent() { + // Two modules pin the same (parent, tag, length); the recorded provenance must + // be the lexicographically-smallest module name regardless of bundle order, so + // the generated `byteLengthSource` is byte-identical across a reordering of the + // input bundles (the byte-identical-output guarantee). + let a = r#"__d("Aaa",[],function(g,r,d,o,e,i,l){ l.p = function(u){ return u.child("skey").child("value").contentBytes(32); };},1);"#; + let z = r#"__d("Zzz",[],function(g,r,d,o,e,i,l){ l.p = function(u){ return u.child("skey").child("value").contentBytes(32); };},1);"#; + let az = format!("{a}\n{z}"); + let za = format!("{z}\n{a}"); + assert_eq!(index(&az).get("skey", "value"), Some((32, "Aaa"))); + assert_eq!(index(&za).get("skey", "value"), Some((32, "Aaa"))); + } + #[test] fn contentuint_length_is_indexed() { let bundle = r#"__d("P",[],function(g,r,d,o,e,i,l){ diff --git a/crates/wa-scan/src/lib.rs b/crates/wa-scan/src/lib.rs index ca8b868..a9cb8b5 100644 --- a/crates/wa-scan/src/lib.rs +++ b/crates/wa-scan/src/lib.rs @@ -19,6 +19,7 @@ mod request; mod response; mod response_index; mod response_smax; +mod send_parser_index; mod srvreq; mod stanza; @@ -155,6 +156,12 @@ pub fn scan_iq_with_diagnostics( // can't (`0x00` and `0x30` are both one byte). let const_values = const_value_index::build_pass(module_defs, source); + // Build the send-site parser index once. A legacy job that hands a sibling + // module's parser to `deprecatedSendIq(iq, o("Mod").parser)` (rather than + // constructing one inline) would otherwise lose its typed response; this resolves + // the referenced parser, keyed by the sending module. + let send_parsers = send_parser_index::build_pass(module_defs, source); + let mut stanzas = Vec::new(); let mut unparseable = Vec::new(); let mut cross = CrossModuleStats::default(); @@ -199,6 +206,30 @@ pub fn scan_iq_with_diagnostics( // unambiguous even where the request nests them differently than the parser does). // A tag spanning namespaces (``: a key id in `encrypt`, a product id in // `w:biz:catalog`) is excluded, so it's never mislabeled. + // Attach a send-site parser to any request left with the `unknown` fallback + // (the module built an `` but defined no inline parser and had no smax + // response) — e.g. `WAWebQueryMediaConnsJob`'s `` response, parsed + // from `o("WAMediaConnParser").mediaConnParser`. The send-site index records one + // parser per module, so only attach when the module has a SINGLE unknown stanza: + // with two, we can't tell which `` the (first-found) send site accompanied, + // so both are left unknown rather than both guessing the same shape. + let mut unknown_per_module: std::collections::HashMap = + std::collections::HashMap::new(); + for s in &stanzas { + if s.response.parser_name == "unknown" { + *unknown_per_module.entry(s.module_name.clone()).or_default() += 1; + } + } + for s in &mut stanzas { + if s.response.parser_name == "unknown" + && unknown_per_module.get(&s.module_name) == Some(&1) + && let Some(resp) = send_parsers.get(&s.module_name) + { + s.response = resp.clone(); + s.parser_name = resp.parser_name.clone(); + } + } + let tag_fallback = single_namespace_content_tags(&stanzas); for s in &mut stanzas { // Pin constant leaf values first (`` → one `0x00` diff --git a/crates/wa-scan/src/response.rs b/crates/wa-scan/src/response.rs index 12adf9a..e97bd92 100644 --- a/crates/wa-scan/src/response.rs +++ b/crates/wa-scan/src/response.rs @@ -8,7 +8,7 @@ use std::collections::HashMap; use oxc_allocator::Allocator; -use oxc_ast::ast::{CallExpression, Expression, NewExpression, VariableDeclaration}; +use oxc_ast::ast::{Argument, CallExpression, Expression, NewExpression, VariableDeclaration}; use oxc_ast_visit::{Visit, walk}; use oxc_span::GetSpan; use wa_ir::wap; @@ -35,7 +35,7 @@ pub(crate) fn parsed_response_from_new_expr( if new_expr.arguments.len() < 2 { return None; } - let name = arg_expr(&new_expr.arguments[0]).and_then(as_string_lit)?; + let name = resolve_parser_name(&new_expr.arguments[0], source)?; let Some(Expression::FunctionExpression(cb)) = arg_expr(&new_expr.arguments[1]) else { return None; }; @@ -54,13 +54,69 @@ pub(crate) fn parsed_response_from_new_expr( let cb_body = &source[body.span.start as usize..body.span.end as usize]; let result = analyze_parser_ast(cb_body, param.as_str()); Some(ParsedResponse { - parser_name: name.to_string(), + parser_name: name, assertions: result.assertions, fields: result.fields, ..Default::default() }) } +/// The parser's name: the inline string literal (the common case), or — when the +/// constructor is `new WADeprecatedWapParser(d, fn)` with the name hoisted into a +/// variable (`d = "mexNotificationParser"`, as `WAWebHandleMexNotification` does) — +/// the string that variable is bound to in the module. Recovering the variable form +/// keeps the mex-notification envelope from being dropped for lack of an inline name. +fn resolve_parser_name(arg: &Argument, source: &str) -> Option { + let expr = arg_expr(arg)?; + if let Some(lit) = as_string_lit(expr) { + return Some(lit.to_string()); + } + resolve_string_binding(source, as_identifier(expr)?) +} + +/// The unique string literal bound to `name` via a `var name = "literal"` +/// declaration in `source`. Returns `None` when the name is unbound or bound to more +/// than one distinct string — an ambiguous name is not worth risking a wrong label. +fn resolve_string_binding(source: &str, name: &str) -> Option { + let alloc = Allocator::default(); + let ret = wa_oxc::parse_cjs(&alloc, source); + if ret.panicked { + return None; + } + let mut finder = StringBindingFinder { + name, + values: Vec::new(), + }; + finder.visit_program(&ret.program); + finder.values.sort(); + finder.values.dedup(); + match finder.values.as_slice() { + [only] => Some(only.clone()), + _ => None, + } +} + +/// Collects the string literals bound to `name` by `var name = "literal"` anywhere in +/// a module, so [`resolve_string_binding`] can require a unique value. +struct StringBindingFinder<'a> { + name: &'a str, + values: Vec, +} + +impl<'a> Visit<'a> for StringBindingFinder<'_> { + fn visit_variable_declaration(&mut self, decl: &VariableDeclaration<'a>) { + for d in &decl.declarations { + if let (Some(id), Some(init)) = (d.id.get_identifier_name(), d.init.as_ref()) + && id.as_str() == self.name + && let Some(lit) = as_string_lit(init) + { + self.values.push(lit.to_string()); + } + } + walk::walk_variable_declaration(self, decl); + } +} + /// Collect every `new WADeprecatedWapParser("name", fn)` in a module slice as a /// [`ParsedResponse`]. Used by non-IQ domains (e.g. notification handlers) to /// recover a stanza's typed content shape with the same parser-body analysis the @@ -727,4 +783,56 @@ mod tests { let r = analyze_parser_ast("{ this is not js ", "e"); assert!(r.fields.is_empty() && r.assertions.is_empty()); } + + #[test] + fn recovers_variable_named_parser() { + // `WAWebHandleMexNotification` hoists the parser name into a variable + // (`d = "mexNotificationParser", m = new WADeprecatedWapParser(d, fn)`); the + // name must be resolved from the binding or the parser is silently dropped. + let module = r#"__d("WAWebHandleMexNotification",["WADeprecatedWapParser"],(function(t,n,r,o,a,i,l){ + var d="mexNotificationParser",m=new(r("WADeprecatedWapParser"))(d,function(e){ + e.assertTag("notification"), e.assertAttr("type","mex"); + return { id: e.attrString("id") }; + }); + }), 1);"#; + let parsers = parse_module_wap_parsers(module); + assert_eq!( + parsers.len(), + 1, + "variable-named parser should be recovered" + ); + assert_eq!(parsers[0].parser_name, "mexNotificationParser"); + assert!(parsers[0].fields.iter().any(|f| f.name == "id")); + } + + #[test] + fn ambiguous_variable_name_is_not_guessed() { + // The same short name bound to two different strings → don't risk a wrong + // label; the parser is left unnamed (dropped) rather than mislabeled. + let module = r#"__d("M",["WADeprecatedWapParser"],(function(t,n,r,o,a,i,l){ + var d="one"; + var d="two"; + var m=new(r("WADeprecatedWapParser"))(d,function(e){ return { id: e.attrString("id") }; }); + }), 1);"#; + assert!(parse_module_wap_parsers(module).is_empty()); + } + + #[test] + fn variable_name_reused_in_another_function_is_not_mislabeled() { + // The binding lookup is module-wide, so a short name reused in an unrelated + // function with a DIFFERENT value makes it ambiguous. The uniqueness guard + // then refuses to guess (the parser is dropped) — the point being that a + // reused name can never attach the WRONG label, only degrade to none. + let module = r#"__d("M",["WADeprecatedWapParser"],(function(t,n,r,o,a,i,l){ + function build(){ var d="mexNotificationParser"; return new(r("WADeprecatedWapParser"))(d,function(e){ return { id: e.attrString("id") }; }); } + function other(){ var d="unrelatedLabel"; return d; } + l.p=build(), l.o=other; + }), 1);"#; + // Two distinct bindings for `d` → ambiguous → the parser is dropped entirely + // (not attached with either label). Asserting emptiness — rather than just the + // absence of the wrong name — keeps this from passing vacuously. + let parsers = parse_module_wap_parsers(module); + assert!(parsers.is_empty()); + assert!(parsers.iter().all(|p| p.parser_name != "unrelatedLabel")); + } } diff --git a/crates/wa-scan/src/response_smax.rs b/crates/wa-scan/src/response_smax.rs index d3a7089..8e9675c 100644 --- a/crates/wa-scan/src/response_smax.rs +++ b/crates/wa-scan/src/response_smax.rs @@ -191,6 +191,10 @@ enum Binding { field_type: ParsedFieldType, required: bool, byte_length: Option, + /// Inclusive byte-length bounds from a `contentBytesRange(node, min, max)` when + /// `min != max` (a payload-size limit). The `min == max` case is a fixed length + /// and is carried in `byte_length` instead, so the two are mutually exclusive. + byte_range: Option<(u32, u32)>, /// Inclusive integer bounds from an `attrIntRange(node, name, min, max)`, when /// the accessor was a range check (and not the timestamp-marker range, which is /// surfaced as `field_type: Timestamp` instead). @@ -442,6 +446,7 @@ fn classify_call( field_type: ft, required: true, byte_length: bl, + byte_range: None, int_range: None, wire_name, source_path, @@ -472,6 +477,7 @@ fn classify_call( field_type: ParsedFieldType::Bytes, required: true, byte_length: None, + byte_range: None, int_range: None, wire_name: None, source_path, @@ -482,6 +488,7 @@ fn classify_call( field_type: ParsedFieldType::String, required: false, byte_length: None, + byte_range: None, int_range: None, wire_name, source_path, @@ -496,11 +503,13 @@ fn classify_call( match inner.and_then(normalize_accessor) { Some((m, ft, bl)) => { let (field_type, int_range) = int_range_and_type(inner, args, ft); + let (cbl, byte_range) = content_byte_length(inner, args); Binding::Field { method: optional_variant(&m), field_type, required: false, - byte_length: bl.or_else(|| content_byte_length(inner, args)), + byte_length: bl.or(cbl), + byte_range, int_range, wire_name, source_path, @@ -522,11 +531,13 @@ fn classify_call( other => match normalize_accessor(other) { Some((m, ft, bl)) => { let (field_type, int_range) = int_range_and_type(Some(other), args, ft); + let (cbl, byte_range) = content_byte_length(Some(other), args); Binding::Field { method: m, field_type, required: true, - byte_length: bl.or_else(|| content_byte_length(Some(other), args)), + byte_length: bl.or(cbl), + byte_range, int_range, wire_name, source_path, @@ -688,18 +699,28 @@ fn int_range_and_type( } } -/// The fixed byte length pinned by a `contentBytesRange(node, min, max)` when `min == -/// max` — a hard wire-contract length (a 32-byte key, a 64-byte signature). A true -/// range (`min != max`) is a max-payload size limit, not a fixed length, so it yields -/// `None` (never a bogus `byteLength`). `contentBytes(node)` carries no length at all. -fn content_byte_length(accessor: Option<&str>, args: &[Argument]) -> Option { +/// Interpret a `contentBytesRange(node, min, max)` accessor, returning +/// `(fixed_length, range)`: +/// - `min == max` → a hard wire-contract length (a 32-byte key, a 64-byte signature) +/// as `(Some(len), None)`; +/// - `min != max` → a max-payload size *limit* (a media buffer 1..1048576, a token +/// 1..128) as `(None, Some((min, max)))` — previously this was silently dropped; +/// - any other accessor (`contentBytes(node)` carries no length) → `(None, None)`. +fn content_byte_length( + accessor: Option<&str>, + args: &[Argument], +) -> (Option, Option<(u32, u32)>) { if accessor != Some("contentBytesRange") { - return None; + return (None, None); } let mut nums = args.iter().filter_map(|a| arg_expr(a).and_then(as_int)); match (nums.next(), nums.next()) { - (Some(min), Some(max)) if min == max => u32::try_from(min).ok(), - _ => None, + (Some(min), Some(max)) if min == max => (u32::try_from(min).ok(), None), + (Some(min), Some(max)) => match (u32::try_from(min), u32::try_from(max)) { + (Ok(min), Ok(max)) => (None, Some((min, max))), + _ => (None, None), + }, + _ => (None, None), } } @@ -1025,6 +1046,7 @@ fn collect_object_fields( field_type, required, byte_length, + byte_range, int_range, wire_name, source_path, @@ -1033,6 +1055,10 @@ fn collect_object_fields( Some((min, max)) => (Some(*min), Some(*max)), None => (None, None), }; + let (byte_min, byte_max) = match byte_range { + Some((min, max)) => (Some(*min), Some(*max)), + None => (None, None), + }; out.push(ParsedField { method: method.clone(), name: key.to_string(), @@ -1040,6 +1066,8 @@ fn collect_object_fields( field_type: *field_type, required: *required && !optional, byte_length: *byte_length, + byte_min, + byte_max, int_min, int_max, source_path: source_path.clone(), @@ -1521,14 +1549,15 @@ mod tests { } #[test] - fn content_bytes_range_pins_fixed_length_only() { + fn content_bytes_range_pins_fixed_length_and_captures_range_bounds() { let module = r#"__d("WASmaxInBazResponseSuccess",["WASmaxParseUtils","WAResultOrError"],(function(t,n,r,o,a,i,l){ function s(t){ var r = o("WASmaxParseUtils").assertTag(t, "iq"); if(!r.success) return r; var a = o("WASmaxParseUtils").contentBytesRange(t, 32, 32); if(!a.success) return a; var b = o("WASmaxParseUtils").contentBytesRange(t, 1, 1048576); if(!b.success) return b; var c = o("WASmaxParseUtils").contentBytes(t); if(!c.success) return c; - return o("WAResultOrError").makeResult({ key: a.value, blob: b.value, raw: c.value }); + var d = o("WASmaxParseUtils").optional(o("WASmaxParseUtils").contentBytesRange, t, 1, 128); + return o("WAResultOrError").makeResult({ key: a.value, blob: b.value, raw: c.value, tok: d.value }); } l.parseBazResponseSuccess = s; }), 1);"#; @@ -1538,14 +1567,24 @@ mod tests { .find(|(n, _)| n == "parseBazResponseSuccess") .expect("parser"); let field = |name: &str| pr.fields.iter().find(|f| f.name == name).expect(name); - // Fixed range (min == max) → a pinned wire length. + // Fixed range (min == max) → a pinned wire length, no range bounds. let key = field("key"); assert_eq!(key.field_type, ParsedFieldType::Bytes); assert_eq!(key.byte_length, Some(32)); - // A true range is a max-size limit, not a fixed length → no byteLength. - assert_eq!(field("blob").byte_length, None); - // Plain contentBytes carries no length. - assert_eq!(field("raw").byte_length, None); + assert_eq!((key.byte_min, key.byte_max), (None, None)); + // A true range is a max-size limit, not a fixed length → the bounds are kept + // as byteMin/byteMax (previously silently dropped), with no bogus byteLength. + let blob = field("blob"); + assert_eq!(blob.byte_length, None); + assert_eq!((blob.byte_min, blob.byte_max), (Some(1), Some(1048576))); + // Plain contentBytes carries neither a length nor bounds. + let raw = field("raw"); + assert_eq!(raw.byte_length, None); + assert_eq!((raw.byte_min, raw.byte_max), (None, None)); + // `optional(contentBytesRange, …)` still captures the bounds and stays optional. + let tok = field("tok"); + assert_eq!((tok.byte_min, tok.byte_max), (Some(1), Some(128))); + assert!(!tok.required); } #[test] diff --git a/crates/wa-scan/src/send_parser_index.rs b/crates/wa-scan/src/send_parser_index.rs new file mode 100644 index 0000000..a57918c --- /dev/null +++ b/crates/wa-scan/src/send_parser_index.rs @@ -0,0 +1,203 @@ +//! Cross-module index of legacy responses attached at the *send* site. +//! +//! A few legacy jobs build an `` and hand a sibling module's parser to +//! `deprecatedSendIq(iq, o("Mod").parser)` instead of constructing a +//! `WADeprecatedWapParser` inline. The per-module scanner only sees inline parsers, +//! so those responses would be lost (the request is captured with `parserName: +//! "unknown"`). This pass finds each send site whose parser reference is an external +//! `o("Mod").parser`, resolves the target module's single legacy parser to its typed +//! [`ParsedResponse`], and keys it by the *sending* module so the scanner can attach +//! it. +//! +//! Only the same-module case (the send site lives in the module that built the +//! ``) is resolved here — e.g. `WAWebQueryMediaConnsJob` sends its own `` +//! with `o("WAMediaConnParser").mediaConnParser`. A send site in a *different* module +//! from the one that built the `` (the iq expression is threaded across a module +//! boundary) is left for the scanner's `unknown` fallback. + +use std::collections::{BTreeMap, HashMap}; + +use oxc_allocator::Allocator; +use oxc_ast::ast::{CallExpression, Expression}; +use oxc_ast_visit::{Visit, walk}; +use wa_ir::ParsedResponse; +use wa_oxc::{arg_expr, as_call, as_member, first_string_arg}; +use wa_transform::ModuleDefinition; + +use crate::response::parse_module_wap_parsers; + +/// The `deprecatedSendIq` export method name. +const SEND_IQ: &str = "deprecatedSendIq"; + +/// The module the `deprecatedSendIq` helper is exported from — the provenance a +/// send call's callee must trace back to (so a same-named local helper isn't mistaken +/// for it). +const SEND_IQ_MODULE: &str = "WADeprecatedSendIq"; + +/// Sending module name → the typed response resolved from its `deprecatedSendIq` +/// parser reference. A `BTreeMap` keeps the build deterministic. +pub(crate) fn build_pass( + defs: &[ModuleDefinition], + source: &str, +) -> BTreeMap { + // module name → its source slice, for resolving a referenced target module. + let slices: HashMap<&str, &str> = defs + .iter() + .map(|m| (m.name.as_str(), &source[m.start..m.end])) + .collect(); + + let mut out = BTreeMap::new(); + let mut seen = std::collections::HashSet::new(); + for m in defs { + // Same module can appear in several bundle shards; resolve each name once. + if !seen.insert(m.name.as_str()) { + continue; + } + let slice = &source[m.start..m.end]; + // Cheap pre-filter: only re-parse modules that actually send an iq. + if !slice.contains(SEND_IQ) { + continue; + } + let Some(target) = find_send_parser_ref(slice) else { + continue; + }; + let Some(target_slice) = slices.get(target.as_str()) else { + continue; + }; + // Resolve the target's legacy parser, but only when it is unambiguous — a + // module with two parsers can't be keyed by an export name we don't track, + // so we leave it to the `unknown` fallback rather than guess. + if let [only] = parse_module_wap_parsers(target_slice).as_slice() { + out.insert(m.name.clone(), only.clone()); + } + } + out +} + +/// The target module name in the first `deprecatedSendIq(iq, o("Mod").parser)` call +/// in `slice` whose parser argument is an external `o("Mod").member` reference. +fn find_send_parser_ref(slice: &str) -> Option { + let alloc = Allocator::default(); + let ret = wa_oxc::parse_cjs(&alloc, slice); + if ret.panicked { + return None; + } + let mut finder = SendFinder { target: None }; + finder.visit_program(&ret.program); + finder.target +} + +/// Whether `e` is the `deprecatedSendIq` helper reference +/// `o("WADeprecatedSendIq").deprecatedSendIq` (a static member whose object requires +/// the [`SEND_IQ_MODULE`]) — the provenance that qualifies a call as a real send. +fn is_send_iq_ref(e: &Expression) -> bool { + matches!(as_member(e), Some((obj, prop)) + if prop == SEND_IQ + && as_call(obj).and_then(first_string_arg) == Some(SEND_IQ_MODULE)) +} + +struct SendFinder { + target: Option, +} + +impl<'a> Visit<'a> for SendFinder { + fn visit_call_expression(&mut self, call: &CallExpression<'a>) { + // Only the inline member form `o("WADeprecatedSendIq").deprecatedSendIq(…)`, + // whose provenance is verified on the spot. A bare `(…)` alias is + // intentionally NOT matched: it doesn't occur in the bundle (all send sites use + // the member form), and resolving it soundly would require per-lexical-scope + // alias tracking — machinery with no real coverage to justify it. + if self.target.is_none() + && is_send_iq_ref(&call.callee) + && let Some(arg1) = call.arguments.get(1).and_then(arg_expr) + // The parser reference is a static member `o("Mod").parser`. + && let Some((obj, _parser)) = as_member(arg1) + // …whose object is a `require("Mod")` call. + && let Some(require_call) = as_call(obj) + && let Some(module) = first_string_arg(require_call) + { + self.target = Some(module.to_string()); + } + walk::walk_call_expression(self, call); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn index(bundle: &str) -> BTreeMap { + let defs = wa_transform::extract_module_definitions(bundle); + build_pass(&defs, bundle) + } + + #[test] + fn resolves_same_module_send_site_parser() { + // `WAWebQueryMediaConnsJob` builds an `` and sends it with a sibling + // module's parser; the response must be recovered from `WAMediaConnParser`. + let bundle = r#" + __d("WAWebQueryMediaConnsJob",["WADeprecatedSendIq","WAMediaConnParser"],(function(t,n,r,o,a,i,l){ + l.query=function(){ + var m=o("WAWap").wap("iq",{xmlns:"w:m",type:"set"}); + return o("WADeprecatedSendIq").deprecatedSendIq(m,o("WAMediaConnParser").mediaConnParser); + }; + }),1); + __d("WAMediaConnParser",["WADeprecatedWapParser"],(function(t,n,r,o,a,i,l){ + l.mediaConnParser=new(r("WADeprecatedWapParser"))("mediaConnParser",function(e){ + e.assertTag("iq"); + return { auth: e.child("media_conn").attrString("auth") }; + }); + }),1); + "#; + let idx = index(bundle); + let resp = idx + .get("WAWebQueryMediaConnsJob") + .expect("send-site parser resolved"); + assert_eq!(resp.parser_name, "mediaConnParser"); + // The `` child is recovered (its `auth` attr nests inside it). + assert!(resp.fields.iter().any(|f| f.name == "media_conn")); + } + + #[test] + fn bare_identifier_send_call_is_not_matched() { + // Only the inline member form is matched. A bare `(…)` call — even one + // aliased from the real helper — is intentionally left unresolved (it doesn't + // occur in the bundle and sound resolution would need per-scope alias tracking). + let bundle = r#" + __d("Job",["WADeprecatedSendIq","P"],(function(t,n,r,o,a,i,l){ + var send=o("WADeprecatedSendIq").deprecatedSendIq; + l.query=function(){ var m=o("WAWap").wap("iq",{}); return send(m,o("P").theParser); }; + }),1); + __d("P",["WADeprecatedWapParser"],(function(t,n,r,o,a,i,l){ + l.theParser=new(r("WADeprecatedWapParser"))("theParser",function(e){ return { id: e.attrString("id") }; }); + }),1); + "#; + assert!(!index(bundle).contains_key("Job")); + } + + #[test] + fn send_site_without_module_ref_parser_is_ignored() { + // A `deprecatedSendIq` whose parser argument is not an `o("Mod").parser` + // reference (here: absent) yields no entry. + let bundle = r#"__d("J",["WADeprecatedSendIq"],(function(t,n,r,o,a,i,l){ + l.q=function(){ var m=o("WAWap").wap("iq",{}); return o("WADeprecatedSendIq").deprecatedSendIq(m); }; + }),1);"#; + assert!(!index(bundle).contains_key("J")); + } + + #[test] + fn ambiguous_target_module_is_not_guessed() { + // The target module defines two parsers; without an export→parser mapping the + // reference can't be resolved unambiguously, so nothing is attached. + let bundle = r#" + __d("J",["WADeprecatedSendIq","Two"],(function(t,n,r,o,a,i,l){ + l.q=function(){ var m=o("WAWap").wap("iq",{}); return o("WADeprecatedSendIq").deprecatedSendIq(m,o("Two").a); }; + }),1); + __d("Two",["WADeprecatedWapParser"],(function(t,n,r,o,a,i,l){ + l.a=new(r("WADeprecatedWapParser"))("pa",function(e){ return { x: e.attrString("x") }; }); + l.b=new(r("WADeprecatedWapParser"))("pb",function(e){ return { y: e.attrString("y") }; }); + }),1); + "#; + assert!(!index(bundle).contains_key("J")); + } +} diff --git a/generated/iq/index.json b/generated/iq/index.json index 73f83c6..257a636 100644 --- a/generated/iq/index.json +++ b/generated/iq/index.json @@ -124,7 +124,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 100 } ], "repeats": false @@ -305,7 +307,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 100 } ], "repeats": false @@ -4015,6 +4019,8 @@ "name": "paddingElementValue", "type": "bytes", "required": true, + "byteMin": 0, + "byteMax": 524288, "sourcePath": [ "padding" ] @@ -4443,6 +4449,8 @@ "name": "paddingElementValue", "type": "bytes", "required": true, + "byteMin": 0, + "byteMax": 524288, "sourcePath": [ "padding" ] @@ -5145,6 +5153,8 @@ "name": "paddingElementValue", "type": "bytes", "required": true, + "byteMin": 0, + "byteMax": 524288, "sourcePath": [ "padding" ] @@ -5547,6 +5557,8 @@ "name": "paddingElementValue", "type": "bytes", "required": true, + "byteMin": 0, + "byteMax": 524288, "sourcePath": [ "padding" ] @@ -6244,7 +6256,7 @@ "content": { "kind": "bytes", "byteLength": 32, - "byteLengthSource": "parse:WAWebRetryRequestParser" + "byteLengthSource": "parse:WAWebDigestKeyJob" }, "repeats": false }, @@ -6255,7 +6267,7 @@ "content": { "kind": "bytes", "byteLength": 64, - "byteLengthSource": "parse:WAWebRetryRequestParser" + "byteLengthSource": "parse:WAWebDigestKeyJob" }, "repeats": false } @@ -7492,7 +7504,7 @@ "content": { "kind": "bytes", "byteLength": 32, - "byteLengthSource": "parse:WAWebRetryRequestParser" + "byteLengthSource": "parse:WAWebDigestKeyJob" }, "repeats": false }, @@ -7503,7 +7515,7 @@ "content": { "kind": "bytes", "byteLength": 64, - "byteLengthSource": "parse:WAWebRetryRequestParser" + "byteLengthSource": "parse:WAWebDigestKeyJob" }, "repeats": false } @@ -7631,7 +7643,7 @@ "content": { "kind": "bytes", "byteLength": 32, - "byteLengthSource": "parse:WAWebRetryRequestParser" + "byteLengthSource": "parse:WAWebDigestKeyJob" }, "repeats": false }, @@ -7642,7 +7654,7 @@ "content": { "kind": "bytes", "byteLength": 64, - "byteLengthSource": "parse:WAWebRetryRequestParser" + "byteLengthSource": "parse:WAWebDigestKeyJob" }, "repeats": false } @@ -11728,7 +11740,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 4096 } ], "repeats": false @@ -11769,7 +11783,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 4096 } ], "repeats": false @@ -13007,7 +13023,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -13075,7 +13093,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -13254,7 +13274,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -13322,7 +13344,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -13428,7 +13452,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -13496,7 +13522,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -13645,7 +13673,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -13713,7 +13743,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -13873,7 +13905,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -13941,7 +13975,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -14037,7 +14073,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -14115,7 +14153,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -14193,7 +14233,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -15073,7 +15115,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -15141,7 +15185,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -15320,7 +15366,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -15388,7 +15436,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -15494,7 +15544,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -15562,7 +15614,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -15711,7 +15765,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -15779,7 +15835,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -15939,7 +15997,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -16007,7 +16067,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -16103,7 +16165,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -16181,7 +16245,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -16259,7 +16325,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -17547,7 +17615,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -17615,7 +17685,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -17794,7 +17866,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -17862,7 +17936,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -17968,7 +18044,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -18036,7 +18114,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -18185,7 +18265,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -18253,7 +18335,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -18413,7 +18497,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -18481,7 +18567,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -18577,7 +18665,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -18655,7 +18745,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -18733,7 +18825,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -19617,7 +19711,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -19685,7 +19781,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -19864,7 +19962,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -19932,7 +20032,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -20038,7 +20140,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -20106,7 +20210,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -20255,7 +20361,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -20323,7 +20431,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -20483,7 +20593,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -20551,7 +20663,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -20647,7 +20761,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -20725,7 +20841,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -20803,7 +20921,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -21910,7 +22030,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -22102,7 +22224,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -22716,7 +22840,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -22765,7 +22891,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -23224,7 +23352,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -23273,7 +23403,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -24140,7 +24272,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -24189,7 +24323,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -24652,7 +24788,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -24701,7 +24839,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -75210,7 +75350,7 @@ "namespace": "w:m", "iqType": "set", "target": "s.whatsapp.net", - "parserName": "unknown", + "parserName": "mediaConnParser", "exportedFunction": "mapParsedMediaConn", "allExports": [ "mapParsedMediaConn", @@ -75230,9 +75370,43 @@ ] }, "response": { - "parserName": "unknown", + "parserName": "mediaConnParser", "assertions": [], - "fields": [] + "fields": [ + { + "method": "child", + "name": "media_conn", + "type": "string", + "required": true, + "tag": "media_conn", + "children": [ + { + "method": "attrString", + "name": "auth", + "type": "string", + "required": true + }, + { + "method": "attrInt", + "name": "max_buckets", + "type": "integer", + "required": true + }, + { + "method": "maybeAttrInt", + "name": "max_manual_retry", + "type": "integer", + "required": false + }, + { + "method": "maybeAttrInt", + "name": "max_auto_download_retry", + "type": "integer", + "required": false + } + ] + } + ] } }, { @@ -76906,6 +77080,8 @@ "name": "encryptedKeyElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 2048, "sourcePath": [ "encrypted_key" ] @@ -76915,6 +77091,8 @@ "name": "nonceElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "nonce" ] @@ -76924,6 +77102,8 @@ "name": "encryptedDataElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 8192, "sourcePath": [ "encrypted_data" ] @@ -76933,6 +77113,8 @@ "name": "authTagElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "auth_tag" ] @@ -77007,6 +77189,8 @@ "name": "encryptedKeyElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 2048, "sourcePath": [ "encrypted_key" ] @@ -77016,6 +77200,8 @@ "name": "nonceElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "nonce" ] @@ -77025,6 +77211,8 @@ "name": "encryptedDataElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 8192, "sourcePath": [ "encrypted_data" ] @@ -77034,6 +77222,8 @@ "name": "authTagElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "auth_tag" ] @@ -78234,6 +78424,8 @@ "name": "encryptedKeyElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 2048, "sourcePath": [ "encrypted_key" ] @@ -78243,6 +78435,8 @@ "name": "nonceElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "nonce" ] @@ -78252,6 +78446,8 @@ "name": "encryptedDataElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 8192, "sourcePath": [ "encrypted_data" ] @@ -78261,6 +78457,8 @@ "name": "authTagElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "auth_tag" ] @@ -78328,6 +78526,8 @@ "name": "encryptedKeyElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 2048, "sourcePath": [ "encrypted_key" ] @@ -78337,6 +78537,8 @@ "name": "nonceElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "nonce" ] @@ -78346,6 +78548,8 @@ "name": "encryptedDataElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 8192, "sourcePath": [ "encrypted_data" ] @@ -78355,6 +78559,8 @@ "name": "authTagElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "auth_tag" ] @@ -78886,6 +79092,8 @@ "name": "encryptedKeyElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 2048, "sourcePath": [ "encrypted_key" ] @@ -78895,6 +79103,8 @@ "name": "nonceElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "nonce" ] @@ -78904,6 +79114,8 @@ "name": "encryptedDataElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 8192, "sourcePath": [ "encrypted_data" ] @@ -78913,6 +79125,8 @@ "name": "authTagElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "auth_tag" ] @@ -78971,6 +79185,8 @@ "name": "encryptedKeyElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 2048, "sourcePath": [ "encrypted_key" ] @@ -78980,6 +79196,8 @@ "name": "nonceElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "nonce" ] @@ -78989,6 +79207,8 @@ "name": "encryptedDataElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 8192, "sourcePath": [ "encrypted_data" ] @@ -78998,6 +79218,8 @@ "name": "authTagElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "auth_tag" ] @@ -79456,7 +79678,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 4092 } ], "repeats": false @@ -79479,7 +79703,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 4092 } ], "repeats": false @@ -79509,7 +79735,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 4092 } ], "repeats": false @@ -79574,7 +79802,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 4092 } ], "repeats": false @@ -79597,7 +79827,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 4092 } ], "repeats": false @@ -79627,7 +79859,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 4092 } ], "repeats": false @@ -79958,6 +80192,8 @@ "name": "encryptedKeyElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 2048, "sourcePath": [ "encrypted_key" ] @@ -79967,6 +80203,8 @@ "name": "nonceElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "nonce" ] @@ -79976,6 +80214,8 @@ "name": "encryptedDataElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 8192, "sourcePath": [ "encrypted_data" ] @@ -79985,6 +80225,8 @@ "name": "authTagElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "auth_tag" ] @@ -80043,6 +80285,8 @@ "name": "encryptedKeyElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 2048, "sourcePath": [ "encrypted_key" ] @@ -80052,6 +80296,8 @@ "name": "nonceElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "nonce" ] @@ -80061,6 +80307,8 @@ "name": "encryptedDataElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 8192, "sourcePath": [ "encrypted_data" ] @@ -80070,6 +80318,8 @@ "name": "authTagElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "auth_tag" ] diff --git a/generated/manifest.json b/generated/manifest.json index 892e861..e818c4f 100644 --- a/generated/manifest.json +++ b/generated/manifest.json @@ -9,20 +9,20 @@ "requestsEnriched": 11, "requestsWithMixins": 108 }, - "degradedResponses": 19, + "degradedResponses": 18, "dropsByReason": { "iq builder substring present but no AST iq call": 5, "mixin fragment (folded into requests, not a standalone stanza)": 56, "namespace/type unresolved (dropped by guard)": 2 }, "stanzas": 159, - "typedResponses": 140, + "typedResponses": 141, "unparseable": 63 }, "notif": { - "degraded": 9, + "degraded": 5, "stanzaTags": 13, - "typedContent": 18, + "typedContent": 22, "types": 27 } }, @@ -50,7 +50,7 @@ "iq": { "file": "iq/index.json", "schema": "schema/iq.schema.json", - "sha256": "7f4f6e2666e69affee79965d5e670445669cffc0c70ab9af6e845c859e3c7226" + "sha256": "3b6d45ea11b29bc38b0720f32e6eeb803e74d53ee2f27166a17c863ae8501483" }, "mex": { "file": "mex/index.json", @@ -60,7 +60,7 @@ "notif": { "file": "notif/index.json", "schema": "schema/notif.schema.json", - "sha256": "b05a6fde725e6d6ec824a304a885dca4751c5c9a8a6e2667e00a888d3a87c6bb" + "sha256": "74e564b675d215285ba0f89dd8d862c15752922b9beb407fbdea8f074244301e" }, "proto": { "file": "proto/WAProto.proto", @@ -69,7 +69,7 @@ "srvreq": { "file": "srvreq/index.json", "schema": "schema/srvreq.schema.json", - "sha256": "03e96c1fd1209d662c7c858de2187e343182012abf8a693c64a4ac118817f158" + "sha256": "47668a3c9414924b1bb96cc4e519f70fee7c58e0c71cce8461f5441281b6d6a5" }, "stanza": { "file": "stanza/index.json", diff --git a/generated/notif/index.json b/generated/notif/index.json index c2ae05a..e3aea7e 100644 --- a/generated/notif/index.json +++ b/generated/notif/index.json @@ -623,7 +623,69 @@ { "type": "crsc_continuation", "handlerModule": "WAWebShortcakeLinkingHandleNotification", - "handlerFunction": "handleShortcakeLinkingNotification" + "handlerFunction": "handleShortcakeLinkingNotification", + "content": { + "parserName": "parseSetPrimaryEphemeralIdentityNotificationRequest", + "assertions": [ + { + "kind": "tag", + "name": "notification" + }, + { + "kind": "attr", + "name": "type", + "value": "crsc_continuation" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "required": true + }, + { + "method": "attrJidWithType", + "name": "from", + "wireName": "from", + "type": "jid", + "required": true + }, + { + "method": "contentBytes", + "name": "primaryEphemeralIdentityElementValue", + "type": "bytes", + "required": true, + "sourcePath": [ + "primary_ephemeral_identity" + ] + }, + { + "method": "attrInt", + "name": "t", + "wireName": "t", + "type": "integer", + "required": true + }, + { + "method": "attrString", + "name": "id", + "wireName": "id", + "type": "string", + "required": true + }, + { + "method": "maybeAttrInt", + "name": "offline", + "wireName": "offline", + "type": "integer", + "required": false, + "intMin": 0, + "intMax": 1024 + } + ] + } }, { "type": "devices", @@ -905,7 +967,63 @@ { "type": "mex", "handlerModule": "WAWebHandleMexNotification", - "handlerFunction": "handleMexNotification" + "handlerFunction": "handleMexNotification", + "content": { + "parserName": "mexNotificationParser", + "assertions": [ + { + "kind": "tag", + "name": "notification" + }, + { + "kind": "attr", + "name": "type", + "value": "mex" + } + ], + "fields": [ + { + "method": "child", + "name": "update", + "type": "string", + "required": true, + "tag": "update", + "children": [ + { + "method": "attrString", + "name": "op_name", + "type": "string", + "required": true + }, + { + "method": "contentString", + "name": "content", + "type": "string", + "required": true + } + ], + "contentType": "string" + }, + { + "method": "attrString", + "name": "id", + "type": "string", + "required": true + }, + { + "method": "attrWapJid", + "name": "from", + "type": "jid", + "required": true + }, + { + "method": "maybeAttrString", + "name": "offline", + "type": "string", + "required": false + } + ] + } }, { "type": "newsletter", @@ -926,7 +1044,78 @@ { "type": "passkey_prologue_request", "handlerModule": "WAWebShortcakeLinkingHandlePasskeyPrologueRequest", - "handlerFunction": "handlePasskeyPrologueRequestNotification" + "handlerFunction": "handlePasskeyPrologueRequestNotification", + "content": { + "parserName": "parsePasskeyPrologueRequestNotificationRequest", + "assertions": [ + { + "kind": "tag", + "name": "notification" + }, + { + "kind": "attr", + "name": "type", + "value": "passkey_prologue_request" + } + ], + "fields": [ + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "required": true + }, + { + "method": "attrJidWithType", + "name": "from", + "wireName": "from", + "type": "jid", + "required": true + }, + { + "method": "attrInt", + "name": "t", + "wireName": "t", + "type": "integer", + "required": true + }, + { + "method": "attrString", + "name": "id", + "wireName": "id", + "type": "string", + "required": true + }, + { + "method": "maybeAttrInt", + "name": "offline", + "wireName": "offline", + "type": "integer", + "required": false, + "intMin": 0, + "intMax": 1024 + }, + { + "method": "child", + "name": "passkeyRequestOptions", + "type": "string", + "required": false, + "tag": "passkey_request_options", + "children": [ + { + "method": "contentBytes", + "name": "elementValue", + "type": "bytes", + "required": true, + "byteMin": 1, + "byteMax": 4096 + } + ], + "repeats": false + } + ] + } }, { "type": "pay", @@ -1618,7 +1807,124 @@ { "type": "waffle", "handlerModule": "WAWebAccountLinkingNotificationHandler", - "handlerFunction": "handleAccountLinkingNotification" + "handlerFunction": "handleAccountLinkingNotification", + "content": { + "parserName": "parseWFNotificationRequest", + "assertions": [ + { + "kind": "tag", + "name": "notification" + }, + { + "kind": "attr", + "name": "type", + "value": "waffle" + } + ], + "fields": [ + { + "method": "attrJidWithType", + "name": "from", + "wireName": "from", + "type": "jid", + "required": true + }, + { + "method": "attrString", + "name": "type", + "wireName": "type", + "type": "string", + "required": true + }, + { + "method": "attrInt", + "name": "notificationMetadataEvent", + "wireName": "event", + "type": "integer", + "required": true, + "intMin": 1, + "intMax": 7, + "sourcePath": [ + "notification_metadata" + ] + }, + { + "method": "maybeAttrEnum", + "name": "notificationMetadataShowUserNotif", + "wireName": "show_user_notif", + "type": "enum", + "required": false, + "sourcePath": [ + "notification_metadata" + ] + }, + { + "method": "maybeAttrInt", + "name": "notificationMetadataType", + "wireName": "type", + "type": "integer", + "required": false, + "intMin": 0, + "intMax": 0, + "sourcePath": [ + "notification_metadata" + ] + }, + { + "method": "maybeAttrEnum", + "name": "notificationMetadataClientResync", + "wireName": "client_resync", + "type": "enum", + "required": false, + "sourcePath": [ + "notification_metadata" + ] + }, + { + "method": "maybeAttrInt", + "name": "notificationMetadataSyncDelay", + "wireName": "sync_delay", + "type": "integer", + "required": false, + "sourcePath": [ + "notification_metadata" + ] + }, + { + "method": "maybeAttrEnum", + "name": "notificationMetadataNpr", + "wireName": "npr", + "type": "enum", + "required": false, + "sourcePath": [ + "notification_metadata" + ] + }, + { + "method": "attrInt", + "name": "t", + "wireName": "t", + "type": "integer", + "required": true + }, + { + "method": "attrString", + "name": "id", + "wireName": "id", + "type": "string", + "required": true + }, + { + "method": "maybeAttrInt", + "name": "offline", + "wireName": "offline", + "type": "integer", + "required": false, + "intMin": 0, + "intMax": 1024 + } + ] + } } ] } diff --git a/generated/schema/incoming.schema.json b/generated/schema/incoming.schema.json index 6469b5e..5f780a1 100644 --- a/generated/schema/incoming.schema.json +++ b/generated/schema/incoming.schema.json @@ -97,6 +97,24 @@ "format": "uint32", "minimum": 0 }, + "byteMax": { + "description": "Inclusive upper bound from the same `contentBytesRange` check (see [`byte_min`]).\n\n[`byte_min`]: ParsedField::byte_min", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "byteMin": { + "description": "Inclusive lower bound on the byte-content length, enforced via\n`contentBytesRange(node, min, max)` when `min != max` — a payload-size *limit*\n(a media buffer capped at 1 MiB, a token capped at 128 bytes), as opposed to a\nfixed [`byte_length`] (the `min == max` case). Present only for a\n[`ParsedFieldType::Bytes`] field with a true range; a consumer can enforce the\nbound rather than guessing an unbounded buffer.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, "children": { "type": [ "array", diff --git a/generated/schema/iq.schema.json b/generated/schema/iq.schema.json index a351f4a..405d867 100644 --- a/generated/schema/iq.schema.json +++ b/generated/schema/iq.schema.json @@ -204,6 +204,24 @@ "format": "uint32", "minimum": 0 }, + "byteMax": { + "description": "Inclusive upper bound from the same `contentBytesRange` check (see [`byte_min`]).\n\n[`byte_min`]: ParsedField::byte_min", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "byteMin": { + "description": "Inclusive lower bound on the byte-content length, enforced via\n`contentBytesRange(node, min, max)` when `min != max` — a payload-size *limit*\n(a media buffer capped at 1 MiB, a token capped at 128 bytes), as opposed to a\nfixed [`byte_length`] (the `min == max` case). Present only for a\n[`ParsedFieldType::Bytes`] field with a true range; a consumer can enforce the\nbound rather than guessing an unbounded buffer.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, "children": { "type": [ "array", diff --git a/generated/schema/notif.schema.json b/generated/schema/notif.schema.json index 17477e7..db3f109 100644 --- a/generated/schema/notif.schema.json +++ b/generated/schema/notif.schema.json @@ -125,6 +125,24 @@ "format": "uint32", "minimum": 0 }, + "byteMax": { + "description": "Inclusive upper bound from the same `contentBytesRange` check (see [`byte_min`]).\n\n[`byte_min`]: ParsedField::byte_min", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "byteMin": { + "description": "Inclusive lower bound on the byte-content length, enforced via\n`contentBytesRange(node, min, max)` when `min != max` — a payload-size *limit*\n(a media buffer capped at 1 MiB, a token capped at 128 bytes), as opposed to a\nfixed [`byte_length`] (the `min == max` case). Present only for a\n[`ParsedFieldType::Bytes`] field with a true range; a consumer can enforce the\nbound rather than guessing an unbounded buffer.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, "children": { "type": [ "array", diff --git a/generated/schema/srvreq.schema.json b/generated/schema/srvreq.schema.json index a29e957..6300ab0 100644 --- a/generated/schema/srvreq.schema.json +++ b/generated/schema/srvreq.schema.json @@ -64,6 +64,24 @@ "format": "uint32", "minimum": 0 }, + "byteMax": { + "description": "Inclusive upper bound from the same `contentBytesRange` check (see [`byte_min`]).\n\n[`byte_min`]: ParsedField::byte_min", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "byteMin": { + "description": "Inclusive lower bound on the byte-content length, enforced via\n`contentBytesRange(node, min, max)` when `min != max` — a payload-size *limit*\n(a media buffer capped at 1 MiB, a token capped at 128 bytes), as opposed to a\nfixed [`byte_length`] (the `min == max` case). Present only for a\n[`ParsedFieldType::Bytes`] field with a true range; a consumer can enforce the\nbound rather than guessing an unbounded buffer.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, "children": { "type": [ "array", diff --git a/generated/schema/stanza.schema.json b/generated/schema/stanza.schema.json index a2ceee6..88a0909 100644 --- a/generated/schema/stanza.schema.json +++ b/generated/schema/stanza.schema.json @@ -113,6 +113,24 @@ "format": "uint32", "minimum": 0 }, + "byteMax": { + "description": "Inclusive upper bound from the same `contentBytesRange` check (see [`byte_min`]).\n\n[`byte_min`]: ParsedField::byte_min", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, + "byteMin": { + "description": "Inclusive lower bound on the byte-content length, enforced via\n`contentBytesRange(node, min, max)` when `min != max` — a payload-size *limit*\n(a media buffer capped at 1 MiB, a token capped at 128 bytes), as opposed to a\nfixed [`byte_length`] (the `min == max` case). Present only for a\n[`ParsedFieldType::Bytes`] field with a true range; a consumer can enforce the\nbound rather than guessing an unbounded buffer.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0 + }, "children": { "type": [ "array", diff --git a/generated/srvreq/index.json b/generated/srvreq/index.json index 3dc2c07..719d037 100644 --- a/generated/srvreq/index.json +++ b/generated/srvreq/index.json @@ -1026,7 +1026,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 4096 } ], "repeats": false @@ -1529,7 +1531,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -1597,7 +1601,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -1776,7 +1782,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -1844,7 +1852,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -1950,7 +1960,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -2018,7 +2030,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -2167,7 +2181,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -2235,7 +2251,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -2395,7 +2413,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -2463,7 +2483,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -2559,7 +2581,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -2637,7 +2661,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -2715,7 +2741,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -4341,6 +4369,8 @@ "name": "pairSuccessDeviceIdentityElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 500, "sourcePath": [ "pair-success", "device-identity" @@ -4469,6 +4499,8 @@ "name": "encryptedKeyElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 2048, "sourcePath": [ "encrypted_key" ] @@ -4478,6 +4510,8 @@ "name": "nonceElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "nonce" ] @@ -4487,6 +4521,8 @@ "name": "encryptedDataElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 8192, "sourcePath": [ "encrypted_data" ] @@ -4496,6 +4532,8 @@ "name": "authTagElementValue", "type": "bytes", "required": true, + "byteMin": 1, + "byteMax": 128, "sourcePath": [ "auth_tag" ] @@ -5148,7 +5186,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -5216,7 +5256,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -5322,7 +5364,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -5454,7 +5498,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -5522,7 +5568,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -5628,7 +5676,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -5696,7 +5746,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -5845,7 +5897,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -5913,7 +5967,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -6073,7 +6129,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -6141,7 +6199,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -6305,7 +6365,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -6383,7 +6445,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -6524,7 +6588,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -7111,7 +7177,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false @@ -7160,7 +7228,9 @@ "method": "contentBytes", "name": "elementValue", "type": "bytes", - "required": true + "required": true, + "byteMin": 1, + "byteMax": 1048576 } ], "repeats": false