1 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 // Copyright by contributors to this project. 3 // SPDX-License-Identifier: (Apache-2.0 OR MIT) 4 5 use alloc::boxed::Box; 6 use alloc::vec::Vec; 7 8 #[cfg(feature = "custom_proposal")] 9 use itertools::Itertools; 10 11 use crate::{ 12 group::{ 13 AddProposal, BorrowedProposal, Proposal, ProposalOrRef, ProposalType, ReInitProposal, 14 RemoveProposal, Sender, 15 }, 16 ExtensionList, 17 }; 18 19 #[cfg(feature = "by_ref_proposal")] 20 use crate::group::{proposal_cache::CachedProposal, LeafIndex, ProposalRef, UpdateProposal}; 21 22 #[cfg(feature = "psk")] 23 use crate::group::PreSharedKeyProposal; 24 25 #[cfg(feature = "custom_proposal")] 26 use crate::group::proposal::CustomProposal; 27 28 use crate::group::ExternalInit; 29 30 use core::iter::empty; 31 32 #[derive(Clone, Debug, Default)] 33 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 34 /// A collection of proposals. 35 pub struct ProposalBundle { 36 pub(crate) additions: Vec<ProposalInfo<AddProposal>>, 37 #[cfg(feature = "by_ref_proposal")] 38 pub(crate) updates: Vec<ProposalInfo<UpdateProposal>>, 39 #[cfg(feature = "by_ref_proposal")] 40 pub(crate) update_senders: Vec<LeafIndex>, 41 pub(crate) removals: Vec<ProposalInfo<RemoveProposal>>, 42 #[cfg(feature = "psk")] 43 pub(crate) psks: Vec<ProposalInfo<PreSharedKeyProposal>>, 44 pub(crate) reinitializations: Vec<ProposalInfo<ReInitProposal>>, 45 pub(crate) external_initializations: Vec<ProposalInfo<ExternalInit>>, 46 pub(crate) group_context_extensions: Vec<ProposalInfo<ExtensionList>>, 47 #[cfg(feature = "custom_proposal")] 48 pub(crate) custom_proposals: Vec<ProposalInfo<CustomProposal>>, 49 } 50 51 impl ProposalBundle { add(&mut self, proposal: Proposal, sender: Sender, source: ProposalSource)52 pub fn add(&mut self, proposal: Proposal, sender: Sender, source: ProposalSource) { 53 match proposal { 54 Proposal::Add(proposal) => self.additions.push(ProposalInfo { 55 proposal: *proposal, 56 sender, 57 source, 58 }), 59 #[cfg(feature = "by_ref_proposal")] 60 Proposal::Update(proposal) => self.updates.push(ProposalInfo { 61 proposal, 62 sender, 63 source, 64 }), 65 Proposal::Remove(proposal) => self.removals.push(ProposalInfo { 66 proposal, 67 sender, 68 source, 69 }), 70 #[cfg(feature = "psk")] 71 Proposal::Psk(proposal) => self.psks.push(ProposalInfo { 72 proposal, 73 sender, 74 source, 75 }), 76 Proposal::ReInit(proposal) => self.reinitializations.push(ProposalInfo { 77 proposal, 78 sender, 79 source, 80 }), 81 Proposal::ExternalInit(proposal) => self.external_initializations.push(ProposalInfo { 82 proposal, 83 sender, 84 source, 85 }), 86 Proposal::GroupContextExtensions(proposal) => { 87 self.group_context_extensions.push(ProposalInfo { 88 proposal, 89 sender, 90 source, 91 }) 92 } 93 #[cfg(feature = "custom_proposal")] 94 Proposal::Custom(proposal) => self.custom_proposals.push(ProposalInfo { 95 proposal, 96 sender, 97 source, 98 }), 99 } 100 } 101 102 /// Remove the proposal of type `T` at `index` 103 /// 104 /// Type `T` can be any of the standard MLS proposal types defined in the 105 /// [`proposal`](crate::group::proposal) module. 106 /// 107 /// `index` is consistent with the index returned by any of the proposal 108 /// type specific functions in this module. remove<T: Proposable>(&mut self, index: usize)109 pub fn remove<T: Proposable>(&mut self, index: usize) { 110 T::remove(self, index); 111 } 112 113 /// Iterate over proposals, filtered by type. 114 /// 115 /// Type `T` can be any of the standard MLS proposal types defined in the 116 /// [`proposal`](crate::group::proposal) module. by_type<'a, T: Proposable + 'a>(&'a self) -> impl Iterator<Item = &'a ProposalInfo<T>>117 pub fn by_type<'a, T: Proposable + 'a>(&'a self) -> impl Iterator<Item = &'a ProposalInfo<T>> { 118 T::filter(self).iter() 119 } 120 121 /// Retain proposals, filtered by type. 122 /// 123 /// Type `T` can be any of the standard MLS proposal types defined in the 124 /// [`proposal`](crate::group::proposal) module. retain_by_type<T, F, E>(&mut self, mut f: F) -> Result<(), E> where T: Proposable, F: FnMut(&ProposalInfo<T>) -> Result<bool, E>,125 pub fn retain_by_type<T, F, E>(&mut self, mut f: F) -> Result<(), E> 126 where 127 T: Proposable, 128 F: FnMut(&ProposalInfo<T>) -> Result<bool, E>, 129 { 130 let mut res = Ok(()); 131 132 T::retain(self, |p| match f(p) { 133 Ok(keep) => keep, 134 Err(e) => { 135 if res.is_ok() { 136 res = Err(e); 137 } 138 false 139 } 140 }); 141 142 res 143 } 144 145 /// Retain custom proposals in the bundle. 146 #[cfg(feature = "custom_proposal")] retain_custom<F, E>(&mut self, mut f: F) -> Result<(), E> where F: FnMut(&ProposalInfo<CustomProposal>) -> Result<bool, E>,147 pub fn retain_custom<F, E>(&mut self, mut f: F) -> Result<(), E> 148 where 149 F: FnMut(&ProposalInfo<CustomProposal>) -> Result<bool, E>, 150 { 151 let mut res = Ok(()); 152 153 self.custom_proposals.retain(|p| match f(p) { 154 Ok(keep) => keep, 155 Err(e) => { 156 if res.is_ok() { 157 res = Err(e); 158 } 159 false 160 } 161 }); 162 163 res 164 } 165 166 /// Retain MLS standard proposals in the bundle. retain<F, E>(&mut self, mut f: F) -> Result<(), E> where F: FnMut(&ProposalInfo<BorrowedProposal<'_>>) -> Result<bool, E>,167 pub fn retain<F, E>(&mut self, mut f: F) -> Result<(), E> 168 where 169 F: FnMut(&ProposalInfo<BorrowedProposal<'_>>) -> Result<bool, E>, 170 { 171 self.retain_by_type::<AddProposal, _, _>(|proposal| { 172 f(&proposal.as_ref().map(BorrowedProposal::from)) 173 })?; 174 175 #[cfg(feature = "by_ref_proposal")] 176 self.retain_by_type::<UpdateProposal, _, _>(|proposal| { 177 f(&proposal.as_ref().map(BorrowedProposal::from)) 178 })?; 179 180 self.retain_by_type::<RemoveProposal, _, _>(|proposal| { 181 f(&proposal.as_ref().map(BorrowedProposal::from)) 182 })?; 183 184 #[cfg(feature = "psk")] 185 self.retain_by_type::<PreSharedKeyProposal, _, _>(|proposal| { 186 f(&proposal.as_ref().map(BorrowedProposal::from)) 187 })?; 188 189 self.retain_by_type::<ReInitProposal, _, _>(|proposal| { 190 f(&proposal.as_ref().map(BorrowedProposal::from)) 191 })?; 192 193 self.retain_by_type::<ExternalInit, _, _>(|proposal| { 194 f(&proposal.as_ref().map(BorrowedProposal::from)) 195 })?; 196 197 self.retain_by_type::<ExtensionList, _, _>(|proposal| { 198 f(&proposal.as_ref().map(BorrowedProposal::from)) 199 })?; 200 201 Ok(()) 202 } 203 204 /// The number of proposals in the bundle length(&self) -> usize205 pub fn length(&self) -> usize { 206 let len = 0; 207 208 #[cfg(feature = "psk")] 209 let len = len + self.psks.len(); 210 211 let len = len + self.external_initializations.len(); 212 213 #[cfg(feature = "custom_proposal")] 214 let len = len + self.custom_proposals.len(); 215 216 #[cfg(feature = "by_ref_proposal")] 217 let len = len + self.updates.len(); 218 219 len + self.additions.len() 220 + self.removals.len() 221 + self.reinitializations.len() 222 + self.group_context_extensions.len() 223 } 224 225 /// Iterate over all proposals inside the bundle. iter_proposals(&self) -> impl Iterator<Item = ProposalInfo<BorrowedProposal<'_>>>226 pub fn iter_proposals(&self) -> impl Iterator<Item = ProposalInfo<BorrowedProposal<'_>>> { 227 let res = self 228 .additions 229 .iter() 230 .map(|p| p.as_ref().map(BorrowedProposal::Add)) 231 .chain( 232 self.removals 233 .iter() 234 .map(|p| p.as_ref().map(BorrowedProposal::Remove)), 235 ) 236 .chain( 237 self.reinitializations 238 .iter() 239 .map(|p| p.as_ref().map(BorrowedProposal::ReInit)), 240 ); 241 242 #[cfg(feature = "by_ref_proposal")] 243 let res = res.chain( 244 self.updates 245 .iter() 246 .map(|p| p.as_ref().map(BorrowedProposal::Update)), 247 ); 248 249 #[cfg(feature = "psk")] 250 let res = res.chain( 251 self.psks 252 .iter() 253 .map(|p| p.as_ref().map(BorrowedProposal::Psk)), 254 ); 255 256 let res = res.chain( 257 self.external_initializations 258 .iter() 259 .map(|p| p.as_ref().map(BorrowedProposal::ExternalInit)), 260 ); 261 262 let res = res.chain( 263 self.group_context_extensions 264 .iter() 265 .map(|p| p.as_ref().map(BorrowedProposal::GroupContextExtensions)), 266 ); 267 268 #[cfg(feature = "custom_proposal")] 269 let res = res.chain( 270 self.custom_proposals 271 .iter() 272 .map(|p| p.as_ref().map(BorrowedProposal::Custom)), 273 ); 274 275 res 276 } 277 278 /// Iterate over proposal in the bundle, consuming the bundle. into_proposals(self) -> impl Iterator<Item = ProposalInfo<Proposal>>279 pub fn into_proposals(self) -> impl Iterator<Item = ProposalInfo<Proposal>> { 280 let res = empty(); 281 282 #[cfg(feature = "custom_proposal")] 283 let res = res.chain( 284 self.custom_proposals 285 .into_iter() 286 .map(|p| p.map(Proposal::Custom)), 287 ); 288 289 let res = res.chain( 290 self.external_initializations 291 .into_iter() 292 .map(|p| p.map(Proposal::ExternalInit)), 293 ); 294 295 #[cfg(feature = "psk")] 296 let res = res.chain(self.psks.into_iter().map(|p| p.map(Proposal::Psk))); 297 298 #[cfg(feature = "by_ref_proposal")] 299 let res = res.chain(self.updates.into_iter().map(|p| p.map(Proposal::Update))); 300 301 res.chain( 302 self.additions 303 .into_iter() 304 .map(|p| p.map(|p| Proposal::Add(alloc::boxed::Box::new(p)))), 305 ) 306 .chain(self.removals.into_iter().map(|p| p.map(Proposal::Remove))) 307 .chain( 308 self.reinitializations 309 .into_iter() 310 .map(|p| p.map(Proposal::ReInit)), 311 ) 312 .chain( 313 self.group_context_extensions 314 .into_iter() 315 .map(|p| p.map(Proposal::GroupContextExtensions)), 316 ) 317 } 318 into_proposals_or_refs(self) -> Vec<ProposalOrRef>319 pub(crate) fn into_proposals_or_refs(self) -> Vec<ProposalOrRef> { 320 self.into_proposals() 321 .filter_map(|p| match p.source { 322 ProposalSource::ByValue => Some(ProposalOrRef::Proposal(Box::new(p.proposal))), 323 #[cfg(feature = "by_ref_proposal")] 324 ProposalSource::ByReference(reference) => Some(ProposalOrRef::Reference(reference)), 325 _ => None, 326 }) 327 .collect() 328 } 329 330 /// Add proposals in the bundle. add_proposals(&self) -> &[ProposalInfo<AddProposal>]331 pub fn add_proposals(&self) -> &[ProposalInfo<AddProposal>] { 332 &self.additions 333 } 334 335 /// Update proposals in the bundle. 336 #[cfg(feature = "by_ref_proposal")] update_proposals(&self) -> &[ProposalInfo<UpdateProposal>]337 pub fn update_proposals(&self) -> &[ProposalInfo<UpdateProposal>] { 338 &self.updates 339 } 340 341 /// Senders of update proposals in the bundle. 342 #[cfg(feature = "by_ref_proposal")] update_proposal_senders(&self) -> &[LeafIndex]343 pub fn update_proposal_senders(&self) -> &[LeafIndex] { 344 &self.update_senders 345 } 346 347 /// Remove proposals in the bundle. remove_proposals(&self) -> &[ProposalInfo<RemoveProposal>]348 pub fn remove_proposals(&self) -> &[ProposalInfo<RemoveProposal>] { 349 &self.removals 350 } 351 352 /// Pre-shared key proposals in the bundle. 353 #[cfg(feature = "psk")] psk_proposals(&self) -> &[ProposalInfo<PreSharedKeyProposal>]354 pub fn psk_proposals(&self) -> &[ProposalInfo<PreSharedKeyProposal>] { 355 &self.psks 356 } 357 358 /// Reinit proposals in the bundle. reinit_proposals(&self) -> &[ProposalInfo<ReInitProposal>]359 pub fn reinit_proposals(&self) -> &[ProposalInfo<ReInitProposal>] { 360 &self.reinitializations 361 } 362 363 /// External init proposals in the bundle. external_init_proposals(&self) -> &[ProposalInfo<ExternalInit>]364 pub fn external_init_proposals(&self) -> &[ProposalInfo<ExternalInit>] { 365 &self.external_initializations 366 } 367 368 /// Group context extension proposals in the bundle. group_context_ext_proposals(&self) -> &[ProposalInfo<ExtensionList>]369 pub fn group_context_ext_proposals(&self) -> &[ProposalInfo<ExtensionList>] { 370 &self.group_context_extensions 371 } 372 373 /// Custom proposals in the bundle. 374 #[cfg(feature = "custom_proposal")] custom_proposals(&self) -> &[ProposalInfo<CustomProposal>]375 pub fn custom_proposals(&self) -> &[ProposalInfo<CustomProposal>] { 376 &self.custom_proposals 377 } 378 group_context_extensions_proposal(&self) -> Option<&ProposalInfo<ExtensionList>>379 pub(crate) fn group_context_extensions_proposal(&self) -> Option<&ProposalInfo<ExtensionList>> { 380 self.group_context_extensions.first() 381 } 382 383 /// Custom proposal types that are in use within this bundle. 384 #[cfg(feature = "custom_proposal")] custom_proposal_types(&self) -> impl Iterator<Item = ProposalType> + '_385 pub fn custom_proposal_types(&self) -> impl Iterator<Item = ProposalType> + '_ { 386 #[cfg(feature = "std")] 387 let res = self 388 .custom_proposals 389 .iter() 390 .map(|v| v.proposal.proposal_type()) 391 .unique(); 392 393 #[cfg(not(feature = "std"))] 394 let res = self 395 .custom_proposals 396 .iter() 397 .map(|v| v.proposal.proposal_type()) 398 .collect::<alloc::collections::BTreeSet<_>>() 399 .into_iter(); 400 401 res 402 } 403 404 /// Standard proposal types that are in use within this bundle. proposal_types(&self) -> impl Iterator<Item = ProposalType> + '_405 pub fn proposal_types(&self) -> impl Iterator<Item = ProposalType> + '_ { 406 let res = (!self.additions.is_empty()) 407 .then_some(ProposalType::ADD) 408 .into_iter() 409 .chain((!self.removals.is_empty()).then_some(ProposalType::REMOVE)) 410 .chain((!self.reinitializations.is_empty()).then_some(ProposalType::RE_INIT)); 411 412 #[cfg(feature = "by_ref_proposal")] 413 let res = res.chain((!self.updates.is_empty()).then_some(ProposalType::UPDATE)); 414 415 #[cfg(feature = "psk")] 416 let res = res.chain((!self.psks.is_empty()).then_some(ProposalType::PSK)); 417 418 let res = res.chain( 419 (!self.external_initializations.is_empty()).then_some(ProposalType::EXTERNAL_INIT), 420 ); 421 422 #[cfg(not(feature = "custom_proposal"))] 423 return res.chain( 424 (!self.group_context_extensions.is_empty()) 425 .then_some(ProposalType::GROUP_CONTEXT_EXTENSIONS), 426 ); 427 428 #[cfg(feature = "custom_proposal")] 429 return res 430 .chain( 431 (!self.group_context_extensions.is_empty()) 432 .then_some(ProposalType::GROUP_CONTEXT_EXTENSIONS), 433 ) 434 .chain(self.custom_proposal_types()); 435 } 436 } 437 438 impl FromIterator<(Proposal, Sender, ProposalSource)> for ProposalBundle { from_iter<I>(iter: I) -> Self where I: IntoIterator<Item = (Proposal, Sender, ProposalSource)>,439 fn from_iter<I>(iter: I) -> Self 440 where 441 I: IntoIterator<Item = (Proposal, Sender, ProposalSource)>, 442 { 443 let mut bundle = ProposalBundle::default(); 444 for (proposal, sender, source) in iter { 445 bundle.add(proposal, sender, source); 446 } 447 bundle 448 } 449 } 450 451 #[cfg(feature = "by_ref_proposal")] 452 impl<'a> FromIterator<(&'a ProposalRef, &'a CachedProposal)> for ProposalBundle { from_iter<I>(iter: I) -> Self where I: IntoIterator<Item = (&'a ProposalRef, &'a CachedProposal)>,453 fn from_iter<I>(iter: I) -> Self 454 where 455 I: IntoIterator<Item = (&'a ProposalRef, &'a CachedProposal)>, 456 { 457 iter.into_iter() 458 .map(|(r, p)| { 459 ( 460 p.proposal.clone(), 461 p.sender, 462 ProposalSource::ByReference(r.clone()), 463 ) 464 }) 465 .collect() 466 } 467 } 468 469 #[cfg(feature = "by_ref_proposal")] 470 impl<'a> FromIterator<&'a (ProposalRef, CachedProposal)> for ProposalBundle { from_iter<I>(iter: I) -> Self where I: IntoIterator<Item = &'a (ProposalRef, CachedProposal)>,471 fn from_iter<I>(iter: I) -> Self 472 where 473 I: IntoIterator<Item = &'a (ProposalRef, CachedProposal)>, 474 { 475 iter.into_iter().map(|pair| (&pair.0, &pair.1)).collect() 476 } 477 } 478 479 #[cfg_attr( 480 all(feature = "ffi", not(test)), 481 safer_ffi_gen::ffi_type(clone, opaque) 482 )] 483 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 484 #[derive(Clone, Debug, PartialEq)] 485 pub enum ProposalSource { 486 ByValue, 487 #[cfg(feature = "by_ref_proposal")] 488 ByReference(ProposalRef), 489 Local, 490 } 491 492 #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type(opaque))] 493 #[derive(Clone, Debug, PartialEq)] 494 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] 495 #[non_exhaustive] 496 /// Proposal description used as input to a 497 /// [`MlsRules`](crate::MlsRules). 498 pub struct ProposalInfo<T> { 499 /// The underlying proposal value. 500 pub proposal: T, 501 /// The sender of this proposal. 502 pub sender: Sender, 503 /// The source of the proposal. 504 pub source: ProposalSource, 505 } 506 507 #[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen)] 508 impl<T> ProposalInfo<T> { 509 /// Create a new ProposalInfo. 510 /// 511 /// The resulting value will be either transmitted with a commit or 512 /// locally injected into a commit resolution depending on the 513 /// `can_transmit` flag. 514 /// 515 /// This function is useful when implementing custom 516 /// [`MlsRules`](crate::MlsRules). 517 #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] new(proposal: T, sender: Sender, can_transmit: bool) -> Self518 pub fn new(proposal: T, sender: Sender, can_transmit: bool) -> Self { 519 let source = if can_transmit { 520 ProposalSource::ByValue 521 } else { 522 ProposalSource::Local 523 }; 524 525 ProposalInfo { 526 proposal, 527 sender, 528 source, 529 } 530 } 531 532 #[cfg(all(feature = "ffi", not(test)))] sender(&self) -> &Sender533 pub fn sender(&self) -> &Sender { 534 &self.sender 535 } 536 537 #[cfg(all(feature = "ffi", not(test)))] source(&self) -> &ProposalSource538 pub fn source(&self) -> &ProposalSource { 539 &self.source 540 } 541 542 #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] map<U, F>(self, f: F) -> ProposalInfo<U> where F: FnOnce(T) -> U,543 pub fn map<U, F>(self, f: F) -> ProposalInfo<U> 544 where 545 F: FnOnce(T) -> U, 546 { 547 ProposalInfo { 548 proposal: f(self.proposal), 549 sender: self.sender, 550 source: self.source, 551 } 552 } 553 554 #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::safer_ffi_gen_ignore)] as_ref(&self) -> ProposalInfo<&T>555 pub fn as_ref(&self) -> ProposalInfo<&T> { 556 ProposalInfo { 557 proposal: &self.proposal, 558 sender: self.sender, 559 source: self.source.clone(), 560 } 561 } 562 563 #[inline(always)] is_by_value(&self) -> bool564 pub fn is_by_value(&self) -> bool { 565 self.source == ProposalSource::ByValue 566 } 567 568 #[inline(always)] is_by_reference(&self) -> bool569 pub fn is_by_reference(&self) -> bool { 570 !self.is_by_value() 571 } 572 573 /// The [`ProposalRef`] of this proposal if its source is [`ProposalSource::ByReference`] 574 #[cfg(feature = "by_ref_proposal")] proposal_ref(&self) -> Option<&ProposalRef>575 pub fn proposal_ref(&self) -> Option<&ProposalRef> { 576 match self.source { 577 ProposalSource::ByReference(ref reference) => Some(reference), 578 _ => None, 579 } 580 } 581 } 582 583 #[cfg(all(feature = "ffi", not(test)))] 584 safer_ffi_gen::specialize!(ProposalInfoFfi = ProposalInfo<Proposal>); 585 586 pub trait Proposable: Sized { 587 const TYPE: ProposalType; 588 filter(bundle: &ProposalBundle) -> &[ProposalInfo<Self>]589 fn filter(bundle: &ProposalBundle) -> &[ProposalInfo<Self>]; remove(bundle: &mut ProposalBundle, index: usize)590 fn remove(bundle: &mut ProposalBundle, index: usize); retain<F>(bundle: &mut ProposalBundle, keep: F) where F: FnMut(&ProposalInfo<Self>) -> bool591 fn retain<F>(bundle: &mut ProposalBundle, keep: F) 592 where 593 F: FnMut(&ProposalInfo<Self>) -> bool; 594 } 595 596 macro_rules! impl_proposable { 597 ($ty:ty, $proposal_type:ident, $field:ident) => { 598 impl Proposable for $ty { 599 const TYPE: ProposalType = ProposalType::$proposal_type; 600 601 fn filter(bundle: &ProposalBundle) -> &[ProposalInfo<Self>] { 602 &bundle.$field 603 } 604 605 fn remove(bundle: &mut ProposalBundle, index: usize) { 606 if index < bundle.$field.len() { 607 bundle.$field.remove(index); 608 } 609 } 610 611 fn retain<F>(bundle: &mut ProposalBundle, keep: F) 612 where 613 F: FnMut(&ProposalInfo<Self>) -> bool, 614 { 615 bundle.$field.retain(keep); 616 } 617 } 618 }; 619 } 620 621 impl_proposable!(AddProposal, ADD, additions); 622 #[cfg(feature = "by_ref_proposal")] 623 impl_proposable!(UpdateProposal, UPDATE, updates); 624 impl_proposable!(RemoveProposal, REMOVE, removals); 625 #[cfg(feature = "psk")] 626 impl_proposable!(PreSharedKeyProposal, PSK, psks); 627 impl_proposable!(ReInitProposal, RE_INIT, reinitializations); 628 impl_proposable!(ExternalInit, EXTERNAL_INIT, external_initializations); 629 impl_proposable!( 630 ExtensionList, 631 GROUP_CONTEXT_EXTENSIONS, 632 group_context_extensions 633 ); 634