diff --git a/src/commands.rs b/src/commands.rs index b37e4eef..9ed4cd39 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -70,7 +70,7 @@ pub trait Command { fn parse_response(parts: ResponseAttributes<'_>) -> ResponseParserResult<'_, Self::Response>; fn parse_raw_response(raw: &str) -> ResponseParserResult<'_, Self::Response> { - Self::parse_response(ResponseAttributes::new(raw)?) + Self::parse_response(ResponseAttributes::new(raw)) } } @@ -102,6 +102,9 @@ pub enum ResponseParserError<'a> { /// A property was found in the response that was not expected. UnexpectedProperty(&'a str), + /// A property was found multiple times in the response, but was only expected once. + DuplicateProperty(&'a str), + /// The property value is parsable, but the value is invalid or nonsensical. InvalidProperty(&'a str, &'a str), diff --git a/src/commands/audio_output_devices/outputs.rs b/src/commands/audio_output_devices/outputs.rs index efe9caf8..dd1697d5 100644 --- a/src/commands/audio_output_devices/outputs.rs +++ b/src/commands/audio_output_devices/outputs.rs @@ -46,7 +46,7 @@ impl Command for Outputs { let mut enabled: Option = None; let mut attributes: HashMap = HashMap::new(); - for (k, v) in Vec::from(parts).into_iter() { + for (k, v) in parts.into_vec()?.into_iter() { match k { "outputid" => { // Reset and store the previous output if all fields are present diff --git a/src/commands/client_to_client/channels.rs b/src/commands/client_to_client/channels.rs index 396947d3..41a6e0b0 100644 --- a/src/commands/client_to_client/channels.rs +++ b/src/commands/client_to_client/channels.rs @@ -32,7 +32,7 @@ impl Command for Channels { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut channel_names = Vec::with_capacity(parts.len()); for (key, value) in parts { debug_assert!(key == "channels"); diff --git a/src/commands/client_to_client/readmessages.rs b/src/commands/client_to_client/readmessages.rs index c2555621..c67fde5d 100644 --- a/src/commands/client_to_client/readmessages.rs +++ b/src/commands/client_to_client/readmessages.rs @@ -35,7 +35,7 @@ impl Command for ReadMessages { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; debug_assert!(parts.len() % 2 == 0); let mut messages = Vec::with_capacity(parts.len() / 2); diff --git a/src/commands/connection_settings/protocol.rs b/src/commands/connection_settings/protocol.rs index f5bd5722..9acdce5c 100644 --- a/src/commands/connection_settings/protocol.rs +++ b/src/commands/connection_settings/protocol.rs @@ -26,7 +26,7 @@ impl Command for Protocol { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts_: Vec<_> = parts.into(); + let parts_: Vec<_> = parts.into_vec()?; if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != "feature") { return Err(ResponseParserError::UnexpectedProperty(k)); } diff --git a/src/commands/connection_settings/protocol_available.rs b/src/commands/connection_settings/protocol_available.rs index 5036b7b7..c2b0416c 100644 --- a/src/commands/connection_settings/protocol_available.rs +++ b/src/commands/connection_settings/protocol_available.rs @@ -26,7 +26,7 @@ impl Command for ProtocolAvailable { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts_: Vec<_> = parts.into(); + let parts_: Vec<_> = parts.into_vec()?; if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != "feature") { return Err(ResponseParserError::UnexpectedProperty(k)); } diff --git a/src/commands/connection_settings/tag_types.rs b/src/commands/connection_settings/tag_types.rs index 5ab65894..6d52873f 100644 --- a/src/commands/connection_settings/tag_types.rs +++ b/src/commands/connection_settings/tag_types.rs @@ -25,7 +25,7 @@ impl Command for TagTypes { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut tagtypes = Vec::with_capacity(parts.len()); for (key, value) in parts.into_iter() { diff --git a/src/commands/connection_settings/tag_types_available.rs b/src/commands/connection_settings/tag_types_available.rs index d995345a..17c2b87d 100644 --- a/src/commands/connection_settings/tag_types_available.rs +++ b/src/commands/connection_settings/tag_types_available.rs @@ -26,7 +26,7 @@ impl Command for TagTypesAvailable { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut tagtypes = Vec::with_capacity(parts.len()); for (key, value) in parts.into_iter() { diff --git a/src/commands/mounts_and_neighbors/listmounts.rs b/src/commands/mounts_and_neighbors/listmounts.rs index bc5fa38c..1cb992c3 100644 --- a/src/commands/mounts_and_neighbors/listmounts.rs +++ b/src/commands/mounts_and_neighbors/listmounts.rs @@ -24,7 +24,7 @@ impl Command for ListMounts { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut result = Vec::with_capacity(parts.len()); for (key, value) in parts.into_iter() { if key != "mount" { diff --git a/src/commands/mounts_and_neighbors/listneighbors.rs b/src/commands/mounts_and_neighbors/listneighbors.rs index b6b74b4b..78c763cc 100644 --- a/src/commands/mounts_and_neighbors/listneighbors.rs +++ b/src/commands/mounts_and_neighbors/listneighbors.rs @@ -28,7 +28,7 @@ impl Command for ListNeighbors { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; debug_assert!(parts.len() % 2 == 0); let mut result = HashMap::with_capacity(parts.len() / 2); diff --git a/src/commands/music_database/albumart.rs b/src/commands/music_database/albumart.rs index 781d9a57..9c9b729c 100644 --- a/src/commands/music_database/albumart.rs +++ b/src/commands/music_database/albumart.rs @@ -53,7 +53,7 @@ impl Command for AlbumArt { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let size = get_and_parse_property!(parts, "size", Text); diff --git a/src/commands/music_database/count.rs b/src/commands/music_database/count.rs index 3cd4ea72..6cff71a5 100644 --- a/src/commands/music_database/count.rs +++ b/src/commands/music_database/count.rs @@ -65,7 +65,7 @@ impl Command for Count { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let songs = get_and_parse_property!(parts, "songs", Text); let playtime = get_and_parse_property!(parts, "playtime", Text); diff --git a/src/commands/music_database/getfingerprint.rs b/src/commands/music_database/getfingerprint.rs index 59c28551..cbd67ad4 100644 --- a/src/commands/music_database/getfingerprint.rs +++ b/src/commands/music_database/getfingerprint.rs @@ -39,7 +39,7 @@ impl Command for GetFingerprint { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let chromaprint = get_and_parse_property!(parts, "chromaprint", Text); diff --git a/src/commands/music_database/list.rs b/src/commands/music_database/list.rs index 6b133c2c..c5efd548 100644 --- a/src/commands/music_database/list.rs +++ b/src/commands/music_database/list.rs @@ -90,7 +90,7 @@ impl Command for List { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts_: Vec<_> = parts.into(); + let parts_: Vec<_> = parts.into_vec()?; debug_assert!({ let key = parts_.first().map(|(k, _)| k); parts_.iter().all(|(k, _)| k == key.unwrap()) diff --git a/src/commands/music_database/lsinfo.rs b/src/commands/music_database/lsinfo.rs index 368d60bc..e08bf5f4 100644 --- a/src/commands/music_database/lsinfo.rs +++ b/src/commands/music_database/lsinfo.rs @@ -46,7 +46,7 @@ impl Command for LsInfo { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut playlists = Vec::new(); for (key, value) in parts { diff --git a/src/commands/music_database/readcomments.rs b/src/commands/music_database/readcomments.rs index 5570f628..98c6e67c 100644 --- a/src/commands/music_database/readcomments.rs +++ b/src/commands/music_database/readcomments.rs @@ -34,7 +34,7 @@ impl Command for ReadComments { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let comments = parts .iter() diff --git a/src/commands/music_database/readpicture.rs b/src/commands/music_database/readpicture.rs index bedf5b0e..4c875974 100644 --- a/src/commands/music_database/readpicture.rs +++ b/src/commands/music_database/readpicture.rs @@ -56,7 +56,7 @@ impl Command for ReadPicture { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; if parts.is_empty() { return Ok(None); diff --git a/src/commands/music_database/rescan.rs b/src/commands/music_database/rescan.rs index fa2e95ba..c25ccdff 100644 --- a/src/commands/music_database/rescan.rs +++ b/src/commands/music_database/rescan.rs @@ -39,7 +39,7 @@ impl Command for Rescan { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let updating_db = get_and_parse_property!(parts, "updating_db", Text); diff --git a/src/commands/music_database/searchcount.rs b/src/commands/music_database/searchcount.rs index 6bf619b5..28c229fa 100644 --- a/src/commands/music_database/searchcount.rs +++ b/src/commands/music_database/searchcount.rs @@ -64,7 +64,7 @@ impl Command for SearchCount { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let songs = get_and_parse_property!(parts, "songs", Text); let playtime = get_and_parse_property!(parts, "playtime", Text); diff --git a/src/commands/music_database/update.rs b/src/commands/music_database/update.rs index fee72622..809b62f7 100644 --- a/src/commands/music_database/update.rs +++ b/src/commands/music_database/update.rs @@ -39,7 +39,7 @@ impl Command for Update { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let updating_db = get_and_parse_property!(parts, "updating_db", Text); diff --git a/src/commands/partition_commands/listpartitions.rs b/src/commands/partition_commands/listpartitions.rs index 7858d6b4..cd7d51fd 100644 --- a/src/commands/partition_commands/listpartitions.rs +++ b/src/commands/partition_commands/listpartitions.rs @@ -26,7 +26,7 @@ impl Command for ListPartitions { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut partitions = Vec::with_capacity(parts.len()); for (key, value) in parts.into_iter() { diff --git a/src/commands/playback_options/getvol.rs b/src/commands/playback_options/getvol.rs index bee608a3..deb2d5cc 100644 --- a/src/commands/playback_options/getvol.rs +++ b/src/commands/playback_options/getvol.rs @@ -26,7 +26,7 @@ impl Command for GetVol { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; assert_eq!(parts.len(), 1); let volume = get_and_parse_property!(parts, "volume", Text); Ok(volume) diff --git a/src/commands/playback_options/replay_gain_status.rs b/src/commands/playback_options/replay_gain_status.rs index 0641c355..07e64e36 100644 --- a/src/commands/playback_options/replay_gain_status.rs +++ b/src/commands/playback_options/replay_gain_status.rs @@ -33,7 +33,7 @@ impl Command for ReplayGainStatus { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let replay_gain_mode = get_property!(parts, "replay_gain_mode", Text); Ok(ReplayGainStatusResponse { diff --git a/src/commands/querying_mpd_status/stats.rs b/src/commands/querying_mpd_status/stats.rs index a3def8f7..3a1e7af8 100644 --- a/src/commands/querying_mpd_status/stats.rs +++ b/src/commands/querying_mpd_status/stats.rs @@ -41,7 +41,7 @@ impl Command for Stats { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let uptime = get_and_parse_property!(parts, "uptime", Text); let playtime = get_and_parse_property!(parts, "playtime", Text); diff --git a/src/commands/querying_mpd_status/status.rs b/src/commands/querying_mpd_status/status.rs index 1712f36a..af49aa80 100644 --- a/src/commands/querying_mpd_status/status.rs +++ b/src/commands/querying_mpd_status/status.rs @@ -66,7 +66,7 @@ pub struct StatusResponse { fn parse_status_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<&str, GenericResponseValue> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let partition = get_property!(parts, "partition", Text).to_string(); let volume = match get_property!(parts, "volume", Text) { diff --git a/src/commands/queue/add.rs b/src/commands/queue/add.rs index 53c8fa5b..a5a48f3b 100644 --- a/src/commands/queue/add.rs +++ b/src/commands/queue/add.rs @@ -15,6 +15,12 @@ pub struct AddRequest { position: Option, } +impl AddRequest { + pub fn new(uri: Uri, position: Option) -> Self { + Self { uri, position } + } +} + impl Command for Add { type Request = AddRequest; type Response = (); diff --git a/src/commands/reflection/commands.rs b/src/commands/reflection/commands.rs index 396a3b91..7bc21b8c 100644 --- a/src/commands/reflection/commands.rs +++ b/src/commands/reflection/commands.rs @@ -25,7 +25,7 @@ impl Command for Commands { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut result = Vec::with_capacity(parts.len()); for (key, value) in parts.into_iter() { if key != "command" { diff --git a/src/commands/reflection/config.rs b/src/commands/reflection/config.rs index fa3c7d0b..2f2ef992 100644 --- a/src/commands/reflection/config.rs +++ b/src/commands/reflection/config.rs @@ -34,7 +34,7 @@ impl Command for Config { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let music_directory = get_property!(parts, "music_directory", Text).to_string(); let playlist_directory = get_property!(parts, "playlist_directory", Text).to_string(); diff --git a/src/commands/reflection/decoders.rs b/src/commands/reflection/decoders.rs index 1c2f0916..f3b43393 100644 --- a/src/commands/reflection/decoders.rs +++ b/src/commands/reflection/decoders.rs @@ -36,7 +36,7 @@ impl Command for Decoders { ) -> Result> { let mut result = Vec::new(); let mut current_decoder: Option = None; - for (key, value) in Vec::from(parts).into_iter() { + for (key, value) in parts.into_vec()?.into_iter() { match key { "plugin" => { if let Some(decoder) = current_decoder.take() { diff --git a/src/commands/reflection/not_commands.rs b/src/commands/reflection/not_commands.rs index 63e28c02..3371dc85 100644 --- a/src/commands/reflection/not_commands.rs +++ b/src/commands/reflection/not_commands.rs @@ -25,7 +25,7 @@ impl Command for NotCommands { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut result = Vec::with_capacity(parts.len()); for (key, value) in parts.into_iter() { if key != "command" { diff --git a/src/commands/reflection/url_handlers.rs b/src/commands/reflection/url_handlers.rs index 9a5db55d..2bbbd245 100644 --- a/src/commands/reflection/url_handlers.rs +++ b/src/commands/reflection/url_handlers.rs @@ -25,7 +25,7 @@ impl Command for UrlHandlers { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut url_handlers = Vec::with_capacity(parts.len()); for (key, value) in parts.into_iter() { if key != "handler" { diff --git a/src/commands/stickers/sticker_find.rs b/src/commands/stickers/sticker_find.rs index d5f289c4..f828471b 100644 --- a/src/commands/stickers/sticker_find.rs +++ b/src/commands/stickers/sticker_find.rs @@ -100,7 +100,7 @@ impl Command for StickerFind { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut stickers = Vec::with_capacity(parts.len() / 2); for sticker_uri_pair in parts.chunks_exact(2) { diff --git a/src/commands/stickers/sticker_list.rs b/src/commands/stickers/sticker_list.rs index 5b3015fc..188b9f20 100644 --- a/src/commands/stickers/sticker_list.rs +++ b/src/commands/stickers/sticker_list.rs @@ -47,7 +47,7 @@ impl Command for StickerList { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; if let Some((k, _)) = parts.iter().find(|(k, _)| *k != "sticker") { return Err(ResponseParserError::UnexpectedProperty(k)); } diff --git a/src/commands/stickers/stickernames.rs b/src/commands/stickers/stickernames.rs index f97fc538..f13a7aa1 100644 --- a/src/commands/stickers/stickernames.rs +++ b/src/commands/stickers/stickernames.rs @@ -26,7 +26,7 @@ impl Command for StickerNames { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts_: Vec<_> = parts.into(); + let parts_: Vec<_> = parts.into_vec()?; if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != "name") { return Err(ResponseParserError::UnexpectedProperty(k)); } diff --git a/src/commands/stickers/stickernamestypes.rs b/src/commands/stickers/stickernamestypes.rs index 78bb5b87..9b82a738 100644 --- a/src/commands/stickers/stickernamestypes.rs +++ b/src/commands/stickers/stickernamestypes.rs @@ -31,7 +31,7 @@ impl Command for StickerNamesTypes { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut result = HashMap::new(); for name_type_pair in parts.chunks_exact(2) { diff --git a/src/commands/stickers/stickertypes.rs b/src/commands/stickers/stickertypes.rs index a3622d5c..8cf5c84e 100644 --- a/src/commands/stickers/stickertypes.rs +++ b/src/commands/stickers/stickertypes.rs @@ -26,7 +26,7 @@ impl Command for StickerTypes { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts_: Vec<_> = parts.into(); + let parts_: Vec<_> = parts.into_vec()?; if let Some((k, _)) = parts_.iter().find(|(k, _)| *k != "stickertype") { return Err(ResponseParserError::UnexpectedProperty(k)); } diff --git a/src/commands/stored_playlists/listplaylist.rs b/src/commands/stored_playlists/listplaylist.rs index 3bdf84eb..c44059b8 100644 --- a/src/commands/stored_playlists/listplaylist.rs +++ b/src/commands/stored_playlists/listplaylist.rs @@ -52,7 +52,7 @@ impl Command for ListPlaylist { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: Vec<_> = parts.into(); + let parts: Vec<_> = parts.into_vec()?; let mut files = Vec::with_capacity(parts.len()); for (key, value) in parts.into_iter() { if key != "file" { diff --git a/src/commands/stored_playlists/playlistlength.rs b/src/commands/stored_playlists/playlistlength.rs index 847f0211..8fcbe73e 100644 --- a/src/commands/stored_playlists/playlistlength.rs +++ b/src/commands/stored_playlists/playlistlength.rs @@ -40,7 +40,7 @@ impl Command for PlaylistLength { fn parse_response( parts: ResponseAttributes<'_>, ) -> Result> { - let parts: HashMap<_, _> = parts.into(); + let parts: HashMap<_, _> = parts.into_map()?; let songs = get_and_parse_property!(parts, "songs", Text); let playtime = get_and_parse_property!(parts, "playtime", Text); diff --git a/src/response_tokenizer.rs b/src/response_tokenizer.rs index c5eda438..d9c7f158 100644 --- a/src/response_tokenizer.rs +++ b/src/response_tokenizer.rs @@ -9,38 +9,56 @@ pub enum GenericResponseValue<'a> { // Many(Vec>), } -#[derive(Debug, Clone, PartialEq)] -pub struct ResponseAttributes<'a>(Vec<(&'a str, GenericResponseValue<'a>)>); +#[derive(Debug, Clone)] +pub struct ResponseAttributes<'a> { + bytestring: &'a [u8], + cursor: usize, +} impl<'a> ResponseAttributes<'a> { - pub fn new(raw: &'a str) -> Result> { - let mut parts = Vec::new(); - let mut lines = raw.lines(); - loop { - let line = lines.next().ok_or(ResponseParserError::UnexpectedEOF)?; - if line.is_empty() { - println!("Warning: empty line in response"); - continue; - } + pub fn new(raw: &'a str) -> Self { + Self::new_from_bytes(raw.as_bytes()) + } - if line == "OK" { - break; - } - - let mut keyval = line.splitn(2, ": "); - let key = keyval - .next() - .ok_or(ResponseParserError::SyntaxError(0, line))?; - - // TODO: handle binary data, also verify binarylimit - let value = keyval - .next() - .ok_or(ResponseParserError::SyntaxError(0, line))?; - - parts.push((key, GenericResponseValue::Text(value))); + pub fn new_from_bytes(bytes: &'a [u8]) -> Self { + Self { + bytestring: bytes, + cursor: 0, } + } - Ok(parts.into()) + pub fn is_empty(&self) -> bool { + self.cursor >= self.bytestring.len() || self.bytestring[self.cursor..].starts_with(b"OK") + } + + pub fn into_map( + self, + ) -> Result>, ResponseParserError<'a>> { + self.into() + } + + pub fn into_vec( + self, + ) -> Result)>, ResponseParserError<'a>> { + self.into() + } + + pub fn into_lazy_vec( + self, + ) -> Vec), ResponseParserError<'a>>> { + self.into() + } + + pub fn verify_all_keys_equal(&self, expected_key: &str) -> Result<(), ResponseParserError<'a>> { + let mut copy = self.clone(); + copy.cursor = 0; + for item in copy { + let (key, _) = item?; + if key != expected_key { + return Err(ResponseParserError::UnexpectedProperty(key)); + } + } + Ok(()) } // pub fn get<'a>(&self, key: &str) -> Option<&GenericResponseValue<'a>> { @@ -48,38 +66,138 @@ impl<'a> ResponseAttributes<'a> { // } } -impl ResponseAttributes<'_> { - pub fn is_empty(&self) -> bool { - self.0.is_empty() +impl<'a> Iterator for ResponseAttributes<'a> { + type Item = Result<(&'a str, GenericResponseValue<'a>), ResponseParserError<'a>>; + + fn next(&mut self) -> Option { + if self.cursor >= self.bytestring.len() { + return Some(Err(ResponseParserError::UnexpectedEOF)); + } + if self.bytestring[self.cursor..].starts_with(b"OK") { + return None; + } + + let remaining = &self.bytestring[self.cursor..]; + let newline_pos = remaining + .iter() + .position(|&b| b == b'\n') + .unwrap_or(remaining.len()); + let line = &remaining[..newline_pos]; + + // Skip empty lines + if line.is_empty() { + self.cursor += newline_pos + 1; + return self.next(); + } + + // NOTE: it is important that this happens before any None returns, + // so that the iterator advances despite errors. + self.cursor += newline_pos + 1; + + let mut keyval = line.splitn(2, |&b| b == b':'); + let key_bytes = keyval.next()?; + + // TODO: should this be a proper runtime error? + debug_assert!(!key_bytes.is_empty()); + debug_assert!(std::str::from_utf8(key_bytes).is_ok()); + let key = std::str::from_utf8(key_bytes).ok()?; + + // In the case of binary data, the following value will be the byte count + // in decimal, and the actual binary data will follow in the next N bytes, + // followed by a newline. + // + // We parse the number and assign the binary data to the "binary" key. + if key == "binary" { + let byte_count = match keyval.next() { + Some(count) => count.trim_ascii_start(), + None => { + // TODO: throw more specific error + return Some(Err(ResponseParserError::UnexpectedEOF)); + } + }; + let byte_count_str = match std::str::from_utf8(byte_count) { + Ok(s) => s, + Err(_) => { + // TODO: throw more specific error + return Some(Err(ResponseParserError::SyntaxError( + 0, + "Invalid byte count", + ))); + } + }; + let byte_count: usize = match byte_count_str.parse() { + Ok(n) => n, + Err(_) => { + // TODO: throw more specific error + return Some(Err(ResponseParserError::SyntaxError( + 0, + "Invalid byte count", + ))); + } + }; + + let value_start = self.cursor; + let value_end = self.cursor + byte_count; + if value_end > self.bytestring.len() { + return Some(Err(ResponseParserError::UnexpectedEOF)); + } + let value_bytes = &self.bytestring[value_start..value_end]; + + debug_assert!( + self.bytestring[value_end..] + .iter() + .next() + .is_none_or(|&b| b == b'\n') + ); + + // Skip the binary data and the following newline + self.cursor = value_end + 1; + + Some(Ok((key, GenericResponseValue::Binary(value_bytes)))) + } else { + let value_bytes = match keyval.next() { + Some(v) => v.trim_ascii_start(), + None => b"", + }; + // TODO: this should be a proper runtime error, the specification + // declares that all string values are UTF-8. + debug_assert!(std::str::from_utf8(value_bytes).is_ok()); + let value_str = std::str::from_utf8(value_bytes).ok()?; + + Some(Ok((key, GenericResponseValue::Text(value_str)))) + } } } -impl<'a> From>> for ResponseAttributes<'a> { - fn from(map: HashMap<&'a str, GenericResponseValue<'a>>) -> Self { - Self(map.into_iter().collect()) - } -} - -impl<'a> From> for HashMap<&'a str, GenericResponseValue<'a>> { +impl<'a> From> + for Result>, ResponseParserError<'a>> +{ fn from(val: ResponseAttributes<'a>) -> Self { - debug_assert!({ - let mut uniq = HashSet::new(); - val.0.iter().all(move |x| uniq.insert(*x)) - }); - - val.0.into_iter().collect() + let mut map = HashMap::new(); + for item in val { + let (k, v) = item?; + if map.contains_key(k) { + return Err(ResponseParserError::DuplicateProperty(k)); + } + map.insert(k, v); + } + Ok(map) } } -impl<'a> From> for Vec<(&'a str, GenericResponseValue<'a>)> { +impl<'a> From> + for Vec), ResponseParserError<'a>>> +{ fn from(val: ResponseAttributes<'a>) -> Self { - val.0 + val.collect() } } -impl<'a> From)>> for ResponseAttributes<'a> { - fn from(val: Vec<(&'a str, GenericResponseValue<'a>)>) -> Self { - Self(val) +impl<'a> From> + for Result)>, ResponseParserError<'a>> +{ + fn from(val: ResponseAttributes<'a>) -> Self { + val.collect() } } @@ -205,11 +323,12 @@ macro_rules! get_next_optional_property { macro_rules! get_next_property { ($parts:ident, $variant:ident) => { match $parts.next() { - Some((name, value)) => ( + Some(Ok((name, value))) => ( name, crate::response_tokenizer::_expect_property_type!({ Some(value) }, name, $variant) .unwrap(), ), + Some(Err(e)) => return Err(e), None => return Err(crate::commands::ResponseParserError::UnexpectedEOF), } }; @@ -239,7 +358,7 @@ macro_rules! get_next_and_parse_optional_property { macro_rules! get_next_and_parse_property { ($parts:ident, $variant:ident) => { match $parts.next() { - Some((name, value)) => { + Some(Ok((name, value))) => { let prop = crate::response_tokenizer::_expect_property_type!( { Some(value) }, name, @@ -251,12 +370,13 @@ macro_rules! get_next_and_parse_property { crate::response_tokenizer::_unwrap_optional_property_type!(name, prop), ) } + Some(Err(e)) => return Err(e), None => return Err(crate::commands::ResponseParserError::UnexpectedEOF), } }; } -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; pub(crate) use _expect_property_type; pub(crate) use _parse_optional_property_type; @@ -276,36 +396,165 @@ use crate::commands::ResponseParserError; #[cfg(test)] mod tests { + use indoc::indoc; + use super::*; #[test] #[cfg(debug_assertions)] - fn test_valid_hashmap_uniqueness_assert() { - let valid_maplike_attrs: ResponseAttributes = vec![ - ("a", GenericResponseValue::Text("1")), - ("A", GenericResponseValue::Text("2")), - ("A ", GenericResponseValue::Text("3")), - ("b", GenericResponseValue::Text("4")), - ] - .into(); - - let map: HashMap<_, _> = valid_maplike_attrs.into(); + fn test_valid_hashmap_uniqueness_assert() -> Result<(), ResponseParserError<'static>> { + let raw_response = indoc! { + "a: 1 + A: 2 + A : 3 + b: 4 + OK" + }; + let attrs = ResponseAttributes::new(raw_response); + let map: HashMap<_, _> = attrs.into_map()?; assert_eq!(map.len(), 4); + Ok(()) } #[test] #[cfg(debug_assertions)] #[should_panic] fn test_invalid_hashmap_uniqueness_assert() { - let invalid_maplike_attrs: ResponseAttributes = vec![ - ("a", GenericResponseValue::Text("1")), - ("b", GenericResponseValue::Text("2")), - ("c", GenericResponseValue::Text("3")), - ("a", GenericResponseValue::Text("4")), - ] - .into(); + let raw_response = indoc! { + "a: 1 + b: 2 + c: 3 + a: 4 + OK" + }; + ResponseAttributes::new(raw_response).into_map().unwrap(); + } - let map: HashMap<_, _> = invalid_maplike_attrs.into(); - assert_eq!(map.len(), 4); + #[test] + fn test_response_attributes_single_attribute() -> Result<(), ResponseParserError<'static>> { + let raw_response = indoc! { + "name: Sticker1 + OK" + }; + let attrs = ResponseAttributes::new(raw_response); + let map: HashMap<_, _> = attrs.into_map()?; + assert_eq!(map.len(), 1); + assert_eq!( + map.get("name"), + Some(&GenericResponseValue::Text("Sticker1")) + ); + Ok(()) + } + + #[test] + fn test_response_attributes_multiple_attributes() -> Result<(), ResponseParserError<'static>> { + let raw_response = indoc! { + "name: Sticker1 + type: emoji + size: 128 + OK" + }; + let attrs = ResponseAttributes::new(raw_response); + let map: HashMap<_, _> = attrs.into_map()?; + assert_eq!(map.len(), 3); + assert_eq!( + map.get("name"), + Some(&GenericResponseValue::Text("Sticker1")) + ); + assert_eq!(map.get("type"), Some(&GenericResponseValue::Text("emoji"))); + assert_eq!(map.get("size"), Some(&GenericResponseValue::Text("128"))); + Ok(()) + } + + #[test] + fn test_response_attributes_empty_response() -> Result<(), ResponseParserError<'static>> { + let raw_response = indoc! { + "OK" + }; + let attrs = ResponseAttributes::new(raw_response); + let map: HashMap<_, _> = attrs.into_map()?; + assert_eq!(map.len(), 0); + Ok(()) + } + + #[test] + fn test_response_attributes_unexpected_eof() -> Result<(), ResponseParserError<'static>> { + let raw_response = indoc! { + "name: Sticker1 + type: emoji" + }; + let attrs = ResponseAttributes::new(raw_response); + let vec: Result, ResponseParserError> = attrs.into_vec(); + assert!(matches!(vec, Err(ResponseParserError::UnexpectedEOF))); + Ok(()) + } + + #[test] + fn test_response_attributes_repeated_attribute() -> Result<(), ResponseParserError<'static>> { + let raw_response = indoc! { + "name: Sticker1 + name: Sticker2 + OK" + }; + let attrs = ResponseAttributes::new(raw_response); + let vec = attrs.into_vec()?; + assert_eq!(vec.len(), 2); + assert_eq!(vec[0], ("name", GenericResponseValue::Text("Sticker1"))); + assert_eq!(vec[1], ("name", GenericResponseValue::Text("Sticker2"))); + Ok(()) + } + + #[test] + fn test_response_attributes_empty_line() -> Result<(), ResponseParserError<'static>> { + let raw_response = indoc! { + "name: Sticker1 + + type: emoji + OK" + }; + let attrs = ResponseAttributes::new(raw_response); + let map: HashMap<_, _> = attrs.into_map()?; + assert_eq!(map.len(), 2); + assert_eq!( + map.get("name"), + Some(&GenericResponseValue::Text("Sticker1")) + ); + assert_eq!(map.get("type"), Some(&GenericResponseValue::Text("emoji"))); + Ok(()) + } + + #[test] + fn test_response_attributes_no_value() -> Result<(), ResponseParserError<'static>> { + let raw_response = indoc! { + "name: + type: emoji + OK" + }; + let attrs = ResponseAttributes::new(raw_response); + let map: HashMap<_, _> = attrs.into_map()?; + assert_eq!(map.len(), 2); + assert_eq!(map.get("name"), Some(&GenericResponseValue::Text(""))); + assert_eq!(map.get("type"), Some(&GenericResponseValue::Text("emoji"))); + Ok(()) + } + + #[test] + fn test_response_attributes_binary_data() -> Result<(), ResponseParserError<'static>> { + let bytestring: &[u8] = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09"; + let raw_response = { + let mut response = format!("binary: {}\n", bytestring.len()).into_bytes(); + response.extend_from_slice(bytestring); + response.extend_from_slice(b"\nOK"); + response + }; + + let attrs = ResponseAttributes::new_from_bytes(&raw_response); + let map: HashMap<_, _> = attrs.into_map().unwrap(); + assert_eq!(map.len(), 1); + assert_eq!( + map.get("binary"), + Some(&GenericResponseValue::Binary(bytestring)) + ); + Ok(()) } }