use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileChange {
pub path: PathBuf,
pub before: Option<String>,
pub after: Option<String>,
pub mode: Option<u32>,
pub change_type: FileChangeType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileChangeType {
Created,
Modified,
Deleted,
ModeChanged,
}
impl FileChange {
pub fn created(path: impl AsRef<Path>, content: &str, mode: Option<u32>) -> Self {
Self {
path: path.as_ref().to_path_buf(),
before: None,
after: Some(content.to_string()),
mode,
change_type: FileChangeType::Created,
}
}
pub fn modified(path: impl AsRef<Path>, before: &str, after: &str) -> Self {
Self {
path: path.as_ref().to_path_buf(),
before: Some(before.to_string()),
after: Some(after.to_string()),
mode: None,
change_type: FileChangeType::Modified,
}
}
pub fn deleted(path: impl AsRef<Path>, content: &str) -> Self {
Self {
path: path.as_ref().to_path_buf(),
before: Some(content.to_string()),
after: None,
mode: None,
change_type: FileChangeType::Deleted,
}
}
pub fn to_diff(&self) -> String {
let path_str = self.path.display().to_string();
let mut diff = String::new();
match self.change_type {
FileChangeType::Created => {
diff.push_str("--- /dev/null\n");
diff.push_str(&format!("+++ b{}\n", path_str));
if let Some(ref content) = self.after {
let lines: Vec<&str> = content.lines().collect();
diff.push_str(&format!("@@ -0,0 +1,{} @@\n", lines.len()));
for line in lines {
diff.push_str(&format!("+{}\n", line));
}
}
}
FileChangeType::Modified => {
diff.push_str(&format!("--- a{}\n", path_str));
diff.push_str(&format!("+++ b{}\n", path_str));
diff.push_str(&self.compute_unified_diff());
}
FileChangeType::Deleted => {
diff.push_str(&format!("--- a{}\n", path_str));
diff.push_str("+++ /dev/null\n");
if let Some(ref content) = self.before {
let lines: Vec<&str> = content.lines().collect();
diff.push_str(&format!("@@ -1,{} +0,0 @@\n", lines.len()));
for line in lines {
diff.push_str(&format!("-{}\n", line));
}
}
}
FileChangeType::ModeChanged => {
diff.push_str(&format!("--- a{}\n", path_str));
diff.push_str(&format!("+++ b{}\n", path_str));
if let Some(mode) = self.mode {
diff.push_str(&format!("# chmod {:o}\n", mode));
}
}
}
diff
}
fn compute_unified_diff(&self) -> String {
let before = self.before.as_deref().unwrap_or("");
let after = self.after.as_deref().unwrap_or("");
let before_lines: Vec<&str> = before.lines().collect();
let after_lines: Vec<&str> = after.lines().collect();
let mut diff = String::new();
let common_prefix = before_lines
.iter()
.zip(after_lines.iter())
.take_while(|(a, b)| a == b)
.count();
let common_suffix = before_lines
.iter()
.rev()
.zip(after_lines.iter().rev())
.take_while(|(a, b)| a == b)
.count();
let before_end = before_lines.len().saturating_sub(common_suffix);
let after_end = after_lines.len().saturating_sub(common_suffix);
let before_changed = before_lines.get(common_prefix..before_end).unwrap_or(&[]);
let after_changed = after_lines.get(common_prefix..after_end).unwrap_or(&[]);
if before_changed.is_empty() && after_changed.is_empty() {
return diff;
}
diff.push_str(&format!(
"@@ -{},{} +{},{} @@\n",
common_prefix + 1,
before_changed.len(),
common_prefix + 1,
after_changed.len()
));
for line in before_changed {
diff.push_str(&format!("-{}\n", line));
}
for line in after_changed {
diff.push_str(&format!("+{}\n", line));
}
diff
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PackageOperation {
Install {
name: String,
version: Option<String>,
},
Remove { name: String },
Upgrade {
name: String,
from_version: Option<String>,
to_version: Option<String>,
},
}
impl PackageOperation {
pub fn install(name: &str, version: Option<&str>) -> Self {
Self::Install {
name: name.to_string(),
version: version.map(String::from),
}
}
pub fn remove(name: &str) -> Self {
Self::Remove {
name: name.to_string(),
}
}
pub fn to_diff_line(&self) -> String {
match self {
Self::Install { name, version } => {
if let Some(v) = version {
format!("+ {} ({})", name, v)
} else {
format!("+ {}", name)
}
}
Self::Remove { name } => format!("- {}", name),
Self::Upgrade {
name,
from_version,
to_version,
} => {
let from = from_version.as_deref().unwrap_or("?");
let to = to_version.as_deref().unwrap_or("?");
format!("~ {} ({} -> {})", name, from, to)
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceOperation {
Enable { name: String },
Disable { name: String },
Start { name: String },
Stop { name: String },
Restart { name: String },
}
impl ServiceOperation {
pub fn to_diff_line(&self) -> String {
match self {
Self::Enable { name } => format!("+ systemctl enable {}", name),
Self::Disable { name } => format!("- systemctl enable {}", name),
Self::Start { name } => format!("+ systemctl start {}", name),
Self::Stop { name } => format!("- systemctl start {}", name),
Self::Restart { name } => format!("~ systemctl restart {}", name),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UserGroupOperation {
AddToGroup { user: String, group: String },
RemoveFromGroup { user: String, group: String },
CreateUser { name: String, groups: Vec<String> },
CreateGroup { name: String },
}
impl UserGroupOperation {
pub fn to_diff_line(&self) -> String {
match self {
Self::AddToGroup { user, group } => format!("+ usermod -aG {} {}", group, user),
Self::RemoveFromGroup { user, group } => format!("- gpasswd -d {} {}", user, group),
Self::CreateUser { name, groups } => {
if groups.is_empty() {
format!("+ useradd {}", name)
} else {
format!("+ useradd -G {} {}", groups.join(","), name)
}
}
Self::CreateGroup { name } => format!("+ groupadd {}", name),
}
}
}
#[derive(Debug, Default)]
pub struct DryRunContext {
fs_changes: HashMap<PathBuf, FileChange>,
package_ops: Vec<PackageOperation>,
service_ops: Vec<ServiceOperation>,
user_ops: Vec<UserGroupOperation>,
_env_changes: HashMap<String, Option<String>>,
simulation_log: Vec<SimulationEntry>,
}
#[derive(Debug, Clone)]
pub struct SimulationEntry {
pub step_id: String,
pub step_name: String,
pub description: String,
pub would_succeed: bool,
pub failure_reason: Option<String>,
}
include!("dry_run_dryruncontext.rs");