diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 479c3e3..252fdcb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -163,36 +163,36 @@ jobs: uses: taiki-e/install-action@cargo-machete - name: Check For Unused Dependencies run: cargo machete - #semver-compliance: - # runs-on: ubuntu-latest - # needs: [clippy, no-unused-dependencies] - # steps: - # - name: Git checkout - # uses: actions/checkout@v3 - # - name: Cache cargo home - # uses: actions/cache@v3 - # env: - # cache-name: cache-cargo-home - # with: - # path: | - # ~/.cargo/bin - # ~/.cargo/registry/index - # ~/.cargo/registry/cache - # ~/.cargo/git/db - # key: ${{ runner.os }}-x86_64-unknown-linux-gnu-build-${{ env.cache-name }}-${{ hashFiles('Cargo.lock') }} - # restore-keys: | - # ${{ runner.os }}-x86_64-unknown-linux-gnu-build-${{ env.cache-name }}- - # - name: Install Rust - # uses: dtolnay/rust-toolchain@master - # with: - # toolchain: stable - # - name: Install Semver Checks - # # no default features so that it uses native Rust TLS instead of trying to link with system TLS. - # uses: taiki-e/install-action@main - # with: - # tool: cargo-semver-checks - # - name: Check Semver Compliance - # run: cargo semver-checks check-release + semver-compliance: + runs-on: ubuntu-latest + needs: [clippy, no-unused-dependencies] + steps: + - name: Git checkout + uses: actions/checkout@v3 + - name: Cache cargo home + uses: actions/cache@v3 + env: + cache-name: cache-cargo-home + with: + path: | + ~/.cargo/bin + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + key: ${{ runner.os }}-x86_64-unknown-linux-gnu-build-${{ env.cache-name }}-${{ hashFiles('Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-x86_64-unknown-linux-gnu-build-${{ env.cache-name }}- + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + - name: Install Semver Checks + # no default features so that it uses native Rust TLS instead of trying to link with system TLS. + uses: taiki-e/install-action@main + with: + tool: cargo-semver-checks + - name: Check Semver Compliance + run: cargo semver-checks check-release msrv-compliance: runs-on: ubuntu-latest needs: [clippy, no-unused-dependencies, find-msrv] diff --git a/Cargo.toml b/Cargo.toml index 86d8d0d..23eea16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "spiel" -version = "0.1.0" +version = "0.2.0" edition = "2021" +authors = [ + "Tait Hoyem ", +] license = "MIT OR Apache-2.0" description = "A pure-Rust Spiel format parser, client, and proxy implementation." repository = "https://github.com/TTWNO/spiel" @@ -19,6 +22,7 @@ std = ["alloc"] alloc = ["serde?/alloc", "dep:bytes"] poll = [] serde = ["serde/derive", "bytes?/serde", "enumflags2?/serde"] +proptests = ["reader", "client"] [dependencies] bytes = { version = "1.9.0", default-features = false, optional = true } @@ -68,4 +72,4 @@ required-features = ["client"] [[example]] name = "test_provider" path = "./examples/test_provider.rs" -required-features = ["client"] +required-features = ["client", "reader"] diff --git a/README.md b/README.md index b350be8..21cf855 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Note that features with an unmarked checkbox are not yet implemented. - [X] `alloc`: pulls in the [`bytes`](https://crates.io/crates/bytes), if `serde` is enabled. It exposes new types like [`crate::MessageOwned`] and [`crate::EventOwned`], which are owned versions of [`crate::Message`] and [`crate::Event`]. - [X] `poll`: add wrapper functions that return `Poll::Pending` when there is not enough data in the buffer. This is not for general use, but rather only if you are creating an async integration. - [X] `serde`: activate [`serde::Serialize`] and [`serde::Deserialize`] on all types. -- [ ] `provider`: activates [`std`] and pulls in the [`zbus`](https://crates.io/crates/zbus) crate. This will provide the `SpeechProvider` struct, which can be used to provide speech over the Spiel protocol via `DBus`. +- [X] `provider`: activates [`std`] and pulls in the [`zbus`](https://crates.io/crates/zbus) crate. This will provide the `SpeechProvider` struct, which can be used to provide speech over the Spiel protocol via `DBus`. ## MSRV diff --git a/examples/provider.rs b/examples/provider.rs index d860dff..5262bd9 100644 --- a/examples/provider.rs +++ b/examples/provider.rs @@ -1,10 +1,6 @@ -use std::{ - io::{PipeWriter, Write}, - os::fd::OwnedFd, - time::Duration, -}; +use std::{io::PipeWriter, os::fd::OwnedFd, time::Duration}; -use spiel::{write_message, Event, EventType, Message, Voice, VoiceFeatureSet}; +use spiel::{Event, EventType, Message, Voice, VoiceFeatureSet, Writer}; use tokio::time::sleep; use zbus::{connection::Builder, fdo::Error, interface, zvariant::Fd}; @@ -39,31 +35,20 @@ impl MySpeechProvider { // etc. // // We are just gonna write a simple `Message::Event` - let header = Message::Version("0.01"); - let msg = Message::Event(Event { + let msgs = [Message::Event(Event { typ: EventType::Word, start: 69, end: 420, name: Some("Hello :)"), - }); + })]; // buffer has fixed size in this case - let mut buffer: [u8; 1024] = [0; 1024]; let writer: OwnedFd = pipe_fd .try_into() .map_err(|_| Error::IOError("Cannot open file descriptor".to_string())) .expect("Unable to open file descriptor!"); - let mut file = PipeWriter::from(writer); - // TODO: implement a more convenient way to not have to store a buffer, etc. - let offset = - write_message(&header, &mut buffer).expect("Unable to write to buffer!"); - let bytes_written_buf = write_message(&msg, &mut buffer[offset..]) - .expect("Unable to write to buffer!"); - let bytes_written_fd = file - .write(&buffer[..bytes_written_buf + offset]) - .map_err(|_| Error::IOError("Cannot write to file descriptor".to_string())) - .expect("Unable to write to file descriptor!"); - println!("Wrote {bytes_written_fd} bytes to Fd"); - assert_eq!(bytes_written_buf + offset, bytes_written_fd); + let file = PipeWriter::from(writer); + let mut writer = Writer::new(file); + writer.write_messages(&msgs).unwrap(); } } diff --git a/examples/test_provider.rs b/examples/test_provider.rs index f7e004b..ac8a21e 100644 --- a/examples/test_provider.rs +++ b/examples/test_provider.rs @@ -2,14 +2,14 @@ //! on DBus. //! And that methods can be sent and dealt with appropriately. -use std::{error::Error, io, io::Read, os::fd::OwnedFd}; +use std::{error::Error, io, os::fd::OwnedFd}; -use spiel::{read_message, Client, Event, EventType, Message}; +use spiel::{Client, Event, EventType, Message, Reader}; #[tokio::main] async fn main() -> Result<(), Box> { let client = Client::new().await?; - let (mut reader, writer_pipe) = io::pipe()?; + let (reader, writer_pipe) = io::pipe()?; let writer = OwnedFd::from(writer_pipe); let providers = client.list_providers().await?; let mut found = false; @@ -18,7 +18,6 @@ async fn main() -> Result<(), Box> { continue; } found = true; - print!("TRY SEND..."); provider.synthesize( writer.into(), // pipe writer "my-voice", // voice ID @@ -29,22 +28,20 @@ async fn main() -> Result<(), Box> { "en-NZ", // English, New Zealand ) .await?; - println!("SENT!"); - let mut buf = Vec::new(); - let bytes_read = reader.read_to_end(&mut buf)?; - println!("BYTES READ: {bytes_read}"); - let (bytes_read2, header) = read_message(&buf[..], false)?; - let (bytes_read3, msg) = read_message(&buf[bytes_read2..], true)?; - assert_eq!(bytes_read, bytes_read2 + bytes_read3); - assert_eq!(header, Message::Version("0.01")); + let mut reader = + Reader::from_source(reader).expect("Unable to create reader from pipe!"); + let header = reader.try_read().unwrap(); + assert_eq!(header, Message::Version("0.01").into_owned()); + let event = reader.try_read().unwrap(); assert_eq!( - msg, + event, Message::Event(Event { typ: EventType::Word, start: 69, end: 420, name: Some("Hello :)"), }) + .into_owned() ); break; } diff --git a/proptest-regressions/proptests.txt b/proptest-regressions/proptests.txt new file mode 100644 index 0000000..266c54c --- /dev/null +++ b/proptest-regressions/proptests.txt @@ -0,0 +1,9 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 8e1f55c50c10fb9f07c393086be490054d60e668b0c2585b8df607d610b52786 # shrinks to msg = Audio([]) +cc 5d163de2a6118738e40571964fcfdba119d89c348e62dbea20ea7ca35d600082 # shrinks to msg = Event(Event { typ: Word, start: 0, end: 0, name: None }) +cc e0f73a0ff9d3f55330c7b49c5dd7d9ce99795ce3583d4254d96886c489da03ca # shrinks to msg = Event(Event { typ: Word, start: 0, end: 0, name: Some("") }) diff --git a/src/lib.rs b/src/lib.rs index 568654b..27a6075 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,11 +17,9 @@ compile_error!("You need at least 32-bit pointers to use this crate."); mod protocol; #[cfg(feature = "poll")] pub use protocol::poll_read_message; -#[cfg(feature = "reader")] -pub use protocol::Reader; pub use protocol::{ - read_message, read_message_type, write_message, ChunkType, Event, EventType, Message, - MessageType, + read_message, read_message_type, write_message, ChunkType, Error, Event, EventType, + Message, MessageType, }; #[cfg(feature = "alloc")] pub use protocol::{EventOwned, MessageOwned}; @@ -33,3 +31,16 @@ extern crate alloc; pub mod client; #[cfg(feature = "client")] pub use client::{Client, Voice, VoiceFeatureSet}; + +#[cfg(feature = "reader")] +pub mod reader; +#[cfg(feature = "reader")] +pub use reader::Reader; + +#[cfg(all(test, feature = "proptests"))] +pub mod proptests; + +#[cfg(feature = "std")] +pub mod writer; +#[cfg(feature = "std")] +pub use writer::Writer; diff --git a/src/proptests.rs b/src/proptests.rs new file mode 100644 index 0000000..83519d9 --- /dev/null +++ b/src/proptests.rs @@ -0,0 +1,192 @@ +use proptest::prelude::*; + +use crate::{protocol::*, Reader, Writer}; + +// Strategy for EventType +impl Arbitrary for EventType { + type Parameters = (); + type Strategy = proptest::sample::Select; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + proptest::sample::select(vec![ + EventType::Word, + EventType::Sentence, + EventType::Range, + EventType::Mark, + ]) + } +} + +// Strategy for ChunkType +impl Arbitrary for ChunkType { + type Parameters = (); + type Strategy = proptest::sample::Select; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + proptest::sample::select(vec![ChunkType::Audio, ChunkType::Event]) + } +} + +// Strategy for Event<'a> +impl Arbitrary for Event<'static> { + type Parameters = (); + type Strategy = BoxedStrategy>; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::(), + any::(), + any::(), + any::>().prop_map(|chrs| { + if chrs.is_empty() { + None + } else { + Some(String::from_iter(&chrs[..])) + } + }), + ) + .prop_map(|(typ, start, end, name)| Event { + typ, + start, + end, + name: if let Some(n) = name { + Some(Box::leak(n.into_boxed_str())) + } else { + None + }, + }) + .boxed() + } +} + +// Strategy for EventOwned +#[cfg(feature = "alloc")] +impl Arbitrary for EventOwned { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + ( + any::(), + any::(), + any::(), + any::>().prop_map(|chrs| { + if chrs.is_empty() { + None + } else { + Some(String::from_iter(&chrs[..])) + } + }), + ) + .prop_map(|(typ, start, end, name)| EventOwned { typ, start, end, name }) + .boxed() + } +} + +// Strategy for Message<'a> +impl Arbitrary for Message<'static> { + type Parameters = (); + type Strategy = BoxedStrategy>; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + // Audio + proptest::collection::vec(any::(), 0..32) + .prop_map(|v| Message::Audio(Box::leak(v.into_boxed_slice()))), + // Event + any::>().prop_map(Message::Event), + ] + .boxed() + } +} + +// Strategy for MessageOwned +#[cfg(feature = "alloc")] +impl Arbitrary for MessageOwned { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + ".{4}".prop_map(MessageOwned::Version), + proptest::collection::vec(any::(), 0..32) + .prop_map(|v| MessageOwned::Audio(bytes::Bytes::from(v))), + any::().prop_map(MessageOwned::Event), + ] + .boxed() + } +} + +// Strategy for MessageType +impl Arbitrary for MessageType { + type Parameters = (); + type Strategy = BoxedStrategy; + + fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy { + prop_oneof![ + // Version + proptest::collection::vec(any::(), 4).prop_map(|v| { + let mut arr = [0; 4]; + for (i, c) in v.into_iter().enumerate().take(4) { + arr[i] = c; + } + MessageType::Version { version: arr } + }), + // Audio + (any::(), any::()).prop_map( + |(samples_offset, samples_len)| MessageType::Audio { + samples_offset, + samples_len, + } + ), + // Event + ( + any::(), + any::(), + any::(), + any::(), + any::(), + ) + .prop_map(|(name_offset, typ, start, end, name_len)| { + MessageType::Event { + name_offset, + typ, + start, + end, + name_len, + } + }), + ] + .boxed() + } +} + +proptest::proptest! { + #[test] + fn message_roundtrip( + msg in any::(), + ) { + let mut writer = Writer::new(Vec::new()); + writer.write_message(&msg)?; + let mut reader = Reader::from(writer.inner); + let header = reader.try_read()?; + assert_eq!(header, Message::Version("0.01").into_owned()); + let decoded = reader.try_read()?; + assert_eq!(msg.into_owned(), decoded); + } +} + +// TODO: an additional proptests for testing round-trips +#[cfg(feature = "alloc")] +proptest::proptest! { + #[test] + fn message_owned_roundtrip(msg in any::()) { + let mut writer = Writer::new(Vec::new()); + writer.write_messages(&vec![msg.clone()][..]).expect("Unable to write message"); + let _ = writer.flush(); + let mut reader = Reader::from(writer.inner); + let _version = reader.try_read()?; + let decoded = reader.try_read()?; + assert_eq!(msg.into_owned(), decoded); + } +} diff --git a/src/protocol.rs b/src/protocol.rs index ea113a3..3a226c8 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -6,6 +6,44 @@ use alloc::{string::String, string::ToString}; use core::task::Poll; use core::{fmt, str::Utf8Error}; +impl Message<'_> { + /// Serializes the message into a Vec in the same binary format as the reader expects. + #[cfg(feature = "alloc")] + #[allow(clippy::cast_possible_truncation)] + #[must_use] + pub fn to_bytes(&self) -> alloc::vec::Vec { + match self { + Message::Version(version) => { + let mut buf = alloc::vec::Vec::with_capacity(4); + buf.extend_from_slice(version.as_bytes()); + buf + } + Message::Audio(samples) => { + let mut buf = alloc::vec::Vec::with_capacity(1 + 4 + samples.len()); + buf.push(1); // ChunkType::Audio + let len = samples.len() as u32; + buf.extend_from_slice(&len.to_ne_bytes()); + buf.extend_from_slice(samples); + buf + } + Message::Event(ev) => { + let name_bytes = ev.name.map_or(&[][..], |n| n.as_bytes()); + let name_len = name_bytes.len() as u32; + let mut buf = alloc::vec::Vec::with_capacity( + 1 + 1 + 4 + 4 + 4 + name_bytes.len(), + ); + buf.push(2); // ChunkType::Event + buf.push(ev.typ.to_ne_bytes()[0]); + buf.extend_from_slice(&ev.start.to_ne_bytes()); + buf.extend_from_slice(&ev.end.to_ne_bytes()); + buf.extend_from_slice(&name_len.to_ne_bytes()); + buf.extend_from_slice(name_bytes); + buf + } + } + } +} + #[derive(Debug, Eq, PartialEq)] pub enum Error { /// Reader does not have enough bytes to complete its read. @@ -56,8 +94,6 @@ impl core::error::Error for Error {} #[cfg(feature = "alloc")] use bytes::Bytes; -#[cfg(feature = "reader")] -use bytes::BytesMut; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -81,7 +117,8 @@ pub struct Event<'a> { pub name: Option<&'a str>, } -#[derive(Debug)] +#[repr(u8)] +#[derive(Debug, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ChunkType { Event, @@ -120,7 +157,7 @@ fn read_version_type(buf: &[u8]) -> Result<(usize, MessageType), Error> { return Err(Error::NotEnoughBytes(4 - buf.len())); } let buf_4: &[u8; 4] = &buf[..4].try_into().expect("Exactly 4 bytes"); - Ok((4, MessageType::Version { version: buf_4.map(char::from) })) + Ok((4, MessageType::Version { version: *buf_4 })) } /// [`read_message`] takes a buffer and triees to read a [`Message`] from it. @@ -210,7 +247,7 @@ pub fn read_message_type( ChunkType::Audio => { let (cs_size, chunk_size) = read_u32(&buf[1..])?; let msg_b = MessageType::Audio { - samples_offset: ct_offset + cs_size, + samples_offset: ct_offset + cs_size + 1, samples_len: chunk_size as usize, }; Ok((cs_size + chunk_size as usize, msg_b)) @@ -329,165 +366,6 @@ fn read_message_event(buf: &[u8]) -> Result<(usize, Message<'_>), Error> { )) } -#[cfg(feature = "reader")] -#[derive(Default)] -#[cfg_attr(all(feature = "serde", feature = "alloc"), derive(Serialize, Deserialize))] -pub struct Reader { - header_done: bool, - buffer: BytesMut, -} - -#[cfg(feature = "reader")] -impl Reader { - pub fn push(&mut self, other: &[u8]) { - self.buffer.extend_from_slice(other); - } - /// Attempt to read from the reader's internal buffer. - /// We further translate the data from [`MessageType`] into an owned [`Message`] for use. - /// - /// # Errors - /// - /// See [`read_message_type`] for failure cases. - pub fn try_read(&mut self) -> Result { - let mut data = self.buffer.split().freeze(); - let (new_buf, message_type) = read_message_type(&data, self.header_done) - .map(|(offset, mt)| (BytesMut::from(&data[offset..]), mt))?; - - let msg = match message_type { - MessageType::Version { version } => { - self.header_done = true; - MessageOwned::Version(version.into_iter().collect()) - } - MessageType::Audio { samples_offset, samples_len } => MessageOwned::Audio( - data.split_off(samples_offset - 1).split_to(samples_len), - ), - MessageType::Event { typ, start, end, name_offset, name_len } => { - MessageOwned::Event(EventOwned { - typ, - start, - end, - name: if name_len == 0 { - None - } else { - // TODO: try to remove this clone! - Some(data - .split_off(name_offset - 1) - .split_to(name_len) - .into_iter() - .map(char::from) - .collect::()) - }, - }) - } - }; - - self.buffer = new_buf; - Ok(msg) - } -} - -#[cfg(feature = "reader")] -#[test] -fn test_wave_reader() { - use alloc::string::ToString; - - use assert_matches::assert_matches; - let mut reader = Reader::default(); - let data: &[u8] = include_bytes!("../test.wav"); - reader.push(data); - assert_eq!(reader.try_read(), Ok(MessageOwned::Version("0.01".to_string()))); - assert_eq!( - reader.try_read(), - Ok(MessageOwned::Event(EventOwned { - typ: EventType::Sentence, - start: 0, - end: 0, - name: None - })) - ); - assert_eq!( - reader.try_read(), - Ok(MessageOwned::Event(EventOwned { - typ: EventType::Word, - start: 0, - end: 4, - name: None - })) - ); - for i in 0..4 { - assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_)), "{i}"); - } - let word_is = MessageOwned::Event(EventOwned { - typ: EventType::Word, - start: 5, - end: 7, - name: None, - }); - let word_a = MessageOwned::Event(EventOwned { - typ: EventType::Word, - start: 8, - end: 9, - name: None, - }); - let word_test = MessageOwned::Event(EventOwned { - typ: EventType::Word, - start: 10, - end: 14, - name: None, - }); - let word_using = MessageOwned::Event(EventOwned { - typ: EventType::Word, - start: 15, - end: 20, - name: None, - }); - let word_spiel = MessageOwned::Event(EventOwned { - typ: EventType::Word, - start: 21, - end: 26, - name: None, - }); - let word_whaha = MessageOwned::Event(EventOwned { - typ: EventType::Word, - start: 28, - end: 35, - name: None, - }); - assert_eq!(reader.try_read(), Ok(word_is)); - for _ in 0..3 { - assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); - } - assert_eq!(reader.try_read(), Ok(word_a)); - assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); - assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); - assert_eq!(reader.try_read(), Ok(word_test)); - for _ in 0..6 { - assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); - } - assert_eq!(reader.try_read(), Ok(word_using)); - for _ in 0..6 { - assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); - } - assert_eq!(reader.try_read(), Ok(word_spiel)); - for _ in 0..14 { - assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); - } - assert_eq!( - reader.try_read(), - Ok(MessageOwned::Event(EventOwned { - typ: EventType::Sentence, - start: 28, - end: 28, - name: None - })) - ); - assert_eq!(reader.try_read(), Ok(word_whaha)); - for _ in 0..10 { - assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); - } - assert_eq!(&reader.buffer.freeze().slice(..)[..], &[]); -} - #[test] fn test_read_write_version() { let mt = Message::Version("wowz"); @@ -598,7 +476,7 @@ pub fn write_message(mt: &Message, buf: &mut [u8]) -> Result { /// Mostly, you should use [`Message`] instead. pub enum MessageType { Version { - version: [char; 4], + version: [u8; 4], }, /// With this variant, you should then be able to: Audio { diff --git a/src/reader.rs b/src/reader.rs new file mode 100644 index 0000000..cb6e8d0 --- /dev/null +++ b/src/reader.rs @@ -0,0 +1,209 @@ +use alloc::{string::ToString, vec::Vec}; +#[cfg(feature = "std")] +use std::io; + +use bytes::BytesMut; + +use crate::{read_message_type, Error, EventOwned, MessageOwned, MessageType}; + +#[derive(Default)] +pub struct Reader { + header_done: bool, + buffer: BytesMut, +} + +#[cfg(feature = "std")] +impl Reader { + /// Uses a generic type that implements [`io::Read`] in order to construct the reader. + /// This will call [`io::Read::read_to_end`] and will block the thread until complete. + /// + /// # Errors + /// + /// See [`io::Error`]. + pub fn from_source(mut reader: T) -> Result + where + T: io::Read, + { + let mut buffer_vec = Vec::new(); + reader.read_to_end(&mut buffer_vec)?; + let mut buffer = BytesMut::new(); + buffer.extend_from_slice(&buffer_vec); + Ok(Reader { header_done: false, buffer }) + } +} + +impl From> for Reader { + fn from(buf: Vec) -> Self { + Reader { header_done: false, buffer: BytesMut::from(&buf[..]) } + } +} + +impl Reader { + #[must_use] + pub fn new() -> Reader { + Reader { header_done: false, buffer: BytesMut::new() } + } + pub fn push(&mut self, other: &[u8]) { + self.buffer.extend_from_slice(other); + } + /// Attempt to read from the reader's internal buffer. + /// We further translate the data from [`MessageType`] into an owned [`Message`] for use. + /// + /// # Errors + /// + /// See [`read_message_type`] for failure cases. + pub fn try_read(&mut self) -> Result { + let mut data = self.buffer.split().freeze(); + let (new_buf, message_type) = read_message_type(&data, self.header_done) + .map(|(offset, mt)| (BytesMut::from(&data[offset..]), mt))?; + + let msg = match message_type { + MessageType::Version { version } => { + self.header_done = true; + MessageOwned::Version( + str::from_utf8(&version[..]) + .map_err(Error::Utf8)? + .to_string(), + ) + } + MessageType::Audio { samples_offset, samples_len } => MessageOwned::Audio( + data.split_off(samples_offset - 1).split_to(samples_len), + ), + MessageType::Event { typ, start, end, name_offset, name_len } => { + MessageOwned::Event(EventOwned { + typ, + start, + end, + name: if name_len == 0 { + None + } else { + let bytes = data + .split_off(name_offset - 1) + .split_to(name_len); + // TODO: try to remove this clone! + let s = str::from_utf8(&bytes[..]) + .map_err(Error::Utf8)? + .to_string(); + Some(s) + }, + }) + } + }; + + self.buffer = new_buf; + Ok(msg) + } +} + +#[cfg(feature = "std")] +#[test] +fn test_std_reader() { + let data: &[u8] = include_bytes!("../test.wav"); + let mut reader = Reader::new(); + reader.push(data); + let std_reader = Reader::from_source(data).expect("Able to make buffer from test.wav"); + assert_eq!(std_reader.buffer, reader.buffer); +} + +#[test] +fn test_wave_reader() { + use alloc::string::ToString; + + use assert_matches::assert_matches; + + use crate::EventType; + let data: &[u8] = include_bytes!("../test.wav"); + let mut reader = Reader::new(); + reader.push(data); + assert_eq!(reader.try_read(), Ok(MessageOwned::Version("0.01".to_string()))); + assert_eq!( + reader.try_read(), + Ok(MessageOwned::Event(EventOwned { + typ: EventType::Sentence, + start: 0, + end: 0, + name: None + })) + ); + assert_eq!( + reader.try_read(), + Ok(MessageOwned::Event(EventOwned { + typ: EventType::Word, + start: 0, + end: 4, + name: None + })) + ); + for i in 0..4 { + assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_)), "{i}"); + } + let word_is = MessageOwned::Event(EventOwned { + typ: EventType::Word, + start: 5, + end: 7, + name: None, + }); + let word_a = MessageOwned::Event(EventOwned { + typ: EventType::Word, + start: 8, + end: 9, + name: None, + }); + let word_test = MessageOwned::Event(EventOwned { + typ: EventType::Word, + start: 10, + end: 14, + name: None, + }); + let word_using = MessageOwned::Event(EventOwned { + typ: EventType::Word, + start: 15, + end: 20, + name: None, + }); + let word_spiel = MessageOwned::Event(EventOwned { + typ: EventType::Word, + start: 21, + end: 26, + name: None, + }); + let word_whaha = MessageOwned::Event(EventOwned { + typ: EventType::Word, + start: 28, + end: 35, + name: None, + }); + assert_eq!(reader.try_read(), Ok(word_is)); + for _ in 0..3 { + assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); + } + assert_eq!(reader.try_read(), Ok(word_a)); + assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); + assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); + assert_eq!(reader.try_read(), Ok(word_test)); + for _ in 0..6 { + assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); + } + assert_eq!(reader.try_read(), Ok(word_using)); + for _ in 0..6 { + assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); + } + assert_eq!(reader.try_read(), Ok(word_spiel)); + for _ in 0..14 { + assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); + } + assert_eq!( + reader.try_read(), + Ok(MessageOwned::Event(EventOwned { + typ: EventType::Sentence, + start: 28, + end: 28, + name: None + })) + ); + assert_eq!(reader.try_read(), Ok(word_whaha)); + for _ in 0..10 { + assert_matches!(reader.try_read(), Ok(MessageOwned::Audio(_))); + } + assert_eq!(&reader.buffer.freeze().slice(..)[..], &[]); +} diff --git a/src/writer.rs b/src/writer.rs new file mode 100644 index 0000000..5ffe5e8 --- /dev/null +++ b/src/writer.rs @@ -0,0 +1,53 @@ +use std::io::{self, Write}; + +use crate::protocol::Message; + +pub struct Writer { + pub(crate) inner: W, + header_done: bool, + version: String, +} + +impl Writer { + pub fn new(inner: W) -> Self { + Writer { inner, version: "0.01".to_string(), header_done: false } + } + + /// Write a single message into the buffer. + /// + /// # Errors + /// + /// See [`io::Error`]. + pub fn write_message(&mut self, message: &Message) -> Result<(), io::Error> { + if !self.header_done { + let header_msg = Message::Version(&self.version); + let bytes = header_msg.to_bytes(); + self.inner.write_all(&bytes)?; + self.header_done = true; + } + let bytes = message.to_bytes(); // Assuming Message has a to_bytes() method + self.inner.write_all(&bytes)?; + Ok(()) + } + + /// Write multiple messages into the buffer. + /// + /// # Errors + /// + /// See [`io::Error`]. + pub fn write_messages(&mut self, messages: &[Message]) -> Result<(), io::Error> { + for message in messages { + self.write_message(message)?; + } + Ok(()) + } + + /// Flush the buffer. Runs [`io::Writer::flush`]. + /// + /// # Errors + /// + /// See [`io::Error`]. + pub fn flush(&mut self) -> io::Result<()> { + self.inner.flush() + } +}