diff options
-rw-r--r-- | store/src/backend.rs | 171 | ||||
-rw-r--r-- | store/src/lib.rs | 238 | ||||
-rw-r--r-- | store/src/store_protocol.capnp | 37 | ||||
-rw-r--r-- | tool/Cargo.toml | 1 | ||||
-rw-r--r-- | tool/src/main.rs | 82 | ||||
-rw-r--r-- | tool/src/usage.rs | 97 |
6 files changed, 624 insertions, 2 deletions
diff --git a/store/src/backend.rs b/store/src/backend.rs index 6bff34e0..fcd3cc67 100644 --- a/store/src/backend.rs +++ b/store/src/backend.rs @@ -107,6 +107,29 @@ impl node::Server for NodeServer { node::store::ToClient::new(store).from_server::<capnp_rpc::Server>())); Promise::ok(()) } + + fn iter(&mut self, + params: node::IterParams, + mut results: node::IterResults) + -> Promise<(), capnp::Error> { + bind_results!(results); + let prefix = pry!(pry!(params.get()).get_domain_prefix()); + let iter = StoreIterServer::new(self.c.clone(), prefix); + pry!(pry!(results.get().get_result()).set_ok( + node::store_iter::ToClient::new(iter).from_server::<capnp_rpc::Server>())); + Promise::ok(()) + } + + fn iter_keys(&mut self, + _: node::IterKeysParams, + mut results: node::IterKeysResults) + -> Promise<(), capnp::Error> { + bind_results!(results); + let iter = KeyIterServer::new(self.c.clone()); + pry!(pry!(results.get().get_result()).set_ok( + node::key_iter::ToClient::new(iter).from_server::<capnp_rpc::Server>())); + Promise::ok(()) + } } struct StoreServer { @@ -214,6 +237,17 @@ impl node::store::Server for StoreServer { &[&self.id])); Promise::ok(()) } + + fn iter(&mut self, + _: node::store::IterParams, + mut results: node::store::IterResults) + -> Promise<(), capnp::Error> { + bind_results!(results); + let iter = BindingIterServer::new(self.c.clone(), self.id); + pry!(pry!(results.get().get_result()).set_ok( + node::binding_iter::ToClient::new(iter).from_server::<capnp_rpc::Server>())); + Promise::ok(()) + } } struct BindingServer { @@ -458,6 +492,137 @@ impl node::key::Server for KeyServer { } } +/* Iterators. */ + +struct StoreIterServer { + c: Rc<Connection>, + prefix: String, + n: i64, +} + +impl StoreIterServer { + fn new(c: Rc<Connection>, prefix: &str) -> Self { + StoreIterServer{c: c, prefix: String::from(prefix) + "%", n: 0} + } +} + +impl node::store_iter::Server for StoreIterServer { + fn next(&mut self, + _: node::store_iter::NextParams, + mut results: node::store_iter::NextResults) + -> Promise<(), capnp::Error> { + bind_results!(results); + let (id, domain, name, network_policy): (i64, String, String, i64) = + sry!(self.c.query_row( + "SELECT id, domain, name, network_policy FROM stores + WHERE id > ?1 AND domain like ?2 + ORDER BY id LIMIT 1", + &[&self.n, &self.prefix], + |row| (row.get(0), row.get(1), row.get(2), row.get(3)))); + + let count: i64 = + sry!(self.c.query_row( + "SELECT count(*) FROM bindings WHERE store = ?1", + &[&id], |row| row.get(0))); + assert!(count >= 0); + + // We cannot implement FromSql and friends for + // core::NetworkPolicy, hence we need to do it by foot. + if network_policy < 0 || network_policy > 3 { + fail!(node::Error::SystemError); + } + let network_policy = core::NetworkPolicy::from(network_policy as u8); + + let mut entry = pry!(results.get().get_result()).init_ok(); + entry.set_domain(&domain); + entry.set_name(&name); + entry.set_network_policy(network_policy.into()); + entry.set_entries(count as u64); + entry.set_store(node::store::ToClient::new( + StoreServer::new(self.c.clone(), id)).from_server::<capnp_rpc::Server>()); + self.n = id; + Promise::ok(()) + } +} + +struct BindingIterServer { + c: Rc<Connection>, + store_id: i64, + n: i64, +} + +impl BindingIterServer { + fn new(c: Rc<Connection>, store_id: i64) -> Self { + BindingIterServer{c: c, store_id: store_id, n: 0} + } +} + +impl node::binding_iter::Server for BindingIterServer { + fn next(&mut self, + _: node::binding_iter::NextParams, + mut results: node::binding_iter::NextResults) + -> Promise<(), capnp::Error> { + bind_results!(results); + let (id, label, fingerprint): (i64, String, String) = + sry!(self.c.query_row( + "SELECT bindings.id, bindings.label, keys.fingerprint FROM bindings + JOIN keys ON bindings.key = keys.id + WHERE bindings.id > ?1 AND bindings.store = ?2 + ORDER BY bindings.id LIMIT 1", + &[&self.n, &self.store_id], + |row| (row.get(0), row.get(1), row.get(2)))); + + let mut entry = pry!(results.get().get_result()).init_ok(); + entry.set_label(&label); + entry.set_fingerprint(&fingerprint); + entry.set_binding(node::binding::ToClient::new( + BindingServer::new(self.c.clone(), id)).from_server::<capnp_rpc::Server>()); + self.n = id; + Promise::ok(()) + } +} + +struct KeyIterServer { + c: Rc<Connection>, + n: i64, +} + +impl KeyIterServer { + fn new(c: Rc<Connection>) -> Self { + KeyIterServer{c: c, n: 0} + } +} + +impl node::key_iter::Server for KeyIterServer { + fn next(&mut self, + _: node::key_iter::NextParams, + mut results: node::key_iter::NextResults) + -> Promise<(), capnp::Error> { + bind_results!(results); + let (id, fingerprint): (i64, String) = + sry!(self.c.query_row( + "SELECT id, fingerprint FROM keys + WHERE keys.id > ?1 + ORDER BY id LIMIT 1", + &[&self.n], + |row| (row.get(0), row.get(1)))); + + let count: i64 = + sry!(self.c.query_row( + "SELECT count(*) FROM bindings WHERE key = ?1", + &[&id], |row| row.get(0))); + assert!(count >= 0); + + let mut entry = pry!(results.get().get_result()).init_ok(); + entry.set_fingerprint(&fingerprint); + entry.set_bindings(count as u64); + entry.set_key(node::key::ToClient::new( + KeyServer::new(self.c.clone(), id)).from_server::<capnp_rpc::Server>()); + self.n = id; + Promise::ok(()) + } +} + /* Error handling. */ impl fmt::Debug for node::Error { @@ -617,6 +782,12 @@ impl<'a> From<&'a core::NetworkPolicy> for node::NetworkPolicy { } } +impl From<core::NetworkPolicy> for node::NetworkPolicy { + fn from(policy: core::NetworkPolicy) -> Self { + (&policy).into() + } +} + impl From<node::NetworkPolicy> for core::NetworkPolicy { fn from(policy: node::NetworkPolicy) -> Self { match policy { diff --git a/store/src/lib.rs b/store/src/lib.rs index b81d3606..028945e5 100644 --- a/store/src/lib.rs +++ b/store/src/lib.rs @@ -143,7 +143,52 @@ impl Store { request.get().set_name(name); let store = make_request!(&mut core, request)?; - Ok(Store{name: name.into(), core: Rc::new(RefCell::new(core)), store: store}) + Ok(Self::new(Rc::new(RefCell::new(core)), name, store)) + } + + fn new(core: Rc<RefCell<Core>>, name: &str, store: node::store::Client) -> Self { + Store{core: core, name: name.into(), store: store} + } + + /// Lists all stores with the given prefix. + pub fn list(c: &Context, domain_prefix: &str) -> Result<StoreIter> { + let descriptor = descriptor(c); + let mut core = Core::new()?; + let handle = core.handle(); + + let mut rpc_system + = match descriptor.connect(&handle) { + Ok(r) => r, + Err(e) => return Err(e.into()), + }; + + let node: node::Client = rpc_system.bootstrap(Side::Server); + handle.spawn(rpc_system.map_err(|_e| ())); + + let mut request = node.iter_request(); + request.get().set_domain_prefix(domain_prefix); + let iter = make_request!(&mut core, request)?; + Ok(StoreIter{core: Rc::new(RefCell::new(core)), iter: iter}) + } + + /// Lists all keys in the common key pool. + pub fn list_keys(c: &Context) -> Result<KeyIter> { + let descriptor = descriptor(c); + let mut core = Core::new()?; + let handle = core.handle(); + + let mut rpc_system + = match descriptor.connect(&handle) { + Ok(r) => r, + Err(e) => return Err(e.into()), + }; + + let node: node::Client = rpc_system.bootstrap(Side::Server); + handle.spawn(rpc_system.map_err(|_e| ())); + + let request = node.iter_keys_request(); + let iter = make_request!(&mut core, request)?; + Ok(KeyIter{core: Rc::new(RefCell::new(core)), iter: iter}) } /// Adds a key identified by fingerprint to the store. @@ -269,6 +314,13 @@ impl Store { let request = self.store.delete_request(); make_request_map!(self.core.borrow_mut(), request, |_| Ok(())) } + + /// Lists all bindings. + pub fn iter(&self) -> Result<BindingIter> { + let request = self.store.iter_request(); + let iter = make_request!(self.core.borrow_mut(), request)?; + Ok(BindingIter{core: self.core.clone(), iter: iter}) + } } /// Represents an entry in a Store. @@ -637,6 +689,115 @@ impl Stamps { } } +/* Iterators. */ + +/// Iterates over stores. +pub struct StoreIter { + core: Rc<RefCell<Core>>, + iter: node::store_iter::Client, +} + +/// Items returned by `StoreIter`. +#[derive(Debug)] +pub struct StoreIterItem { + pub domain: String, + pub name: String, + pub network_policy: core::NetworkPolicy, + pub entries: usize, + pub store: Store, +} + +impl Iterator for StoreIter { + type Item = StoreIterItem; + + fn next(&mut self) -> Option<Self::Item> { + let request = self.iter.next_request(); + let doit = || { + make_request_map!( + self.core.borrow_mut(), request, + |r: node::store_iter::item::Reader| + Ok(StoreIterItem{ + domain: r.get_domain()?.into(), + name: r.get_name()?.into(), + network_policy: r.get_network_policy()?.into(), + entries: r.get_entries() as usize, + store: Store::new(self.core.clone(), r.get_name()?, r.get_store()?), + })) + }; + doit().ok() + } +} + +/// Iterates over bindings in a store. +pub struct BindingIter { + core: Rc<RefCell<Core>>, + iter: node::binding_iter::Client, +} + +/// Items returned by `BindingIter`. +#[derive(Debug)] +pub struct BindingIterItem { + pub label: String, + pub fingerprint: openpgp::Fingerprint, + pub binding: Binding, +} + +impl Iterator for BindingIter { + type Item = BindingIterItem; + + fn next(&mut self) -> Option<Self::Item> { + let request = self.iter.next_request(); + let doit = || { + make_request_map!( + self.core.borrow_mut(), request, + |r: node::binding_iter::item::Reader| { + let label = String::from(r.get_label()?); + let binding = Binding::new(self.core.clone(), &label, r.get_binding()?); + Ok(BindingIterItem{ + label: label, + fingerprint: openpgp::Fingerprint::from_hex(r.get_fingerprint()?).unwrap(), + binding: binding, + }) + }) + }; + doit().ok() + } +} + +/// Iterates over keys in the common key pool. +pub struct KeyIter { + core: Rc<RefCell<Core>>, + iter: node::key_iter::Client, +} + +/// Items returned by `KeyIter`. +#[derive(Debug)] +pub struct KeyIterItem { + pub fingerprint: openpgp::Fingerprint, + pub bindings: usize, + pub key: Key, +} + +impl Iterator for KeyIter { + type Item = KeyIterItem; + + fn next(&mut self) -> Option<Self::Item> { + let request = self.iter.next_request(); + let doit = || { + make_request_map!( + self.core.borrow_mut(), request, + |r: node::key_iter::item::Reader| { + Ok(KeyIterItem{ + fingerprint: openpgp::Fingerprint::from_hex(r.get_fingerprint()?).unwrap(), + bindings: r.get_bindings() as usize, + key: Key::new(self.core.clone(), r.get_key()?), + }) + }) + }; + doit().ok() + } +} + /* Error handling. */ /// Results for sequoia-store. @@ -825,5 +986,80 @@ mod store_test { assert_match!(Err(Error::NotFound) = b1.stats()); assert_match!(Err(Error::NotFound) = b1.key()); } + + fn make_some_stores() -> core::Context { + let ctx0 = core::Context::configure("org.sequoia-pgp.tests.foo") + .ephemeral() + .network_policy(core::NetworkPolicy::Offline) + .build().unwrap(); + let store = Store::open(&ctx0, "default").unwrap(); + let fp = Fingerprint::from_bytes(b"bbbbbbbbbbbbbbbbbbbb"); + store.add("Mister B.", &fp).unwrap(); + store.add("B4", &fp).unwrap(); + + Store::open(&ctx0, "another store").unwrap(); + + let ctx1 = core::Context::configure("org.sequoia-pgp.tests.bar") + .home(ctx0.home()) + .network_policy(core::NetworkPolicy::Offline) + .build().unwrap(); + let store = Store::open(&ctx1, "default").unwrap(); + let fp = Fingerprint::from_bytes(b"cccccccccccccccccccc"); + store.add("Mister C.", &fp).unwrap(); + + ctx0 + } + + #[test] + fn store_iterator() { + let ctx = make_some_stores(); + let mut iter = Store::list(&ctx, "org.sequoia-pgp.tests.f").unwrap(); + let item = iter.next().unwrap(); + assert_eq!(item.domain, "org.sequoia-pgp.tests.foo"); + assert_eq!(item.name, "default"); + assert_eq!(item.network_policy, core::NetworkPolicy::Offline); + assert_eq!(item.entries, 2); + let fp = Fingerprint::from_bytes(b"bbbbbbbbbbbbbbbbbbbb"); + item.store.add("Mister B.", &fp).unwrap(); + let item = iter.next().unwrap(); + assert_eq!(item.domain, "org.sequoia-pgp.tests.foo"); + assert_eq!(item.name, "another store"); + assert_eq!(item.network_policy, core::NetworkPolicy::Offline); + assert_eq!(item.entries, 0); + item.store.add("Mister B.", &fp).unwrap(); + assert!(iter.next().is_none()); + } + + #[test] + fn binding_iterator() { + let ctx = make_some_stores(); + let store = Store::open(&ctx, "default").unwrap(); + let mut iter = store.iter().unwrap(); + let item = iter.next().unwrap(); + let fp = Fingerprint::from_bytes(b"bbbbbbbbbbbbbbbbbbbb"); + assert_eq!(item.label, "Mister B."); + assert_eq!(item.fingerprint, fp); + item.binding.stats().unwrap(); + let item = iter.next().unwrap(); + assert_eq!(item.label, "B4"); + assert_eq!(item.fingerprint, fp); + item.binding.stats().unwrap(); + assert!(iter.next().is_none()); + } + + #[test] + fn key_iterator() { + let ctx = make_some_stores(); + let mut iter = Store::list_keys(&ctx).unwrap(); + let item = iter.next().unwrap(); + assert_eq!(item.fingerprint, Fingerprint::from_bytes(b"bbbbbbbbbbbbbbbbbbbb")); + assert_eq!(item.bindings, 2); + item.key.stats().unwrap(); + let item = iter.next().unwrap(); + assert_eq!(item.fingerprint, Fingerprint::from_bytes(b"cccccccccccccccccccc")); + assert_eq!(item.bindings, 1); + item.key.stats().unwrap(); + assert!(iter.next().is_none()); + } } diff --git a/store/src/store_protocol.capnp b/store/src/store_protocol.capnp index f29565da..6f11da0f 100644 --- a/store/src/store_protocol.capnp +++ b/store/src/store_protocol.capnp @@ -3,12 +3,14 @@ interface Node { open @0 (domain: Text, networkPolicy: NetworkPolicy, ephemeral: Bool, name: Text) -> (result: Result(Store)); + iter @1 (domainPrefix: Text) -> (result: Result(StoreIter)); + iterKeys @2 () -> (result: Result(KeyIter)); interface Store { add @0 (label: Text, fingerprint: Text) -> (result: Result(Binding)); lookup @1 (label: Text) -> (result: Result(Binding)); delete @2 () -> (result: Result(Unit)); - #iterate @3 (id: UInt32) -> (result: Result(Binding)); + iter @3 () -> (result: Result(BindingIter)); } interface Binding { @@ -26,6 +28,39 @@ interface Node { import @2 (key: Data) -> (result: Result(Data)); } + # Iterators. + interface StoreIter { + next @0 () -> (result: Result(Item)); + + struct Item { + domain @0 :Text; + name @1 :Text; + networkPolicy @2 :NetworkPolicy; + entries @3 :UInt64; + store @4 :Store; + } + } + + interface BindingIter { + next @0 () -> (result: Result(Item)); + + struct Item { + label @0 :Text; + fingerprint @1 :Text; + binding @2 :Binding; + } + } + + interface KeyIter { + next @0 () -> (result: Result(Item)); + + struct Item { + fingerprint @0 :Text; + bindings @1 :UInt64; + key @2 :Key; + } + } + # Unit struct. Useful with Result. struct Unit {} diff --git a/tool/Cargo.toml b/tool/Cargo.toml index 3becea63..f0118c1a 100644 --- a/tool/Cargo.toml +++ b/tool/Cargo.toml @@ -9,6 +9,7 @@ sequoia-core = { path = "../core" } sequoia-net = { path = "../net" } sequoia-store = { path = "../store" } clap = "2.27.1" +prettytable-rs = "0.6.7" [[bin]] name = "sq" diff --git a/tool/src/main.rs b/tool/src/main.rs index dd791252..0df05a36 100644 --- a/tool/src/main.rs +++ b/tool/src/main.rs @@ -1,8 +1,13 @@ /// A command-line frontend for Sequoia. extern crate clap; +#[macro_use] +extern crate prettytable; use clap::{Arg, App, SubCommand, AppSettings}; +use prettytable::Table; +use prettytable::cell::Cell; +use prettytable::row::Row; use std::fs::File; use std::io; use std::process::exit; @@ -113,6 +118,8 @@ fn real_main() -> Result<()> { .arg(Arg::with_name("name").value_name("NAME") .required(true) .help("Name of the store")) + .subcommand(SubCommand::with_name("list") + .about("Lists keys in the store")) .subcommand(SubCommand::with_name("add") .about("Add a key identified by fingerprint") .arg(Arg::with_name("label").value_name("LABEL") @@ -160,6 +167,18 @@ fn real_main() -> Result<()> { .arg(Arg::with_name("label").value_name("LABEL") .required(true) .help("Label to use")))) + .subcommand(SubCommand::with_name("list") + .about("Lists key stores and known keys") + .subcommand(SubCommand::with_name("stores") + .about("Lists key stores") + .arg(Arg::with_name("prefix").value_name("PREFIX") + .help("List only stores with the given domain prefix"))) + .subcommand(SubCommand::with_name("bindings") + .about("Lists all bindings in all key stores") + .arg(Arg::with_name("prefix").value_name("PREFIX") + .help("List only bindings from stores with the given domain prefix"))) + .subcommand(SubCommand::with_name("keys") + .about("Lists all keys in the common key pool"))) .get_matches(); let policy = match matches.value_of("policy") { @@ -271,6 +290,9 @@ fn real_main() -> Result<()> { .expect("Failed to open store"); match m.subcommand() { + ("list", Some(_)) => { + list_bindings(&store); + }, ("add", Some(m)) => { let fp = Fingerprint::from_hex(m.value_of("fingerprint").unwrap()) .expect("Malformed fingerprint"); @@ -333,6 +355,53 @@ fn real_main() -> Result<()> { }, } }, + ("list", Some(m)) => { + match m.subcommand() { + ("stores", Some(m)) => { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + table.set_titles(row!["domain", "name", "network policy", "# of entries"]); + + for item in Store::list(&ctx, m.value_of("prefix").unwrap_or("")) + .expect("Failed to iterate over stores") { + table.add_row(Row::new(vec![ + Cell::new(&item.domain), + Cell::new(&item.name), + Cell::new(&format!("{:?}", item.network_policy)), + Cell::new(&format!("{}", item.entries))]) + ); + } + + table.printstd(); + }, + ("bindings", Some(m)) => { + for item in Store::list(&ctx, m.value_of("prefix").unwrap_or("")) + .expect("Failed to iterate over stores") { + println!("Domain {:?} Name {:?}:", item.domain, item.name); + list_bindings(&item.store); + } + }, + ("keys", Some(_)) => { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + table.set_titles(row!["fingerprint", "# of bindings"]); + + for item in Store::list_keys(&ctx) + .expect("Failed to iterate over keys") { + table.add_row(Row::new(vec![ + Cell::new(&item.fingerprint.to_string()), + Cell::new(&format!("{}", item.bindings))]) + ); + } + + table.printstd(); + }, + _ => { + eprintln!("No list subcommand given."); + exit(1); + }, + } + }, _ => { eprintln!("No subcommand given."); exit(1); @@ -342,4 +411,17 @@ fn real_main() -> Result<()> { return Ok(()) } +fn list_bindings(store: &Store) { + let mut table = Table::new(); + table.set_format(*prettytable::format::consts::FORMAT_NO_LINESEP_WITH_TITLE); + table.set_titles(row!["label", "fingerprint"]); + for item in store.iter().expect("Failed to iterate over bindings") { + table.add_row(Row::new(vec![ + Cell::new(&item.label), + Cell::new(&item.fingerprint.to_string())])); + } + table.printstd(); +} + + fn main() { real_main().expect("An error occured"); } diff --git a/tool/src/usage.rs b/tool/src/usage.rs index 07cdc099..213ac00b 100644 --- a/tool/src/usage.rs +++ b/tool/src/usage.rs @@ -22,6 +22,7 @@ //! enarmor Applies ASCII Armor to a file //! help Prints this message or the help of the given subcommand(s) //! keyserver Interacts with keyservers +//! list Lists key stores and known keys //! store Interacts with key stores //! ``` //! @@ -135,6 +136,70 @@ //! -i, --input <FILE> Sets the input file to use //! ``` //! +//! ## Subcommand list +//! +//! ```text +//! Lists key stores and known keys +//! +//! USAGE: +//! sq list [SUBCOMMAND] +//! +//! FLAGS: +//! -h, --help Prints help information +//! -V, --version Prints version information +//! +//! SUBCOMMANDS: +//! bindings Lists all bindings in all key stores +//! help Prints this message or the help of the given subcommand(s) +//! keys Lists all keys in the common key pool +//! stores Lists key stores +//! ``` +//! +//! ### Subcommand list bindings +//! +//! ```text +//! Lists all bindings in all key stores +//! +//! USAGE: +//! sq list bindings [PREFIX] +//! +//! FLAGS: +//! -h, --help Prints help information +//! -V, --version Prints version information +//! +//! ARGS: +//! <PREFIX> List only bindings from stores with the given domain prefix +//! ``` +//! +//! ### Subcommand list keys +//! +//! ```text +//! Lists all keys in the common key pool +//! +//! USAGE: +//! sq list keys +//! +//! FLAGS: +//! -h, --help Prints help information +//! -V, --version Prints version information +//! ``` +//! +//! ### Subcommand list stores +//! +//! ```text +//! Lists key stores +//! +//! USAGE: +//! sq list stores [PREFIX] +//! +//! FLAGS: +//! -h, --help Prints help information +//! -V, --version Prints version information +//! +//! ARGS: +//! <PREFIX> List only stores with the given domain prefix +//! ``` +//! //! ## Subcommand store //! //! ```text @@ -152,9 +217,11 @@ //! //! SUBCOMMANDS: //! add Add a key identified by fingerprint +//! delete Deletes bindings or stores //! export Exports a key //! help Prints this message or the help of the given subcommand(s) //! import Imports a key +//! list Lists keys in the store //! stats Get stats for the given label //! ``` //! @@ -175,6 +242,23 @@ //! <FINGERPRINT> Key to add //! ``` //! +//! ### Subcommand store delete +//! +//! ```text +//! Deletes bindings or stores +//! +//! USAGE: +//! sq store <NAME> delete [FLAGS] [LABEL] +//! +//! FLAGS: +//! -h, --help Prints help information +//! --the-store Delete the whole store +//! -V, --version Prints version information +//! +//! ARGS: +//! <LABEL> Delete binding with this label +//! ``` +//! //! ### Subcommand store export //! //! ```text @@ -215,6 +299,19 @@ //! <LABEL> Label to use //! ``` //! +//! ### Subcommand store list +//! +//! ```text +//! Lists keys in the store +//! +//! USAGE: +//! sq store <NAME> list +//! +//! FLAGS: +//! -h, --help Prints help information +//! -V, --version Prints version information +//! ``` +//! //! ### Subcommand store stats //! //! ```text |