azure_speech/connector/
utils.rs

1use crate::Headers;
2
3static CRLF: &str = "\r\n";
4static HEADER_JSON_SEPARATOR: &str = "\r\n\r\n";
5
6pub fn make_text_payload(headers: Headers, data: Option<&str>) -> String {
7    let headers = transform_headers_to_string(headers);
8    let data = data.map_or("", |d| d);
9
10    format!("{}{CRLF}{}", headers, data)
11}
12
13pub fn make_binary_payload(headers: Headers, data: Option<&[u8]>) -> Vec<u8> {
14    let headers = transform_headers_to_string(headers);
15
16    let data_length = if let Some(d) = data { d.len() } else { 0 };
17
18    let header_buffer: Vec<_> = headers.bytes().collect();
19    let header_length = header_buffer.len();
20    let mut payload = vec![0; 2 + header_length + data_length];
21    payload[0] = ((header_length >> 8) & 0xff) as u8;
22    payload[1] = (header_length & 0xff) as u8;
23    payload[2..2 + header_length].copy_from_slice(&header_buffer);
24
25    if let Some(d) = data {
26        payload[2 + header_length..].copy_from_slice(d);
27    }
28
29    payload
30}
31
32pub fn extract_headers_and_data_from_binary_message(
33    data: &[u8],
34) -> Result<(Headers, Option<Vec<u8>>), crate::Error> {
35    if data.len() < 2 {
36        return Err(crate::Error::ParseError(
37            "binary message too short".to_string(),
38        ));
39    }
40
41    let header_length = ((data[0] as usize) << 8) + data[1] as usize;
42
43    if data.len() < 2 + header_length {
44        return Err(crate::Error::ParseError(format!(
45            "binary header length {} exceeds data len {}",
46            header_length, data.len()
47        )));
48    }
49
50    let headers = std::str::from_utf8(&data[2..2 + header_length])
51        .map_err(|_| crate::Error::ParseError("Error parsing headers".to_string()))?;
52    let data = if header_length + 2 < data.len() {
53        Some(data[2 + header_length..].to_vec())
54    } else {
55        None
56    };
57
58    Ok((explode_headers_message(headers), data))
59}
60
61pub fn extract_headers_and_data_from_text_message(
62    text: &str,
63) -> Result<(Headers, Option<String>), crate::Error> {
64    let mut split_response = text.split(HEADER_JSON_SEPARATOR);
65
66    let headers = explode_headers_message(split_response.next().unwrap_or_default());
67
68    Ok((headers, split_response.next().map(|x| x.to_string())))
69}
70
71fn transform_headers_to_string(map: Headers) -> String {
72    let mut headers = String::new();
73    for (content_type, value) in map {
74        headers.push_str(format!("{content_type}:{value}{CRLF}").as_str());
75    }
76
77    headers
78}
79
80// Example of message received:
81// X-RequestId:5FF045681350489AAF1CD740EE5ACDDD
82// Path:turn.start
83// Content-Type:application/json; charset=utf-8
84fn explode_headers_message(headers: &str) -> Headers {
85    headers
86        .split(CRLF)
87        .map(|x| {
88            let mut split = x.split(":");
89            (
90                split.next().unwrap_or("").to_string(),
91                split.next().unwrap_or("").to_string(),
92            )
93        })
94        .filter(|(k, _)| !k.is_empty())
95        .collect()
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn explode_message() {
104        let text = "X-RequestId:91067ed0-bd0d-4682-891f-446a95996c19\r\nContent-Type:application/json; charset=utf-8\r\nPath:audio.metadata\r\n\r\n{\"Metadata\": [{\"Type\": \"SessionEnd\",\"Data\": {\"Offset\": 11250000}}]}";
105        let result = extract_headers_and_data_from_text_message(text);
106        match result {
107            Ok((headers, data)) => {
108                assert_eq!(
109                    headers,
110                    vec![
111                        (
112                            "X-RequestId".to_string(),
113                            "91067ed0-bd0d-4682-891f-446a95996c19".to_string()
114                        ),
115                        (
116                            "Content-Type".to_string(),
117                            "application/json; charset=utf-8".to_string()
118                        ),
119                        ("Path".to_string(), "audio.metadata".to_string()),
120                    ]
121                );
122                assert_eq!(data, Some("{\"Metadata\": [{\"Type\": \"SessionEnd\",\"Data\": {\"Offset\": 11250000}}]}".to_string()));
123            }
124            Err(e) => {
125                panic!("Error: {:?}", e);
126            }
127        }
128    }
129
130    #[test]
131    fn explode_headers_message_returns_correct_pairs_for_valid_input() {
132        let headers = "X-RequestId:5FF045681350489AAF1CD740EE5ACDDD\r\nPath:turn.start\r\nContent-Type:application/json; charset=utf-8";
133        let result = explode_headers_message(headers);
134        assert_eq!(
135            result,
136            vec![
137                (
138                    "X-RequestId".to_string(),
139                    "5FF045681350489AAF1CD740EE5ACDDD".to_string()
140                ),
141                ("Path".to_string(), "turn.start".to_string()),
142                (
143                    "Content-Type".to_string(),
144                    "application/json; charset=utf-8".to_string()
145                ),
146            ]
147        );
148    }
149
150    #[test]
151    fn explode_headers_message_returns_empty_vector_for_empty_input() {
152        let headers = "";
153        let result = explode_headers_message(headers);
154        assert_eq!(result, Vec::<(String, String)>::new());
155    }
156
157    #[test]
158    fn explode_headers_message_ignores_lines_without_colon() {
159        let headers =
160            "X-RequestId:5FF045681350489AAF1CD740EE5ACDDD\r\nInvalidLine\r\nPath:turn.start";
161        let result = explode_headers_message(headers);
162        assert_eq!(
163            result,
164            vec![
165                (
166                    "X-RequestId".to_string(),
167                    "5FF045681350489AAF1CD740EE5ACDDD".to_string()
168                ),
169                ("InvalidLine".to_string(), "".to_string()),
170                ("Path".to_string(), "turn.start".to_string()),
171            ]
172        );
173    }
174
175    #[test]
176    fn explode_headers_message_handles_lines_with_multiple_colons() {
177        let headers = "X-RequestId:5FF045681350489AAF1CD740EE5ACDDD\r\nPath:turn.start\r\nContent-Type:application/json; charset=utf-8\r\nMulti:Part:Header";
178        let result = explode_headers_message(headers);
179        assert_eq!(
180            result,
181            vec![
182                (
183                    "X-RequestId".to_string(),
184                    "5FF045681350489AAF1CD740EE5ACDDD".to_string()
185                ),
186                ("Path".to_string(), "turn.start".to_string()),
187                (
188                    "Content-Type".to_string(),
189                    "application/json; charset=utf-8".to_string()
190                ),
191                ("Multi".to_string(), "Part".to_string()),
192            ]
193        );
194    }
195
196    #[test]
197    fn extract_headers_and_data_from_binary_message_rejects_short_frames() {
198        // header length is encoded as 10 but only 5 bytes of data provided
199        let data = [0u8, 10, 1, 2, 3];
200        let res = extract_headers_and_data_from_binary_message(&data);
201        assert!(matches!(res, Err(crate::Error::ParseError(_))));
202    }
203}