| name | rust-best-practices |
| description | Rust coding best practices for cc-audit development. Use when writing new Rust code, reviewing code, or refactoring. Covers error handling, safety, performance, and idiomatic patterns. |
Rust Best Practices for cc-audit
Error Handling
Prefer ? Over unwrap()
let content = fs::read_to_string(path).unwrap();
let content = fs::read_to_string(path)?;
let content = fs::read_to_string(path)
.map_err(|e| ScanError::IoError { path: path.into(), source: e })?;
Use thiserror for Custom Errors
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ScanError {
#[error("failed to read file: {path}")]
IoError {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("invalid pattern: {0}")]
InvalidPattern(String),
}
When unwrap() is Acceptable
- In tests
- After validation guarantees success
- With
unreachable!() comment explaining why
if value.is_some() {
let v = value.unwrap();
}
if let Some(v) = value {
}
Option and Result Patterns
Prefer Combinators
let result = match opt {
Some(v) => Some(v.to_uppercase()),
None => None,
};
let result = opt.map(|v| v.to_uppercase());
let result = opt
.filter(|s| !s.is_empty())
.map(|s| s.trim())
.unwrap_or_default();
ok_or vs ok_or_else
let value = opt.ok_or(MyError::NotFound)?;
let value = opt.ok_or_else(|| MyError::detailed(context))?;
Ownership and Borrowing
Prefer Borrowing Over Cloning
fn process(data: String) { }
process(my_string.clone());
fn process(data: &str) { }
process(&my_string);
Use Cow for Flexible Ownership
use std::borrow::Cow;
fn normalize_path(path: &str) -> Cow<'_, str> {
if path.contains('\\') {
Cow::Owned(path.replace('\\', "/"))
} else {
Cow::Borrowed(path)
}
}
String Handling
&str vs String
fn scan_content(content: &str) -> Result<Findings, Error>
fn get_message(&self) -> String
fn log_message(msg: impl AsRef<str>)
Efficient String Building
let s = "prefix".to_string() + &middle + "suffix";
let s = format!("prefix{}suffix", middle);
let mut s = String::with_capacity(estimated_len);
s.push_str("prefix");
s.push_str(&middle);
s.push_str("suffix");
Collections
Prefer Iterators Over Loops
let mut results = Vec::new();
for item in items {
if item.is_valid() {
results.push(item.transform());
}
}
let results: Vec<_> = items
.iter()
.filter(|item| item.is_valid())
.map(|item| item.transform())
.collect();
Pre-allocate When Size is Known
let mut vec = Vec::new();
for i in 0..1000 {
vec.push(i);
}
let mut vec = Vec::with_capacity(1000);
for i in 0..1000 {
vec.push(i);
}
let vec: Vec<_> = (0..1000).collect();
Structs and Enums
Use Builder Pattern for Complex Structs
pub struct ScanConfig {
path: PathBuf,
recursive: bool,
max_depth: Option<usize>,
}
impl ScanConfig {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
recursive: true,
max_depth: None,
}
}
pub fn recursive(mut self, value: bool) -> Self {
self.recursive = value;
self
}
pub fn max_depth(mut self, depth: usize) -> Self {
self.max_depth = Some(depth);
self
}
}
let config = ScanConfig::new("./src")
.recursive(true)
.max_depth(5);
Derive Common Traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RuleId(String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Low,
Medium,
High,
Critical,
}
Traits
Use Trait Objects Sparingly
fn scan<S: Scanner>(scanner: &S, content: &str) -> Result<Findings>
fn scan_all(scanners: &[Box<dyn Scanner>], content: &str) -> Result<Findings>
Implement Standard Traits
impl std::fmt::Display for Finding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}: {}", self.severity, self.rule_id, self.message)
}
}
impl Default for ScanConfig {
fn default() -> Self {
Self {
recursive: true,
max_depth: None,
}
}
}
Performance
Avoid Unnecessary Allocations
fn get_prefix() -> String {
"PREFIX_".to_string()
}
fn get_prefix() -> &'static str {
"PREFIX_"
}
Use once_cell for Lazy Static
use once_cell::sync::Lazy;
use regex::Regex;
static PATTERN: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"malicious_pattern").expect("valid regex")
});
Security (Critical for cc-audit)
Validate All External Input
pub fn scan_path(path: &Path) -> Result<(), ScanError> {
if !path.exists() {
return Err(ScanError::PathNotFound(path.into()));
}
let canonical = path.canonicalize()?;
if !canonical.starts_with(&allowed_root) {
return Err(ScanError::PathTraversal(path.into()));
}
Ok(())
}
Avoid Unsafe Unless Absolutely Necessary
unsafe {
}
unsafe fn read_raw(ptr: *const u8) -> u8 {
*ptr
}
Documentation
Doc Comments for Public Items
pub fn scan(&self, content: &str, path: &Path) -> Result<ScanResult, ScanError>
Quick Reference
| Pattern | Bad | Good |
|---|
| Error handling | .unwrap() | ? with context |
| String params | String | &str or impl AsRef<str> |
| Collections | Manual loops | Iterator chains |
| Optionals | Nested if let | Combinators (map, and_then) |
| Allocation | Repeated small allocs | Pre-allocate or Cow |
| Cloning | Clone everything | Borrow when possible |