1use super::openai_speech::parse_pcm_content_type;
7use crate::audio::EncodedAudioFormat;
8use crate::error::{ProviderError, Result};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ExpectedWireFormat {
13 PcmS16Le { sample_rate_hz: u32, channels: u16 },
15 Mp3,
17 Wav,
19}
20
21impl ExpectedWireFormat {
22 pub fn pcm(sample_rate_hz: u32, channels: u16) -> Self {
23 Self::PcmS16Le {
24 sample_rate_hz,
25 channels: channels.max(1),
26 }
27 }
28}
29
30fn looks_like_text_envelope(body: &[u8]) -> bool {
31 let trimmed = body
32 .iter()
33 .position(|b| !b.is_ascii_whitespace())
34 .map(|i| &body[i..])
35 .unwrap_or(&[]);
36 if trimmed.is_empty() {
37 return false;
38 }
39 trimmed.starts_with(b"{")
40 || trimmed.starts_with(b"[")
41 || trimmed.starts_with(b"<")
42 || trimmed.windows(5).any(|w| w.eq_ignore_ascii_case(b"<html"))
43 || trimmed.windows(5).any(|w| w.eq_ignore_ascii_case(b"<?xml"))
44}
45
46pub fn resolve_encoded_format(
51 provider: &str,
52 expected: ExpectedWireFormat,
53 content_type: &str,
54 body: &[u8],
55) -> Result<EncodedAudioFormat> {
56 let ct_raw = content_type.trim();
57 if ct_raw.is_empty() {
58 return Err(ProviderError::InvalidProviderPayload {
59 provider: provider.into(),
60 reason: "missing Content-Type on remote audio response".into(),
61 }
62 .into());
63 }
64
65 let ct_lower = ct_raw.to_ascii_lowercase();
66 let primary = ct_lower.split(';').next().unwrap_or("").trim();
67
68 if primary.starts_with("application/json")
70 || primary.starts_with("text/")
71 || primary.starts_with("application/xml")
72 || primary.starts_with("application/xhtml")
73 || looks_like_text_envelope(body)
74 {
75 return Err(ProviderError::InvalidProviderPayload {
76 provider: provider.into(),
77 reason: "remote audio response is not binary audio (JSON/text/HTML envelope)".into(),
78 }
79 .into());
80 }
81
82 match expected {
83 ExpectedWireFormat::PcmS16Le {
84 sample_rate_hz,
85 channels,
86 } => {
87 if primary.contains("mpeg") || primary == "audio/mp3" {
88 return Err(ProviderError::InvalidProviderPayload {
89 provider: provider.into(),
90 reason: "expected PCM but Content-Type is MP3".into(),
91 }
92 .into());
93 }
94 if primary.contains("wav") || body.starts_with(b"RIFF") {
95 return Err(ProviderError::InvalidProviderPayload {
96 provider: provider.into(),
97 reason: "expected PCM but response looks like WAV".into(),
98 }
99 .into());
100 }
101 if primary == "application/octet-stream" || primary == "binary/octet-stream" {
102 return Err(ProviderError::InvalidProviderPayload {
103 provider: provider.into(),
104 reason: "generic Content-Type cannot be accepted as PCM".into(),
105 }
106 .into());
107 }
108 let pcm_ok = primary == "audio/pcm"
110 || primary == "audio/l16"
111 || primary == "audio/l16;rate"
112 || primary.starts_with("audio/pcm")
113 || primary.starts_with("audio/l16");
114 if !pcm_ok && primary != "audio/raw" {
116 if primary != "audio/s16le" && primary != "audio/x-raw" {
118 return Err(ProviderError::InvalidProviderPayload {
119 provider: provider.into(),
120 reason: format!(
121 "Content-Type '{primary}' is not an allowlisted PCM type for requested PCM"
122 ),
123 }
124 .into());
125 }
126 }
127
128 if let Some((rate, ch)) = parse_pcm_content_type(ct_raw) {
130 if rate != sample_rate_hz {
131 return Err(ProviderError::InvalidProviderPayload {
132 provider: provider.into(),
133 reason: format!(
134 "PCM rate {rate} does not match requested {sample_rate_hz}"
135 ),
136 }
137 .into());
138 }
139 if ch != channels {
140 return Err(ProviderError::InvalidProviderPayload {
141 provider: provider.into(),
142 reason: format!("PCM channels {ch} do not match requested {channels}"),
143 }
144 .into());
145 }
146 }
147
148 if !body.len().is_multiple_of(2) {
149 return Err(ProviderError::InvalidProviderPayload {
150 provider: provider.into(),
151 reason: "PCM body length is not a multiple of 2".into(),
152 }
153 .into());
154 }
155 if body.is_empty() {
156 return Err(ProviderError::InvalidProviderPayload {
157 provider: provider.into(),
158 reason: "empty PCM body".into(),
159 }
160 .into());
161 }
162
163 Ok(EncodedAudioFormat::PcmS16Le {
164 sample_rate_hz,
165 channels,
166 })
167 }
168 ExpectedWireFormat::Mp3 => {
169 let mp3_ok = primary.contains("mpeg")
170 || primary == "audio/mp3"
171 || primary == "audio/x-mp3"
172 || primary == "audio/mpeg3";
173 if !mp3_ok {
174 return Err(ProviderError::InvalidProviderPayload {
175 provider: provider.into(),
176 reason: format!(
177 "Content-Type '{primary}' is not allowlisted MP3 for requested MP3"
178 ),
179 }
180 .into());
181 }
182 Ok(EncodedAudioFormat::Mp3)
183 }
184 ExpectedWireFormat::Wav => {
185 let wav_ok = primary == "audio/wav"
186 || primary == "audio/wave"
187 || primary == "audio/x-wav"
188 || primary == "audio/vnd.wave";
189 if !wav_ok {
190 return Err(ProviderError::InvalidProviderPayload {
191 provider: provider.into(),
192 reason: format!(
193 "Content-Type '{primary}' is not allowlisted WAV for requested WAV"
194 ),
195 }
196 .into());
197 }
198 if body.len() >= 4 && !body.starts_with(b"RIFF") {
199 return Err(ProviderError::InvalidProviderPayload {
200 provider: provider.into(),
201 reason: "WAV Content-Type without RIFF magic".into(),
202 }
203 .into());
204 }
205 Ok(EncodedAudioFormat::Wav)
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 #[test]
215 fn pcm_requires_allowlisted_mime() {
216 let exp = ExpectedWireFormat::pcm(24_000, 1);
217 let pcm = [0u8; 4];
218 assert!(resolve_encoded_format("t", exp, "audio/pcm;rate=24000;channels=1", &pcm).is_ok());
219 assert!(resolve_encoded_format("t", exp, "", &pcm).is_err());
220 assert!(resolve_encoded_format("t", exp, "application/octet-stream", &pcm).is_err());
221 assert!(resolve_encoded_format("t", exp, "application/json", b"{}").is_err());
222 assert!(resolve_encoded_format("t", exp, "audio/mpeg", &pcm).is_err());
223 assert!(resolve_encoded_format("t", exp, "audio/pcm", b"RIFF....").is_err());
224 assert!(resolve_encoded_format("t", exp, "text/html", b"<html>").is_err());
225 assert!(resolve_encoded_format("t", exp, "audio/pcm;rate=16000;channels=1", &pcm).is_err());
227 }
228
229 #[test]
230 fn json_body_never_pcm() {
231 let exp = ExpectedWireFormat::pcm(24_000, 1);
232 let err =
233 resolve_encoded_format("t", exp, "audio/pcm", b"{\"error\":\"nope\"}").unwrap_err();
234 let s = err.to_string();
235 assert!(!s.contains("nope")); assert!(s.contains("envelope") || s.contains("not binary"));
237 }
238
239 #[test]
240 fn mp3_and_wav_allowlists() {
241 assert!(resolve_encoded_format(
242 "t",
243 ExpectedWireFormat::Mp3,
244 "audio/mpeg",
245 &[0xFF, 0xFB, 0, 0]
246 )
247 .is_ok());
248 assert!(
249 resolve_encoded_format("t", ExpectedWireFormat::Wav, "audio/wav", b"RIFF....WAVE")
250 .is_ok()
251 );
252 assert!(
253 resolve_encoded_format("t", ExpectedWireFormat::Wav, "audio/wav", b"not-riff").is_err()
254 );
255 }
256}