Skip to main content

xmtp_mls_common/app_data/components/
metadata_attributes.rs

1//! [`Component`] impls for the eight `GroupMutableMetadata`-backed
2//! attribute components.
3//!
4//! Three flavours, distinguished by the typed `Value` they expose:
5//!
6//! - **String** (`GROUP_NAME`, `GROUP_DESCRIPTION`, `GROUP_IMAGE_URL`,
7//!   `APP_DATA`, `MIN_SUPPORTED_PROTOCOL_VERSION`): `Value = String`,
8//!   wire bytes are UTF-8.
9//! - **Big-endian `i64`** (`MESSAGE_DISAPPEAR_FROM_NS`,
10//!   `MESSAGE_DISAPPEAR_IN_NS`): `Value = i64`, wire bytes are exactly
11//!   8 bytes (`i64::to_be_bytes`). The legacy `GroupMutableMetadata`
12//!   path stringifies these as decimal; the AppData path uses the
13//!   binary representation directly so callers don't have to round-trip
14//!   through ASCII.
15//! - **Fixed-length signer key** (`COMMIT_LOG_SIGNER`):
16//!   `Value = xmtp_cryptography::Secret` (zeroized on drop) holding
17//!   exactly `ED25519_KEY_LENGTH` bytes — the raw Ed25519 private-key
18//!   material that the legacy path hex-encoded into a string. The
19//!   length is enforced at decode time; callers continue to handle a
20//!   `Secret` end-to-end without a hex round-trip.
21//!
22//! `AppDataUpdate::Update` payloads pass through verbatim after the
23//! component-specific length/UTF-8 validation, and there is no delta
24//! encoding.
25
26use openmls::messages::proposals::AppDataUpdateOperation;
27use xmtp_cryptography::{Secret, configuration::ED25519_KEY_LENGTH};
28use xmtp_proto::xmtp::mls::message_contents::ComponentType;
29
30use crate::app_data::{
31    component_id::ComponentId,
32    component_registry::ComponentOp,
33    typed::{Component, ComponentInvariantError, ComponentTypedError, ExpandedComponentChange},
34};
35
36/// Maximum byte length of a group name.
37pub const MAX_GROUP_NAME_LENGTH: usize = 100;
38/// Maximum byte length of a group description.
39pub const MAX_GROUP_DESCRIPTION_LENGTH: usize = 1000;
40/// Maximum byte length of a group image URL.
41pub const MAX_GROUP_IMAGE_URL_LENGTH: usize = 2048;
42/// Maximum byte length of the opaque application-data string.
43pub const MAX_APP_DATA_LENGTH: usize = 8192;
44
45/// Apply a passthrough Update payload — no delta math, the payload is
46/// the new full value bytes.
47pub(crate) fn apply_passthrough(payload: &[u8]) -> Result<Vec<u8>, ComponentTypedError> {
48    Ok(payload.to_vec())
49}
50
51/// Expand an `AppDataUpdate` proposal for a passthrough component:
52/// `Update` produces one `Update` change carrying the payload bytes;
53/// `Remove` produces one `Delete` change with no value.
54pub(crate) fn expand_passthrough(
55    op: &AppDataUpdateOperation,
56) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
57    match op {
58        AppDataUpdateOperation::Update(payload) => Ok(vec![ExpandedComponentChange {
59            op: ComponentOp::Update,
60            key: None,
61            value: Some(payload.as_slice().to_vec()),
62        }]),
63        AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
64            op: ComponentOp::Delete,
65            key: None,
66            value: None,
67        }]),
68    }
69}
70
71/// Decode UTF-8 bytes into a `String`, surfacing a structured
72/// [`ComponentTypedError::MalformedValue`] on invalid input rather than
73/// the bare `Utf8Error`.
74pub(crate) fn decode_utf8(
75    component_id: ComponentId,
76    bytes: &[u8],
77) -> Result<String, ComponentTypedError> {
78    std::str::from_utf8(bytes)
79        .map(str::to_owned)
80        .map_err(|err| ComponentTypedError::MalformedValue {
81            component_id,
82            reason: format!("not valid UTF-8: {err}"),
83        })
84}
85
86/// Decode an 8-byte big-endian `i64`, surfacing
87/// [`ComponentTypedError::MalformedValue`] on length mismatch.
88fn decode_be_i64(component_id: ComponentId, bytes: &[u8]) -> Result<i64, ComponentTypedError> {
89    let arr: [u8; 8] = bytes
90        .try_into()
91        .map_err(|_| ComponentTypedError::MalformedValue {
92            component_id,
93            reason: format!("expected 8 bytes (big-endian i64), got {}", bytes.len()),
94        })?;
95    Ok(i64::from_be_bytes(arr))
96}
97
98/// Validate a byte slice has the expected fixed length, surfacing
99/// [`ComponentTypedError::MalformedValue`] on mismatch.
100fn require_exact_len(
101    component_id: ComponentId,
102    bytes: &[u8],
103    expected: usize,
104) -> Result<(), ComponentTypedError> {
105    if bytes.len() != expected {
106        return Err(ComponentTypedError::MalformedValue {
107            component_id,
108            reason: format!("expected {} bytes, got {}", expected, bytes.len()),
109        });
110    }
111    Ok(())
112}
113
114/// Internal macro for declaring a passthrough metadata-attribute
115/// component impl.
116///
117/// Each invocation produces a unit struct + `Component` impl. The
118/// `decode_value` / `encode_value` / `encode_mutation` bodies vary
119/// between the `String` and `Bytes` shapes, so the macro takes them as
120/// arms.
121///
122/// The macro stays internal (`macro_rules!` with no `pub` attribute) —
123/// it's a within-this-file convenience, not a public API.
124macro_rules! passthrough_string_component {
125    ($struct_name:ident, $id:expr $(, $max_len:expr)?) => {
126        pub struct $struct_name;
127
128        impl Component for $struct_name {
129            const ID: ComponentId = $id;
130            const COMPONENT_TYPE: ComponentType = ComponentType::String;
131            type Value = String;
132            type Mutation = String;
133
134            fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
135                decode_utf8(Self::ID, bytes)
136            }
137
138            fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
139                Ok(value.as_bytes().to_vec())
140            }
141
142            fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
143                Ok(mutation.as_bytes().to_vec())
144            }
145
146            fn apply_update_payload(
147                payload: &[u8],
148                _prior: Option<&[u8]>,
149            ) -> Result<Vec<u8>, ComponentTypedError> {
150                // Validate UTF-8 before accepting the bytes into the
151                // dict. Without this, a peer could land a value every
152                // receiver ACCEPTS at commit time but none can READ
153                // back (`decode_value` requires UTF-8) — an
154                // accepted-but-unreadable component. Mirrors the
155                // `be_i64_component!` family's validate-then-passthrough
156                // shape, and matches the type-aware fallback for
157                // unknown String-typed ids, which already rejects
158                // non-UTF-8 — so known-impl and unknown-id receivers
159                // agree.
160                let _ = decode_utf8(Self::ID, payload)?;
161                apply_passthrough(payload)
162            }
163
164            fn expand_to_changes(
165                op: &AppDataUpdateOperation,
166                _prior: Option<&[u8]>,
167            ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
168                // Keep the validator's verdict in lockstep with apply.
169                if let AppDataUpdateOperation::Update(payload) = op {
170                    let _ = decode_utf8(Self::ID, payload.as_slice())?;
171                }
172                expand_passthrough(op)
173            }
174
175            fn validate_invariant(
176                _change: &crate::app_data::validation::ComponentChange<'_>,
177                _registry: &crate::app_data::component_registry::ComponentRegistry,
178            ) -> Result<(), ComponentInvariantError> {
179                $(
180                    // Registry policies only evaluate the actor. The byte
181                    // bound is a predicate over the resulting value, so it
182                    // must be enforced by this component on every receiver.
183                    if let Some(value) = _change.new_value
184                        && value.len() > $max_len
185                    {
186                        return Err(ComponentInvariantError::Violation {
187                            component_id: Self::ID,
188                            reason: format!("value exceeds {} bytes", $max_len),
189                        });
190                    }
191                )?
192                Ok(())
193            }
194        }
195    };
196}
197
198/// Internal macro for the two `i64` disappearance-window components.
199/// Both share the wire shape (8-byte BE) and only differ in their ID.
200macro_rules! be_i64_component {
201    ($struct_name:ident, $id:expr) => {
202        pub struct $struct_name;
203
204        impl Component for $struct_name {
205            const ID: ComponentId = $id;
206            const COMPONENT_TYPE: ComponentType = ComponentType::Bytes;
207            type Value = i64;
208            type Mutation = i64;
209
210            fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
211                decode_be_i64(Self::ID, bytes)
212            }
213
214            fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
215                Ok(value.to_be_bytes().to_vec())
216            }
217
218            fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
219                Ok(mutation.to_be_bytes().to_vec())
220            }
221
222            fn apply_update_payload(
223                payload: &[u8],
224                _prior: Option<&[u8]>,
225            ) -> Result<Vec<u8>, ComponentTypedError> {
226                // Validate length+shape and pass through.
227                let _ = decode_be_i64(Self::ID, payload)?;
228                Ok(payload.to_vec())
229            }
230
231            fn expand_to_changes(
232                op: &AppDataUpdateOperation,
233                _prior: Option<&[u8]>,
234            ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
235                match op {
236                    AppDataUpdateOperation::Update(payload) => {
237                        let _ = decode_be_i64(Self::ID, payload.as_slice())?;
238                        Ok(vec![ExpandedComponentChange {
239                            op: ComponentOp::Update,
240                            key: None,
241                            value: Some(payload.as_slice().to_vec()),
242                        }])
243                    }
244                    AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
245                        op: ComponentOp::Delete,
246                        key: None,
247                        value: None,
248                    }]),
249                }
250            }
251        }
252    };
253}
254
255passthrough_string_component!(
256    GroupNameComponent,
257    ComponentId::GROUP_NAME,
258    MAX_GROUP_NAME_LENGTH
259);
260passthrough_string_component!(
261    GroupDescriptionComponent,
262    ComponentId::GROUP_DESCRIPTION,
263    MAX_GROUP_DESCRIPTION_LENGTH
264);
265passthrough_string_component!(
266    GroupImageUrlComponent,
267    ComponentId::GROUP_IMAGE_URL,
268    MAX_GROUP_IMAGE_URL_LENGTH
269);
270passthrough_string_component!(AppDataComponent, ComponentId::APP_DATA, MAX_APP_DATA_LENGTH);
271passthrough_string_component!(
272    MinSupportedProtocolVersionComponent,
273    ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION
274);
275
276be_i64_component!(
277    MessageDisappearFromNsComponent,
278    ComponentId::MESSAGE_DISAPPEAR_FROM_NS
279);
280be_i64_component!(
281    MessageDisappearInNsComponent,
282    ComponentId::MESSAGE_DISAPPEAR_IN_NS
283);
284
285/// `Component` impl for the `COMMIT_LOG_SIGNER` component.
286///
287/// The decoded value is an [`xmtp_cryptography::Secret`] holding
288/// exactly `ED25519_KEY_LENGTH` bytes — the raw Ed25519 private-key
289/// material. The length is enforced at decode time so callers can
290/// always treat the resulting `Secret` as a 32-byte key.
291pub struct CommitLogSignerComponent;
292
293impl Component for CommitLogSignerComponent {
294    const ID: ComponentId = ComponentId::COMMIT_LOG_SIGNER;
295    const COMPONENT_TYPE: ComponentType = ComponentType::Bytes;
296    type Value = Secret;
297    type Mutation = Secret;
298
299    fn decode_value(bytes: &[u8]) -> Result<Self::Value, ComponentTypedError> {
300        require_exact_len(Self::ID, bytes, ED25519_KEY_LENGTH)?;
301        Ok(Secret::new(bytes.to_vec()))
302    }
303
304    fn encode_value(value: &Self::Value) -> Result<Vec<u8>, ComponentTypedError> {
305        require_exact_len(Self::ID, value.as_slice(), ED25519_KEY_LENGTH)?;
306        Ok(value.as_slice().to_vec())
307    }
308
309    fn encode_mutation(mutation: &Self::Mutation) -> Result<Vec<u8>, ComponentTypedError> {
310        require_exact_len(Self::ID, mutation.as_slice(), ED25519_KEY_LENGTH)?;
311        Ok(mutation.as_slice().to_vec())
312    }
313
314    fn apply_update_payload(
315        payload: &[u8],
316        _prior: Option<&[u8]>,
317    ) -> Result<Vec<u8>, ComponentTypedError> {
318        require_exact_len(Self::ID, payload, ED25519_KEY_LENGTH)?;
319        Ok(payload.to_vec())
320    }
321
322    fn expand_to_changes(
323        op: &AppDataUpdateOperation,
324        _prior: Option<&[u8]>,
325    ) -> Result<Vec<ExpandedComponentChange>, ComponentTypedError> {
326        match op {
327            AppDataUpdateOperation::Update(payload) => {
328                require_exact_len(Self::ID, payload.as_slice(), ED25519_KEY_LENGTH)?;
329                Ok(vec![ExpandedComponentChange {
330                    op: ComponentOp::Update,
331                    key: None,
332                    value: Some(payload.as_slice().to_vec()),
333                }])
334            }
335            AppDataUpdateOperation::Remove => Ok(vec![ExpandedComponentChange {
336                op: ComponentOp::Delete,
337                key: None,
338                value: None,
339            }]),
340        }
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347    use crate::app_data::{
348        component_registry::ComponentRegistry,
349        validation::{ActorAuthority, ComponentChange},
350    };
351    use openmls::messages::proposals::AppDataUpdateOperation;
352
353    #[xmtp_common::test(unwrap_try = true)]
354    fn string_component_round_trip() {
355        let name = String::from("Test Group 🚀");
356        let bytes = GroupNameComponent::encode_value(&name).unwrap();
357        let decoded = GroupNameComponent::decode_value(&bytes).unwrap();
358        assert_eq!(decoded, name);
359    }
360
361    #[xmtp_common::test(unwrap_try = true)]
362    fn string_component_decode_rejects_non_utf8() {
363        let bytes = vec![0xff, 0xfe, 0xfd];
364        let err = GroupDescriptionComponent::decode_value(&bytes).unwrap_err();
365        match err {
366            ComponentTypedError::MalformedValue { component_id, .. } => {
367                assert_eq!(component_id, ComponentId::GROUP_DESCRIPTION);
368            }
369            other => panic!("expected MalformedValue, got {other:?}"),
370        }
371    }
372
373    /// Apply/read symmetry: bytes the read path can't decode must be
374    /// rejected at APPLY time too. Pre-fix, `apply_update_payload` was
375    /// a raw passthrough — a peer could land a value every receiver
376    /// accepted at commit time but none could read back.
377    #[xmtp_common::test(unwrap_try = true)]
378    fn string_component_apply_rejects_non_utf8() {
379        let err = AppDataComponent::apply_update_payload(&[0xff, 0xfe, 0xfd], None).unwrap_err();
380        match err {
381            ComponentTypedError::MalformedValue { component_id, .. } => {
382                assert_eq!(component_id, ComponentId::APP_DATA);
383            }
384            other => panic!("expected MalformedValue, got {other:?}"),
385        }
386        // Valid UTF-8 still passes through byte-identical.
387        let ok = AppDataComponent::apply_update_payload("héllo".as_bytes(), None).unwrap();
388        assert_eq!(ok, "héllo".as_bytes());
389    }
390
391    /// Same symmetry on the validator's expansion path — apply and
392    /// expand must agree so every honest receiver returns the same
393    /// verdict regardless of which stage sees the payload first.
394    #[xmtp_common::test(unwrap_try = true)]
395    fn string_component_expand_rejects_non_utf8() {
396        let op = AppDataUpdateOperation::Update(vec![0xff, 0xfe, 0xfd].into());
397        let err = GroupNameComponent::expand_to_changes(&op, None).unwrap_err();
398        match err {
399            ComponentTypedError::MalformedValue { component_id, .. } => {
400                assert_eq!(component_id, ComponentId::GROUP_NAME);
401            }
402            other => panic!("expected MalformedValue, got {other:?}"),
403        }
404        // Remove never carries bytes and stays valid.
405        let changes =
406            GroupNameComponent::expand_to_changes(&AppDataUpdateOperation::Remove, None).unwrap();
407        assert_eq!(changes.len(), 1);
408        assert_eq!(changes[0].op, ComponentOp::Delete);
409    }
410
411    #[xmtp_common::test(unwrap_try = true)]
412    fn commit_log_signer_round_trip() {
413        let key = Secret::new(vec![0xAB; ED25519_KEY_LENGTH]);
414        let bytes = CommitLogSignerComponent::encode_value(&key).unwrap();
415        assert_eq!(bytes.len(), ED25519_KEY_LENGTH);
416        let decoded = CommitLogSignerComponent::decode_value(&bytes).unwrap();
417        assert_eq!(decoded.as_slice(), key.as_slice());
418    }
419
420    #[xmtp_common::test(unwrap_try = true)]
421    fn commit_log_signer_rejects_wrong_length() {
422        let too_short = vec![0u8; ED25519_KEY_LENGTH - 1];
423        let err = CommitLogSignerComponent::decode_value(&too_short).unwrap_err();
424        match err {
425            ComponentTypedError::MalformedValue { component_id, .. } => {
426                assert_eq!(component_id, ComponentId::COMMIT_LOG_SIGNER);
427            }
428            other => panic!("expected MalformedValue, got {other:?}"),
429        }
430        // Encoding a Secret that doesn't hold exactly 32 bytes also rejects.
431        let wrong = Secret::new(vec![0u8; ED25519_KEY_LENGTH + 1]);
432        let err = CommitLogSignerComponent::encode_value(&wrong).unwrap_err();
433        assert!(matches!(err, ComponentTypedError::MalformedValue { .. }));
434    }
435
436    #[xmtp_common::test(unwrap_try = true)]
437    fn message_disappear_round_trip() {
438        // Use a value that exercises sign-extension: large negative i64.
439        let v: i64 = -1_234_567_890_123;
440        let bytes = MessageDisappearFromNsComponent::encode_value(&v).unwrap();
441        assert_eq!(bytes.len(), 8);
442        let decoded = MessageDisappearFromNsComponent::decode_value(&bytes).unwrap();
443        assert_eq!(decoded, v);
444    }
445
446    #[xmtp_common::test(unwrap_try = true)]
447    fn message_disappear_rejects_wrong_length() {
448        // Catches a sender that emitted decimal-stringified bytes
449        // (e.g. b"3600000000000") under the legacy convention.
450        let ascii_decimal = b"3600000000000";
451        let err = MessageDisappearInNsComponent::decode_value(ascii_decimal).unwrap_err();
452        match err {
453            ComponentTypedError::MalformedValue { component_id, .. } => {
454                assert_eq!(component_id, ComponentId::MESSAGE_DISAPPEAR_IN_NS);
455            }
456            other => panic!("expected MalformedValue, got {other:?}"),
457        }
458    }
459
460    #[xmtp_common::test(unwrap_try = true)]
461    fn apply_update_passthrough() {
462        // String component still passes payloads through verbatim.
463        let payload = b"new value";
464        let new = GroupNameComponent::apply_update_payload(payload, Some(b"old")).unwrap();
465        assert_eq!(new, payload);
466
467        // i64 components validate the length-and-shape but pass the
468        // bytes through unchanged so receivers don't re-encode.
469        let valid = 42_i64.to_be_bytes();
470        let new = MessageDisappearInNsComponent::apply_update_payload(&valid, None).unwrap();
471        assert_eq!(new.as_slice(), valid.as_slice());
472
473        let err =
474            MessageDisappearInNsComponent::apply_update_payload(b"not 8 bytes", None).unwrap_err();
475        assert!(matches!(err, ComponentTypedError::MalformedValue { .. }));
476    }
477
478    #[xmtp_common::test(unwrap_try = true)]
479    fn expand_update_yields_one_change() {
480        let payload = b"hello".to_vec();
481        let op = AppDataUpdateOperation::Update(payload.clone().into());
482        let changes = GroupNameComponent::expand_to_changes(&op, None).unwrap();
483        assert_eq!(changes.len(), 1);
484        assert_eq!(changes[0].op, ComponentOp::Update);
485        assert_eq!(changes[0].value.as_deref(), Some(payload.as_slice()));
486    }
487
488    #[xmtp_common::test(unwrap_try = true)]
489    fn expand_remove_yields_delete_with_no_value() {
490        let op = AppDataUpdateOperation::Remove;
491        let changes = AppDataComponent::expand_to_changes(&op, None).unwrap();
492        assert_eq!(changes.len(), 1);
493        assert_eq!(changes[0].op, ComponentOp::Delete);
494        assert!(changes[0].value.is_none());
495    }
496
497    #[xmtp_common::test(unwrap_try = true)]
498    fn component_id_constants_match_static_id() {
499        // Sanity check that each component reports its own ID via the
500        // trait — a copy-paste error in the macro invocations would
501        // surface here.
502        assert_eq!(GroupNameComponent::ID, ComponentId::GROUP_NAME);
503        assert_eq!(
504            GroupDescriptionComponent::ID,
505            ComponentId::GROUP_DESCRIPTION
506        );
507        assert_eq!(GroupImageUrlComponent::ID, ComponentId::GROUP_IMAGE_URL);
508        assert_eq!(AppDataComponent::ID, ComponentId::APP_DATA);
509        assert_eq!(
510            MinSupportedProtocolVersionComponent::ID,
511            ComponentId::MIN_SUPPORTED_PROTOCOL_VERSION
512        );
513        assert_eq!(
514            MessageDisappearFromNsComponent::ID,
515            ComponentId::MESSAGE_DISAPPEAR_FROM_NS
516        );
517        assert_eq!(
518            MessageDisappearInNsComponent::ID,
519            ComponentId::MESSAGE_DISAPPEAR_IN_NS
520        );
521        assert_eq!(CommitLogSignerComponent::ID, ComponentId::COMMIT_LOG_SIGNER);
522    }
523
524    #[xmtp_common::test(unwrap_try = true)]
525    fn component_types_are_correct() {
526        assert_eq!(GroupNameComponent::COMPONENT_TYPE, ComponentType::String);
527        assert_eq!(
528            CommitLogSignerComponent::COMPONENT_TYPE,
529            ComponentType::Bytes
530        );
531        assert_eq!(
532            MessageDisappearFromNsComponent::COMPONENT_TYPE,
533            ComponentType::Bytes
534        );
535        assert_eq!(
536            MinSupportedProtocolVersionComponent::COMPONENT_TYPE,
537            ComponentType::String
538        );
539    }
540
541    macro_rules! bounded_component_test {
542        ($test_name:ident, $component:ty, $component_id:expr, $max_length:expr) => {
543            #[xmtp_common::test(unwrap_try = true)]
544            fn $test_name() {
545                let value = vec![b'x'; $max_length + 1];
546                let change = ComponentChange::builder()
547                    .component_id($component_id)
548                    .op(ComponentOp::Update)
549                    .actor(ActorAuthority {
550                        is_admin: true,
551                        is_super_admin: true,
552                    })
553                    .new_value(&value)
554                    .build();
555                let err = <$component>::validate_invariant(&change, &ComponentRegistry::new())
556                    .unwrap_err();
557                assert!(matches!(err, ComponentInvariantError::Violation { .. }));
558            }
559        };
560    }
561
562    bounded_component_test!(
563        group_name_rejects_overlong_post_state,
564        GroupNameComponent,
565        ComponentId::GROUP_NAME,
566        MAX_GROUP_NAME_LENGTH
567    );
568    bounded_component_test!(
569        group_description_rejects_overlong_post_state,
570        GroupDescriptionComponent,
571        ComponentId::GROUP_DESCRIPTION,
572        MAX_GROUP_DESCRIPTION_LENGTH
573    );
574    bounded_component_test!(
575        group_image_url_rejects_overlong_post_state,
576        GroupImageUrlComponent,
577        ComponentId::GROUP_IMAGE_URL,
578        MAX_GROUP_IMAGE_URL_LENGTH
579    );
580    bounded_component_test!(
581        app_data_rejects_overlong_post_state,
582        AppDataComponent,
583        ComponentId::APP_DATA,
584        MAX_APP_DATA_LENGTH
585    );
586}