use std::collections::HashMap;
use std::path::PathBuf;
use serde::Deserialize;
use crate::facts::FactSpec;
use crate::level::Level;
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct Config {
pub version: u32,
#[serde(default)]
pub extends: Vec<ExtendsEntry>,
#[serde(default)]
pub ignore: Vec<String>,
#[serde(default = "default_respect_gitignore")]
pub respect_gitignore: bool,
#[serde(default)]
pub vars: HashMap<String, String>,
#[serde(default)]
pub facts: Vec<FactSpec>,
#[serde(default)]
pub rules: Vec<RuleSpec>,
#[serde(default = "default_fix_size_limit")]
pub fix_size_limit: Option<u64>,
#[serde(default)]
pub nested_configs: bool,
}
#[allow(clippy::unnecessary_wraps)]
fn default_fix_size_limit() -> Option<u64> {
Some(1 << 20)
}
fn default_respect_gitignore() -> bool {
true
}
impl Config {
pub const CURRENT_VERSION: u32 = 1;
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum ExtendsEntry {
Url(String),
Filtered {
url: String,
#[serde(default)]
only: Option<Vec<String>>,
#[serde(default)]
except: Option<Vec<String>>,
},
}
impl ExtendsEntry {
pub fn url(&self) -> &str {
match self {
Self::Url(s) | Self::Filtered { url: s, .. } => s,
}
}
pub fn only(&self) -> Option<&[String]> {
match self {
Self::Filtered { only: Some(v), .. } => Some(v),
_ => None,
}
}
pub fn except(&self) -> Option<&[String]> {
match self {
Self::Filtered {
except: Some(v), ..
} => Some(v),
_ => None,
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum PathsSpec {
Single(String),
Many(Vec<String>),
IncludeExclude {
#[serde(default, deserialize_with = "string_or_vec")]
include: Vec<String>,
#[serde(default, deserialize_with = "string_or_vec")]
exclude: Vec<String>,
},
}
fn string_or_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum OneOrMany {
One(String),
Many(Vec<String>),
}
match OneOrMany::deserialize(deserializer)? {
OneOrMany::One(s) => Ok(vec![s]),
OneOrMany::Many(v) => Ok(v),
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct RuleSpec {
pub id: String,
pub kind: String,
pub level: Level,
#[serde(default)]
pub paths: Option<PathsSpec>,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub policy_url: Option<String>,
#[serde(default)]
pub when: Option<String>,
#[serde(default)]
pub fix: Option<FixSpec>,
#[serde(default)]
pub git_tracked_only: bool,
#[serde(flatten)]
pub extra: serde_yaml_ng::Mapping,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum FixSpec {
FileCreate {
file_create: FileCreateFixSpec,
},
FileRemove {
file_remove: FileRemoveFixSpec,
},
FilePrepend {
file_prepend: FilePrependFixSpec,
},
FileAppend {
file_append: FileAppendFixSpec,
},
FileRename {
file_rename: FileRenameFixSpec,
},
FileTrimTrailingWhitespace {
file_trim_trailing_whitespace: FileTrimTrailingWhitespaceFixSpec,
},
FileAppendFinalNewline {
file_append_final_newline: FileAppendFinalNewlineFixSpec,
},
FileNormalizeLineEndings {
file_normalize_line_endings: FileNormalizeLineEndingsFixSpec,
},
FileStripBidi {
file_strip_bidi: FileStripBidiFixSpec,
},
FileStripZeroWidth {
file_strip_zero_width: FileStripZeroWidthFixSpec,
},
FileStripBom {
file_strip_bom: FileStripBomFixSpec,
},
FileCollapseBlankLines {
file_collapse_blank_lines: FileCollapseBlankLinesFixSpec,
},
}
impl FixSpec {
pub fn op_name(&self) -> &'static str {
match self {
Self::FileCreate { .. } => "file_create",
Self::FileRemove { .. } => "file_remove",
Self::FilePrepend { .. } => "file_prepend",
Self::FileAppend { .. } => "file_append",
Self::FileRename { .. } => "file_rename",
Self::FileTrimTrailingWhitespace { .. } => "file_trim_trailing_whitespace",
Self::FileAppendFinalNewline { .. } => "file_append_final_newline",
Self::FileNormalizeLineEndings { .. } => "file_normalize_line_endings",
Self::FileStripBidi { .. } => "file_strip_bidi",
Self::FileStripZeroWidth { .. } => "file_strip_zero_width",
Self::FileStripBom { .. } => "file_strip_bom",
Self::FileCollapseBlankLines { .. } => "file_collapse_blank_lines",
}
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FileCreateFixSpec {
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub content_from: Option<PathBuf>,
#[serde(default)]
pub path: Option<PathBuf>,
#[serde(default = "default_create_parents")]
pub create_parents: bool,
}
fn default_create_parents() -> bool {
true
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct FileRemoveFixSpec {}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FilePrependFixSpec {
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub content_from: Option<PathBuf>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FileAppendFixSpec {
#[serde(default)]
pub content: Option<String>,
#[serde(default)]
pub content_from: Option<PathBuf>,
}
pub fn resolve_content_source(
rule_id: &str,
op_name: &str,
inline: &Option<String>,
from: &Option<PathBuf>,
) -> crate::error::Result<ContentSourceSpec> {
match (inline, from) {
(Some(_), Some(_)) => Err(crate::error::Error::rule_config(
rule_id,
format!("fix.{op_name}: `content` and `content_from` are mutually exclusive"),
)),
(None, None) => Err(crate::error::Error::rule_config(
rule_id,
format!("fix.{op_name}: one of `content` or `content_from` is required"),
)),
(Some(s), None) => Ok(ContentSourceSpec::Inline(s.clone())),
(None, Some(p)) => Ok(ContentSourceSpec::File(p.clone())),
}
}
#[derive(Debug, Clone)]
pub enum ContentSourceSpec {
Inline(String),
File(PathBuf),
}
impl From<String> for ContentSourceSpec {
fn from(s: String) -> Self {
Self::Inline(s)
}
}
impl From<&str> for ContentSourceSpec {
fn from(s: &str) -> Self {
Self::Inline(s.to_string())
}
}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct FileRenameFixSpec {}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct FileTrimTrailingWhitespaceFixSpec {}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct FileAppendFinalNewlineFixSpec {}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct FileNormalizeLineEndingsFixSpec {}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct FileStripBidiFixSpec {}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct FileStripZeroWidthFixSpec {}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct FileStripBomFixSpec {}
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct FileCollapseBlankLinesFixSpec {}
impl RuleSpec {
pub fn deserialize_options<T>(&self) -> crate::error::Result<T>
where
T: serde::de::DeserializeOwned,
{
Ok(serde_yaml_ng::from_value(serde_yaml_ng::Value::Mapping(
self.extra.clone(),
))?)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct NestedRuleSpec {
pub kind: String,
#[serde(default)]
pub paths: Option<PathsSpec>,
#[serde(default)]
pub message: Option<String>,
#[serde(default)]
pub policy_url: Option<String>,
#[serde(default)]
pub when: Option<String>,
#[serde(flatten)]
pub extra: serde_yaml_ng::Mapping,
}
impl NestedRuleSpec {
pub fn instantiate(
&self,
parent_id: &str,
idx: usize,
level: Level,
tokens: &crate::template::PathTokens,
) -> RuleSpec {
RuleSpec {
id: format!("{parent_id}.require[{idx}]"),
kind: self.kind.clone(),
level,
paths: self
.paths
.as_ref()
.map(|p| crate::template::render_paths_spec(p, tokens)),
message: self
.message
.as_deref()
.map(|m| crate::template::render_path(m, tokens)),
policy_url: self.policy_url.clone(),
when: self.when.clone(),
fix: None,
git_tracked_only: false,
extra: crate::template::render_mapping(self.extra.clone(), tokens),
}
}
}