diff --git a/Cargo.lock b/Cargo.lock index 5986350..93d7cc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -77,9 +88,18 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "37bf3594c4c988a53154954629820791dde498571819ae4ca50ca811e060cc95" + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "autocfg" @@ -99,6 +119,15 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -113,9 +142,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.1.28" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "shlex", ] @@ -140,11 +169,21 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" -version = "4.5.19" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -152,9 +191,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.19" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -222,12 +261,42 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crossterm" version = "0.25.0" @@ -269,6 +338,38 @@ dependencies = [ "winapi", ] +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "dirs" version = "5.0.1" @@ -290,6 +391,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dyn-clone" version = "1.0.17" @@ -366,6 +478,16 @@ dependencies = [ "byteorder", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -401,6 +523,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.9" @@ -449,6 +580,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "inquire" version = "0.7.5" @@ -486,9 +626,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -501,9 +641,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libredox" @@ -631,6 +771,25 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -655,9 +814,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" dependencies = [ "unicode-ident", ] @@ -671,6 +830,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.5.7" @@ -768,6 +957,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -826,11 +1026,17 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" -version = "2.0.79" +version = "2.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" dependencies = [ "proc-macro2", "quote", @@ -908,6 +1114,7 @@ dependencies = [ "tempfile", "toml", "which", + "zip", ] [[package]] @@ -944,6 +1151,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -968,6 +1181,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -976,9 +1195,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -987,9 +1206,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -1002,9 +1221,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1012,9 +1231,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -1025,9 +1244,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "which" @@ -1243,3 +1462,66 @@ name = "winsafe" version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" +dependencies = [ + "aes", + "arbitrary", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "hmac", + "indexmap", + "memchr", + "pbkdf2", + "rand", + "sha1", + "thiserror", + "zeroize", +] diff --git a/Cargo.toml b/Cargo.toml index 55e848c..638141f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ regex = "1.11.0" serde = { version = "1.0.210", features = ["derive"] } toml = "0.8.19" which = "6.0.3" +zip = { version = "2.2.0", default-features = false, features = ["aes-crypto"] } [target.'cfg(unix)'.dependencies] stdio-override = "0.1.3" diff --git a/src/boxops.rs b/src/boxops.rs index 2740671..a087d8e 100644 --- a/src/boxops.rs +++ b/src/boxops.rs @@ -58,7 +58,11 @@ pub fn file_manager() -> Result<()> { pub fn edit_box(cur_box: &str, diffwith: Option) { let boxpath = get_inbox_file(cur_box); - _ = TaskBox::new(boxpath.clone()); // only touch file + let tb = TaskBox::new(boxpath.clone()); + if tb.encrypted { + println!("cannot edit {}box, plz decrypt first", S_failure!(LOCKED)); + std::process::exit(1); + } if let Some(other) = diffwith { let otherf = if other.ends_with(".md") { @@ -102,14 +106,26 @@ pub fn list_boxes() { let mut boxes = Vec::new(); for entry in std::fs::read_dir(&basedir).expect("cannot read dir") { let path = entry.expect("cannot get entry").path(); - if path.is_file() && path.extension() == Some(OsStr::new("md")) { - boxes.push(String::from(path.file_stem().unwrap().to_str().unwrap())) + if path.is_file() { + if path.extension() == Some(OsStr::new("md")) { + boxes.push((String::from(path.file_stem().unwrap().to_str().unwrap()), false)) + } else if path.extension() == Some(OsStr::new("mdx")) { + boxes.push((String::from(path.file_stem().unwrap().to_str().unwrap()), true)) + } } } - boxes.sort(); boxes.reverse(); boxes.into_iter().for_each( - |b| { - print!("{} {}",S_checkbox!(TASKBOX), b); - if let Some(alias) = get_box_alias(&b) { + boxes.sort_by(|a,b| b.0.cmp(&a.0)); + boxes.into_iter().for_each( + |(boxname, encrypted)| { + if encrypted { + print!("{} ", S_warning!(LOCKED)); + } else { + print!(" "); + + } + print!("{} {}",S_checkbox!(TASKBOX), boxname); + let alias = get_box_alias(&boxname); + if alias != boxname { println!(" ({})", S_hints!(alias)) } else { println!() diff --git a/src/cli.rs b/src/cli.rs index 8144283..9e80467 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -69,6 +69,12 @@ pub enum Commands { #[clap(visible_aliases(["lb"]))] Listbox, + /// -> encrypt todo box file + Enc, + + /// -> decrypt todo box file + Dec, + /// -> edit todo inbox file #[clap(visible_aliases(["e", "ed"]))] Edit { diff --git a/src/main.rs b/src/main.rs index e8e83b9..4987f9c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,6 +43,8 @@ fn main() { match args.command { Some(Commands::List) | None => TaskBox::new(inbox_path).list(false), Some(Commands::Listall) => TaskBox::new(inbox_path).list(true), + Some(Commands::Enc) => TaskBox::new(inbox_path).encrypt().unwrap(), + Some(Commands::Dec) => TaskBox::new(inbox_path).decrypt().unwrap(), Some(Commands::Routines) => TaskBox::new(get_inbox_file(ROUTINE_BOXNAME)).list(true), Some(Commands::Count) => { @@ -151,6 +153,7 @@ fn main() { inbox_path = get_inbox_file("routine") } let mut todo = TaskBox::new(inbox_path); + todo.load(); #[allow(clippy::redundant_closure)] let input = what.unwrap_or_else(|| i_gettext()); diff --git a/src/styles.rs b/src/styles.rs index fee09d7..30b6f9f 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -16,6 +16,7 @@ pub const QUESTION: &str = "󱜹"; pub const ROUTINES: &str = "󰃯"; pub const DATESTAMP: &str = "󰴹"; // 󰃵 pub const WEEKLINE: &str = "󰕶"; +pub const LOCKED: &str = "󰍁"; // S means Style #[macro_export] macro_rules! S_fpath { ($e:expr) => { $e.to_string().purple() }; } @@ -68,6 +69,21 @@ pub fn get_confirm_style() -> RenderConfig<'static> { .with_attr(Attributes::BOLD) ) } +pub fn get_pass_input_style() -> RenderConfig<'static> { + RenderConfig::default() + .with_prompt_prefix(LOCKED.into()) + .with_answered_prompt_prefix(LOCKED.into()) + .with_help_message( + StyleSheet::default() + .with_fg(Color::DarkGrey) + .with_attr(Attributes::ITALIC) + ) + .with_answer( + StyleSheet::default() + .with_fg(Color::DarkGreen) + .with_attr(Attributes::BOLD) + ) +} pub fn get_text_input_style() -> RenderConfig<'static> { RenderConfig::default() .with_prompt_prefix(CHECKBOX.into()) diff --git a/src/taskbox.rs b/src/taskbox.rs index e1d47ab..fcc95a1 100644 --- a/src/taskbox.rs +++ b/src/taskbox.rs @@ -1,9 +1,12 @@ use std::fs; use std::path::{Path, PathBuf}; use std::collections::HashSet; +use std::io::{Read, Write}; use regex::Regex; use colored::Colorize; use lazy_static::lazy_static; +use zip::*; +use anyhow::Result; use crate::cli::*; use crate::util::*; @@ -19,7 +22,7 @@ lazy_static! { Regex::new(r"\{󰃯:(daily|weekly|biweekly|qweekly|monthly)\} (.*)").unwrap(); } -pub const INBOX_NAME :&str = "INBOX"; +pub const INBOX_BOXNAME :&str = "INBOX"; pub const ROUTINE_BOXNAME :&str = "ROUTINES"; const PREFIX_OPEN :&str = "- [ ] "; @@ -29,20 +32,27 @@ const PREFIX_SUBT :&str = " 󱞩 "; #[derive(Debug)] pub struct TaskBox { pub fpath: PathBuf, - pub title: Option, + pub tbname: String, pub alias: Option, pub tasks: Vec<(String, bool)>, pub selected: Option>, + pub encrypted: bool, + pub passwd_mem: Option, } impl TaskBox { pub fn new(fpath: PathBuf) -> Self { + let encrypted = fpath.extension().unwrap_or_default() == "mdx"; + let tbname = fpath.file_stem().unwrap().to_str().unwrap().to_string(); + Self { fpath, - title: None, // None means not loaded - alias: None, + tbname, + alias: None, // None means not loaded tasks: vec![], selected: None, + encrypted, + passwd_mem: None, } } @@ -53,10 +63,25 @@ impl TaskBox { sib } + fn _load_file(&mut self) -> String { + if self.encrypted { + let passwd = i_getpass(false, Some("the password for encrypted box:")); + self.passwd_mem = Some(passwd.clone()); + self._load_file_with_pass(&passwd).unwrap_or_else(|_| { + println!("{}", S_failure!("wrong password, abort")); + std::process::exit(1); + }) + } else { + fs::read_to_string(&self.fpath).expect("Failed to read file") + } + } + + // load from md file, should be called only once pub fn load(&mut self) { - if self.title.is_some() { return } // avoid load() twice + if self.alias.is_some() { return } // avoid load() twice if ! self.fpath.exists() { + // initial box file `touch` let fpath = &self.fpath; let title = fpath.file_stem() .and_then(|s| s.to_str()) @@ -88,9 +113,7 @@ impl TaskBox { let mut postfix_sub = String::new(); let mut last_is_sub = false; - for (index, rline) in fs::read_to_string(&self.fpath) - .expect("Failed to read file") - .lines().enumerate() { + for (index, rline) in self._load_file().lines().enumerate() { let line = rline.trim_end(); if index == 0 { @@ -133,13 +156,12 @@ impl TaskBox { } } - self.alias = get_box_alias(&title); - self.title = Some(title); + self.alias = Some(get_box_alias(&title)); self.tasks = tasks; } - fn _dump(&mut self) { - let mut content = format!("# {}\n\n", self.title.clone().unwrap()); + fn _dump(&mut self) -> Result<()> { + let mut content = format!("# {}\n\n", self.tbname.clone()); for (mut task, done) in self.tasks.clone() { task = task.trim_end().to_string(); @@ -154,7 +176,14 @@ impl TaskBox { content.push_str(&(task + "\n")) } - fs::write(&self.fpath, content).expect("cannot write file") + if self.encrypted { + self._dump_with_passwd(&content, self.passwd_mem.as_ref().unwrap())? + } else { + fs::write(&self.fpath, &content)? + } + + self.alias = None; // trigger load() next time + Ok(()) } // mark the task which has "done" subtask as "done" @@ -207,18 +236,10 @@ impl TaskBox { if tasks.is_empty() { return } tasks.iter().for_each(|t| self._addone(t.to_string())); - self._dump() + self._dump().unwrap() } pub fn collect_from(&mut self, tb_from: &mut TaskBox) { - fn _get_nickname(fp: &Path) -> String { - let title_from_fp = fp.file_stem() - .and_then(|s| s.to_str()) - .unwrap() - .to_string(); - get_box_alias(&title_from_fp).unwrap_or(title_from_fp) - } - let tasks_in = tb_from.get_all_to_mark(); if tasks_in.is_empty() { return } @@ -227,8 +248,8 @@ impl TaskBox { } // print title line - let from = _get_nickname(&tb_from.fpath); - let to = _get_nickname(&self.fpath); + let from = tb_from.alias.clone().unwrap(); + let to = self.alias.clone().unwrap_or(get_box_alias(&self.tbname)); println!("{} {} {} {}", S_movefrom!(from), MOVING, S_moveto!(to), PROGRESS); @@ -291,7 +312,7 @@ impl TaskBox { S_failure!(WARN), S_checkbox!(CHECKBOX), task); - } else if RE_ROUTINES_CHECKOUT.is_match(&task) && to == INBOX_NAME { + } else if RE_ROUTINES_CHECKOUT.is_match(&task) && to == INBOX_BOXNAME { // ignore checkout routine task println!("{} {} : {} {}", S_failure!(WARN), @@ -310,9 +331,9 @@ impl TaskBox { // "ROUTINES" not drain if from != ROUTINE_BOXNAME { - tb_from._dump(); + tb_from._dump().unwrap(); } - self._dump(); + self._dump().unwrap(); } pub fn add(&mut self, what: String, @@ -323,7 +344,7 @@ impl TaskBox { let task = if let Some(routine) = routine { format!("{{{}:{} {}{} 󰳟}} {}", - ROUTINES, + ROUTINES, match routine { Routine::Daily => "d", Routine::Weekly => "w", @@ -340,7 +361,7 @@ impl TaskBox { } else { what }; self._addone(task); - self._dump() + self._dump().unwrap() } pub fn get_all_to_mark(&mut self) -> Vec { @@ -372,7 +393,7 @@ impl TaskBox { let left : Vec<_> = self.tasks.iter().filter(|(_,done)| !done).map(|(task, _)| task.clone()).collect(); let dones : Vec<_> = self.tasks.iter().filter(|(_,done)| *done).map(|(task, _)| task.clone()).collect(); - let checkbox_style = if self.title == Some("ROUTINES".into()) { + let checkbox_style = if self.tbname == "ROUTINES" { ROUTINES } else { CHECKBOX @@ -447,7 +468,7 @@ impl TaskBox { self.tasks.retain(|(task, done)| !done || !items.contains(task)) } - self._dump(); + self._dump().unwrap() } pub fn purge(&mut self, sort: bool) { @@ -484,7 +505,7 @@ impl TaskBox { if sort { newtasks.sort_by(|a, b| b.1.cmp(&a.1)) } self.tasks = newtasks; - self._dump(); + self._dump().unwrap() } // specified markdown file -> cur @@ -494,7 +515,7 @@ impl TaskBox { let fpath = Path::new(&mdfile); if ! fpath.is_file() { - eprintln!("not a file or not exists: {}", S_fpath!(mdfile)); + eprintln!("not a file or not exists: {}", S_fpath!(mdfile)); std::process::exit(1) } println!("importing {} {}", S_fpath!(mdfile), PROGRESS); @@ -524,4 +545,113 @@ impl TaskBox { self.add_tasks(newt); self.sibling(ROUTINE_BOXNAME).add_tasks(newr); } + + fn _dump_with_passwd(&self, content: &str, passwd: &str) -> Result<()> { + let mut zfile = ZipWriter::new(fs::File::create(&self.fpath)?); + let zopt = write::SimpleFileOptions::default() + .compression_method(CompressionMethod::Stored) + .with_aes_encryption(AesMode::Aes256, passwd); + zfile.start_file(&self.tbname, zopt)?; + zfile.write_all(content.as_bytes())?; + zfile.finish()?; + Ok(()) + } + + fn _load_file_with_pass(&self, passwd: &str) -> Result { + let mut zfile = ZipArchive::new(fs::File::open(&self.fpath)?)?; + let tbname = self.tbname.clone(); + + if zfile.len() != 1 { + println!("Taskbox: {} is not a valid encrypted taskbox, skipped", S_checkbox!(tbname)); + std::process::exit(1); + } + + let mut entry = zfile.by_index_decrypt(0, passwd.as_bytes())?; + if entry.name() != tbname { + println!("Taskbox: {} is not a valid encrypted taskbox, skipped", S_checkbox!(tbname)); + std::process::exit(1); + } + + let mut content = String::new(); + entry.read_to_string(&mut content)?; + + Ok(content) + } + + pub fn encrypt(&mut self) -> Result<()> { + let tbname = self.tbname.clone(); + + // validating encryption status + if self.encrypted { + println!("Taskbox: {} was already encrypted, skipped", S_checkbox!(tbname)); + std::process::exit(1); + } + + // validating box name: reserved and date format box cannot enc + let can_be = match tbname.as_ref() { + ROUTINE_BOXNAME | INBOX_BOXNAME => false, + _ if Regex::new(r"\d{4}-\d{2}-\d{2}").unwrap().is_match(&tbname) => false, + _ => true + }; + if ! can_be { + println!("Taskbox: {} cannot be encrypted, skipped", S_checkbox!(tbname)); + std::process::exit(1); + } + if !self.fpath.exists() { + println!("Taskbox: {} hasn't initialized, skipped", S_checkbox!(tbname)); + std::process::exit(1); + } + + let passwd = i_getpass(true, None); + if passwd.is_empty() { + println!("password is empty, canceled"); + std::process::exit(1); + } + + println!("Encrypting taskbox: {}", S_checkbox!(tbname)); + + let original_fpath = self.fpath.clone(); + self.fpath.set_extension("mdx"); + self.encrypted = true; + + self._dump_with_passwd(&fs::read_to_string(&original_fpath)?, &passwd)?; + fs::remove_file(&original_fpath)?; + + Ok(()) + } + + pub fn decrypt(&mut self) -> Result<()> { + let tbname = self.tbname.clone(); + + // validating ext name + if ! self.encrypted { + println!("Taskbox: {} was not encrypted, skipped", S_checkbox!(tbname)); + std::process::exit(1); + } + if ! self.fpath.exists() { + println!("Taskbox: {} hasn't initialized, skipped", S_checkbox!(tbname)); + std::process::exit(1); + } + + let passwd = i_getpass(false, None); + if passwd.is_empty() { + println!("password is empty, canceled"); + std::process::exit(1); + } + + println!("Decrypting taskbox: {}", S_checkbox!(tbname)); + + let content = self._load_file_with_pass(&passwd).unwrap_or_else(|_| { + println!("{}", S_failure!("wrong password, abort")); + std::process::exit(1); + }); + let original_fpath = self.fpath.clone(); + + self.fpath.set_extension("md"); + self.encrypted = false; + fs::write(&self.fpath, &content)?; + fs::remove_file(&original_fpath)?; + + Ok(()) + } } diff --git a/src/util.rs b/src/util.rs index 027aaa0..e2e595d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -91,16 +91,13 @@ pub fn match_routine(kind: &str, s_date_str: &str, match_to: &str) -> bool { s_date == match_to_date } -pub fn get_box_alias(name_in: &str) -> Option { - let alias = match name_in { +pub fn get_box_alias(name_in: &str) -> String { + match name_in { _ if name_in == get_today() => "today", _ if name_in == get_tomorrow() => "tomorrow", _ if name_in == get_yesterday() => "yesterday", - _ => "", - }; - - if alias.is_empty() { None } - else { Some(alias.to_string()) } + _ => name_in, + }.into() } pub fn get_box_unalias(alias: &str) -> String { @@ -108,7 +105,7 @@ pub fn get_box_unalias(alias: &str) -> String { "today" => get_today(), "yesterday" => get_yesterday(), "tomorrow" => get_tomorrow(), - "inbox" => taskbox::INBOX_NAME.into(), + "inbox" => taskbox::INBOX_BOXNAME.into(), "routine" | "routines" => taskbox::ROUTINE_BOXNAME.into(), _ => alias.into(), } @@ -116,7 +113,10 @@ pub fn get_box_unalias(alias: &str) -> String { pub fn get_inbox_file(inbox: &str) -> PathBuf { let basedir = PathBuf::from(Config_get!("basedir")); - basedir.join(get_box_unalias(inbox)).with_extension("md") + let enc_box = basedir.join(inbox).with_extension("mdx"); + + if enc_box.exists() { enc_box } + else { basedir.join(get_box_unalias(inbox)).with_extension("md") } } // following i_* fn are for "inquire" based wrappers @@ -129,6 +129,21 @@ pub fn i_confirm(question: &str) -> bool { .prompt().unwrap_or(false) } +pub fn i_getpass(confirm: bool, msg: Option<&str>) -> String { + let mut com = inquire::Password::new(msg.unwrap_or("the password:")) + .with_help_message(" | ctrl+r | ctrl+c") + .with_render_config(get_pass_input_style()) + .with_display_toggle_enabled(); + + if ! confirm { com = com.without_confirmation() } + + execute!(std::io::stdout(), SteadyBar).expect("failed to set cursor"); + let pass = com.prompt().unwrap_or_else(|_| String::new()); + execute!(std::io::stdout(), DefaultUserShape).expect("failed to set cursor"); + + pass +} + pub fn i_gettext() -> String { execute!(std::io::stdout(), BlinkingBlock).expect("failed to set cursor"); let input = inquire::Text::new("") @@ -169,11 +184,11 @@ mod tests { #[test] fn test_aliases() { - assert_eq!(get_box_alias(&get_today()), Some("today".into())); - assert_eq!(get_box_alias(&get_yesterday()), Some("yesterday".into())); - assert_eq!(get_box_alias(&get_tomorrow()), Some("tomorrow".into())); - assert_eq!(get_box_alias("dummy"), None); - assert_eq!(get_box_alias(""), None); + assert_eq!(get_box_alias(&get_today()), "today"); + assert_eq!(get_box_alias(&get_yesterday()), "yesterday"); + assert_eq!(get_box_alias(&get_tomorrow()), "tomorrow"); + assert_eq!(get_box_alias("dummy"), "dummy"); + assert_eq!(get_box_alias(""), ""); } #[test] diff --git a/tests/taskbox.rs b/tests/taskbox.rs index 6f69be5..624a076 100644 --- a/tests/taskbox.rs +++ b/tests/taskbox.rs @@ -23,7 +23,7 @@ fn setup_test_taskbox(name: &str) -> (TaskBox, tempfile::TempDir) { #[test] fn test_taskbox_new() { let (tb, _dir) = setup_test_taskbox("test"); - assert_eq!(tb.title, None); + assert_eq!(tb.tbname, "test"); assert_eq!(tb.alias, None); assert_eq!(tb.tasks.len(), 0); } @@ -366,7 +366,7 @@ fn test_import_somefile_to_inbox() { - [ ] {󰃯:m 2024-10-31Mon 󰳟} one montly to import "#; - let (mut inbox, dir) = setup_test_taskbox(INBOX_NAME); + let (mut inbox, dir) = setup_test_taskbox(INBOX_BOXNAME); let mut routine = inbox.sibling("routine"); let fpath = dir.path().join("import-input").with_extension("md");