osc_codec10/
lib.rs

1//! osc-codec10: a small, no_std-friendly OSC 1.0 encoder/decoder
2//!
3//! - Zero-copy leaning: decoded Strings/Blobs borrow from the input buffer.
4//! - Strict 4-byte OSC alignment for strings/blobs.
5//! - Big endian numeric encoding per the OSC 1.0 spec.
6//! - Minimal scope: Messages and Bundles (bundle contains only messages in this first cut).
7//!
8//! ## no_std
9//! Default builds use `std`. For `no_std + alloc`:
10//! ```shell
11//! cargo build -p osc-codec10 --no-default-features --features alloc
12//! ```
13//!
14//! ## Examples
15//! See `examples/` for UDP send/recv samples (require `std`).
16
17#![cfg_attr(not(feature = "std"), no_std)]
18
19extern crate alloc;
20use alloc::string::String;
21use alloc::vec::Vec;
22use byteorder::{BigEndian, ByteOrder};
23use core::str;
24use osc_types10::{Bundle, Message, OscPacket, OscType};
25
26/// Errors that can occur while decoding.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Error {
29    Truncated,
30    InvalidString,
31    InvalidTag,
32    UnexpectedEof,
33    /// Error for malformed bundle elements (deprecated - bundles can now contain both messages and bundles).
34    NonMessageInBundle,
35}
36
37pub type Result<T> = core::result::Result<T, Error>;
38
39#[inline]
40fn pad4_len(len: usize) -> usize {
41    (4 - (len & 3)) & 3
42}
43
44fn put_str(buf: &mut Vec<u8>, s: &str) {
45    buf.extend_from_slice(s.as_bytes());
46    buf.push(0);
47    let pad = pad4_len(s.len() + 1);
48    buf.extend(std::iter::repeat_n(0, pad));
49}
50
51fn get_cstr_4(bytes: &[u8], mut off: usize) -> Result<(&str, usize)> {
52    // Find NUL terminator
53    let start = off;
54    while off < bytes.len() && bytes[off] != 0 {
55        off += 1;
56    }
57    if off >= bytes.len() {
58        return Err(Error::Truncated);
59    }
60    let s = core::str::from_utf8(&bytes[start..off]).map_err(|_| Error::InvalidString)?;
61    off += 1; // skip NUL
62              // Skip padding to 4-byte boundary
63    let pad = pad4_len(off - start);
64    if off + pad > bytes.len() {
65        return Err(Error::Truncated);
66    }
67    Ok((s, off + pad))
68}
69
70#[inline]
71fn put_i32(buf: &mut Vec<u8>, v: i32) {
72    let mut tmp = [0u8; 4];
73    BigEndian::write_i32(&mut tmp, v);
74    buf.extend_from_slice(&tmp);
75}
76#[inline]
77fn put_f32(buf: &mut Vec<u8>, v: f32) {
78    let mut tmp = [0u8; 4];
79    BigEndian::write_f32(&mut tmp, v);
80    buf.extend_from_slice(&tmp);
81}
82
83#[inline]
84fn get_i32(bytes: &[u8], off: &mut usize) -> Result<i32> {
85    if *off + 4 > bytes.len() {
86        return Err(Error::UnexpectedEof);
87    }
88    let v = BigEndian::read_i32(&bytes[*off..*off + 4]);
89    *off += 4;
90    Ok(v)
91}
92#[inline]
93fn get_f32(bytes: &[u8], off: &mut usize) -> Result<f32> {
94    if *off + 4 > bytes.len() {
95        return Err(Error::UnexpectedEof);
96    }
97    let v = BigEndian::read_f32(&bytes[*off..*off + 4]);
98    *off += 4;
99    Ok(v)
100}
101
102/// Encode a single OSC message into bytes.
103pub fn encode_message(msg: &Message<'_>) -> Vec<u8> {
104    let mut buf = Vec::new();
105    put_str(&mut buf, msg.address);
106
107    // Type tag (starts with ',')
108    let mut tag = String::from(",");
109    for a in &msg.args {
110        match a {
111            OscType::Int(_) => tag.push('i'),
112            OscType::Float(_) => tag.push('f'),
113            OscType::String(_) => tag.push('s'),
114            OscType::Blob(_) => tag.push('b'),
115        }
116    }
117    put_str(&mut buf, &tag);
118
119    for a in &msg.args {
120        match a {
121            OscType::Int(v) => put_i32(&mut buf, *v),
122            OscType::Float(v) => put_f32(&mut buf, *v),
123            OscType::String(s) => put_str(&mut buf, s),
124            OscType::Blob(b) => {
125                put_i32(&mut buf, b.len() as i32);
126                buf.extend_from_slice(b);
127                let pad = pad4_len(b.len());
128                buf.extend(std::iter::repeat_n(0, pad));
129            }
130        }
131    }
132    buf
133}
134
135/// Decode a single OSC message from bytes, returning the message and number of bytes consumed.
136pub fn decode_message<'a>(bytes: &'a [u8]) -> Result<(Message<'a>, usize)> {
137    let (address, mut off) = get_cstr_4(bytes, 0)?;
138    let (tag, off2) = get_cstr_4(bytes, off)?;
139    off = off2;
140
141    let mut args = Vec::new();
142    let mut chars = tag.chars();
143    if chars.next() != Some(',') {
144        return Err(Error::InvalidTag);
145    }
146
147    for t in chars {
148        match t {
149            'i' => {
150                args.push(OscType::Int(get_i32(bytes, &mut off)?));
151            }
152            'f' => {
153                args.push(OscType::Float(get_f32(bytes, &mut off)?));
154            }
155            's' => {
156                let (s, new_off) = get_cstr_4(bytes, off)?;
157                args.push(OscType::String(s));
158                off = new_off;
159            }
160            'b' => {
161                let len = get_i32(bytes, &mut off)? as usize;
162                if off + len > bytes.len() {
163                    return Err(Error::UnexpectedEof);
164                }
165                let blob = &bytes[off..off + len];
166                off += len;
167                let pad = pad4_len(len);
168                if off + pad > bytes.len() {
169                    return Err(Error::UnexpectedEof);
170                }
171                off += pad;
172                args.push(OscType::Blob(blob));
173            }
174            _ => return Err(Error::InvalidTag),
175        }
176    }
177
178    Ok((Message::new(address, args), off))
179}
180
181const BUNDLE_TAG: &str = "#bundle";
182
183/// Encode a bundle that can contain messages and nested bundles.
184pub fn encode_bundle(b: &Bundle<'_>) -> Vec<u8> {
185    let mut buf = Vec::new();
186    put_str(&mut buf, BUNDLE_TAG);
187    // 64-bit big-endian NTP timetag
188    let mut tt = [0u8; 8];
189    BigEndian::write_u64(&mut tt, b.timetag);
190    buf.extend_from_slice(&tt);
191
192    for packet in &b.packets {
193        let pkt = match packet {
194            OscPacket::Message(msg) => encode_message(msg),
195            OscPacket::Bundle(bundle) => encode_bundle(bundle),
196        };
197        put_i32(&mut buf, pkt.len() as i32);
198        buf.extend_from_slice(&pkt);
199    }
200    buf
201}
202
203/// Decode a bundle that can contain messages and nested bundles. Returns the bundle and number of bytes consumed.
204pub fn decode_bundle<'a>(bytes: &'a [u8]) -> Result<(Bundle<'a>, usize)> {
205    let (tag, mut off) = get_cstr_4(bytes, 0)?;
206    if tag != BUNDLE_TAG {
207        return Err(Error::InvalidString);
208    }
209    if off + 8 > bytes.len() {
210        return Err(Error::Truncated);
211    }
212    let timetag = BigEndian::read_u64(&bytes[off..off + 8]);
213    off += 8;
214
215    let mut packets = Vec::new();
216    while off < bytes.len() {
217        let size = get_i32(bytes, &mut off)? as usize;
218        if off + size > bytes.len() {
219            return Err(Error::Truncated);
220        }
221
222        let element_bytes = &bytes[off..off + size];
223
224        // Try to determine if this is a bundle by checking if it has a valid bundle structure
225        // A bundle must have at minimum: "#bundle\0" (8 bytes aligned) + 8-byte timetag = 16 bytes
226        let is_bundle =
227            if element_bytes.len() >= 16 && element_bytes.starts_with(BUNDLE_TAG.as_bytes()) {
228                // Check if it's properly null-terminated and 4-byte aligned like a real bundle
229                let tag_end = BUNDLE_TAG.len();
230                element_bytes.get(tag_end) == Some(&0) && {
231                    // Calculate where the timetag should start (after null-terminated "#bundle" + padding)
232                    let tag_with_null_len = tag_end + 1;
233                    let padding = pad4_len(tag_with_null_len);
234                    let timetag_start = tag_with_null_len + padding;
235                    // Ensure we have enough bytes for the timetag
236                    element_bytes.len() >= timetag_start + 8
237                }
238            } else {
239                false
240            };
241
242        if is_bundle {
243            // Try to decode as bundle first, fall back to message if it fails
244            match decode_bundle(element_bytes) {
245                Ok((bundle, used)) if used == size => {
246                    packets.push(OscPacket::Bundle(bundle));
247                }
248                _ => {
249                    // Bundle decoding failed, treat as message
250                    let (msg, used) = decode_message(element_bytes)?;
251                    if used != size {
252                        return Err(Error::InvalidTag);
253                    }
254                    packets.push(OscPacket::Message(msg));
255                }
256            }
257        } else {
258            let (msg, used) = decode_message(element_bytes)?;
259            if used != size {
260                return Err(Error::InvalidTag);
261            }
262            packets.push(OscPacket::Message(msg));
263        }
264
265        off += size;
266    }
267    Ok((Bundle::new(timetag, packets), off))
268}