Skip to main content

ez_ffmpeg/core/cli/
error.rs

1//! Typed diagnostics for the CLI-compat layer.
2//!
3//! Every rejection is anchored to the offending token (its argv index and
4//! text), explains why the token is outside the supported subset, and ends
5//! with the static call-to-action line — a rejected command must never fail
6//! silently or vaguely.
7
8/// Static call-to-action appended to every subset rejection.
9pub(crate) const CTA: &str =
10    "want this supported? open an issue: https://github.com/YeautyYE/ez-ffmpeg/issues";
11
12/// ` (token #N)` when the position is known, empty otherwise.
13fn fmt_at(index: &Option<usize>) -> String {
14    match index {
15        Some(index) => format!(" (token #{index})"),
16        None => String::new(),
17    }
18}
19
20/// Where in the command an option was found (positional scoping: options
21/// apply to the next file on the command line).
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum CliScope {
25    /// An option the support table classifies as global: it applies to the
26    /// whole run wherever it appears (e.g. `-y`), so no file position is
27    /// reported for it.
28    Global,
29    /// Before `-i`: applies to the input file.
30    Input,
31    /// After the input, before the output path: applies to the output file.
32    Output,
33    /// After the output path: in ffmpeg grammar an option here would apply
34    /// to a FOLLOWING output file, which the single-output subset does not
35    /// have.
36    AfterOutput,
37}
38
39impl std::fmt::Display for CliScope {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            CliScope::Global => write!(f, "global scope"),
43            CliScope::Input => write!(f, "input #0"),
44            CliScope::Output => write!(f, "output #0"),
45            CliScope::AfterOutput => {
46                write!(f, "after output #0 (would apply to a following output)")
47            }
48        }
49    }
50}
51
52/// Error type of the CLI-compat entry points ([`from_cli_args`],
53/// [`from_cli`], [`emit_rust_code`], [`emit_rust_code_from_args`]).
54///
55/// The subset contract: every argv token must classify against the
56/// compatibility manifest or the whole command is rejected with one of these
57/// variants — the layer never guesses, approximates, or silently drops a
58/// token.
59///
60/// [`from_cli_args`]: crate::core::cli::from_cli_args
61/// [`from_cli`]: crate::core::cli::from_cli
62/// [`emit_rust_code`]: crate::core::cli::emit_rust_code
63/// [`emit_rust_code_from_args`]: crate::core::cli::emit_rust_code_from_args
64#[derive(thiserror::Error, Debug)]
65#[non_exhaustive]
66pub enum CliError {
67    /// The single-string form could not be tokenized under the documented
68    /// POSIX word-splitting contract (shell constructs, unterminated quotes,
69    /// bare newlines, trailing backslash…). `offset` is the byte offset of
70    /// the offending character in the original string.
71    #[error("tokenize error at byte {offset}: {message}\n  the string form implements POSIX word splitting only — no variables, globs, tilde, pipes, redirects, comments or command lists; pass an argv slice to from_cli_args to sidestep shell quoting entirely\n  {CTA}")]
72    Tokenize {
73        /// What is wrong at `offset` (e.g. `unterminated single quote`).
74        message: String,
75        /// Byte offset of the offending character in the original command
76        /// string.
77        offset: usize,
78    },
79
80    /// A token in option position did not classify into the supported option
81    /// table. `reason` explains the status (unknown, documented gap, alias,
82    /// per-stream indexed variant, …); `hint` may name the nearest supported
83    /// spelling.
84    #[error("unsupported option `{option}` (token #{index}, {scope})\n  {reason}{}\n  {CTA}", .hint.as_deref().map(|h| format!("\n  {h}")).unwrap_or_default())]
85    UnsupportedOption {
86        /// The rejected option token, exactly as typed.
87        option: String,
88        /// Zero-based index of the option token in the parsed argv.
89        index: usize,
90        /// Where in the command the token was found.
91        scope: CliScope,
92        /// Why the spelling is outside the subset (unknown, documented gap,
93        /// alias, per-stream indexed variant, …).
94        reason: String,
95        /// Nearest supported spelling, when one is close enough to suggest.
96        hint: Option<String>,
97    },
98
99    /// The option is supported but this value form is not.
100    #[error("unsupported value `{value}` for `{option}` (token #{index})\n  {reason}\n  {CTA}")]
101    UnsupportedValue {
102        /// The option whose value failed its grammar.
103        option: String,
104        /// The rejected value token, verbatim.
105        value: String,
106        /// Zero-based argv index of the VALUE token (not of the option that
107        /// introduced it).
108        index: usize,
109        /// The value grammar the token violated.
110        reason: String,
111    },
112
113    /// The command's structure is outside the subset (missing/duplicated
114    /// files, non-canonical ordering, trailing tokens, …). This is a subset
115    /// limitation, not necessarily invalid ffmpeg syntax.
116    #[error("unsupported command layout at token #{index} (`{token}`)\n  {reason}\n  {CTA}")]
117    UnsupportedLayout {
118        /// Token at which the violation was detected; empty when the command
119        /// ended before a required token (missing input or output).
120        token: String,
121        /// Zero-based argv index of `token`; the total token count when the
122        /// command ended early.
123        index: usize,
124        /// The layout rule the command violated.
125        reason: String,
126    },
127
128    /// Two options that cannot coexist (e.g. `-t` and `-to` in the same
129    /// scope, `-map` together with `-vf`). The indexes anchor each side's
130    /// first occurrence in the argv when known.
131    #[error("conflicting options `{first}`{} and `{second}`{}\n  {reason}\n  {CTA}", fmt_at(.first_index), fmt_at(.second_index))]
132    ConflictingOptions {
133        /// Display name of the first participant; may carry a value
134        /// qualifier (`-c:v copy`) or be collective (`-hls_*`).
135        first: String,
136        /// Display name of the second participant (same conventions as
137        /// `first`).
138        second: String,
139        /// Argv index of `first`'s earliest participating occurrence, when
140        /// resolvable.
141        first_index: Option<usize>,
142        /// Argv index of `second`'s earliest participating occurrence, when
143        /// resolvable.
144        second_index: Option<usize>,
145        /// Why the pair cannot coexist in one command.
146        reason: String,
147    },
148
149    /// The command has no `-y`. The CLI would prompt interactively before
150    /// overwriting; this crate always opens outputs with O_CREAT|O_TRUNC and
151    /// has no prompt, so a command without `-y` cannot be run faithfully.
152    #[error("missing mandatory `-y`\n  without -y the ffmpeg CLI prompts before overwriting an existing output; this library always creates/truncates the output and cannot reproduce that prompt, so the subset requires an explicit -y\n  {CTA}")]
153    MissingOverwriteFlag,
154
155    /// Every token classified, but the command shape is not in the verified
156    /// set of the compatibility manifest — runtime execution is refused.
157    /// The emitters still accept this command and label their output as
158    /// unverified scaffolding.
159    #[error("command shape is not verified for execution\n  parsed options: [{}]\n  only shapes backed by a semantic golden may run; use emit_rust_code / emit_rust_code_from_args to generate unverified scaffolding code instead\n  {CTA}", .parsed_options.join(", "))]
160    NotVerified {
161        /// The command's manifest fingerprint: sorted, scope-qualified
162        /// canonical option keys (e.g. `in:-ss`, `out:-c:v`).
163        parsed_options: Vec<String>,
164    },
165
166    /// Every token classified, but the command's option-set fingerprint
167    /// matches neither a verified shape nor a documented emit-only entry.
168    /// The manifest enumerates its emit surface explicitly — arbitrary
169    /// combinations are rejected, not silently scaffolded.
170    #[error("command shape is not in the compatibility manifest\n  parsed options: [{}]\n  neither a verified shape nor a documented emit-only entry; nothing is generated for unenumerated shapes\n  {CTA}", .parsed_options.join(", "))]
171    UnmatchedShape {
172        /// The command's manifest fingerprint: sorted, scope-qualified
173        /// canonical option keys (e.g. `in:-ss`, `out:-c:v`).
174        parsed_options: Vec<String>,
175    },
176
177    /// A `-vf` command's source stream is not structurally unique: the
178    /// opened input carries more or fewer than exactly one video stream.
179    /// The CLI would score-select one stream and filter it; the subset runs
180    /// the filter only when no selection is involved at all (the hard
181    /// simple-filter prerequisite), so ambiguous inputs are rejected after
182    /// probing instead of silently filtering a chosen stream.
183    #[error("-vf requires an input with exactly one video stream; this input has {video_streams}\n  the ffmpeg CLI would score-select one stream to filter; the subset only executes filters over a structurally unique source\n  {CTA}")]
184    AmbiguousFilterSource {
185        /// Video-stream count of the opened input (anything but exactly 1).
186        video_streams: usize,
187    },
188
189    /// The linked FFmpeg libraries are not one of the verified runtime
190    /// profiles. Raised before any I/O.
191    #[error("linked FFmpeg is not a verified runtime profile\n  linked: libavcodec {linked_avcodec}, libavformat {linked_avformat}; verified profiles: {verified}\n  emit_rust_code still works — only in-process execution is gated\n  {CTA}")]
192    UnverifiedRuntimeProfile {
193        /// `major.minor` of the linked libavcodec.
194        linked_avcodec: String,
195        /// `major.minor` of the linked libavformat.
196        linked_avformat: String,
197        /// Rendered list of the verified profiles (name plus the
198        /// libavcodec/libavformat pairs each one requires).
199        verified: String,
200    },
201
202    /// The command classified and its shape is verified, but building the
203    /// pipeline failed (I/O, codec availability, filter validation…). This
204    /// wraps the crate's own typed error.
205    #[error("building the pipeline failed: {0}")]
206    Build(
207        /// The crate's typed error from context building.
208        #[from]
209        crate::error::Error,
210    ),
211}