use serde::{Deserialize, Serialize}; use serde_dhall::StaticType; use serde_json as json; use std::fs; use std::io::Write; use std::path::PathBuf; use structopt::StructOpt; use secrecy::ExposeSecret; use age::x25519::Recipient; #[derive(Deserialize, Serialize, StaticType, Debug)] #[serde(rename_all="lowercase")] enum Lang { de, en } #[derive(Deserialize, Serialize, StaticType, Debug)] struct Survey { title: String, description: String, questions: Vec, pubkey: Option, lang: Lang } #[derive(Deserialize, Serialize, StaticType, Debug)] struct Question { question: String, name: String, space: AnswerSpace, } #[derive(Deserialize, Serialize, StaticType, Debug, PartialEq)] enum AnswerSpace { Single(Vec), Multiple(Vec), YesOrNo, Freeform(String), Date, } #[derive(StructOpt, Debug)] struct Options { /// a dhall configuration file that describes a survey #[structopt(long, short, parse(from_os_str))] config_file: PathBuf, /// encrypt the survey with a passphrase (will be printed to stderr) #[structopt(long, short)] encrypt: bool, /// file to write the configuration to (will otherwise print to stdout) #[structopt(long, short)] out_file: Option } fn main () { let opt = Options::from_args(); let config_file = std::fs::read_to_string(opt.config_file).unwrap(); // hacky way to get a "prelude" in dhall which doesn't have to be // imported: just wrap our input code into a dhall "let"-statement. // Probably doesn't scale very vell, though ... let code = format!( "let Question = {} \nlet Answers = {} \nlet Lang = {}\nin {}", Question::static_type(), AnswerSpace::static_type(), Lang::static_type(), config_file ); match serde_dhall::from_str(&code) .static_type_annotation() .parse::() { Err(e) => { eprintln!("There is an error in your dhall code!\n{}", e); std::process::exit(1); }, Ok(data) => { if data.questions.iter().any(|q| q.space == AnswerSpace::Date) { eprintln!(">> Warning: html input type date is not supported by Safari and Internet Explorer.\n See https://caniuse.com/input-datetime for details."); } let json = json::to_string(&data).unwrap(); // if a public key is given to encrypt the survey, ad-hoc typecheck it // (not sure if the dhall crate allows defining custom types which are // opaque to dhall ...) match data.pubkey { Some (key) => key.parse::().is_err(), None => false }.then(|| { println!("field pubkey is not a valid public key, aborting ..."); std::process::exit(1); }); // out here to avoid borrowing issues — if it were in the password // branch below, it would go out of scope at its end, since .as_slice() // just borrows its argument let mut encrypted = vec![]; // are we restricting access to the survey? if so, encrypt it with // the password as passphrase. let outdata = match opt.encrypt { false => json.as_bytes(), true => { let key = age::x25519::Identity::generate(); let pubkey = key.to_public(); let encryptor = age::Encryptor::with_recipients(vec![Box::new(pubkey)]); let mut writer = encryptor.wrap_output(&mut encrypted).unwrap(); writer.write_all(&json.as_bytes()).unwrap(); writer.finish().unwrap(); eprintln!("Passphrase for this survey: {}", key.to_string().expose_secret()); encrypted.as_slice() } }; match opt.out_file { Some(file) => fs::write(file.clone(), outdata) .expect(&format!("cannot write to file {:?}!", file)), None => { let mut out = std::io::stdout(); out.write_all(outdata).unwrap(); out.flush().unwrap() } }; } } }