Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub fn app() -> App<'static, 'static> {
.short("n")
.help("Do not use color to highlight the diff"),
)
.arg(Arg::with_name("json").long("json").help("Return in JSON format"))
.arg(
// Legacy: This is exported now, and in fact this setting is ignored
// completely if $__shadowenv_data is present in the environment.
Expand Down
239 changes: 178 additions & 61 deletions src/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,120 @@ use crate::undo;

use std::collections::BTreeMap;
use std::env;
use std::io;
use std::io::Write;

trait Logger {
enum ChangeType {
Add,
Remove,
}

trait Logger<'out> {
fn with_output<W: Write>(o: &'out mut W, color: bool, json: bool) -> Self
where
Self: Sized;
fn print(&mut self, value: String);
fn pre_decoration(&self, change: Option<&ChangeType>) -> &str {
match change {
None => " ",
Some(ChangeType::Add) => "+ ",
Some(ChangeType::Remove) => "- ",
}
}
fn post_decoration(&self, _change: Option<&ChangeType>) -> &str {
""
}
fn serialize(&self, name: &str, value: &str, change: Option<&ChangeType>) -> String;
fn format_name_value(&self, name: &str, value: &str, change: Option<&ChangeType>) -> String {
format!(
"{pre}{serialized}{post}",
pre = self.pre_decoration(change),
post = self.post_decoration(change),
serialized = self.serialize(name, value, change)
)
}
}

struct OutputLogger<'out> {
output: &'out mut dyn Write,
color: bool,
json: bool,
}
struct StdoutLogger;

impl Logger for StdoutLogger {
impl<'out> Logger<'out> for OutputLogger<'out> {
fn with_output<W: Write>(o: &'out mut W, color: bool, json: bool) -> Self
where
Self: Sized,
{
Self {
output: o,
color,
json,
}
}
fn print(&mut self, value: String) {
println!("{}", value);
write!(self.output, "{}", value).expect("Could not print to output!");
}

fn pre_decoration(&self, change: Option<&ChangeType>) -> &str {
match (self.json, self.color, change) {
(true, _, _) => "",
(_, _, None) => " ",
(_, true, Some(ChangeType::Add)) => "\x1b[92m+ ",
(_, true, Some(ChangeType::Remove)) => "\x1b[91m- ",
(_, false, Some(ChangeType::Add)) => "+ ",
(_, false, Some(ChangeType::Remove)) => "- ",
}
}

fn post_decoration(&self, _change: Option<&ChangeType>) -> &str {
if self.color {
// Clearing to EOL with \x1b[K prevents a weird issue where a wrapped line uses the last
// non-null background color for the newline character, filling the rest of the space in the
// line.
"\x1b[0m\x1b[K"
} else {
""
}
}

fn serialize(&self, name: &str, value: &str, change: Option<&ChangeType>) -> String {
if self.json {
format!(
"{{\"type\":\"{}\",\"name\":\"{}\",\"value\":{}}}",
match change {
None => "verbose",
Some(ChangeType::Add) => "add",
Some(ChangeType::Remove) => "remove",
},
name,
{
let vals: Vec<&str> = value.split(":").collect();
if vals.len() == 1 {
format!("\"{}\"", value)
} else {
format!(
"[{}]",
vals.iter()
.map(|v| format!("\"{}\"", v))
.collect::<Vec<String>>()
.join(",")
)
}
},
)
} else {
format!("{}={}", name, value)
}
}
}

/// print a diff of the env
pub fn run(verbose: bool, color: bool, shadowenv_data: String) -> i32 {
pub fn run(verbose: bool, color: bool, json: bool, shadowenv_data: String) -> i32 {
run_with_logger(
&mut StdoutLogger {},
&mut OutputLogger::with_output(&mut io::stdout().lock(), color, json),
env::vars().collect(),
verbose,
color,
shadowenv_data,
)
}
Expand All @@ -29,7 +124,6 @@ fn run_with_logger(
logger: &mut dyn Logger,
env_vars: Vec<(String, String)>,
verbose: bool,
color: bool,
shadowenv_data: String,
) -> i32 {
let mut parts = shadowenv_data.splitn(2, ':');
Expand All @@ -49,32 +143,24 @@ fn run_with_logger(

for (name, value) in env_vars {
if let Some(scalar) = scalars.remove(&name) {
diff_scalar(logger, &scalar, color)
diff_scalar(logger, &scalar)
} else if let Some(list) = lists.remove(&name) {
diff_list(logger, &list, &value, color)
diff_list(logger, &list, &value)
} else if verbose {
print_verbose(logger, &name, &value)
}
}
scalars
.iter()
.for_each(|(_name, scalar)| diff_scalar(logger, scalar, color));
.for_each(|(_name, scalar)| diff_scalar(logger, scalar));
lists
.iter()
.for_each(|(_name, list)| diff_list(logger, list, "", color));
.for_each(|(_name, list)| diff_list(logger, list, ""));
0
}

fn diff_list(logger: &mut dyn Logger, list: &undo::List, current: &str, color: bool) {
let formatted_deletions: Vec<String> = if color {
list.deletions
.iter()
.map(|x| "\x1b[48;5;52m".to_string() + x + "\x1b[0;91m")
.collect()
} else {
list.deletions.clone()
};
let mut prefix = formatted_deletions.join(":");
fn diff_list(logger: &mut dyn Logger, list: &undo::List, current: &str) {
let mut prefix = list.deletions.clone().join(":");

let items = current
.split(':')
Expand All @@ -84,69 +170,102 @@ fn diff_list(logger: &mut dyn Logger, list: &undo::List, current: &str, color: b
if !suffix.is_empty() && !prefix.is_empty() {
prefix += ":";
}
diff_remove(logger, &list.name, &(prefix + &suffix), color);

diff_remove(logger, &list.name, &(prefix + &suffix));
// TODO: fix
let items = current.split(':').map(|x| {
if list.additions.contains(&x.to_string()) && color {
if list.additions.contains(&x.to_string()) && false {
"\x1b[48;5;22m".to_string() + x + "\x1b[0;92m"
} else {
x.to_string()
}
});
let items: Vec<String> = items.collect();
let newline = items.join(":");
diff_add(logger, &list.name, &newline, color);
diff_add(logger, &list.name, &newline);
}

fn diff_scalar(logger: &mut dyn Logger, scalar: &undo::Scalar, color: bool) {
fn diff_scalar(logger: &mut dyn Logger, scalar: &undo::Scalar) {
if let Some(value) = &scalar.original {
diff_remove(logger, &scalar.name, value, color);
diff_remove(logger, &scalar.name, value);
}
if let Some(value) = &scalar.current {
diff_add(logger, &scalar.name, value, color);
diff_add(logger, &scalar.name, value);
}
}

fn diff_add(logger: &mut dyn Logger, name: &str, value: &str, color: bool) {
if color {
// Clearing to EOL with \x1b[K prevents a weird issue where a wrapped line uses the last
// non-null background color for the newline character, filling the rest of the space in the
// line.
logger.print(format!("\x1b[92m+ {}={}\x1b[0m\x1b[K", name, value));
} else {
logger.print(format!("+ {}={}", name, value));
}
fn diff_add(logger: &mut dyn Logger, name: &str, value: &str) {
logger.print(logger.format_name_value(name, value, Some(&ChangeType::Add)))
}

fn diff_remove(logger: &mut dyn Logger, name: &str, value: &str, color: bool) {
if color {
// Clearing to EOL with \x1b[K prevents a weird issue where a wrapped line uses the last
// non-null background colour for the newline character, filling the rest of the space in the
// line.
logger.print(format!("\x1b[91m- {}={}\x1b[0m\x1b[K", name, value));
} else {
logger.print(format!("- {}={}", name, value));
}
fn diff_remove(logger: &mut dyn Logger, name: &str, value: &str) {
logger.print(logger.format_name_value(name, value, Some(&ChangeType::Remove)))
}

fn print_verbose(logger: &mut dyn Logger, name: &str, value: &str) {
logger.print(format!(" {}={}", name, value))
logger.print(logger.format_name_value(name, value, None))
}

#[cfg(test)]
mod tests {
use super::*;
#[derive(Default)]
struct DummyLogger(Vec<String>);
impl Logger for DummyLogger {
fn print(&mut self, value: String) {
self.0.push(value);
}

#[test]
fn color_test() {
let mut out: Vec<u8> = vec![];
let mut logger = OutputLogger::with_output(&mut out, true, false);
let data = r#"62b0b9f86cda84d4:{"scalars":[],"lists":[{"name":"VAR_C","additions":["/added"],"deletions":["/removed"]},{"name":"VAR_B","additions":["/added"],"deletions":[]},{"name":"VAR_A","additions":["/added"],"deletions":[]}]}"#;
let env_vars = vec![
("VAR_A".to_string(), "/added:/existent".to_string()),
("VAR_B".to_string(), "/added".to_string()),
("VAR_C".to_string(), "/added:/existent".to_string()),
];

let result = run_with_logger(&mut logger, env_vars, false, data.to_string());

let expected: String = vec![
"\x1b[91m- VAR_A=/existent\x1b[0m\x1b[K",
"\x1b[92m+ VAR_A=/added:/existent\x1b[0m\x1b[K",
"\x1b[91m- VAR_B=\x1b[0m\x1b[K",
"\x1b[92m+ VAR_B=/added\x1b[0m\x1b[K",
"\x1b[91m- VAR_C=/removed:/existent\x1b[0m\x1b[K",
"\x1b[92m+ VAR_C=/added:/existent\x1b[0m\x1b[K",
]
.join("");
assert_eq!(result, 0);
assert_eq!(String::from_utf8(out).unwrap(), expected);
}

#[test]
fn json_test() {
let mut out: Vec<u8> = vec![];
let mut logger = OutputLogger::with_output(&mut out, false, true);
let data = r#"62b0b9f86cda84d4:{"scalars":[],"lists":[{"name":"VAR_C","additions":["/added"],"deletions":["/removed"]},{"name":"VAR_B","additions":["/added"],"deletions":[]},{"name":"VAR_A","additions":["/added"],"deletions":[]}]}"#;
let env_vars = vec![
("VAR_A".to_string(), "/added:/existent".to_string()),
("VAR_B".to_string(), "/added".to_string()),
("VAR_C".to_string(), "/added:/existent".to_string()),
];

let result = run_with_logger(&mut logger, env_vars, false, data.to_string());

let expected: String = vec![
// Did you try?
"[",
"{\"type\":\"add\",\"name\":\"VAR_A\",\"value\":[\"/added\",\"/existent\"]},",
"{\"type\":\"add\",\"name\":\"VAR_B\",\"value\":\"/added\"},",
"{\"type\":\"remove\",\"name\":\"VAR_C\",\"value\":[\"/removed\", \"/existent\"]},",
"{\"type\":\"add\",\"name\":\"VAR_C\",\"value\":[\"/existent\"]},",
"]",
]
.join("");
assert_eq!(result, 0);
assert_eq!(String::from_utf8(out).unwrap(), expected);
}

#[test]
fn nominal_test() {
let mut logger = DummyLogger::default();
let mut out: Vec<u8> = vec![];
let mut logger = OutputLogger::with_output(&mut out, false, false);

let env_vars = vec![
("VAR_A".to_string(), "/added:/existent".to_string()),
Expand All @@ -155,20 +274,18 @@ mod tests {
];

let data = r#"62b0b9f86cda84d4:{"scalars":[],"lists":[{"name":"VAR_C","additions":["/added"],"deletions":["/removed"]},{"name":"VAR_B","additions":["/added"],"deletions":[]},{"name":"VAR_A","additions":["/added"],"deletions":[]}]}"#;
let result = run_with_logger(&mut logger, env_vars, false, false, data.to_string());
let result = run_with_logger(&mut logger, env_vars, false, data.to_string());

let expected: Vec<_> = vec![
let expected: String = vec![
"- VAR_A=/existent",
"+ VAR_A=/added:/existent",
"- VAR_B=",
"+ VAR_B=/added",
"- VAR_C=/removed:/existent",
"+ VAR_C=/added:/existent",
]
.iter()
.map(ToString::to_string)
.collect();
.join("");
assert_eq!(result, 0);
assert_eq!(logger.0, expected);
assert_eq!(String::from_utf8(out).unwrap(), expected);
}
}
3 changes: 2 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ fn main() {
("diff", Some(matches)) => {
let verbose = matches.is_present("verbose");
let color = !matches.is_present("no-color");
let json = matches.is_present("json");
let legacy_fallback_data = matches.value_of("$__shadowenv_data").map(|d| d.to_string());
let data = Shadowenv::load_shadowenv_data_or_legacy_fallback(legacy_fallback_data);
process::exit(diff::run(verbose, color, data));
process::exit(diff::run(verbose, color, json, data));
}
("trust", Some(_)) => {
if let Err(err) = trust::run() {
Expand Down