Welcome to little lamb

Code » ssp » commit fb4e563

Update to latest changes in limb, switch to loadopt..

author Olivier Brunel
2023-05-19 14:10:26 UTC
committer Olivier Brunel
2023-07-10 09:33:53 UTC
parent aed379eab0b8e039821d7e9dd8a93c284e3d80c6

Update to latest changes in limb, switch to loadopt..

..and use it for both dealing w/ arguments and unescaping things (database
and entry names), use shldata-rw to read/write database.

Also tweak for commands :
- edit takes a required option -e/--entry to set the entry. It should be
  specified first, as anything before will basically be ignored, but this
  isn't enforced.
- get also takes a -e/--entry, but it is optional. One cna simply use
  -s/--secret to specified a secret, and any-/every-thing else needed in
  into to get a password w/out using an entry/database.
- get: add -C/--counter-val to specify a counter value w/out updating the
  underlying entry's counter value.

limb +0 -1
src/include/ssp.h +5 -21
src/ssp/add.c +91 -90
src/ssp/database.c +95 -120
src/ssp/edit.c +89 -93
src/ssp/export.c +27 -22
src/ssp/get.c +119 -101
src/ssp/import.c +103 -42
src/ssp/list.c +50 -42
src/ssp/remove.c +45 -11
src/ssp/rename.c +60 -18
src/ssp/show.c +41 -40
src/ssp/ssp.c +23 -60

diff --git a/limb b/limb
deleted file mode 120000
index c00f7b8..0000000
--- a/limb
+++ /dev/null
@@ -1 +0,0 @@
-../limb
\ No newline at end of file
diff --git a/src/include/ssp.h b/src/include/ssp.h
index c79f90c..51c5c24 100644
--- a/src/include/ssp.h
+++ b/src/include/ssp.h
@@ -13,8 +13,7 @@ enum {
     OPT_HELP = 1 << 0,
 };
 
-#define SSP_MAGIC       0x53535000
-#define SSP_MAGIC_INT   0x73737042
+#define SSP_MAGIC       0xa5e9f801
 
 #define KEY_LEN         32
 #define SALT_LEN        32
@@ -27,7 +26,7 @@ struct ssp {
     cdb cdb;
     cdb_data key;
     cdb_data val;
-    const char *db;
+    size_t cdboff;
     unsigned int options;
     u32 pos;
     char pwd[PWD_MAX + 1];
@@ -37,23 +36,9 @@ enum {
     TYPE_COUNTER = 0,
     TYPE_TIME,
     TYPE_UNKNOWN = (u8) -1,
-    /* note a "real" type, used by get to know when the counter value has been
-     * manually passed and therefore no db update should be done */
-    TYPE_COUNTER_MANUAL = TYPE_UNKNOWN - 1
 };
 
-enum {
-    ALGO_SHA1 = 0,
-    ALGO_SHA256,
-    ALGO_SHA512,
-    ALGO_SHA3_224,
-    ALGO_SHA3_256,
-    ALGO_SHA3_512,
-    ALGO_BLAKE3,
-    ALGO_UNKNOWN = (u8) -1
-};
-
-extern const char *algos[];
+#define ALGO_UNKNOWN ((u8) -1)
 
 struct otp
 {
@@ -76,8 +61,6 @@ struct otp
 /* ssp.c */
 int exitcode_from_errno(int e);
 int run_command(const char *name, int argc, const char *argv[], const char *env[], void *ctx);
-ssize_t unesc(char *dst, size_t dlen, const char *sce, size_t slen);
-const char *get_entry_name(const char *s, struct ssp *ctx);
 
 /* database.c */
 int rebuild_cdb(cdbmaker_sa *mkr, const char *oldkey, const char *newkey,
@@ -85,8 +68,9 @@ int rebuild_cdb(cdbmaker_sa *mkr, const char *oldkey, const char *newkey,
 int write_db(char *data, size_t dlen, struct ssp *ctx);
 void rebuild_db(const char *oldkey, const char *newkey, const struct otp *otp, struct ssp *ctx);
 int open_db(struct ssp *ctx);
+void close_db(struct ssp *ctx);
 
-#define db_file(ctx)        (((ctx)->db) ? (ctx)->db : (ctx)->sa.s)
+#define db_file(ctx)        (ctx)->sa.s
 #define db_reset(ctx)       (ctx)->pos = CDB_TRAVERSE_INIT()
 #define db_next(ctx)        cdb_traverse_next(&(ctx)->cdb, &(ctx)->key, &(ctx)->val, &(ctx)->pos)
 #define db_find(entry,ctx)  ((ctx)->key.s = entry, (ctx)->key.len = strlen(entry) + 1, \
diff --git a/src/ssp/add.c b/src/ssp/add.c
index 5e3ade8..b2ee8fc 100644
--- a/src/ssp/add.c
+++ b/src/ssp/add.c
@@ -7,33 +7,38 @@
 #include <limb/command.h>
 #include <limb/esc.h>
 #include <limb/exitcode.h>
+#include <limb/hasher.h> /* ALGO_* */
+#include <limb/loadopt.h>
 #include <limb/output.h>
-#include <limb/parseopt.h>
+#include <limb/stralloc.h>
 #include <limb/u16.h>
 #include "ssp.h"
 
 struct add {
+    stralloc sa;
+    const char *secret;
+    size_t cmtoff;
+    size_t entoff;
     struct otp otp;
-    const char *comments;
 };
 
 int
 validate_algo(int *first, const char *data, size_t dlen)
 {
-    dbg("validating algo ", LEN(data, dlen), "...");
-    int algo = byte_get_match(first, data, dlen, algos);
+    dbg("validating algo ", ESC, PMLEN(data, dlen), ESC);
+    int algo = byte_get_match(first, data, dlen, (const char **) algos);
     if (algo < 0)
-        warn("invalid algorithm: ", LEN(data, dlen));
+        warn("invalid algorithm: ", PMLEN(data, dlen));
     return algo;
 }
 
 int
 validate_digits(const char *data, size_t dlen)
 {
-    dbg("validating digits ", LEN(data, dlen), "...");
+    dbg("validating digits ", ESC, PMLEN(data, dlen), ESC);
     int digits = *data - '0';
     if (dlen > 1 || digits < 5 || digits > 9) {
-        warn("invalid number of digits, must be between 5 and 9: ", LEN(data, dlen));
+        warn("invalid number of digits, must be between 5 and 9: ", PMLEN(data, dlen));
         return -1;
     }
     return digits;
@@ -42,54 +47,61 @@ validate_digits(const char *data, size_t dlen)
 int
 validate_counter(u64 *c, const char *data, size_t dlen)
 {
+    dbg("validating counter", ESC, PMLEN(data, dlen), ESC);
     if (u64_scan(c, data) != dlen) {
-        warn("invalid counter value: ", LEN(data, dlen));
+        warn("invalid counter value: ", PMLEN(data, dlen));
         return -1;
     }
-    return 1;
+    return 0;
 }
 
 int
 validate_precision(const char *data, size_t dlen)
 {
-    dbg("validating precision ", LEN(data, dlen), "...");
+    dbg("validating precision ", ESC, PMLEN(data, dlen), ESC);
     u16 u;
     if (u16_scan(&u, data) != dlen || u < 10 || u > 59) {
-        warn("invalid precision argument, must be between 10 and 59: ", LEN(data, dlen));
+        warn("invalid precision argument, must be between 10 and 59: ", PMLEN(data, dlen));
         return -1;
     }
     return u;
 }
 
-COMMAND(add, "Add a new entry",
-        "<entry> [OPTION..] <secret>",
+enum {
+    ARGID_ENTRY = OPTID_FIRST,
+    ARGID_SECRET
+};
+
+COMMAND(add, "Add a new entry", "[OPTION..] <entry> <secret>",
 " -a, --algo ALGO                       Set ALGO as hashing algorithm [sha1]\n"
 "                                       Use 'list' to list available algorithms\n"
 " -C, --comments COMMENTS               Set COMMENTS as entry's comments\n"
-" -c, --counter NUM                     Set to counter-based (HOTP) with counter from NUM [1]\n"
+" -c, --counter NUM                     Set to counter-based (HOTP) with counter of NUM [1]\n"
 " -d, --digits NUM                      Set to return an OTP of NUM digits [6]\n"
 " -t, --time[=SECS]                     Set to time-based (TOTP) with precision of SECS [30]\n"
 );
 
-
 static int
-parse_cmdline(int argc, const char *argv[], const char usage[], struct add *add)
+parse_cmdline(int argc, const char *argv[], const char usage[], struct add *ctx)
 {
     const struct option options[] = {
-        OPTION_ARG_REQ ('a', "algo",                    0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('C', "comments",                0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('c', "counter",                 0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('d', "digits",                  0, OPTID_SHORTOPT),
-        OPTION_ARG_OPT ('t', "time",                    0, OPTID_SHORTOPT),
-        OPTION_DONE
+        OPTION_ARG_REQ ('a', "algo",                    0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('C', "comments",                OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('c', "counter",                 0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('d', "digits",                  0,          OPTID_SHORTOPT),
+        OPTION_ARG_OPT ('t', "time",                    0,          OPTID_SHORTOPT),
+        LOADOPT_ARGUMENTS,
+        ARGUMENT_REQ( "entry",                          OPT_PATH,   ARGID_ENTRY),
+        ARGUMENT_REQ( "secret",                         0,          ARGID_SECRET),
+        LOADOPT_DONE
     };
-    struct parseopt po = { 0 };
+    struct loadopt lo = LOADOPT_ZERO;
 
     int c, r;
-    while ((c = parseopt(argc, argv, options, 0, &po))) switch (c) {
+    while ((c = loadopt(&ctx->sa, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
         case 'a':
             {
-                if (!strcmp(PO_ARG(&po), "list")) {
+                if (!strcmp(LO_ARG(&lo), "list")) {
                     out("Available algorithms:");
                     for (r = 0; algos[r]; ++r)
                         out("- ", algos[r]);
@@ -97,114 +109,103 @@ parse_cmdline(int argc, const char *argv[], const char usage[], struct add *add)
                 }
 
                 int first = -1;
-                r = validate_algo(&first, PO_ARG(&po), strlen(PO_ARG(&po)));
+                r = validate_algo(&first, LO_ARG(&lo), strlen(LO_ARG(&lo)));
                 if (r < 0) {
                     if (first > -1)
                         list_matches(obuffer_1, OLVL_NORMAL, "did you mean ",
-                                     NULL, " or ", " ?", PO_ARG(&po), strlen(PO_ARG(&po)),
-                                     first, algos);
+                                     NULL, " or ", " ?", LO_ARG(&lo), strlen(LO_ARG(&lo)),
+                                     first, (const char **) algos);
                     diecmdusage(EX_USAGE, usage, &command_add);
                 }
-                add->otp.algo = r;
+                ctx->otp.algo = r;
             }
             break;
         case 'C':
-            add->comments = PO_ARG(&po);
+            ctx->cmtoff = LO_OFF(&lo);
             break;
         case 'c':
-            if (validate_counter(&add->otp.c, PO_ARG(&po), strlen(PO_ARG(&po))) < 0)
+            if (validate_counter(&ctx->otp.c, LO_ARG(&lo), strlen(LO_ARG(&lo))) < 0)
                 diecmdusage(EX_USAGE, usage, &command_add);
-            add->otp.type = TYPE_COUNTER;
+            ctx->otp.type = TYPE_COUNTER;
             break;
         case 'd':
-            r = validate_digits(PO_ARG(&po), strlen(PO_ARG(&po)));
+            r = validate_digits(LO_ARG(&lo), strlen(LO_ARG(&lo)));
             if (r < 0)
                 diecmdusage(EX_USAGE, usage, &command_add);
-            add->otp.digits = r;
+            ctx->otp.digits = r;
             break;
         case 't':
-            if (!PO_ARG(&po)) {
-                add->otp.t.precision = 30;
+            if (!LO_ARG(&lo)) {
+                ctx->otp.t.precision = 30;
             } else {
-                r = validate_precision(PO_ARG(&po), strlen(PO_ARG(&po)));
+                r = validate_precision(LO_ARG(&lo), strlen(LO_ARG(&lo)));
                 if (r < 0)
                     diecmdusage(EX_USAGE, usage, &command_add);
-                add->otp.t.precision = r;
+                ctx->otp.t.precision = r;
             }
-            add->otp.type = TYPE_TIME;
+            ctx->otp.type = TYPE_TIME;
             break;
+
+        case ARGID_ENTRY:
+            ctx->entoff = LO_OFF(&lo);
+            break;
+        case ARGID_SECRET:
+            ctx->secret = LO_ARG(&lo);
+            break;
+
         case -1:
             diecmdusage(EX_USAGE, usage, &command_add);
         default:
-            die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt");
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
     };
 
-    return PO_CUR(&po);
+    return LO_CUR(&lo);
 }
 
 int
-add_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
+add_main(int argc, const char *argv[], const char *env[], const char usage[], void *ssp_)
 {
-    struct ssp *ctx = ctx_;
-    struct add add = { .otp = OTP_DEFAULT, .comments = "" };
-    const char *entry;
-    int i;
-
-    if (argc == 1) {
-        warn("argument \"entry\" missing");
-        diecmdusage(EX_USAGE, usage, &command_add);
-    }
-    entry = get_entry_name(argv[1], ctx);
-    if (!entry)
-        diefusys(exitcode_from_errno(errno), "parse entry's name");
-    if (*entry == '-') {
-        warn("argument \"entry\" cannot start with a '-'");
-        diecmdusage(EX_DATA_ERR, usage, &command_add);
-    }
+    struct ssp *ssp = ssp_;
+    struct add ctx = { .sa = STRALLOC_ZERO, .otp = OTP_DEFAULT,
+        .secret = NULL, .cmtoff = (size_t) -1, .entoff = 0 };
 
-    --argc;
-    ++argv;
-    i = parse_cmdline(argc, argv, usage, &add);
-
-    if (argc == i) {
-        warn("argument \"secret\" missing");
-        diecmdusage(EX_USAGE, usage, &command_add);
-    } else if (argc > i + 1) {
-        warn("too many arguments");
-        diecmdusage(EX_USAGE, usage, &command_add);
-    }
+    (void) env;
+    parse_cmdline(argc, argv, usage, &ctx);
 
-    const char *secret = argv[i];
-    ssize_t l = base32_scan(NULL, secret, strlen(secret), 0);
+    ssize_t l = base32_scan(NULL, ctx.secret, strlen(ctx.secret), 0);
     if (l < 0)
         dief(EX_DATA_ERR, "invalid secret: Not in base32");
     if (!l)
         dief(EX_DATA_ERR, "secret is empty");
 
-    size_t clen = strlen(add.comments);
-    char buf[sizeof(add.otp) + l + clen + 1];
-    struct otp *p = (struct otp *) buf;
-    memcpy(p, &add.otp, sizeof(add.otp));
+    size_t clen = (ctx.cmtoff != (size_t) -1) ? strlen(ctx.sa.s + ctx.cmtoff) : 0;
+    size_t otplen = sizeof(ctx.otp) + l + clen + 1;
+
+    if (!stralloc_readyplus(&ctx.sa, otplen))
+        diefusys(EX_TEMPFAIL, "add entry ", ESC, ctx.sa.s + ctx.entoff, ESC);
+
+    struct otp *p = (struct otp *) (ctx.sa.s + ctx.sa.len);
+    ctx.sa.len += otplen;
+    memcpy(p, &ctx.otp, sizeof(ctx.otp));
     p->slen = l;
-    base32_scan(p->data, secret, strlen(secret), 0);
-    if (clen >= 2 && add.comments[0] == '"' && add.comments[clen - 1] == '"') {
-        ++add.comments;
-        clen -= 2;
-        dbg("unescaping comments");
-        l = unesc(p->data + l, clen, add.comments, clen);
-        if (l < 0)
-            diefusys(EX_DATA_ERR, "read comments");
-    } else {
-        memcpy(p->data + l, add.comments, clen + 1);
-    }
+    base32_scan(p->data, ctx.secret, strlen(ctx.secret), 0);
+    if (clen)
+        memcpy(p->data + l, ctx.sa.s + ctx.cmtoff, clen + 1);
+    else
+        p->data[l] = 0;
 
-    if (open_db(ctx) < 0)
+    if (open_db(ssp) < 0)
         diefu(exitcode_from_errno(errno), "open database");
-    out("Adding entry ", ESC, entry, ESC, "... ");
-    rebuild_db(NULL, entry, p, ctx);
 
+    const char *entry = ctx.sa.s + ctx.entoff;
+    out("Adding entry ", ESC, entry, ESC, "...");
+    rebuild_db(NULL, entry, p, ssp);
+
+    dbg("showing added entry");
+    close_db(ssp);
     const char *av[] = { NULL, entry };
-    run_command("show", sizeof(av) / sizeof(*av), av, env, ctx);
+    run_command("show", sizeof(av) / sizeof(*av), av, env, ssp);
 
+    stralloc_free(&ctx.sa);
     return 0;
 }
diff --git a/src/ssp/database.c b/src/ssp/database.c
index 0602571..a3ecbdd 100644
--- a/src/ssp/database.c
+++ b/src/ssp/database.c
@@ -2,123 +2,106 @@
  * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
 /* SPDX-License-Identifier: GPL-2.0-only */
 #include <errno.h>
+#include <fcntl.h> /* AT_FDCWD */
 #include <string.h>
-#include <skalibs/random.h>
-#include <limb/djbunix.h>
+#include <unistd.h>
 #include <limb/bytestr.h>
 #include <limb/cdb.h>
 #include <limb/cdbmake.h>
-#include <limb/chacha20.h>
 #include <limb/exitcode.h>
-#include <limb/hasher_sha3_256.h>
+#include <limb/hasher.h>
 #include <limb/output.h>
-#include <limb/pbkdf2.h>
+#include <limb/shldata-rw.h>
+#include <limb/stralloc.h>
 #include <limb/term.h>
 #include <limb/u32.h>
 #include "ssp.h"
 
-int
-get_key(char key[KEY_LEN], const char salt[SALT_LEN], const char *prompt, struct ssp *ctx)
+static ssize_t
+get_pwd(const char *prompt, struct ssp *ctx)
 {
     ssize_t plen;
     if (!*ctx->pwd) {
         dbg("asking for password");
+        if (prompt) adde("[", PUTMSG_TOGGLE_ESC, db_file(ctx), PUTMSG_TOGGLE_ESC, "] ");
         plen = ask_password(ctx->pwd, sizeof(ctx->pwd), prompt);
-        if (plen < 0) return 0;
     } else {
+        dbg("password known");
         plen = strlen(ctx->pwd);
     }
-
-    dbg("deriving encryption key");
-    /* get encryption key */
-    pbkdf2(key, KEY_LEN, sha3_256, ctx->pwd, plen, salt, SALT_LEN, ITER);
-
-    return 1;
+    return plen;
 }
 
 int
 open_db(struct ssp *ctx)
 {
-    dbg("opening database...");
-    int fd = open_read(db_file(ctx));
-    if (fd < 0) {
-        /* no file is not an error at this point */
-        if (errno == ENOENT) {
-            ctx->cdb = cdb_zero;
-            return 0;
-        }
-        warnusys("open ", ESC, db_file(ctx), ESC);
-        return -1;
-    }
+    dbg("opening database");
 
-    size_t salen = ctx->sa.len;
-    if (!slurp(&ctx->sa, fd)) {
-        warnusys("read ", ESC, db_file(ctx), ESC);
-        fd_close(fd);
+    if (ctx->cdb.map) {
+        warnu("open ", ESC, db_file(ctx), ESC, ": database already opened");
+        errno = EALREADY;
         return -1;
     }
-    fd_close(fd);
 
-    dbg("checking magic: ", HEX(ctx->sa.s + salen, sizeof(u32)));
-    /* check our magic */
-    u32p_be((u32 *) (ctx->sa.s + salen));
-    if (* (u32 *) (ctx->sa.s + salen) != SSP_MAGIC) {
-        warnu("read ", ESC, db_file(ctx), ESC, ": not an SSP database");
-        ctx->sa.len = salen;
-        return (errno = ENOMSG, -1);
-    }
-    /* set off past the magic, to the salt */
-    size_t off = salen + sizeof(u32);
-    char salt[SALT_LEN];
-    memcpy(salt, ctx->sa.s + off, sizeof(salt));
-    /* move off past salt, to encrypted data */
-    off += sizeof(salt);
+    size_t off  = ctx->sa.len;
+    ssize_t plen;
 
-    /* check the fileis big enough to contain up to encrypted data */
-    if (ctx->sa.len <= off) {
-        warnu("read ", ESC, db_file(ctx), ESC, ": file corrupted");
-        ctx->sa.len = salen;
-        return (errno = EBADE, -1);
+    plen = get_pwd("Enter database password: ", ctx);
+    if (plen < 0) {
+        warnusys("open ", ESC, db_file(ctx), ESC, ": cannot read password");
+        return -1;
     }
 
-    char key[KEY_LEN];
-    if (!get_key(key, salt, "Enter database password: ", ctx)) {
-        warnusys("read ", ESC, db_file(ctx), ESC, ": cannot read password");
-        ctx->sa.len = salen;
+    u32 magic = SSP_MAGIC;
+    u64 ver;
+    if (!shldata_read(&magic, &ver, &ctx->sa, AT_FDCWD, db_file(ctx), ctx->pwd, plen)) {
+        if (errno == ENOENT) {
+            ctx->cdb = cdb_zero;
+            ctx->cdboff = off;
+            return 0;
+        }
+        if (errno == EINVAL && magic != SSP_MAGIC)
+            warnu("open database ", ESC, db_file(ctx), ESC, ": ", "not an SSP database");
+        else if (errno == EBADMSG)
+            warnu("open database ", ESC, db_file(ctx), ESC, ": ", "wrong password");
+        else
+            warnusys("open ", ESC, db_file(ctx), ESC);
         return -1;
     }
 
-    dbg("decrypting database");
-    /* decrypt data in-place */
-    char nonce[NONCE_LEN] = { 0 };
-    chacha20(ctx->sa.s + salen, key, nonce, ctx->sa.s + off, ctx->sa.len - off);
-    ctx->sa.len -= off - salen;
-    off = salen;
-
-    dbg("checking internal magic: ", HEX(ctx->sa.s + off, sizeof(u32)));
-    /* check internal magic, to "validate" the password */
-    u32p_be((u32 *) (ctx->sa.s + off));
-    if (* (u32 *) (ctx->sa.s + off) != SSP_MAGIC_INT) {
-        warnu("read ", ESC, db_file(ctx), ESC, ": wrong password");
-        ctx->sa.len = salen;
-        return (errno = EINVAL, -1);
+    if (ver > 0) {
+        warnu("open database ", ESC, db_file(ctx), ESC, ": ", "version too recent");
+        return -1;
     }
-    /* move off past internal magic, to actual cdb content */
-    off += sizeof(u32);
 
-    dbg("initializing cdb");
+    ctx->cdboff = off;
     cdb_init_frommem(&ctx->cdb, ctx->sa.s + off, ctx->sa.len - off);
+    dbg("initialized cdb [off=", PMUINT(off), " size=", PMUINT(ctx->cdb.size), "]");
+
     return 1;
 }
 
+void
+close_db(struct ssp *ctx)
+{
+    dbg("closing database");
+
+    if (ctx->cdb.map) {
+        stralloc_remove(&ctx->sa, ctx->cdboff, ctx->cdb.size);
+        ctx->cdboff = 0;
+        ctx->cdb.map = NULL;
+        ctx->cdb.size = 0;
+    }
+}
+
 int
 rebuild_cdb(cdbmaker_sa *mkr, const char *oldkey, const char *newkey,
             const struct otp *otp, struct ssp *ctx)
 {
     size_t olen = 0, nlen = 0, vlen = 0;
-    int r = 0;
+    int r, n = 0;
 
-    dbg("rebuiling cdb...");
+    dbg("rebuiling cdb");
 
     /* oldkey & newkey  : renaming/editing an entry
      * oldkey & NULL    : removing an entry
@@ -132,27 +115,27 @@ rebuild_cdb(cdbmaker_sa *mkr, const char *oldkey, const char *newkey,
     }
     if (oldkey) olen = strlen(oldkey) + 1;
 
-    dbg("oldkey=", ESC, oldkey, ESC, " newkey=", ESC, newkey, ESC,
-        " vlen=", PUTMSG_UINT(vlen));
+    dbg("oldkey=", ESC, oldkey, ESC, " newkey=", ESC, newkey, ESC, " vlen=", PMUINT(vlen));
 
     if (!cdbmaker_sa_start(mkr))
-        return 0;
+        return -1;
     /* new file? */
     if (!ctx->cdb.map) {
         dbg("new file, adding newkey");
         if (nlen && !cdbmaker_sa_add(mkr, newkey, nlen, (char *) otp, vlen))
-            return 0;
+            return -1;
+        ++n;
     } else {
         int a = !nlen;
         db_reset(ctx);
-        dbg("iterating current cdb...");
+        dbg("iterating current cdb");
         while ((r = db_next(ctx)) == 1) {
             dbg("key=", ESC, db_entry(ctx), ESC);
             /* existing newkey isn't allowed when adding or renaming */
             if (nlen == ctx->key.len && !strcmp(newkey, ctx->key.s)
                     && (!olen || strcmp(oldkey, newkey))) {
                 warnu("add entry ", ESC, newkey, ESC, ": Entry already exists");
-                return (errno = EINVAL, 0);
+                return (errno = EINVAL, -1);
             } else {
                 /* Note the use of strcoll() here to ensure we order entries
                  * using locale-specific rules. */
@@ -160,7 +143,8 @@ rebuild_cdb(cdbmaker_sa *mkr, const char *oldkey, const char *newkey,
                     dbg("inserting newkey before");
                     /* insert otp at the right place */
                     if (!cdbmaker_sa_add(mkr, newkey, nlen, (char *) otp, vlen))
-                        return 0;
+                        return -1;
+                    ++n;
                     a = 1;
                 }
                 /* if current entry isn't the old one nor the new one, re-add */
@@ -169,89 +153,80 @@ rebuild_cdb(cdbmaker_sa *mkr, const char *oldkey, const char *newkey,
                     dbg("preserving key");
                     if (!cdbmaker_sa_add(mkr, ctx->key.s, ctx->key.len,
                                 ctx->val.s, ctx->val.len))
-                        return 0;
+                        return -1;
+                    ++n;
                 }
             }
         }
         if (r < 0) {
             warnu("read database: file corrupted");
-            return (errno = EINVAL, 0);
+            return (errno = EINVAL, -1);
         }
         if (!a) {
             dbg("inserting newkey last");
             /* insert last */
             if (!cdbmaker_sa_add(mkr, newkey, nlen, (char *) otp, vlen))
-                return 0;
+                return -1;
+            ++n;
         }
     }
+    dbg("finishing rebuild (", PMINT(n), " entries)");
     if (!cdbmaker_sa_finish(mkr))
-        return 0;
+        return -1;
 
-    return 1;
+    return n;
 }
 
 int
 write_db(char *data, size_t dlen, struct ssp *ctx)
 {
-    dbg("writing db...");
+    dbg("writing db");
 
-    char salt[SALT_LEN];
-    char key[KEY_LEN];
-    char nonce[NONCE_LEN] = { 0 };
+    ssize_t plen;
 
-    /* each time we save the db, we generate a new key to encrypt it */
-    random_buf(salt, sizeof(salt));
-    if (!get_key(key, salt, "Enter new database password: ", ctx)) {
+    plen = get_pwd("Enter new database password: ", ctx);
+    if (plen < 0) {
         warnusys("read password");
         return (errno = EINVAL, 0);
     }
 
-    u32 magic_int = SSP_MAGIC_INT;
-    u32p_be(&magic_int);
-
-    dbg("encrypting database");
-    chacha20_ctx chacha;
-    chacha20_init(key, nonce, &chacha);
-    chacha20_crypt(&magic_int, &magic_int, sizeof(magic_int), &chacha);
-    chacha20_crypt(data, data, dlen, &chacha);
-    chacha20_clear(&chacha);
+    struct iovec v;
+    v.iov_base = data;
+    v.iov_len = dlen;
 
-    u32 magic = SSP_MAGIC;
-    u32p_be(&magic);
-
-    struct iovec v[4];
-    v[0].iov_base = &magic;
-    v[0].iov_len = sizeof(magic);
-    v[1].iov_base = salt;
-    v[1].iov_len = sizeof(salt);
-    v[2].iov_base = &magic_int;
-    v[2].iov_len = sizeof(magic_int);
-    v[3].iov_base = data;
-    v[3].iov_len = dlen;
-
-    dbg("saving database");
-    if (!openwritevnclose(db_file(ctx), v, 4)) {
+    if (!shldata_write(AT_FDCWD, db_file(ctx), SSP_MAGIC, 0, ctx->pwd, plen,
+                ALGO_SHA3_256, ITER, 1, &v, 1)) {
         warnusys("write database to ", ESC, db_file(ctx), ESC);
         return 0;
     }
 
+    dbg("database written to ", ESC, db_file(ctx), ESC);
     return 1;
 }
 
 void
 rebuild_db(const char *oldkey, const char *newkey, const struct otp *otp, struct ssp *ctx)
 {
-    dbg("rebuilding database...");
+    dbg("rebuilding database");
 
     cdbmaker_sa mkr = CDBMAKER_SA_ZERO;
 
-    if (!rebuild_cdb(&mkr, oldkey, newkey, otp, ctx))
+    int r = rebuild_cdb(&mkr, oldkey, newkey, otp, ctx);
+    if (r < 0)
         diefu(exitcode_from_errno(errno), "save database",
               (errno == ENOMEM) ? ": Out of memory" : NULL);
 
-    if (!write_db(mkr.sa.s, mkr.sa.len, ctx))
-        diefusys((errno == EINVAL) ? EX_DATA_ERR : EX_CANTCREAT, "save database");
+    if (!r) {
+        dbg("empty cdb, removing database");
+        if (unlink(db_file(ctx)) < 0 && errno != ENOENT)
+            diefusys(EX_IOERR, "remove database");
+        out("Database empty, ", ESC, db_file(ctx), ESC, " removed");
+    } else {
+        dbg("saving new database");
+        if (!write_db(mkr.sa.s, mkr.sa.len, ctx))
+            diefusys((errno == EINVAL) ? EX_DATA_ERR : EX_CANTCREAT, "save database");
+        out("Database saved to ", ESC, db_file(ctx), ESC);
+    }
 
     cdbmaker_sa_free(&mkr);
-    out("Database saved to ", ESC, db_file(ctx), ESC);
 }
diff --git a/src/ssp/edit.c b/src/ssp/edit.c
index dca99ee..8da75a7 100644
--- a/src/ssp/edit.c
+++ b/src/ssp/edit.c
@@ -7,48 +7,53 @@
 #include <limb/command.h>
 #include <limb/esc.h>
 #include <limb/exitcode.h>
+#include <limb/hasher.h> /* ALGO_* */
+#include <limb/loadopt.h>
 #include <limb/output.h>
-#include <limb/parseopt.h>
+#include <limb/stralloc.h>
 #include <limb/u16.h>
 #include "ssp.h"
 
 struct edit {
-    struct otp otp;
-    const char *comments;
+    stralloc sa;
     const char *secret;
+    size_t cmtoff;
+    size_t entoff;
+    struct otp otp;
 };
 
-COMMAND(edit, "Edit an entry",
-        "<entry> OPTION[..]",
+COMMAND(edit, "Edit an entry", "-e ENTRY OPTION[..]",
 " -a, --algo ALGO                       Set ALGO as hashing algorithm\n"
 "                                       Use 'list' to list available algorithms\n"
 " -C, --comments COMMENTS               Set COMMENTS as entry's comments\n"
-" -c, --counter[=NUM]                   Set to counter-based (HOTP) with counter from NUM [1]\n"
+" -c, --counter[=NUM]                   Set to counter-based (HOTP) with counter of NUM [1]\n"
 " -d, --digits NUM                      Set to return an OTP of NUM digits [6]\n"
+" -e, --entry ENTRY                     Edit entry ENTRY\n"
 " -s, --secret SECRET                   Set SECRET as entry's secret\n"
 " -t, --time[=SECS]                     Set to time-based (TOTP) with precision of SECS [30]\n"
 );
 
 
 static int
-parse_cmdline(int argc, const char *argv[], const char usage[], struct edit *edit)
+parse_cmdline(int argc, const char *argv[], const char usage[], struct edit *ctx)
 {
     const struct option options[] = {
-        OPTION_ARG_REQ ('a', "algo",                    0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('C', "comments",                0, OPTID_SHORTOPT),
-        OPTION_ARG_OPT ('c', "count",                   0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('d', "digits",                  0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('s', "secret",                  0, OPTID_SHORTOPT),
-        OPTION_ARG_OPT ('t', "time",                    0, OPTID_SHORTOPT),
-        OPTION_DONE
+        OPTION_ARG_REQ ('a', "algo",                    0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('C', "comments",                OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_OPT ('c', "count",                   0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('d', "digits",                  0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('e', "entry",                   OPT_PATH | OPT_REQ, OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('s', "secret",                  0,          OPTID_SHORTOPT),
+        OPTION_ARG_OPT ('t', "time",                    0,          OPTID_SHORTOPT),
+        LOADOPT_DONE
     };
-    struct parseopt po = { 0 };
+    struct loadopt lo = LOADOPT_ZERO;
 
     int c, r;
-    while ((c = parseopt(argc, argv, options, 0, &po))) switch (c) {
+    while ((c = loadopt(&ctx->sa, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
         case 'a':
             {
-                if (!strcmp(PO_ARG(&po), "list")) {
+                if (!strcmp(LO_ARG(&lo), "list")) {
                     out("Available algorithms:");
                     for (r = 0; algos[r]; ++r)
                         out("- ", algos[r]);
@@ -56,108 +61,93 @@ parse_cmdline(int argc, const char *argv[], const char usage[], struct edit *edi
                 }
 
                 int first = -1;
-                r = validate_algo(&first, PO_ARG(&po), strlen(PO_ARG(&po)));
+                r = validate_algo(&first, LO_ARG(&lo), strlen(LO_ARG(&lo)));
                 if (r < 0) {
                     if (first > -1)
                         list_matches(obuffer_1, OLVL_NORMAL, "did you mean ",
-                                     NULL, " or ", " ?", PO_ARG(&po), strlen(PO_ARG(&po)),
-                                     first, algos);
+                                     NULL, " or ", " ?", LO_ARG(&lo), strlen(LO_ARG(&lo)),
+                                     first, (const char **) algos);
                     diecmdusage(EX_USAGE, usage, &command_edit);
                 }
-                edit->otp.algo = r;
+                ctx->otp.algo = r;
             }
             break;
         case 'C':
-            edit->comments = PO_ARG(&po);
+            ctx->cmtoff = LO_OFF(&lo);
             break;
         case 'c':
-            if (!PO_ARG(&po)) {
-                edit->otp.c = 1;
+            if (!LO_ARG(&lo)) {
+                ctx->otp.c = 1;
             } else {
-                if (validate_counter(&edit->otp.c, PO_ARG(&po), strlen(PO_ARG(&po))) < 0)
+                if (validate_counter(&ctx->otp.c, LO_ARG(&lo), strlen(LO_ARG(&lo))) < 0)
                     diecmdusage(EX_USAGE, usage, &command_edit);
             }
-            edit->otp.type = TYPE_COUNTER;
+            ctx->otp.type = TYPE_COUNTER;
             break;
         case 'd':
-            r = validate_digits(PO_ARG(&po), strlen(PO_ARG(&po)));
+            r = validate_digits(LO_ARG(&lo), strlen(LO_ARG(&lo)));
             if (r < 0)
                 diecmdusage(EX_USAGE, usage, &command_edit);
-            edit->otp.digits = r;
+            ctx->otp.digits = r;
+            break;
+        case 'e':
+            ctx->entoff = LO_OFF(&lo);
             break;
         case 's':
-            edit->secret = PO_ARG(&po);
+            ctx->secret = LO_ARG(&lo);
             break;
         case 't':
-            if (!PO_ARG(&po)) {
-                edit->otp.t.precision = 30;
+            if (!LO_ARG(&lo)) {
+                ctx->otp.t.precision = 30;
             } else {
-                r = validate_precision(PO_ARG(&po), strlen(PO_ARG(&po)));
+                r = validate_precision(LO_ARG(&lo), strlen(LO_ARG(&lo)));
                 if (r < 0)
                     diecmdusage(EX_USAGE, usage, &command_edit);
-                edit->otp.t.precision = r;
+                ctx->otp.t.precision = r;
             }
-            edit->otp.type = TYPE_TIME;
+            ctx->otp.type = TYPE_TIME;
             break;
+
         case -1:
             diecmdusage(EX_USAGE, usage, &command_edit);
         default:
-            die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt");
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
     };
 
-    return PO_CUR(&po);
+    return LO_CUR(&lo);
 }
 
 int
-edit_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
+edit_main(int argc, const char *argv[], const char *env[], const char usage[], void *ssp_)
 {
-    struct ssp *ctx = ctx_;
-    struct edit edit = { .otp.type = TYPE_UNKNOWN, .otp.algo = ALGO_UNKNOWN, };
-    const char *entry;
-    int i;
-
-    if (argc == 1) {
-        warn("argument \"entry\" missing");
-        diecmdusage(EX_USAGE, usage, &command_edit);
-    }
-    entry = get_entry_name(argv[1], ctx);
-    if (!entry)
-        diefusys(exitcode_from_errno(errno), "parse entry's name");
-    if (*entry == '-') {
-        warn("argument \"entry\" cannot start with a '-'");
-        diecmdusage(EX_DATA_ERR, usage, &command_edit);
-    }
-
-    if (argc == 2) die(0, "nothing to do");
+    struct ssp *ssp = ssp_;
+    struct edit ctx = { .sa = STRALLOC_ZERO, .otp.type = TYPE_UNKNOWN,
+        .otp.algo = ALGO_UNKNOWN, .cmtoff = (size_t) -1 };
 
-    --argc;
-    ++argv;
-    i = parse_cmdline(argc, argv, usage, &edit);
+    (void) env;
+    parse_cmdline(argc, argv, usage, &ctx);
 
-    if (argc > i) {
-        warn("too many arguments");
-        diecmdusage(EX_USAGE, usage, &command_edit);
-    }
+    const char *entry = ctx.sa.s + ctx.entoff;
 
-    int r = open_db(ctx);
+    int r = open_db(ssp);
     if (r < 0)
         diefu(exitcode_from_errno(errno), "open database");
     if (!r)
         dief(EX_NOINPUT, "no database");
 
-    r = db_find(entry, ctx);
+    r = db_find(entry, ssp);
     if (r < 0)
         diefu(EX_DATA_ERR, "find entry ", ESC, entry, ESC);
     if (!r)
         dief(EX_DATA_ERR, "no entry ", ESC, entry, ESC, " in database");
 
-    const struct otp *cur = db_otp(ctx);
+    const struct otp *cur = db_otp(ssp);
 
     ssize_t slen;
-    if (!edit.secret) {
+    if (!ctx.secret) {
         slen = cur->slen;
     } else {
-        slen = base32_scan(NULL, edit.secret, strlen(edit.secret), 0);
+        slen = base32_scan(NULL, ctx.secret, strlen(ctx.secret), 0);
         if (slen < 0)
             dief(EX_DATA_ERR, "invalid secret: Not in base32");
         if (!slen)
@@ -165,46 +155,52 @@ edit_main(int argc, const char *argv[], const char *env[], const char usage[], v
     }
 
     size_t clen;
-    if (!edit.comments)
+    if (ctx.cmtoff == (size_t) -1)
         clen = strlen(cur->data + cur->slen);
     else
-        clen = strlen(edit.comments);
+        clen = strlen(ctx.sa.s + ctx.cmtoff);
+
+    size_t otplen = sizeof(ctx.otp) + slen + clen + 1;
 
-    char buf[sizeof(edit.otp) + slen + clen + 1];
-    struct otp *p = (struct otp *) buf;
-    p->algo = (edit.otp.algo != ALGO_UNKNOWN) ? edit.otp.algo : cur->algo;
-    p->type = (edit.otp.type != TYPE_UNKNOWN) ? edit.otp.type : cur->type;
-    p->digits = (edit.otp.digits) ? edit.otp.digits : cur->digits;
+    if (!stralloc_readyplus(&ctx.sa, otplen))
+        diefusys(EX_TEMPFAIL, "ctx entry ", ESC, entry, ESC);
+    /* re-align */
+    entry = ctx.sa.s + ctx.entoff;
+
+    dbg("constructing edited entry");
+    struct otp *p = (struct otp *) (ctx.sa.s + ctx.sa.len);
+    ctx.sa.len += otplen;
+    p->algo = (ctx.otp.algo != ALGO_UNKNOWN) ? ctx.otp.algo : cur->algo;
+    p->type = (ctx.otp.type != TYPE_UNKNOWN) ? ctx.otp.type : cur->type;
+    p->digits = (ctx.otp.digits) ? ctx.otp.digits : cur->digits;
     if (p->type == TYPE_COUNTER)
-        p->c = (edit.otp.type != TYPE_UNKNOWN) ? edit.otp.c : cur->c;
+        p->c = (ctx.otp.type != TYPE_UNKNOWN) ? ctx.otp.c : cur->c;
     else
-        p->t.precision = (edit.otp.type != TYPE_UNKNOWN) ? edit.otp.t.precision : cur->t.precision;
+        p->t.precision = (ctx.otp.type != TYPE_UNKNOWN) ? ctx.otp.t.precision : cur->t.precision;
 
     p->slen = slen;
-    if (edit.secret)
-        base32_scan(p->data, edit.secret, strlen(edit.secret), 0);
+    if (ctx.secret)
+        base32_scan(p->data, ctx.secret, strlen(ctx.secret), 0);
     else
         memcpy(p->data, cur->data, slen);
 
-    if (edit.comments) {
-        if (clen >= 2 && edit.comments[0] == '"' && edit.comments[clen - 1] == '"') {
-            ++edit.comments;
-            clen -= 2;
-            slen = unesc(p->data + slen, clen, edit.comments, clen);
-            if (slen < 0)
-                diefusys(EX_DATA_ERR, "read comments");
-        } else {
-            memcpy(p->data + slen, edit.comments, clen + 1);
-        }
-    } else {
-        memcpy(p->data + slen, cur->data + slen, clen + 1);
-    }
+    if (ctx.cmtoff == (size_t) -1)
+        memcpy(p->data + slen, cur->data + cur->slen, clen + 1);
+    else
+        memcpy(p->data + slen, ctx.sa.s + ctx.cmtoff, clen + 1);
+
+    if (cur->slen == p->slen && strlen(cur->data + cur->slen) == clen
+            && !memcmp(p, cur, otplen))
+        dief(EX_DATA_ERR, "nothing to do");
 
     out("Editing entry ", ESC, entry, ESC, "... ");
-    rebuild_db(entry, entry, p, ctx);
+    rebuild_db(entry, entry, p, ssp);
 
+    dbg("showing edited entry");
+    close_db(ssp);
     const char *av[] = { NULL, entry };
-    run_command("show", sizeof(av) / sizeof(*av), av, env, ctx);
+    run_command("show", sizeof(av) / sizeof(*av), av, env, ssp);
 
+    stralloc_free(&ctx.sa);
     return 0;
 }
diff --git a/src/ssp/export.c b/src/ssp/export.c
index 9554c3b..949bed5 100644
--- a/src/ssp/export.c
+++ b/src/ssp/export.c
@@ -4,8 +4,8 @@
 #include <errno.h>
 #include <limb/command.h>
 #include <limb/exitcode.h>
+#include <limb/loadopt.h>
 #include <limb/output.h>
-#include <limb/parseopt.h>
 #include "ssp.h"
 
 enum {
@@ -13,9 +13,14 @@ enum {
 };
 
 struct export {
+    const char *ptrn;
     int options;
 };
 
+enum {
+    ARGID_PATTERN = OPTID_FIRST,
+};
+
 COMMAND(export, "Export entries", "[OPTION..] [<pattern>]",
 " -C, --no-comments                     Do not export comments\n"
 );
@@ -24,42 +29,42 @@ static int
 parse_cmdline(int argc, const char *argv[], const char usage[], struct export *ctx)
 {
     const struct option options[] = {
-        OPTION_ARG_NONE('C', "no-comments",             0, OPTID_SHORTOPT),
-        OPTION_DONE
+        OPTION_ARG_NONE('C', "no-comments",             0,          OPTID_SHORTOPT),
+        LOADOPT_ARGUMENTS,
+        ARGUMENT_OPT( "pattern",                        0,          ARGID_PATTERN),
+        LOADOPT_DONE
     };
-    struct parseopt po = { 0 };
+    struct loadopt lo = LOADOPT_ZERO;
 
     int c;
-    while ((c = parseopt(argc, argv, options, 0, &po))) switch (c) {
+    while ((c = loadopt(NULL, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
         case 'C':
             ctx->options |= OPT_NO_COMMENTS;
             break;
+
+        case ARGID_PATTERN:
+            ctx->ptrn = LO_ARG(&lo);
+            break;
+
         case -1:
             diecmdusage(EX_USAGE, usage, &command_export);
         default:
-            die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt");
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
     };
 
-    return PO_CUR(&po);
+    return LO_CUR(&lo);
 }
 
 int
-export_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
+export_main(int argc, const char *argv[], const char *env[], const char usage[], void *ssp_)
 {
-    struct export export = { 0 };
-
-    int i = parse_cmdline(argc, argv, usage, &export);
+    struct ssp *ssp = ssp_;
+    struct export ctx = { .ptrn = NULL, .options = 0 };
 
-    if (argc > i + 1) {
-        warn("too many arguments");
-        diecmdusage(EX_USAGE, usage, &command_export);
-    } else if (argc == i) {
-        argc = i = 0;
-    } else {
-        argc = 1;
-    }
+    (void) env;
+    parse_cmdline(argc, argv, usage, &ctx);
 
-    int C = !!(export.options & OPT_NO_COMMENTS);
-    const char *av[] = { NULL, "--format", (C) ? "--no-comments" : argv[i], argv[i] };
-    return run_command("list", 2 + C + argc, av, env, ctx_);
+    int C = !!(ctx.options & OPT_NO_COMMENTS);
+    const char *av[] = { NULL, "--format", (C) ? "--no-comments" : ctx.ptrn, ctx.ptrn };
+    return run_command("list", 2 + C + !!ctx.ptrn, av, env, ssp);
 }
diff --git a/src/ssp/get.c b/src/ssp/get.c
index 35ec2aa..50c15f2 100644
--- a/src/ssp/get.c
+++ b/src/ssp/get.c
@@ -15,8 +15,9 @@
 #include <limb/hasher_sha3_256.h>
 #include <limb/hasher_sha3_512.h>
 #include <limb/hasher_blake3.h>
+#include <limb/loadopt.h>
 #include <limb/output.h>
-#include <limb/parseopt.h>
+#include <limb/stralloc.h>
 #include <limb/u32.h>
 #include "ssp.h"
 #include "hotp.h"
@@ -24,155 +25,172 @@
 struct get {
     struct otp otp;
     const char *secret;
+    struct ssp *ssp;
     u64 ts;
+    u8 upd_entry;
 };
 
-COMMAND(get, "Get a One-Time Password", "<entry> [OPTION..]",
+COMMAND(get, "Get a One-Time Password", "[-e ENTRY | -s SECRET] [OPTION..]",
 " -a, --algo ALGO                       Set ALGO as hashing algorithm [sha1]\n"
-" -c, --counter[=NUM]                   Set to counter-based (HOTP) with counter from NUM [1]\n"
+" -C, --counter-val NUM                 Same as --counter but don't update entry\n"
+" -c, --counter[=NUM]                   Set to counter-based (HOTP) with counter of NUM [1]\n"
 " -d, --digits NUM                      Set to return an OTP of NUM digits [6]\n"
+" -e, --entry ENTRY                     Get a password for ENTRY\n"
 " -s, --secret SECRET                   Set SECRET as entry's secret\n"
 " -T, --use-time TS                     Use TS as unix timestamp instead of current time\n"
 " -t, --time[=SECS]                     Set to time-based (TOTP) with precision of SECS [30]\n"
 );
 
 static int
-parse_cmdline(int argc, const char *argv[], const char usage[], struct get *get)
+parse_cmdline(int argc, const char *argv[], const char usage[], struct get *ctx)
 {
     const struct option options[] = {
-        OPTION_ARG_REQ ('a', "algo",                    0, OPTID_SHORTOPT),
-        OPTION_ARG_OPT ('c', "counter",                 0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('d', "digits",                  0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('s', "secret",                  0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('T', "use-time",                0, OPTID_SHORTOPT),
-        OPTION_ARG_OPT ('t', "time",                    0, OPTID_SHORTOPT),
-        OPTION_DONE
+        OPTION_ARG_REQ ('a', "algo",                    0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('C', "counter",                 0,          OPTID_SHORTOPT),
+        OPTION_ARG_OPT ('c', "counter",                 0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('d', "digits",                  0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('e', "entry",                   OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('s', "secret",                  0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('T', "use-time",                0,          OPTID_SHORTOPT),
+        OPTION_ARG_OPT ('t', "time",                    0,          OPTID_SHORTOPT),
+        LOADOPT_DONE
     };
-    struct parseopt po = { 0 };
+    struct loadopt lo = LOADOPT_ZERO;
 
     int c, r;
-    while ((c = parseopt(argc, argv, options, 0, &po))) switch (c) {
+    while ((c = loadopt(&ctx->ssp->sa, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
         case 'a':
             {
                 int first = -1;
-                r = validate_algo(&first, PO_ARG(&po), strlen(PO_ARG(&po)));
+                r = validate_algo(&first, LO_ARG(&lo), strlen(LO_ARG(&lo)));
                 if (r < 0) {
                     if (first > -1)
                         list_matches(obuffer_1, OLVL_NORMAL, "did you mean ",
-                                     NULL, " or ", " ?", PO_ARG(&po), strlen(PO_ARG(&po)),
-                                     first, algos);
+                                     NULL, " or ", " ?", LO_ARG(&lo), strlen(LO_ARG(&lo)),
+                                     first, (const char **) algos);
                     diecmdusage(EX_USAGE, usage, &command_get);
                 }
-                get->otp.algo = r;
+                ctx->otp.algo = r;
+                dbg("setting algo to ", algos[r]);
             }
             break;
+        case 'C':
+            ctx->upd_entry = 0;
+            dbg("will not update entry's counter value");
+            /* fall through */
         case 'c':
-            if (!PO_ARG(&po)) {
-                get->otp.c = 1;
+            if (!LO_ARG(&lo)) {
+                ctx->otp.c = 1;
             } else {
-                if (validate_counter(&get->otp.c, PO_ARG(&po), strlen(PO_ARG(&po))) < 0)
+                if (validate_counter(&ctx->otp.c, LO_ARG(&lo), strlen(LO_ARG(&lo))) < 0)
                     diecmdusage(EX_USAGE, usage, &command_get);
             }
-            get->otp.type = TYPE_COUNTER_MANUAL;
+            ctx->otp.type = TYPE_COUNTER;
+            dbg("setting to HOTP with counter=", PMUINT(ctx->otp.c));
             break;
         case 'd':
-            r = validate_digits(PO_ARG(&po), strlen(PO_ARG(&po)));
+            r = validate_digits(LO_ARG(&lo), strlen(LO_ARG(&lo)));
             if (r < 0)
                 diecmdusage(EX_USAGE, usage, &command_get);
-            get->otp.digits = r;
+            ctx->otp.digits = r;
+            dbg("setting to ", PMUINT(r), " digits");
+            break;
+        case 'e':
+            {
+                const char *entry = ctx->ssp->sa.s + LO_OFF(&lo);
+                int r;
+
+                dbg("looking for entry ", ESC, entry, ESC);
+
+                if (ctx->ssp->cdb.map) {
+                    /* re-align cdb in case of a re-alloc */
+                    ctx->ssp->cdb.map = ctx->ssp->sa.s + ctx->ssp->cdboff;
+                } else {
+                    r = open_db(ctx->ssp);
+                    if (r < 0)
+                        diefu(exitcode_from_errno(errno), "open database");
+                    if (!r)
+                        dief(EX_NOINPUT, "no database");
+                    /* re-align */
+                    entry = ctx->ssp->sa.s + LO_OFF(&lo);
+                }
+
+                r = db_find(entry, ctx->ssp);
+                if (r < 0)
+                    diefu(EX_DATA_ERR, "find entry ", ESC, entry, ESC);
+                if (!r)
+                    dief(EX_DATA_ERR, "no entry ", ESC, entry, ESC, " in database");
+
+                ctx->otp = *db_otp(ctx->ssp);
+                ctx->secret = db_otp(ctx->ssp)->data;
+                ctx->upd_entry = 1;
+                dbg("using entry ", ESC, entry, ESC);
+            }
             break;
         case 's':
-            get->secret = PO_ARG(&po);
-            get->otp.slen = 0;
+            ctx->secret = LO_ARG(&lo);
+            ctx->otp.slen = 0;
+            dbg("setting secret to ", ctx->secret);
             break;
         case 'T':
-            if (!u640_scan(&get->ts, PO_ARG(&po))) {
-                warn("invalid time stamp: ", PO_ARG(&po));
+            if (!u640_scan(&ctx->ts, LO_ARG(&lo))) {
+                warn("invalid time stamp: ", LO_ARG(&lo));
                 diecmdusage(EX_USAGE, usage, &command_get);
             }
+            dbg("using time value of ", PMUINT(ctx->ts));
             break;
         case 't':
-            if (!PO_ARG(&po)) {
-                get->otp.t.precision = 30;
+            if (!LO_ARG(&lo)) {
+                ctx->otp.t.precision = 30;
             } else {
-                r = validate_precision(PO_ARG(&po), strlen(PO_ARG(&po)));
+                r = validate_precision(LO_ARG(&lo), strlen(LO_ARG(&lo)));
                 if (r < 0)
                     diecmdusage(EX_USAGE, usage, &command_get);
-                get->otp.t.precision = r;
+                ctx->otp.t.precision = r;
             }
-            get->otp.type = TYPE_TIME;
+            ctx->otp.type = TYPE_TIME;
+            dbg("setting to TOTP with precision=", PMUINT(ctx->otp.t.precision));
             break;
+
         case -1:
             diecmdusage(EX_USAGE, usage, &command_get);
         default:
-            die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt");
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
     };
 
-    return PO_CUR(&po);
+    return LO_CUR(&lo);
 }
 
 int
-get_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
+get_main(int argc, const char *argv[], const char *env[], const char usage[], void *ssp_)
 {
-    struct ssp *ctx = ctx_;
-    struct get get = { .otp = OTP_DEFAULT, .otp.type = TYPE_COUNTER_MANUAL,
-        .secret = NULL, .ts = (u64) -1 };
+    struct ssp *ssp = ssp_;
+    struct get ctx = { .otp = OTP_DEFAULT, .secret = NULL, .ssp = ssp,
+        .ts = (u64) -1, .upd_entry = 0 };
 
-    if (argc == 1) {
-        warn("argument \"entry\" missing");
-        diecmdusage(EX_USAGE, usage, &command_get);
-    }
+    (void) env;
+    parse_cmdline(argc, argv, usage, &ctx);
 
-    struct otp *otp;
-    if (strcmp(argv[1], "-")) {
-        const char *entry = get_entry_name(argv[1], ctx);
-        if (!entry)
-            diefusys(exitcode_from_errno(errno), "parse entry's name");
-
-        int r = open_db(ctx);
-        if (r < 0)
-            diefu(exitcode_from_errno(errno), "open database");
-        if (!r)
-            dief(EX_NOINPUT, "no database");
-
-        r = db_find(entry, ctx);
-        if (r < 0)
-            diefu(EX_DATA_ERR, "find entry ", ESC, entry, ESC);
-        if (!r)
-            dief(EX_DATA_ERR, "no entry ", ESC, entry, ESC, " in database");
-
-        get.otp = *db_otp(ctx);
-        get.secret = db_otp(ctx)->data;
-    }
-
-    --argc;
-    ++argv;
-    int i = parse_cmdline(argc, argv, usage, &get);
-
-    if (argc > i) {
-        warn("too many arguments");
-        diecmdusage(EX_USAGE, usage, &command_get);
-    }
-
-    if (!get.secret)
+    if (!ctx.secret)
         dief(EX_DATA_ERR, "secret missing");
 
-    if (!get.otp.slen) {
-        size_t l = strlen(get.secret);
-        ssize_t r = base32_scan(NULL, get.secret, l, 0);
+    if (!ctx.otp.slen) {
+        dbg("decoding secret from base32");
+        size_t l = strlen(ctx.secret);
+        ssize_t r = base32_scan(NULL, ctx.secret, l, 0);
         if (r < 0)
             dief(EX_DATA_ERR, "invalid secret: Not in base32");
-        if (!stralloc_readyplus(&ctx->sa, r))
+        if (!stralloc_readyplus(&ssp->sa, r))
             diefusys(EX_TEMPFAIL, "process secret");
-        base32_scan(ctx->sa.s + ctx->sa.len, get.secret, l , 0);
-        get.secret = ctx->sa.s + ctx->sa.len;
-        /* we don't update ctx->sa.len on purpose, so in case we need to rewrite
+        base32_scan(ssp->sa.s + ssp->sa.len, ctx.secret, l , 0);
+        ctx.secret = ssp->sa.s + ssp->sa.len;
+        /* we don't update ssp->sa.len on purpose, so in case we need to rewrite
          * the db the calculation is easier */
-        get.otp.slen = r;
+        ctx.otp.slen = r;
     }
 
     hasher *hr = sha1;
-    switch (get.otp.algo) {
+    switch (ctx.otp.algo) {
         case ALGO_SHA256:   hr = sha256;   break;
         case ALGO_SHA512:   hr = sha512;   break;
         case ALGO_SHA3_224: hr = sha3_224; break;
@@ -182,31 +200,31 @@ get_main(int argc, const char *argv[], const char *env[], const char usage[], vo
     }
 
     u64 c;
-
-    if (get.otp.type == TYPE_TIME) {
-        if (get.ts == (u64) -1)
-            get.ts = time(NULL);
-        c = get.ts / get.otp.t.precision;
+    if (ctx.otp.type == TYPE_TIME) {
+        if (ctx.ts == (u64) -1)
+            ctx.ts = time(NULL);
+        c = ctx.ts / ctx.otp.t.precision;
     } else {
-        c = get.otp.c;
+        c = ctx.otp.c;
     }
 
-    dbg("calculating HOTP c=", PUTMSG_UINT(c), " digits=", PUTMSG_UINT(get.otp.digits));
-    int r = hotp(hr, get.secret, get.otp.slen, c, get.otp.digits);
-    add((get.otp.type == TYPE_COUNTER) ? "H" : "T", "OTP: ");
-    char buf[get.otp.digits + 1];
+    dbg("calculating HOTP c=", PMUINT(c), " digits=", PMUINT(ctx.otp.digits));
+    int r = hotp(hr, ctx.secret, ctx.otp.slen, c, ctx.otp.digits);
+    add((ctx.otp.type == TYPE_COUNTER) ? "H" : "T", "OTP: ");
+    char buf[ctx.otp.digits + 1];
     buf[u320_fmt(buf, r, sizeof(buf) - 1)] = 0;
     quiet(buf);
 
     /* TYPE_COUNTER implies to write the db with an updated/incremented
-     * counter (for next time) */
-    if (get.otp.type == TYPE_COUNTER) {
-        struct otp *e = (struct otp *) db_otp(ctx);
-        ++e->c;
-
-        size_t off = (ctx->db) ? 0 : strlen(ctx->sa.s) + 1;
-        off += sizeof(u32);
-        if (!write_db(ctx->sa.s + off, ctx->sa.len - off, ctx))
+     * counter (for next time), unless asked not to */
+    if (ctx.upd_entry && ctx.otp.type == TYPE_COUNTER
+            /* make sure the entry is also in COUNTER mode */
+            && db_otp(ssp)->type == TYPE_COUNTER) {
+        struct otp *e = (struct otp *) db_otp(ssp);
+        e->c = ++ctx.otp.c;
+        dbg("updating entry's counter to ", PMUINT(e->c));
+
+        if (!write_db(ssp->sa.s + ssp->cdboff, ssp->cdb.size, ssp))
             diefusys((errno == EINVAL) ? EX_DATA_ERR : EX_CANTCREAT, "save database");
     }
 
diff --git a/src/ssp/import.c b/src/ssp/import.c
index 0d59128..9c31d8f 100644
--- a/src/ssp/import.c
+++ b/src/ssp/import.c
@@ -2,41 +2,94 @@
  * Copyright (C) 2023 Olivier Brunel                          jjk@jjacky.com */
 /* SPDX-License-Identifier: GPL-2.0-only */
 #include <errno.h>
-#include <skalibs/stralloc.h>
 #include <limb/base32.h>
+#include <limb/bytestr.h>
 #include <limb/cdbmake.h>
 #include <limb/command.h>
 #include <limb/copa.h>
 #include <limb/djbunix.h>
+#include <limb/esc.h>
 #include <limb/exitcode.h>
+#include <limb/hasher.h> /* ALGO_* */
+#include <limb/loadopt.h>
 #include <limb/output.h>
+#include <limb/stralloc.h>
 #include <limb/u16.h>
 #include "ssp.h"
 
+struct import {
+    struct ssp *ssp;
+    size_t fileoff;
+};
+
+enum {
+    ARGID_FILE = OPTID_FIRST,
+};
 
 COMMAND(import, "Import entries from file", "<file>", NULL);
 
+static int
+parse_cmdline(int argc, const char *argv[], const char usage[], struct import *ctx)
+{
+    const struct option options[] = {
+        LOADOPT_ARGUMENTS,
+        ARGUMENT_REQ( "file",                           OPT_PATH,   ARGID_FILE),
+        LOADOPT_DONE
+    };
+    struct loadopt lo = LOADOPT_ZERO;
+
+    int c;
+    while ((c = loadopt(&ctx->ssp->sa, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
+
+        case ARGID_FILE:
+            ctx->fileoff = LO_OFF(&lo);
+            break;
+
+        case -1:
+            diecmdusage(EX_USAGE, usage, &command_import);
+        default:
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
+    };
+
+    return LO_CUR(&lo);
+}
+
+static ssize_t
+unesc(char *dst, size_t dlen, const char *sce, size_t slen)
+{
+    /* unescape it */
+    ssize_t l = esc_scan(dst, dlen, sce, slen);
+    if (l < 0) return -1;
+    /* make sure there was no encoded NUL byte */
+    if (byte_chr(dst, l, 0) < (size_t) l)
+        return (errno = EINVAL, -1);
+    dst[l++] = 0;
+    return l;
+}
+
 int
-import_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
+import_main(int argc, const char *argv[], const char *env[], const char usage[], void *ssp_)
 {
-    struct ssp *ctx = ctx_;
+    struct ssp *ssp = ssp_;
+    struct import ctx = { .ssp = ssp_, .fileoff = 0 };
+    stralloc sacopa = STRALLOC_ZERO;
+    stralloc saotp = STRALLOC_ZERO;
 
-    if (argc != 2)
-        diecmdusage(EX_USAGE, usage, &command_import);
+    (void) env;
+    parse_cmdline(argc, argv, usage, &ctx);
 
-    int fd = open_parsed_name(argv[1], open_readb);
+    int fd = open_parsed_name(ssp->sa.s + ctx.fileoff, openb_read);
     if (fd < 0)
-        diefusys(EX_NOINPUT, "open ", ESC, argv[1], ESC);
+        diefusys(EX_NOINPUT, "open ", ESC, ssp->sa.s + ctx.fileoff, ESC);
 
-    dbg("reading ", ESC, argv[1], ESC);
-    stralloc sa = STRALLOC_ZERO;
-    if (!slurp(&sa, fd))
-        diefusys(exitcode_from_errno(errno), "read ", ESC, argv[1], ESC);
+    dbg("reading ", ESC, ssp->sa.s + ctx.fileoff, ESC);
+    if (!slurp(&sacopa, fd))
+        diefusys(exitcode_from_errno(errno), "read ", ESC, ssp->sa.s + ctx.fileoff, ESC);
 
     cdbmaker_sa mkr = CDBMAKER_SA_ZERO;
     struct otp otp;
     const char *name = NULL;
-    size_t nlen;
+    size_t nlen = 0;
     const char *secret;
     ssize_t slen;
     size_t slen32;
@@ -45,20 +98,24 @@ import_main(int argc, const char *argv[], const char *env[], const char usage[],
     struct copa copa;
     int r, n = 0;
     dbg("init parsing");
-    copa_init(sa.s, sa.len, &copa);
+    comments = NULL; clen = 0; /* silence warnings */
+    copa_init(sacopa.s, sacopa.len, &copa);
     while ((r = copa_next(&copa)) >= 0) {
-        dbg("r=", PUTMSG_INT(r));
-        if (r) dbg("is_section=", PUTMSG_INT(copa_is_section(&copa)),
-                   " name=", LEN(copa_name(&copa), copa_nlen(&copa)));
+        dbg("r=", PMINT(r));
+        if (r) dbg("is_section=", PMINT(copa_is_section(&copa)),
+                   " name=", PMLEN(copa_name(&copa), copa_nlen(&copa)));
         if (!r || copa_is_section(&copa)) {
             if (name) {
                 if (!slen)
                     die(EX_DATA_ERR, "missing secret in section/entry ",
-                        ESC, LEN(name, nlen), ESC);
+                        ESC, PMLEN(name, nlen), ESC);
+
+                dbg("preparing entry ", ESC, PMLEN(name, nlen), ESC);
+                size_t otplen = sizeof(otp) + slen + clen + 1;
+                if (!stralloc_ready(&saotp, otplen))
+                    diefusys(EX_TEMPFAIL, "prepare entry ", ESC, PMLEN(name, nlen), ESC);
 
-                dbg("preparing entry ", ESC, LEN(name, nlen), ESC);
-                char buf[sizeof(otp) + slen + clen + 1];
-                struct otp *p = (struct otp *) buf;
+                struct otp *p = (struct otp *) saotp.s;
                 memcpy(p, &otp, sizeof(otp));
                 p->slen = slen;
                 base32_scan(p->data, secret, slen32, 0);
@@ -68,7 +125,7 @@ import_main(int argc, const char *argv[], const char *env[], const char usage[],
                     dbg("unescaping comments");
                     if (unesc(p->data + slen, clen, comments, clen) < 0)
                         diefusys(EX_DATA_ERR, "read comments in section/entry ",
-                                 ESC, LEN(name, nlen), ESC);
+                                 ESC, PMLEN(name, nlen), ESC);
                 } else {
                     memcpy(p->data + slen, comments, clen + 1);
                 }
@@ -79,21 +136,25 @@ import_main(int argc, const char *argv[], const char *env[], const char usage[],
                  */
                 if (mkr.sa.s) {
                     dbg("making previously generated cdb live");
-                    stralloc sa = ctx->sa;
-                    ctx->sa = mkr.sa;
-                    cdb_init_frommem(&ctx->cdb, ctx->sa.s, ctx->sa.len);
+                    stralloc sa = ssp->sa;
+                    ssp->sa = mkr.sa;
+                    /* insert back db name (& import file name) */
+                    if (!stralloc_insertb(&ssp->sa, 0, sa.s, ssp->cdboff))
+                        diefusys(EX_TEMPFAIL, "prepare for importation");
+                    cdb_init_frommem(&ssp->cdb, ssp->sa.s + ssp->cdboff,
+                                     ssp->sa.len);
                     mkr.sa = sa;
                 }
 
                 /* open db if we haven't already */
-                if (!ctx->cdb.map && open_db(ctx) < 0)
+                if (!ssp->cdb.map && open_db(ssp) < 0)
                     diefu(exitcode_from_errno(errno), "open database");
 
                 char key[nlen + 1];
                 if (unesc(key, nlen, name, nlen) < 0)
-                    dief(EX_DATA_ERR, "invalid section/entry name: ", LEN(name, nlen));
+                    dief(EX_DATA_ERR, "invalid section/entry name: ", PMLEN(name, nlen));
                 out("Importing ", ESC, key, ESC, "...");
-                if (!rebuild_cdb(&mkr, NULL , key, p, ctx))
+                if (rebuild_cdb(&mkr, NULL , key, p, ssp) < 0)
                     diefu(exitcode_from_errno(errno), "import data",
                           (errno == ENOMEM) ? ": Out of memory" : NULL);
                 ++n;
@@ -116,10 +177,10 @@ import_main(int argc, const char *argv[], const char *env[], const char usage[],
             /* all options must have a value */
             if (!copa_vlen(&copa))
                 dief(EX_DATA_ERR, "no value for option ",
-                     ESC, LEN(copa_name(&copa), copa_nlen(&copa)), ESC,
-                     " in section/entry ", ESC, LEN(name, nlen), ESC);
+                     ESC, PMLEN(copa_name(&copa), copa_nlen(&copa)), ESC,
+                     " in section/entry ", ESC, PMLEN(name, nlen), ESC);
 
-            dbg("value=", ESC, LEN(copa_value(&copa), copa_vlen(&copa)), ESC);
+            dbg("value=", ESC, PMLEN(copa_value(&copa), copa_vlen(&copa)), ESC);
 
             if (copa_nlen(&copa) == 4 && !strncmp(copa_name(&copa), "algo", 4)) {
                 r = validate_algo(NULL, copa_value(&copa), copa_vlen(&copa));
@@ -139,8 +200,8 @@ import_main(int argc, const char *argv[], const char *env[], const char usage[],
                     otp.type = TYPE_COUNTER;
                 else
                     dief(EX_DATA_ERR, "invalid value for \"time\" in section/entry ",
-                         ESC, LEN(name, nlen), ESC, ": ",
-                         LEN(copa_value(&copa), copa_vlen(&copa)));
+                         ESC, PMLEN(name, nlen), ESC, ": ",
+                         PMLEN(copa_value(&copa), copa_vlen(&copa)));
             } else if (otp.type == TYPE_TIME && copa_nlen(&copa) == 9
                     && !strncmp(copa_name(&copa), "precision", 9)) {
                 r = validate_precision(copa_value(&copa), copa_vlen(&copa));
@@ -150,8 +211,8 @@ import_main(int argc, const char *argv[], const char *env[], const char usage[],
                 slen = base32_scan(NULL, copa_value(&copa), copa_vlen(&copa), 0);
                 if (slen < 0)
                     dief(EX_DATA_ERR, "invalid secret in section/entry ",
-                         ESC, LEN(name, nlen), ESC, ": Not in base32: ",
-                         LEN(copa_value(&copa), copa_vlen(&copa)));
+                         ESC, PMLEN(name, nlen), ESC, ": Not in base32: ",
+                         PMLEN(copa_value(&copa), copa_vlen(&copa)));
                 secret = copa_value(&copa);
                 slen32 = copa_vlen(&copa);
             } else if (copa_nlen(&copa) == 8 && !strncmp(copa_name(&copa), "comments", 8)) {
@@ -159,26 +220,26 @@ import_main(int argc, const char *argv[], const char *env[], const char usage[],
                 clen = copa_vlen(&copa);
             } else {
                 dief(EX_DATA_ERR, "invalid data, unknown value ",
-                     ESC, LEN(copa_name(&copa), copa_nlen(&copa)), ESC,
-                     " in section/entry ", ESC, LEN(name, nlen), ESC);
+                     ESC, PMLEN(copa_name(&copa), copa_nlen(&copa)), ESC,
+                     " in section/entry ", ESC, PMLEN(name, nlen), ESC);
             }
         }
     }
     if (r < 0)
-        diefu(EX_DATA_ERR, "parse ", ESC, argv[1], ESC);
+        diefu(EX_DATA_ERR, "parse ", ESC, ssp->sa.s + ctx.fileoff, ESC);
 
-    if (mkr.sa.s && !write_db(mkr.sa.s, mkr.sa.len, ctx))
+    if (mkr.sa.s && !write_db(mkr.sa.s, mkr.sa.len, ssp))
         diefusys((errno == EINVAL) ? EX_DATA_ERR : EX_CANTCREAT, "save database");
     cdbmaker_sa_free(&mkr);
 
     if (!n)
         dief(EX_DATA_ERR, "nothing to import");
-    out("Successfully imported ", PUTMSG_UINT(n), " entries.");
+    out("Successfully imported ", PMUINT(n), " entries to ", ESC, db_file(ssp), ESC);
 
-    stralloc_free(&sa);
+    stralloc_free(&sacopa);
+    stralloc_free(&saotp);
     return 0;
 
 err:
-    stralloc_free(&sa);
-    dief(EX_DATA_ERR, "invalid section/entry ", ESC, LEN(name, nlen), ESC);
+    dief(EX_DATA_ERR, "invalid section/entry ", ESC, PMLEN(name, nlen), ESC);
 }
diff --git a/src/ssp/list.c b/src/ssp/list.c
index 6b5c51e..7588ab3 100644
--- a/src/ssp/list.c
+++ b/src/ssp/list.c
@@ -5,8 +5,8 @@
 #include <fnmatch.h>
 #include <limb/command.h>
 #include <limb/exitcode.h>
+#include <limb/loadopt.h>
 #include <limb/output.h>
-#include <limb/parseopt.h>
 #include "ssp.h"
 
 enum {
@@ -16,14 +16,18 @@ enum {
     OPT_FMT_INI     = 1 << 3,
 };
 
+enum {
+    ARGID_PATTERN = OPTID_FIRST,
+};
+
 struct list {
-    unsigned options;
+    struct ssp *ssp;
     const char *sep;
-    const char *ptrn;
+    size_t ptrnoff;
+    unsigned options;
 };
 
-COMMAND(list, "List all entries",
-        "[OPTION..] [<pattern>]",
+COMMAND(list, "List all entries", "[OPTION..] [<pattern>]",
 " -C, --no-comments                     Do not show comments with --details/--format\n"
 " -d, --details                         Show entries' details\n"
 " -f, --format                          Output in INI-like format\n"
@@ -35,17 +39,19 @@ static int
 parse_cmdline(int argc, const char *argv[], const char usage[], struct list *ctx)
 {
     const struct option options[] = {
-        OPTION_ARG_NONE('C', "no-comments",             0, OPTID_SHORTOPT),
-        OPTION_ARG_NONE('d', "details",                 0, OPTID_SHORTOPT),
-        OPTION_ARG_NONE('f', "format",                  0, OPTID_SHORTOPT),
-        OPTION_ARG_NONE('S', "secret",                  0, OPTID_SHORTOPT),
-        OPTION_ARG_REQ ('s', "sep",                     0, OPTID_SHORTOPT),
-        OPTION_DONE
+        OPTION_ARG_NONE('C', "no-comments",             0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE('d', "details",                 0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE('f', "format",                  0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE('S', "secret",                  0,          OPTID_SHORTOPT),
+        OPTION_ARG_REQ ('s', "sep",                     0,          OPTID_SHORTOPT),
+        LOADOPT_ARGUMENTS,
+        ARGUMENT_OPT( "pattern",                        OPT_PATH,   ARGID_PATTERN),
+        LOADOPT_DONE
     };
-    struct parseopt po = { 0 };
+    struct loadopt lo = LOADOPT_ZERO;
 
     int c;
-    while ((c = parseopt(argc, argv, options, 0, &po))) switch (c) {
+    while ((c = loadopt(&ctx->ssp->sa, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
         case 'C':
             ctx->options |= OPT_NO_COMMENTS;
             break;
@@ -59,63 +65,65 @@ parse_cmdline(int argc, const char *argv[], const char usage[], struct list *ctx
             ctx->options |= OPT_DETAILS | OPT_SECRET;
             break;
         case 's':
-            ctx->sep = PO_ARG(&po);
+            ctx->sep = LO_ARG(&lo);
+            break;
+
+        case ARGID_PATTERN:
+            ctx->ptrnoff = LO_OFF(&lo);
             break;
+
         case -1:
             diecmdusage(EX_USAGE, usage, &command_list);
         default:
-            die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt");
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
     };
 
-    return PO_CUR(&po);
+    return LO_CUR(&lo);
 }
 
 int
-list_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
+list_main(int argc, const char *argv[], const char *env[], const char usage[], void *ssp_)
 {
-    struct ssp *ctx = ctx_;
-    struct list list = { 0, "\n", NULL };
-
-    int i = parse_cmdline(argc, argv, usage, &list);
+    struct ssp *ssp = ssp_;
+    struct list ctx = { .ssp = ssp, .sep = "\n", .ptrnoff = (size_t) -1 };
 
-    if (i + 1 == argc)
-        list.ptrn = argv[i];
+    (void) env;
+    parse_cmdline(argc, argv, usage, &ctx);
 
-    if (i + 1 < argc) {
-        warn("too many arguments");
-        diecmdusage(EX_USAGE, usage, &command_list);
-    }
-
-    int r = open_db(ctx);
+    int r = open_db(ssp);
     if (r < 0)
         diefu(exitcode_from_errno(errno), "open database");
     if (!r)
         dief(EX_NOINPUT, "no database");
 
-    i = 0;
-    db_reset(ctx);
+    const char *ptrn = NULL;
+    if (ctx.ptrnoff != (size_t) -1)
+        ptrn = ssp->sa.s + ctx.ptrnoff;
+
+    int i = 0;
+    db_reset(ssp);
     dbg("iterating database...");
-    while ((r = db_next(ctx)) == 1) {
-        dbg("entry ", ESC, db_entry(ctx), ESC);
-        if (list.ptrn && fnmatch(list.ptrn, db_entry(ctx), 0))
+    while ((r = db_next(ssp)) == 1) {
+        dbg("entry ", ESC, db_entry(ssp), ESC);
+        if (ptrn && fnmatch(ptrn, db_entry(ssp), 0))
             continue;
-        if (list.options & OPT_FMT_INI) {
-            show_entry_ini(list.options & OPT_NO_COMMENTS, ctx);
-        } else if (list.options & OPT_DETAILS) {
-            show_entry(list.options & (OPT_NO_COMMENTS | OPT_SECRET), ctx);
+        if (ctx.options & OPT_FMT_INI) {
+            show_entry_ini(ctx.options & OPT_NO_COMMENTS, ssp);
+        } else if (ctx.options & OPT_DETAILS) {
+            show_entry(ctx.options & (OPT_NO_COMMENTS | OPT_SECRET), ssp);
         } else {
-            if (i) add(list.sep);
-            add(ESC, db_entry(ctx), ESC);
+            if (i) add(ctx.sep);
+            add(ESC, db_entry(ssp), ESC);
         }
         i = 1;
     }
     if (r < 0)
         diefu(EX_DATA_ERR, "read database: file corrupted");
     if (!i)
-        dief(EX_DATA_ERR, "no entry matching ", ESC, list.ptrn, ESC);
+        dief(EX_DATA_ERR, "no entry matching ", ESC, ptrn, ESC);
 
     /* add new line + flush buffers */
-    if (!(list.options & (OPT_DETAILS | OPT_FMT_INI))) out(NULL);
+    if (!(ctx.options & (OPT_DETAILS | OPT_FMT_INI))) out(NULL);
 
     return 0;
 }
diff --git a/src/ssp/remove.c b/src/ssp/remove.c
index d96daf0..3d430df 100644
--- a/src/ssp/remove.c
+++ b/src/ssp/remove.c
@@ -4,38 +4,72 @@
 #include <errno.h>
 #include <limb/command.h>
 #include <limb/exitcode.h>
+#include <limb/loadopt.h>
 #include <limb/output.h>
 #include "ssp.h"
 
+struct remove {
+    struct ssp *ssp;
+    size_t entoff;
+};
+
+enum {
+    ARGID_ENTRY = OPTID_FIRST,
+};
 
 COMMAND(remove, "Remove an entry", "<entry>", NULL);
 
-int
-remove_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
+static int
+parse_cmdline(int argc, const char *argv[], const char usage[], struct remove *ctx)
 {
-    struct ssp *ctx = ctx_;
+    const struct option options[] = {
+        LOADOPT_ARGUMENTS,
+        ARGUMENT_REQ( "entry",                          OPT_PATH,   ARGID_ENTRY),
+        LOADOPT_DONE
+    };
+    struct loadopt lo = LOADOPT_ZERO;
+
+    int c;
+    while ((c = loadopt(&ctx->ssp->sa, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
+
+        case ARGID_ENTRY:
+            ctx->entoff = LO_OFF(&lo);
+            break;
+
+        case -1:
+            diecmdusage(EX_USAGE, usage, &command_remove);
+        default:
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
+    };
 
-    if (argc != 2)
-        diecmdusage(EX_USAGE, usage, &command_remove);
+    return LO_CUR(&lo);
+}
 
-    const char *entry = get_entry_name(argv[1], ctx);
-    if (!entry)
-        diefusys(exitcode_from_errno(errno), "parse entry's name");
+int
+remove_main(int argc, const char *argv[], const char *env[], const char usage[], void *ssp_)
+{
+    struct ssp *ssp = ssp_;
+    struct remove ctx = { .ssp = ssp };
 
-    int r = open_db(ctx);
+    (void) env;
+    parse_cmdline(argc, argv, usage, &ctx);
+
+    int r = open_db(ssp);
     if (r < 0)
         diefu(exitcode_from_errno(errno), "open database");
     if (!r)
         dief(EX_NOINPUT, "no database");
 
-    r = db_find(entry, ctx);
+    const char *entry = ssp->sa.s + ctx.entoff;
+
+    r = db_find(entry, ssp);
     if (r < 0)
         diefu(EX_DATA_ERR, "find entry ", ESC, entry, ESC);
     if (!r)
         dief(EX_DATA_ERR, "no entry ", ESC, entry, ESC, " in database");
 
     out("Removing ", ESC, entry, ESC, "...");
-    rebuild_db(entry, NULL, NULL, ctx);
+    rebuild_db(entry, NULL, NULL, ssp);
 
     return 0;
 }
diff --git a/src/ssp/rename.c b/src/ssp/rename.c
index 7053bd1..19840d0 100644
--- a/src/ssp/rename.c
+++ b/src/ssp/rename.c
@@ -4,48 +4,90 @@
 #include <errno.h>
 #include <limb/command.h>
 #include <limb/exitcode.h>
+#include <limb/loadopt.h>
 #include <limb/output.h>
+#include <limb/stralloc.h>
 #include "ssp.h"
 
+struct rename {
+    stralloc sa;
+    size_t entoff;
+    size_t newoff;
+};
+
+enum {
+    ARGID_ENTRY = OPTID_FIRST,
+    ARGID_NEWNAME,
+};
 
 COMMAND(rename, "Rename an entry", "<entry> <newname>", NULL);
 
-int
-rename_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
+static int
+parse_cmdline(int argc, const char *argv[], const char usage[], struct rename *ctx)
 {
-    struct ssp *ctx = ctx_;
+    const struct option options[] = {
+        LOADOPT_ARGUMENTS,
+        ARGUMENT_REQ( "entry",                          OPT_PATH,   ARGID_ENTRY),
+        ARGUMENT_REQ( "newname",                        OPT_PATH,   ARGID_NEWNAME),
+        LOADOPT_DONE
+    };
+    struct loadopt lo = LOADOPT_ZERO;
+
+    int c;
+    while ((c = loadopt(&ctx->sa, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
+
+        case ARGID_ENTRY:
+            ctx->entoff = LO_OFF(&lo);
+            break;
+        case ARGID_NEWNAME:
+            ctx->newoff = LO_OFF(&lo);
+            break;
+
+        case -1:
+            diecmdusage(EX_USAGE, usage, &command_rename);
+        default:
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
+    };
 
-    if (argc != 3)
-        diecmdusage(EX_USAGE, usage, &command_rename);
+    return LO_CUR(&lo);
+}
 
-    const char *entry = get_entry_name(argv[1], ctx);
-    if (!entry)
-        diefusys(exitcode_from_errno(errno), "parse entry's name");
+int
+rename_main(int argc, const char *argv[], const char *env[], const char usage[], void *ssp_)
+{
+    struct ssp *ssp = ssp_;
+    struct rename ctx = { .sa = STRALLOC_ZERO };
 
-    const char *new = get_entry_name(argv[2], ctx);
-    if (!new)
-        diefusys(exitcode_from_errno(errno), "parse new name");
+    (void) env;
+    parse_cmdline(argc, argv, usage, &ctx);
 
-    if (!strcmp(entry, new))
+    const char *entry = ctx.sa.s + ctx.entoff;
+    const char *newname = ctx.sa.s + ctx.newoff;
+
+    if (!strcmp(entry, newname))
         diefu(EX_DATA_ERR, "rename entry ", ESC, entry, ESC, ": new name is the same");
 
-    int r = open_db(ctx);
+    int r = open_db(ssp);
     if (r < 0)
         diefu(exitcode_from_errno(errno), "open database");
     if (!r)
         dief(EX_NOINPUT, "no database");
 
-    r = db_find(entry, ctx);
+
+    r = db_find(entry, ssp);
     if (r < 0)
         diefu(EX_DATA_ERR, "find entry ", ESC, entry, ESC);
     if (!r)
         dief(EX_DATA_ERR, "no entry ", ESC, entry, ESC, " in database");
 
-    out("Renaming ", ESC, entry, ESC, " to ", ESC, new, ESC, "...");
-    rebuild_db(entry, new, db_otp(ctx), ctx);
+    out("Renaming ", ESC, entry, ESC, " to ", ESC, newname, ESC, "...");
+    rebuild_db(entry, newname, db_otp(ssp), ssp);
 
-    const char *av[] = { NULL, new };
-    run_command("show", sizeof(av) / sizeof(*av), av, env, ctx);
+    dbg("showing renamed entry");
+    close_db(ssp);
+    const char *av[] = { NULL, newname };
+    run_command("show", sizeof(av) / sizeof(*av), av, env, ssp);
 
+    stralloc_free(&ctx.sa);
     return 0;
 }
diff --git a/src/ssp/show.c b/src/ssp/show.c
index 79aa504..ab088e4 100644
--- a/src/ssp/show.c
+++ b/src/ssp/show.c
@@ -6,8 +6,9 @@
 #include <limb/bytestr.h>
 #include <limb/command.h>
 #include <limb/exitcode.h>
+#include <limb/hasher.h> /* ALGO_* */
+#include <limb/loadopt.h>
 #include <limb/output.h>
-#include <limb/parseopt.h>
 #include "ssp.h"
 
 enum {
@@ -17,11 +18,16 @@ enum {
 };
 
 struct show {
+    struct ssp *ssp;
+    size_t entoff;
     unsigned options;
 };
 
-COMMAND(show, "Show an entry",
-        "[OPTION..] <entry>",
+enum {
+    ARGID_ENTRY = OPTID_FIRST,
+};
+
+COMMAND(show, "Show an entry", "[OPTION..] <entry>",
 " -C, --no-comments                     Do not show entry's comments\n"
 " -f, --format                          Output in INI-like format\n"
 " -S, --secret                          Show entry's secret\n"
@@ -31,15 +37,17 @@ static int
 parse_cmdline(int argc, const char *argv[], const char usage[], struct show *ctx)
 {
     const struct option options[] = {
-        OPTION_ARG_NONE('C', "no-comments",             0, OPTID_SHORTOPT),
-        OPTION_ARG_NONE('f', "format",                  0, OPTID_SHORTOPT),
-        OPTION_ARG_NONE('S', "secret",                  0, OPTID_SHORTOPT),
-        OPTION_DONE
+        OPTION_ARG_NONE('C', "no-comments",             0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE('f', "format",                  0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE('S', "secret",                  0,          OPTID_SHORTOPT),
+        LOADOPT_ARGUMENTS,
+        ARGUMENT_REQ( "entry",                          OPT_PATH,   ARGID_ENTRY),
+        LOADOPT_DONE
     };
-    struct parseopt po = { 0 };
+    struct loadopt lo = LOADOPT_ZERO;
 
     int c;
-    while ((c = parseopt(argc, argv, options, 0, &po))) switch (c) {
+    while ((c = loadopt(&ctx->ssp->sa, argc, argv, options, 0, NULL, 0, &lo))) switch (c) {
         case 'C':
             ctx->options |= OPT_NO_COMMENTS;
             break;
@@ -49,13 +57,18 @@ parse_cmdline(int argc, const char *argv[], const char usage[], struct show *ctx
         case 'S':
             ctx->options |= OPT_SECRET;
             break;
+
+        case ARGID_ENTRY:
+            ctx->entoff = LO_OFF(&lo);
+            break;
+
         case -1:
             diecmdusage(EX_USAGE, usage, &command_show);
         default:
-            die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt");
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
     };
 
-    return PO_CUR(&po);
+    return LO_CUR(&lo);
 }
 
 void
@@ -67,10 +80,10 @@ show_entry(unsigned options, struct ssp *ctx)
     out("   Algo: ", algos[otp->algo]);
     add("   ", types[otp->type], "-based; ");
     if (otp->type == TYPE_COUNTER)
-        out("Counter: ", PUTMSG_UINT(otp->c));
+        out("Counter: ", PMUINT(otp->c));
     else /* TYPE_TIME */
-        out("Precision: ", PUTMSG_UINT(otp->t.precision), "s");
-    out("   Digits: ", PUTMSG_UINT(otp->digits));
+        out("Precision: ", PMUINT(otp->t.precision), "s");
+    out("   Digits: ", PMUINT(otp->digits));
     if (options & OPT_SECRET) {
         size_t l = base32_fmt(NULL, otp->data, otp->slen, 0);
         char buf[l+1];
@@ -92,12 +105,12 @@ show_entry_ini(unsigned options, struct ssp *ctx)
 
     out("[", PUTMSG_TOGGLE_ESC, db_entry(ctx), PUTMSG_TOGGLE_ESC, "]");
     out("algo=", algos[otp->algo]);
-    out("digits=", PUTMSG_UINT(otp->digits));
+    out("digits=", PMUINT(otp->digits));
     if (otp->type == TYPE_TIME) {
         out("time=1");
-        out("precision=", PUTMSG_UINT(otp->t.precision));
+        out("precision=", PMUINT(otp->t.precision));
     } else {
-        out("counter=", PUTMSG_UINT(otp->c));
+        out("counter=", PMUINT(otp->c));
     }
     size_t l = base32_fmt(NULL, otp->data, otp->slen, 1);
     char buf[l + 1];
@@ -109,43 +122,31 @@ show_entry_ini(unsigned options, struct ssp *ctx)
 }
 
 int
-show_main(int argc, const char *argv[], const char *env[], const char usage[], void *ctx_)
+show_main(int argc, const char *argv[], const char *env[], const char usage[], void *ssp_)
 {
-    struct ssp *ctx = ctx_;
-    struct show show = { 0 };
+    struct ssp *ssp = ssp_;
+    struct show ctx = { .ssp = ssp };
 
-    int i = parse_cmdline(argc, argv, usage, &show);
+    (void) env;
+    parse_cmdline(argc, argv, usage, &ctx);
 
-    if (i == argc) {
-        warn("argument \"entry\" missing");
-        diecmdusage(EX_USAGE, usage, &command_show);
-    }
-
-    if (i + 1 < argc) {
-        warn("too many arguments");
-        diecmdusage(EX_USAGE, usage, &command_show);
-    }
-
-    int r = open_db(ctx);
+    int r = open_db(ssp);
     if (r < 0)
         diefu(exitcode_from_errno(errno), "open database");
     if (!r)
         dief(EX_NOINPUT, "no database");
 
-    const char *entry = get_entry_name(argv[i], ctx);
-    if (!entry)
-        diefusys(exitcode_from_errno(errno), "parse entry's name");
-
-    r = db_find(entry, ctx);
+    const char *entry = ssp->sa.s + ctx.entoff;
+    r = db_find(entry, ssp);
     if (r < 0)
         diefu(EX_DATA_ERR, "find entry ", ESC, entry, ESC);
     if (!r)
         dief(EX_DATA_ERR, "no entry ", ESC, entry, ESC, " in database");
 
-    if (show.options & OPT_FMT_INI)
-        show_entry_ini(show.options & OPT_NO_COMMENTS, ctx);
+    if (ctx.options & OPT_FMT_INI)
+        show_entry_ini(ctx.options & OPT_NO_COMMENTS, ssp);
     else
-        show_entry(show.options, ctx);
+        show_entry(ctx.options, ssp);
 
     return 0;
 }
diff --git a/src/ssp/ssp.c b/src/ssp/ssp.c
index 05ed0b6..c19575c 100644
--- a/src/ssp/ssp.c
+++ b/src/ssp/ssp.c
@@ -9,16 +9,13 @@
 #include <limb/command.h>
 #include <limb/esc.h>
 #include <limb/exitcode.h>
-#include <limb/parseopt.h>
+#include <limb/loadopt.h>
 #include <limb/output.h>
 #include "ssp.h"
 #include "config.h"
 
 const char *PROG = "ssp";
 
-const char *algos[] = { "sha1", "sha256", "sha512", "sha3-224", "sha3-256",
-    "sha3-512", "blake3", 0 };
-
 enum {
     OPTID_VERSION = OPTID_FIRST,
     OPTID_DEBUG,
@@ -41,74 +38,39 @@ exitcode_from_errno(int e)
 int
 run_command(const char *name, int argc, const char *argv[], const char *env[], void *ctx)
 {
-    dbg("running command ", name, "...");
+    dbg("running command ", name);
     for (struct command **c = commands; *c; ++c)
         if (!strcmp((*c)->name, name))
             return (*c)->main(argc, argv, env, "", ctx);
     return -1;
 }
 
-ssize_t
-unesc(char *dst, size_t dlen, const char *sce, size_t slen)
-{
-    /* unescape it */
-    ssize_t l = esc_scan(dst, dlen, sce, slen);
-    if (l < 0) return -1;
-    /* make sure there was no encoded NUL byte */
-    if (byte_chr(dst, l, 0) < (size_t) l)
-        return (errno = EINVAL, -1);
-    dst[l++] = 0;
-    return l;
-}
-
-const char *
-get_entry_name(const char *s, struct ssp *ctx)
-{
-    ssize_t l = strlen(s);
-
-    if (*s != '"' || s[l - 1] != '"')
-        /* not quoted, return as-is */
-        return s;
-
-    size_t salen = ctx->sa.len;
-    l -= 2;
-    if (!stralloc_readyplus(&ctx->sa, l + 1))
-        return NULL;
-
-    /* unescape it */
-    l = unesc(ctx->sa.s + ctx->sa.len, l, s + 1, l);
-    if (l < 0) return NULL;
-    ctx->sa.len += l;
-    return ctx->sa.s + salen;
-}
-
 static struct command *
 parse_cmdline(int *argc, const char **argv[], struct ssp *ctx)
 {
-    const char usage[] = "[-h] [-D database] command [...]";
+    const char usage[] = "[-h] [OPTION..] command [...]";
     const struct option options[] = {
-        OPTION_ARG_OPT ( 0 , "debug",                   0, OPTID_DEBUG),
-        OPTION_ARG_REQ ('D', "database",                0, OPTID_SHORTOPT),
-        OPTION_ARG_NONE('h', "help",                    0, OPTID_SHORTOPT),
-        OPTION_ARG_NONE('q', "quiet",                   0, OPTID_SHORTOPT),
-        OPTION_ARG_NONE( 0 , "version",                 0, OPTID_VERSION),
-        OPTION_DONE
+        OPTION_ARG_OPT ( 0 , "debug",                   0,          OPTID_DEBUG),
+        OPTION_ARG_REQ ('D', "database",                OPT_PATH,   OPTID_SHORTOPT),
+        OPTION_ARG_NONE('h', "help",                    0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE('q', "quiet",                   0,          OPTID_SHORTOPT),
+        OPTION_ARG_NONE( 0 , "version",                 0,          OPTID_VERSION),
+        LOADOPT_STOP
     };
-    struct parseopt po = { 0 };
+    struct loadopt lo = LOADOPT_ZERO;
 
     int c;
-    while ((c = parseopt(*argc, *argv, options, 0, &po))) switch (c) {
+    while ((c = loadopt(&ctx->sa, *argc, *argv, options, 0, NULL, 0, &lo))) switch (c) {
         case 'D':
-            ctx->db = PO_ARG(&po);
             break;
         case 'h':
             ctx->options |= OPT_HELP;
             break;
         case 'q':
-            autoopt_quiet(&options[PO_IDX(&po)], PO_ARG(&po));
+            autoopt_quiet(&options[LO_IDX(&lo)], LO_ARG(&lo));
             break;
         case OPTID_DEBUG:
-            if (!autoopt_debug(&options[PO_IDX(&po)], PO_ARG(&po)))
+            if (!autoopt_debug(&options[LO_IDX(&lo)], LO_ARG(&lo)))
                 dieusage(EX_USAGE, usage);
             break;
         case OPTID_VERSION:
@@ -116,11 +78,11 @@ parse_cmdline(int *argc, const char **argv[], struct ssp *ctx)
         case -1:
             dieusage(EX_USAGE, usage);
         default:
-            die(EX_SOFTWARE, "unexpected return value ", PUTMSG_INT(c), " from parseopt");
+            die(EX_SOFTWARE, "unexpected return value ", PMINT(c), " from loadopt");
     };
 
     /* no command specified */
-    if (PO_CUR(&po) == *argc)
+    if (LO_CUR(&lo) == *argc)
         dienocommand(EX_USAGE, usage, (ctx->options & OPT_HELP) ?
 " -D, --database FILE                   Use FILE as database\n"
 "\n"
@@ -131,8 +93,8 @@ parse_cmdline(int *argc, const char **argv[], struct ssp *ctx)
 "     --version                         Show version information and exit\n"
 : NULL);
 
-    *argc -= PO_CUR(&po);
-    *argv += PO_CUR(&po);
+    *argc -= LO_CUR(&lo);
+    *argv += LO_CUR(&lo);
 
     return getcommandordie(EX_USAGE, usage, **argv);
 }
@@ -140,28 +102,29 @@ parse_cmdline(int *argc, const char **argv[], struct ssp *ctx)
 int
 main(int argc, const char *argv[], const char *env[])
 {
-    const char usage[] = "[-h] [-D database] ";
+    const char usage[] = "[-h] [-D database] [-q] ";
     struct ssp ctx = { 0 };
 
     setlocale(LC_ALL, "");
 
     struct command *command = parse_cmdline(&argc, &argv, &ctx);
 
-    if (!ctx.db) {
+    if (!ctx.sa.len) {
         const char *home = env_get2(env, "HOME");
+        dbg("no database set, using default. $HOME=", ESC, home, ESC);
         if (!home || !*home) {
             warn("$HOME not defined, using current directory instead");
             home = ".";
         }
-        if (!stralloc_cats(&ctx.sa, home)
-                || !stralloc_cats(&ctx.sa, "/ssp.db") || !stralloc_0(&ctx.sa))
+        if (!stralloc_cats(&ctx.sa, home) || !stralloc_cats0(&ctx.sa, "/ssp.db"))
             diefusys(EX_TEMPFAIL, "set database path");
     }
+    dbg("database: ", ESC, db_file(&ctx), ESC);
 
     if (ctx.options & OPT_HELP)
         diecmdhelp(0, usage, command);
 
-    dbg("database: ", ESC, db_file(&ctx), ESC, "; running command ", command->name, "...");
+    dbg("running command ", command->name);
     int r = command->main(argc, argv, env, usage, &ctx);
     stralloc_free(&ctx.sa);
     return r;