chore(hardening): tighten security and footprint defaults

This commit is contained in:
2026-04-24 19:03:57 +01:00
parent 55b34d8ffc
commit b333c6229e
36 changed files with 8702 additions and 254 deletions

View File

@@ -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:?}"