From aef1bdf5886298584c36cb735133ca0cc8ae310a Mon Sep 17 00:00:00 2001 From: "Aode (Lion)" Date: Mon, 17 Jan 2022 14:02:18 -0600 Subject: [PATCH] Start checking again --- .drone.yml | 52 ++++++- CHANGELOG.md | 6 + Cargo.toml | 4 +- activitystreams-kinds/Cargo.toml | 4 +- src/activity.rs | 60 +++++++- src/actor.rs | 242 ++++++++++++++++++++++++++++--- src/base.rs | 97 ++++++++++++- src/checked.rs | 19 +++ src/lib.rs | 1 + src/primitives/one_or_many.rs | 18 +++ 10 files changed, 460 insertions(+), 43 deletions(-) create mode 100644 src/checked.rs diff --git a/.drone.yml b/.drone.yml index 9bdc445..7c9f446 100644 --- a/.drone.yml +++ b/.drone.yml @@ -39,7 +39,7 @@ trigger: kind: pipeline type: docker -name: tests +name: tests-amd64 platform: arch: amd64 @@ -70,6 +70,39 @@ trigger: --- +kind: pipeline +type: docker +name: tests-arm64v8 + +platform: + arch: arm64 + +clone: + disable: true + +steps: +- name: clone + image: alpine/git:latest + user: root + commands: + - git clone $DRONE_GIT_HTTP_URL . + - git checkout $DRONE_COMMIT + - chown -R 991:991 . + +- name: tests + image: asonix/rust-builder:latest-linux-amd64 + pull: always + commands: + - cargo test + +trigger: + event: + - tag + - push + - pull_request + +--- + kind: pipeline type: docker name: check-amd64 @@ -210,6 +243,11 @@ trigger: event: - tag +depends_on: +- clippy +- tests-amd64 +- tests-arm64v8 + --- kind: pipeline @@ -242,6 +280,11 @@ trigger: event: - tag +depends_on: +- clippy +- tests-amd64 +- tests-arm64v8 + --- kind: pipeline @@ -274,6 +317,11 @@ trigger: event: - tag +depends_on: +- clippy +- tests-amd64 +- tests-arm64v8 + --- kind: pipeline @@ -328,8 +376,6 @@ steps: - cargo publish -p activitystreams-ext --token $CRATES_IO_TOKEN depends_on: -- clippy -- tests - build-amd64 - build-arm64v8 - build-arm32v7 diff --git a/CHANGELOG.md b/CHANGELOG.md index bc281ac..46d533f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ # Unreleased +- implement `IntoIterator` for `&OneOrMany` and `&mut OneOrMany` +- add `check` function for verifying an IRI's authority +- add `BaseExt::check_authority` for verifying an IRI's authority against an object's ID +- add back checked `Base::id`, `Activity::actor`, `Activity::object`, `Actor::inbox`, + `Actor::outbox`, `Actor::following`, `Actor::followers`, `Actor::liked`, `Actor::streams`, + `Actor::endpoints` # 0.7.0-alpha.14 - switch to iri-string from url diff --git a/Cargo.toml b/Cargo.toml index 89e8a8b..4422ed0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ members = [ ] [dependencies] -activitystreams-kinds = { version = "0.1.0", path = "./activitystreams-kinds/", default-features = false, features = ["iri-string"] } -iri-string = { version = "0.4.1", features = ["serde", "serde-std"] } +activitystreams-kinds = { version = "0.2.0", path = "./activitystreams-kinds/", default-features = false, features = ["iri-string"] } +iri-string = { version = "0.5.0-beta.1", features = ["alloc", "serde", "serde-alloc", "serde-std", "std"] } mime = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/activitystreams-kinds/Cargo.toml b/activitystreams-kinds/Cargo.toml index ddbb8a5..127d4bc 100644 --- a/activitystreams-kinds/Cargo.toml +++ b/activitystreams-kinds/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "activitystreams-kinds" description = "Type-safe activitystreams 'type' values" -version = "0.1.3" +version = "0.2.0" license = "GPL-3.0" authors = ["asonix "] repository = "https://git.asonix.dog/asonix/activitystreams" @@ -17,7 +17,7 @@ default = ["url"] [dependencies] serde = { version = "1", features = ["derive"] } url = { version = "2", optional = true } -iri-string = { version = "0.4.1", optional = true } +iri-string = { version = "0.5.0-beta.1", optional = true } [dev-dependencies] anyhow = "1" diff --git a/src/activity.rs b/src/activity.rs index d9cf83e..10d2bfb 100644 --- a/src/activity.rs +++ b/src/activity.rs @@ -24,8 +24,10 @@ //! ``` use crate::{ base::{AnyBase, AsBase, Base, Extends}, + checked::CheckError, markers, object::{ApObject, AsObject, Object}, + prelude::BaseExt, primitives::OneOrMany, unparsed::{Unparsed, UnparsedMut, UnparsedMutExt}, }; @@ -420,7 +422,32 @@ pub trait ActorAndObjectRefExt: ActorAndObjectRef { /// let actor_ref = create.actor(); /// println!("{:?}", actor_ref); /// ``` - fn actor(&self) -> &OneOrMany { + fn actor(&self) -> Result<&OneOrMany, CheckError> + where + Self: BaseExt, + { + let actor = self.actor_unchecked(); + + for any_base in actor { + let id = any_base.id().ok_or(CheckError)?; + self.check_authority(id)?; + } + + Ok(actor) + } + + /// Fetch the actor for the current activity + /// + /// ```rust + /// # use activitystreams::{context, activity::Create}; + /// # let mut create = Create::new(context(), context()); + /// # + /// use activitystreams::prelude::*; + /// + /// let actor_ref = create.actor_unchecked(); + /// println!("{:?}", actor_ref); + /// ``` + fn actor_unchecked(&self) -> &OneOrMany { self.actor_field_ref() } @@ -439,7 +466,7 @@ pub trait ActorAndObjectRefExt: ActorAndObjectRef { /// # } /// ``` fn actor_is(&self, id: &IriString) -> bool { - self.actor().is_single_id(id) + self.actor_unchecked().is_single_id(id) } /// Set the actor for the current activity @@ -526,7 +553,32 @@ pub trait ActorAndObjectRefExt: ActorAndObjectRef { /// let object_ref = create.object(); /// println!("{:?}", object_ref); /// ``` - fn object(&self) -> &OneOrMany { + fn object(&self) -> Result<&OneOrMany, CheckError> + where + Self: BaseExt, + { + let object = self.object_unchecked(); + + for any_base in object { + let id = any_base.id().ok_or(CheckError)?; + self.check_authority(id)?; + } + + Ok(object) + } + + /// Fetch the object for the current activity + /// + /// ```rust + /// # use activitystreams::{context, activity::Create}; + /// # let mut create = Create::new(context(), context()); + /// # + /// use activitystreams::prelude::*; + /// + /// let object_ref = create.object_unchecked(); + /// println!("{:?}", object_ref); + /// ``` + fn object_unchecked(&self) -> &OneOrMany { self.object_field_ref() } @@ -545,7 +597,7 @@ pub trait ActorAndObjectRefExt: ActorAndObjectRef { /// # } /// ``` fn object_is(&self, id: &IriString) -> bool { - self.object().is_single_id(id) + self.object_unchecked().is_single_id(id) } /// Set the object for the current activity diff --git a/src/actor.rs b/src/actor.rs index 5297871..8142c4b 100644 --- a/src/actor.rs +++ b/src/actor.rs @@ -23,8 +23,10 @@ //! ``` use crate::{ base::{AsBase, Base, Extends}, + checked::CheckError, markers, object::{ApObject, AsApObject, AsObject, Object}, + prelude::BaseExt, primitives::OneOrMany, unparsed::{Unparsed, UnparsedMut, UnparsedMutExt}, }; @@ -60,7 +62,25 @@ pub trait ApActorExt: AsApActor { /// /// let inbox_ref = person.inbox(); /// ``` - fn inbox<'a>(&'a self) -> &'a IriString + fn inbox<'a, Kind>(&'a self) -> Result<&'a IriString, CheckError> + where + Inner: 'a, + Self: BaseExt, + { + let inbox = self.inbox_unchecked(); + self.check_authority(inbox) + } + + /// Fetch the inbox for the current actor + /// + /// ```rust + /// # use activitystreams::{actor::{ApActor, Person}, context}; + /// # let mut person = ApActor::new(context(), Person::new()); + /// use activitystreams::prelude::*; + /// + /// let inbox_ref = person.inbox_unchecked(); + /// ``` + fn inbox_unchecked<'a>(&'a self) -> &'a IriString where Inner: 'a, { @@ -109,7 +129,26 @@ pub trait ApActorExt: AsApActor { /// /// let outbox_ref = person.outbox(); /// ``` - fn outbox<'a>(&'a self) -> Option<&'a IriString> + fn outbox<'a, Kind>(&'a self) -> Result, CheckError> + where + Inner: 'a, + Self: BaseExt, + { + self.outbox_unchecked() + .map(|outbox| self.check_authority(outbox)) + .transpose() + } + + /// Fetch the outbox for the current actor + /// + /// ```rust + /// # use activitystreams::{actor::{ApActor, Person}, context}; + /// # let mut person = ApActor::new(context(), Person::new()); + /// use activitystreams::prelude::*; + /// + /// let outbox_ref = person.outbox_unchecked(); + /// ``` + fn outbox_unchecked<'a>(&'a self) -> Option<&'a IriString> where Inner: 'a, { @@ -175,9 +214,9 @@ pub trait ApActorExt: AsApActor { /// # person.set_outbox(iri!("https://example.com/outbox")); /// use activitystreams::prelude::*; /// - /// assert!(person.outbox().is_some()); + /// assert!(person.outbox_unchecked().is_some()); /// person.delete_outbox(); - /// assert!(person.outbox().is_none()); + /// assert!(person.outbox_unchecked().is_none()); /// # Ok(()) /// # } /// ``` @@ -193,11 +232,32 @@ pub trait ApActorExt: AsApActor { /// # let mut person = ApActor::new(context(), Person::new()); /// use activitystreams::prelude::*; /// - /// if let Some(following) = person.following() { + /// if let Ok(following) = person.following() { /// println!("{:?}", following); /// } /// ``` - fn following<'a>(&'a self) -> Option<&'a IriString> + fn following<'a, Kind>(&'a self) -> Result, CheckError> + where + Inner: 'a, + Self: BaseExt, + { + self.following_unchecked() + .map(|following| self.check_authority(following)) + .transpose() + } + + /// Fetch the following link for the current actor + /// + /// ```rust + /// # use activitystreams::{actor::{ApActor, Person}, context}; + /// # let mut person = ApActor::new(context(), Person::new()); + /// use activitystreams::prelude::*; + /// + /// if let Some(following) = person.following_unchecked() { + /// println!("{:?}", following); + /// } + /// ``` + fn following_unchecked<'a>(&'a self) -> Option<&'a IriString> where Inner: 'a, { @@ -263,9 +323,9 @@ pub trait ApActorExt: AsApActor { /// # person.set_following(iri!("https://example.com/following")); /// use activitystreams::prelude::*; /// - /// assert!(person.following().is_some()); + /// assert!(person.following_unchecked().is_some()); /// person.delete_following(); - /// assert!(person.following().is_none()); + /// assert!(person.following_unchecked().is_none()); /// # Ok(()) /// # } /// ``` @@ -281,11 +341,32 @@ pub trait ApActorExt: AsApActor { /// # let mut person = ApActor::new(context(), Person::new()); /// use activitystreams::prelude::*; /// - /// if let Some(followers) = person.followers() { + /// if let Ok(followers) = person.followers() { /// println!("{:?}", followers); /// } /// ``` - fn followers<'a>(&'a self) -> Option<&'a IriString> + fn followers<'a, Kind>(&'a self) -> Result, CheckError> + where + Inner: 'a, + Self: BaseExt, + { + self.followers_unchecked() + .map(|followers| self.check_authority(followers)) + .transpose() + } + + /// Fetch the followers link for the current actor + /// + /// ```rust + /// # use activitystreams::{actor::{ApActor, Person}, context}; + /// # let mut person = ApActor::new(context(), Person::new()); + /// use activitystreams::prelude::*; + /// + /// if let Some(followers) = person.followers_unchecked() { + /// println!("{:?}", followers); + /// } + /// ``` + fn followers_unchecked<'a>(&'a self) -> Option<&'a IriString> where Inner: 'a, { @@ -351,9 +432,9 @@ pub trait ApActorExt: AsApActor { /// # person.set_followers(iri!("https://example.com/followers")); /// use activitystreams::prelude::*; /// - /// assert!(person.followers().is_some()); + /// assert!(person.followers_unchecked().is_some()); /// person.delete_followers(); - /// assert!(person.followers().is_none()); + /// assert!(person.followers_unchecked().is_none()); /// # Ok(()) /// # } /// ``` @@ -369,11 +450,32 @@ pub trait ApActorExt: AsApActor { /// # let mut person = ApActor::new(context(), Person::new()); /// use activitystreams::prelude::*; /// - /// if let Some(liked) = person.liked() { + /// if let Ok(liked) = person.liked() { /// println!("{:?}", liked); /// } /// ``` - fn liked<'a>(&'a self) -> Option<&'a IriString> + fn liked<'a, Kind>(&'a self) -> Result, CheckError> + where + Inner: 'a, + Self: BaseExt, + { + self.liked_unchecked() + .map(|liked| self.check_authority(liked)) + .transpose() + } + + /// Fetch the liked link for the current actor + /// + /// ```rust + /// # use activitystreams::{actor::{ApActor, Person}, context}; + /// # let mut person = ApActor::new(context(), Person::new()); + /// use activitystreams::prelude::*; + /// + /// if let Some(liked) = person.liked_unchecked() { + /// println!("{:?}", liked); + /// } + /// ``` + fn liked_unchecked<'a>(&'a self) -> Option<&'a IriString> where Inner: 'a, { @@ -439,9 +541,9 @@ pub trait ApActorExt: AsApActor { /// # person.set_liked(iri!("https://example.com/liked")); /// use activitystreams::prelude::*; /// - /// assert!(person.liked().is_some()); + /// assert!(person.liked_unchecked().is_some()); /// person.delete_liked(); - /// assert!(person.liked().is_none()); + /// assert!(person.liked_unchecked().is_none()); /// # Ok(()) /// # } /// ``` @@ -457,11 +559,38 @@ pub trait ApActorExt: AsApActor { /// # let mut person = ApActor::new(context(), Person::new()); /// use activitystreams::prelude::*; /// - /// if let Some(streams) = person.streams() { + /// if let Ok(streams) = person.streams() { /// println!("{:?}", streams); /// } /// ``` - fn streams<'a>(&'a self) -> Option> + fn streams<'a, Kind>(&'a self) -> Result>, CheckError> + where + Inner: 'a, + Self: BaseExt, + { + if let Some(streams) = self.streams_unchecked() { + for id in &streams { + self.check_authority(id)?; + } + + Ok(Some(streams)) + } else { + Ok(None) + } + } + + /// Fetch the streams links for the current actor + /// + /// ```rust + /// # use activitystreams::{actor::{ApActor, Person}, context}; + /// # let mut person = ApActor::new(context(), Person::new()); + /// use activitystreams::prelude::*; + /// + /// if let Some(streams) = person.streams_unchecked() { + /// println!("{:?}", streams); + /// } + /// ``` + fn streams_unchecked<'a>(&'a self) -> Option> where Inner: 'a, { @@ -581,9 +710,9 @@ pub trait ApActorExt: AsApActor { /// # person.set_stream(iri!("https://example.com/streams")); /// use activitystreams::prelude::*; /// - /// assert!(person.streams().is_some()); + /// assert!(person.streams_unchecked().is_some()); /// person.delete_streams(); - /// assert!(person.streams().is_none()); + /// assert!(person.streams_unchecked().is_none()); /// # Ok(()) /// # } /// ``` @@ -662,6 +791,71 @@ pub trait ApActorExt: AsApActor { self } + /// Fetch the endpoints for the current actor, erroring if the Endpoints' domains do not + /// match the ID's domain + /// + /// ```rust + /// # fn main() -> Result<(), anyhow::Error> { + /// # use activitystreams::{actor::{ApActor, Endpoints, Person}, context}; + /// # let mut person = ApActor::new(context(), Person::new()); + /// # person.set_id(context()).set_endpoints(Endpoints { + /// # shared_inbox: Some(context()), + /// # ..Default::default() + /// # }); + /// use activitystreams::prelude::*; + /// + /// if let Some(endpoints) = person.endpoints()? { + /// println!("{:?}", endpoints); + /// } + /// # Ok(()) + /// # } + /// ``` + fn endpoints<'a, Kind>(&'a self) -> Result>, CheckError> + where + Self: BaseExt, + Inner: 'a, + Kind: 'a, + { + if let Some(endpoints) = self.endpoints_unchecked() { + let authority_opt = self.id_unchecked().and_then(|id| id.authority_components()); + + let mut any_failed = false; + + any_failed |= endpoints + .proxy_url + .map(|u| u.authority_components() != authority_opt) + .unwrap_or(false); + any_failed |= endpoints + .oauth_authorization_endpoint + .map(|u| u.authority_components() != authority_opt) + .unwrap_or(false); + any_failed |= endpoints + .oauth_token_endpoint + .map(|u| u.authority_components() != authority_opt) + .unwrap_or(false); + any_failed |= endpoints + .provide_client_key + .map(|u| u.authority_components() != authority_opt) + .unwrap_or(false); + any_failed |= endpoints + .sign_client_key + .map(|u| u.authority_components() != authority_opt) + .unwrap_or(false); + any_failed |= endpoints + .shared_inbox + .map(|u| u.authority_components() != authority_opt) + .unwrap_or(false); + + if any_failed { + return Err(CheckError); + } + + return Ok(Some(endpoints)); + } + + Ok(None) + } + /// Fetch the endpoints for the current actor /// /// ```rust @@ -669,11 +863,11 @@ pub trait ApActorExt: AsApActor { /// # let mut person = ApActor::new(context(), Person::new()); /// use activitystreams::prelude::*; /// - /// if let Some(endpoints) = person.endpoints() { + /// if let Some(endpoints) = person.endpoints_unchecked() { /// println!("{:?}", endpoints); /// } /// ``` - fn endpoints<'a>(&'a self) -> Option> + fn endpoints_unchecked<'a>(&'a self) -> Option> where Inner: 'a, { @@ -744,9 +938,9 @@ pub trait ApActorExt: AsApActor { /// # person.set_endpoints(Default::default()); /// use activitystreams::prelude::*; /// - /// assert!(person.endpoints().is_some()); + /// assert!(person.endpoints_unchecked().is_some()); /// person.delete_endpoints(); - /// assert!(person.endpoints().is_none()); + /// assert!(person.endpoints_unchecked().is_none()); /// ``` fn delete_endpoints(&mut self) -> &mut Self { self.ap_actor_mut().endpoints = None; diff --git a/src/base.rs b/src/base.rs index c94334e..cca27af 100644 --- a/src/base.rs +++ b/src/base.rs @@ -28,12 +28,13 @@ //! # } //! ``` use crate::{ + checked::{check, CheckError}, either::Either, markers, primitives::{AnyString, MimeMediaType, OneOrMany}, unparsed::{Unparsed, UnparsedMut}, }; -use iri_string::types::IriString; +use iri_string::types::{IriStr, IriString}; use mime::Mime; /// Implements conversion between `Base` and other ActivityStreams objects defined in this @@ -245,6 +246,24 @@ pub trait BaseExt: AsBase { self } + /// Check the authority of a given IRI matches this object's ID + /// + /// ```rust + /// # use activitystreams::{base::BaseExt, object::Video, iri}; + /// # fn main() -> anyhow::Result<()> { + /// # let video = Video::new(); + /// let res = video.check_authority(&iri!("https://example.com")); + /// # Ok(()) + /// # } + /// ``` + fn check_authority<'a, T: AsRef>(&self, iri: T) -> Result { + let authority = self + .id_unchecked() + .and_then(|id| id.authority_components()) + .ok_or(CheckError)?; + check(iri, authority.host(), authority.port()) + } + /// Fetch the id for the current object /// /// ```rust @@ -253,11 +272,40 @@ pub trait BaseExt: AsBase { /// # /// use activitystreams::prelude::*; /// - /// if let Some(id) = video.id() { + /// if let Ok(Some(id)) = video.id("exmaple.com", Some("443")) { /// println!("{:?}", id); /// } /// ``` - fn id<'a>(&'a self) -> Option<&'a IriString> + fn id<'a>(&'a self, host: &str, port: Option<&str>) -> Result, CheckError> + where + Kind: 'a, + { + self.id_unchecked() + .and_then(|id| { + let authority = id.authority_components()?; + + if authority.host() == host && authority.port() == port { + Some(Ok(id)) + } else { + Some(Err(CheckError)) + } + }) + .transpose() + } + + /// Fetch the id for the current object + /// + /// ```rust + /// # use activitystreams::object::Video; + /// # let mut video = Video::new(); + /// # + /// use activitystreams::prelude::*; + /// + /// if let Some(id) = video.id_unchecked() { + /// println!("{:?}", id); + /// } + /// ``` + fn id_unchecked<'a>(&'a self) -> Option<&'a IriString> where Kind: 'a, { @@ -272,11 +320,44 @@ pub trait BaseExt: AsBase { /// # /// use activitystreams::prelude::*; /// - /// if let Some(id) = video.id_mut() { + /// if let Ok(Some(id)) = video.id_mut("example.com", Some("443")) { /// println!("{:?}", id); /// } /// ``` - fn id_mut<'a>(&'a mut self) -> Option<&'a mut IriString> + fn id_mut<'a>( + &'a mut self, + host: &str, + port: Option<&str>, + ) -> Result, CheckError> + where + Kind: 'a, + { + self.id_mut_unchecked() + .and_then(|id| { + let authority = id.authority_components()?; + + if authority.host() == host && authority.port() == port { + Some(Ok(id)) + } else { + Some(Err(CheckError)) + } + }) + .transpose() + } + + /// Mutably borrow the ID from the current object + /// + /// ```rust + /// # use activitystreams::object::Video; + /// # let mut video = Video::new(); + /// # + /// use activitystreams::prelude::*; + /// + /// if let Some(id) = video.id_mut_unchecked() { + /// println!("{:?}", id); + /// } + /// ``` + fn id_mut_unchecked<'a>(&'a mut self) -> Option<&'a mut IriString> where Kind: 'a, { @@ -296,7 +377,7 @@ pub trait BaseExt: AsBase { /// # } /// ``` fn is_id(&self, id: &IriString) -> bool { - self.id() == Some(id) + self.id_unchecked() == Some(id) } /// Set the id for the current object @@ -344,9 +425,9 @@ pub trait BaseExt: AsBase { /// # /// use activitystreams::prelude::*; /// - /// assert!(video.id().is_some()); + /// assert!(video.id_unchecked().is_some()); /// video.delete_id(); - /// assert!(video.id().is_none()); + /// assert!(video.id_unchecked().is_none()); /// ``` fn delete_id(&mut self) -> &mut Self { self.base_mut().id = None; diff --git a/src/checked.rs b/src/checked.rs new file mode 100644 index 0000000..d722e99 --- /dev/null +++ b/src/checked.rs @@ -0,0 +1,19 @@ +use iri_string::types::IriStr; + +#[derive(Debug, thiserror::Error)] +#[error("IRI failed host and port check")] +pub struct CheckError; + +pub(crate) fn check<'a, T: AsRef>( + iri: T, + host: &str, + port: Option<&str>, +) -> Result { + let authority = iri.as_ref().authority_components().ok_or(CheckError)?; + + if authority.host() != host || authority.port() != port { + return Err(CheckError); + } + + Ok(iri) +} diff --git a/src/lib.rs b/src/lib.rs index 0ed4c22..12adcc8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -298,6 +298,7 @@ pub mod activity; pub mod actor; pub mod base; +pub mod checked; pub mod collection; mod either; pub mod link; diff --git a/src/primitives/one_or_many.rs b/src/primitives/one_or_many.rs index 01b7c77..5b12b6e 100644 --- a/src/primitives/one_or_many.rs +++ b/src/primitives/one_or_many.rs @@ -362,6 +362,24 @@ impl IntoIterator for OneOrMany { } } +impl<'a, T> IntoIterator for &'a OneOrMany { + type Item = &'a T; + type IntoIter = Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl<'a, T> IntoIterator for &'a mut OneOrMany { + type Item = &'a mut T; + type IntoIter = IterMut<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + impl<'a, T> Iterator for Iter<'a, T> { type Item = &'a T;