Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .argv.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
--no-playlist --newline -f h1080+ba/b -o /tmp/grab-fakehls-157968/staging/grab-hls.%(ext)s --ffmpeg-location /usr/bin --retries 3 --print after_move:filepath --merge-output-format mp4 --user-agent Grab-test/1.0 -- https://x.com/u/status/1
--no-playlist --newline -f h1080+ba/b -o /tmp/grab-fakehls-158301/staging/grab-hls.%(ext)s --ffmpeg-location /usr/bin --retries 3 --print after_move:filepath --merge-output-format mp4 --user-agent Grab-test/1.0 -- https://x.com/u/status/1
33 changes: 25 additions & 8 deletions src/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1217,8 +1217,8 @@ fn fmt_video_bytes(n: u64) -> String {
}
}

/// Listable video-only formats for one video: best per height, tallest
/// first. Only directly fetchable streams qualify (plain HTTPS, no DRM);
/// Listable video-only formats for one video: best per height (codec
/// rank, then filesize), tallest first. Only directly fetchable streams qualify (plain HTTPS, no DRM);
/// HLS variants fill heights with no direct stream (the worker pulls
/// those via ffmpeg); muxed files stay on the automatic path, which
/// already adopts them. Audio-only formats never appear here.
Expand Down Expand Up @@ -1279,14 +1279,23 @@ pub fn video_format_options(video: &Video, newest_first: bool) -> Vec<VideoForma
let short = if f.protocol == Protocol::M3U8Native {
"HLS".to_string()
} else {
f.codec_info
.video_codec
.as_deref()
.unwrap_or("?")
// Remote extractor string in a plain-text row: allowlist
// to label-safe chars so bidi overrides, newlines or
// oversized values can't spoof the dropdown.
let raw = f.codec_info.video_codec.as_deref().unwrap_or("?");
let clean: String = raw
.split('.')
.next()
.unwrap_or("?")
.to_string()
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '-' || *c == '_' || *c == '+')
.take(16)
.collect();
if clean.is_empty() {
"?".to_string()
} else {
clean
}
};
let label = match filesize_of(f) {
Some(n) => format!("{height}p · {short} · {}", fmt_video_bytes(n)),
Expand Down Expand Up @@ -2758,7 +2767,15 @@ fn hls_format_spec(quality: &str, pinned: Option<&str>, audio_only: bool) -> Str
if audio_only {
return "ba/b".to_string();
}
if let Some(id) = pinned.map(str::trim).filter(|s| !s.is_empty()) {
// Pinned ids are remote extractor strings: allowlist to selector-safe
// chars so a hostile id can't widen the yt-dlp format set (`,`, `[]`,
// `()` all change set semantics). Anything else falls through to the
// height rule below instead of failing the row.
if let Some(id) = pinned.map(str::trim).filter(|s| {
!s.is_empty()
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == ':')
}) {
return format!("{id}+ba/b");
}
match quality_height(quality) {
Expand Down
120 changes: 120 additions & 0 deletions src/video_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2804,3 +2804,123 @@ fn format_lines_survive_split_reads() {
trace_format_lines(&mut pending, b"");
assert!(pending.is_empty());
}

#[test]
fn hls_format_spec_rejects_hostile_pins() {
// A hostile pinned id must not widen the yt-dlp format set: `,`,
// `[]` and `()` all change set semantics. Rejected pins fall
// through to the height rule instead of failing the row.
assert_eq!(
hls_format_spec("1080p", Some("hls-99,hls-720"), false),
"bv*[height<=1080]+ba/b"
);
assert_eq!(
hls_format_spec("1080p", Some("best[height=1080]"), false),
"bv*[height<=1080]+ba/b"
);
assert_eq!(
hls_format_spec("1080p", Some("(hls-99)"), false),
"bv*[height<=1080]+ba/b"
);
// Dashes, underscores and colons are legitimate extractor id chars.
assert_eq!(
hls_format_spec("1080p", Some("hls-720_p:1"), false),
"hls-720_p:1+ba/b"
);
}

#[test]
fn picker_label_sanitizes_remote_codec() {
// Extractor-controlled codec strings render as plain text but must
// not spoof rows: bidi overrides, newlines and oversized values
// are stripped to label-safe chars (max 16).
let video = test_video(serde_json::json!([test_format_full(
"evil",
"avc1\u{202e}gnp8001\u{000a}FREE",
"none",
Some(720),
None,
"https",
false
),]));
let opts = video_format_options(&video, true);
assert_eq!(opts.len(), 1);
assert_eq!(opts[0].label, "720p · avc1gnp8001FREE");
// Empty-after-filter degrades to the placeholder, never an empty tag.
let video = test_video(serde_json::json!([test_format_full(
"weird",
"...",
"none",
Some(720),
None,
"https",
false
),]));
let opts = video_format_options(&video, true);
assert_eq!(opts[0].label, "720p · ?");
}

#[test]
fn plan_unknown_adoption_is_first_match() {
// Restored semantics: the Unknown fallback takes the first
// fetchable video container in extractor order, not the tallest.
// Two sparse candidates pin that ordering down.
let video = test_video(serde_json::json!([
serde_json::json!({
"format": "low",
"format_id": "low",
"protocol": "https",
"ext": "mp4",
"url": "https://cdn.example/low.mp4",
"http_headers": {},
}),
serde_json::json!({
"format": "high",
"format_id": "high",
"protocol": "https",
"ext": "mp4",
"url": "https://cdn.example/high.mp4",
"http_headers": {},
}),
test_format_full("music", "none", "mp4a.40.2", None, None, "https", false),
]));
let plan = plan_streams(&video, "1080p", false, None, true, 1);
assert!(plan.video_sel.is_none());
assert_eq!(plan.audio_sel.expect("adopted").format_id, "low");
assert!(plan.audio_only);
}

#[test]
fn plan_stale_pin_to_unlisted_id_resolves_as_split() {
// Pins are not restricted to listed ids: a stale persisted pin to
// a fetchable-but-unlisted split (here v1080-avc loses the 1080
// slot to v1080-vp9 on the codec tie-break, so it never lists)
// resolves through find_usable_format as a split paired with
// audio — never an adoption, never a failure.
let video = test_video(serde_json::json!([
test_format_full("v1080-vp9", "vp9", "none", Some(1080), None, "https", false),
test_format_full(
"v1080-avc",
"avc1.640028",
"none",
Some(1080),
None,
"https",
false
),
test_format_full("music", "none", "mp4a.40.2", None, None, "https", false),
]));
let listed: Vec<String> = video_format_options(&video, true)
.iter()
.map(|o| o.id.clone())
.collect();
assert!(
!listed.contains(&"v1080-avc".to_string()),
"fixture must keep the pin unlisted: {listed:?}"
);
let plan = plan_streams(&video, "1080p", false, Some("v1080-avc"), true, 1);
let v = plan.video_sel.expect("stale pin resolves");
assert_eq!(v.format_id, "v1080-avc");
assert!(plan.audio_sel.is_some(), "split pairs with audio");
assert!(!plan.audio_only, "split, never adoption");
}
Loading