Never crash: filmcraft-export without expect()/unreachable!()

The five 'mux created' / 'audio' expects in the stepped exporter now return
an ExportError::Encode instead of panicking if the writer or audio pipeline
is missing, and the non-stepped dispatch returns Unsupported instead of
unreachable!(). The crate root gets the never-crash deny attribute, so every
FilmCraft production crate now carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-06 07:58:07 -04:00
co-authored by Claude Opus 5.5
parent 508a5a933f
commit 136424dc93
2 changed files with 15 additions and 6 deletions
+10 -5
View File
@@ -189,7 +189,7 @@ impl Exporter {
if n == 0 {
return Ok(());
}
let mux = self.mux.as_mut().expect("mux created");
let mux = self.mux.as_mut().ok_or_else(|| ExportError::Encode("internal: the container writer was not created".into()))?;
match self.aenc.as_mut() {
Some(a) => {
for au in a.encode(&buf)? {
@@ -221,7 +221,7 @@ impl Exporter {
if let Some(m) = self.mxf.as_mut() {
return m.write_video(packets);
}
let mux = self.mux.as_mut().expect("mux created");
let mux = self.mux.as_mut().ok_or_else(|| ExportError::Encode("internal: the container writer was not created".into()))?;
for p in packets {
mux.write_sample(self.vt, WriteSample { data: &p.data, duration: p.duration, composition_offset: p.composition_offset, is_sync: p.key })
.map_err(|e| ExportError::Io(e.to_string()))?;
@@ -282,7 +282,7 @@ impl Exporter {
if !self.settings.part_of_batch {
progress.set_status("Measuring loudness");
}
let a = self.audio.as_mut().expect("audio");
let a = self.audio.as_mut().ok_or_else(|| ExportError::Encode("internal: the audio pipeline was not created".into()))?;
a.measure(&self.settings, sources, &|| progress.cancel.load(Ordering::Relaxed))?;
if filmcraft_media::pending::take() {
return Ok(Step::Pending);
@@ -361,7 +361,7 @@ impl Exporter {
self.write_audio(mixed)?;
if let (Some(at), Some(a)) = (self.at, self.aenc.as_mut()) {
let fs = a.frame_size();
let mux = self.mux.as_mut().expect("mux created");
let mux = self.mux.as_mut().ok_or_else(|| ExportError::Encode("internal: the container writer was not created".into()))?;
for au in a.flush()? {
mux.write_sample(at, WriteSample { data: &au, duration: fs, composition_offset: 0, is_sync: true })
.map_err(|e| ExportError::Io(e.to_string()))?;
@@ -370,7 +370,12 @@ impl Exporter {
let (bytes, extra_files) = match self.mxf.take() {
Some(m) => m.finish(&self.settings)?,
None => {
let w = self.mux.take().expect("mux created").finish().map_err(|e| ExportError::Io(e.to_string()))?;
let w = self
.mux
.take()
.ok_or_else(|| ExportError::Encode("internal: the container writer was not created".into()))?
.finish()
.map_err(|e| ExportError::Io(e.to_string()))?;
(w.finish(&self.settings)?, Vec::new())
}
};
+5 -1
View File
@@ -11,6 +11,7 @@
//! [`ExportSettings`] carries every Export-mode setting (frame size, rate, bitrate encoding, audio
//! format, multiplexer, captions, effects, metadata) as serde data; [`presets`] defines the
//! built-in presets.
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
use std::io::Write;
@@ -1116,7 +1117,10 @@ pub fn export(project: &Arc<Project>, seq: ItemId, settings: &ExportSettings, so
}
(total, count)
}
Format::H264 | Format::ProRes | Format::DnxHr | Format::Mjpeg | Format::MxfOp1a | Format::MxfOpAtom => unreachable!("stepped export"),
Format::H264 | Format::ProRes | Format::DnxHr | Format::Mjpeg | Format::MxfOp1a | Format::MxfOpAtom => {
// Handled by the stepped exporter above; reaching here would be a dispatch bug.
return Err(ExportError::Unsupported(format!("{:?} must run as a stepped export", settings.format)));
}
};
let secs = t0.elapsed().as_secs_f64();
if !settings.part_of_batch {