mod git;
mod repository;
use std::io::Read;
use std::process::ExitCode;
fn manifest() -> String {
format!(
r#"{{"protocol_version":1,"name":"github","version":"{}","description":"Keeps a vault in a git repository","operations":["push","pull"]}}"#,
env!("CARGO_PKG_VERSION")
)
}
fn main() -> ExitCode {
let argument = std::env::args().nth(1);
match argument.as_deref() {
Some("--manifest") => {
println!("{}", manifest());
ExitCode::SUCCESS
}
Some("run") => run(),
Some("--version") => {
println!("{}", env!("CARGO_PKG_VERSION"));
ExitCode::SUCCESS
}
_ => {
eprintln!(
"sefy-plugin-github is a transport for sefy, not a command to run by hand.\n\
\n\
Install it where sefy looks for plugins and use `sefy push`, `sefy pull`\n\
or `sefy sync`. Set SEFY_GITHUB_REPO to the repository to keep the vault in."
);
ExitCode::FAILURE
}
}
}
fn run() -> ExitCode {
let mut request = String::new();
if let Err(error) = std::io::stdin().read_to_string(&mut request) {
return fail(&format!("could not read the request: {error}"));
}
match repository::handle(&request) {
Ok(message) => {
println!("{}", report(&message));
ExitCode::SUCCESS
}
Err(error) => fail(&error.to_string()),
}
}
fn fail(reason: &str) -> ExitCode {
eprintln!("{reason}");
ExitCode::FAILURE
}
fn report(message: &str) -> String {
format!("{{\"message\":\"{}\"}}", escape(message))
}
fn escape(text: &str) -> String {
let mut escaped = String::with_capacity(text.len());
for character in text.chars() {
match character {
'"' => escaped.push_str("\\\""),
'\\' => escaped.push_str("\\\\"),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
control if control.is_control() => {
escaped.push_str(&format!("\\u{:04x}", control as u32));
}
other => escaped.push(other),
}
}
escaped
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_manifest_is_the_json_the_protocol_asks_for() {
let parsed: serde_json::Value = serde_json::from_str(&manifest()).unwrap();
assert_eq!(parsed["protocol_version"], 1);
assert_eq!(parsed["name"], "github");
assert_eq!(parsed["operations"][0], "push");
assert_eq!(parsed["operations"][1], "pull");
}
#[test]
fn the_manifest_declares_a_version_at_all() {
let parsed: serde_json::Value = serde_json::from_str(&manifest()).unwrap();
let version = parsed["version"].as_str().expect("a version must be there");
assert!(!version.is_empty());
assert!(version.contains('.'), "got: {version}");
}
#[test]
fn a_report_survives_a_message_with_quotes_in_it() {
let line = report(r#"pushed "vault" to origin"#);
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
assert_eq!(parsed["message"], r#"pushed "vault" to origin"#);
}
#[test]
fn a_report_survives_a_message_with_a_windows_path_in_it() {
let line = report(r"cloned into C:\Users\someone\AppData");
let parsed: serde_json::Value = serde_json::from_str(&line).unwrap();
assert_eq!(parsed["message"], r"cloned into C:\Users\someone\AppData");
}
}