1use similar::{ChangeTag, TextDiff};
7
8use console::Style;
9
10use crate::format::format_tree_with_options;
11use crate::match_tree::CompiledPolicy;
12
13pub fn tree_diff(before: &CompiledPolicy, after: &CompiledPolicy) -> Option<String> {
17 let before_lines = format_tree_with_options(before, false);
18 let after_lines = format_tree_with_options(after, false);
19
20 let before_text = before_lines.join("\n");
21 let after_text = after_lines.join("\n");
22
23 if before_text == after_text {
24 return None;
25 }
26
27 let diff = TextDiff::from_lines(&before_text, &after_text);
28 let mut output = String::new();
29
30 for change in diff.iter_all_changes() {
31 let line = change.value().trim_end_matches('\n');
32 match change.tag() {
33 ChangeTag::Delete => {
34 output.push_str(&Style::new().red().apply_to(format!("- {line}")).to_string());
35 output.push('\n');
36 }
37 ChangeTag::Insert => {
38 output.push_str(
39 &Style::new()
40 .green()
41 .apply_to(format!("+ {line}"))
42 .to_string(),
43 );
44 output.push('\n');
45 }
46 ChangeTag::Equal => {
47 output.push_str(&format!(" {line}"));
48 output.push('\n');
49 }
50 }
51 }
52
53 Some(output)
54}
55
56pub fn tree_diff_plain(before: &CompiledPolicy, after: &CompiledPolicy) -> Option<String> {
60 let before_lines = format_tree_with_options(before, false);
61 let after_lines = format_tree_with_options(after, false);
62
63 let before_text = before_lines.join("\n");
64 let after_text = after_lines.join("\n");
65
66 if before_text == after_text {
67 return None;
68 }
69
70 let diff = TextDiff::from_lines(&before_text, &after_text);
71 let mut output = String::new();
72
73 for change in diff.iter_all_changes() {
74 let line = change.value().trim_end_matches('\n');
75 match change.tag() {
76 ChangeTag::Delete => {
77 output.push_str(&format!("- {line}\n"));
78 }
79 ChangeTag::Insert => {
80 output.push_str(&format!("+ {line}\n"));
81 }
82 ChangeTag::Equal => {
83 output.push_str(&format!(" {line}\n"));
84 }
85 }
86 }
87
88 Some(output)
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::match_tree::*;
95 use std::collections::HashMap;
96
97 fn empty_policy() -> CompiledPolicy {
98 CompiledPolicy {
99 sandboxes: HashMap::new(),
100 tree: vec![],
101 default_effect: crate::Effect::Deny,
102 default_sandbox: None,
103 on_sandbox_violation: Default::default(),
104 harness_defaults: None,
105 }
106 }
107
108 fn policy_with_rule(bin: &str, decision: Decision) -> CompiledPolicy {
109 let node = Node::Condition {
110 observe: Observable::ToolName,
111 pattern: Pattern::Literal(Value::Literal("Bash".into())),
112 children: vec![Node::Condition {
113 observe: Observable::PositionalArg(0),
114 pattern: Pattern::Literal(Value::Literal(bin.into())),
115 children: vec![Node::Decision(decision)],
116 doc: None,
117 source: None,
118 terminal: false,
119 }],
120 doc: None,
121 source: None,
122 terminal: false,
123 };
124 CompiledPolicy {
125 tree: vec![node],
126 ..empty_policy()
127 }
128 }
129
130 #[test]
131 fn identical_policies_produce_no_diff() {
132 let p = policy_with_rule("git", Decision::Allow(None));
133 assert!(tree_diff_plain(&p, &p).is_none());
134 }
135
136 #[test]
137 fn added_rule_shows_in_diff() {
138 let before = empty_policy();
139 let after = policy_with_rule("git", Decision::Allow(None));
140 let diff = tree_diff_plain(&before, &after).unwrap();
141 assert!(diff.contains("+ "), "expected additions in diff:\n{diff}");
142 assert!(diff.contains("git"), "expected 'git' in diff:\n{diff}");
143 }
144
145 #[test]
146 fn removed_rule_shows_in_diff() {
147 let before = policy_with_rule("git", Decision::Allow(None));
148 let after = empty_policy();
149 let diff = tree_diff_plain(&before, &after).unwrap();
150 assert!(diff.contains("- "), "expected deletions in diff:\n{diff}");
151 }
152
153 #[test]
154 fn changed_decision_shows_in_diff() {
155 let before = policy_with_rule("git", Decision::Allow(None));
156 let after = policy_with_rule("git", Decision::Deny);
157 let diff = tree_diff_plain(&before, &after).unwrap();
158 assert!(diff.contains("- "), "expected deletions:\n{diff}");
159 assert!(diff.contains("+ "), "expected additions:\n{diff}");
160 }
161}