use std::{io::{Stdin, IsTerminal}, process::{exit, Command, Stdio}}; struct Config { ssh_destination: String, xochitl_path: String, ssh_key_path: Option, extensions: Extensions } #[derive(Default, Clone)] struct Extensions { unavailable_response: bool, info: bool } fn get_line(stdin: &mut Stdin) -> Option { let mut buf = String::new(); match stdin.read_line(&mut buf) { Ok(0) => None, Err(err) => { eprintln!("read encountered error: {}", err); None } _ => Some(buf) } } fn get_value(stdin: &mut Stdin) -> Option { if let Some(line) = get_line(stdin) { if let Some(value) = line.strip_prefix("VALUE ") { if value.split_ascii_whitespace().count() == 0 { return None } else { return Some(value.to_owned()); } } if line == "VALUE" { return None; } } eprintln!("did not receive value when expected"); exit(1); } fn get_config_value(stdin: &mut Stdin, key: &str) -> Option { println!("GETCONFIG {}", key); get_value(stdin) } fn get_config(stdin: &mut Stdin, extensions: &Extensions) -> Config { Config { ssh_destination: get_config_value(stdin, "ssh_destination").unwrap() .strip_suffix("\n").unwrap().to_string(), xochitl_path: get_config_value(stdin, "xochitl_directory") .map(|s| s.strip_suffix("\n").unwrap().to_string()) .unwrap_or_else(|| ".local/share/remarkable/xochitl".to_owned()), ssh_key_path: get_config_value(stdin, "ssh_key"), extensions: extensions.to_owned() } } fn startup(stdin: &mut Stdin) -> Config { let mut extensions = Extensions::default(); while let Some(line) = get_line(stdin) { let mut words = line.split_ascii_whitespace(); let verb = words.next().expect("missing command verb"); match verb { "EXTENSIONS" => { let supported: Vec<_> = words.collect(); extensions.unavailable_response = supported.contains(&"UNAVAILABLERESPONSE"); extensions.info = supported.contains(&"INFO"); println!( "EXTENSIONS {} {}", if extensions.unavailable_response { "UNAVAILABLERESPONSE" } else { "" }, if extensions.info { "INFO" } else { "" } ) } "PREPARE" => { let config = get_config(stdin, &extensions); println!("PREPARE-SUCCESS"); return config; }, "LISTCONFIGS" => { println!("CONFIG ssh_destination ssh destination under which the reMarkable tablet can be reached."); println!("CONFIG ssh_key the path to an ssh key to use (optional)."); println!("CONFIG xochitl_directory the directory on the remarkable device in which data is stored (default .local/share/remarkable/xochitl)."); println!("CONFIGEND") }, "INITREMOTE" => { let config = get_config(stdin, &extensions); let check = Command::new("ssh") .arg(config.ssh_destination) .arg("ls") .arg(config.xochitl_path) .stdout(Stdio::null()) .status(); if let Ok(status) = check { if status.success() { println!("INITREMOTE-SUCCESS"); } else { println!("INITREMOTE-FAILURE ssh check returned {}", status); } } else { println!("INITREMOTE-FAILURE failed to spawn ssh check command"); } } _ => { println!("UNSUPPORTED-REQUEST"); eprintln!("got unsupported verb while getting config: {}", line); } } } println!("DEBUG done early?"); exit(0); } fn key_to_uuid(key: &str) -> String { let maybe = Command::new("uuidgen") .arg("-s") .arg("-n") .arg("435bb27b-6704-416f-bc5b-3e2d02d0cf8f") .arg("-N") .arg(key) .output(); match maybe { Ok(uuid) => String::from_utf8_lossy(&uuid.stdout) .split_ascii_whitespace().next().unwrap().to_owned(), Err(e) => { println!("DEBUG conversion to uuid failed: {}", e); exit(1) } } } fn check_key_present<'a>(config: &Config, mut words: impl Iterator) { let key = words.next().unwrap(); assert_no_args_left(words); let uuid = key_to_uuid(key); let check = Command::new("ssh") .arg(&config.ssh_destination) .arg(format!("test -f {dir}/{uuid}.pdf && test ! -f {dir}/{uuid}.tombstone", dir=config.xochitl_path, uuid=uuid)) .status(); if let Ok(code) = check { if code.success() { println!("CHECKPRESENT-SUCCESS {}", key); } else { println!("CHECKPRESENT-FAILURE {}", key); // TODO: check if remote present or not } } else { println!("CHECKPRESENT-UNKNOWN {} ssh failed", key); } } fn store_key<'a>(config: &Config, mut words: impl Iterator) { let key = words.next().unwrap(); let file = words.next().unwrap(); assert_no_args_left(words); let uuid = key_to_uuid(key); let check = Command::new("scp") .arg(file) .arg(format!("{}:{}/{}.pdf", config.ssh_destination, config.xochitl_path, uuid)) .status(); if let Ok(code) = check { if code.success() { println!("TRANSFER-SUCCESS STORE {}", key); } else { println!("TRANSFER-FAILURE STORE {}", key); // TODO: check if remote present or not } } else { println!("TRANSFER-FAILURE STORE {} ssh failed", key); } } fn retrieve_key<'a>(config: &Config, mut words: impl Iterator) { let key = words.next().unwrap(); let file = words.next().unwrap(); assert_no_args_left(words); let uuid = key_to_uuid(key); let check = Command::new("scp") .arg(format!("{}:{}/{}.pdf", config.ssh_destination, config.xochitl_path, uuid)) .arg(file) .status(); if let Ok(code) = check { if code.success() { println!("TRANSFER-SUCCESS RETRIEVE {}", key); } else { println!("TRANSFER-FAILURE RETRIEVE {}", key); // TODO: check if remote present or not } } else { println!("TRANSFER-FAILURE RETRIEVE {} ssh failed", key); } } fn remove_key<'a>(config: &Config, mut words: impl Iterator) { let key = words.next().unwrap(); assert_no_args_left(words); let uuid = key_to_uuid(key); let cmd = Command::new("ssh") .arg(&config.ssh_destination) .arg(format!("rm -r {dir}/{uuid}*", dir=config.xochitl_path, uuid=uuid)) .status(); if let Ok(code) = cmd { if code.success() { println!("REMOVE-SUCCESS {}", key); } else { println!("REMOVE-FAILURE {} command failed", key); // TODO: check if remote present or not } } else { println!("REMOVE-FAILURE {} ssh failed", key); } } fn whereis<'a>(config: &Config, mut words: impl Iterator) { let key = words.next().unwrap(); assert_no_args_left(words); let uuid = key_to_uuid(key); println!("WHEREIS-SUCCESS {ssh}:{dir}/{uuid}.pdf", ssh=config.ssh_destination, dir=config.xochitl_path, uuid=uuid); } fn assert_no_args_left<'a>(mut words: impl Iterator) { if words.next().is_some() { println!("UNSUPPORTED-REQUEST"); println!("ERROR"); exit(1); } } fn main() { let mut stdin = std::io::stdin(); if stdin.is_terminal() { eprintln!("warning: this program is not meant to be invoked by hand."); } println!("VERSION 2"); let config = startup(&mut stdin); while let Some(line) = get_line(&mut stdin) { let mut words = line.split_ascii_whitespace(); let verb = words.next().expect("missing command verb"); match verb { "CHECKPRESENT" => check_key_present(&config, words), "TRANSFER" => match words.next().expect("missing STORE/RETRIEVE") { "STORE" => store_key(&config, words), "RETRIEVE" => retrieve_key(&config, words), _ => println!("UNSUPPORTED-REQUEST") } "REMOVE" => remove_key(&config, words), "WHEREIS" => whereis(&config, words), "GETAVAILABILITY" => { println!("ERROR"); unimplemented!() // check routing table if usb network device is present? } _ => println!("UNSUPPORTED-REQUEST") } } }