commit ccb0f86bbb4f2f7b6de133a74204a96c45b980e0 Author: Kiyomichi Kosaka Date: Sun Nov 27 14:03:00 2022 +0000 Initial commit diff --git a/.replit b/.replit new file mode 100644 index 0000000..319e417 --- /dev/null +++ b/.replit @@ -0,0 +1,22 @@ +run = ["cargo", "run"] +hidden = ["target"] + +entrypoint = "src/main.rs" + +[packager] +language = "rust" + +[packager.features] +packageSearch = true + +[languages.rust] +pattern = "**/*.rs" + +[languages.rust.languageServer] +start = "rust-analyzer" + +[nix] +channel = "stable-22_05" + +[gitHubImport] +requiredFiles = [".replit", "replit.nix"] \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..01d316c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "my-project" +version = "0.1.0" +authors = ["runner"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = "0.4.23" +peg = "0.8.1" +anyhow = "1.0.66" +lazy_static = "1.4.0" +regex = "1.6.0" +rustyline = "10.0.0" +thiserror = "1.0.37" +anyerror = "0.1.7" diff --git a/replit.nix b/replit.nix new file mode 100644 index 0000000..f0e8e08 --- /dev/null +++ b/replit.nix @@ -0,0 +1,9 @@ +{ pkgs }: { + deps = [ + pkgs.rustc + pkgs.rustfmt + pkgs.cargo + pkgs.cargo-edit + pkgs.rust-analyzer + ]; +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..01914d0 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,22 @@ +#[macro_use] +extern crate lazy_static; + +pub mod structs; +pub mod parser; +pub mod repl; + +use rustyline::Editor; +use std::{cell::RefCell, rc::Rc}; + +pub fn main() { + let lk = Rc::new(RefCell::new(LK { db: HashMap::new() })); + let mut lkread = LKRead::new( + Editor::<()>::new().unwrap(), + String::from("❯ "), + lk.clone()); + + while lkread.read().eval().print() { + lkread.refresh(); + } + lkread.quit(); +} \ No newline at end of file diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..f8aeb85 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,125 @@ +extern crate peg; + +peg::parser!{ + grammar command_parser() for str { + pub rule cmd() -> Command<'input> = c:( + help_cmd() + / add_cmd() + / quit_cmd() + / error_cmd() + / ls_cmd() + / mv_cmd() + ) { c } + pub rule name() -> Password = name:(jname() / pname() / mname() / sname()) { name } + + rule _() -> &'input str = s:$((" " / "\t" / "\r" / "\n")+) { s } + rule comment() -> &'input str = _ c:$([' '..='~']+) { c } + rule word() -> &'input str = n:$(['!'..='~']+) { n } + rule num() -> u32 = n:$(['0'..='9']+) {? n.parse().or(Err("not a number")) } + rule pname() -> Password = &(word() _ word() _ num()? mode() _ num() _ date()) pr:word() _ pn:word() _ pl:num()? pm:mode() _ ps:num() _ pd:date() pc:comment()? { + Password { prefix: Some(pr.to_string()), length: pl, name: Rc::new(pn.to_string()), mode: pm, seq: ps, date: pd, parent: None, + comment: match pc { Some(s) => Some(s.to_string()), None => None } } } + rule jname() -> Password = &(word() _ num()? mode() _ num() _ date()) pn:word() _ pl:num()? pm:mode() _ ps:num() _ pd:date() pc:comment()? { + Password { prefix: None, length: pl, name: Rc::new(pn.to_string()), mode: pm, seq: ps, date: pd, parent: None, + comment: match pc { Some(s) => Some(s.to_string()), None => None } } } + rule mname() -> Password = &(word() _ word() _ num()? mode() _ date()) pr:word() _ pn:word() _ pl:num()? pm:mode() _ pd:date() pc:comment()? { + Password { prefix: Some(pr.to_string()), length: pl, name: Rc::new(pn.to_string()), mode: pm, seq: 99, date: pd, parent: None, + comment: match pc { Some(s) => Some(s.to_string()), None => None } } } + rule sname() -> Password = &(word() _ num()? mode() _ date()) pn:word() _ pl:num()? pm:mode() _ pd:date() pc:comment()? { + Password { prefix: None, length: pl, name: Rc::new(pn.to_string()), mode: pm, seq: 99, date: pd, parent: None, + comment: match pc { Some(s) => Some(s.to_string()), None => None } } } + rule date() -> NaiveDate = y:$("-"? ['0'..='9']*<1,4>) "-" m:$(['0'..='9']*<1,2>) "-" d:$(['0'..='9']*<1,2>) {? + let year: i32 = match y.parse() { Ok(n) => n, Err(_) => return Err("year") }; + let month: u32 = match m.parse() { Ok(n) => n, Err(_) => return Err("month") }; + let day: u32 = match d.parse() { Ok(n) => n, Err(_) => return Err("day") }; + NaiveDate::from_ymd_opt(year, month, day).ok_or("date") + } + rule umode() -> Mode = ("U" / "u") m:$("R" / "r" / "N" / "n" / "H" / "h" / "B" / "b") {? + match m.to_uppercase().as_str() { + "R" => Ok(Mode::RegularUpcase), + "N" => Ok(Mode::NoSpaceUpcase), + "H" => Ok(Mode::HexUpcase), + "B" => Ok(Mode::Base64Upcase), + _ => Err("unknown mode"), + } + } + rule rmode() -> Mode = m:$("R" / "r" / "U" / "u" / "N" / "n" / "H" / "h" / "B" / "b" / "D" / "d") {? + match m.to_uppercase().as_str() { + "R" => Ok(Mode::Regular), + "N" => Ok(Mode::NoSpace), + "U" => Ok(Mode::RegularUpcase), + "H" => Ok(Mode::Hex), + "B" => Ok(Mode::Base64), + "D" => Ok(Mode::Decimal), + _ => Err("unknown mode"), + } + } + rule mode() -> Mode = m:(umode() / rmode()) { m } + rule help_cmd() -> Command<'input> = "help" { Command::Help } + rule quit_cmd() -> Command<'input> = "quit" { Command::Quit } + rule ls_cmd() -> Command<'input> = "ls" { Command::Ls } + rule add_cmd() -> Command<'input> = "add" _ name:name() { Command::Add(Rc::new(RefCell::new(name))) } + rule error_cmd() -> Command<'input> = "error" _ e:$(([' '..='~'])+) { Command::Error(LKErr::Error(e)) } + rule mv_cmd() -> Command<'input> = "mv" _ name:word() _ folder:word() { Command::Mv(name.to_string(), folder.to_string()) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_password_test() { + assert_eq!(command_parser::name("ableton89 R 99 2020-12-09 xx.ableton@domain.info https://www.ableton.com"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: None, mode: Mode::Regular, + length: None, seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("xx.ableton@domain.info https://www.ableton.com".to_string()) })); + assert_eq!(command_parser::name("ableton89 U 99 2020-12-09 xx.ableton@domain.info https://www.ableton.com"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: None, mode: Mode::RegularUpcase, + length: None, seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("xx.ableton@domain.info https://www.ableton.com".to_string()) })); + assert_eq!(command_parser::name("ableton89 U 2020-12-09"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: None, mode: Mode::RegularUpcase, + length: None, seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), comment: None })); + assert_eq!(command_parser::name("#W9 ableton89 R 99 2020-12-09 xx.ableton@domain.info https://www.ableton.com"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: Some("#W9".to_string()), mode: Mode::Regular, + length: None, seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("xx.ableton@domain.info https://www.ableton.com".to_string()) })); + assert_eq!(command_parser::name("#W9 ableton89 N 99 2020-12-09 xx.ableton@domain.info https://www.ableton.com"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: Some("#W9".to_string()), mode: Mode::NoSpace, + length: None, seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("xx.ableton@domain.info https://www.ableton.com".to_string()) })); + assert_eq!(command_parser::name("#W9 ableton89 UN 99 2020-12-09 xx.ableton@domain.info https://www.ableton.com"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: Some("#W9".to_string()), mode: Mode::NoSpaceUpcase, + length: None, seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("xx.ableton@domain.info https://www.ableton.com".to_string()) })); + assert_eq!(command_parser::name("#W9 ableton89 20R 99 2020-12-09 a b c"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: Some("#W9".to_string()), mode: Mode::Regular, + length: Some(20), seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("a b c".to_string()) })); + assert_eq!(command_parser::name("#W9 ableton89 20UR 99 2020-12-09 a b c"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: Some("#W9".to_string()), mode: Mode::RegularUpcase, + length: Some(20), seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("a b c".to_string()) })); + assert_eq!(command_parser::name("#W9 ableton89 20UH 99 2020-12-09 a b c"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: Some("#W9".to_string()), mode: Mode::HexUpcase, + length: Some(20), seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("a b c".to_string()) })); + assert_eq!(command_parser::name("#W9 ableton89 20UB 99 2020-12-09 a b c"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: Some("#W9".to_string()), mode: Mode::Base64Upcase, + length: Some(20), seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("a b c".to_string()) })); + assert_eq!(command_parser::name("#W9 ableton89 20D 99 2020-12-09 a b c"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: Some("#W9".to_string()), mode: Mode::Decimal, + length: Some(20), seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("a b c".to_string()) })); + assert_eq!(command_parser::name("ableton89 20D 98 2020-12-09 a b c"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: None, mode: Mode::Decimal, + length: Some(20), seq: 98, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("a b c".to_string()) })); + assert_eq!(command_parser::name("ableton89 20D 2020-12-09 a b c"), + Ok(Password { name: "ableton89".to_string(), parent: None, prefix: None, mode: Mode::Decimal, + length: Some(20), seq: 99, date: NaiveDate::from_ymd_opt(2020, 12, 09).unwrap(), + comment: Some("a b c".to_string()) })); + } +} \ No newline at end of file diff --git a/src/repl.rs b/src/repl.rs new file mode 100644 index 0000000..4a4b301 --- /dev/null +++ b/src/repl.rs @@ -0,0 +1,119 @@ +#[derive(Debug)] +struct LKRead { + rl: Editor::<()>, + prompt: String, + state: Rc>, + cmd: String, +} + +#[derive(Debug)] +struct LKEval<'a> { + cmd: Command<'a>, + state: Rc>, +} + +#[derive(Debug)] +struct LKPrint { + out: Vec, + quit: bool, + // state: Rc>, +} + +impl LKRead { + fn new(rl: Editor::<()>, prompt: String, state: Rc>) -> Self { + Self { rl, prompt, state, cmd: "".to_string() } + } + + fn read(&mut self) -> LKEval { + self.cmd = match self.rl.readline(&*self.prompt) { + Ok(str) => str, + Err(err) => return LKEval::new(Command::Error(LKErr::ReadError(err.to_string())), self.state.clone()), + }; + match command_parser::cmd(self.cmd.as_str()) { + Ok(cmd) => LKEval::new(cmd, self.state.clone()), + Err(err) => LKEval::new(Command::Error(LKErr::PegParseError(err)), self.state.clone()), + } + } + + fn refresh(&mut self) { + + } + + fn quit(&mut self) { + + } +} + +impl<'a> LKEval<'a> { + fn new(cmd: Command<'a>, state: Rc>) -> Self { Self { cmd, state } } + + fn eval(&mut self) -> LKPrint { + let mut out: Vec = vec![]; + let mut quit: bool = false; + + match &self.cmd { + Command::Quit => { + out.push("Bye!".to_string()); + quit = true; + }, + Command::Ls => { + for (_, name) in &self.state.borrow().db { + let pw = name.borrow(); + let prefix = match pw.prefix.as_ref() { Some(s) => format!("{} ", s), None => "".to_string() }; + let length = match pw.length { Some(l) => format!("{}", l), None => "".to_string() }; + let comment = match pw.comment.as_ref() { Some(s) => format!(" {}", s), None => "".to_string() }; + let parent = match &pw.parent { Some(s) => format!(" ^{}", s.borrow().name), None => "".to_string() }; + out.push(format!("{}{} {}{} {} {}{}{}", prefix, pw.name, length, pw.mode, pw.seq, pw.date, comment, parent)); + } + }, + Command::Add(name) => { + if self.state.borrow().db.get(&name.borrow().name).is_some() { + out.push("error: password already exist".to_string()); + } else { + self.state.borrow_mut().db.insert(name.borrow().name.clone(), name.clone()); + self.state.borrow().fix_hierarchy(); + } + }, + Command::Help => { + out.push("HELP".to_string()); + }, + Command::Mv(name, folder) => { + for (_, tmp) in &self.state.borrow().db { + if *tmp.borrow().name == *name { + if folder == "/" { tmp.borrow_mut().parent = None } + else { + for (_, fld) in &self.state.borrow().db { + if *fld.borrow().name == *folder { + tmp.borrow_mut().parent = Some(fld.clone()); + break; + } + } + } + break; + } + } + }, + Command::Error(err) => { + match err { + LKErr::PegParseError(e) => { out.push(e.to_string()) }, + LKErr::ReadError(e) => { out.push(e.to_string()) }, + LKErr::Error(e) => { out.push(format!("error: {}", e.to_string())) }, + _ => out.push(format!("error: {:?}", err)), + } + } + } + + LKPrint::new(out, quit) + } +} + +impl LKPrint { + fn new(out: Vec, quit: bool) -> Self { Self { out, quit } } + + fn print(&mut self) -> bool { + for line in &self.out { + println!("{}", line); + } + return !self.quit; + } +} diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..58e159a --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,102 @@ +use std::collections::HashMap; +use regex::{Regex, Captures}; +use chrono::naive::NaiveDate; + +#[derive(thiserror::Error, Debug, PartialEq)] +pub enum LKErr<'a> { + #[error("Error: {0}")] + Error(&'a str), + #[error("Failed to read the line: {0}")] + ReadError(String), + #[error("Failed to parse {0}: {1}")] + ParseError(&'a str, &'a str), + #[error("Failed to parse {0}: {1}")] + ParseErrorS(&'a str, String), + #[error("Failed to parse: {0}")] + PegParseError(peg::error::ParseError), +} + +#[derive(PartialEq, Debug)] +pub enum Mode { + Regular, + RegularUpcase, + NoSpace, + NoSpaceUpcase, + Hex, + HexUpcase, + Base64, + Base64Upcase, + Decimal, +} + +#[derive(PartialEq, Debug)] +pub struct Password { + parent: Option>>, + prefix: Option, + name: Rc, + length: Option, + mode: Mode, + seq: u32, + date: NaiveDate, + comment: Option, +} + +#[derive(PartialEq, Debug)] +pub enum Command<'a> { + Add(Rc>), + Ls, + Mv(String, String), + Error(LKErr<'a>), + Help, + Quit +} + +#[derive(Debug)] +struct LK { + db: HashMap, Rc>>, +} + +impl std::fmt::Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Mode::Regular => write!(f, "R"), + Mode::RegularUpcase => write!(f, "UR"), + Mode::NoSpace => write!(f, "N"), + Mode::NoSpaceUpcase => write!(f, "UN"), + Mode::Hex => write!(f, "H"), + Mode::HexUpcase => write!(f, "UH"), + Mode::Base64 => write!(f, "B"), + Mode::Base64Upcase => write!(f, "UB"), + Mode::Decimal => write!(f, "D"), + } + } +} + +impl LK { + fn fix_hierarchy(&self) { + lazy_static! { + static ref RE: Regex = Regex::new(r"\s*\^([!-~]+)").unwrap(); + } + for (_, name) in &self.db { + if name.borrow().comment.is_some() { + let mut folder: Option = None; + let prev_comment = name.borrow().comment.as_ref().unwrap().clone(); + let comment = RE.replace(prev_comment.as_str(), |c: &Captures| { folder = Some(c[1].to_string()); "" }); + if folder.is_some() { + let folder_name = folder.unwrap(); + for (_, entry) in &self.db { + if *entry.borrow().name == *folder_name { + { + let mut tmp = name.borrow_mut(); + tmp.parent = Some(entry.clone()); + if comment.len() == 0 { tmp.comment = None } + else { tmp.comment = Some(comment.to_string()) } + } + break; + } + } + } + } + } + } +} \ No newline at end of file