├── .gitignore ├── samples ├── data.pass ├── screenshot.png └── data.txt ├── src ├── util │ ├── mod.rs │ ├── xdg_path.rs │ ├── clip.rs │ ├── crypt │ │ ├── key.rs │ │ ├── header.rs │ │ └── mod.rs │ ├── proc.rs │ ├── user_io │ │ ├── style.rs │ │ └── mod.rs │ ├── secret │ │ ├── mod.rs │ │ └── erase.rs │ ├── record │ │ ├── ir.rs │ │ └── mod.rs │ └── file.rs ├── backup.rs ├── input_pw.rs ├── serial.rs ├── output.rs ├── error.rs ├── find.rs ├── tui │ ├── cmd.rs │ └── mod.rs ├── main.rs └── env.rs ├── Makefile ├── config.rs ├── Cargo.toml ├── README.md ├── Cargo.lock └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /samples/data.pass: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swarthe/pass-rs/HEAD/samples/data.pass -------------------------------------------------------------------------------- /samples/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Swarthe/pass-rs/HEAD/samples/screenshot.png -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod clip; 2 | pub mod crypt; 3 | pub mod file; 4 | pub mod proc; 5 | pub mod record; 6 | pub mod secret; 7 | pub mod user_io; 8 | pub mod xdg_path; 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RC := cargo build --release 2 | MKDIR := mkdir -p 3 | RM := rm -f 4 | INSTALLBIN := install -C 5 | #INSTALLMAN := install -m 644 6 | 7 | BINPREFIX := target/release 8 | BIN := pass 9 | DESTPREFIX := /usr/local 10 | DEST := bin 11 | #MANDIR := share/man/man1 12 | 13 | build: 14 | $(RC) 15 | 16 | install: $(BINPREFIX)/$(BIN) 17 | $(MKDIR) $(DESTPREFIX)/$(DEST) 18 | $(INSTALLBIN) $(BINPREFIX)/$(BIN) $(DESTPREFIX)/$(DEST)/$(BIN) 19 | 20 | uninstall: 21 | $(RM) $(DESTPREFIX)/$(DEST)/$(BIN) 22 | 23 | .PHONY: build install uninstall 24 | -------------------------------------------------------------------------------- /config.rs: -------------------------------------------------------------------------------- 1 | //! Compile time options. 2 | 3 | /// Default time in seconds to keep an item in the clipboard. Should be a low 4 | /// value for security. 5 | pub const DEFAULT_CLIP_TIME: u64 = 10; 6 | 7 | /// The default item to view in a group (usually the password). Always an 8 | /// exact match. 9 | pub const DEFAULT_ITEM: &str = "password"; 10 | 11 | /// Name of the default pass file containing encrypted data. Must be a valid 12 | /// file name. 13 | pub const DEFAULT_PASS_FILE_NAME: &str = "data.pass"; 14 | 15 | // TODO: necessary ? 16 | /// The maximum number of times to prompt for the password if entered 17 | /// incorrectly. 18 | pub const PASSWORD_ATTEMPTS: u32 = 3; 19 | -------------------------------------------------------------------------------- /src/util/xdg_path.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use xdg::BaseDirectoriesError as XdgError; 4 | 5 | pub type Result = std::result::Result; 6 | 7 | pub type Error = XdgError; 8 | 9 | // TODO: probably use crate `directories` instead, and rename stuff (modules and 10 | // help text) 11 | // perhaps then delete this module 12 | 13 | /* XXX 14 | impl Display for Error { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | use Error::*; 17 | 18 | match self { 19 | ResolvingDataDir(e) => 20 | write!(f, "cannot resolve XDG data directory: {e}") 21 | } 22 | } 23 | } 24 | */ 25 | 26 | /// Returns a path to the `name`-specific XDG data directory. 27 | pub fn data_dir(name: &str) -> Result { 28 | use xdg::BaseDirectories; 29 | 30 | Ok(BaseDirectories::with_prefix(name)? 31 | .get_data_home()) 32 | } 33 | -------------------------------------------------------------------------------- /samples/data.txt: -------------------------------------------------------------------------------- 1 | // Password is "4321". 2 | 3 | Group( 4 | name: "Accounts", 5 | members: [ 6 | Group( 7 | name: "Bar", 8 | members: [ 9 | Item( 10 | name: "note", 11 | value: "I have something to say.\nNevermind ¯\\_(ツ)_/¯", 12 | metadata: {}, 13 | ), 14 | Item( 15 | name: "password", 16 | value: "Doveryay, no proveryay", 17 | metadata: {}, 18 | ), 19 | ], 20 | metadata: {}, 21 | ), 22 | Group( 23 | name: "Foo", 24 | members: [ 25 | Item( 26 | name: "password", 27 | value: "\\o/", 28 | metadata: {}, 29 | ), 30 | Item( 31 | name: "username", 32 | value: "Louise Michel", 33 | metadata: {}, 34 | ), 35 | ], 36 | metadata: {}, 37 | ), 38 | ], 39 | metadata: {}, 40 | ) 41 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passers" 3 | description = "A secure encrypted data manager" 4 | version = "0.1.0" 5 | license = "GPL-3.0-or-later" 6 | keywords = ["encryption", "password-manager", "command-line", "rust"] 7 | categories = ["command-line-utilities", "cryptography", "data-structures"] 8 | authors = ["Emil Overbeck "] 9 | readme = "README.md" 10 | repository = "https://github.com/Swarthe/pass-rs" 11 | documentation = "https://github.com/Swarthe/pass-rs" 12 | homepage = "https://github.com/Swarthe/pass-rs" 13 | edition = "2021" 14 | 15 | [[bin]] 16 | name = "pass" 17 | path = "src/main.rs" 18 | 19 | [dependencies] 20 | lexopt = "0.3.0" 21 | owo-colors = "4.0.0" 22 | supports-color = "2.1.0" 23 | xdg = "2.4.1" 24 | path-absolutize = "3.0.14" 25 | nix = { version = "0.27.1", features = ["mman", "process", "resource", "term"] } 26 | rand = "0.8.5" 27 | rust-argon2 = "2.1.0" 28 | chacha20poly1305 = { version = "0.10.1", features = ["stream"] } 29 | serde = { version = "1.0.150", features = ["derive"] } 30 | ron = "0.8.0" 31 | sublime_fuzzy = "0.7.0" 32 | arboard = { version = "3.2.0", default-features = false } 33 | shell-words = "1.1.0" 34 | 35 | [profile.release] 36 | strip = true 37 | # TODO: use once available 38 | #oom = "panic" # Allows destructors for sensitive data to run on OOM. 39 | -------------------------------------------------------------------------------- /src/util/clip.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | thread 4 | }; 5 | 6 | use std::fmt::Display; 7 | 8 | use std::{ 9 | time::Duration, 10 | borrow::Cow 11 | }; 12 | 13 | pub struct Clipboard(arboard::Clipboard); 14 | 15 | #[allow(clippy::enum_variant_names)] 16 | pub enum Error { 17 | AccessingClipboard(arboard::Error), 18 | SettingClipboard(arboard::Error), 19 | ClearingClipboard(arboard::Error) 20 | } 21 | 22 | pub type Result = std::result::Result; 23 | 24 | impl Clipboard { 25 | pub fn new() -> Result { 26 | Ok(Self( 27 | arboard::Clipboard::new() 28 | .map_err(Error::AccessingClipboard)? 29 | )) 30 | } 31 | 32 | /// XXX: copies `text` to the primary clipboard and keeps it there for 33 | /// `time` 34 | pub fn hold<'t, T>(&mut self, text: T, time: Duration) -> Result<()> 35 | where 36 | T: Into> 37 | { 38 | use arboard::SetExtLinux; 39 | use arboard::LinuxClipboardKind; 40 | 41 | self.0.set() 42 | .clipboard(LinuxClipboardKind::Primary) 43 | .text(text) 44 | .map_err(Error::SettingClipboard)?; 45 | 46 | thread::sleep(time); 47 | 48 | self.0.clear() 49 | .map_err(Error::ClearingClipboard) 50 | } 51 | } 52 | 53 | impl Display for Error { 54 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 55 | use Error::*; 56 | 57 | match self { 58 | AccessingClipboard(e) => 59 | write!(f, "cannot access clipboard: {e}"), 60 | SettingClipboard(e) => 61 | write!(f, "cannot set clipboard: {e}"), 62 | ClearingClipboard(e) => 63 | write!(f, "cannot clear clipboard: {e}") 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/backup.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | info, 3 | confirm 4 | }; 5 | 6 | use crate::util::{ 7 | user_io, 8 | file 9 | }; 10 | 11 | use crate::env::PROGNAME; 12 | 13 | use crate::util::file::SafePath; 14 | 15 | use std::fmt; 16 | 17 | use std::fmt::Display; 18 | 19 | pub enum Error { 20 | UserIo(user_io::Error), 21 | File(file::Error), 22 | Removal(file::Error), 23 | RemovalRefusal, 24 | } 25 | 26 | pub type Result = std::result::Result<(), Error>; 27 | 28 | impl From for Error { 29 | fn from(e: user_io::Error) -> Self { 30 | Self::UserIo(e) 31 | } 32 | } 33 | 34 | /// Informs the user of `path`'s backup if it exists, and recovers it if the 35 | /// user consents. 36 | pub fn maybe_recover(path: &SafePath) -> Result { 37 | let is_backed_up = path.is_backed_up() 38 | .map_err(Error::File)?; 39 | 40 | if !is_backed_up { 41 | return Ok(()); 42 | } 43 | 44 | info!("Backup found for '{}'", path.display()); 45 | 46 | println!( 47 | "This may mean that '{PROGNAME}' crashed while modifying it, \ 48 | or that another instance is modifying it." 49 | ); 50 | 51 | if confirm!("Recover the backup?")? { 52 | path.recover() 53 | .map_err(Error::File) 54 | } else if confirm!("Remove the backup anyway?")? { 55 | path.remove_backup() 56 | .map_err(Error::Removal) 57 | } else { 58 | Err(Error::RemovalRefusal) 59 | } 60 | } 61 | 62 | impl Display for Error { 63 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 64 | use Error::*; 65 | 66 | match self { 67 | UserIo(e) => write!(f, "{e}"), 68 | File(e) => write!(f, "{e}"), 69 | Removal(e) => write!(f, "removal failed: {e}"), 70 | RemovalRefusal => write!(f, "user refusal") 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/util/crypt/key.rs: -------------------------------------------------------------------------------- 1 | use crate::util::secret::Erase; 2 | 3 | use super::header::Header; 4 | 5 | use std::fmt; 6 | 7 | use std::fmt::Display; 8 | 9 | /// A private encryption key. 10 | /// 11 | /// Should be secured and erased from memory after use, for example by wrapping 12 | /// it in a [`Secret`][`crate::util::secret::Secret`]. 13 | #[derive(PartialEq, Eq)] 14 | pub struct Key(Vec); 15 | 16 | pub enum Error { 17 | HashingPassword(argon2::Error), 18 | } 19 | 20 | pub type Result = std::result::Result; 21 | 22 | impl Key { 23 | /// The length in bytes of an encryption key, according to 24 | /// [`chacha20poly1305`] documentation. 25 | pub const LEN: usize = 32; 26 | 27 | /// Returns a `Key` hashed from `pw`. 28 | /// 29 | /// Uses the salt in `head`. 30 | pub fn from_password

(pw: P, head: &Header) -> Result 31 | where 32 | P: AsRef<[u8]> 33 | { 34 | use argon2::{Config, Variant, Version}; 35 | 36 | // TODO: we want to use rfc9106 recommended defaults, but for now we'll 37 | // leave it like this for simplicity. eventually we'll want to introduce 38 | // a pass file versioning scheme 39 | let hash_conf = Config { 40 | variant: Variant::Argon2id, 41 | version: Version::Version13, 42 | hash_length: Self::LEN as u32, 43 | mem_cost: 0x800, // The default causes a crash on debug. 44 | ..Config::original() 45 | }; 46 | 47 | let result = argon2::hash_raw( 48 | pw.as_ref(), 49 | head.salt(), 50 | &hash_conf 51 | ).map_err(Error::HashingPassword)?; 52 | 53 | Ok(Self(result)) 54 | } 55 | 56 | /// Returns a reference to the contained key. 57 | /// 58 | /// The returned slice is guaranteed to be `Self::LEN` bytes long. 59 | pub fn as_slice(&self) -> &[u8] { 60 | &self.0 61 | } 62 | } 63 | 64 | impl Erase for Key { 65 | #[inline(never)] 66 | fn erase(&mut self) { 67 | self.0.erase(); 68 | } 69 | } 70 | 71 | impl Display for Error { 72 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 73 | use Error::*; 74 | 75 | match self { 76 | HashingPassword(e) => write!(f, "cannot hash password: {e}"), 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/util/crypt/header.rs: -------------------------------------------------------------------------------- 1 | use std::io::{ 2 | Read, 3 | Write 4 | }; 5 | 6 | /// XXX: [Encryption diagram][1] 7 | /// public encryption metadata 8 | /// large struct, be careful with passing between functions excessively 9 | /// 10 | /// [1]: https://docs.rs/aead/latest/aead/stream/index.html 11 | pub struct Header { 12 | /// The password salt and associated data for AEAD (encryption). 13 | salt: [u8; SALT_LEN], 14 | /// The nonce used for AEAD. 15 | nonce: [u8; NONCE_LEN] 16 | } 17 | 18 | pub type Error = std::io::Error; 19 | 20 | pub type Result = std::result::Result; 21 | 22 | impl Header { 23 | pub fn generate() -> Self { 24 | Self { 25 | salt: rand_bytes(), 26 | nonce: rand_bytes() 27 | } 28 | } 29 | 30 | pub fn salt(&self) -> &[u8] { 31 | &self.salt 32 | } 33 | 34 | pub fn nonce(&self) -> &[u8] { 35 | &self.nonce 36 | } 37 | 38 | /// XXX: reads `SALT_LEN + NONCE_LEN` 39 | #[inline(always)] // The returned struct is very large. 40 | pub fn read_from(mut src: R) -> Result { 41 | let mut salt = [0_u8; SALT_LEN]; 42 | 43 | src.read_exact(&mut salt)?; 44 | 45 | let mut nonce = [0_u8; NONCE_LEN]; 46 | 47 | src.read_exact(&mut nonce)?; 48 | 49 | Ok(Self { salt, nonce }) 50 | } 51 | 52 | /// XXX: writes everything or fails 53 | /// writes `SALT_LEN + NONCE_LEN` 54 | pub fn write_to(&self, mut dest: W) -> Result<()> { 55 | dest.write_all(&self.salt)?; 56 | dest.write_all(&self.nonce)?; 57 | 58 | Ok(()) 59 | } 60 | } 61 | 62 | /// The recommended salt length in bytes for `Argon2`, according to [`argon2`] 63 | /// documentation. 64 | const SALT_LEN: usize = 16; 65 | 66 | /// The recommended length in bytes for a randomly generated 67 | /// [`XChaCha20Poly1305`][1] nonce, according to [`chacha20poly1305`] 68 | /// documentation. 69 | /// 70 | /// The standard length is 24 bytes, but [5 bytes are used by 71 | /// `XChaCha20Poly1305`][1] for a counter and "last block" flag, so we only need 72 | /// 24 - 5 bytes of random data. 73 | /// 74 | /// [1]: chacha20poly1305::aead::stream::StreamBE32 75 | const NONCE_LEN: usize = 19; 76 | 77 | /// XXX: cryptographically secure 78 | #[inline(always)] // Copying large arrays is inefficient. 79 | fn rand_bytes() -> [u8; N] { 80 | use rand::RngCore; 81 | use rand::rngs::OsRng; 82 | 83 | let mut result = [0_u8; N]; 84 | 85 | // `OsRng` implements `CryptoRng` so it is cryptographically secure. 86 | OsRng.fill_bytes(&mut result); 87 | result 88 | } 89 | -------------------------------------------------------------------------------- /src/input_pw.rs: -------------------------------------------------------------------------------- 1 | use crate::util::{ 2 | user_io, 3 | crypt::key 4 | }; 5 | 6 | use crate::util::{ 7 | crypt::{Key, Header}, 8 | secret::Secret, 9 | }; 10 | 11 | use crate::err; 12 | 13 | use std::fmt; 14 | 15 | use std::fmt::Display; 16 | 17 | pub enum Error { 18 | HidingInput(user_io::Error), 19 | ShowingInput(user_io::Error), 20 | ReadingInput(user_io::Error), 21 | GeneratingKey(key::Error) 22 | } 23 | 24 | pub type Result = std::result::Result; 25 | 26 | pub fn confirm_to_key( 27 | head: &Header, 28 | prompt_1: &str, 29 | prompt_2: &str 30 | ) -> Result { 31 | loop { 32 | // Instead of comparing the passwords directly (potentially in variable 33 | // time, and thus enabling side-channel attacks), we compare their 34 | // hashes in constant time. 35 | let (key, key_confirm) = ( 36 | Secret::new(read_to_key(head, prompt_1)?), 37 | Secret::new(read_to_key(head, prompt_2)?) 38 | ); 39 | 40 | if *key == *key_confirm { 41 | break Ok(key.into_inner()) ; 42 | } else { 43 | err!("passwords do not match"); 44 | } 45 | } 46 | } 47 | 48 | /// XXX: reads pw and generates key with `head` 49 | pub fn read_to_key(head: &Header, prompt: &str) -> Result { 50 | let pw = Secret::new(read(prompt)?); 51 | 52 | let result = Key::from_password(pw, head) 53 | .map_err(Error::GeneratingKey)?; 54 | 55 | Ok(result) 56 | } 57 | 58 | /// hidden input 59 | pub fn read(prompt: &str) -> Result { 60 | use crate::{input, warn}; 61 | 62 | user_io::hide_input() 63 | .map_err(Error::HidingInput)?; 64 | 65 | match input!("{prompt}") { 66 | Ok(pw) => { 67 | // The user-entered newline is hidden; print it ourselves. 68 | eprintln!(); 69 | 70 | let result = Secret::new(pw); 71 | 72 | match user_io::show_input() { 73 | Ok(()) => Ok(result.into_inner()), 74 | Err(e) => Err(Error::ShowingInput(e)) 75 | } 76 | }, 77 | 78 | Err(e) => { 79 | if let Err(e) = user_io::show_input() { 80 | warn!("{}", Error::ShowingInput(e)); 81 | } 82 | 83 | Err(Error::ReadingInput(e)) 84 | } 85 | } 86 | } 87 | 88 | impl Display for Error { 89 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 90 | use Error::*; 91 | 92 | match self { 93 | HidingInput(e) => write!(f, "cannot hide input: {e}"), 94 | ShowingInput(e) => write!(f, "cannot show input: {e}"), 95 | ReadingInput(e) => write!(f, "{e}"), 96 | GeneratingKey(e) => write!(f, "{e}") 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/serial.rs: -------------------------------------------------------------------------------- 1 | use crate::find; 2 | 3 | use crate::util::record; 4 | 5 | use crate::util::{ 6 | record::{Record, Node, Ir}, 7 | secret::Secret 8 | }; 9 | 10 | use std::{ 11 | fmt, 12 | str 13 | }; 14 | 15 | use std::fmt::Display; 16 | 17 | pub enum Error { 18 | NonUtf8Data(str::Utf8Error), 19 | Deserialisation(record::Error), 20 | Serialisation(record::Error), 21 | InvalidRecord(find::Error) 22 | } 23 | 24 | pub type Result = std::result::Result; 25 | 26 | impl From for Error { 27 | fn from(e: record::Error) -> Self { 28 | Self::Deserialisation(e) 29 | } 30 | } 31 | 32 | impl From for Error { 33 | fn from(e: str::Utf8Error) -> Self { 34 | Self::NonUtf8Data(e) 35 | } 36 | } 37 | 38 | pub fn parse(bytes: &[u8]) -> Result> { 39 | let serial = str::from_utf8(bytes)?; 40 | // This needs not be wrapped in a `Secret` because it will infallibly be 41 | // converted by value into a `Record`. 42 | let ir = Ir::from_str(serial)?; 43 | 44 | Ok(Record::from(ir)) 45 | } 46 | 47 | pub fn str_from(bytes: &[u8]) -> Result<&str> { 48 | Ok(str::from_utf8(bytes)?) 49 | } 50 | 51 | pub fn ir_from(bytes: &[u8]) -> Result { 52 | let serial = str_from(bytes)?; 53 | 54 | Ir::from_str(serial) 55 | .map_err(Error::Deserialisation) 56 | } 57 | 58 | pub fn bytes_from(rec_secret: Secret>) -> Result> { 59 | let rec = rec_secret.into_inner(); 60 | let ir = Secret::new(Ir::from(rec)); 61 | 62 | let result = ir.to_string() 63 | .map_err(Error::Serialisation)? 64 | .into_bytes(); 65 | 66 | Ok(result) 67 | } 68 | 69 | /// XXX: returns Ok(()) if valid serial data 70 | pub fn validate(s: &str) -> Result<()> { 71 | use find::Error::NotAGroup; 72 | 73 | let ir = Ir::from_str(s)?; 74 | let rec = Secret::new(Record::from(ir)); 75 | let rec_ref = &*rec.borrow(); 76 | 77 | match rec_ref { 78 | // The root group can obviously not be an item. 79 | Record::Item(i) => Err(Error::InvalidRecord(NotAGroup { 80 | name: i.borrow().name().to_owned(), 81 | pat: None 82 | })), 83 | 84 | Record::Group(_) => Ok(()) 85 | } 86 | } 87 | 88 | /// XXX: empty group record in serial form 89 | pub fn new_empty(name: String) -> String { 90 | let rec = Record::new_group(name); 91 | let ir = Secret::new(Ir::from(rec)); 92 | 93 | // Serialising an empty `Record` should never fail. 94 | ir.to_string().unwrap() 95 | } 96 | 97 | impl Display for Error { 98 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 99 | use Error::*; 100 | 101 | match self { 102 | NonUtf8Data(e) => write!(f, "{e}"), 103 | Deserialisation(e) => write!(f, "{e}"), 104 | Serialisation(e) => write!(f, "{e}"), 105 | InvalidRecord(e) => write!(f, "{e}") 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/util/proc.rs: -------------------------------------------------------------------------------- 1 | use nix::{ 2 | sys::{mman, resource}, 3 | unistd 4 | }; 5 | 6 | use nix::sys::{ 7 | mman::MlockAllFlags, 8 | resource::Resource 9 | }; 10 | 11 | use nix::unistd::{ForkResult, Pid}; 12 | 13 | #[derive(Clone, Copy, PartialEq, Eq)] 14 | // Our version of `ForkResult` has a `must_use` attribute, which encourages the 15 | // user to handle it. This is vital, as the child process is likely intended to 16 | // follow a different path of execution. 17 | #[must_use = "the currently executing process could be the child or parent"] 18 | pub enum Process { 19 | Child, 20 | Parent { child: Pid } 21 | } 22 | 23 | pub type Error = nix::Error; 24 | 25 | pub type Result = std::result::Result; 26 | 27 | impl From for Process { 28 | fn from(f: ForkResult) -> Self { 29 | use ForkResult::{Child, Parent}; 30 | 31 | match f { 32 | Child => Self::Child, 33 | Parent { child } => Self::Parent { child } 34 | } 35 | } 36 | } 37 | 38 | /// XXX: refer to manpages 39 | 40 | /// Locks all current and future mapped memory pages, preventing them from being 41 | /// swapped to disk, and disables process core dumps. 42 | /// 43 | /// This can be used to avoid leaking passwords and other sensitive data to the 44 | /// disk, thus potentially exposing it to external actors. 45 | pub fn secure_mem() -> Result<()> { 46 | mman::mlockall(MlockAllFlags::all())?; 47 | disable_dumps()?; 48 | 49 | Ok(()) 50 | } 51 | 52 | /// Reverses the effects of [`secure_mem`]. 53 | pub fn expose_mem() -> Result<()> { 54 | mman::munlockall()?; 55 | // Linux does not allow core dumps to be re-enabled after having been 56 | // disabled, even as root. 57 | // 58 | #[cfg(not(target_os = "linux"))] 59 | enable_dumps()?; 60 | 61 | Ok(()) 62 | } 63 | 64 | /// # Safety 65 | /// 66 | /// This function is completely safe if called from a single-threaded process. 67 | /// However, the newly created process is not an exact duplicate of the 68 | /// original. For example, it does not inherit its parent's memory locks (such 69 | /// as those applied by [`secure_mem`]). Further differences are available at 70 | /// the [`fork(2)`] man page. 71 | /// 72 | /// If called from a multi-threaded program, undefined behaviour is possible 73 | /// under circumstances outlined in the documentation for [`unistd::fork`]. In 74 | /// particular, only async safe functions may be called from the child process. 75 | /// 76 | /// [`fork(2)`]: https://man7.org/linux/man-pages/man2/fork.2.html 77 | pub unsafe fn fork() -> Result { 78 | Ok(unistd::fork()?.into()) 79 | } 80 | 81 | fn disable_dumps() -> Result<()> { 82 | resource::setrlimit(Resource::RLIMIT_CORE, 0, 0) 83 | } 84 | 85 | /// XXX: doesnt work on linux, even as root 86 | /// 87 | #[allow(unused)] // Unused on Linux. 88 | fn enable_dumps() -> Result<()> { 89 | use nix::libc::RLIM_INFINITY; 90 | 91 | resource::setrlimit( 92 | Resource::RLIMIT_CORE, 93 | RLIM_INFINITY, 94 | RLIM_INFINITY 95 | ) 96 | } 97 | -------------------------------------------------------------------------------- /src/util/user_io/style.rs: -------------------------------------------------------------------------------- 1 | use owo_colors::AnsiColors; 2 | 3 | use supports_color::Stream::{self, Stdout, Stderr}; 4 | 5 | use std::fmt; 6 | 7 | use std::fmt::Display; 8 | 9 | /// For a text object that can be styled with ANSI codes. 10 | /// 11 | /// The functions only style the object if the terminal and relevant stream 12 | /// support it, otherwise they return an unstyled `StyledMsg`. The 13 | /// [`NO_COLOR`](https://no-color.org/) convention is supported. 14 | /// 15 | /// The implementation for `str` performs no heap allocations, and doesn't copy 16 | /// the styled string. It is therefore usable for sensitive strings. 17 | pub trait Style: Display { 18 | fn as_error(&self) -> StyledMsg<&Self> { 19 | maybe_colour(self, Stderr, AnsiColors::BrightRed) 20 | } 21 | 22 | fn as_warning(&self) -> StyledMsg<&Self> { 23 | maybe_colour(self, Stderr, AnsiColors::Yellow) 24 | } 25 | 26 | fn as_notice(&self) -> StyledMsg<&Self> { 27 | maybe_colour(self, Stderr, AnsiColors::Blue) 28 | } 29 | 30 | fn as_prompt(&self) -> StyledMsg<&Self> { 31 | maybe_colour(self, Stderr, AnsiColors::Cyan) 32 | } 33 | 34 | fn as_title(&self) -> StyledMsg<&Self> { 35 | maybe_colour(self, Stdout, AnsiColors::Green) 36 | } 37 | 38 | fn as_heading(&self) -> StyledMsg<&Self> { 39 | maybe_colour(self, Stdout, AnsiColors::BrightMagenta) 40 | } 41 | 42 | fn as_name(&self) -> StyledMsg<&Self> { 43 | if supports_color::on_cached(Stdout).is_some() { 44 | StyledMsg::with_style(self, owo_colors::Style::new().bold()) 45 | } else { 46 | StyledMsg::new(self) 47 | } 48 | } 49 | } 50 | 51 | impl Style for str {} 52 | 53 | // we cannot use `owo_colors::Styled` or `owo_colors::SupportsColorsDisplay` 54 | // because it can only be created as a double reference (e.g. `Styled<&&str>`), 55 | // which upsets the borrow checker when we try to return it. 56 | pub struct StyledMsg { 57 | msg: M, 58 | style: owo_colors::Style 59 | } 60 | 61 | impl StyledMsg { 62 | /// Returns a `StyledMsg` without an associated style. 63 | fn new(msg: M) -> Self { 64 | StyledMsg { msg, style: owo_colors::Style::new() } 65 | } 66 | 67 | fn with_style(msg: M, style: owo_colors::Style) -> Self { 68 | Self { msg, style } 69 | } 70 | 71 | fn with_colour(msg: M, colour: AnsiColors) -> Self { 72 | let style = owo_colors::Style::new() 73 | .bold().color(colour); 74 | 75 | Self { msg, style } 76 | } 77 | } 78 | 79 | impl Display for StyledMsg { 80 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 81 | use owo_colors::OwoColorize; 82 | 83 | write!(f, "{}", self.msg.style(self.style)) 84 | } 85 | } 86 | 87 | /// If styling is enabled on `strm`, returns `msg` coloured with `col`. 88 | /// Otherwise, returns an unstyled `StyledMsg`. 89 | fn maybe_colour(msg: &M, strm: Stream, col: AnsiColors) -> StyledMsg<&M> 90 | where 91 | M: Display + ?Sized 92 | { 93 | if supports_color::on_cached(strm).is_some() { 94 | StyledMsg::with_colour(msg, col) 95 | } else { 96 | StyledMsg::new(msg) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/util/secret/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | mem, 3 | ptr 4 | }; 5 | 6 | use std::ops::{ 7 | Deref, 8 | DerefMut 9 | }; 10 | 11 | pub mod erase; 12 | 13 | pub use erase::Erase; 14 | 15 | /// Wrapper for securing data in memory, intended for cryptographic secrets. 16 | /// 17 | /// The contained data is automatically erased when dropped by being 18 | /// overwritten with zeros. The operation is ensured to not be compromised by 19 | /// optimisations, with additional guarantees specified in the documentation for 20 | /// [`Erase`]. 21 | /// 22 | /// No other protections are applied to the memory occupied by the data. It is 23 | /// recommended to prevent it from being swapped to disk or exposed in 24 | /// core dumps if this is a necessity. Functions in the 25 | /// [`mem`][crate::util::proc] module can be used for this. Furthermore, 26 | /// constant time algorithms may be used if the data is compared to other data 27 | /// to mitigate the vulnerability to side channel attacks. 28 | // We do not provide `Debug` or `Display` implementations to prevent 29 | // accidental exposures of the contained data or its metadata. 30 | pub struct Secret(T); 31 | 32 | impl Secret { 33 | /// Constructs a new `Secret`. 34 | pub fn new(data: T) -> Self { 35 | Self(data) 36 | } 37 | 38 | /// Unwraps this `Secret`, returning the data contained within it. 39 | /// 40 | /// Consumes the given `Secret`, thereby disabling the memory protections 41 | /// applied to the data. 42 | pub fn into_inner(self) -> T { 43 | // SAFETY: A single copy of the data is made and the original is 44 | // "forgotten", so its destructor will only run once. The `Secret` 45 | // destructor will never run at all, so the data is not modified. 46 | unsafe { 47 | let inner = ptr::read(&self.0); 48 | 49 | mem::forget(self); 50 | inner 51 | } 52 | } 53 | } 54 | 55 | impl Drop for Secret { 56 | /// Erases the data contained within the `Secret`. 57 | /// 58 | /// The data's destructor is called afterwards as usual. 59 | fn drop(&mut self) { 60 | self.0.erase(); 61 | } 62 | } 63 | 64 | impl Deref for Secret { 65 | type Target = T; 66 | 67 | /// Returns a reference into the given `Secret`. 68 | /// 69 | /// Improper copying of the referred-to data may expose it, by creating a 70 | /// copies that aren't protected by a `Secret`. 71 | fn deref(&self) -> &Self::Target { 72 | &self.0 73 | } 74 | } 75 | 76 | impl DerefMut for Secret { 77 | /// Returns a mutable reference into the given `Secret`. 78 | /// 79 | /// Improper manipulation of the reference obtained with this function may 80 | /// deallocate data without erasing it, thereby potentially exposing it. For 81 | /// example, this may happen if [`Vec::shrink_to`] is called on a [`Vec`] 82 | /// within the `Secret`. It is the responsibility of the caller that this 83 | /// does not happen. 84 | fn deref_mut(&mut self) -> &mut Self::Target { 85 | &mut self.0 86 | } 87 | } 88 | 89 | impl AsRef for Secret 90 | where 91 | U: ?Sized, 92 | as Deref>::Target: AsRef 93 | { 94 | fn as_ref(&self) -> &U { 95 | self.deref().as_ref() 96 | } 97 | } 98 | 99 | impl AsMut for Secret 100 | where 101 | U: ?Sized, 102 | as Deref>::Target: AsMut 103 | { 104 | fn as_mut(&mut self) -> &mut U { 105 | self.deref_mut().as_mut() 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/util/record/ir.rs: -------------------------------------------------------------------------------- 1 | //! XXX: intermediate representation 2 | 3 | use super::{Record, Error, Node}; 4 | 5 | use crate::util::secret::Erase; 6 | use crate::util::secret::Secret; 7 | 8 | use serde::{Serialize, Deserialize}; 9 | 10 | use std::fmt; 11 | 12 | use std::fmt::Display; 13 | 14 | use std::{ 15 | collections::BTreeMap, 16 | rc::Rc 17 | }; 18 | 19 | /// XXX: intermediate representation 20 | #[derive(Serialize, Deserialize)] 21 | pub enum Ir { 22 | Group { 23 | name: String, 24 | members: Vec, 25 | #[allow(unused)] // May be useful later. 26 | metadata: Metadata 27 | }, 28 | Item { 29 | name: String, 30 | value: String, 31 | #[allow(unused)] 32 | metadata: Metadata 33 | } 34 | } 35 | 36 | type Metadata = BTreeMap; 37 | 38 | type Result = std::result::Result; 39 | 40 | impl Ir { 41 | pub fn name(&self) -> &str { 42 | match self { 43 | Self::Group { name, .. } => name, 44 | Self::Item { name, .. } => name 45 | } 46 | } 47 | 48 | pub fn clone_from(rec: &Node) -> Self { 49 | match &*rec.borrow() { 50 | Record::Group(g) => { 51 | let g = g.borrow(); 52 | 53 | let members = g.members.values() 54 | .map(Self::clone_from) 55 | .collect(); 56 | 57 | Self::Group { 58 | name: g.meta.name.clone(), 59 | members, 60 | metadata: BTreeMap::new() 61 | } 62 | } 63 | 64 | Record::Item(i) => { 65 | let i = i.borrow(); 66 | 67 | Self::Item { 68 | name: i.meta.name.clone(), 69 | value: i.value.clone(), 70 | metadata: BTreeMap::new() 71 | } 72 | } 73 | } 74 | } 75 | 76 | pub fn from_str(s: &str) -> Result { 77 | ron::from_str(s) 78 | .map_err(|e| Error::Deserialisation(Box::new(e))) 79 | } 80 | 81 | pub fn to_string(&self) -> Result { 82 | ron::to_string(self) 83 | .map_err(|e| Error::Serialisation(Box::new(e))) 84 | } 85 | } 86 | 87 | impl From> for Ir { 88 | fn from(r: Node) -> Self { 89 | Ir::from(take(r)) 90 | } 91 | } 92 | 93 | impl From for Ir { 94 | fn from(r: Record) -> Self { 95 | match r { 96 | Record::Group(g) => { 97 | let g = take(g); 98 | 99 | let members = g.members.into_values() 100 | .map(Ir::from) 101 | .collect(); 102 | 103 | Self::Group { 104 | name: g.meta.name, 105 | members, 106 | metadata: BTreeMap::new() 107 | } 108 | } 109 | 110 | Record::Item(i) => { 111 | let i = take(i); 112 | 113 | Self::Item { 114 | name: i.meta.name, 115 | value: i.value, 116 | metadata: BTreeMap::new() 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | impl Display for Ir { 124 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 125 | use ron::ser::PrettyConfig; 126 | 127 | let conf = PrettyConfig::default(); 128 | 129 | // TODO: find a way to write directly to formatter using 130 | // `to_writer_pretty` or something. this approach creates allocates a 131 | // string and requires erasing it 132 | let serial = Secret::new( 133 | ron::ser::to_string_pretty(self, conf) 134 | .map_err(|e| Error::Serialisation(Box::new(e))) 135 | .unwrap() 136 | ); 137 | 138 | write!(f, "{}", *serial) 139 | } 140 | } 141 | 142 | impl Erase for Ir { 143 | #[inline(never)] 144 | fn erase(&mut self) { 145 | match self { 146 | Self::Group { name, members, metadata: _ } => { 147 | name.erase(); 148 | members.erase(); 149 | //metadata.erase(); 150 | } 151 | 152 | Self::Item { name, value, metadata: _ } => { 153 | name.erase(); 154 | value.erase(); 155 | //metadata.erase(); 156 | } 157 | } 158 | } 159 | } 160 | 161 | /// XXX: may panic 162 | fn take(v: Node) -> T { 163 | Rc::try_unwrap(v) 164 | .unwrap_or_else(|_| panic!("Rc is owned by someone else")) 165 | .into_inner() 166 | } 167 | -------------------------------------------------------------------------------- /src/util/secret/erase.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BTreeMap, 3 | rc::Rc, 4 | cell::RefCell 5 | }; 6 | 7 | /// For securely erasing data from memory. 8 | /// 9 | /// Allows wrapping the type in a [`Secret`][`super::Secret`]. 10 | /// 11 | /// The implementation of [`Erase::erase`] must abide by the constraints 12 | /// expressed in its documentation. Thus, when implementing it (and already 13 | /// existing implementations cannot be used) functions like [`set_volatile`] and 14 | /// [`atomic_fence`] should be used. 15 | pub trait Erase { 16 | /// Erases an object by overwriting its data with zeros. 17 | /// 18 | /// This function is intended for pointer types, like [`Vec`], 19 | /// [`String`]. It does only one thing: erasing the raw data they contain. 20 | /// However, it does not erase associated metadata, like the length stored 21 | /// within a [`Vec`]. This is because it very rarely serves a purpose to do 22 | /// so - the most common use case of this trait is in the context of a 23 | /// [`Secret`][`super::Secret`], which is constructed by moving an object 24 | /// into it. Indeed, this is likely to copy and leak the aforementioned 25 | /// metadata anyway (as is the case of the length stored in a [`Vec`]). If 26 | /// the metadata must also be kept secret, it must never be copied or moved 27 | /// and must be manually erased once it is no longer needed. 28 | /// 29 | /// `self` remains valid after this operation, although it no longer 30 | /// contains any useful data. 31 | /// 32 | /// Uses volatile writes to ensure that the operation is not compromised by 33 | /// compiler optimisations. Additionally, atomic fences are used after these 34 | /// writes to prevent the compiler or CPU from reordering memory operations. 35 | /// Thus, the data appears erased to all accessors after this function is 36 | /// applied to it. 37 | /// 38 | /// The write operations themselves must be constant-time per byte, 39 | /// irrespective of its value. This is to mitigate side-channel attacks. 40 | /// The data is overwritten in place, without being copied or otherwise 41 | /// leaked. This must be carefully ensured with [`Copy`] types. 42 | /// 43 | /// This function should never be inlined to prevent other types of 44 | /// optimisation-related security breaches. The attribute `inline(never)` 45 | /// can be used for this purpose. 46 | fn erase(&mut self); 47 | } 48 | 49 | impl Erase for Vec { 50 | #[inline(never)] 51 | fn erase(&mut self) { 52 | for v in self.as_mut_slice() { 53 | v.erase(); 54 | } 55 | 56 | atomic_fence(); 57 | } 58 | } 59 | 60 | impl Erase for Vec { 61 | #[inline(never)] 62 | fn erase(&mut self) { 63 | for v in self.as_mut_slice() { 64 | set_volatile(v, 0); 65 | } 66 | 67 | atomic_fence(); 68 | } 69 | } 70 | 71 | impl Erase for String { 72 | #[inline(never)] 73 | fn erase(&mut self) { 74 | // SAFETY: The `Erase` implementation for `Vec` overwrites its data with 75 | // zeroes, which are valid utf-8 code points. Thus, `self` is left in a 76 | // valid state. 77 | unsafe { 78 | self.as_mut_vec().erase(); 79 | } 80 | } 81 | } 82 | 83 | impl<'k, V: Erase> Erase for BTreeMap<&'k str, V> { 84 | #[inline(never)] 85 | fn erase(&mut self) { 86 | for v in self.values_mut() { 87 | v.erase(); 88 | } 89 | 90 | atomic_fence(); 91 | } 92 | } 93 | 94 | impl Erase for BTreeMap 95 | where 96 | K: Erase + Ord, 97 | V: Erase 98 | { 99 | #[inline(never)] 100 | fn erase(&mut self) { 101 | // TODO: creates copy if K or V are Copy, fix if possible using mutable 102 | // references 103 | // inefficient, does comparisons although we can pop any element 104 | while let Some((mut k, mut v)) = self.pop_last() { 105 | k.erase(); 106 | v.erase(); 107 | } 108 | 109 | atomic_fence(); 110 | } 111 | } 112 | 113 | impl Erase for Rc> { 114 | #[inline(never)] 115 | fn erase(&mut self) { 116 | self.borrow_mut().erase(); 117 | } 118 | } 119 | 120 | /// Sets each element of `dest` to `val` such that the operation cannot be 121 | /// "optimised away". 122 | pub fn set_volatile(dest: &mut T, val: T) { 123 | use std::ptr::write_volatile; 124 | 125 | // SAFETY: `dest` is a valid, properly aligned and writable pointer (as it 126 | // is an exclusive reference). Furthermore, the value replaced implements 127 | // `Copy` and can therefore be safely overwritten. 128 | unsafe { 129 | write_volatile(dest, val); 130 | } 131 | } 132 | 133 | /// Prevent memory accesses around a call to this function from being reordered. 134 | pub fn atomic_fence() { 135 | use std::sync::atomic::Ordering; 136 | use std::sync::atomic::fence; 137 | 138 | fence(Ordering::SeqCst); 139 | } 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pass-rs 2 | 3 | `pass-rs` is a program for managing encrypted textual data. Its primary use is 4 | as a password manager, but its features permit other use cases. Contributions 5 | are welcome, especially in the form of security testing. 6 | 7 | ![Screenshot](samples/screenshot.png "Is this security?") 8 | 9 | Notable features include: 10 | 11 | - Clipboard support 12 | - An interactive TUI for editing 13 | - Versatile data format (exportable, arbitrary depth...) 14 | - ChaCha20-Poly1305 encryption with Argon2id for key generation 15 | - Crash protection and recovery 16 | 17 | ## Installation 18 | 19 | ### With Cargo 20 | 21 | To install and update: 22 | ``` 23 | cargo install passers 24 | ``` 25 | 26 | To uninstall: 27 | ``` 28 | cargo uninstall passers 29 | ``` 30 | 31 | ### Manual 32 | 33 | To install: 34 | ``` 35 | git clone https://github.com/Swarthe/pass-rs 36 | cd pass-rs 37 | make && sudo make install 38 | ``` 39 | 40 | To update: 41 | ```` 42 | git pull 43 | make && sudo make install 44 | ```` 45 | 46 | To uninstall: 47 | ``` 48 | sudo make uninstall 49 | ``` 50 | 51 | ## Usage 52 | 53 | ``` 54 | Usage: pass [OPTION...] [TARGET...] 55 | Securely manage hierarchical data. 56 | 57 | -c, --clip copy target item to primary clipboard instead of displaying 58 | -l, --list list the target's contents (root if not specified) 59 | -t, --tree display a tree of the target (root if not specified) 60 | 61 | -e, --exact find exact match of target (default: fuzzy match) 62 | -d, --duration time in seconds to keep target in clipboard (default: 10) 63 | -f, --file specify a pass file (default: standard data file) 64 | 65 | -M, --modify launch editing interface (respects '-e' and '-d') 66 | -P, --change-pw change the pass file's password 67 | 68 | -E, --export output data in serial form 69 | -I, --import create a pass file from serial data (read from stdin) 70 | -C, --create create an empty pass file with the specified root name 71 | 72 | -h, --help display this help text 73 | -v, --version display version information 74 | 75 | Note: By default, the target item is printed to standard output. 76 | Targets are passed as dot-separated record paths. 77 | Passing a group as a target item implies its child item 'password'. 78 | 79 | Example: pass -d5 -c foo.bar 80 | ``` 81 | 82 | ## TODO: program documentation 83 | 84 | Document encryption and security with links to further docs (Argon2id and 85 | XChaCha20Poly1305, best available). 86 | 87 | Standard man page also with environment info etc., and advice to see it in help 88 | text. 89 | 90 | document that we can configure with `config.rs` 91 | 92 | document where the data is stored by default 93 | - XDG data directory 94 | - backups in [XDG data]/backup 95 | 96 | The focus of this program (and what mainly distinguishes from other similar 97 | ones) is on maximum possible security and strict safety, with usability and 98 | performance as important secondary objectives. Unlike zx2c4's `pass` 99 | [hyperlink], this program rejects the Unix philosophy for its purpose, as it 100 | requires us to trust other programs in order to extend functionality. In the 101 | context of highly sensitive data, we consider this an unacceptable breach of 102 | security, and extra functionality is instead built into the program as far as 103 | possible. `pass-rs` also includes crash / data corruption protection, and 104 | various security measures to protect the data. 105 | 106 | Document particularities of fuzzy matching to help users 107 | ([docs](https://crates.io/crates/sublime_fuzzy)) 108 | smartcase etc. 109 | 110 | ## TODO: features 111 | 112 | Rofi integration (optional and separate, see other similar implementations)/ 113 | 114 | Password generation maybe with real words etc so can be remembered 115 | (customisable). 116 | 117 | Maybe include more metadata like creation date. 118 | 119 | ## TODO: distribution 120 | 121 | Distribute as statically linked (portable) binary, and as source with AUR. 122 | 123 | Perhaps cross-platform (at least Unix/POSIX). 124 | 125 | Possibly add to the Free Software Directory (we are gplv3) 126 | 127 | ## TODO: dependencies 128 | 129 | - `rustc` and `cargo` 130 | - Internet connection (for initial installation and updates) 131 | - Unix system (currently only tested on Arch Linux) 132 | 133 | ## License 134 | 135 | Copyright (C) 2023 Emil Overbeck ``. 136 | 137 | This file is part of `pass-rs`. 138 | 139 | This software is free software: you can redistribute it and/or modify it under 140 | the terms of the GNU General Public License as published by the Free Software 141 | Foundation, either version 3 of the License, or (at your option) any later 142 | version. 143 | 144 | `pass-rs` is distributed in the hope that it will be useful, but WITHOUT ANY 145 | WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 146 | A PARTICULAR PURPOSE. See the GNU General Public License for more details. 147 | 148 | You should have received a copy of the GNU General Public License along with 149 | `pass-rs`. If not, see . 150 | -------------------------------------------------------------------------------- /src/output.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | error::{Error, Result}, 3 | find::{RecordPath, MatchKind} 4 | }; 5 | 6 | use crate::util::{clip, proc}; 7 | 8 | use crate::util::{ 9 | record::{Record, Node}, 10 | proc::Process 11 | }; 12 | 13 | use std::fmt::Display; 14 | 15 | use std::time::Duration; 16 | 17 | /// XXX: several paths 18 | pub struct PrintTarget { 19 | paths: Vec, 20 | mk: MatchKind 21 | } 22 | 23 | /// XXX: single paths 24 | pub struct ClipTarget { 25 | path: RecordPath, 26 | mk: MatchKind, 27 | time: Duration 28 | } 29 | 30 | /// a result-like type that carries extra information on whether the process is 31 | /// a child or a parent, if it was forked 32 | /// 33 | /// This allows us to track what kind of process this is, even if the result is 34 | /// an `Err`. 35 | /// 36 | /// if .0 is None, then the process was not forked 37 | /// if .0 is Some(p), then p determines whether or not process is forked 38 | pub type ResultForked = (Option, Result<()>); 39 | 40 | impl PrintTarget { 41 | pub fn new(paths: Vec, mk: MatchKind) -> Self { 42 | Self { paths, mk } 43 | } 44 | 45 | pub fn print_values(self, data: &Node) { 46 | for p in self.paths { 47 | match p.find_item_or_default_in(data, self.mk) { 48 | Ok(item) => println!("{}", item.borrow().value()), 49 | Err(e) => Error::from(e).print_full() 50 | } 51 | } 52 | } 53 | 54 | pub fn print_lists(self, data: &Node) { 55 | print_each_spaced(self.paths, |p| { 56 | let rec = p.find_in(data, self.mk)?; 57 | 58 | Ok(Record::display_list(&rec)) 59 | }) 60 | } 61 | 62 | pub fn print_trees(self, data: &Node) { 63 | print_each_spaced(self.paths, |p| { 64 | let rec = p.find_in(data, self.mk)?; 65 | 66 | Ok(Record::display_tree(&rec)) 67 | }) 68 | } 69 | } 70 | 71 | impl ClipTarget { 72 | pub fn new(path: RecordPath, mk: MatchKind, time: Duration) -> Self { 73 | Self { path, mk, time } 74 | } 75 | 76 | /// Finds the target in `data` and copies it to the clipboard. 77 | /// 78 | /// Forks the process into a parent a child, the latter of which is 79 | /// responsible for preserving the clipboard. See [`clip_timed`] for more 80 | /// details. 81 | pub fn clip(self, data: &Node) -> ResultForked { 82 | let item_result = self.path 83 | .find_item_or_default_in(data, self.mk); 84 | 85 | let item = match item_result { 86 | Ok(i) => i, 87 | Err(e) => return (None, Err(e.into())) 88 | }; 89 | 90 | let item = item.borrow(); 91 | let value = item.value(); 92 | 93 | clip_timed(value, self.time) 94 | } 95 | } 96 | 97 | /// Copies `text` to the primary clipboard, and clears it after `time`. 98 | /// 99 | /// This operation is non-blocking for the calling process, as an identical 100 | /// child process is started to preserve the clipboard as long as necessary 101 | /// before continuing execution. The child process' memory is secured using 102 | /// [`proc::secure_mem`]. 103 | /// 104 | /// Returns a value indicating whether the current process is the child or 105 | /// parent. An expected usage pattern is to immediately end the child process 106 | /// without it performing any IO. 107 | pub fn clip_timed(text: &str, time: Duration) -> ResultForked { 108 | use clip::Clipboard; 109 | 110 | // SAFETY: Forking the process is completely safe because ours is 111 | // single-threaded. 112 | let proc_result = unsafe { 113 | // Since we will not modify memory allocated by the parent process, the 114 | // kernel should be able to apply COW optimisations, allowing for a low 115 | // performance penalty. 116 | proc::fork() 117 | }; 118 | 119 | let proc = match proc_result { 120 | Ok(p) => p, 121 | Err(e) => return (None, Err(Error::StartingProcess(e))) 122 | }; 123 | 124 | if proc == Process::Child { 125 | // TODO: use `try` blocks once available 126 | let result = (|| -> Result<()> { 127 | // The child process does not inherit the parent's memory 128 | // protections, so they must be reapplied. 129 | proc::secure_mem() 130 | .map_err(Error::SecuringMemory)?; 131 | 132 | Clipboard::new()? 133 | .hold(text, time)?; 134 | 135 | Ok(()) 136 | })(); 137 | 138 | (Some(proc), result.map_err(Error::from)) 139 | } else { 140 | (Some(proc), Ok(())) 141 | } 142 | } 143 | 144 | /// Applies 'f' to each element of `paths` and prints the result separated with 145 | /// empty lines. 146 | /// 147 | /// If `f` returns an error, it is printed and execution continues 148 | fn print_each_spaced(paths: Vec, f: F) 149 | where 150 | F: Fn(RecordPath) -> Result, 151 | D: Display 152 | { 153 | let mut paths = paths.into_iter(); 154 | 155 | if let Some(p) = paths.next() { 156 | match f(p) { 157 | Ok(d) => println!("{d}"), 158 | Err(e) => e.print_full() 159 | } 160 | 161 | for p in paths { 162 | println!(); 163 | 164 | match f(p) { 165 | Ok(d) => println!("{d}"), 166 | Err(e) => e.print_full() 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/util/file.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs, 3 | io, 4 | path 5 | }; 6 | 7 | use std::{ 8 | ffi::{OsStr, OsString}, 9 | path::{Path, PathBuf}, 10 | fs::File 11 | }; 12 | 13 | /// A path to a backed up file. 14 | /// 15 | /// Contains two paths, one to the file itself and one to its backup. Supports 16 | /// methods to create and handle the backup. 17 | pub struct SafePath { 18 | pub main: PathBuf, 19 | pub backup: PathBuf 20 | } 21 | 22 | /// Permissions with which to open a file. 23 | /// 24 | /// Similar to [`File::options`]. 25 | #[derive(Clone, Copy)] 26 | pub enum Mode { 27 | Read, 28 | ReadWrite, 29 | /// Fail if the file already exists. 30 | CreateWrite 31 | } 32 | 33 | pub type Error = io::Error; 34 | 35 | pub type Result = std::result::Result; 36 | 37 | impl SafePath { 38 | /// Constructs a new `SafePath`. 39 | pub fn new(file_path: P, backup_path: Q) -> Self 40 | where 41 | P: Into, 42 | Q: Into 43 | { 44 | Self { 45 | main: file_path.into(), 46 | backup: backup_path.into() 47 | } 48 | } 49 | 50 | /// Returns an object that implements `Display` for printing the main path, 51 | /// possibly performing lossy conversion, like [`std::path::Path::display`]. 52 | /// 53 | /// This is a convenience function equivalent to 54 | /// `SafePath::main().display()`. 55 | pub fn display(&self) -> path::Display { 56 | self.main.display() 57 | } 58 | 59 | /// Opens the main path in a manner determined by `mode`. 60 | pub fn open(&self, mode: Mode) -> Result { 61 | use Mode::*; 62 | 63 | let mut opts = File::options(); 64 | 65 | match mode { 66 | Read => opts.read(true), 67 | ReadWrite => opts.read(true).write(true), 68 | CreateWrite => opts.write(true).create_new(true) 69 | }.open(&self.main) 70 | } 71 | 72 | /// Backs up the file at `main`, copying it to `backup`. 73 | pub fn make_backup(&self) -> Result<()> { 74 | // ATOMICITY: Not atomic, but no data loss occurs on failure. 75 | fs::copy(&self.main, &self.backup)?; 76 | Ok(()) 77 | } 78 | 79 | /// Verifies if the file at `backup` exists. 80 | pub fn is_backed_up(&self) -> Result { 81 | self.backup.try_exists() 82 | } 83 | 84 | /// Recovers the file at `backup`, moving it to and replacing `main`. 85 | pub fn recover(&self) -> Result<()> { 86 | // ATOMICITY: same as with `backup()`. 87 | fs::rename(&self.backup, &self.main) 88 | } 89 | 90 | /// Removes the file at `main`. 91 | pub fn remove(&self) -> Result<()> { 92 | fs::remove_file(&self.main) 93 | } 94 | 95 | /// Removes the file at `backup`. 96 | pub fn remove_backup(&self) -> Result<()> { 97 | fs::remove_file(&self.backup) 98 | } 99 | } 100 | 101 | /// Returns a path to a file in `backup_dir` suitable for a backup of 102 | /// `file_path`. 103 | /// 104 | /// A file name is considered to be a path devoid of path separators. 105 | /// 106 | /// Does not create `backup_dir` or the returned file path if they do not exist. 107 | /// 108 | /// This function is guaranteed to map any two different paths (as `file_path`) 109 | /// with different absolute forms to two different file names. In other words, 110 | /// every possible input has a (functionally) unique output, so a name collision 111 | /// should not occur. This is only true for paths in their absolute and resolved 112 | /// forms (for example, the presence of symlinks may nullify these guarantees). 113 | pub fn backup_path_from(file_path: P, backup_dir: Q) -> PathBuf 114 | where 115 | P: AsRef, 116 | Q: Into 117 | { 118 | let backup_name = backup_name_from(file_path.as_ref()); 119 | let mut result = Into::::into(backup_dir); 120 | 121 | result.push(backup_name); 122 | result 123 | } 124 | 125 | /// Empties and resets `f`. 126 | /// 127 | /// Truncates `f` to a length of 0 and rewinds it to the beginning of the file. 128 | pub fn clear(f: &mut File) -> Result<()> { 129 | use std::io::Seek; 130 | 131 | f.set_len(0)?; 132 | f.rewind() 133 | } 134 | 135 | /// Returns a file name suitable for a backup of `file_path`. 136 | /// 137 | /// Same unicity conditions as [`file_name_from`]. 138 | fn backup_name_from(file_path: &Path) -> OsString { 139 | const BACKUP_EXTENSION: &str = ".bak"; 140 | 141 | let mut file_name = file_name_from(file_path); 142 | 143 | file_name.push(BACKUP_EXTENSION); 144 | 145 | file_name 146 | } 147 | 148 | /// Returns `path` as a file name. 149 | /// 150 | /// A file name is considered to be a path devoid of path separators. 151 | /// 152 | /// This function is guaranteed to map any two different paths to two different 153 | /// file names. In other words, every possible input has a unique output, so a 154 | /// name collision cannot occur. 155 | fn file_name_from(path: &Path) -> OsString { 156 | use path_absolutize::Absolutize; 157 | use std::os::unix::ffi::OsStrExt; 158 | 159 | const SEP_SUBSTITUTE: char = '%'; 160 | const SEP_SUBSTITUTE_STR: &str = "%"; 161 | const ESCAPED_SUBSTITUTE: &str = "%%"; 162 | 163 | // TODO: use `std::path::absolute` once available 164 | // It seems that `absolutize()` can never fail. 165 | let path = path.absolutize().unwrap(); 166 | let path = path.as_os_str(); 167 | 168 | // The length of the result is equal to that of `path` if the latter 169 | // contains no substitute characters. 170 | let mut result = OsString::with_capacity(path.len()); 171 | 172 | for &b in path.as_bytes() { 173 | if b == path::MAIN_SEPARATOR as u8 { 174 | result.push(SEP_SUBSTITUTE_STR); 175 | } else if b == SEP_SUBSTITUTE as u8 { 176 | result.push(ESCAPED_SUBSTITUTE); 177 | } else { 178 | result.push(OsStr::from_bytes(&[b])) 179 | } 180 | } 181 | 182 | result 183 | } 184 | -------------------------------------------------------------------------------- /src/util/crypt/mod.rs: -------------------------------------------------------------------------------- 1 | use super::secret::Secret; 2 | 3 | use chacha20poly1305::aead::stream; 4 | 5 | use chacha20poly1305::aead::KeyInit; 6 | 7 | // Authenticated encryption: allows us to detect if the key is invalid (and 8 | // therefore the password incorrect). 9 | use chacha20poly1305::{ 10 | XChaCha20Poly1305, 11 | aead::Payload 12 | }; 13 | 14 | use std::{ 15 | fmt, 16 | io 17 | }; 18 | 19 | use std::io::{ 20 | Read, 21 | Write 22 | }; 23 | 24 | use std::fmt::Display; 25 | 26 | pub mod header; 27 | pub mod key; 28 | 29 | pub use header::Header; 30 | pub use key::Key; 31 | 32 | /// The length in bytes of a block of data to encrypt at a time with stream 33 | /// encryption. This is approximately equivalent to the total amount of memory 34 | /// used by [`CryptCtx::encrypt`]. 35 | pub const PLAIN_BLOCK_LEN: usize = 0x1000; 36 | 37 | /// The length in bytes of a block of date decryptable into a plain data block 38 | /// with stream decryption. This is the length of a plain data block added to 39 | /// the length of the [Mac tag](https://en.wikipedia.org/wiki/VMAC). It 40 | /// represents the approximate minimum amount of memory used by 41 | /// [`CryptCtx::decrypt`]. 42 | pub const ENCRYPTED_BLOCK_LEN: usize = PLAIN_BLOCK_LEN + 0x10; 43 | 44 | /// Cryptographic context, containing necessary cryptographic data. 45 | /// 46 | /// Includes the private key, salt and nonce. 47 | pub struct CryptCtx<'k, 'h> { 48 | key: &'k Key, 49 | head: &'h Header 50 | } 51 | 52 | #[allow(clippy::enum_variant_names)] 53 | pub enum Error { 54 | EncryptingBlock, 55 | DecryptingBlock, 56 | WritingBlock(io::Error), 57 | ReadingBlock(io::Error), 58 | } 59 | 60 | pub type Result = std::result::Result; 61 | 62 | impl<'k, 'h> CryptCtx<'k, 'h> { 63 | /// Constructs a new `CryptCtx`. 64 | pub fn new(key: &'k Key, head: &'h Header) -> Self { 65 | Self { key, head } 66 | } 67 | 68 | /// Encrypts the data in `src` and writes it to `dest` block by block. 69 | /// 70 | /// Has no effect if `src` is empty. Plain data is never copied. 71 | pub fn encrypt(&self, mut src: &[u8], mut dest: D) -> Result<()> 72 | where 73 | D: Write 74 | { 75 | use stream::EncryptorBE32 as Cryptor; 76 | 77 | let cipher = XChaCha20Poly1305::new(self.key.as_slice().into()); 78 | let mut cryptor = Cryptor::from_aead(cipher, self.head.nonce().into()); 79 | 80 | while !src.is_empty() { 81 | let (block, at_last_block) = if src.len() <= PLAIN_BLOCK_LEN { 82 | (src, true) 83 | } else { 84 | (&src[..PLAIN_BLOCK_LEN], false) 85 | }; 86 | 87 | // Use the salt as the AAD. 88 | let payload = payload_with(block, self.head.salt()); 89 | 90 | // Unfortunately, `encrypt_next` allocated a new `Vec` for every 91 | // block decrypted, which may impact performance. However, a decent 92 | // allocator should reuse the same memory on every loop iteration, 93 | // so the performance impact may be minimal. 94 | let crypted_block = cryptor.encrypt_next(payload) 95 | .map_err(|_| Error::EncryptingBlock)?; 96 | 97 | dest.write_all(&crypted_block) 98 | .map_err(Error::WritingBlock)?; 99 | 100 | if at_last_block { 101 | break; 102 | } else { 103 | src = &src[PLAIN_BLOCK_LEN..]; // Advance by one chunk. 104 | } 105 | } 106 | 107 | Ok(()) 108 | } 109 | 110 | /// Encrypts the data in `src` block by block and returns it. 111 | /// 112 | /// Returns an empty `Vec` if `src` is empty. Clears plain data buffers 113 | /// using [`Erase::erase`][1] before returning, and clears the decrypted 114 | /// data if an error occurs. 115 | /// 116 | /// [1]: [`super::secret::Erase`] 117 | pub fn decrypt(&self, mut src: S) -> Result> 118 | where 119 | S: Read 120 | { 121 | use stream::DecryptorBE32 as Cryptor; 122 | 123 | let cipher = XChaCha20Poly1305::new(self.key.as_slice().into()); 124 | let mut cryptor = Cryptor::from_aead(cipher, self.head.nonce().into()); 125 | 126 | let mut result = Secret::new(Vec::::new()); 127 | let mut crypted_block = [0_u8; ENCRYPTED_BLOCK_LEN]; 128 | 129 | loop { 130 | let read_len = src.read(&mut crypted_block) 131 | .map_err(Error::ReadingBlock)?; 132 | 133 | if read_len == 0 { 134 | break; 135 | } 136 | 137 | let (block, at_last_block) = if read_len < crypted_block.len() { 138 | (&crypted_block[..read_len], true) 139 | } else { 140 | (crypted_block.as_slice(), false) 141 | }; 142 | 143 | // As with `encrypt`. 144 | let payload = payload_with(block, self.head.salt()); 145 | 146 | let decrypted_block = Secret::new( 147 | // As with `encrypt`. 148 | cryptor.decrypt_next(payload) 149 | .map_err(|_| Error::DecryptingBlock)? 150 | ); 151 | 152 | result.extend_from_slice(&decrypted_block); 153 | 154 | if at_last_block { 155 | break; 156 | } 157 | } 158 | 159 | Ok(result.into_inner()) 160 | } 161 | } 162 | 163 | impl Display for Error { 164 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 165 | use Error::*; 166 | 167 | match self { 168 | EncryptingBlock => write!(f, "cannot encrypt block"), 169 | DecryptingBlock => write!(f, "cannot decrypt block"), 170 | WritingBlock(e) => write!(f, "cannot write block: {e}"), 171 | ReadingBlock(e) => write!(f, "cannot read block: {e}"), 172 | } 173 | } 174 | } 175 | 176 | fn payload_with<'m, 'a>(msg: &'m [u8], aad: &'a [u8]) -> Payload<'m, 'a> { 177 | Payload { msg, aad } 178 | } 179 | -------------------------------------------------------------------------------- /src/util/user_io/mod.rs: -------------------------------------------------------------------------------- 1 | use nix::sys::termios; 2 | 3 | use nix::sys::termios::{ 4 | Termios, 5 | LocalFlags 6 | }; 7 | 8 | use std::{fmt, io}; 9 | 10 | use std::{ 11 | io::ErrorKind::UnexpectedEof, 12 | io::Stdin 13 | }; 14 | 15 | pub mod style; 16 | 17 | pub use style::Style; 18 | 19 | /// Prints a formatted message to standard error with a newline, as an error. 20 | #[macro_export] 21 | macro_rules! err { 22 | ($($arg:tt)*) => {{ 23 | use $crate::util::user_io::Style; 24 | use ::std::{eprintln, format_args}; 25 | 26 | eprintln!( 27 | "{} {}", 28 | "error:".as_error(), 29 | format_args!($($arg)*) 30 | ); 31 | }} 32 | } 33 | 34 | /// Prints a formatted message to standard error with a newline, as a warning. 35 | #[macro_export] 36 | macro_rules! warn { 37 | ($($arg:tt)*) => {{ 38 | use $crate::util::user_io::Style; 39 | use ::std::{eprintln, format_args}; 40 | 41 | eprintln!( 42 | "{} {}", 43 | "warning:".as_warning(), 44 | format_args!($($arg)*) 45 | ); 46 | }} 47 | } 48 | 49 | /// Prints a formatted message to standard error with a newline, as a notice. 50 | #[macro_export] 51 | macro_rules! info { 52 | ($($arg:tt)*) => {{ 53 | use $crate::util::user_io::Style; 54 | use ::std::{eprintln, format_args}; 55 | 56 | eprintln!( 57 | "{} {}", 58 | "notice:".as_notice(), 59 | format_args!($($arg)*) 60 | ); 61 | }} 62 | } 63 | 64 | /// Reads input from the user. 65 | /// 66 | /// This macro has two forms. If arguments are passed, it displays them as a 67 | /// formatted prompt to standard error. Otherwise, it displays a simple 68 | /// graphical prompt. In both cases, it will then read a line from standard 69 | /// input. 70 | /// 71 | /// Returns the (possible empty) read input stripped of the trailing newline. 72 | #[macro_export] 73 | macro_rules! input { 74 | () => {{ 75 | use $crate::util::user_io::Style; 76 | use $crate::util::user_io::get_line; 77 | 78 | use ::std::format_args; 79 | 80 | get_line(format_args!( 81 | "{} ", 82 | ">".as_prompt(), 83 | )) 84 | }}; 85 | 86 | ($($arg:tt)*) => {{ 87 | use $crate::util::user_io::Style; 88 | use $crate::util::user_io::get_line; 89 | 90 | use ::std::format_args; 91 | 92 | get_line(format_args!( 93 | "{} {}", 94 | "::".as_prompt(), 95 | format_args!($($arg)*) 96 | )) 97 | }} 98 | } 99 | 100 | /// Displays a formatted prompt to standard error with an appended suffix 101 | /// indicating the possible responses and interprets user input. 102 | /// 103 | /// Returns true or false depending on whether or not the user confirmed 104 | /// the prompt. 105 | #[macro_export] 106 | macro_rules! confirm { 107 | ($($arg:tt)*) => {{ 108 | use $crate::util::user_io::Result; 109 | use $crate::input; 110 | 111 | use ::std::format_args; 112 | 113 | let result: Result = loop { 114 | let input_res = input!( 115 | "{} [y/n] ", 116 | format_args!($($arg)*) 117 | ); 118 | 119 | let line = match input_res { 120 | Ok(l) => l, 121 | Err(e) => break Result::Err(e) 122 | }; 123 | 124 | match line.as_str() { 125 | "y" => break Result::Ok(true), 126 | "n" => break Result::Ok(false), 127 | _ => continue 128 | } 129 | }; 130 | 131 | result 132 | }} 133 | } 134 | 135 | pub type Error = io::Error; 136 | 137 | pub type Result = std::result::Result; 138 | 139 | /// XXX: shows prompt on stderr and reads line non-empty (refuses EOF) 140 | /// prints a newline on stderr if EOF is received (further IO is on next line) 141 | /// line is returned without trailing newline 142 | pub fn get_line(prompt: fmt::Arguments) -> Result { 143 | eprint!("{prompt}"); 144 | 145 | let mut line = read_line().map_err(|e| { 146 | // Simulate a newline if the user closed the stream. 147 | if e.kind() == UnexpectedEof { eprintln!() }; 148 | 149 | e 150 | })?; 151 | 152 | // Remove the trailing newline entered by the user. `read_line()` always 153 | // returns a non-empty `String` so this must succeed. 154 | line.pop(); 155 | Ok(line) 156 | } 157 | 158 | /// XXX: reads stdin until EOF 159 | /// useful for reading piped input 160 | pub fn read_stdin() -> Result { 161 | use std::io::Read; 162 | 163 | let mut stdin = io::stdin().lock(); 164 | let mut result = String::new(); 165 | 166 | stdin.read_to_string(&mut result)?; 167 | 168 | Ok(result) 169 | } 170 | 171 | /// XXX: hides user input henceforth 172 | /// useful for reading sensitive data, like passwords 173 | pub fn hide_input() -> Result<()> { 174 | mutate_termios(io::stdin(), |term| { 175 | term.local_flags.remove(LocalFlags::ECHO); 176 | }) 177 | } 178 | 179 | /// XXX: shows user input henceforth 180 | pub fn show_input() -> Result<()> { 181 | mutate_termios(io::stdin(), |term| { 182 | term.local_flags.insert(LocalFlags::ECHO); 183 | }) 184 | } 185 | 186 | /// Reads a line from standard input. 187 | /// 188 | /// The returned [`String`] is never empty, and contains a trailing newline. 189 | /// Receiving EOF is considered an error. 190 | fn read_line() -> Result { 191 | let mut input = String::new(); 192 | 193 | let read_len = io::stdin().read_line(&mut input)?; 194 | 195 | if read_len > 0 { 196 | Ok(input) 197 | } else { 198 | Err(UnexpectedEof.into()) 199 | } 200 | } 201 | 202 | /// XXX 203 | /// mutates termios of `fd` (usually stdin/stdout/stderr) with `op` 204 | /// changes are applied immediately (`TCSANOW`) 205 | // We do not use a struct with Termios data because it is a global resource, and 206 | // may be modified through other means which we cannot control 207 | fn mutate_termios(f: Stdin, op: O) -> io::Result<()> 208 | where 209 | O: FnOnce(&mut Termios) 210 | { 211 | use termios::SetArg; 212 | use termios::{tcgetattr, tcsetattr}; 213 | 214 | let mut term = tcgetattr(&f)?; 215 | 216 | op(&mut term); 217 | tcsetattr(&f, SetArg::TCSANOW, &term)?; 218 | 219 | Ok(()) 220 | } 221 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | env, 3 | backup, 4 | input_pw, 5 | serial, 6 | find 7 | }; 8 | 9 | use crate::{ 10 | err, 11 | warn 12 | }; 13 | 14 | use crate::env::PROGNAME; 15 | 16 | use crate::util::file::SafePath; 17 | 18 | use crate::util::{ 19 | user_io, 20 | crypt, 21 | file, 22 | proc, 23 | clip, 24 | record 25 | }; 26 | 27 | use std::{ 28 | fmt, 29 | io 30 | }; 31 | 32 | use std::fmt::Display; 33 | 34 | /// Program errors. 35 | pub enum Error { 36 | Environment(env::Error), 37 | ReadingInput(user_io::Error), 38 | InputPw(input_pw::Error), 39 | 40 | ReadingHeader(crypt::header::Error), 41 | WritingHeader(crypt::header::Error), 42 | Crypt(crypt::Error), 43 | OpeningFile(file::Error, file::Mode, SafePath), 44 | ReadingStdin(user_io::Error), 45 | FileSerial(serial::Error), 46 | InputSerial(serial::Error), 47 | 48 | FindingRecord(find::Error), 49 | /// name of the record, and the group to which it is added 50 | AddingRecord(record::Error, String, String), 51 | SerialisingRecord(serial::Error), 52 | 53 | Clipboard(clip::Error), 54 | SecuringMemory(proc::Error), 55 | ExposingMemory(proc::Error), 56 | StartingProcess(proc::Error), 57 | 58 | RecoveringBackup(backup::Error, SafePath), 59 | MakingBackup(file::Error, SafePath), 60 | ClearingFile(file::Error), 61 | RemovingFile(file::Error, SafePath), 62 | RemovingBackup(file::Error, SafePath), 63 | } 64 | 65 | pub type Result = std::result::Result; 66 | 67 | /// User advice messages. 68 | /// 69 | /// Typically recommending actions to be manually done by the user, if failed by 70 | /// the program. 71 | pub enum Advice { 72 | ViewingUsage, 73 | CreatingFile, 74 | SpecifyingFile, 75 | MovingBackup, 76 | RemovingBackup, 77 | RecoveringBackup, 78 | RemovingFile, 79 | InvalidFile, 80 | IncorrectPassword, 81 | InvalidInput 82 | } 83 | 84 | impl Error { 85 | /// XXX: also prints advice if applicable 86 | pub fn print_full(self) { 87 | err!("{self}"); 88 | 89 | if let Some(a) = self.advice() { 90 | eprintln!("{a}"); 91 | } 92 | } 93 | 94 | /// XXX: also prints advice if applicable 95 | pub fn warn_full(self) { 96 | warn!("{self}"); 97 | 98 | if let Some(a) = self.advice() { 99 | eprintln!("{a}"); 100 | } 101 | } 102 | 103 | /// Returns user advice relevant to this error if applicable. 104 | pub fn advice(&self) -> Option { 105 | use Error::*; 106 | use env::Error::*; 107 | use backup::Error::{RemovalRefusal, File, Removal}; 108 | use crypt::Error::DecryptingBlock; 109 | 110 | use file::Mode::CreateWrite; 111 | 112 | use io::ErrorKind::{UnexpectedEof, NotFound}; 113 | 114 | Some(match self { 115 | Environment(ParsingArgs(..)) => 116 | Advice::ViewingUsage, 117 | Environment(ResolvingDataPath(..)) => 118 | Advice::SpecifyingFile, 119 | ReadingHeader(e) if e.kind() == UnexpectedEof => 120 | Advice::InvalidFile, 121 | Crypt(DecryptingBlock) => 122 | Advice::IncorrectPassword, 123 | FileSerial(..) => 124 | Advice::InvalidFile, 125 | InputSerial(..) => 126 | Advice::InvalidInput, 127 | 128 | RecoveringBackup(RemovalRefusal, ..) => 129 | Advice::MovingBackup, 130 | RecoveringBackup(File(e) | Removal(e), ..) if e.kind() != NotFound => 131 | Advice::RecoveringBackup, 132 | OpeningFile(_, CreateWrite, ..) => 133 | Advice::SpecifyingFile, 134 | OpeningFile(e, ..) if e.kind() == NotFound => 135 | Advice::CreatingFile, 136 | RemovingFile(e, ..) if e.kind() != NotFound => 137 | Advice::RemovingFile, 138 | RemovingBackup(..) => 139 | Advice::RemovingBackup, 140 | 141 | _ => return None 142 | }) 143 | } 144 | } 145 | 146 | impl Display for Error { 147 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 148 | use Error::*; 149 | use file::Mode; 150 | 151 | match self { 152 | Environment(e) => 153 | write!(f, "{e}"), 154 | ReadingInput(e) => 155 | write!(f, "{e}"), 156 | InputPw(e) => 157 | write!(f, "{e}"), 158 | 159 | ReadingHeader(e) => 160 | write!(f, "cannot read file header: {e}"), 161 | WritingHeader(e) => 162 | write!(f, "cannot write header to file: {e}"), 163 | Crypt(e) => 164 | write!(f, "{e}"), 165 | OpeningFile(e, mode, p) => match mode { 166 | Mode::CreateWrite => 167 | write!(f, "cannot create '{}': {e}", p.display()), 168 | _ => 169 | write!(f, "cannot open '{}': {e}", p.display()) 170 | } 171 | ReadingStdin(e) => 172 | write!(f, "cannot read stdin: {e}"), 173 | FileSerial(e) => 174 | write!(f, "invalid file contents: {e}"), 175 | InputSerial(e) => 176 | write!(f, "invalid input: {e}"), 177 | 178 | FindingRecord(e) => 179 | write!(f, "{e}"), 180 | AddingRecord(e, name, dest) => 181 | write!(f, "cannot create '{name}' in '{dest}': {e}"), 182 | SerialisingRecord(e) => 183 | write!(f, "{e}"), 184 | 185 | Clipboard(e) => 186 | write!(f, "{e}"), 187 | SecuringMemory(e) => 188 | write!(f, "cannot secure process memory: {e}"), 189 | ExposingMemory(e) => 190 | write!(f, "cannot disable process memory protections: {e}"), 191 | StartingProcess(e) => 192 | write!(f, "cannot start clipboard process: {e}"), 193 | 194 | RecoveringBackup(e, p) => 195 | write!(f, "cannot recover backup '{}': {e}", p.backup.display()), 196 | MakingBackup(e, p) => 197 | write!(f, "cannot backup '{}': {e}", p.display()), 198 | ClearingFile(e) => 199 | write!(f, "cannot clear pass file: {e}"), 200 | RemovingFile(e, p) => 201 | write!(f, "cannot recover backup '{}': {e}", p.backup.display()), 202 | RemovingBackup(e, p) => 203 | write!(f, "cannot remove backup '{}': {e}", p.backup.display()) 204 | } 205 | } 206 | } 207 | 208 | impl From for Error { 209 | fn from(e: env::Error) -> Self { 210 | Self::Environment(e) 211 | } 212 | } 213 | 214 | impl From for Error { 215 | fn from(e: input_pw::Error) -> Self { 216 | Self::InputPw(e) 217 | } 218 | } 219 | 220 | impl From for Error { 221 | fn from(e: crypt::Error) -> Self { 222 | Self::Crypt(e) 223 | } 224 | } 225 | 226 | impl From for Error { 227 | fn from(e: serial::Error) -> Self { 228 | Self::FileSerial(e) 229 | } 230 | } 231 | 232 | impl From for Error { 233 | fn from(e: find::Error) -> Self { 234 | Self::FindingRecord(e) 235 | } 236 | } 237 | 238 | impl From for Error { 239 | fn from(e: clip::Error) -> Self { 240 | Self::Clipboard(e) 241 | } 242 | } 243 | 244 | impl From for Error { 245 | fn from(e: user_io::Error) -> Self { 246 | Self::ReadingInput(e) 247 | } 248 | } 249 | 250 | impl Display for Advice { 251 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 252 | use Advice::*; 253 | 254 | match self { 255 | ViewingUsage => 256 | write!(f, "Try '{PROGNAME} -h' for more information."), 257 | SpecifyingFile => 258 | write!(f, "Try '{PROGNAME} -f' to specify a pass file."), 259 | CreatingFile => 260 | write!(f, "Try '{PROGNAME} -C' to create a pass file."), 261 | RemovingBackup => 262 | write!(f, "Try manually removing the backup file."), 263 | RecoveringBackup => 264 | write!(f, "Try manually recovering the backup file."), 265 | RemovingFile => 266 | write!(f, "Try manually removing the file."), 267 | MovingBackup => 268 | write!(f, "Rename or move the backup file to continue anyway."), 269 | InvalidFile => 270 | write!(f, "The pass file may be invalid."), 271 | InvalidInput => 272 | // TODO: point to ron documentation/examples or something 273 | write!(f, "The input format might be invalid."), 274 | IncorrectPassword => 275 | write!(f, "The entered password may be incorrect.") 276 | } 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/find.rs: -------------------------------------------------------------------------------- 1 | use MatchKind::{Fuzzy, Exact}; 2 | 3 | use crate::config::DEFAULT_ITEM; 4 | 5 | use crate::util::record; 6 | 7 | use crate::util::record::{ 8 | Record, Group, Item, 9 | Node 10 | }; 11 | 12 | use std::fmt; 13 | 14 | use std::fmt::Display; 15 | 16 | use std::rc::Rc; 17 | 18 | /// XXX: an empty path is root 19 | #[derive(Debug)] 20 | pub struct RecordPath(String); 21 | 22 | /// The method by which a record is searched for. 23 | #[derive(Default, Clone, Copy, PartialEq, Eq)] 24 | pub enum MatchKind { 25 | /// Matches each element of the path to record names using a fuzzy matching 26 | /// algorithm. 27 | #[default] 28 | Fuzzy, 29 | /// Matches each element of the path to record names. 30 | Exact 31 | } 32 | 33 | #[derive(Debug)] 34 | #[allow(clippy::enum_variant_names)] 35 | pub enum Error { 36 | // Clippy recommends we wrap `record::Error` in a `Box` because of its size. 37 | /// Could not find record matching `pat` in group of name `in_group`. 38 | NotFound { e: Box, pat: String, in_group: String }, 39 | /// Expected a group, but got `rec` matching `pat` instead. 40 | /// 41 | /// `pat` is None if the root group was matched, or if an exact match was 42 | /// found (in which case the pattern was the name). 43 | NotAGroup { name: String, pat: Option }, 44 | /// Expected an item, but got `rec` matching `pat` instead. 45 | NotAnItem { name: String, pat: Option } 46 | } 47 | 48 | pub type Result = std::result::Result; 49 | 50 | pub type SplitResult = std::result::Result<(T, T), T>; 51 | 52 | impl RecordPath { 53 | pub const DELIM: char = '.'; 54 | 55 | pub fn iter(&self) -> impl Iterator { 56 | self.0 57 | .split(Self::DELIM) 58 | .filter(|e| !e.is_empty()) // Ignore extraneous delimiters. 59 | } 60 | 61 | /// if self is not root, returns its parent path, and the trailing path 62 | /// element. returned trailing element won't have any delimiters 63 | /// if self is root, returns err and does not modify 64 | /// if self only has 1 element, leading will be empty and trailing will 65 | /// contain it 66 | pub fn split_last(mut self) -> SplitResult { 67 | let trailing = match self.iter().last() { 68 | Some(e) => RecordPath::from(e.to_owned()), 69 | None => return Err(self) 70 | }; 71 | 72 | self.strip_last(); 73 | 74 | Ok((self, trailing)) 75 | } 76 | 77 | pub fn into_inner(self) -> String { 78 | self.0 79 | } 80 | 81 | pub fn find_in( 82 | &self, 83 | rec: &Node, 84 | mk: MatchKind 85 | ) -> Result> { 86 | let found = self.find_rec_in(rec, mk)?; 87 | 88 | Ok(found.rec) 89 | } 90 | 91 | pub fn find_group_in( 92 | &self, 93 | rec: &Node, 94 | mk: MatchKind 95 | ) -> Result> { 96 | let FoundRecord { rec, matched_pat } = self.find_rec_in(rec, mk)?; 97 | let rec = rec.borrow(); 98 | 99 | match &*rec { 100 | Record::Group(g) => Ok(Rc::clone(g)), 101 | 102 | Record::Item(i) => Err(Error::NotAGroup { 103 | name: i.borrow().name().to_owned(), 104 | pat: matched_pat 105 | }) 106 | } 107 | } 108 | 109 | pub fn find_item_in( 110 | &self, 111 | rec: &Node, 112 | mk: MatchKind 113 | ) -> Result> { 114 | let FoundRecord { rec, matched_pat } = self.find_rec_in(rec, mk)?; 115 | let rec = rec.borrow(); 116 | 117 | match &*rec { 118 | Record::Group(g) => Err(Error::NotAnItem { 119 | name: g.borrow().name().to_owned(), 120 | pat: matched_pat 121 | }), 122 | 123 | Record::Item(i) => Ok(Rc::clone(i)) 124 | } 125 | } 126 | 127 | /// XXX: 128 | /// if item is found, return that 129 | /// if group is found, return `DEFAULT_ITEM` directly inside it if it exists 130 | pub fn find_item_or_default_in( 131 | &self, 132 | rec: &Node, 133 | mk: MatchKind 134 | ) -> Result> { 135 | let found = self.find_in(rec, mk)?; 136 | let found_ref = &*found.borrow(); 137 | 138 | match found_ref { 139 | Record::Group(_) => RecordPath::from(DEFAULT_ITEM) 140 | .find_item_in(&found, Exact), 141 | 142 | Record::Item(i) => Ok(Rc::clone(i)) 143 | } 144 | } 145 | } 146 | 147 | impl> From for RecordPath { 148 | fn from(s: S) -> Self { 149 | Self(s.into()) 150 | } 151 | } 152 | 153 | impl Display for RecordPath { 154 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 155 | self.0.fmt(f) 156 | } 157 | } 158 | 159 | impl MatchKind { 160 | pub fn from_str(s: &str) -> Option { 161 | // "exact" and "fuzzy" are completely distinct strings, so the following 162 | // won't have unexpected results. 163 | if "exact".starts_with(s) { 164 | Some(Exact) 165 | } else if "fuzzy".starts_with(s) { 166 | Some(Fuzzy) 167 | } else { 168 | None 169 | } 170 | } 171 | } 172 | 173 | impl Display for MatchKind { 174 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 175 | match self { 176 | Fuzzy => f.write_str("fuzzy"), 177 | Exact => f.write_str("exact") 178 | } 179 | } 180 | } 181 | 182 | impl Display for Error { 183 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 184 | use Error::*; 185 | 186 | match self { 187 | NotFound { e, pat, in_group } => 188 | write!(f, "'{pat}' in group '{in_group}': {e}"), 189 | 190 | // No need to specify the pattern if it is equal to the record name. 191 | NotAGroup { name, pat } => match pat { 192 | Some(pat) => write!(f, "'{pat}': '{name}' is not a group"), 193 | None => write!(f, "'{name}' is not a group") 194 | } 195 | 196 | // No need to specify the pattern if it is equal to the record name. 197 | NotAnItem { name, pat } => match pat { 198 | Some(pat) => write!(f, "'{pat}': '{name}' is not an item"), 199 | None => write!(f, "'{name}' is not an item") 200 | } 201 | } 202 | } 203 | } 204 | 205 | struct FoundRecord { 206 | rec: Node, 207 | /// None if `rec` is root. 208 | matched_pat: Option 209 | } 210 | 211 | impl RecordPath { 212 | /// Finds a record matching the target within `rec` or its children. 213 | fn find_rec_in( 214 | &self, 215 | rec: &Node, 216 | mk: MatchKind 217 | ) -> Result { 218 | let mut rec = Rc::clone(rec); 219 | let mut matched_pat = Option::<&str>::None; 220 | 221 | for pat in self.iter().peekable() { 222 | let found = match &*rec.borrow() { 223 | Record::Group(g) => match mk { 224 | Fuzzy => g.borrow().get_fuzzy(pat), 225 | Exact => g.borrow().get(pat) 226 | }.map_err(|e| Error::NotFound { 227 | e: Box::new(e), 228 | pat: pat.to_owned(), 229 | in_group: g.borrow().name().to_owned() 230 | })?, 231 | 232 | Record::Item(i) => return Err(Error::NotAGroup { 233 | name: i.borrow().name().to_owned(), 234 | pat: match mk { 235 | Fuzzy => Some(pat.to_owned()), 236 | Exact => None 237 | } 238 | }) 239 | }; 240 | 241 | rec = found; 242 | matched_pat = Some(pat); 243 | } 244 | 245 | let matched_pat = matched_pat 246 | .filter(|_| mk != Exact) // The pattern equals the record name. 247 | .map(ToOwned::to_owned); 248 | 249 | Ok(FoundRecord { rec, matched_pat }) 250 | } 251 | } 252 | 253 | impl RecordPath { 254 | /// if self is not root, removes its last path element 255 | /// (effectively turning it into its parent path) 256 | /// if it is root, removes any extraneous delimiters if they exist 257 | fn strip_last(&mut self) { 258 | // TODO: make this code cleaner if possible 259 | 260 | // Remove the extraneous delimiters if they exist to access the 261 | // trailing path element. 262 | if let Some(Self::DELIM) = self.last_char() { 263 | while let Some(Self::DELIM) = self.last_char() { 264 | self.0.pop(); 265 | } 266 | } 267 | 268 | // TODO: replace with `while self.last_char().is_some_and(|c| c != Self::DELIM)` 269 | // or let chains when available 270 | 271 | // Remove the trailing path element. 272 | while let Some(c) = self.last_char() { 273 | if c == Self::DELIM { break; } 274 | self.0.pop(); 275 | } 276 | } 277 | 278 | fn last_char(&self) -> Option { 279 | self.0.chars().last() 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /src/tui/cmd.rs: -------------------------------------------------------------------------------- 1 | use Cmd::{Read, Edit, Meta}; 2 | 3 | use crate::find::{MatchKind, RecordPath}; 4 | 5 | use std::{num, fmt}; 6 | 7 | use std::fmt::Display; 8 | 9 | use std::time::Duration; 10 | 11 | /// The command to be executed. 12 | pub enum Cmd { 13 | Read(ReadCmd), 14 | Edit(EditCmd), 15 | Meta(MetaCmd) 16 | } 17 | 18 | /// Obtaining or showing data. 19 | pub enum ReadCmd { 20 | Show(Vec), 21 | Clip(RecordPath), 22 | List(Option>), 23 | Tree(Option>), 24 | Export 25 | } 26 | 27 | /// Editing data. 28 | pub enum EditCmd { 29 | // Group and item operations. 30 | Remove { paths: Vec }, 31 | // TODO: these 2 should work like createitem, with split name 32 | Move { src: RecordPath, dest: RecordPath }, // XXX: also used for renaming, possible also root 33 | Copy { src: RecordPath, dest: RecordPath }, 34 | // Group operations. 35 | CreateGroup { paths: Vec }, 36 | // Item operations 37 | CreateItem { paths: Vec }, // XXX: this and changevalue 38 | // accept whitespace escapes 39 | // (multiline values) for input 40 | ChangeValue { paths: Vec }, 41 | } 42 | 43 | /// TUI management and information. 44 | pub enum MetaCmd { 45 | /// XXX: only affects temporary runtime conf 46 | SetOpt(OptVal), 47 | ShowConfig, 48 | ShowUsage(Option>), 49 | /// Exiting the TUI and saving. 50 | Exit, 51 | /// Exiting the TUI without saving. 52 | Abort 53 | } 54 | 55 | /// A `RecordPath` split into its parent group and its name. 56 | /// 57 | /// `name` is guaranteed not to contain any delimiters (valid record name) 58 | pub struct SplitPath { 59 | pub group: RecordPath, 60 | pub name: String 61 | } 62 | 63 | #[derive(Clone, Copy)] 64 | pub enum OptVal { 65 | ClipTime(Duration), 66 | MatchKind(MatchKind) 67 | } 68 | 69 | /// Non-algebraic [`Cmd`] for parsing and validation. 70 | #[derive(Clone, Copy)] 71 | pub enum CmdVerb { 72 | Show, 73 | Clip, 74 | List, 75 | Tree, 76 | Export, 77 | 78 | Remove, 79 | Move, 80 | Copy, 81 | CreateItem, 82 | CreateGroup, 83 | ChangeValue, 84 | 85 | SetOption, 86 | ShowConfig, 87 | ShowUsage, 88 | Exit, 89 | Abort 90 | } 91 | 92 | pub enum Error { 93 | InvalidInput(shell_words::ParseError), 94 | InvalidCmd(String), 95 | MissingArg, 96 | ExtraArg(String), 97 | InvalidArg(String), 98 | InvalidName(RecordPath), 99 | InvalidIntArg(String, num::ParseIntError) 100 | } 101 | 102 | pub type Result = std::result::Result; 103 | 104 | impl Cmd { 105 | /// returns None if no command ('s' empty) 106 | pub fn from_str(s: &str) -> Result> { 107 | let mut words = shell_words::split(s) 108 | .map_err(Error::InvalidInput)?; 109 | 110 | if words.is_empty() { 111 | Ok(None) 112 | } else { 113 | Ok(Some(Self::from_parts( 114 | CmdVerb::from_str(&words.remove(0))?, 115 | words 116 | )?)) 117 | } 118 | } 119 | } 120 | 121 | // TODO: some sort of advice "try 'help' for more info" 122 | impl Display for Error { 123 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 124 | use Error::*; 125 | 126 | match self { 127 | InvalidInput(e) => write!(f, "invalid input: {e}"), 128 | InvalidCmd(c) => write!(f, "invalid command '{c}'"), 129 | MissingArg => write!(f, "missing argument"), 130 | ExtraArg(a) => write!(f, "extra argument '{a}'"), 131 | InvalidName(r) => write!(f, "invalid record name '{r}'"), 132 | InvalidArg(r) => write!(f, "invalid argument '{r}'"), 133 | InvalidIntArg(a, e) => write!(f, "invalid argument '{a}': {e}") 134 | } 135 | } 136 | } 137 | 138 | impl Cmd { 139 | /// XXX: args can be empty 140 | fn from_parts(verb: CmdVerb, args: Vec) -> Result { 141 | use CmdVerb::*; 142 | 143 | let have_args = !args.is_empty(); 144 | let mut args = verb.check_args(args)?.into_iter(); 145 | 146 | Ok(match verb { 147 | Export => Read(ReadCmd::Export), 148 | Exit => Meta(MetaCmd::Exit), 149 | Abort => Meta(MetaCmd::Abort), 150 | ShowConfig => Meta(MetaCmd::ShowConfig), 151 | 152 | Clip => Read(ReadCmd::Clip(next_into(args))), 153 | ChangeValue => Edit(EditCmd::ChangeValue { paths: into_collect(args) }), 154 | Show => Read(ReadCmd::Show(into_collect(args))), 155 | Remove => Edit(EditCmd::Remove { paths: into_collect(args) }), 156 | 157 | // By splitting the name from a path element, we guarantee that it 158 | // is valid as a new record name (doesn't contain separators). 159 | CreateGroup => Edit(EditCmd::CreateGroup { 160 | paths: split_each(args.map(RecordPath::from))? 161 | }), 162 | CreateItem => Edit(EditCmd::CreateItem { 163 | paths: split_each(args.map(RecordPath::from))? 164 | }), 165 | 166 | List => Read(ReadCmd::List(match have_args { 167 | true => Some(into_collect(args)), 168 | false => None, 169 | })), 170 | Tree => Read(ReadCmd::Tree(match have_args { 171 | true => Some(into_collect(args)), 172 | false => None, 173 | })), 174 | ShowUsage => Meta(MetaCmd::ShowUsage(match have_args { 175 | true => Some( 176 | args.map(|a| CmdVerb::from_str(&a)) 177 | .collect::>>()? 178 | ), 179 | false => None, 180 | })), 181 | 182 | Move => Edit(EditCmd::Move { 183 | src: next_into(&mut args), 184 | dest: next_into(&mut args) 185 | }), 186 | Copy => Edit(EditCmd::Copy { 187 | src: next_into(&mut args), 188 | dest: next_into(&mut args) 189 | }), 190 | SetOption => Meta(MetaCmd::SetOpt(OptVal::new( 191 | next_into(&mut args), 192 | next_into(&mut args), 193 | )?)) 194 | }) 195 | } 196 | } 197 | 198 | impl OptVal { 199 | fn new(name: String, val: String) -> Result { 200 | Ok(match name.as_str() { 201 | "ct" | "clip-time" => Self::ClipTime(Duration::from_secs( 202 | val.parse::() 203 | .map_err(|e| Error::InvalidIntArg(val, e))? 204 | )), 205 | 206 | "mk" | "match-kind" => Self::MatchKind( 207 | MatchKind::from_str(&val) 208 | .ok_or(Error::InvalidArg(val))? 209 | ), 210 | 211 | _ => return Err(Error::InvalidArg(name)) 212 | }) 213 | } 214 | } 215 | 216 | impl CmdVerb { 217 | fn from_str(s: &str) -> Result { 218 | use CmdVerb::*; 219 | 220 | Ok(match s { 221 | "sh" | "show" => Show, 222 | "cl" | "clip" => Clip, 223 | "ls" | "list" => List, 224 | "tr" | "tree" => Tree, 225 | "ex" | "export" => Export, 226 | 227 | "rm" | "remove" => Remove, 228 | "mv" | "move" => Move, 229 | "cp" | "copy" => Copy, 230 | "mg" | "mkgrp" => CreateGroup, 231 | "mi" | "mkitm" => CreateItem, 232 | "cv" | "chval" => ChangeValue, 233 | 234 | "so" | "setopt" => SetOption, 235 | "sc" | "showconf" => ShowConfig, 236 | "hp" | "help" => ShowUsage, 237 | "et" | "exit" => Exit, 238 | "at" | "abort" => Abort, 239 | 240 | other => return Err(Error::InvalidCmd(other.to_owned())) 241 | }) 242 | } 243 | 244 | // TODO: perhaps equivalent method in env 245 | // env could generally be cleaner like here 246 | /// we have to do this because some commands accept varying numbers of args 247 | /// (like `List`) 248 | /// returns `a` unchanged 249 | fn check_args(self, a: Vec) -> Result> { 250 | use CmdVerb::*; 251 | use Error::{MissingArg, ExtraArg}; 252 | 253 | match self { 254 | Export | Exit | Abort | ShowConfig => 255 | if a.is_empty() { Ok(a) } else { Err(ExtraArg(take(a, 0))) } 256 | 257 | Clip => match a.len() { 258 | 1 => Ok(a), 259 | 0 => Err(MissingArg), 260 | _ => Err(Error::ExtraArg(take(a, 1))) 261 | } 262 | 263 | Move | Copy | SetOption => match a.len() { 264 | 2 => Ok(a), 265 | 1 | 0 => Err(MissingArg), 266 | _ => Err(ExtraArg(take(a, 2))) 267 | } 268 | 269 | Show | CreateGroup | CreateItem | ChangeValue | Remove => 270 | if !a.is_empty() { Ok(a) } else { Err(MissingArg) } 271 | 272 | List | Tree | ShowUsage => Ok(a) 273 | } 274 | } 275 | } 276 | 277 | fn split_each(paths: I) -> Result> 278 | where 279 | I: Iterator 280 | { 281 | paths.map(|path| { 282 | // If the path only contains one element, root will be taken as the 283 | // leading path. 284 | let (leading, trailing) = path 285 | .split_last() 286 | .map_err(Error::InvalidName)?; 287 | 288 | Ok(SplitPath { 289 | group: leading, 290 | name: trailing.into_inner() 291 | }) 292 | }).collect() 293 | } 294 | 295 | fn into_collect(iter: impl Iterator) -> Vec 296 | where 297 | I: Into 298 | { 299 | iter.map(Into::::into).collect() 300 | } 301 | 302 | /// Panics XXX 303 | fn next_into(mut iter: impl Iterator) -> J 304 | where 305 | I: Into 306 | { 307 | iter.next().unwrap().into() 308 | } 309 | 310 | /// Moves value at `idx` out `v` and returns it. 311 | /// 312 | /// Panics if `idx` is out of bounds of `v`. 313 | fn take(v: Vec, idx: usize) -> T { 314 | v.into_iter().nth(idx).unwrap() 315 | } 316 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // TODO: module comments ? (use //!, with short title description) 2 | // TODO: decent source documentation for most things 3 | 4 | #[path = "../config.rs"] 5 | mod config; 6 | 7 | mod error; 8 | mod env; 9 | mod backup; 10 | mod input_pw; 11 | mod serial; 12 | mod find; 13 | mod tui; 14 | mod output; 15 | mod util; 16 | 17 | use error::{Error, Result}; 18 | use env::prelude::*; 19 | 20 | use util::{ 21 | file, 22 | user_io, 23 | proc, 24 | }; 25 | 26 | use util::{ 27 | file::{SafePath, Mode}, 28 | record::Record, 29 | secret::Secret 30 | }; 31 | 32 | use util::crypt::{CryptCtx, Header, Key}; 33 | 34 | use std::{ 35 | process::ExitCode, 36 | fs::File 37 | }; 38 | 39 | fn main() -> ExitCode { 40 | let result = Cmd::from_env() 41 | .map_err(Error::from) 42 | .and_then(Cmd::exec); 43 | 44 | match result { 45 | Ok(()) => 46 | ExitCode::SUCCESS, 47 | 48 | Err(e) => { 49 | e.print_full(); 50 | ExitCode::FAILURE 51 | } 52 | } 53 | } 54 | 55 | impl Cmd { 56 | fn exec(self) -> Result<()> { 57 | use Cmd::*; 58 | 59 | match self { 60 | ShowUsage(usg) => println!("{usg}"), 61 | ShowVersion(ver) => println!("{ver}"), 62 | HandleFile(cmd, path) => cmd.exec(path)?, 63 | } 64 | 65 | Ok(()) 66 | } 67 | } 68 | 69 | impl FileCmd { 70 | fn exec(self, path: SafePath) -> Result<()> { 71 | use FileCmd::*; 72 | 73 | if let Err(e) = backup::maybe_recover(&path) { 74 | return Err(Error::RecoveringBackup(e, path)) 75 | } 76 | 77 | with_secured_mem(|| match self { 78 | Read(cmd) => cmd.exec(path), 79 | Change(cmd) => cmd.exec(path), 80 | Create(cmd) => cmd.exec(path) 81 | }) 82 | } 83 | } 84 | 85 | impl ReadCmd { 86 | fn exec(self, path: SafePath) -> Result<()> { 87 | use output::{PrintTarget, ClipTarget}; 88 | use ReadCmd::*; 89 | 90 | let data = Secret::new({ 91 | let (mut file, _) = open(Mode::Read, path)?; 92 | let (serial, _) = decrypt(&mut file)?; 93 | 94 | if let Export = self { 95 | let ir = Secret::new(serial::ir_from(&serial)?); 96 | 97 | println!("{}", *ir); 98 | return Ok(()); 99 | } else { 100 | serial::parse(&serial)? 101 | } 102 | }); 103 | 104 | match self { 105 | Show(paths, mk) => PrintTarget::new(paths, mk) 106 | .print_values(&data), 107 | 108 | Clip(path, mk, time) => { 109 | // It doesn't matter if this is the parent or child process, 110 | // because it is about to exit without further effects. 111 | let (_, result) = ClipTarget::new(path, mk, time) 112 | .clip(&data); 113 | 114 | result?; 115 | } 116 | 117 | List(opt_paths, mk) => match opt_paths { 118 | Some(paths) => PrintTarget::new(paths, mk) 119 | .print_lists(&data), 120 | None => println!("{}", Record::display_list(&data)) 121 | } 122 | 123 | Tree(opt_paths, mk) => match opt_paths { 124 | Some(paths) => PrintTarget::new(paths, mk) 125 | .print_trees(&data), 126 | None => println!("{}", Record::display_tree(&data)) 127 | } 128 | 129 | // Already handled. 130 | Export => unreachable!() 131 | } 132 | 133 | Ok(()) 134 | } 135 | } 136 | 137 | impl ChangeCmd { 138 | fn exec(self, path: SafePath) -> Result<()> { 139 | use backup::Error::File as RecoverError; 140 | use ChangeCmd::*; 141 | use tui::{Tui, Status}; 142 | 143 | let (mut file, path) = open(Mode::ReadWrite, path)?; 144 | let (serial, pw) = decrypt(&mut file)?; 145 | let mut proc_is_child = false; 146 | 147 | if let Err(e) = path.make_backup() { 148 | return Err(Error::MakingBackup(e, path)); 149 | } 150 | 151 | // TODO: use `try` blocks once available 152 | let result = || -> Result<()> { 153 | match self { 154 | Modify(config) => { 155 | let data = Secret::new(serial::parse(&serial)?); 156 | let mut tui = Tui::new(config); 157 | 158 | drop(serial); // Old serial data unneeded if changing. 159 | 160 | // TODO: maybe launch this in separate proc/thread so we can 161 | // catch ctrl-c and exit cleanly 162 | let result = tui.run(&data); 163 | 164 | if tui.status() == Status::Clipped { 165 | proc_is_child = true; 166 | } 167 | 168 | // This code won't be executed if `result` is `Err`. 169 | if tui.should_save_data() { 170 | let new_serial = Secret::new( 171 | serial::bytes_from(data) 172 | .map_err(Error::SerialisingRecord)? 173 | ); 174 | 175 | over_encrypt(&new_serial, file, |head| { 176 | Key::from_password(pw, head) 177 | .map_err(input_pw::Error::GeneratingKey) 178 | })?; 179 | } 180 | 181 | result 182 | } 183 | 184 | ChangePassword => { 185 | drop(pw); // Old password unneeded if we are changing it. 186 | 187 | over_encrypt(&serial, file, |head| { 188 | input_pw::confirm_to_key( 189 | head, 190 | "New password: ", 191 | "Confirm password: " 192 | ) 193 | })?; 194 | 195 | Ok(()) 196 | } 197 | } 198 | }(); 199 | 200 | // The parent process will take care of the backup. 201 | if proc_is_child { 202 | return result; 203 | } 204 | 205 | match &result { 206 | Ok(_) => if let Err(e) = path.remove_backup() { 207 | Error::RemovingBackup(e, path).warn_full(); 208 | } 209 | 210 | Err(_) => if let Err(e) = path.recover() { 211 | Error::RecoveringBackup(RecoverError(e), path).warn_full(); 212 | } 213 | } 214 | 215 | Ok(()) 216 | } 217 | } 218 | 219 | impl CreateCmd { 220 | fn exec(self, path: SafePath) -> Result<()> { 221 | use CreateCmd::*; 222 | 223 | let (file, path) = open(Mode::CreateWrite, path)?; 224 | 225 | // TODO: use `try` blocks once available 226 | let result = || -> Result<()> { 227 | let serial = match self { 228 | CreateEmpty(root_name) => { 229 | Secret::new(serial::new_empty(root_name)) 230 | } 231 | 232 | Import => { 233 | let input = Secret::new( 234 | user_io::read_stdin() 235 | .map_err(Error::ReadingStdin)? 236 | ); 237 | 238 | serial::validate(&input) 239 | .map_err(Error::InputSerial)?; 240 | 241 | input 242 | } 243 | }; 244 | 245 | over_encrypt(serial.as_bytes(), file, |head| { 246 | input_pw::confirm_to_key( 247 | head, 248 | "Password: ", 249 | "Confirm password: " 250 | ) 251 | }) 252 | }(); 253 | 254 | if result.is_err() { 255 | if let Err(e) = path.remove() { 256 | Error::RemovingFile(e, path).warn_full(); 257 | } 258 | } 259 | 260 | result 261 | } 262 | } 263 | 264 | /// Runs `op` in a context where the process address space is secured by 265 | /// [`proc::secure_mem`]. 266 | /// 267 | /// Returns the result of `op`, and prints a warning if [`proc::expose_mem`] 268 | /// failed after the execution of `op`. Failure to reverse the memory 269 | /// protections is not considered a fatal error. 270 | fn with_secured_mem(op: O) -> Result<()> 271 | where 272 | O: FnOnce() -> Result<()> 273 | { 274 | proc::secure_mem() 275 | .map_err(Error::SecuringMemory)?; 276 | 277 | let result = op(); 278 | 279 | if let Err(e) = proc::expose_mem() { 280 | Error::ExposingMemory(e).warn_full(); 281 | } 282 | 283 | result 284 | } 285 | 286 | /// Opens the main path of `path` with `mode`. 287 | /// 288 | /// Returns the opened file and the passed path unchanged. 289 | fn open(mode: file::Mode, path: SafePath) -> Result<(File, SafePath)> { 290 | match path.open(mode) { 291 | Ok(f) => Ok((f, path)), 292 | Err(e) => Err(Error::OpeningFile(e, mode, path)) 293 | } 294 | } 295 | 296 | // TODO: maybe implement password retry if incorrect 297 | /// reads header and password, returns decrypted data and pw 298 | fn decrypt( 299 | mut data: &mut File 300 | ) -> Result<(Secret>, Secret)> { 301 | let head = Header::read_from(&mut data) 302 | .map_err(Error::ReadingHeader)?; 303 | 304 | let pw = Secret::new(input_pw::read("Password: ")?); 305 | 306 | let key = Secret::new( 307 | Key::from_password(&pw, &head) 308 | .map_err(input_pw::Error::GeneratingKey)? 309 | ); 310 | 311 | let crypt_ctx = CryptCtx::new(&key, &head); 312 | let serial = Secret::new(crypt_ctx.decrypt(data)?); 313 | 314 | Ok((serial, pw)) 315 | } 316 | 317 | /// generates new key, salt and nonce (good for security) 318 | /// and empties the file before writing 319 | /// uses `key` to get the key (wrapped in a secret immediately after call) 320 | fn over_encrypt(data: &[u8], mut dest: File, key: F) -> Result<()> 321 | where 322 | F: FnOnce(&Header) -> input_pw::Result 323 | { 324 | let head = Header::generate(); 325 | let key = Secret::new(key(&head)?); 326 | 327 | let crypt_ctx = CryptCtx::new(&key, &head); 328 | 329 | file::clear(&mut dest) 330 | .map_err(Error::ClearingFile)?; 331 | 332 | head.write_to(&mut dest) 333 | .map_err(Error::WritingHeader)?; 334 | 335 | Ok(crypt_ctx.encrypt(data, &mut dest)?) 336 | } 337 | -------------------------------------------------------------------------------- /src/tui/mod.rs: -------------------------------------------------------------------------------- 1 | mod cmd; 2 | 3 | use Status::{Running, Stopped, Aborted, Clipped}; 4 | 5 | use cmd::{Cmd, ReadCmd, EditCmd, MetaCmd, SplitPath, OptVal}; 6 | 7 | use crate::{input, err, info}; 8 | 9 | use crate::{error, output}; 10 | 11 | use crate::find::MatchKind; 12 | 13 | use crate::util::{user_io, record}; 14 | 15 | use crate::util::secret::Erase; 16 | 17 | use crate::util::{ 18 | record::{Record, Group, Node, Ir}, 19 | secret::Secret, 20 | proc::Process 21 | }; 22 | 23 | use std::{io, mem, fmt}; 24 | 25 | use std::fmt::Display; 26 | 27 | use std::time::Duration; 28 | 29 | // TODO: perhaps add option for hiding input 30 | pub struct Tui { 31 | conf: Config, 32 | /// Useful to avoid unnecessarily writing to a file if the data is 33 | /// unchanged. 34 | changes_made: bool, 35 | status: Status 36 | } 37 | 38 | pub struct Config { 39 | pub match_kind: MatchKind, 40 | pub clip_time: Duration 41 | } 42 | 43 | #[derive(Clone, Copy, PartialEq, Eq)] 44 | pub enum Status { 45 | /// Running the TUI. 46 | Running, 47 | /// Exited the TUI normally. 48 | Stopped, 49 | /// Exited the TUI abnormally. 50 | /// 51 | /// Signals that the pass file should not be written to. 52 | Aborted, 53 | /// Used in the clipboard holder process, to signal that the pass file 54 | /// should not be written to (thus avoiding a race condition). 55 | Clipped 56 | } 57 | 58 | pub type Error = error::Error; 59 | 60 | pub type Result = error::Result<()>; 61 | 62 | /// XXX: not hygienic 63 | macro_rules! err_continue { 64 | ($($arg:tt)*) => {{ 65 | err!($($arg)*); 66 | continue; 67 | }}; 68 | } 69 | 70 | macro_rules! unwrap_continue { 71 | ($res:expr) => {{ 72 | match $res { 73 | Ok(v) => v, 74 | Err(e) => err_continue!("{e}") 75 | } 76 | }}; 77 | } 78 | 79 | impl Tui { 80 | pub fn new(conf: Config) -> Self { 81 | Self { 82 | conf, 83 | changes_made: false, 84 | status: Stopped 85 | } 86 | } 87 | 88 | /// after normal return, status should not be `Running` 89 | pub fn run(&mut self, data: &Node) -> Result { 90 | use io::ErrorKind::UnexpectedEof; 91 | 92 | self.status = Running; 93 | 94 | while self.status == Running { 95 | match input!() { 96 | Ok(l) => { 97 | let cmd = match Cmd::from_str(&l) { 98 | Ok(Some(cmd)) => cmd, 99 | Ok(None) => continue, 100 | Err(e) => err_continue!("{e}") 101 | }; 102 | 103 | if let Err(e) = cmd.exec(data, self) { 104 | e.print_full(); 105 | } 106 | } 107 | 108 | Err(e) => if e.kind() == UnexpectedEof { 109 | // Exit if the user closed the stream (C-d). 110 | self.status = Stopped; 111 | } else { 112 | self.status = Aborted; 113 | return Err(Error::ReadingInput(e)) 114 | } 115 | } 116 | } 117 | 118 | Ok(()) 119 | } 120 | 121 | pub fn status(&self) -> Status { 122 | self.status 123 | } 124 | 125 | pub fn should_save_data(&self) -> bool { 126 | self.status == Stopped && self.changes_made 127 | } 128 | } 129 | 130 | impl Config { 131 | fn set(&mut self, opt: OptVal) { 132 | use OptVal::*; 133 | 134 | match opt { 135 | ClipTime(t) => self.clip_time = t, 136 | MatchKind(k) => self.match_kind = k 137 | } 138 | } 139 | } 140 | 141 | impl Display for Config { 142 | #[allow(clippy::write_with_newline)] 143 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 144 | // Function alias. 145 | let name = ::as_name; 146 | 147 | // Writes each element aligned and coloured. 148 | write!(f, "{} {}\n", name("match-kind :"), self.match_kind)?; 149 | write!(f, "{} {}", name("clip-time :"), self.clip_time.as_secs()) 150 | } 151 | } 152 | 153 | impl Cmd { 154 | fn exec(self, data: &Node, tui: &mut Tui) -> Result { 155 | use Cmd::*; 156 | 157 | match self { 158 | Read(cmd) => cmd.exec(data, tui), 159 | Edit(cmd) => cmd.exec(data, tui), 160 | Meta(cmd) => cmd.exec(tui), 161 | } 162 | } 163 | } 164 | 165 | impl ReadCmd { 166 | fn exec(self, data: &Node, tui: &mut Tui) -> Result { 167 | use ReadCmd::*; 168 | use output::{PrintTarget, ClipTarget}; 169 | 170 | let Config { match_kind, clip_time } = tui.conf; 171 | 172 | match self { 173 | Show(paths) => PrintTarget::new(paths, match_kind) 174 | .print_values(data), 175 | 176 | Clip(path) => { 177 | let result_forked = ClipTarget::new(path, match_kind, clip_time) 178 | .clip(data); 179 | 180 | let (opt_proc, result) = result_forked; 181 | 182 | //// The clipboard should exit immediately without performing IO. 183 | if let Some(Process::Child) = opt_proc { 184 | tui.status = Clipped; 185 | } 186 | 187 | return result; 188 | } 189 | 190 | List(opt_paths) => match opt_paths { 191 | Some(paths) => PrintTarget::new(paths, match_kind) 192 | .print_lists(data), 193 | None => println!("{}", Record::display_list(data)) 194 | } 195 | 196 | Tree(opt_paths) => match opt_paths { 197 | Some(paths) => PrintTarget::new(paths, match_kind) 198 | .print_trees(data), 199 | None => println!("{}", Record::display_tree(data)) 200 | } 201 | 202 | Export => { 203 | let ir = Secret::new(Ir::clone_from(data)); 204 | println!("{}", *ir); 205 | } 206 | } 207 | 208 | Ok(()) 209 | } 210 | } 211 | 212 | impl EditCmd { 213 | fn exec(self, data: &Node, tui: &mut Tui) -> Result { 214 | use EditCmd::*; 215 | use record::Error::AlreadyExists; 216 | 217 | let match_kind = tui.conf.match_kind; 218 | 219 | match self { 220 | Remove { paths } => for p in paths { 221 | let mut rec = unwrap_continue!(p.find_in(data, match_kind)); 222 | 223 | let parent = match rec.borrow().parent() { 224 | Some(p) => p, 225 | None => err_continue!("'{p}': cannot remove root group") 226 | }; 227 | 228 | let mut parent = parent.borrow_mut(); 229 | 230 | // We must clone the name to avoid calling `rec.do_with_meta()`. 231 | // If we did so, `parent.remove()` would panic as it mutably 232 | // borrows `rec`. 233 | let name = rec.borrow() 234 | .do_with_meta(|meta| meta.name().to_owned()); 235 | 236 | info!("Removing '{name}' in '{}'", parent.name()); 237 | // `rec` is known to be a child of `parent`, so it can be 238 | // infallibly removed. 239 | parent.remove(&name).unwrap(); 240 | rec.erase(); // `rec` is now orphaned and should be erased. 241 | } 242 | 243 | Move { src, dest } => { 244 | // TODO 245 | err!("unimplemented: '{src}', '{dest}'"); 246 | // we will split path_2 247 | } 248 | 249 | Copy { src, dest } => { 250 | // TODO 251 | err!("unimplemented: '{src}', '{dest}'"); 252 | } 253 | 254 | CreateItem { paths } => for SplitPath { group, name } in paths { 255 | let parent = unwrap_continue!( 256 | group.find_group_in(data, match_kind) 257 | ); 258 | 259 | info!("Creating item '{name}' in '{}'", parent.borrow().name()); 260 | 261 | // Don't ask for a value if the item cannot be created. 262 | if parent.borrow().get(&name).is_ok() { 263 | err_continue!("{}", Error::AddingRecord( 264 | AlreadyExists, name, 265 | clone_name(&parent) 266 | )); 267 | } 268 | 269 | let value = unwrap_continue!(input_escaped("Value: ")); 270 | let item = Record::new_item(name, value); 271 | 272 | unwrap_continue!(insert(item, &parent)); 273 | } 274 | 275 | CreateGroup { paths } => for SplitPath { group, name } in paths { 276 | let parent = unwrap_continue!( 277 | group.find_group_in(data, match_kind) 278 | ); 279 | 280 | info!("Creating group '{name}' in '{}'", parent.borrow().name()); 281 | unwrap_continue!(insert(Record::new_group(name), &parent)); 282 | } 283 | 284 | ChangeValue { paths } => for p in paths { 285 | let item = unwrap_continue!(p.find_item_in(data, match_kind)); 286 | // An item cannot be root, so `item` must have a parent. 287 | let parent = item.borrow().parent().unwrap(); 288 | 289 | info!( 290 | "Changing value of '{}' in '{}'", 291 | item.borrow().name(), 292 | parent.borrow().name() 293 | ); 294 | 295 | // We don't need to wrap this in a `Secret` because it will be 296 | // immediately and infallibly swapped into a protected record. 297 | let mut value = unwrap_continue!(input_escaped("New value: ")); 298 | 299 | mem::swap(item.borrow_mut().value_mut(), &mut value); 300 | value.erase(); // Erase the old value. 301 | } 302 | } 303 | 304 | tui.changes_made = true; 305 | 306 | Ok(()) 307 | } 308 | } 309 | 310 | impl MetaCmd { 311 | fn exec(self, tui: &mut Tui) -> Result { 312 | use MetaCmd::*; 313 | 314 | match self { 315 | SetOpt(opt) => tui.conf.set(opt), 316 | ShowConfig => println!("{}", tui.conf), 317 | 318 | // TODO 319 | ShowUsage(verb) => { 320 | err!("unimplemented"); 321 | 322 | println!("\ 323 | sh | show => Show, 324 | cl | clip => Clip, 325 | ls | list => List, 326 | tr | tree => Tree, 327 | ex | export => Export, 328 | 329 | rm | remove => Remove, 330 | mv | move => Move, 331 | cp | copy => Copy, 332 | mg | mkgrp => CreateGroup, 333 | mi | mkitm => CreateItem, 334 | cv | chval => ChangeValue, 335 | 336 | so | setopt => SetOption, 337 | sc | showconf => ShowConfig, 338 | hp | help => ShowUsage, 339 | et | exit => Exit, 340 | at | abort => Abort" 341 | ); 342 | } 343 | 344 | Exit => tui.status = Stopped, 345 | Abort => tui.status = Aborted, 346 | } 347 | 348 | Ok(()) 349 | } 350 | } 351 | 352 | fn input_escaped(prompt: &str) -> error::Result { 353 | let input = Secret::new(input!("{prompt}")?); 354 | 355 | Ok(unescape(&input)) 356 | } 357 | 358 | /// Returns `s` with whitespace escapes converted into the whitespace they 359 | /// represent. 360 | /// 361 | /// Handles '\n', '\r', '\t' and '\\'. As this function never fails, a single 362 | /// trailing backslash is kept unchanged if it exists. 363 | fn unescape(s: &str) -> String { 364 | let mut result = String::with_capacity(s.len()); 365 | let mut chars = s.chars(); 366 | 367 | while let Some(c) = chars.next() { 368 | if c == '\\' { 369 | let Some(c2) = chars.next() else { 370 | // Handle the single trailing backslash. 371 | result.push(c); 372 | break; 373 | }; 374 | 375 | result.push(match c2 { 376 | 'n' => '\n', 377 | 'r' => '\r', 378 | 't' => '\t', 379 | '\\' => '\\', 380 | c2 => c2 381 | }); 382 | } else { 383 | result.push(c); 384 | } 385 | } 386 | 387 | result 388 | } 389 | 390 | /// erase `rec` on failure 391 | fn insert(mut rec: Node, group: &Node) -> Result { 392 | Group::insert(group, &rec).map_err(|e| { 393 | let name = rec.borrow() 394 | .do_with_meta(|meta| meta.name().to_owned()); 395 | 396 | rec.erase(); 397 | 398 | Error::AddingRecord(e, name, clone_name(group)) 399 | }) 400 | } 401 | 402 | fn clone_name(group: &Node) -> String { 403 | group.borrow().name().to_owned() 404 | } 405 | -------------------------------------------------------------------------------- /src/env.rs: -------------------------------------------------------------------------------- 1 | use crate::config; 2 | use crate::tui; 3 | 4 | use crate::find::{ 5 | RecordPath, 6 | MatchKind 7 | }; 8 | 9 | use crate::util::{ 10 | xdg_path, 11 | file 12 | }; 13 | 14 | use crate::util::file::SafePath; 15 | 16 | use std::{ 17 | fmt, 18 | io 19 | }; 20 | 21 | use std::fmt::Display; 22 | 23 | use std::{ 24 | path::{Path, PathBuf}, 25 | time::Duration 26 | }; 27 | 28 | /// Commonly used data structures relevant to the environment, and useful for 29 | /// parsing it. 30 | pub mod prelude { 31 | pub use super::{ 32 | Cmd, FileCmd, 33 | ReadCmd, ChangeCmd, CreateCmd 34 | }; 35 | } 36 | 37 | /// Program binary name. 38 | pub const PROGNAME: &str = env!("CARGO_BIN_NAME"); 39 | 40 | /// The command to be executed. 41 | // TODO: maybe find way to improve such that there is no impurity (creating dirs) 42 | // maybe shouldnt even create safepath: only Option for user provided 43 | // path (thus renaming mod to `args` or smth) 44 | pub enum Cmd { 45 | ShowUsage(Usage), 46 | ShowVersion(Version), 47 | HandleFile(FileCmd, SafePath) 48 | } 49 | 50 | /// Handling a pass file. 51 | pub enum FileCmd { 52 | Read(ReadCmd), 53 | Change(ChangeCmd), 54 | Create(CreateCmd) 55 | } 56 | 57 | /// Reading data from a pass file. 58 | pub enum ReadCmd { 59 | /// Displaying an item. 60 | Show(Vec, MatchKind), 61 | /// Copying an item to the clipboard, and keeping it there for a `Duration`. 62 | Clip(RecordPath, MatchKind, Duration), 63 | /// Displaying the names of a group's records, or of an item. 64 | List(Option>, MatchKind), 65 | /// Displaying a tree representation of a group, or an item. Only the names 66 | /// of the records are shown, and their layout. If no target is provided, 67 | /// the root group is considered the target. 68 | Tree(Option>, MatchKind), 69 | /// Displaying a serial representation of the data. 70 | Export 71 | } 72 | 73 | /// Editing a pass file. 74 | pub enum ChangeCmd { 75 | /// Modifying the data. 76 | Modify(tui::Config), 77 | /// Changing the password used to access the data. 78 | ChangePassword 79 | } 80 | 81 | /// Creating a new pass file. 82 | pub enum CreateCmd { 83 | /// Creating a pass file with from input data in serial form. 84 | Import, 85 | /// Creating a pass file with no data, and with specified name for root 86 | /// group. 87 | CreateEmpty(String) 88 | } 89 | 90 | pub struct Usage; 91 | 92 | /// XXX: version and copyright 93 | pub struct Version; 94 | 95 | pub enum Error { 96 | ParsingArgs(lexopt::Error), 97 | ResolvingDataPath(xdg_path::Error), 98 | CreatingBackupDir(io::Error, PathBuf) 99 | } 100 | 101 | pub type Result = std::result::Result; 102 | 103 | impl Cmd { 104 | /// Parses command line arguments and sets up the environment. 105 | /// 106 | /// May create some standard XDG directories if necessary. 107 | pub fn from_env() -> Result { 108 | use FileCmdVerb::*; 109 | use Cmd::*; 110 | 111 | use lexopt::prelude::*; 112 | use lexopt::Parser; 113 | use lexopt::Error::Custom; 114 | 115 | let mut parser = Parser::from_env(); 116 | let mut opts = FileCmdOpts::default(); 117 | let mut cmd = FileCmdVerb::default(); 118 | let mut file_path = Option::::None; 119 | 120 | while let Some(arg) = parser.next()? { 121 | let old_cmd = cmd; 122 | 123 | match arg { 124 | Short('c') | Long("clip") => cmd = Clip, 125 | Short('l') | Long("list") => cmd = List, 126 | Short('t') | Long("tree") => cmd = Tree, 127 | 128 | Short('e') | Long("exact") => 129 | opts.match_kind = MatchKind::Exact, 130 | Short('d') | Long("duration") => 131 | opts.clip_time = parser.value()?.parse()?, 132 | Short('f') | Long("file") => 133 | file_path = Some(parser.value()?.into()), 134 | 135 | Short('M') | Long("modify") => cmd = Edit, 136 | Short('P') | Long("change-pw") => cmd = ChangePassword, 137 | 138 | Short('E') | Long("export") => cmd = Export, 139 | Short('I') | Long("import") => cmd = Import, 140 | 141 | Short('C') | Long("create") => { 142 | opts.root_name = parser.value()?.parse()?; 143 | cmd = CreateEmpty; 144 | } 145 | 146 | Value(v) => opts.record_paths_raw.push(v.parse()?), 147 | 148 | Short('h') | Long("help") => return Ok(ShowUsage(Usage)), 149 | Short('v') | Long("version") => return Ok(ShowVersion(Version)), 150 | 151 | _ => return Err(arg.unexpected().into()) 152 | } 153 | 154 | // Verify that the user doesn't pass multiple conflicting options. 155 | if old_cmd.conflicts_with(cmd) { 156 | return Err(Custom("conflicting options".into()).into()); 157 | } 158 | } 159 | 160 | let file_cmd = FileCmd::from_parts(cmd, opts)?; 161 | 162 | let data_dir = xdg_path::data_dir(PROGNAME)?; 163 | 164 | let file_path = file_path.unwrap_or_else(|| { 165 | data_dir.join(config::DEFAULT_PASS_FILE_NAME) 166 | }); 167 | 168 | let path = ensured_path_from(file_path, data_dir)?; 169 | 170 | Ok(Cmd::HandleFile(file_cmd, path)) 171 | } 172 | } 173 | 174 | impl Display for Usage { 175 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 176 | write!(f, "\ 177 | Usage: {} [OPTION...] [TARGET...] 178 | Securely manage hierarchical data. 179 | 180 | -c, --clip copy target item to primary clipboard instead of displaying 181 | -l, --list list the target's contents (root if not specified) 182 | -t, --tree display a tree of the target (root if not specified) 183 | 184 | -e, --exact find exact match of target (default: fuzzy match) 185 | -d, --duration time in seconds to keep target in clipboard (default: {}) 186 | -f, --file specify a pass file (default: standard data file) 187 | 188 | -M, --modify launch editing interface (respects '-e' and '-d') 189 | -P, --change-pw change the pass file's password 190 | 191 | -E, --export output data in serial form 192 | -I, --import create a pass file from serial data (read from stdin) 193 | -C, --create create an empty pass file with the specified root name 194 | 195 | -h, --help display this help text 196 | -v, --version display version information 197 | 198 | Note: By default, the target item is printed to standard output. 199 | Targets are passed as dot-separated record paths. 200 | Passing a group as a target item implies its child item '{}'. 201 | 202 | Example: pass -d5 -c foo.bar", 203 | PROGNAME, 204 | config::DEFAULT_CLIP_TIME, 205 | config::DEFAULT_ITEM 206 | ) 207 | } 208 | } 209 | 210 | impl Display for Version { 211 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 212 | const LICENSE_URL: &str = "https://gnu.org/licenses/gpl.html"; 213 | 214 | write!( 215 | f, concat!( 216 | env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION"), " 217 | Copyright (C) 2023 ", env!("CARGO_PKG_AUTHORS"), ". 218 | License ", env!("CARGO_PKG_LICENSE"), " <{}>. 219 | This is free software: you are free to change and redistribute it. 220 | There is NO WARRANTY, to the extent permitted by law." 221 | ), LICENSE_URL 222 | ) 223 | } 224 | } 225 | 226 | impl Display for Error { 227 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 228 | use Error::*; 229 | 230 | match self { 231 | ParsingArgs(e) => 232 | write!(f, "{e}"), 233 | ResolvingDataPath(e) => 234 | write!(f, "cannot resolve XDG data directory: {e}"), 235 | CreatingBackupDir(e, p) => 236 | write!(f, "backup directory '{}': {e}", p.display()), 237 | } 238 | } 239 | } 240 | 241 | impl From for Error { 242 | fn from(e: lexopt::Error) -> Self { 243 | Self::ParsingArgs(e) 244 | } 245 | } 246 | 247 | impl From for Error { 248 | fn from(e: xdg_path::Error) -> Self { 249 | Self::ResolvingDataPath(e) 250 | } 251 | } 252 | 253 | /// Options relevant to handling a file. 254 | struct FileCmdOpts { 255 | /// The path of the target `Record`. 256 | record_paths_raw: Vec, 257 | match_kind: MatchKind, 258 | clip_time: u64, 259 | root_name: String 260 | } 261 | 262 | /// Non-algebraic [`FileCmd`] for parsing. 263 | #[derive(Default, Clone, Copy, PartialEq, Eq)] 264 | enum FileCmdVerb { 265 | #[default] 266 | Show, 267 | Clip, 268 | List, 269 | Tree, 270 | 271 | Edit, 272 | ChangePassword, 273 | 274 | Export, 275 | Import, 276 | CreateEmpty 277 | } 278 | 279 | impl FileCmd { 280 | fn from_parts(cmd: FileCmdVerb, opts: FileCmdOpts) -> Result { 281 | use FileCmd::*; 282 | use FileCmdVerb::*; 283 | 284 | use tui::Config; 285 | use lexopt::Error::{MissingValue, UnexpectedArgument}; 286 | 287 | let FileCmdOpts { 288 | record_paths_raw: rec_paths_raw, 289 | match_kind, 290 | clip_time, 291 | root_name 292 | } = opts; 293 | 294 | let clip_time = Duration::from_secs(clip_time); 295 | 296 | // Check the validity of the arguments. 297 | match cmd { 298 | Show | Clip 299 | if rec_paths_raw.is_empty() => 300 | return Err(MissingValue { option: None }.into()), 301 | 302 | Clip 303 | if rec_paths_raw.len() > 1 => 304 | // `record_paths` second element was verified to exist. 305 | return Err(UnexpectedArgument( 306 | take(rec_paths_raw, 1).into() 307 | ).into()), 308 | 309 | Edit | ChangePassword | Export | Import 310 | if !rec_paths_raw.is_empty() => 311 | // `record_paths` is not empty so its first element exists. 312 | return Err(UnexpectedArgument( 313 | take(rec_paths_raw, 0).into() 314 | ).into()), 315 | 316 | _ => () 317 | } 318 | 319 | let rec_paths = rec_paths_raw.into_iter() 320 | .map(RecordPath::from) 321 | .collect::>(); 322 | 323 | Ok(match cmd { 324 | Show => Read(ReadCmd::Show(rec_paths, match_kind)), 325 | Clip => Read(ReadCmd::Clip(take(rec_paths, 0), match_kind, clip_time)), 326 | List => Read(ReadCmd::List(empty_or_some(rec_paths), match_kind)), 327 | Tree => Read(ReadCmd::Tree(empty_or_some(rec_paths), match_kind)), 328 | 329 | Edit => Change(ChangeCmd::Modify(Config { match_kind, clip_time })), 330 | ChangePassword => Change(ChangeCmd::ChangePassword), 331 | 332 | Export => Read(ReadCmd::Export), 333 | Import => Create(CreateCmd::Import), 334 | CreateEmpty => Create(CreateCmd::CreateEmpty(root_name)) 335 | }) 336 | } 337 | } 338 | 339 | impl Default for FileCmdOpts { 340 | fn default() -> Self { 341 | Self { 342 | /// The path of the target `Record`, root group by default. 343 | record_paths_raw: Default::default(), 344 | match_kind: Default::default(), 345 | clip_time: config::DEFAULT_CLIP_TIME, 346 | root_name: Default::default(), 347 | } 348 | } 349 | } 350 | 351 | impl FileCmdVerb { 352 | /// Verifies if `other` can logically supersede `self`. 353 | /// 354 | /// Returns true if `self` is neither equal to `other` nor the default 355 | /// command, and returns false otherwise. In other words, returns true if 356 | /// `other` cannot logically override `self.` 357 | fn conflicts_with(self, other: Self) -> bool { 358 | self != other && self != Self::default() 359 | } 360 | } 361 | 362 | /// Returns a [`SafePath`] with `file_path` as the main path, and a file 363 | /// located in a subdirectory of `data_dir` as the backup path. 364 | /// 365 | /// The data path and backup paths are created if they do not exist. 366 | fn ensured_path_from( 367 | file_path: PathBuf, 368 | data_dir: PathBuf 369 | ) -> Result { 370 | use std::fs::create_dir_all; 371 | 372 | const BACKUP_DIR: &str = "backup"; 373 | 374 | let backup_dir = joined(data_dir, BACKUP_DIR); 375 | 376 | if let Err(e) = create_dir_all(&backup_dir) { 377 | return Err(Error::CreatingBackupDir(e, backup_dir)) 378 | } 379 | 380 | let backup_path = file::backup_path_from(&file_path, backup_dir); 381 | 382 | Ok(SafePath::new(file_path, backup_path)) 383 | } 384 | 385 | /// Appends `other` to `path` and returns the result. 386 | /// 387 | /// Doesn't allocate a new [`PathBuf`], unlike [`Path::join`]. 388 | fn joined>(mut path: PathBuf, other: P) -> PathBuf { 389 | path.push(other); 390 | path 391 | } 392 | 393 | /// Moves value at `idx` out `v` and returns it. 394 | /// 395 | /// Panics if `idx` is out of bounds of `v`. 396 | fn take(v: Vec, idx: usize) -> T { 397 | v.into_iter().nth(idx).unwrap() 398 | } 399 | 400 | /// Converts `v` into an [`Option`]. 401 | /// 402 | /// Returns `None` if `v` is empty, otherwise `Some(v)`. 403 | fn empty_or_some(v: Vec) -> Option> { 404 | if v.is_empty() { 405 | None 406 | } else { 407 | Some(v) 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /src/util/record/mod.rs: -------------------------------------------------------------------------------- 1 | mod ir; 2 | 3 | use super::{ 4 | secret::Erase, 5 | user_io::Style 6 | }; 7 | 8 | use std::{fmt, mem}; 9 | 10 | use std::{ 11 | fmt::Display, 12 | cmp::Ordering 13 | }; 14 | 15 | use std::{ 16 | collections::BTreeMap, 17 | rc::{Rc, Weak}, 18 | cell::RefCell 19 | }; 20 | 21 | pub use ir::Ir; 22 | 23 | pub enum Record { 24 | Group(Node), 25 | Item(Node) 26 | } 27 | 28 | pub struct Group { 29 | /// XXX: static reference to avoid lifetimes (cant deserialise then) 30 | /// always valid as long as the child records exist (which is always the 31 | /// case if this group exists), 32 | /// but not truly static 33 | /// -- btreemap is always ordered 34 | members: BTreeMap<&'static str, Node>, 35 | meta: Metadata 36 | } 37 | 38 | pub struct Item { 39 | value: String, 40 | meta: Metadata 41 | } 42 | 43 | pub struct Metadata { 44 | /// XXX: should not modified be if a parent exists (to avoid invalidating 45 | /// the hashmap) 46 | name: String, 47 | parent: Option>, 48 | } 49 | 50 | #[derive(Debug)] 51 | pub enum Error { 52 | // Clippy recommends that we box this (very large) error. 53 | Serialisation(Box), 54 | Deserialisation(Box), 55 | NotFound, 56 | MultipleMatches, 57 | AlreadyExists, 58 | } 59 | 60 | pub type Node = Rc>; 61 | /// XXX: non owning, prevents cycle 62 | type WeakNode = Weak>; 63 | 64 | pub type Result = std::result::Result; 65 | 66 | impl Record { 67 | pub fn from(ir: Ir) -> Node { 68 | match ir { 69 | Ir::Group { name, members, metadata: _ } => { 70 | let group = new_node(Group { 71 | members: BTreeMap::new(), 72 | meta: Metadata::for_root(name) 73 | }); 74 | 75 | group.borrow_mut().members = members.into_iter().map(|ir| { 76 | // TODO: technically unsound because the child (and its 77 | // name) might be deallocated before the reference when 78 | // removed or dropped 79 | 80 | // SAFETY: We guarantee that the group will only keep `name` 81 | // as long as `rec` is owned by it (and that the reference 82 | // is therefore valid). The name will never be modified, so 83 | // it will never be reallocated. Therefore, an immutable 84 | // reference is valid. Furthermore, the record and its name 85 | // will never be moved as it is kept behind an `Rc`. 86 | let name = unsafe { 87 | std::mem::transmute::<_, &'static str>(ir.name()) 88 | }; 89 | 90 | let rec = Record::with_parent(ir, &group); 91 | 92 | (name, rec) 93 | }).collect::>(); 94 | 95 | new_node(Record::Group(group)) 96 | } 97 | 98 | Ir::Item { name, value, metadata: _ } => { 99 | new_node(Record::Item(new_node(Item { 100 | value, 101 | meta: Metadata::for_root(name) 102 | }))) 103 | } 104 | } 105 | } 106 | 107 | pub fn new_group(name: String) -> Node { 108 | new_node(Record::Group(Group::new(name))) 109 | } 110 | 111 | pub fn new_item(name: String, value: String) -> Node { 112 | new_node(Record::Item(Item::new(name, value))) 113 | } 114 | 115 | pub fn display_list(this: &Node) -> impl Display { 116 | DisplayList(Rc::clone(this)) 117 | } 118 | 119 | pub fn display_tree(this: &Node) -> impl Display { 120 | DisplayTree(Rc::clone(this)) 121 | } 122 | 123 | pub fn do_with_meta(&self, op: O) -> R 124 | where 125 | O: FnOnce(&Metadata) -> R 126 | { 127 | match self { 128 | Self::Group(g) => op(&g.borrow().meta), 129 | Self::Item(i) => op(&i.borrow().meta) 130 | } 131 | } 132 | 133 | pub fn parent(&self) -> Option> { 134 | match self { 135 | Self::Group(g) => g.borrow().parent(), 136 | Self::Item(i) => i.borrow().parent() 137 | } 138 | } 139 | } 140 | 141 | // `Erase` is already implemented for `Node` where `T` implements `Erase`. 142 | // Like this, a `Secret` containing a `Node` can be created. 143 | impl Erase for Record { 144 | #[inline(never)] 145 | fn erase(&mut self) { 146 | match self { 147 | Self::Group(g) => g.erase(), 148 | Self::Item(i) => i.erase() 149 | } 150 | } 151 | } 152 | 153 | impl Group { 154 | pub fn new(name: String) -> Node { 155 | new_node(Self { 156 | members: BTreeMap::new(), 157 | meta: Metadata::for_root(name) 158 | }) 159 | } 160 | 161 | pub fn name(&self) -> &str { 162 | self.meta.name() 163 | } 164 | 165 | pub fn parent(&self) -> Option> { 166 | self.meta.parent() 167 | } 168 | 169 | pub fn get(&self, name: &str) -> Result> { 170 | let result = self.members 171 | .get(name) 172 | .ok_or(Error::NotFound)?; 173 | 174 | Ok(Rc::clone(result)) 175 | } 176 | 177 | pub fn get_fuzzy(&self, name_pat: &str) -> Result> { 178 | let mut first_match = Option::::None; 179 | let mut members_iter = self.members.iter(); 180 | 181 | for (name, rec) in &mut members_iter { 182 | if let Some(m) = Match::make(name_pat, name, rec) { 183 | first_match = Some(m); 184 | break; 185 | } 186 | } 187 | 188 | let mut best_match = first_match 189 | .ok_or(Error::NotFound)?; 190 | 191 | let mut have_multiple_matches = false; 192 | 193 | // Iterate through the remaining `Records`s. 194 | for (name, rec) in members_iter { 195 | if let Some(m) = Match::make(name_pat, name, rec) { 196 | match m.cmp_score(&best_match) { 197 | Ordering::Greater => { 198 | best_match = m; 199 | // No other match exists yet at this new highest score. 200 | have_multiple_matches = false; 201 | } 202 | 203 | Ordering::Equal => have_multiple_matches = true, 204 | Ordering::Less => continue 205 | } 206 | } 207 | } 208 | 209 | if have_multiple_matches { 210 | Err(Error::MultipleMatches) 211 | } else { 212 | Ok(Rc::clone(best_match.val)) 213 | } 214 | } 215 | 216 | /// XXX: fails if record of same name already exists, and rec is left 217 | /// unchanged 218 | pub fn insert(this: &Node, rec: &Node) -> Result<()> { 219 | let members = &mut this.borrow_mut().members; 220 | 221 | // TODO: use `BTreeMap::try_insert` once available 222 | let name = rec.borrow_mut().mutate_meta(|meta| { 223 | let name = meta.name.as_str(); 224 | 225 | // If the insertion cannot be done, we return before modifying 226 | // 'rec'. 227 | if members.contains_key(name) { 228 | return Err(Error::AlreadyExists); 229 | } 230 | 231 | meta.parent = Some(Rc::downgrade(this)); 232 | 233 | // SAFETY: Same as with `Record::from`. 234 | Ok(unsafe { 235 | mem::transmute::<_, &'static str>(name) 236 | }) 237 | })?; 238 | 239 | members.insert(name, Rc::clone(rec)); 240 | Ok(()) 241 | } 242 | 243 | /// mutably borrows the removed record (may panic) 244 | pub fn remove(&mut self, name: &str) -> Result> { 245 | let removed = self.members 246 | .remove(name) 247 | .ok_or(Error::NotFound)?; 248 | 249 | removed.borrow().set_parent(None); 250 | 251 | Ok(removed) 252 | } 253 | } 254 | 255 | impl Erase for Group { 256 | #[inline(never)] 257 | fn erase(&mut self) { 258 | self.members.erase(); 259 | self.meta.erase(); 260 | } 261 | } 262 | 263 | impl Item { 264 | pub fn new(name: String, value: String) -> Node { 265 | new_node(Self { 266 | value, 267 | meta: Metadata::for_root(name) 268 | }) 269 | } 270 | 271 | pub fn name(&self) -> &str { 272 | self.meta.name() 273 | } 274 | 275 | pub fn value(&self) -> &String { 276 | &self.value 277 | } 278 | 279 | pub fn value_mut(&mut self) -> &mut String { 280 | &mut self.value 281 | } 282 | 283 | pub fn parent(&self) -> Option> { 284 | self.meta.parent() 285 | } 286 | } 287 | 288 | impl Erase for Item { 289 | #[inline(never)] 290 | fn erase(&mut self) { 291 | self.value.erase(); 292 | self.meta.erase(); 293 | } 294 | } 295 | 296 | impl Metadata { 297 | pub fn name(&self) -> &str { 298 | &self.name 299 | } 300 | 301 | pub fn parent(&self) -> Option> { 302 | let parent = &self.parent.as_ref()?; 303 | 304 | // A record's parent cannot have been dropped before the record itself. 305 | Some(parent.upgrade().unwrap()) 306 | } 307 | } 308 | 309 | impl Erase for Metadata { 310 | #[inline(never)] 311 | fn erase(&mut self) { 312 | self.name.erase(); 313 | } 314 | } 315 | 316 | impl Display for Error { 317 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 318 | use Error::*; 319 | 320 | match self { 321 | Deserialisation(e) => 322 | write!(f, "{e}"), 323 | Serialisation(e) => 324 | write!(f, "{e}"), 325 | NotFound => 326 | write!(f, "record not found"), 327 | MultipleMatches => 328 | write!(f, "multiple matches found"), 329 | AlreadyExists => 330 | write!(f, "record already exists"), 331 | } 332 | } 333 | } 334 | 335 | impl Record { 336 | fn with_parent(ir: Ir, parent: &Node) -> Node { 337 | let result = Record::from(ir); 338 | 339 | result.borrow_mut().set_parent(Rc::downgrade(parent)); 340 | result 341 | } 342 | 343 | fn mutate_meta(&self, op: O) -> R 344 | where 345 | O: FnOnce(&mut Metadata) -> R 346 | { 347 | match self { 348 | Self::Group(g) => op(&mut g.borrow_mut().meta), 349 | Self::Item(i) => op(&mut i.borrow_mut().meta) 350 | } 351 | } 352 | 353 | fn set_parent

(&self, p: P) 354 | where 355 | P: Into>> 356 | { 357 | self.mutate_meta(|meta| meta.parent = p.into()); 358 | } 359 | } 360 | 361 | impl Metadata { 362 | fn for_root(name: String) -> Self { 363 | Self { name, parent: None } 364 | } 365 | } 366 | 367 | fn new_node(v: T) -> Node { 368 | Rc::new(RefCell::new(v)) 369 | } 370 | 371 | /// XXX: doesnt display values 372 | /// displays one layer, like unix `ls` 373 | /// doesnt leak any actual data 374 | struct DisplayList(Node); 375 | 376 | /// XXX: doesnt display values 377 | /// displays all layers, like unix `tree` 378 | /// doesnt leak any actual data 379 | struct DisplayTree(Node); 380 | 381 | struct Match<'r> { 382 | val: &'r Node, 383 | score: isize 384 | } 385 | 386 | impl Display for DisplayList { 387 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 388 | match &*self.0.borrow() { 389 | Record::Group(g) => { 390 | let g = g.borrow(); 391 | let mut members_iter = g.members.iter(); 392 | 393 | if let Some((name, rec)) = members_iter.next() { 394 | rec.borrow().fmt_name(f, name)?; 395 | 396 | for (name, rec) in members_iter { 397 | writeln!(f)?; 398 | rec.borrow().fmt_name(f, name)?; 399 | } 400 | } 401 | } 402 | 403 | Record::Item(i) => write!(f, "{}", i.borrow().name())? 404 | } 405 | 406 | Ok(()) 407 | } 408 | } 409 | 410 | impl Display for DisplayTree { 411 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 412 | match &*self.0.borrow() { 413 | Record::Group(g) => { 414 | let g = g.borrow(); 415 | 416 | write!(f, "{}", g.name().as_title())?; 417 | g.fmt_as_branch(f, &mut String::new()) 418 | } 419 | 420 | Record::Item(i) => write!(f, "{}", i.borrow().name()) 421 | } 422 | } 423 | } 424 | 425 | impl<'r> Match<'r> { 426 | /// XXX: matches `pattern` to `target` 427 | /// - if made: returns `Self` containing `target` 428 | /// - otherwise: returns `None` 429 | fn make(pattern: &str, name: &str, val: &'r Node) -> Option { 430 | use sublime_fuzzy::best_match; 431 | 432 | let score = best_match(pattern, name)?.score(); 433 | 434 | Some(Self { val, score }) 435 | } 436 | 437 | fn cmp_score(&self, other: &Self) -> Ordering { 438 | self.score.cmp(&other.score) 439 | } 440 | } 441 | 442 | impl Record { 443 | fn fmt_name(&self, f: &mut fmt::Formatter, name: &str) -> fmt::Result { 444 | match self { 445 | Record::Group(_) => write!(f, "{}", name.as_heading()), 446 | Record::Item(_) => write!(f, "{}", name) 447 | } 448 | } 449 | } 450 | 451 | impl Group { 452 | /// XXX: always prints leading newline, unless `self` is empty 453 | /// recursively formats the entire group 454 | /// `buffer` is reset to state before passed when func returns 455 | /// if called on root group, `buffer` should be empty 456 | fn fmt_as_branch( 457 | &self, 458 | dest: &mut fmt::Formatter, 459 | buffer: &mut String 460 | ) -> fmt::Result { 461 | const BAR: &str = "\u{2502} "; 462 | const SPACE: &str = " "; 463 | const FORK: &str = "\u{251C}\u{2500}\u{2500} "; 464 | const FORK_END: &str = "\u{2514}\u{2500}\u{2500} "; 465 | 466 | // `peekable()` allows us to track if we are at the last member. 467 | let mut members_iter = self.members.iter().peekable(); 468 | 469 | #[allow(clippy::write_with_newline)] 470 | while let Some((name, rec)) = members_iter.next() { 471 | write!(dest, "\n")?; 472 | write!(dest, "{buffer}")?; 473 | 474 | match members_iter.peek() { 475 | Some(_) => write!(dest, "{FORK}")?, 476 | None => write!(dest, "{FORK_END}")? 477 | }; 478 | 479 | rec.borrow().fmt_name(dest, name)?; 480 | 481 | if let Record::Group(g) = &*rec.borrow() { 482 | let old_len = buffer.len(); 483 | 484 | match members_iter.peek() { 485 | Some(_) => buffer.push_str(BAR), 486 | None => buffer.push_str(SPACE), 487 | } 488 | 489 | g.borrow().fmt_as_branch(dest, buffer)?; 490 | buffer.truncate(old_len); // Revert `buf`. 491 | } 492 | } 493 | 494 | Ok(()) 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aead" 7 | version = "0.5.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" 10 | dependencies = [ 11 | "crypto-common", 12 | "generic-array", 13 | ] 14 | 15 | [[package]] 16 | name = "arboard" 17 | version = "3.2.0" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854" 20 | dependencies = [ 21 | "clipboard-win", 22 | "log", 23 | "objc", 24 | "objc-foundation", 25 | "objc_id", 26 | "once_cell", 27 | "parking_lot", 28 | "thiserror", 29 | "winapi", 30 | "x11rb", 31 | ] 32 | 33 | [[package]] 34 | name = "arrayref" 35 | version = "0.3.7" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" 38 | 39 | [[package]] 40 | name = "arrayvec" 41 | version = "0.7.2" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" 44 | 45 | [[package]] 46 | name = "autocfg" 47 | version = "1.1.0" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 50 | 51 | [[package]] 52 | name = "base64" 53 | version = "0.13.1" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 56 | 57 | [[package]] 58 | name = "base64" 59 | version = "0.21.7" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 62 | 63 | [[package]] 64 | name = "bitflags" 65 | version = "1.3.2" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 68 | 69 | [[package]] 70 | name = "bitflags" 71 | version = "2.4.2" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 74 | 75 | [[package]] 76 | name = "blake2b_simd" 77 | version = "1.0.1" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "3c2f0dc9a68c6317d884f97cc36cf5a3d20ba14ce404227df55e1af708ab04bc" 80 | dependencies = [ 81 | "arrayref", 82 | "arrayvec", 83 | "constant_time_eq 0.2.6", 84 | ] 85 | 86 | [[package]] 87 | name = "block" 88 | version = "0.1.6" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 91 | 92 | [[package]] 93 | name = "cfg-if" 94 | version = "1.0.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 97 | 98 | [[package]] 99 | name = "chacha20" 100 | version = "0.9.1" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" 103 | dependencies = [ 104 | "cfg-if", 105 | "cipher", 106 | "cpufeatures", 107 | ] 108 | 109 | [[package]] 110 | name = "chacha20poly1305" 111 | version = "0.10.1" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" 114 | dependencies = [ 115 | "aead", 116 | "chacha20", 117 | "cipher", 118 | "poly1305", 119 | "zeroize", 120 | ] 121 | 122 | [[package]] 123 | name = "cipher" 124 | version = "0.4.4" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 127 | dependencies = [ 128 | "crypto-common", 129 | "inout", 130 | "zeroize", 131 | ] 132 | 133 | [[package]] 134 | name = "clipboard-win" 135 | version = "4.5.0" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" 138 | dependencies = [ 139 | "error-code", 140 | "str-buf", 141 | "winapi", 142 | ] 143 | 144 | [[package]] 145 | name = "constant_time_eq" 146 | version = "0.2.6" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" 149 | 150 | [[package]] 151 | name = "constant_time_eq" 152 | version = "0.3.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" 155 | 156 | [[package]] 157 | name = "cpufeatures" 158 | version = "0.2.7" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" 161 | dependencies = [ 162 | "libc", 163 | ] 164 | 165 | [[package]] 166 | name = "crypto-common" 167 | version = "0.1.6" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 170 | dependencies = [ 171 | "generic-array", 172 | "rand_core", 173 | "typenum", 174 | ] 175 | 176 | [[package]] 177 | name = "errno" 178 | version = "0.3.8" 179 | source = "registry+https://github.com/rust-lang/crates.io-index" 180 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 181 | dependencies = [ 182 | "libc", 183 | "windows-sys 0.52.0", 184 | ] 185 | 186 | [[package]] 187 | name = "error-code" 188 | version = "2.3.1" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" 191 | dependencies = [ 192 | "libc", 193 | "str-buf", 194 | ] 195 | 196 | [[package]] 197 | name = "generic-array" 198 | version = "0.14.7" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 201 | dependencies = [ 202 | "typenum", 203 | "version_check", 204 | ] 205 | 206 | [[package]] 207 | name = "gethostname" 208 | version = "0.2.3" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" 211 | dependencies = [ 212 | "libc", 213 | "winapi", 214 | ] 215 | 216 | [[package]] 217 | name = "getrandom" 218 | version = "0.2.10" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" 221 | dependencies = [ 222 | "cfg-if", 223 | "libc", 224 | "wasi", 225 | ] 226 | 227 | [[package]] 228 | name = "hermit-abi" 229 | version = "0.3.3" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" 232 | 233 | [[package]] 234 | name = "home" 235 | version = "0.5.5" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" 238 | dependencies = [ 239 | "windows-sys 0.48.0", 240 | ] 241 | 242 | [[package]] 243 | name = "inout" 244 | version = "0.1.3" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 247 | dependencies = [ 248 | "generic-array", 249 | ] 250 | 251 | [[package]] 252 | name = "is-terminal" 253 | version = "0.4.10" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" 256 | dependencies = [ 257 | "hermit-abi", 258 | "rustix", 259 | "windows-sys 0.52.0", 260 | ] 261 | 262 | [[package]] 263 | name = "is_ci" 264 | version = "1.1.1" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" 267 | 268 | [[package]] 269 | name = "lexopt" 270 | version = "0.3.0" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "baff4b617f7df3d896f97fe922b64817f6cd9a756bb81d40f8883f2f66dcb401" 273 | 274 | [[package]] 275 | name = "libc" 276 | version = "0.2.152" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" 279 | 280 | [[package]] 281 | name = "linux-raw-sys" 282 | version = "0.4.13" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 285 | 286 | [[package]] 287 | name = "lock_api" 288 | version = "0.4.10" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" 291 | dependencies = [ 292 | "autocfg", 293 | "scopeguard", 294 | ] 295 | 296 | [[package]] 297 | name = "log" 298 | version = "0.4.19" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" 301 | 302 | [[package]] 303 | name = "malloc_buf" 304 | version = "0.0.6" 305 | source = "registry+https://github.com/rust-lang/crates.io-index" 306 | checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 307 | dependencies = [ 308 | "libc", 309 | ] 310 | 311 | [[package]] 312 | name = "memoffset" 313 | version = "0.6.5" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" 316 | dependencies = [ 317 | "autocfg", 318 | ] 319 | 320 | [[package]] 321 | name = "nix" 322 | version = "0.24.3" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" 325 | dependencies = [ 326 | "bitflags 1.3.2", 327 | "cfg-if", 328 | "libc", 329 | "memoffset", 330 | ] 331 | 332 | [[package]] 333 | name = "nix" 334 | version = "0.27.1" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" 337 | dependencies = [ 338 | "bitflags 2.4.2", 339 | "cfg-if", 340 | "libc", 341 | ] 342 | 343 | [[package]] 344 | name = "objc" 345 | version = "0.2.7" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 348 | dependencies = [ 349 | "malloc_buf", 350 | ] 351 | 352 | [[package]] 353 | name = "objc-foundation" 354 | version = "0.1.1" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" 357 | dependencies = [ 358 | "block", 359 | "objc", 360 | "objc_id", 361 | ] 362 | 363 | [[package]] 364 | name = "objc_id" 365 | version = "0.1.1" 366 | source = "registry+https://github.com/rust-lang/crates.io-index" 367 | checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" 368 | dependencies = [ 369 | "objc", 370 | ] 371 | 372 | [[package]] 373 | name = "once_cell" 374 | version = "1.19.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 377 | 378 | [[package]] 379 | name = "opaque-debug" 380 | version = "0.3.0" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 383 | 384 | [[package]] 385 | name = "owo-colors" 386 | version = "4.0.0" 387 | source = "registry+https://github.com/rust-lang/crates.io-index" 388 | checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" 389 | 390 | [[package]] 391 | name = "parking_lot" 392 | version = "0.12.1" 393 | source = "registry+https://github.com/rust-lang/crates.io-index" 394 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 395 | dependencies = [ 396 | "lock_api", 397 | "parking_lot_core", 398 | ] 399 | 400 | [[package]] 401 | name = "parking_lot_core" 402 | version = "0.9.8" 403 | source = "registry+https://github.com/rust-lang/crates.io-index" 404 | checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" 405 | dependencies = [ 406 | "cfg-if", 407 | "libc", 408 | "redox_syscall", 409 | "smallvec", 410 | "windows-targets 0.48.0", 411 | ] 412 | 413 | [[package]] 414 | name = "passers" 415 | version = "0.1.0" 416 | dependencies = [ 417 | "arboard", 418 | "chacha20poly1305", 419 | "lexopt", 420 | "nix 0.27.1", 421 | "owo-colors", 422 | "path-absolutize", 423 | "rand", 424 | "ron", 425 | "rust-argon2", 426 | "serde", 427 | "shell-words", 428 | "sublime_fuzzy", 429 | "supports-color", 430 | "xdg", 431 | ] 432 | 433 | [[package]] 434 | name = "path-absolutize" 435 | version = "3.1.0" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "43eb3595c63a214e1b37b44f44b0a84900ef7ae0b4c5efce59e123d246d7a0de" 438 | dependencies = [ 439 | "path-dedot", 440 | ] 441 | 442 | [[package]] 443 | name = "path-dedot" 444 | version = "3.1.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "9d55e486337acb9973cdea3ec5638c1b3bcb22e573b2b7b41969e0c744d5a15e" 447 | dependencies = [ 448 | "once_cell", 449 | ] 450 | 451 | [[package]] 452 | name = "poly1305" 453 | version = "0.8.0" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" 456 | dependencies = [ 457 | "cpufeatures", 458 | "opaque-debug", 459 | "universal-hash", 460 | ] 461 | 462 | [[package]] 463 | name = "ppv-lite86" 464 | version = "0.2.17" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 467 | 468 | [[package]] 469 | name = "proc-macro2" 470 | version = "1.0.60" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" 473 | dependencies = [ 474 | "unicode-ident", 475 | ] 476 | 477 | [[package]] 478 | name = "quote" 479 | version = "1.0.28" 480 | source = "registry+https://github.com/rust-lang/crates.io-index" 481 | checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" 482 | dependencies = [ 483 | "proc-macro2", 484 | ] 485 | 486 | [[package]] 487 | name = "rand" 488 | version = "0.8.5" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 491 | dependencies = [ 492 | "libc", 493 | "rand_chacha", 494 | "rand_core", 495 | ] 496 | 497 | [[package]] 498 | name = "rand_chacha" 499 | version = "0.3.1" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 502 | dependencies = [ 503 | "ppv-lite86", 504 | "rand_core", 505 | ] 506 | 507 | [[package]] 508 | name = "rand_core" 509 | version = "0.6.4" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 512 | dependencies = [ 513 | "getrandom", 514 | ] 515 | 516 | [[package]] 517 | name = "redox_syscall" 518 | version = "0.3.5" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 521 | dependencies = [ 522 | "bitflags 1.3.2", 523 | ] 524 | 525 | [[package]] 526 | name = "ron" 527 | version = "0.8.0" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff" 530 | dependencies = [ 531 | "base64 0.13.1", 532 | "bitflags 1.3.2", 533 | "serde", 534 | ] 535 | 536 | [[package]] 537 | name = "rust-argon2" 538 | version = "2.1.0" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "9d9848531d60c9cbbcf9d166c885316c24bc0e2a9d3eba0956bb6cbbd79bc6e8" 541 | dependencies = [ 542 | "base64 0.21.7", 543 | "blake2b_simd", 544 | "constant_time_eq 0.3.0", 545 | ] 546 | 547 | [[package]] 548 | name = "rustix" 549 | version = "0.38.30" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" 552 | dependencies = [ 553 | "bitflags 2.4.2", 554 | "errno", 555 | "libc", 556 | "linux-raw-sys", 557 | "windows-sys 0.52.0", 558 | ] 559 | 560 | [[package]] 561 | name = "scopeguard" 562 | version = "1.1.0" 563 | source = "registry+https://github.com/rust-lang/crates.io-index" 564 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 565 | 566 | [[package]] 567 | name = "serde" 568 | version = "1.0.164" 569 | source = "registry+https://github.com/rust-lang/crates.io-index" 570 | checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" 571 | dependencies = [ 572 | "serde_derive", 573 | ] 574 | 575 | [[package]] 576 | name = "serde_derive" 577 | version = "1.0.164" 578 | source = "registry+https://github.com/rust-lang/crates.io-index" 579 | checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" 580 | dependencies = [ 581 | "proc-macro2", 582 | "quote", 583 | "syn", 584 | ] 585 | 586 | [[package]] 587 | name = "shell-words" 588 | version = "1.1.0" 589 | source = "registry+https://github.com/rust-lang/crates.io-index" 590 | checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" 591 | 592 | [[package]] 593 | name = "smallvec" 594 | version = "1.10.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 597 | 598 | [[package]] 599 | name = "str-buf" 600 | version = "1.0.6" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" 603 | 604 | [[package]] 605 | name = "sublime_fuzzy" 606 | version = "0.7.0" 607 | source = "registry+https://github.com/rust-lang/crates.io-index" 608 | checksum = "fa7986063f7c0ab374407e586d7048a3d5aac94f103f751088bf398e07cd5400" 609 | 610 | [[package]] 611 | name = "subtle" 612 | version = "2.5.0" 613 | source = "registry+https://github.com/rust-lang/crates.io-index" 614 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 615 | 616 | [[package]] 617 | name = "supports-color" 618 | version = "2.1.0" 619 | source = "registry+https://github.com/rust-lang/crates.io-index" 620 | checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" 621 | dependencies = [ 622 | "is-terminal", 623 | "is_ci", 624 | ] 625 | 626 | [[package]] 627 | name = "syn" 628 | version = "2.0.18" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" 631 | dependencies = [ 632 | "proc-macro2", 633 | "quote", 634 | "unicode-ident", 635 | ] 636 | 637 | [[package]] 638 | name = "thiserror" 639 | version = "1.0.40" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" 642 | dependencies = [ 643 | "thiserror-impl", 644 | ] 645 | 646 | [[package]] 647 | name = "thiserror-impl" 648 | version = "1.0.40" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" 651 | dependencies = [ 652 | "proc-macro2", 653 | "quote", 654 | "syn", 655 | ] 656 | 657 | [[package]] 658 | name = "typenum" 659 | version = "1.16.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 662 | 663 | [[package]] 664 | name = "unicode-ident" 665 | version = "1.0.9" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" 668 | 669 | [[package]] 670 | name = "universal-hash" 671 | version = "0.5.1" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" 674 | dependencies = [ 675 | "crypto-common", 676 | "subtle", 677 | ] 678 | 679 | [[package]] 680 | name = "version_check" 681 | version = "0.9.4" 682 | source = "registry+https://github.com/rust-lang/crates.io-index" 683 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 684 | 685 | [[package]] 686 | name = "wasi" 687 | version = "0.11.0+wasi-snapshot-preview1" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 690 | 691 | [[package]] 692 | name = "winapi" 693 | version = "0.3.9" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 696 | dependencies = [ 697 | "winapi-i686-pc-windows-gnu", 698 | "winapi-x86_64-pc-windows-gnu", 699 | ] 700 | 701 | [[package]] 702 | name = "winapi-i686-pc-windows-gnu" 703 | version = "0.4.0" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 706 | 707 | [[package]] 708 | name = "winapi-wsapoll" 709 | version = "0.1.1" 710 | source = "registry+https://github.com/rust-lang/crates.io-index" 711 | checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" 712 | dependencies = [ 713 | "winapi", 714 | ] 715 | 716 | [[package]] 717 | name = "winapi-x86_64-pc-windows-gnu" 718 | version = "0.4.0" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 721 | 722 | [[package]] 723 | name = "windows-sys" 724 | version = "0.48.0" 725 | source = "registry+https://github.com/rust-lang/crates.io-index" 726 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 727 | dependencies = [ 728 | "windows-targets 0.48.0", 729 | ] 730 | 731 | [[package]] 732 | name = "windows-sys" 733 | version = "0.52.0" 734 | source = "registry+https://github.com/rust-lang/crates.io-index" 735 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 736 | dependencies = [ 737 | "windows-targets 0.52.0", 738 | ] 739 | 740 | [[package]] 741 | name = "windows-targets" 742 | version = "0.48.0" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 745 | dependencies = [ 746 | "windows_aarch64_gnullvm 0.48.0", 747 | "windows_aarch64_msvc 0.48.0", 748 | "windows_i686_gnu 0.48.0", 749 | "windows_i686_msvc 0.48.0", 750 | "windows_x86_64_gnu 0.48.0", 751 | "windows_x86_64_gnullvm 0.48.0", 752 | "windows_x86_64_msvc 0.48.0", 753 | ] 754 | 755 | [[package]] 756 | name = "windows-targets" 757 | version = "0.52.0" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" 760 | dependencies = [ 761 | "windows_aarch64_gnullvm 0.52.0", 762 | "windows_aarch64_msvc 0.52.0", 763 | "windows_i686_gnu 0.52.0", 764 | "windows_i686_msvc 0.52.0", 765 | "windows_x86_64_gnu 0.52.0", 766 | "windows_x86_64_gnullvm 0.52.0", 767 | "windows_x86_64_msvc 0.52.0", 768 | ] 769 | 770 | [[package]] 771 | name = "windows_aarch64_gnullvm" 772 | version = "0.48.0" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 775 | 776 | [[package]] 777 | name = "windows_aarch64_gnullvm" 778 | version = "0.52.0" 779 | source = "registry+https://github.com/rust-lang/crates.io-index" 780 | checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" 781 | 782 | [[package]] 783 | name = "windows_aarch64_msvc" 784 | version = "0.48.0" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 787 | 788 | [[package]] 789 | name = "windows_aarch64_msvc" 790 | version = "0.52.0" 791 | source = "registry+https://github.com/rust-lang/crates.io-index" 792 | checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" 793 | 794 | [[package]] 795 | name = "windows_i686_gnu" 796 | version = "0.48.0" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 799 | 800 | [[package]] 801 | name = "windows_i686_gnu" 802 | version = "0.52.0" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" 805 | 806 | [[package]] 807 | name = "windows_i686_msvc" 808 | version = "0.48.0" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 811 | 812 | [[package]] 813 | name = "windows_i686_msvc" 814 | version = "0.52.0" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" 817 | 818 | [[package]] 819 | name = "windows_x86_64_gnu" 820 | version = "0.48.0" 821 | source = "registry+https://github.com/rust-lang/crates.io-index" 822 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 823 | 824 | [[package]] 825 | name = "windows_x86_64_gnu" 826 | version = "0.52.0" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" 829 | 830 | [[package]] 831 | name = "windows_x86_64_gnullvm" 832 | version = "0.48.0" 833 | source = "registry+https://github.com/rust-lang/crates.io-index" 834 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 835 | 836 | [[package]] 837 | name = "windows_x86_64_gnullvm" 838 | version = "0.52.0" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" 841 | 842 | [[package]] 843 | name = "windows_x86_64_msvc" 844 | version = "0.48.0" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 847 | 848 | [[package]] 849 | name = "windows_x86_64_msvc" 850 | version = "0.52.0" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" 853 | 854 | [[package]] 855 | name = "x11rb" 856 | version = "0.10.1" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" 859 | dependencies = [ 860 | "gethostname", 861 | "nix 0.24.3", 862 | "winapi", 863 | "winapi-wsapoll", 864 | "x11rb-protocol", 865 | ] 866 | 867 | [[package]] 868 | name = "x11rb-protocol" 869 | version = "0.10.0" 870 | source = "registry+https://github.com/rust-lang/crates.io-index" 871 | checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" 872 | dependencies = [ 873 | "nix 0.24.3", 874 | ] 875 | 876 | [[package]] 877 | name = "xdg" 878 | version = "2.5.0" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "688597db5a750e9cad4511cb94729a078e274308099a0382b5b8203bbc767fee" 881 | dependencies = [ 882 | "home", 883 | ] 884 | 885 | [[package]] 886 | name = "zeroize" 887 | version = "1.6.0" 888 | source = "registry+https://github.com/rust-lang/crates.io-index" 889 | checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" 890 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------