diff --git a/src/ape/tag/mod.rs b/src/ape/tag/mod.rs index 17d59573..336795cb 100644 --- a/src/ape/tag/mod.rs +++ b/src/ape/tag/mod.rs @@ -6,7 +6,7 @@ use crate::ape::tag::item::{ApeItem, ApeItemRef}; use crate::error::{LoftyError, Result}; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{Tag, TagType}; -use crate::traits::{Accessor, TagExt}; +use crate::traits::{Accessor, SplitAndRejoinTag, TagExt}; use std::borrow::Cow; use std::convert::TryInto; @@ -289,8 +289,8 @@ impl TagExt for ApeTag { } } -impl From for Tag { - fn from(input: ApeTag) -> Self { +impl SplitAndRejoinTag for ApeTag { + fn split_tag(&mut self) -> Tag { fn split_pair( content: &str, tag: &mut Tag, @@ -312,7 +312,7 @@ impl From for Tag { let mut tag = Tag::new(TagType::APE); - for item in input.items { + for item in std::mem::take(&mut self.items) { let item_key = ItemKey::from_key(TagType::APE, item.key()); // The text pairs need some special treatment @@ -321,13 +321,13 @@ impl From for Tag { if split_pair(val, &mut tag, ItemKey::TrackNumber, ItemKey::TrackTotal) .is_some() => { - continue + continue; // Item consumed }, (ItemKey::DiscNumber | ItemKey::DiscTotal, ItemValue::Text(val)) if split_pair(val, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal) .is_some() => { - continue + continue; // Item consumed }, (ItemKey::MovementNumber | ItemKey::MovementTotal, ItemValue::Text(val)) if split_pair( @@ -338,36 +338,46 @@ impl From for Tag { ) .is_some() => { - continue + continue; // Item consumed + }, + (k, _) => { + tag.items.push(TagItem::new(k, item.value)); }, - (k, _) => tag.items.push(TagItem::new(k, item.value)), } } tag } + + fn rejoin_tag(&mut self, tag: Tag) { + for item in tag.items { + if let Ok(ape_item) = item.try_into() { + self.insert(ape_item) + } + } + + for pic in tag.pictures { + if let Some(key) = pic.pic_type.as_ape_key() { + if let Ok(item) = + ApeItem::new(key.to_string(), ItemValue::Binary(pic.as_ape_bytes())) + { + self.insert(item) + } + } + } + } +} + +impl From for Tag { + fn from(mut input: ApeTag) -> Self { + input.split_tag() + } } impl From for ApeTag { fn from(input: Tag) -> Self { let mut ape_tag = Self::default(); - - for item in input.items { - if let Ok(ape_item) = item.try_into() { - ape_tag.insert(ape_item) - } - } - - for pic in input.pictures { - if let Some(key) = pic.pic_type.as_ape_key() { - if let Ok(item) = - ApeItem::new(key.to_string(), ItemValue::Binary(pic.as_ape_bytes())) - { - ape_tag.insert(item) - } - } - } - + ape_tag.rejoin_tag(input); ape_tag } } diff --git a/src/id3/v1/tag.rs b/src/id3/v1/tag.rs index b3a95a58..5edda140 100644 --- a/src/id3/v1/tag.rs +++ b/src/id3/v1/tag.rs @@ -2,7 +2,7 @@ use crate::error::{LoftyError, Result}; use crate::id3::v1::constants::GENRES; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{Tag, TagType}; -use crate::traits::{Accessor, TagExt}; +use crate::traits::{Accessor, SplitAndRejoinTag, TagExt}; use std::borrow::Cow; use std::fs::{File, OpenOptions}; @@ -238,26 +238,32 @@ impl TagExt for ID3v1Tag { } } -impl From for Tag { - fn from(input: ID3v1Tag) -> Self { - let mut tag = Self::new(TagType::ID3v1); +impl SplitAndRejoinTag for ID3v1Tag { + fn split_tag(&mut self) -> Tag { + let mut tag = Tag::new(TagType::ID3v1); - input.title.map(|t| tag.insert_text(ItemKey::TrackTitle, t)); - input - .artist + self.title + .take() + .map(|t| tag.insert_text(ItemKey::TrackTitle, t)); + self.artist + .take() .map(|a| tag.insert_text(ItemKey::TrackArtist, a)); - input.album.map(|a| tag.insert_text(ItemKey::AlbumTitle, a)); - input.year.map(|y| tag.insert_text(ItemKey::Year, y)); - input.comment.map(|c| tag.insert_text(ItemKey::Comment, c)); + self.album + .take() + .map(|a| tag.insert_text(ItemKey::AlbumTitle, a)); + self.year.take().map(|y| tag.insert_text(ItemKey::Year, y)); + self.comment + .take() + .map(|c| tag.insert_text(ItemKey::Comment, c)); - if let Some(t) = input.track_number { + if let Some(t) = self.track_number.take() { tag.items.push(TagItem::new( ItemKey::TrackNumber, ItemValue::Text(t.to_string()), )) } - if let Some(genre_index) = input.genre { + if let Some(genre_index) = self.genre.take() { if let Some(genre) = GENRES.get(genre_index as usize) { tag.insert_text(ItemKey::Genre, (*genre).to_string()); } @@ -265,6 +271,16 @@ impl From for Tag { tag } + + fn rejoin_tag(&mut self, tag: Tag) { + *self = tag.into(); + } +} + +impl From for Tag { + fn from(mut input: ID3v1Tag) -> Self { + input.split_tag() + } } impl From for ID3v1Tag { diff --git a/src/id3/v2/tag.rs b/src/id3/v2/tag.rs index d3c32d8e..c6e43bec 100644 --- a/src/id3/v2/tag.rs +++ b/src/id3/v2/tag.rs @@ -6,10 +6,10 @@ use crate::error::{LoftyError, Result}; use crate::id3::v2::frame::FrameRef; use crate::id3::v2::items::encoded_text_frame::EncodedTextFrame; use crate::id3::v2::items::language_frame::LanguageFrame; -use crate::picture::{Picture, PictureType}; +use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE}; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{Tag, TagType}; -use crate::traits::{Accessor, TagExt}; +use crate::traits::{Accessor, SplitAndRejoinTag, TagExt}; use crate::util::text::TextEncoding; use std::borrow::Cow; @@ -533,8 +533,8 @@ impl TagExt for ID3v2Tag { } } -impl From for Tag { - fn from(input: ID3v2Tag) -> Self { +impl SplitAndRejoinTag for ID3v2Tag { + fn split_tag(&mut self) -> Tag { fn split_pair( content: &str, tag: &mut Tag, @@ -555,13 +555,13 @@ impl From for Tag { Some(()) } - let mut tag = Self::new(TagType::ID3v2); + let mut tag = Tag::new(TagType::ID3v2); - for frame in input.frames { - let id = frame.id; + self.frames.retain_mut(|frame| { + let id = &frame.id; // The text pairs need some special treatment - match (id.as_str(), frame.value) { + match (id.as_str(), &mut frame.value) { ("TRCK", FrameValue::Text { value: content, .. }) if split_pair( &content, @@ -571,13 +571,13 @@ impl From for Tag { ) .is_some() => { - continue + false // Frame consumed }, ("TPOS", FrameValue::Text { value: content, .. }) if split_pair(&content, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal) .is_some() => { - continue + false // Frame consumed }, ("MVIN", FrameValue::Text { value: content, .. }) if split_pair( @@ -588,7 +588,7 @@ impl From for Tag { ) .is_some() => { - continue + false // Frame consumed }, // Store TXXX/WXXX frames by their descriptions, rather than their IDs ( @@ -606,6 +606,7 @@ impl From for Tag { ItemValue::Text(c.to_string()), )); } + false // Frame consumed }, ( "WXXX", @@ -622,6 +623,7 @@ impl From for Tag { ItemValue::Locator(c.to_string()), )); } + false // Frame consumed }, (id, value) => { let item_key = ItemKey::from_key(TagType::ID3v2, id); @@ -642,13 +644,14 @@ impl From for Tag { description, .. }) => { - if description == EMPTY_CONTENT_DESCRIPTOR { + if *description == EMPTY_CONTENT_DESCRIPTOR { for c in content.split(V4_MULTI_VALUE_SEPARATOR) { tag.items.push(TagItem::new( item_key.clone(), ItemValue::Text(c.to_string()), )); } + return false; // Frame consumed } // ...else do not convert text frames with a non-empty content // descriptor that would otherwise unintentionally be modified @@ -656,8 +659,7 @@ impl From for Tag { // TODO: How to convert these frames consistently and safely // such that the content descriptor is preserved during read/write // round trips? - - continue; + return true; // Keep frame }, FrameValue::Text { value: content, .. } => { for c in content.split(V4_MULTI_VALUE_SEPARATOR) { @@ -666,34 +668,34 @@ impl From for Tag { ItemValue::Text(c.to_string()), )); } - - continue; + return false; // Frame consumed }, FrameValue::URL(content) - | FrameValue::UserURL(EncodedTextFrame { content, .. }) => ItemValue::Locator(content), + | FrameValue::UserURL(EncodedTextFrame { content, .. }) => { + ItemValue::Locator(std::mem::take(content)) + }, FrameValue::Picture { picture, .. } => { - tag.push_picture(picture); - continue; + tag.push_picture(std::mem::replace(picture, TOMBSTONE_PICTURE)); + return false; // Frame consumed }, FrameValue::Popularimeter(popularimeter) => { ItemValue::Binary(popularimeter.as_bytes()) }, - FrameValue::Binary(binary) => ItemValue::Binary(binary), + FrameValue::Binary(binary) => ItemValue::Binary(std::mem::take(binary)), }; tag.items.push(TagItem::new(item_key, item_value)); + false // Frame consumed }, } - } + }); tag } -} -impl From for ID3v2Tag { - fn from(mut input: Tag) -> Self { - fn join_items(input: &mut Tag, key: &ItemKey) -> String { - let mut iter = input.take_strings(key); + fn rejoin_tag(&mut self, mut tag: Tag) { + fn join_items(tag: &mut Tag, key: &ItemKey) -> String { + let mut iter = tag.take_strings(key); match iter.next() { None => String::new(), @@ -710,25 +712,22 @@ impl From for ID3v2Tag { } } - let mut id3v2_tag = ID3v2Tag { - frames: Vec::with_capacity(input.item_count() as usize), - ..ID3v2Tag::default() - }; + self.frames.reserve(tag.item_count() as usize); - let artists = join_items(&mut input, &ItemKey::TrackArtist); - id3v2_tag.set_artist(artists); + let artists = join_items(&mut tag, &ItemKey::TrackArtist); + self.set_artist(artists); - for item in input.items { + for item in tag.items { let frame: Frame<'_> = match item.into() { Some(frame) => frame, None => continue, }; - id3v2_tag.insert(frame); + self.insert(frame); } - for picture in input.pictures { - id3v2_tag.frames.push(Frame { + for picture in tag.pictures { + self.frames.push(Frame { id: FrameID::Valid(Cow::Borrowed("APIC")), value: FrameValue::Picture { encoding: TextEncoding::UTF8, @@ -737,7 +736,19 @@ impl From for ID3v2Tag { flags: FrameFlags::default(), }) } + } +} +impl From for Tag { + fn from(mut input: ID3v2Tag) -> Self { + input.split_tag() + } +} + +impl From for ID3v2Tag { + fn from(input: Tag) -> Self { + let mut id3v2_tag = ID3v2Tag::default(); + id3v2_tag.rejoin_tag(input); id3v2_tag } } diff --git a/src/iff/aiff/tag.rs b/src/iff/aiff/tag.rs index bc9c8375..c5fc1af8 100644 --- a/src/iff/aiff/tag.rs +++ b/src/iff/aiff/tag.rs @@ -3,7 +3,7 @@ use crate::iff::chunk::Chunks; use crate::macros::err; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{Tag, TagType}; -use crate::traits::{Accessor, TagExt}; +use crate::traits::{Accessor, SplitAndRejoinTag, TagExt}; use std::borrow::Cow; use std::convert::TryFrom; @@ -216,9 +216,19 @@ impl TagExt for AIFFTextChunks { } } +impl SplitAndRejoinTag for AIFFTextChunks { + fn split_tag(&mut self) -> Tag { + std::mem::take(self).into() + } + + fn rejoin_tag(&mut self, tag: Tag) { + *self = tag.into(); + } +} + impl From for Tag { fn from(input: AIFFTextChunks) -> Self { - let mut tag = Tag::new(TagType::AIFFText); + let mut tag = Self::new(TagType::AIFFText); let push_item = |field: Option, item_key: ItemKey, tag: &mut Tag| { if let Some(text) = field { diff --git a/src/iff/wav/tag/mod.rs b/src/iff/wav/tag/mod.rs index 47a5a05d..e5823047 100644 --- a/src/iff/wav/tag/mod.rs +++ b/src/iff/wav/tag/mod.rs @@ -4,7 +4,7 @@ mod write; use crate::error::{LoftyError, Result}; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{Tag, TagType}; -use crate::traits::{Accessor, TagExt}; +use crate::traits::{Accessor, SplitAndRejoinTag, TagExt}; use std::borrow::Cow; use std::fs::{File, OpenOptions}; @@ -211,9 +211,19 @@ impl TagExt for RIFFInfoList { } } +impl SplitAndRejoinTag for RIFFInfoList { + fn split_tag(&mut self) -> Tag { + std::mem::take(self).into() + } + + fn rejoin_tag(&mut self, tag: Tag) { + *self = tag.into(); + } +} + impl From for Tag { fn from(input: RIFFInfoList) -> Self { - let mut tag = Tag::new(TagType::RIFFInfo); + let mut tag = Self::new(TagType::RIFFInfo); for (k, v) in input.items { let item_key = ItemKey::from_key(TagType::RIFFInfo, &k); diff --git a/src/lib.rs b/src/lib.rs index 6581f8c2..9d087df1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -178,7 +178,7 @@ pub use crate::tag::{Tag, TagType}; pub use tag::item::{ItemKey, ItemValue, TagItem}; pub use util::text::TextEncoding; -pub use crate::traits::{Accessor, TagExt}; +pub use crate::traits::{Accessor, SplitAndRejoinTag, TagExt}; pub use picture::PictureInformation; diff --git a/src/mp4/ilst/atom.rs b/src/mp4/ilst/atom.rs index 0a1d4165..e7c97502 100644 --- a/src/mp4/ilst/atom.rs +++ b/src/mp4/ilst/atom.rs @@ -12,10 +12,10 @@ pub(super) enum AtomDataStorage { } impl AtomDataStorage { - pub(super) fn take_first(self) -> AtomData { + pub(super) fn first_mut(&mut self) -> &mut AtomData { match self { AtomDataStorage::Single(val) => val, - AtomDataStorage::Multiple(mut data) => data.swap_remove(0), + AtomDataStorage::Multiple(data) => data.first_mut().expect("not empty"), } } } diff --git a/src/mp4/ilst/mod.rs b/src/mp4/ilst/mod.rs index e2441d8c..603da303 100644 --- a/src/mp4/ilst/mod.rs +++ b/src/mp4/ilst/mod.rs @@ -7,10 +7,10 @@ pub(crate) mod write; use super::AtomIdent; use crate::error::LoftyError; use crate::mp4::ilst::atom::AtomDataStorage; -use crate::picture::{Picture, PictureType}; +use crate::picture::{Picture, PictureType, TOMBSTONE_PICTURE}; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{Tag, TagType}; -use crate::traits::{Accessor, TagExt}; +use crate::traits::{Accessor, SplitAndRejoinTag, TagExt}; use atom::{AdvisoryRating, Atom, AtomData}; use std::borrow::Cow; @@ -377,20 +377,23 @@ impl TagExt for Ilst { } } -impl From for Tag { - fn from(input: Ilst) -> Self { - let mut tag = Self::new(TagType::MP4ilst); +impl SplitAndRejoinTag for Ilst { + fn split_tag(&mut self) -> Tag { + let mut tag = Tag::new(TagType::MP4ilst); - for atom in input.atoms { + self.atoms.retain_mut(|atom| { let Atom { ident, data } = atom; - let value = match data.take_first() { - AtomData::UTF8(text) | AtomData::UTF16(text) => ItemValue::Text(text), - AtomData::Picture(pic) => { - tag.pictures.push(pic); - continue; + let value = match data.first_mut() { + AtomData::UTF8(text) | AtomData::UTF16(text) => { + ItemValue::Text(std::mem::take(text)) + }, + AtomData::Picture(picture) => { + tag.pictures + .push(std::mem::replace(picture, TOMBSTONE_PICTURE)); + return false; // Atom consumed }, AtomData::Bool(b) => { - let text = if b { "1".to_owned() } else { "0".to_owned() }; + let text = if *b { "1".to_owned() } else { "0".to_owned() }; ItemValue::Text(text) }, // We have to special case track/disc numbers since they are stored together @@ -403,6 +406,7 @@ impl From for Tag { tag.insert_text(ItemKey::TrackNumber, current.to_string()); tag.insert_text(ItemKey::TrackTotal, total.to_string()); + return false; // Atom consumed }, b"disk" => { let current = u16::from_be_bytes([data[2], data[3]]); @@ -410,14 +414,17 @@ impl From for Tag { tag.insert_text(ItemKey::DiscNumber, current.to_string()); tag.insert_text(ItemKey::DiscTotal, total.to_string()); + return false; // Atom consumed }, _ => {}, } } - continue; + return true; // Keep atom + }, + _ => { + return true; // Keep atom }, - _ => continue, }; let key = ItemKey::from_key( @@ -433,14 +440,13 @@ impl From for Tag { ); tag.items.push(TagItem::new(key, value)); - } + false // Atom consumed + }); tag } -} -impl From for Ilst { - fn from(input: Tag) -> Self { + fn rejoin_tag(&mut self, tag: Tag) { fn convert_to_uint(space: &mut Option, cont: &str) { if let Ok(num) = cont.parse::() { *space = Some(num); @@ -465,13 +471,11 @@ impl From for Ilst { } } - let mut ilst = Self::default(); - // Storage for integer pairs let mut tracks: (Option, Option) = (None, None); let mut discs: (Option, Option) = (None, None); - for item in input.items { + for item in tag.items { let key = item.item_key; if let Ok(ident) = TryInto::>::try_into(&key) { @@ -495,13 +499,13 @@ impl From for Ilst { continue; }, }; - ilst.atoms.push(Atom { + self.atoms.push(Atom { ident: ident.into_owned(), data: AtomDataStorage::Single(AtomData::Bool(data)), }) } }, - _ => ilst.atoms.push(Atom { + _ => self.atoms.push(Atom { ident: ident.into_owned(), data: AtomDataStorage::Single(AtomData::UTF8(data)), }), @@ -509,20 +513,32 @@ impl From for Ilst { } } - for mut picture in input.pictures { + for mut picture in tag.pictures { // Just for correctness, since we can't actually // assign a picture type in this format picture.pic_type = PictureType::Other; - ilst.atoms.push(Atom { + self.atoms.push(Atom { ident: AtomIdent::Fourcc([b'c', b'o', b'v', b'r']), data: AtomDataStorage::Single(AtomData::Picture(picture)), }) } - create_int_pair(&mut ilst, *b"trkn", tracks); - create_int_pair(&mut ilst, *b"disk", discs); + create_int_pair(self, *b"trkn", tracks); + create_int_pair(self, *b"disk", discs); + } +} +impl From for Tag { + fn from(mut input: Ilst) -> Self { + input.split_tag() + } +} + +impl From for Ilst { + fn from(input: Tag) -> Self { + let mut ilst = Self::default(); + ilst.rejoin_tag(input); ilst } } diff --git a/src/ogg/tag.rs b/src/ogg/tag.rs index ce138463..43be0fb1 100644 --- a/src/ogg/tag.rs +++ b/src/ogg/tag.rs @@ -7,7 +7,7 @@ use crate::picture::{Picture, PictureInformation}; use crate::probe::Probe; use crate::tag::item::{ItemKey, ItemValue, TagItem}; use crate::tag::{Tag, TagType}; -use crate::traits::{Accessor, TagExt}; +use crate::traits::{Accessor, SplitAndRejoinTag, TagExt}; use std::borrow::Cow; use std::fs::{File, OpenOptions}; @@ -328,11 +328,11 @@ impl TagExt for VorbisComments { } } -impl From for Tag { - fn from(input: VorbisComments) -> Self { +impl SplitAndRejoinTag for VorbisComments { + fn split_tag(&mut self) -> Tag { let mut tag = Tag::new(TagType::VorbisComments); - for (k, v) in input.items { + for (k, v) in std::mem::take(&mut self.items) { tag.items.push(TagItem::new( ItemKey::from_key(TagType::VorbisComments, &k), ItemValue::Text(v), @@ -347,31 +347,28 @@ impl From for Tag { { tag.items.push(TagItem::new( ItemKey::EncoderSoftware, - ItemValue::Text(input.vendor), + // Preserve the original vendor by cloning + ItemValue::Text(self.vendor.clone()), )); } - for (pic, _info) in input.pictures { + for (pic, _info) in std::mem::take(&mut self.pictures) { tag.push_picture(pic) } tag } -} - -impl From for VorbisComments { - fn from(mut input: Tag) -> Self { - let mut vorbis_comments = Self::default(); + fn rejoin_tag(&mut self, mut tag: Tag) { if let Some(TagItem { item_value: ItemValue::Text(val), .. - }) = input.take(&ItemKey::EncoderSoftware).next() + }) = tag.take(&ItemKey::EncoderSoftware).next() { - vorbis_comments.vendor = val; + self.vendor = val; } - for item in input.items { + for item in tag.items { let item_key = item.item_key; let item_value = item.item_value; @@ -386,15 +383,27 @@ impl From for VorbisComments { Some(k) => k, }; - vorbis_comments.items.push((key.to_string(), val)); + self.items.push((key.to_string(), val)); } - for picture in input.pictures { + for picture in tag.pictures { if let Ok(information) = PictureInformation::from_picture(&picture) { - vorbis_comments.pictures.push((picture, information)) + self.pictures.push((picture, information)) } } + } +} +impl From for Tag { + fn from(mut input: VorbisComments) -> Self { + input.split_tag() + } +} + +impl From for VorbisComments { + fn from(input: Tag) -> Self { + let mut vorbis_comments = Self::default(); + vorbis_comments.rejoin_tag(input); vorbis_comments } } diff --git a/src/picture.rs b/src/picture.rs index f44dc5c3..f5cc6f45 100644 --- a/src/picture.rs +++ b/src/picture.rs @@ -502,8 +502,8 @@ impl Picture { Self { pic_type, mime_type, - description: description.map(Cow::from), - data: Cow::from(data), + description: description.map(Cow::Owned), + data: Cow::Owned(data), } } @@ -890,3 +890,11 @@ impl Picture { } } } + +// A placeholder that is needed during conversions. +pub(crate) const TOMBSTONE_PICTURE: Picture = Picture { + pic_type: PictureType::Other, + mime_type: MimeType::Unknown(String::new()), + description: None, + data: Cow::Owned(Vec::new()), +}; diff --git a/src/tag/mod.rs b/src/tag/mod.rs index decb5ca4..7d52b26d 100644 --- a/src/tag/mod.rs +++ b/src/tag/mod.rs @@ -6,7 +6,7 @@ use crate::file::FileType; use crate::macros::err; use crate::picture::{Picture, PictureType}; use crate::probe::Probe; -use crate::traits::{Accessor, TagExt}; +use crate::traits::{Accessor, SplitAndRejoinTag, TagExt}; use item::{ItemKey, ItemValue, TagItem}; use std::borrow::Cow; @@ -583,6 +583,16 @@ impl TagExt for Tag { } } +impl SplitAndRejoinTag for Tag { + fn split_tag(&mut self) -> Self { + std::mem::replace(self, Self::new(self.tag_type)) + } + + fn rejoin_tag(&mut self, tag: Self) { + *self = tag; + } +} + /// The tag's format #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] diff --git a/src/traits.rs b/src/traits.rs index 608d7b17..39141683 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -233,6 +233,30 @@ pub trait TagExt: Accessor + Into + Sized { fn clear(&mut self); } +/// Split and rejoin tags. +/// +/// Useful and required for implementing read/modify/write round trips. +pub trait SplitAndRejoinTag { + /// Extract and split generic contents into a [`Tag`]. + /// + /// Leaves the remainder that cannot be represented in the + /// resulting tag in `self`. This is useful if the modified [`Tag`] + /// is rejoined later using [`SplitAndRejoinTag::rejoin_tag`]. + // NOTE: Using the "typestate pattern" (http://cliffle.com/blog/rust-typestate/) + // to represent the intermediate state turned out as less flexible + // and useful than expected. + fn split_tag(&mut self) -> Tag; + + /// Rejoin a [`Tag`]. + /// + /// Rejoin a tag that has previously been obtained with + /// [`SplitAndRejoinTag::split_tag`]. + /// + /// Restores the original representation merged with the contents of + /// `tag` for further processing, e.g. writing back into a file. + fn rejoin_tag(&mut self, tag: Tag); +} + // TODO: https://github.com/rust-lang/rust/issues/59359 pub(crate) trait SeekStreamLen: std::io::Seek { fn stream_len(&mut self) -> crate::error::Result {