//! The AppleMIDI session control protocol. //! //! Control packets travel on the session's control port and carry no MIDI. They open a session, //! close it, and keep the two sides' clocks aligned. //! //! The layout was verified against Apple's own implementation rather than taken from //! documentation: an invitation sent to a Mac running Network MIDI was accepted, and the reply //! is kept below as a test fixture. use std::fmt; /// Marks a packet as session control rather than RTP. /// /// Two 0xFF bytes cannot begin a valid RTP header, which is how a receiver tells the two apart on /// a shared port. pub const SIGNATURE: [u8; 2] = [0xFF, 0xFF]; /// The only protocol version Apple's implementation speaks. pub const PROTOCOL_VERSION: u32 = 2; /// Bytes before the name field in an invitation-family packet. const INVITATION_HEADER_LEN: usize = 16; /// Bytes in a clock synchronisation packet carrying all three timestamps. const CLOCK_PACKET_LEN: usize = 36; /// Bytes in a receiver feedback packet. const FEEDBACK_PACKET_LEN: usize = 12; /// Longest peer name accepted, to bound allocation on hostile input. const MAX_NAME_LEN: usize = 256; /// Highest valid clock synchronisation count. const MAX_CLOCK_COUNT: u8 = 2; /// Why a control packet could not be parsed. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum ParseError { /// The packet did not begin with the control signature. #[error("not a session control packet")] NotControl, /// The packet ended before a field it declared. #[error("packet is {actual} bytes, needs at least {expected}")] TooShort { /// How many bytes were present. actual: usize, /// How many were needed. expected: usize, }, /// The two command bytes are not a command this implementation knows. #[error("unknown session command {0:?}")] UnknownCommand([u8; 2]), /// The peer speaks a protocol version this implementation does not. #[error("peer speaks protocol version {found}, expected {PROTOCOL_VERSION}")] UnsupportedVersion { /// The version the peer declared. found: u32, }, /// The name field is longer than any legitimate peer name. #[error("peer name exceeds {MAX_NAME_LEN} bytes")] NameTooLong, /// The clock packet declared more timestamps than the protocol allows. #[error("clock count {0} exceeds the maximum of {MAX_CLOCK_COUNT}")] ClockCountOutOfRange(u8), } /// Which control message a packet carries. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SessionCommand { /// Asking to open a session. Invitation, /// Accepting an invitation. Accepted, /// Refusing an invitation. Rejected, /// Ending a session. EndSession, /// Exchanging timestamps. ClockSync, /// Reporting the highest sequence number received, which trims the sender's journal. ReceiverFeedback, } impl SessionCommand { /// Returns the two ASCII bytes identifying this command on the wire. pub fn as_bytes(&self) -> [u8; 2] { match self { Self::Invitation => *b"IN", Self::Accepted => *b"OK", Self::Rejected => *b"NO", Self::EndSession => *b"BY", Self::ClockSync => *b"CK", Self::ReceiverFeedback => *b"RS", } } /// Reads a command from its two wire bytes. pub fn from_bytes(bytes: [u8; 2]) -> Result { match &bytes { b"IN" => Ok(Self::Invitation), b"OK" => Ok(Self::Accepted), b"NO" => Ok(Self::Rejected), b"BY" => Ok(Self::EndSession), b"CK" => Ok(Self::ClockSync), b"RS" => Ok(Self::ReceiverFeedback), _ => Err(ParseError::UnknownCommand(bytes)), } } } impl fmt::Display for SessionCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let bytes = self.as_bytes(); f.write_str(std::str::from_utf8(&bytes).unwrap_or("??")) } } /// The four commands that open and close a session. /// /// Narrower than `SessionCommand` on purpose: a session packet cannot carry a clock exchange or a /// feedback report, and the type should not pretend otherwise. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Handshake { /// Asking to open a session. Invitation, /// Accepting an invitation. Accepted, /// Refusing an invitation. Rejected, /// Ending a session. EndSession, } impl Handshake { /// Returns the wire command this handshake corresponds to. pub fn command(&self) -> SessionCommand { match self { Self::Invitation => SessionCommand::Invitation, Self::Accepted => SessionCommand::Accepted, Self::Rejected => SessionCommand::Rejected, Self::EndSession => SessionCommand::EndSession, } } /// Reads a handshake from a wire command, returning `None` for the other two. pub fn from_command(command: SessionCommand) -> Option { match command { SessionCommand::Invitation => Some(Self::Invitation), SessionCommand::Accepted => Some(Self::Accepted), SessionCommand::Rejected => Some(Self::Rejected), SessionCommand::EndSession => Some(Self::EndSession), SessionCommand::ClockSync | SessionCommand::ReceiverFeedback => None, } } } impl fmt::Display for Handshake { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.command().fmt(f) } } /// A parsed session control packet. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ControlPacket { /// An invitation, acceptance, rejection, or session end. Session { /// Which of the four this is. command: Handshake, /// Chosen by the initiator and echoed by the responder, tying a reply to its request. token: u32, /// Identifies the sending endpoint for the life of the session. ssrc: u32, /// The peer's display name. Absent on `BY`, where it carries no information. name: Option, }, /// A timestamp exchange. ClockSync { /// The sender's synchronisation source. ssrc: u32, /// How far through the three-message exchange this packet is. count: u8, /// Timestamps in units of 100 microseconds, indexed by exchange position. timestamps: [u64; 3], }, /// A receiver reporting what it has seen, which trims the sender's journal. /// /// ReceiverFeedback's 32-bit field is kept whole because implementations disagree on it: /// Apple's Network MIDI writes the RTP sequence number in the upper half and reads it from /// there, while rtpmidid reads the upper half but writes the lower (R-071). ReceiverFeedback { /// The reporting endpoint. ssrc: u32, /// The field as sent, holding an RTP sequence number in one half or the other. acknowledged: u32, }, } impl ControlPacket { /// Builds receiver feedback for an RTP sequence number, in the upper half as Apple writes it. pub fn feedback(ssrc: u32, sequence: u16) -> Self { Self::ReceiverFeedback { ssrc, acknowledged: u32::from(sequence) << 16, } } /// Builds an invitation to open a session. pub fn invitation(token: u32, ssrc: u32, name: impl Into) -> Self { Self::Session { command: Handshake::Invitation, token, ssrc, name: Some(name.into()), } } /// Builds an acceptance of an invitation. pub fn accepted(token: u32, ssrc: u32, name: impl Into) -> Self { Self::Session { command: Handshake::Accepted, token, ssrc, name: Some(name.into()), } } /// Builds a rejection of an invitation. pub fn rejected(token: u32, ssrc: u32) -> Self { Self::Session { command: Handshake::Rejected, token, ssrc, name: None, } } /// Builds a message ending a session. pub fn end_session(token: u32, ssrc: u32) -> Self { Self::Session { command: Handshake::EndSession, token, ssrc, name: None, } } /// Returns which command this packet carries. pub fn command(&self) -> SessionCommand { match self { Self::Session { command, .. } => command.command(), Self::ClockSync { .. } => SessionCommand::ClockSync, Self::ReceiverFeedback { .. } => SessionCommand::ReceiverFeedback, } } /// Returns the sending endpoint's synchronisation source. pub fn ssrc(&self) -> u32 { match self { Self::Session { ssrc, .. } | Self::ClockSync { ssrc, .. } | Self::ReceiverFeedback { ssrc, .. } => *ssrc, } } /// Encodes the packet onto the wire. pub fn encode(&self) -> Vec { let mut out = Vec::with_capacity(CLOCK_PACKET_LEN); out.extend_from_slice(&SIGNATURE); out.extend_from_slice(&self.command().as_bytes()); match self { Self::Session { token, ssrc, name, .. } => { out.extend_from_slice(&PROTOCOL_VERSION.to_be_bytes()); out.extend_from_slice(&token.to_be_bytes()); out.extend_from_slice(&ssrc.to_be_bytes()); if let Some(name) = name { // Truncate on a character boundary so a long name cannot produce invalid // UTF-8 on the wire. let mut end = name.len().min(MAX_NAME_LEN); while end > 0 && !name.is_char_boundary(end) { end -= 1; } if let Some(text) = name.get(..end) { out.extend_from_slice(text.as_bytes()); } out.push(0); } } Self::ClockSync { ssrc, count, timestamps, } => { out.extend_from_slice(&ssrc.to_be_bytes()); out.push(*count); // Three bytes of padding keep the timestamps eight-byte aligned. out.extend_from_slice(&[0, 0, 0]); for timestamp in timestamps { out.extend_from_slice(×tamp.to_be_bytes()); } } Self::ReceiverFeedback { ssrc, acknowledged } => { out.extend_from_slice(&ssrc.to_be_bytes()); out.extend_from_slice(&acknowledged.to_be_bytes()); } } out } /// Parses a packet received from a peer. /// /// Returns `NotControl` rather than an error for anything that is not a control packet, since /// RTP data shares the same socket pair and reaching here is normal. pub fn parse(bytes: &[u8]) -> Result { // Validate the framing before reading anything. let signature = bytes.get(..2).ok_or(ParseError::NotControl)?; if signature != SIGNATURE { return Err(ParseError::NotControl); } let raw_command: [u8; 2] = bytes .get(2..4) .and_then(|s| s.try_into().ok()) .ok_or(ParseError::TooShort { actual: bytes.len(), expected: 4, })?; let command = SessionCommand::from_bytes(raw_command)?; match command { SessionCommand::ClockSync => parse_clock_sync(bytes), SessionCommand::ReceiverFeedback => parse_feedback(bytes), other => { // The remaining four are exactly the handshake family, so this cannot fail. let handshake = Handshake::from_command(other) .ok_or(ParseError::UnknownCommand(raw_command))?; parse_session(handshake, bytes) } } } } /// Parses an invitation, acceptance, rejection or session end. fn parse_session(command: Handshake, bytes: &[u8]) -> Result { if bytes.len() < INVITATION_HEADER_LEN { return Err(ParseError::TooShort { actual: bytes.len(), expected: INVITATION_HEADER_LEN, }); } let version = read_u32(bytes, 4)?; if version != PROTOCOL_VERSION { return Err(ParseError::UnsupportedVersion { found: version }); } let token = read_u32(bytes, 8)?; let ssrc = read_u32(bytes, 12)?; // The name is optional and null-terminated. A peer that omits it is legitimate. let name = match bytes.get(INVITATION_HEADER_LEN..) { None | Some([]) => None, Some(tail) => { if tail.len() > MAX_NAME_LEN { return Err(ParseError::NameTooLong); } let end = tail.iter().position(|b| *b == 0).unwrap_or(tail.len()); tail.get(..end) .map(|text| String::from_utf8_lossy(text).into_owned()) } }; Ok(ControlPacket::Session { command, token, ssrc, name, }) } /// Parses a timestamp exchange. fn parse_clock_sync(bytes: &[u8]) -> Result { if bytes.len() < CLOCK_PACKET_LEN { return Err(ParseError::TooShort { actual: bytes.len(), expected: CLOCK_PACKET_LEN, }); } let ssrc = read_u32(bytes, 4)?; let count = *bytes.get(8).ok_or(ParseError::TooShort { actual: bytes.len(), expected: 9, })?; if count > MAX_CLOCK_COUNT { return Err(ParseError::ClockCountOutOfRange(count)); } let mut timestamps = [0u64; 3]; for (index, slot) in timestamps.iter_mut().enumerate() { // Timestamps begin after four signature and command bytes, four ssrc, one count and // three padding. let offset = 12 + index * 8; *slot = read_u64(bytes, offset)?; } Ok(ControlPacket::ClockSync { ssrc, count, timestamps, }) } /// Parses a receiver feedback packet. fn parse_feedback(bytes: &[u8]) -> Result { if bytes.len() < FEEDBACK_PACKET_LEN { return Err(ParseError::TooShort { actual: bytes.len(), expected: FEEDBACK_PACKET_LEN, }); } Ok(ControlPacket::ReceiverFeedback { ssrc: read_u32(bytes, 4)?, acknowledged: read_u32(bytes, 8)?, }) } /// Reads a big-endian `u32` without risking an out-of-bounds read. fn read_u32(bytes: &[u8], offset: usize) -> Result { let end = offset.saturating_add(4); bytes .get(offset..end) .and_then(|slice| slice.try_into().ok()) .map(u32::from_be_bytes) .ok_or(ParseError::TooShort { actual: bytes.len(), expected: end, }) } /// Reads a big-endian `u64` without risking an out-of-bounds read. fn read_u64(bytes: &[u8], offset: usize) -> Result { let end = offset.saturating_add(8); bytes .get(offset..end) .and_then(|slice| slice.try_into().ok()) .map(u64::from_be_bytes) .ok_or(ParseError::TooShort { actual: bytes.len(), expected: end, }) } #[cfg(test)] mod tests { use super::*; /// An acceptance captured from Apple's Network MIDI on macOS 15, replying to an invitation /// this implementation sent, with the peer's machine name replaced by a generic one. const APPLE_OK: &[u8] = &[ 0xff, 0xff, 0x4f, 0x4b, // signature, "OK" 0x00, 0x00, 0x00, 0x02, // protocol version 2 0x12, 0x34, 0x56, 0x78, // our token, echoed back 0xc0, 0x85, 0x8e, 0xfe, // the peer's ssrc // "Studio Mac\0", the peer's name, which ends the packet 0x53, 0x74, 0x75, 0x64, 0x69, 0x6f, 0x20, 0x4d, 0x61, 0x63, 0x00, ]; /// The invitation that drew [`APPLE_OK`] from Apple's Network MIDI, so known to be accepted. const OUR_IN: &[u8] = &[ 0xff, 0xff, 0x49, 0x4e, // signature, "IN" 0x00, 0x00, 0x00, 0x02, // protocol version 2 0x12, 0x34, 0x56, 0x78, // our token 0x0b, 0xad, 0xf0, 0x0d, // our ssrc // "Harbor Spike\0" 0x48, 0x61, 0x72, 0x62, 0x6f, 0x72, 0x20, 0x53, 0x70, 0x69, 0x6b, 0x65, 0x00, ]; /// Apple's Network MIDI acknowledging RTP packet 0xFDE5, as recorded in R-068. const APPLE_RS: &[u8] = &[ 0xff, 0xff, b'R', b'S', // signature, "RS" 0x37, 0x19, 0x3c, 0x27, // the peer's ssrc 0xfd, 0xe5, 0x00, 0x00, // the sequence number in the upper half ]; /// Bytes Apple's Network MIDI sent or accepted parse to the packet they carry, and that /// packet encodes back to the same bytes. /// /// Interoperability is a requirement, so the authority is Apple's own traffic rather than our /// encoder agreeing with itself. The `RS` row pins the half Apple uses: the sequence number /// `0xFDE5` in the upper sixteen bits, `0xFDE5 << 16 = 0xFDE5_0000`. #[test] fn apples_wire_bytes_parse_and_encode_exactly() { let cases = [ ( "Apple's acceptance", APPLE_OK, ControlPacket::accepted(0x1234_5678, 0xc085_8efe, "Studio Mac"), ), ( "our invitation Apple accepted", OUR_IN, ControlPacket::invitation(0x1234_5678, 0x0bad_f00d, "Harbor Spike"), ), ( "Apple's receiver feedback", APPLE_RS, ControlPacket::feedback(0x3719_3c27, 0xfde5), ), ]; for (name, bytes, packet) in cases { assert_eq!( ControlPacket::parse(bytes).as_ref(), Ok(&packet), "{name}: Apple's bytes must parse to this packet" ); assert_eq!( packet.encode(), bytes, "{name}: this packet must encode to exactly Apple's bytes" ); } } /// Parsing accepts each boundary of the AppleMIDI layout and refuses the input just past it, /// naming what was wrong. /// /// RTP data shares the port pair, so it is reported as not control rather than as an error. /// Apple speaks only protocol version 2, and a clock exchange has counts 0 to 2. A name is /// bounded at `MAX_NAME_LEN = 256` bytes including its terminator, so 255 characters and a /// null are accepted and 256 characters and a null are not. A goodbye may carry no name at /// all, which leaves the 16-byte header, and 15 bytes is one short of it. #[test] fn parse_accepts_each_boundary_and_refuses_the_input_past_it() { let with_version = |version: u32| { let mut bytes = ControlPacket::invitation(1, 2, "x").encode(); bytes[4..8].copy_from_slice(&version.to_be_bytes()); bytes }; let with_count = |count: u8| { let mut bytes = ControlPacket::ClockSync { ssrc: 1, count: 0, timestamps: [10, 20, 30], } .encode(); bytes[8] = count; bytes }; let with_name = |length: usize| ControlPacket::invitation(1, 2, "A".repeat(length)).encode(); let goodbye = ControlPacket::end_session(7, 8).encode(); let mut unknown = ControlPacket::invitation(1, 2, "x").encode(); unknown[2..4].copy_from_slice(b"ZZ"); let cases: [(&str, Vec, Result); 11] = [ ( "RTP data on the shared port", vec![0x80, 0x61, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00], Err(ParseError::NotControl), ), ( "an unknown command", unknown, Err(ParseError::UnknownCommand(*b"ZZ")), ), ( "protocol version 2", with_version(2), Ok(ControlPacket::invitation(1, 2, "x")), ), ( "protocol version 3", with_version(3), Err(ParseError::UnsupportedVersion { found: 3 }), ), ( "clock count 2, the last of an exchange", with_count(2), Ok(ControlPacket::ClockSync { ssrc: 1, count: 2, timestamps: [10, 20, 30], }), ), ( "clock count 3", with_count(3), Err(ParseError::ClockCountOutOfRange(3)), ), ( "a name filling the bound", with_name(MAX_NAME_LEN - 1), Ok(ControlPacket::invitation( 1, 2, "A".repeat(MAX_NAME_LEN - 1), )), ), ( "a name one byte past the bound", with_name(MAX_NAME_LEN), Err(ParseError::NameTooLong), ), ( "a goodbye with no name", goodbye.clone(), Ok(ControlPacket::end_session(7, 8)), ), ( "a header one byte short", goodbye[..INVITATION_HEADER_LEN - 1].to_vec(), Err(ParseError::TooShort { actual: INVITATION_HEADER_LEN - 1, expected: INVITATION_HEADER_LEN, }), ), ( "receiver feedback", ControlPacket::feedback(3, 4).encode(), Ok(ControlPacket::feedback(3, 4)), ), ]; for (name, bytes, want) in cases { assert_eq!( ControlPacket::parse(&bytes), want, "{name}: the parse must return this result" ); } } /// Every truncation of a packet short of its fixed layout is refused rather than read past /// or misread. /// /// An invitation's fixed part is its 16-byte header, a clock packet is 36 bytes (four of /// signature and command, four of SSRC, one count, three padding, three eight-byte /// timestamps), and receiver feedback is 12. A shorter packet that parsed would have taken /// its fields from the wrong offsets. #[test] fn a_truncated_packet_is_refused() { let cases = [ ( "an invitation", ControlPacket::invitation(1, 2, "Studio Mac").encode(), 16, ), ( "a clock packet", ControlPacket::ClockSync { ssrc: 1, count: 2, timestamps: [1, 2, 3], } .encode(), 36, ), ( "receiver feedback", ControlPacket::feedback(1, 2).encode(), 12, ), ]; for (name, packet, fixed) in cases { for length in 0..fixed { assert!( ControlPacket::parse(&packet[..length]).is_err(), "{name} cut to {length} bytes must be refused, not read as a shorter packet" ); } } } /// A name longer than the bound is cut on a character boundary when encoded, so what goes on /// the wire is bounded and still valid UTF-8. /// /// U+2019 is three bytes in UTF-8, so 256 of them are 768 bytes. The last character boundary /// at or below `MAX_NAME_LEN = 256` is `85 * 3 = 255`, so 85 characters are sent and the /// packet is `16 + 255 + 1 = 272` bytes with the terminator. #[test] fn an_encoded_name_is_cut_on_a_character_boundary() { let encoded = ControlPacket::invitation(1, 2, "\u{2019}".repeat(MAX_NAME_LEN)).encode(); assert_eq!( encoded.len(), 272, "the name must be cut to the last whole character within the bound" ); assert_eq!( ControlPacket::parse(&encoded), Ok(ControlPacket::invitation(1, 2, "\u{2019}".repeat(85))), "the cut name must parse back as whole characters" ); } }