helwasm: hierarchical quick-gen, pure sugar over the engine
Quick form now drives the one shared engine through commands only, so it and the console give identical results (form == console). - password hook answers only the "/" prompt, so read_master climbs and computes every ^parent from a single root master (Req 4) - new master in the field is authoritative via `unpass`; empty field reuses the stored session master, so later names generate without re-typing it (Req 1/2) - `unpass` with no name now clears ALL cached masters (real command, not a private hook) so the form stays reproducible in the console - result colour = root master marked correct (correct / / uncorrect /), derived live from the engine; black otherwise (Req 3) - Mark-correct toggle button; click the master to reveal it; honest memory-only hint - master stays in memory only, never written to storage; only the one-way correct hash is saved on the device parser: bare-name forms now keep a trailing comment, and a ^-led token is never a name, so `name ^parent` / `name MODE ^parent` link the parent instead of qpname reading it as prefix+name (was silently mis-stored).
This commit is contained in:
+34
-10
@@ -38,17 +38,20 @@ peg::parser! {
|
||||
rule mname() -> Password = &(word() _ word() _ num()? mode() _ date()) pr:word() _ pn:word() _ pl:num()? pm:mode() _ pd:date() pc:comment()?
|
||||
{ Password::new(Some(pr), pn, pl, pm, 99, pd, pc) }
|
||||
// prefix + name + [len]mode (no seq/date) -> defaults seq 99, date now
|
||||
rule npname() -> Password = &(word() _ word() _ num()? mode()) pr:word() _ pn:word() _ pl:num()? pm:mode()
|
||||
{ Password::new(Some(pr), pn, pl, pm, 99, Date::now(), None) }
|
||||
rule npname() -> Password = &(word() _ word() _ num()? mode()) pr:word() _ pn:word() _ pl:num()? pm:mode() pc:comment()?
|
||||
{ Password::new(Some(pr), pn, pl, pm, 99, Date::now(), pc) }
|
||||
rule sname() -> Password = &(word() _ num()? mode() _ date()) pn:word() _ pl:num()? pm:mode() _ pd:date() pc:comment()?
|
||||
{ Password::new(None, pn, pl, pm, 99, pd, pc) }
|
||||
rule nname() -> Password = &(word() _ num()? mode()) pn:word() _ pl:num()? pm:mode()
|
||||
{ Password::new(None, pn, pl, pm, 99, Date::now(), None) }
|
||||
// prefix + name only -> defaults mode R, seq 99, date now
|
||||
rule qpname() -> Password = &(word() _ word()) pr:word() _ pn:word()
|
||||
{ Password::new(Some(pr), pn, None, Mode::Regular, 99, Date::now(), None) }
|
||||
rule qname() -> Password = &(word()) pn:word()
|
||||
{ Password::new(None, pn, None, Mode::Regular, 99, Date::now(), None) }
|
||||
rule nname() -> Password = &(word() _ num()? mode()) pn:word() _ pl:num()? pm:mode() pc:comment()?
|
||||
{ Password::new(None, pn, pl, pm, 99, Date::now(), pc) }
|
||||
// prefix + name (+ optional comment) -> defaults mode R, seq 99, date now.
|
||||
// A `^parent` token is never a name, so a `^`-leading second word fails here and
|
||||
// falls through to qname, which keeps it as the comment (the parent marker).
|
||||
rule qpname() -> Password = &(word() _ word()) pr:word() _ pn:$(!"^" ['!'..='~']+) pc:comment()?
|
||||
{ Password::new(Some(pr), pn.to_string(), None, Mode::Regular, 99, Date::now(), pc) }
|
||||
// name (+ optional comment) -> e.g. `github` or `github ^work`
|
||||
rule qname() -> Password = pn:word() pc:comment()?
|
||||
{ Password::new(None, pn, None, Mode::Regular, 99, Date::now(), pc) }
|
||||
pub rule name() -> Password = name:(jname() / pname() / mname() / npname() / sname() / nname() / qpname() / qname())? {?
|
||||
match name { Some(n) => Ok(n), None => Err("failed to parse password description") }
|
||||
}
|
||||
@@ -107,7 +110,7 @@ peg::parser! {
|
||||
rule pass_cmd() -> Command<'input> = p:(pass_long_cmd() / pass_short_cmd()) { p }
|
||||
rule correct_cmd() -> Command<'input> = "correct" _ name:word() { Command::Correct(name) }
|
||||
rule uncorrect_cmd() -> Command<'input> = "uncorrect" _ name:word() { Command::Uncorrect(name) }
|
||||
rule unpass_cmd() -> Command<'input> = "unpass" _ name:word() { Command::UnPass(name) }
|
||||
rule unpass_cmd() -> Command<'input> = "unpass" name:(_ w:word() { w })? { Command::UnPass(name) }
|
||||
rule enc_cmd() -> Command<'input> = "enc" _ name:word() { Command::Enc(name) }
|
||||
rule rm_cmd() -> Command<'input> = "rm" _ name:word() { Command::Rm(name) }
|
||||
rule comment_cmd() -> Command<'input> = "comment" _ name:word() c:comment()? { Command::Comment(name, c) }
|
||||
@@ -245,6 +248,27 @@ add t3 C 99 2022-12-14
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_short_parent_test() {
|
||||
// `name ^parent` (no mode/date): name is the name, ^parent stays in the comment
|
||||
// for fix_hierarchy — it must NOT be read as prefix+name.
|
||||
let p = command_parser::name("x ^acc").unwrap();
|
||||
assert_eq!(p.name, "x");
|
||||
assert_eq!(p.prefix, None);
|
||||
assert_eq!(p.comment, Some("^acc".to_string()));
|
||||
// `name [len]mode ^parent` keeps the parent in the comment too
|
||||
let p2 = command_parser::name("x 20R ^acc").unwrap();
|
||||
assert_eq!(p2.name, "x");
|
||||
assert_eq!(p2.length, Some(20));
|
||||
assert_eq!(p2.mode, Mode::Regular);
|
||||
assert_eq!(p2.comment, Some("^acc".to_string()));
|
||||
// a real prefix + name (second word not ^-led) still parses as prefix+name
|
||||
let p3 = command_parser::name("#W9 github").unwrap();
|
||||
assert_eq!(p3.prefix, Some("#W9".to_string()));
|
||||
assert_eq!(p3.name, "github");
|
||||
assert_eq!(p3.comment, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_password_test() {
|
||||
assert_eq!(
|
||||
|
||||
+6
-2
@@ -133,10 +133,14 @@ impl<'a> LKEval<'a> {
|
||||
Command::Set(key, value) => { to_history = false; self.cmd_set(&out, key, value); }
|
||||
Command::Pass(name, None) => self.cmd_pass(&out, &name, &None),
|
||||
Command::Pass(name, pass) => { to_history = false; self.cmd_pass(&out, &name, &pass); },
|
||||
Command::UnPass(name) => match self.state.lock().borrow_mut().secrets.remove(name) {
|
||||
Command::UnPass(Some(name)) => match self.state.lock().borrow_mut().secrets.remove(name) {
|
||||
Some(_) => out.o(format!("Removed saved password for {}", name)),
|
||||
None => out.e(format!("error: saved password for {} not found", name)),
|
||||
},
|
||||
Command::UnPass(None) => {
|
||||
self.state.lock().borrow_mut().secrets.clear();
|
||||
out.o("forgot all cached masters".to_string());
|
||||
},
|
||||
Command::Correct(name) => self.cmd_correct(&out, name, true, None),
|
||||
Command::Uncorrect(name) => self.cmd_correct(&out, name, false, None),
|
||||
Command::Noop => { to_history = false; },
|
||||
@@ -156,7 +160,7 @@ impl<'a> LKEval<'a> {
|
||||
" enc <name> show the generated password\n",
|
||||
" gen[N] <name> N numbered variants; name ends in G.. (all) or X.. (random)\n",
|
||||
" pass <name> [pw] cache a master / override for an entry's subtree\n",
|
||||
" unpass <name> forget a cached password (unpass / = the root master)\n",
|
||||
" unpass [name] forget a cached password (unpass / = root; unpass = all)\n",
|
||||
" correct <name> trust this password's hash uncorrect <name> untrust it\n",
|
||||
"\n",
|
||||
"catalog\n",
|
||||
|
||||
+3
-2
@@ -97,7 +97,7 @@ pub enum Command<'a> {
|
||||
Enc(Name),
|
||||
Gen(u32, PasswordRef),
|
||||
Pass(Name, Option<String>),
|
||||
UnPass(Name),
|
||||
UnPass(Option<Name>),
|
||||
Correct(Name),
|
||||
Uncorrect(Name),
|
||||
PasteBuffer(String),
|
||||
@@ -153,7 +153,8 @@ impl<'a> std::fmt::Display for Command<'a> {
|
||||
Command::Gen(a, b) => write!(f, "gen{} {}", a, b.lock().borrow().to_string().trim()),
|
||||
Command::Pass(a, None) => write!(f, "pass {}", a),
|
||||
Command::Pass(a, Some(b)) => write!(f, "pass {} {}", a, b),
|
||||
Command::UnPass(s) => write!(f, "unpass {}", s),
|
||||
Command::UnPass(None) => write!(f, "unpass"),
|
||||
Command::UnPass(Some(s)) => write!(f, "unpass {}", s),
|
||||
Command::Correct(s) => write!(f, "correct {}", s),
|
||||
Command::Uncorrect(s) => write!(f, "uncorrect {}", s),
|
||||
Command::PasteBuffer(s) => write!(f, "pb {}", s),
|
||||
|
||||
Reference in New Issue
Block a user