Validate stream-selection PIDs per class, not across both

StreamSelection::apply validated a listed PID by scanning ALL streams, so a PID
named in the wrong class's filter passed validation — an audio filter listing a
subtitle PID, say. `keeps` then matched it against the audio streams only, so
the requested track was silently absent from the output. That is precisely the
outcome this validation documents itself as preventing: "fail loud rather than
silently emit an MKV missing a requested track".

Each filter is now checked against its own stream class. The `listed_pids`
helper existed only for the cross-class scan and is removed rather than left
behind as dead code.

Test covers both directions plus the sanity case, and asserts a rejected
selection leaves the title unpruned.
This commit is contained in:
Matthew Jackson
2026-07-29 19:00:53 -07:00
parent 0e23a6b291
commit d4c913e0d3
+73 -16
View File
@@ -64,10 +64,33 @@ impl StreamSelection {
// Validate every listed PID exists in the title before mutating, so an // Validate every listed PID exists in the title before mutating, so an
// unknown PID leaves the title untouched (no partial prune). // unknown PID leaves the title untouched (no partial prune).
for pid in self.listed_pids() { //
let present = title.streams.iter().any(|s| stream_pid(s) == Some(pid)); // Validate PER CLASS. Scanning both classes let a PID listed in the WRONG
if !present { // filter pass validation — an audio filter naming a subtitle PID, say —
return Err(Error::SelectionPidUnknown { pid }); // and `keeps` then matches it against the audio streams only, so the
// requested track is silently absent from the output. That is exactly the
// "fail loud rather than silently emit an MKV missing a requested track"
// contract this validation exists to enforce.
if let PidFilter::Only(pids) = &self.audio {
for &pid in pids {
let present = title
.streams
.iter()
.any(|s| matches!(s, Stream::Audio(_)) && stream_pid(s) == Some(pid));
if !present {
return Err(Error::SelectionPidUnknown { pid });
}
}
}
if let PidFilter::Only(pids) = &self.subtitle {
for &pid in pids {
let present = title
.streams
.iter()
.any(|s| matches!(s, Stream::Subtitle(_)) && stream_pid(s) == Some(pid));
if !present {
return Err(Error::SelectionPidUnknown { pid });
}
} }
} }
@@ -105,18 +128,6 @@ impl StreamSelection {
Stream::Subtitle(s) => filter_keeps(&self.subtitle, s.pid), Stream::Subtitle(s) => filter_keeps(&self.subtitle, s.pid),
} }
} }
/// Every PID explicitly listed across both filters (for existence checking).
fn listed_pids(&self) -> Vec<u16> {
let mut v = Vec::new();
if let PidFilter::Only(pids) = &self.audio {
v.extend_from_slice(pids);
}
if let PidFilter::Only(pids) = &self.subtitle {
v.extend_from_slice(pids);
}
v
}
} }
fn filter_keeps(filter: &PidFilter, pid: u16) -> bool { fn filter_keeps(filter: &PidFilter, pid: u16) -> bool {
@@ -296,4 +307,50 @@ mod tests {
"codec_privates pruned to match the retained streams, in order" "codec_privates pruned to match the retained streams, in order"
); );
} }
/// A PID listed in the WRONG class's filter must fail loud, not validate and
/// then quietly vanish. Validation used to scan both audio and subtitle
/// streams, so an audio filter naming a subtitle PID passed — and `keeps`
/// then matched it against audio streams only, dropping the requested track
/// from the output with no error. That defeats the documented "fail loud
/// rather than silently emit an MKV missing a requested track" contract.
#[test]
fn a_pid_listed_in_the_wrong_class_filter_is_rejected() {
let mut t = title();
let before = t.streams.len();
// 0x1200 is a SUBTITLE pid, listed here in the AUDIO filter.
let sel = StreamSelection {
audio: PidFilter::Only(vec![0x1200]),
subtitle: PidFilter::All,
};
assert!(
sel.apply(&mut t).is_err(),
"a subtitle PID in the audio filter must be rejected"
);
assert_eq!(
t.streams.len(),
before,
"a rejected selection must not prune"
);
// And the mirror case: an audio pid listed in the subtitle filter.
let sel = StreamSelection {
audio: PidFilter::All,
subtitle: PidFilter::Only(vec![0x1100]),
};
assert!(
sel.apply(&mut t).is_err(),
"an audio PID in the subtitle filter must be rejected"
);
// Sanity: each PID in its OWN class still validates.
let sel = StreamSelection {
audio: PidFilter::Only(vec![0x1100]),
subtitle: PidFilter::Only(vec![0x1200]),
};
assert!(
sel.apply(&mut t).is_ok(),
"correctly-classed PIDs must apply"
);
}
} }