chore(hardening): tighten security and footprint defaults
This commit is contained in:
@@ -21,6 +21,13 @@ use kon_core::types::AudioSamples;
|
||||
/// input silently returned `Ok` with whatever had decoded before the
|
||||
/// failure — flagged by the 2026-04-22 review (RB-09).
|
||||
pub fn decode_audio_file(path: &Path) -> Result<AudioSamples> {
|
||||
decode_audio_file_limited(path, None)
|
||||
}
|
||||
|
||||
pub fn decode_audio_file_limited(
|
||||
path: &Path,
|
||||
max_duration_secs: Option<f64>,
|
||||
) -> Result<AudioSamples> {
|
||||
let file = File::open(path)
|
||||
.map_err(|e| KonError::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
|
||||
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
||||
@@ -30,13 +37,48 @@ pub fn decode_audio_file(path: &Path) -> Result<AudioSamples> {
|
||||
hint.with_extension(ext);
|
||||
}
|
||||
|
||||
decode_media_stream(mss, &hint)
|
||||
decode_media_stream(mss, &hint, max_duration_secs)
|
||||
}
|
||||
|
||||
pub fn probe_audio_duration_secs(path: &Path) -> Result<Option<f64>> {
|
||||
let file = File::open(path)
|
||||
.map_err(|e| KonError::AudioDecodeFailed(format!("Cannot open file: {e}")))?;
|
||||
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
||||
let mut hint = Hint::new();
|
||||
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||
hint.with_extension(ext);
|
||||
}
|
||||
|
||||
let probed = symphonia::default::get_probe()
|
||||
.format(
|
||||
&hint,
|
||||
mss,
|
||||
&FormatOptions::default(),
|
||||
&MetadataOptions::default(),
|
||||
)
|
||||
.map_err(|e| KonError::AudioDecodeFailed(format!("Unsupported format: {e}")))?;
|
||||
let track = probed
|
||||
.format
|
||||
.default_track()
|
||||
.ok_or_else(|| KonError::AudioDecodeFailed("No audio track found".into()))?;
|
||||
let sample_rate = track
|
||||
.codec_params
|
||||
.sample_rate
|
||||
.ok_or_else(|| KonError::AudioDecodeFailed("Unknown sample rate".into()))?;
|
||||
Ok(track
|
||||
.codec_params
|
||||
.n_frames
|
||||
.map(|frames| frames as f64 / sample_rate as f64))
|
||||
}
|
||||
|
||||
/// Decode from an already-constructed `MediaSourceStream`. Split out so
|
||||
/// tests can inject a custom `MediaSource` (for example, one that
|
||||
/// returns a mid-stream I/O error) to verify error propagation.
|
||||
fn decode_media_stream(mss: MediaSourceStream, hint: &Hint) -> Result<AudioSamples> {
|
||||
fn decode_media_stream(
|
||||
mss: MediaSourceStream,
|
||||
hint: &Hint,
|
||||
max_duration_secs: Option<f64>,
|
||||
) -> Result<AudioSamples> {
|
||||
let probed = symphonia::default::get_probe()
|
||||
.format(
|
||||
hint,
|
||||
@@ -61,6 +103,7 @@ fn decode_media_stream(mss: MediaSourceStream, hint: &Hint) -> Result<AudioSampl
|
||||
}
|
||||
|
||||
let track_id = track.id;
|
||||
let max_samples = max_duration_secs.map(|secs| (secs * sample_rate as f64).ceil() as usize);
|
||||
|
||||
let mut decoder = symphonia::default::get_codecs()
|
||||
.make(&track.codec_params, &DecoderOptions::default())
|
||||
@@ -111,6 +154,15 @@ fn decode_media_stream(mss: MediaSourceStream, hint: &Hint) -> Result<AudioSampl
|
||||
samples.push(sum / channels as f32);
|
||||
}
|
||||
}
|
||||
if max_samples
|
||||
.map(|limit| samples.len() > limit)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(KonError::AudioDecodeFailed(format!(
|
||||
"Audio is longer than the {:.0} minute import limit",
|
||||
max_duration_secs.unwrap_or(0.0) / 60.0
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if samples.is_empty() {
|
||||
@@ -222,7 +274,7 @@ mod tests {
|
||||
let mut hint = Hint::new();
|
||||
hint.with_extension("wav");
|
||||
|
||||
let result = decode_media_stream(mss, &hint);
|
||||
let result = decode_media_stream(mss, &hint, None);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"mid-stream I/O error must surface, got: {result:?}"
|
||||
|
||||
Reference in New Issue
Block a user