Exit Codes And Error Handling#
IMPORTANT NOTE: The background context provided for this article references the "Gold Digger" project with database-specific exit codes (EXIT_NO_ROWS, EXIT_DB_AUTH_ERROR, EXIT_QUERY_ERROR) and SQL credential redaction. However, the actual codebase is libmagic-rs (a file type identification tool), which has a different exit code taxonomy and no database/SQL functionality. This article documents the actual implementation found in the libmagic-rs repository.
Overview#
Exit codes and error handling in libmagic-rs implement a Unix-convention exit code taxonomy with five distinct non-zero codes distinguishing configuration errors, file access issues, magic file problems, and resource limit violations. The system uses Rust's thiserror crate for structured error classification through a hierarchy of LibmagicError, ParseError, and EvaluationError types.
The error-to-exit-code mapping operates through the handle_error function in src/main.rs, which delegates to specialized handlers (handle_io_error, handle_parse_error_new, handle_evaluation_error_new, handle_timeout_error) that examine error subtypes and return appropriate exit codes. Exit codes are returned as numeric literals (0-5) rather than named constants.
The implementation provides automation-friendly error semantics through a strict mode flag: standard mode continues processing all files and exits 0 even on errors, while strict mode captures the first error and returns its exit code after processing completes. This design prioritizes batch processing utility while maintaining precise error signaling when needed.
Exit Code Taxonomy#
Code Definitions#
The rmagic CLI tool uses six distinct exit codes:
| Exit Code | Meaning | Trigger Conditions |
|---|---|---|
| 0 | Success | All files processed without errors |
| 1 | General error | EvaluationError or ConfigError |
| 2 | Misuse of shell command | IoError(InvalidInput) |
| 3 | File not found or access denied | IoError(NotFound/PermissionDenied) or FileError |
| 4 | Magic file not found or invalid | ParseError |
| 5 | Evaluation timeout or resource limits exceeded | Timeout error |
The taxonomy follows BSD-style exit code conventions: codes 0-2 align with standard Unix semantics (success, general error, usage error), while codes 3-5 provide application-specific error classification.
Design Rationale#
The exit code design prioritizes user-facing error classification over internal error granularity. File access errors (exit 3), argument validation errors (exit 2), and magic file problems (exit 4) receive distinct codes because they represent different corrective actions for users. Internal errors like EvaluationError variants (buffer overrun, recursion limit exceeded, invalid string encoding) all map to exit code 1 because they signal "file cannot be evaluated" regardless of the internal cause.
This decision reflects the tool's role as a Unix filter utility: automation scripts need to distinguish "file doesn't exist" (retry with different path) from "magic file corrupt" (fix configuration) from "evaluation timed out" (adjust limits), but do not need to distinguish between different evaluation failure modes.
Error Classification System#
Error Type Hierarchy#
The error taxonomy uses three primary error types defined with Rust's thiserror crate:
LibmagicError (top-level enum):
ParseError(ParseError)- Magic file parsing failures → exit 4EvaluationError(EvaluationError)- File evaluation failures → exit 1IoError(std::io::Error)- I/O operations → exit 2 or 3Timeout { timeout_ms: u64 }- Evaluation timeout → exit 5ConfigError { reason: String }- Configuration problems → exit 1FileError(String)- File access problems → exit 3
ParseError (8 variants):
InvalidSyntax- Malformed magic file syntaxUnsupportedFeature- Magic file uses unsupported featuresInvalidOffset/InvalidType/InvalidOperator/InvalidValue- Invalid magic rule componentsUnsupportedFormat- Binary .mgc files not supportedIoError- I/O failure during magic file reading
EvaluationError (9 variants):
BufferOverrun- Read beyond file boundariesInvalidOffset- Offset calculation failedUnsupportedType- Type not implementedRecursionLimitExceeded- Indirect rule depth exceededStringLengthExceeded- String longer than limitInvalidStringEncoding- String decoding failedTimeout- Evaluation took too longTypeReadError- Failed to read typed dataInternalError- Bug in evaluator (should not occur)
Error Mapping Implementation#
The handle_error function performs top-level error classification through pattern matching on LibmagicError variants:
fn handle_error(error: LibmagicError) -> i32 {
match error {
LibmagicError::IoError(ref io_err) => handle_io_error(io_err),
LibmagicError::ParseError(ref parse_err) => handle_parse_error_new(parse_err),
LibmagicError::EvaluationError(ref eval_err) => handle_evaluation_error_new(eval_err),
LibmagicError::Timeout { timeout_ms } => handle_timeout_error(timeout_ms),
LibmagicError::ConfigError { ref reason } => {
eprintln!("Configuration error: {reason}");
1
}
LibmagicError::FileError(ref msg) => {
eprintln!("File error: {msg}");
3
}
}
}
Delegated error handlers inspect error subtypes:
handle_io_error examines std::io::ErrorKind:
NotFound→ exit 3 ("File not found")PermissionDenied→ exit 3 ("Permission denied")InvalidInput→ exit 2 ("Invalid input")- Other → exit 3 ("File access failed")
handle_parse_error_new treats all ParseError variants uniformly → exit 4
handle_evaluation_error_new treats all EvaluationError variants uniformly → exit 1
handle_timeout_error handles timeout errors → exit 5
Each handler prints a descriptive error message to stderr before returning its exit code, ensuring users receive actionable diagnostics even in non-strict mode.
Error Propagation Pattern#
thiserror-Based Error Types#
Unlike ad-hoc error handling, libmagic-rs uses structured error types built with the thiserror crate. Each error variant includes the #[error("...")] attribute for automatic Display implementation and the #[from] attribute for automatic conversion:
#[derive(Debug, thiserror::Error)]
pub enum LibmagicError {
#[error("Parse error: {0}")]
ParseError(#[from] ParseError),
#[error("Evaluation error: {0}")]
EvaluationError(#[from] EvaluationError),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
// ...
}
The #[from] attribute enables automatic error type conversion when using the ? operator: a function returning Result<T, LibmagicError> can propagate a std::io::Error directly via ?, which automatically wraps it in LibmagicError::IoError.
Result Type Usage#
All fallible functions return Result<T, LibmagicError> and use the ? operator for error propagation. Example from load_magic_database:
fn load_magic_database(args: &Args) -> Result<MagicDatabase, LibmagicError> {
let config = args.to_evaluation_config();
if args.use_builtin {
return MagicDatabase::with_builtin_rules_and_config(config);
}
let magic_file_path = args.get_magic_file_path();
if !magic_file_path.exists() {
return Err(LibmagicError::ParseError(
libmagic_rs::ParseError::invalid_syntax(0, format!("Magic file not found..."))
));
}
validate_magic_file(&magic_file_path)?; // Propagates error via ?
MagicDatabase::load_from_file_with_config(&magic_file_path, config)
}
The ? operator propagates errors up the call stack until they reach run_analysis, where per-file error handling logic determines whether to accumulate errors or return immediately.
Batch Processing Error Semantics#
Standard vs Strict Mode#
The run_analysis function implements two error handling modes controlled by the --strict flag:
Standard Mode (default):
- Errors for individual files are printed to stderr
- Processing continues for all remaining files
- Function returns
Ok(())regardless of errors - Exit code is 0
Strict Mode (--strict flag):
- First error is captured in
first_errorvariable - Processing continues for all files (errors still printed)
- After all files processed, returns first
Errencountered - Results in non-zero exit code via
handle_error
Implementation pattern:
let mut first_error: Option<LibmagicError> = None;
for file_or_stdin in &args.files {
match process_file(file_or_stdin, &db, args) {
Ok(()) => {}
Err(e) => {
eprintln!("Error processing {}: {}", file_or_stdin.filename(), e);
if first_error.is_none() {
first_error = Some(e);
}
}
}
}
if let Some(error) = first_error {
if args.strict {
return Err(error);
}
}
Ok(())
This design reflects a deliberate trade-off: graceful degradation (process what you can) in standard mode vs strict failure signaling (report first error to caller) in strict mode. Both modes complete processing of all files to maximize utility of each invocation.
Automation Implications#
The dual-mode error handling affects automation differently:
Monitoring scripts should use --strict to detect any failures across a batch of files. Exit code 0 guarantees all files succeeded; non-zero exit indicates at least one file failed (check stderr for details).
Best-effort processing scripts should use standard mode to maximize output even when some files fail. Exit code 0 does not guarantee all files succeeded; must parse stderr to identify individual failures.
The main function unconditionally calls process::exit(exit_code), ensuring the exit code is always propagated to the shell.
Usage Examples#
Exit Code Testing#
Test specific error conditions by examining the exit code:
# Success case
rmagic /etc/passwd
echo $? # 0
# File not found (exit 3)
rmagic /nonexistent/file
echo $? # 3
# Permission denied (exit 3)
touch /tmp/noaccess
chmod 000 /tmp/noaccess
rmagic /tmp/noaccess
echo $? # 3
# Invalid magic file (exit 4)
echo "invalid syntax" > /tmp/bad.magic
rmagic -m /tmp/bad.magic /etc/passwd
echo $? # 4
# Timeout (exit 5, requires crafted input)
rmagic --timeout 1 <file-that-triggers-recursion>
echo $? # 5
Automation Scripts#
Check for specific error classes in shell scripts:
#!/bin/bash
rmagic "$FILE"
EXIT_CODE=$?
case $EXIT_CODE in
0)
echo "Success"
;;
3)
echo "File access error - check path and permissions"
exit 1
;;
4)
echo "Magic file configuration error - check --magic-file"
exit 1
;;
5)
echo "Timeout - consider increasing --timeout"
exit 1
;;
*)
echo "General error - see stderr"
exit 1
;;
esac
Strict Mode for CI Pipelines#
Use --strict in CI/CD to fail fast on any error:
# Process all files, exit non-zero if any fail
rmagic --strict *.bin
# Equivalent to:
if rmagic --strict *.bin; then
echo "All files processed successfully"
else
echo "At least one file failed (exit code $?)"
exit 1
fi
Relevant Code Files#
| File Path | Purpose | Key Components |
|---|---|---|
| src/main.rs | CLI entry point and error handling | handle_error, handle_io_error, handle_parse_error_new, handle_evaluation_error_new, handle_timeout_error, run_analysis, main |
| src/error.rs | Error type definitions | LibmagicError, ParseError, EvaluationError |
| Cargo.toml | Dependency declarations | thiserror dependency |
Related Topics#
- Magic File Format: The
ParseErrortaxonomy reflects supported and unsupported magic file syntax - Evaluation Configuration: Timeout errors (exit 5) are controlled by
EvaluationConfigsettings - CLI Argument Parsing: Invalid arguments trigger
InvalidInputI/O errors (exit 2) - Strict Mode: The
--strictflag affects error propagation in batch processing
Architectural Context#
The exit code system represents a durable architectural decision about error signaling for automation. Key design principles:
-
User-facing classification over internal granularity: File access errors, magic file errors, and timeouts get distinct codes because they require different remediation. Internal evaluation failures share exit code 1.
-
Unix convention adherence: Exit codes 0-2 follow BSD conventions, making the tool a well-behaved Unix citizen.
-
Graceful degradation by default: Standard mode processes all files and exits 0 even on errors, maximizing utility for exploratory use. Strict mode enables failure detection for monitoring.
-
Numeric literals over constants: Exit codes are returned as raw integers (0-5) rather than named constants, reducing indirection in error handlers.
-
Structured error types over string messages: thiserror-based error types enable compile-time exhaustiveness checking when adding new error variants.
These decisions balance automation-friendliness (predictable exit codes), user-friendliness (descriptive stderr messages), and developer-friendliness (type-safe error handling) without compromising any dimension.