From de5e36aa5d322ca49c9164e55e8f97780cf1b861 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 14 May 2020 01:30:00 -0400 Subject: [PATCH] kbs2: Genesis This is a basically functional version of kbs2, with many components either missing or only partially implemented. --- .gitignore | 1 + Cargo.lock | 693 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 20 ++ src/kbs2/backend.rs | 86 ++++++ src/kbs2/command.rs | 204 +++++++++++++ src/kbs2/config.rs | 185 ++++++++++++ src/kbs2/error.rs | 83 ++++++ src/kbs2/input.rs | 49 ++++ src/kbs2/mod.rs | 8 + src/kbs2/record.rs | 94 ++++++ src/kbs2/session.rs | 92 ++++++ src/kbs2/util.rs | 8 + src/main.rs | 180 ++++++++++++ 13 files changed, 1703 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/kbs2/backend.rs create mode 100644 src/kbs2/command.rs create mode 100644 src/kbs2/config.rs create mode 100644 src/kbs2/error.rs create mode 100644 src/kbs2/input.rs create mode 100644 src/kbs2/mod.rs create mode 100644 src/kbs2/record.rs create mode 100644 src/kbs2/session.rs create mode 100644 src/kbs2/util.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..db6e7038 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,693 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "aho-corasick" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "blake2b_simd" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "cc" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d87b23d6a92cd03af510a5ade527033f6aa6fa92161e2d5863a907d4c5e31d" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim", + "textwrap", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clipboard" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a904646c0340239dcf7c51677b33928bf24fdf424b79a57909c0109075b2e7" +dependencies = [ + "clipboard-win", + "objc", + "objc-foundation", + "objc_id", + "x11-clipboard", +] + +[[package]] +name = "clipboard-win" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a093d6fed558e5fe24c3dfc85a68bb68f1c824f440d3ba5aca189e2998786b" +dependencies = [ + "winapi", +] + +[[package]] +name = "console" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea0f3e2e8d7dba335e913b97f9e1992c86c4399d54f8be1d31c8727d0652064" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "termios", + "unicode-width", + "winapi", + "winapi-util", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "dialoguer" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aa86af7b19b40ef9cbef761ed411a49f0afa06b7b6dcd3dfe2f96a3c546138" +dependencies = [ + "console", + "lazy_static", + "tempfile", +] + +[[package]] +name = "dirs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +dependencies = [ + "cfg-if", + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa0b23de8fd801745c471deffa6e12d248f962c9fd4b4c33787b055599bde7b" +dependencies = [ + "cfg-if", + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61565ff7aaace3525556587bd2dc31d4a07071957be715e63ce7b1eccf51a8f4" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "itoa" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e" + +[[package]] +name = "kbs2" +version = "0.1.0" +dependencies = [ + "Inflector", + "atty", + "clap", + "clipboard", + "dialoguer", + "dirs", + "env_logger", + "log", + "nix", + "serde", + "serde_json", + "shellexpand", + "toml", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e85c08494b21a9054e7fe1374a732aeadaff3980b6990b94bfd3a70f690005" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" + +[[package]] +name = "nix" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e4785f2c3b7589a0d0c1dd60285e1188adac4006e8abd6dd578e1567027363" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "void", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b" + +[[package]] +name = "proc-macro2" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8872cf6f48eee44265156c111456a700ab3483686b3f96df4cf5481c89157319" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c1f4b0efa5fc5e8ceb705136bfee52cfdb6a4e3509f770b478cd6ed434232a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "redox_users" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + +[[package]] +name = "regex" +version = "1.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6020f034922e3194c711b82a627453881bc4682166cabb07134a10c26ba7692" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", + "thread_local", +] + +[[package]] +name = "regex-syntax" +version = "0.6.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe5bd57d1d7414c6b5ed48563a2c855d995ff777729dcd91c369ec7fea395ae" + +[[package]] +name = "remove_dir_all" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e" +dependencies = [ + "winapi", +] + +[[package]] +name = "rust-argon2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "ryu" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3d612bc64430efeb3f7ee6ef26d590dce0c43249217bddc62112540c7941e1" + +[[package]] +name = "serde" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e7b308464d16b56eba9964e4972a3eee817760ab60d88c3f86e1fecb08204c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818fbf6bfa9a42d3bfcaca148547aa00c7b915bec71d1757aa2d44ca68771984" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993948e75b189211a9b31a7528f950c6adc21f9720b6438ff80a7fa2f864cea2" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shellexpand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2b22262a9aaf9464d356f656fea420634f78c881c5eebd5ef5e66d8b9bc603" +dependencies = [ + "dirs", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "syn" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e5aa70697bb26ee62214ae3288465ecec0000f05182f039b477001f08f5ae7" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +dependencies = [ + "cfg-if", + "libc", + "rand", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8038f95fc7a6f351163f4b964af631bd26c9e828f7db085f2a84aca56f70d13b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "termios" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0fcee7b24a25675de40d5bb4de6e41b0df07bc9856295e7e2b3a3600c400c2" +dependencies = [ + "libc", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "toml" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "x11-clipboard" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89bd49c06c9eb5d98e6ba6536cf64ac9f7ee3a009b2f53996d405b3944f6bcea" +dependencies = [ + "xcb", +] + +[[package]] +name = "xcb" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e917a3f24142e9ff8be2414e36c649d47d6cc2ba81f16201cdef96e533e02de" +dependencies = [ + "libc", + "log", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..974f3d52 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "kbs2" +version = "0.1.0" +authors = ["William Woodruff "] +edition = "2018" + +[dependencies] +atty = "0.2.14" +dialoguer = "0.6.2" +dirs = "2.0.2" +clap = "2.33.0" +clipboard = "0.5.0" +env_logger = "0.7" +Inflector = "0.11.4" +log = "0.4" +nix = "0.17.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +shellexpand = "2.0.0" +toml = "0.5.6" diff --git a/src/kbs2/backend.rs b/src/kbs2/backend.rs new file mode 100644 index 00000000..c50b55c1 --- /dev/null +++ b/src/kbs2/backend.rs @@ -0,0 +1,86 @@ +use std::io::Write; +use std::path::Path; +use std::process::{Command, Stdio}; + +use crate::kbs2::config; +use crate::kbs2::error::Error; +use crate::kbs2::record::Record; + +pub trait Backend { + fn create_keypair(&self, path: &Path) -> Result; + fn encrypt(&self, config: &config::Config, record: &Record) -> Result; + fn decrypt(&self, config: &config::Config, encrypted: &str) -> Result; +} + +pub struct AgeCLI { + pub age: String, + pub age_keygen: String, +} + +impl Backend for AgeCLI { + fn create_keypair(&self, path: &Path) -> Result { + if path.exists() { + std::fs::remove_file(path)?; + } + + match Command::new(&self.age_keygen).arg("-o").arg(path).output() { + Err(e) => Err(e.into()), + Ok(output) => { + log::debug!("output: {:?}", output); + let public_key = { + let stderr = String::from_utf8(output.stderr)?; + stderr + .trim_start_matches("Public key: ") + .trim_end() + .to_string() + }; + Ok(public_key) + } + } + } + + fn encrypt(&self, config: &config::Config, record: &Record) -> Result { + let mut child = Command::new(&self.age) + .arg("-a") + .arg("-r") + .arg(&config.public_key) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + + { + let stdin = child + .stdin + .as_mut() + .ok_or::("couldn't get input for encrypting".into())?; + stdin.write_all(serde_json::to_string(record)?.as_bytes())?; + } + + let output = child.wait_with_output()?; + log::debug!("output: {:?}", output); + + Ok(String::from_utf8(output.stdout)?) + } + + fn decrypt(&self, config: &config::Config, encrypted: &str) -> Result { + let mut child = Command::new(&self.age) + .arg("-d") + .arg("-i") + .arg(&config.keyfile) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .spawn()?; + + { + let stdin = child + .stdin + .as_mut() + .ok_or::("couldn't get input for encrypting".into())?; + stdin.write_all(encrypted.as_bytes())?; + } + + let output = child.wait_with_output()?; + + Ok(serde_json::from_str(std::str::from_utf8(&output.stdout)?)?) + } +} diff --git a/src/kbs2/command.rs b/src/kbs2/command.rs new file mode 100644 index 00000000..78dfc058 --- /dev/null +++ b/src/kbs2/command.rs @@ -0,0 +1,204 @@ +use atty::Stream; +use clap::ArgMatches; +use clipboard::{ClipboardContext, ClipboardProvider}; +use inflector::Inflector; +use nix::unistd::{fork, ForkResult}; + +use std::path::Path; + +use crate::kbs2::config; +use crate::kbs2::error::Error; +use crate::kbs2::input; +use crate::kbs2::record; +use crate::kbs2::session; +use crate::kbs2::util; + +pub fn init(matches: &ArgMatches, config_dir: &Path) -> Result<(), Error> { + log::debug!("initializing a new config"); + + if config_dir.join(config::CONFIG_BASENAME).exists() && !matches.is_present("force") { + return Err("refusing to overwrite your current config without --force".into()); + } + + config::initialize(&config_dir) +} + +pub fn new(matches: &ArgMatches, config: config::Config) -> Result<(), Error> { + log::debug!("creating a new record"); + + let session = session::Session::new(config); + + if let Some(pre_hook) = &session.config.commands.new.pre_hook { + log::debug!("pre-hook: {}", pre_hook); + if !util::run_with_status(pre_hook, &[]).unwrap_or(false) { + return Err(format!("pre-hook failed: {}", pre_hook).into()); + } + } + + let label = matches.value_of("label").unwrap(); + if session.has_record(label) && !matches.is_present("force") { + return Err("refusing to overwrite a record without --force".into()); + } + + let terse = atty::isnt(Stream::Stdin) || matches.is_present("terse"); + + // TODO: new_* below is a little silly. This should be de-duped. + match matches.value_of("kind").unwrap() { + "login" => new_login(label, terse, &session)?, + "environment" => new_environment(label, terse, &session)?, + "unstructured" => new_unstructured(label, terse, &session)?, + _ => unreachable!(), + } + + if let Some(post_hook) = &session.config.commands.new.post_hook { + log::debug!("post-hook: {}", post_hook); + if !util::run_with_status(post_hook, &[&label]).unwrap_or(false) { + // NOTE(ww): Maybe make this a warning instead? + return Err(format!("post-hook failed: {}", post_hook).into()); + } + } + + Ok(()) +} + +fn new_login(label: &str, terse: bool, session: &session::Session) -> Result<(), Error> { + // TODO(ww): Passing whether or not a field is sensitive with this tuple is ugly. + // We should really do something like Insensitive("username"), Sensitive("password"), etc. + let fields = input::fields(&[("Username", false), ("Password", true)], terse)?; + let record = record::new_login(label, &fields[0], &fields[1]); + + session.add_record(&record) +} + +fn new_environment(label: &str, terse: bool, session: &session::Session) -> Result<(), Error> { + let fields = input::fields(&[("Variable", false), ("Value", true)], terse)?; + let record = record::new_environment(label, &fields[0], &fields[1]); + + session.add_record(&record) +} + +fn new_unstructured(label: &str, terse: bool, session: &session::Session) -> Result<(), Error> { + let fields = input::fields(&[("Contents", false)], terse)?; + let record = record::new_unstructured(label, &fields[0]); + + session.add_record(&record) +} + +pub fn list(matches: &ArgMatches, config: config::Config) -> Result<(), Error> { + log::debug!("listing records"); + + let (details, filter_kind) = (matches.is_present("details"), matches.is_present("kind")); + let session = session::Session::new(config); + + for label in session.record_labels()? { + let mut display = String::new(); + + if details || filter_kind { + let record = session.get_record(&label)?; + + if filter_kind { + let kind = matches.value_of("kind").unwrap(); + if record.kind.to_string() != kind { + continue; + } + } + + display.push_str(&label); + + if details { + display.push_str(&format!( + "\n\tKind: {}\n\tTimestamp: {}", + record.kind, record.timestamp + )); + } + } else { + display.push_str(&label); + } + + println!("{}", display); + } + + Ok(()) +} + +pub fn rm(matches: &ArgMatches, config: config::Config) -> Result<(), Error> { + log::debug!("removing a record"); + + let session = session::Session::new(config); + session.delete_record(matches.value_of("label").unwrap()) +} + +pub fn dump(matches: &ArgMatches, config: config::Config) -> Result<(), Error> { + log::debug!("dumping a record"); + + let session = session::Session::new(config); + let label = matches.value_of("label").unwrap(); + let record = session.get_record(&label)?; + + if matches.is_present("json") { + println!("{}", serde_json::to_string(&record)?); + } else { + println!("Label: {}\n\tKind: {}", label, record.kind.to_string()); + + for field in record.fields { + println!("\t{}: {}", field.name.to_sentence_case(), field.value); + } + } + + Ok(()) +} + +pub fn pass(matches: &ArgMatches, config: config::Config) -> Result<(), Error> { + log::debug!("getting a login's password"); + + let session = session::Session::new(config); + let label = matches.value_of("label").unwrap(); + let record = session.get_record(&label)?; + + if record.kind != record::RecordKind::Login { + return Err(format!("not a login record: {}", label).into()); + } + + let password = record + .fields + .iter() + .find(|f| f.name == "password") + .ok_or("missing password field in login record")?; + + if matches.is_present("clipboard") { + let clipboard_duration = session.config.commands.pass.clipboard_duration; + let clear_after = session.config.commands.pass.clear_after; + + // NOTE(ww): We fork here for two reasons: one X11 specific, and one general. + // + // 1. X11's clipboard's are tied to processes, meaning that they disappear when the + // creating process terminates. There are ways around that, but the clipboard + // crate doesn't implement them in the interest of simplicity. Therefore, we + // fork to ensure that a process outlives our "main" kbs2 process for pasting purposes. + // 2. Forking gives us a way to clear the password from the clipboard after + // a particular duration, without resorting to an external daemon or other service. + match fork() { + Ok(ForkResult::Child) => { + // TODO(ww): Support x11_clipboard config option. + let mut ctx: ClipboardContext = + ClipboardProvider::new().map_err(|_| "unable to grab the clipboard")?; + + ctx.set_contents(password.value.to_owned()) + .map_err(|_| "unable to store to the clipboard")?; + + std::thread::sleep(std::time::Duration::from_secs(clipboard_duration)); + + if clear_after { + ctx.set_contents("".to_owned()) + .map_err(|_| "unable to clear the clipboard")?; + } + } + Err(_) => return Err("clipboard fork failed".into()), + _ => {} + } + } else { + println!("{}", password.value); + } + + Ok(()) +} diff --git a/src/kbs2/config.rs b/src/kbs2/config.rs new file mode 100644 index 00000000..ac6e2e49 --- /dev/null +++ b/src/kbs2/config.rs @@ -0,0 +1,185 @@ +use dirs; +use serde::{de, Deserialize, Serialize}; +use toml; + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use crate::kbs2::backend::{AgeCLI, Backend}; +use crate::kbs2::error::Error; + +// The default base config directory name, placed relative to the user's config +// directory by default. +pub static CONFIG_BASEDIR: &'static str = "kbs2"; + +// The default basename for the main config file, relative to the configuration +// directory. +pub static CONFIG_BASENAME: &'static str = "kbs2.conf"; + +// The default generate age key is placed in this file, relative to +// the configuration directory. +pub static DEFAULT_KEY_BASENAME: &'static str = "key"; + +// The default base directory name for the secret store, placed relative to +// the user's data directory by default. +pub static STORE_BASEDIR: &'static str = "kbs2"; + +pub static KNOWN_AGE_CLIS: &'static [&(&'static str, &'static str)] = + &[&("rage", "rage-keygen"), &("age", "age-keygen")]; + +#[derive(Default, Debug, Deserialize, Serialize)] +pub struct Config { + #[serde(default)] + pub debug: bool, + #[serde(rename = "age-backend")] + pub age_backend: String, + #[serde(rename = "age-keygen-backend")] + pub age_keygen_backend: String, + #[serde(rename = "public-key")] + pub public_key: String, + #[serde(deserialize_with = "deserialize_with_tilde")] + pub keyfile: String, + #[serde(deserialize_with = "deserialize_with_tilde")] + pub store: String, + #[serde(default)] + pub commands: CommandConfigs, +} + +#[derive(Default, Debug, Deserialize, Serialize)] +#[serde(default)] +pub struct CommandConfigs { + pub new: NewConfig, + pub pass: PassConfig, +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(default)] +pub struct NewConfig { + // TODO(ww): This deserialize_with is ugly. There's probably a better way to do this. + #[serde(deserialize_with = "deserialize_optional_with_tilde")] + #[serde(rename = "pre-hook")] + pub pre_hook: Option, + #[serde(deserialize_with = "deserialize_optional_with_tilde")] + #[serde(rename = "post-hook")] + pub post_hook: Option, +} + +impl Default for NewConfig { + fn default() -> Self { + NewConfig { + pre_hook: None, + post_hook: None, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(default)] +pub struct PassConfig { + #[serde(rename = "clipboard-duration")] + pub clipboard_duration: u64, + #[serde(rename = "clear-after")] + pub clear_after: bool, + #[serde(rename = "x11-clipboard")] + pub x11_clipboard: X11Clipboard, +} + +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +pub enum X11Clipboard { + Clipboard, + Primary, +} + +impl Default for PassConfig { + fn default() -> Self { + PassConfig { + clipboard_duration: 10, + clear_after: true, + x11_clipboard: X11Clipboard::Clipboard, + } + } +} + +fn deserialize_with_tilde<'de, D>(deserializer: D) -> Result +where + D: de::Deserializer<'de>, +{ + let unexpanded: &str = Deserialize::deserialize(deserializer)?; + Ok(shellexpand::tilde(unexpanded).into_owned()) +} + +fn deserialize_optional_with_tilde<'de, D>(deserializer: D) -> Result, D::Error> +where + D: de::Deserializer<'de>, +{ + let unexpanded: Option<&str> = Deserialize::deserialize(deserializer)?; + + match unexpanded { + Some(unexpanded) => Ok(Some(shellexpand::tilde(unexpanded).into_owned())), + None => Ok(None), + } +} + +pub fn find_config_dir() -> Result { + match dirs::config_dir() { + Some(path) => Ok(path.join(CONFIG_BASEDIR)), + // NOTE(ww): Probably excludes *BSD users for no good reason. + None => Err("couldn't find a suitable config directory".into()), + } +} + +fn data_dir() -> Result { + match dirs::data_dir() { + Some(dir) => Ok(dir + .join(STORE_BASEDIR) + .to_str() + .ok_or::("couldn't stringify user data dir".into())? + .into()), + None => Err("couldn't find a suitable data directory for the secret store".into()), + } +} + +pub fn find_age_cli() -> Result { + for (age, age_keygen) in KNOWN_AGE_CLIS { + if Command::new(age).arg("-h").output().is_ok() + && Command::new(age_keygen).arg("-h").output().is_ok() + { + return Ok(AgeCLI { + age: (*age).into(), + age_keygen: (*age_keygen).into(), + }); + } + } + + Err("couldn't find an age-compatible CLI".into()) +} + +pub fn initialize(config_dir: &Path) -> Result<(), Error> { + let backend = find_age_cli()?; + + let keyfile = config_dir.join(DEFAULT_KEY_BASENAME); + let public_key = backend.create_keypair(&keyfile)?; + log::debug!("public key: {}", public_key); + + let serialized = toml::to_string(&Config { + debug: false, + age_backend: backend.age, + age_keygen_backend: backend.age_keygen, + public_key: public_key, + keyfile: keyfile.to_str().unwrap().into(), + store: data_dir()?, + commands: Default::default(), + })?; + + fs::write(config_dir.join(CONFIG_BASENAME), serialized)?; + + Ok(()) +} + +pub fn load(config_dir: &Path) -> Result { + let config_path = config_dir.join(CONFIG_BASENAME); + let contents = fs::read_to_string(config_path)?; + + toml::from_str(&contents).map_err(|e| format!("config loading error: {}", e).into()) +} diff --git a/src/kbs2/error.rs b/src/kbs2/error.rs new file mode 100644 index 00000000..b0a87098 --- /dev/null +++ b/src/kbs2/error.rs @@ -0,0 +1,83 @@ +use std::error; +use std::fmt; + +// TODO(ww): This custom Error and collection of From<...>s is terrible. +// It should be replaced with anyhow: https://github.com/dtolnay/anyhow +#[derive(Debug, Clone)] +pub struct Error { + message: String, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } +} + +impl From<&str> for Error { + fn from(err: &str) -> Error { + Error { + message: err.to_string(), + } + } +} + +impl From for Error { + fn from(err: String) -> Error { + Error { message: err } + } +} + +impl From for Error { + fn from(err: std::io::Error) -> Error { + Error { + message: err.to_string(), + } + } +} + +impl From for Error { + fn from(err: toml::de::Error) -> Error { + Error { + message: err.to_string(), + } + } +} + +impl From for Error { + fn from(err: toml::ser::Error) -> Error { + Error { + message: err.to_string(), + } + } +} + +impl From for Error { + fn from(err: std::string::FromUtf8Error) -> Error { + Error { + message: err.to_string(), + } + } +} + +impl From for Error { + fn from(err: std::str::Utf8Error) -> Error { + Error { + message: err.to_string(), + } + } +} + +impl From for Error { + fn from(err: serde_json::error::Error) -> Error { + Error { + message: err.to_string(), + } + } +} diff --git a/src/kbs2/input.rs b/src/kbs2/input.rs new file mode 100644 index 00000000..79c40907 --- /dev/null +++ b/src/kbs2/input.rs @@ -0,0 +1,49 @@ +use dialoguer::{Input, Password}; + +use std::io::{self, Read}; + +use crate::kbs2::error::Error; + +// TODO(ww): Make this configurable. +pub static TERSE_IFS: &'static str = "\x01"; + +fn terse_fields(names: &[(&str, bool)]) -> Result, Error> { + let mut input = String::new(); + io::stdin().read_to_string(&mut input)?; + + let fields = input.split(TERSE_IFS).collect::>(); + if fields.len() == names.len() { + Ok(fields.iter().map(|f| f.to_string()).collect()) + } else { + Err(format!( + "field count mismatch: expected {}, found {}", + names.len(), + fields.len() + ) + .as_str() + .into()) + } +} + +fn interactive_fields(names: &[(&str, bool)]) -> Result, Error> { + let mut fields = vec![]; + + for (name, sensitive) in names { + let field = match sensitive { + true => Password::new().with_prompt(*name).interact()?, + false => Input::::new().with_prompt(*name).interact()?, + }; + + fields.push(field); + } + + Ok(fields) +} + +pub fn fields(names: &[(&str, bool)], terse: bool) -> Result, Error> { + if terse { + terse_fields(names) + } else { + interactive_fields(names) + } +} diff --git a/src/kbs2/mod.rs b/src/kbs2/mod.rs new file mode 100644 index 00000000..c20c3d9c --- /dev/null +++ b/src/kbs2/mod.rs @@ -0,0 +1,8 @@ +pub mod backend; +pub mod command; +pub mod config; +pub mod error; +pub mod input; +pub mod record; +pub mod session; +pub mod util; diff --git a/src/kbs2/record.rs b/src/kbs2/record.rs new file mode 100644 index 00000000..e21e956f --- /dev/null +++ b/src/kbs2/record.rs @@ -0,0 +1,94 @@ +use serde::{Deserialize, Serialize}; + +use std::time::{SystemTime, UNIX_EPOCH}; + +// TODO(ww): Figure out how to generate this from the RecordKind enum below. +pub static RECORD_KINDS: &'static [&'static str] = &["login", "environment", "unstructured"]; + +#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)] +pub enum RecordKind { + Login, + Environment, + Unstructured, +} + +impl std::fmt::Display for RecordKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + RecordKind::Login => write!(f, "login"), + RecordKind::Environment => write!(f, "environment"), + RecordKind::Unstructured => write!(f, "unstructured"), + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Record { + pub timestamp: u64, + pub label: String, + pub kind: RecordKind, + pub fields: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Field { + pub name: String, + pub value: String, +} + +fn current_timestamp() -> u64 { + // NOTE(ww): This unwrap should be safe, since every time should be + // greater than or equal to the epoch. + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() +} + +pub fn new_login(label: &str, username: &str, password: &str) -> Record { + Record { + timestamp: current_timestamp(), + label: label.to_string(), + kind: RecordKind::Login, + fields: vec![ + Field { + name: "username".into(), + value: username.into(), + }, + Field { + name: "password".into(), + value: password.into(), + }, + ], + } +} + +pub fn new_environment(label: &str, variable: &str, value: &str) -> Record { + Record { + timestamp: current_timestamp(), + label: label.to_string(), + kind: RecordKind::Login, + fields: vec![ + Field { + name: "variable".into(), + value: variable.into(), + }, + Field { + name: "value".into(), + value: value.into(), + }, + ], + } +} + +pub fn new_unstructured(label: &str, contents: &str) -> Record { + Record { + timestamp: current_timestamp(), + label: label.to_string(), + kind: RecordKind::Login, + fields: vec![Field { + name: "contents".into(), + value: contents.into(), + }], + } +} diff --git a/src/kbs2/session.rs b/src/kbs2/session.rs new file mode 100644 index 00000000..26460979 --- /dev/null +++ b/src/kbs2/session.rs @@ -0,0 +1,92 @@ +use std::fs; +use std::io; +use std::path::Path; + +use crate::kbs2::backend; +use crate::kbs2::config; +use crate::kbs2::error::Error; +use crate::kbs2::record; + +pub struct Session { + pub backend: Box, + pub config: config::Config, +} + +impl Session { + pub fn new(config: config::Config) -> Session { + Session { + backend: Box::new(backend::AgeCLI { + age: config.age_backend.clone(), + age_keygen: config.age_keygen_backend.clone(), + }), + config: config, + } + } + + pub fn record_labels(&self) -> Result, Error> { + let store = Path::new(&self.config.store); + + if !store.is_dir() { + return Err("secret store is not a directory".into()); + } + + let mut labels = vec![]; + for entry in fs::read_dir(store)? { + let path = entry?.path(); + if !path.is_file() { + log::debug!("skipping non-file in store: {:?}", path); + continue; + } + + // NOTE(ww): This unwrap is safe, since file_name always returns Some + // for non-directories. + let label = path.file_name().unwrap(); + + // NOTE(ww): This one isn't safe, but we don't care. Non-UTF-8 labels aren't supported. + labels.push(label.to_str().unwrap().into()); + } + + Ok(labels) + } + + pub fn has_record(&self, label: &str) -> bool { + let record_path = Path::new(&self.config.store).join(label); + + record_path.is_file() + } + + pub fn get_record(&self, label: &str) -> Result { + if !self.has_record(label) { + return Err(format!("no such record: {}", label).into()); + } + + let record_path = Path::new(&self.config.store).join(label); + let record_contents = fs::read_to_string(&record_path).map_err(|e| match e.kind() { + io::ErrorKind::NotFound => format!("no such record: {}", label).into(), + _ => e.to_string(), + })?; + + match self.backend.decrypt(&self.config, &record_contents) { + Ok(record) => Ok(record), + Err(e) => Err(e), + } + } + + pub fn add_record(&self, record: &record::Record) -> Result<(), Error> { + let record_path = Path::new(&self.config.store).join(&record.label); + + let record_contents = self.backend.encrypt(&self.config, record)?; + std::fs::write(&record_path, &record_contents)?; + + Ok(()) + } + + pub fn delete_record(&self, label: &str) -> Result<(), Error> { + let record_path = Path::new(&self.config.store).join(label); + + std::fs::remove_file(&record_path).map_err(|e| match e.kind() { + io::ErrorKind::NotFound => format!("no such record: {}", label).into(), + _ => e.into(), + }) + } +} diff --git a/src/kbs2/util.rs b/src/kbs2/util.rs new file mode 100644 index 00000000..2970e1b0 --- /dev/null +++ b/src/kbs2/util.rs @@ -0,0 +1,8 @@ +use std::process::Command; + +pub fn run_with_status(command: &str, args: &[&str]) -> Option { + Command::new(command) + .args(args) + .status() + .map_or(None, |s| Some(s.success())) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 00000000..0db69732 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,180 @@ +use clap::{App, AppSettings, Arg}; + +use std::path::Path; +use std::process; + +mod kbs2; + +fn app<'a, 'b>() -> App<'a, 'b> { + // TODO(ww): Put this in a separate file, or switch to YAML. + // The latter probably won't work with env!, though. + App::new(env!("CARGO_PKG_NAME")) + .setting(AppSettings::AllowExternalSubcommands) + .version(env!("CARGO_PKG_VERSION")) + .about(env!("CARGO_PKG_DESCRIPTION")) + .arg( + Arg::with_name("config") + .help("use the specified config file") + .short("c") + .long("config") + .value_name("FILE") + .takes_value(true), + ) + .subcommand( + App::new("init") + .about("initialize kbs2 with a new config") + .arg( + Arg::with_name("force") + .help("overwrite, if already present") + .short("f") + .long("force"), + ) + .arg( + Arg::with_name("keygen") + .help("generate a new key with the config") + .short("k") + .long("keygen"), + ), + ) + .subcommand( + App::new("new") + .about("create a new record") + .arg( + Arg::with_name("force") + .help("overwrite, if already present") + .short("f") + .long("force"), + ) + .arg( + Arg::with_name("kind") + .help("the kind of record to create") + .index(1) + .required(true) + .possible_values(kbs2::record::RECORD_KINDS), + ) + .arg( + Arg::with_name("label") + .help("the record's label") + .index(2) + .required(true), + ), + ) + .subcommand( + App::new("list") + .about("list records") + .arg( + Arg::with_name("details") + .help("print (non-field) details for each record") + .short("d") + .long("details"), + ) + .arg( + Arg::with_name("kind") + .help("list only records of this kind") + .short("k") + .long("kind") + .takes_value(true) + .possible_values(kbs2::record::RECORD_KINDS), + ), + ) + .subcommand( + App::new("rm").about("remove a record").arg( + Arg::with_name("label") + .help("the record's label") + .index(1) + .required(true), + ), + ) + .subcommand( + App::new("dump") + .about("dump a record") + .arg( + Arg::with_name("label") + .help("the record's label") + .index(1) + .required(true), + ) + .arg( + Arg::with_name("json") + .help("dump in JSON format") + .short("j") + .long("json"), + ), + ) + .subcommand( + App::new("pass") + .about("get the password in a login record") + .arg( + Arg::with_name("label") + .help("the record's label") + .index(1) + .required(true), + ) + .arg( + Arg::with_name("clipboard") + .help("copy the password to the clipboard") + .short("c") + .long("clipboard"), + ), + ) +} + +fn run() -> Result<(), kbs2::error::Error> { + let matches = app().get_matches(); + + let config_dir = match matches.value_of("config") { + Some(path) => Path::new(path).to_path_buf(), + None => kbs2::config::find_config_dir()?, + }; + + log::debug!("config dir: {:?}", config_dir); + std::fs::create_dir_all(&config_dir)?; + + // `init` is a special case, since it doesn't have access to a preexisting config. + if let ("init", Some(matches)) = matches.subcommand() { + kbs2::command::init(&matches, &config_dir) + } else { + let config = kbs2::config::load(&config_dir)?; + + log::debug!("loaded config: {:?}", config); + + match matches.subcommand() { + ("new", Some(matches)) => kbs2::command::new(&matches, config), + ("list", Some(matches)) => kbs2::command::list(&matches, config), + ("rm", Some(matches)) => kbs2::command::rm(&matches, config), + ("dump", Some(matches)) => kbs2::command::dump(&matches, config), + ("pass", Some(matches)) => kbs2::command::pass(&matches, config), + (cmd, Some(matches)) => { + let ext_args: Vec<&str> = match matches.values_of("") { + Some(values) => values.collect(), + None => vec![], + }; + + log::debug!("external command requested: {} (args: {:?})", cmd, ext_args); + + match kbs2::util::run_with_status(cmd, &ext_args) { + Some(true) => Ok(()), + Some(false) => process::exit(2), + None => Err(format!("no such command: {}", cmd).into()), + } + } + ("", None) => Ok(println!( + "{}\n\nSee --help for more information.", + matches.usage() + )), + _ => unreachable!(), + } + } +} + +fn main() { + env_logger::init(); + + process::exit(match run() { + Ok(()) => 0, + Err(e) => { + eprintln!("Fatal: {}", e); + 1 + } + }); +}