Initial vid-repair scaffold

This commit is contained in:
2025-12-31 22:07:42 -05:00
commit dddac108fe
30 changed files with 3220 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::{Command, Stdio};
use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
use anyhow::{Context, Result};
use crate::rules::RuleSet;
#[derive(Debug)]
pub struct DecodeOutput {
pub lines: Vec<String>,
pub early_stop: bool,
}
pub fn run_decode(path: &Path, ffmpeg_path: &str, ruleset: &RuleSet) -> Result<DecodeOutput> {
let mut child = Command::new(ffmpeg_path)
.arg("-v")
.arg("error")
.arg("-i")
.arg(path)
.arg("-f")
.arg("null")
.arg("-")
.stderr(Stdio::piped())
.stdout(Stdio::null())
.spawn()
.with_context(|| format!("Failed to run ffmpeg decode for {}", path.display()))?;
let stderr = child.stderr.take().context("Failed to capture ffmpeg stderr")?;
let reader = BufReader::new(stderr);
let early_stop = Arc::new(AtomicBool::new(false));
let early_stop_flag = early_stop.clone();
let mut lines = Vec::new();
for line in reader.lines() {
let line = line.unwrap_or_default();
if line.is_empty() {
continue;
}
lines.push(line.clone());
if should_stop(&line, ruleset) {
early_stop_flag.store(true, Ordering::SeqCst);
let _ = child.kill();
break;
}
}
let _ = child.wait();
Ok(DecodeOutput {
lines,
early_stop: early_stop.load(Ordering::SeqCst),
})
}
fn should_stop(line: &str, ruleset: &RuleSet) -> bool {
for rule in &ruleset.rules {
if !rule.rule.stop_scan {
continue;
}
if rule.patterns.iter().any(|re| re.is_match(line)) {
return true;
}
}
false
}

View File

@@ -0,0 +1,83 @@
use std::path::Path;
use std::process::Command;
use anyhow::{Context, Result};
use serde_json::Value;
use super::types::{ProbeData, StreamInfo};
pub fn run_ffprobe(path: &Path, ffprobe_path: &str) -> Result<ProbeData> {
let output = Command::new(ffprobe_path)
.arg("-v")
.arg("error")
.arg("-print_format")
.arg("json")
.arg("-show_format")
.arg("-show_streams")
.arg(path)
.output()
.with_context(|| format!("Failed to run ffprobe on {}", path.display()))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!(
"ffprobe failed for {}: {}",
path.display(),
stderr.trim()
);
}
let raw: Value = serde_json::from_slice(&output.stdout)
.with_context(|| format!("Failed to parse ffprobe output for {}", path.display()))?;
let format_name = raw
.get("format")
.and_then(|fmt| fmt.get("format_name"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let duration = raw
.get("format")
.and_then(|fmt| fmt.get("duration"))
.and_then(|v| v.as_str())
.and_then(|s| s.parse::<f64>().ok());
let streams = raw
.get("streams")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|stream| StreamInfo {
codec_type: stream
.get("codec_type")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
codec_name: stream
.get("codec_name")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
width: stream.get("width").and_then(|v| v.as_u64()).map(|v| v as u32),
height: stream
.get("height")
.and_then(|v| v.as_u64())
.map(|v| v as u32),
sample_rate: stream
.get("sample_rate")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
channels: stream
.get("channels")
.and_then(|v| v.as_u64())
.map(|v| v as u32),
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
Ok(ProbeData {
format_name,
duration,
streams,
raw,
})
}

View File

@@ -0,0 +1,48 @@
use std::path::Path;
use anyhow::Result;
use crate::config::Config;
use crate::rules::{build_context, RuleMatch, RuleSet};
mod decode;
mod ffprobe;
mod types;
pub use types::{Issue, ProbeData, ScanOutcome, ScanRequest};
pub fn scan_file(path: &Path, config: &Config, ruleset: &RuleSet) -> Result<ScanOutcome> {
let probe = ffprobe::run_ffprobe(path, &config.ffprobe_path)?;
let decode = decode::run_decode(path, &config.ffmpeg_path, ruleset)?;
let context = build_context(&probe);
let matches = ruleset.match_lines(&decode.lines, &context);
let issues = matches
.iter()
.map(|hit| issue_from_match(hit))
.collect::<Vec<_>>();
Ok(ScanOutcome {
path: path.to_path_buf(),
probe,
issues,
decode_errors: decode.lines,
early_stop: decode.early_stop,
})
}
fn issue_from_match(hit: &RuleMatch) -> Issue {
Issue {
code: hit.rule_id.clone(),
severity: hit.severity,
fix_tier: hit.fix_tier,
message: hit
.notes
.clone()
.unwrap_or_else(|| hit.rule_id.clone()),
evidence: hit.evidence.clone(),
action: hit.action.clone(),
}
}

View File

@@ -0,0 +1,47 @@
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use crate::rules::{FixTier, Severity};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StreamInfo {
pub codec_type: Option<String>,
pub codec_name: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub sample_rate: Option<String>,
pub channels: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProbeData {
pub format_name: Option<String>,
pub duration: Option<f64>,
pub streams: Vec<StreamInfo>,
pub raw: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Issue {
pub code: String,
pub severity: Severity,
pub fix_tier: FixTier,
pub message: String,
pub evidence: Vec<String>,
pub action: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanOutcome {
pub path: PathBuf,
pub probe: ProbeData,
pub issues: Vec<Issue>,
pub decode_errors: Vec<String>,
pub early_stop: bool,
}
#[derive(Debug, Clone)]
pub struct ScanRequest {
pub path: PathBuf,
}