The old parser based on shlex::split had a few serious disadvantages:
* "x!!"=y and "x!"!=y produced the same result because shlex::split
would split both of them as ["x!!=y"]. Though the first one is a
public pair "x!!"="y", while the second is private pair "x!"="y".
The same applies for '?'.
* Similar to the previous one, placing '=' inside a key is impossible
because the parser can't distinguish between "a=b"=c and "a"="b=c"
* Keys were for some reason limited to the regex ^[-_A-Za-z]+$, which
disallowed any non-ASCII characters as well as special characters
such as '[' or ']'. This limitation prevented a lot of website
credentials (e.g. GitLab) from being stored in a form useful to
himitsu-firefox.
* Newlines could not be supported because the only way to get a newline
into shlex is provide a literal newline character, which was not
possible because himitsu's protocol is line-oriented.
The new parser will no longer operate on slices of strings returned from
shlex::split (this is still supported via parse_items), but rather on
full strings. Word splitting will be done during parsing automatically.
The parser operates a smaller version of shlex::split that, will unwrap
any quotes and stop at unquoted '=', '!', '?' or unquoted whitespace
when parsing the key, then will parse the value until unquoted
whitespace. The backslash was also expanded to provide a literal
newline on "\n".
himitsu::query::quote is also provided for safe escaping of newlines.
The old parser was also used differently in different places. With the
new parser everything had to be unified:
* himitsud will parse input from clients as a query
* himitsu::client::next will automatically parse the query instead of
returning a string
* hiq will use himitsu::client instead of its custom client
---
cmd/himitsud/cmd.ha | 70 ++++++-----
cmd/hiq/main.ha | 104 ++++++----------
himitsu/client/client.ha | 7 +-
himitsu/query/parse.ha | 249 ++++++++++++++++++++++++++++++++-------
himitsu/query/unparse.ha | 65 +++++++++-
secstore/serialize.ha | 10 +-
6 files changed, 345 insertions(+), 160 deletions(-)
diff --git a/cmd/himitsud/cmd.ha b/cmd/himitsud/cmd.ha
index 6bb9ec0..868052e 100644
--- a/cmd/himitsud/cmd.ha
+++ b/cmd/himitsud/cmd.ha
@@ -35,20 +35,30 @@ fn strerror(err: cmderror) const str = {
fn exec(serv: *server, client: *client, cmd: str) (void | servererror) = {
// TODO: Better logging of client activity
- const args = match (shlex::split(cmd)) {
- case shlex::syntaxerr =>
+ const q = match (query::parse_string(cmd)) {
+ case let q: query::query =>
+ yield q;
+ case =>
writefmt(client, "error Invalid command syntax");
return;
- case let items: []str =>
- yield items;
};
- defer strings::freeall(args);
- if (len(args) == 0) {
+ defer query::finish(&q);
+ if (len(q.items) == 0) {
writefmt(client, "error Invalid command syntax");
return;
};
- const cmd = switch (args[0]) {
+ // FIXME: this is not very good
+ if (len(q.items[0].value) != 0 || q.items[0].private
+ || q.items[0].optional) {
+ writefmt(client, "error Unknown command");
+ return;
+ };
+ const cmd = q.items[0].key;
+ defer free(cmd);
+ delete(q.items[0]);
+
+ const cmd = switch (cmd) {
case "add" =>
yield &exec_add;
case "del" =>
@@ -62,7 +72,7 @@ fn exec(serv: *server, client: *client, cmd: str) (void | servererror) = {
return;
};
- match (cmd(serv, client, args)) {
+ match (cmd(serv, client, &q)) {
case let err: cmderror =>
// XXX: Probably a harec bug
match (err) {
@@ -79,7 +89,7 @@ fn exec(serv: *server, client: *client, cmd: str) (void | servererror) = {
};
};
-fn exec_add(serv: *server, client: *client, args: []str) (void | cmderror) = {
+fn exec_add(serv: *server, client: *client, q: *query::query) (void | cmderror) = {
if (serv.store.state != secstore::state::UNLOCKED) {
const prompter = prompt::newprompter(serv.conf.prompter[0],
serv.conf.prompter[1..])?;
@@ -91,10 +101,8 @@ fn exec_add(serv: *server, client: *client, args: []str) (void | cmderror) = {
prompt::close(&prompter)?;
};
- const q = query::parse_items(args[1..])?;
- defer query::finish(&q);
// TODO: Prompt user to fill in incomplete keys
- let entry = secstore::add(serv.store, &q)!;
+ let entry = secstore::add(serv.store, q)!;
let buf = bufio::dynamic(io::mode::WRITE);
fmt::fprint(&buf, "key ")?;
secstore::write(serv.store, &buf, entry, false)?;
@@ -102,10 +110,7 @@ fn exec_add(serv: *server, client: *client, args: []str) (void | cmderror) = {
write(client, bufio::buffer(&buf));
};
-fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = {
- const q = query::parse_items(args[1..])?;
- defer query::finish(&q);
-
+fn exec_del(serv: *server, client: *client, q: *query::query) (void | cmderror) = {
let buf = bufio::dynamic(io::mode::WRITE);
let prompter: (prompt::prompter | void) = void;
@@ -119,7 +124,7 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = {
};
};
- const iter = secstore::query(serv.store, &q);
+ const iter = secstore::query(serv.store, q);
let matches: []*secstore::entry = [];
for (true) {
const item = match (secstore::next(serv.store, &iter)) {
@@ -151,7 +156,7 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = {
return;
};
- secstore::del(serv.store, &q)!;
+ secstore::del(serv.store, q)!;
} else {
match (prompter) {
case let p: prompt::prompter =>
@@ -164,21 +169,15 @@ fn exec_del(serv: *server, client: *client, args: []str) (void | cmderror) = {
writebuf(client, bufio::buffer(&buf));
};
-fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
- const cmd = getopt::parse(args,
- "query the key store",
- ('d', "decrypt private keys"),
- "query..."
- );
- defer getopt::finish(&cmd);
-
+fn exec_query(serv: *server, client: *client, q: *query::query) (void | cmderror) = {
let decrypt = false;
- for (let i = 0z; i < len(cmd.opts); i += 1) {
- switch (cmd.opts[i].0) {
- case 'd' =>
- decrypt = true;
- case => abort();
- };
+ // FIXME: this is not very good
+ if (len(q.items) > 0 && q.items[0].key == "-d"
+ && len(q.items[0].value) == 0 && !q.items[0].private
+ && !q.items[0].optional) {
+ free(q.items[0].key);
+ delete(q.items[0]);
+ decrypt = true;
};
let prompter: (prompt::prompter | void) = void;
@@ -196,10 +195,7 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
prompter = new;
};
- const q = query::parse_items(cmd.args)?;
- defer query::finish(&q);
-
- const iter = secstore::query(serv.store, &q);
+ const iter = secstore::query(serv.store, q);
let matches: []*secstore::entry = [];
for (true) {
const item = match (secstore::next(serv.store, &iter)) {
@@ -249,7 +245,7 @@ fn exec_query(serv: *server, client: *client, args: []str) (void | cmderror) = {
writebuf(client, bufio::buffer(&buf));
};
-fn exec_quit(serv: *server, client: *client, args: []str) (void | cmderror) = {
+fn exec_quit(serv: *server, client: *client, q: *query::query) (void | cmderror) = {
if (!serv.daemonized) {
writefmt(client, "error Server is not damonized, use a service manager");
return;
diff --git a/cmd/hiq/main.ha b/cmd/hiq/main.ha
index 57e8f6d..cff4f8a 100644
--- a/cmd/hiq/main.ha
+++ b/cmd/hiq/main.ha
@@ -22,18 +22,15 @@ type flag = enum uint {
fn write_item(
out: io::handle,
- items: str,
+ q: *query::query,
field: str
) (void | io::error) = {
if (field == "") {
- fmt::fprintln(out, items)!;
+ query::unparse(out, q)?;
return;
};
- let items = shlex::split(items)!;
- defer strings::freeall(items);
- let query = query::parse_items(items)!;
- for (let i = 0z; i < len(query.items); i += 1) {
- let item = query.items[i];
+ for (let i = 0z; i < len(q.items); i += 1) {
+ let item = q.items[i];
if (item.key != field) {
continue;
};
@@ -74,16 +71,11 @@ export fn main() void = {
};
};
- let buf = path::init()!;
- // TODO: Bubble up dirs::runtime errors
- const sockpath = path::set(&buf, dirs::runtime()!, "himitsu")!;
- let conn = match (unix::connect(sockpath)) {
- case let s: net::socket =>
- yield s;
- case errors::noentry =>
- fmt::fatal("error: himitsud connection failed (is it running?)");
- case let e: net::error =>
- fmt::fatal("error:", net::strerror(e));
+ const conn = match (client::connect()) {
+ case let socket: net::socket =>
+ yield socket;
+ case let err: client::error =>
+ fmt::fatal("Error:", client::strerror(err));
};
defer io::close(conn)!;
@@ -98,25 +90,28 @@ export fn main() void = {
// NB. Can't defer free(line), causes a
// use-after-free in fmt::fatal
const line = strings::fromutf8(line)!;
- const query = match (shlex::split(line)) {
- case let q: []str =>
+ const q = match (query::parse_string(line)) {
+ case let q: query::query =>
yield q;
case =>
fmt::fatal("Invalid query:", line);
};
- defer strings::freeall(query);
+ defer query::finish(&q);
free(line);
- send(conn, client::operation::ADD, flags, field, query);
+ send(conn, client::operation::ADD, flags, field,
+ &q);
};
};
} else {
- let query = alloc(cmd.args...);
- defer free(query);
- if (flags & flag::DECRYPT != 0) {
- insert(query[0], "-d");
+ const q = match (query::parse_items(cmd.args)) {
+ case let q: query::query =>
+ yield q;
+ case =>
+ fmt::fatal("Invalid query"); // TODO
};
- send(conn, op, flags, field, query);
+ defer query::finish(&q);
+ send(conn, op, flags, field, &q);
};
};
@@ -125,50 +120,25 @@ fn send(
op: client::operation,
flags: flag,
field: str,
- query: []str,
+ q: *query::query,
) void = {
- fmt::fprint(conn, switch (op) {
- case client::operation::QUERY =>
- yield "query";
- case client::operation::ADD =>
- yield "add";
- case client::operation::DEL =>
- yield "del";
- case client::operation::QUIT =>
- yield "quit";
- })!;
- for (let i = 0z; i < len(query); i += 1) {
- fmt::fprint(conn, " ")!;
- shlex::quote(conn, query[i])!;
+ // TODO: error handling
+ const client_flags: client::flags = if (flags & flag::DECRYPT != 0) {
+ yield client::flags::DECRYPT;
+ } else {
+ yield 0;
};
- fmt::fprintln(conn)!;
-
- let buf = strio::dynamic();
- defer io::close(&buf)!;
+ const iter = client::query(conn, op, q, client_flags)!;
- for (let n = 0; true; n += 1) match (bufio::scanline(conn)!) {
- case io::EOF => break;
- case let line: []u8 =>
- // NB. Can't defer free(line), causes a use-after-free in
- // fmt::fatal
- let resp = strings::fromutf8(line)!;
- let resp = strings::cut(resp, " ");
- switch (resp.0) {
- case "key" =>
- if (flags & flag::ONE != 0 && n > 0) {
- fmt::fatal("error: Ambiguous match");
- };
- write_item(&buf, resp.1, field)!;
- case "error" =>
- fmt::fatal("error:", resp.1);
- case "end" =>
- free(line);
- break;
- case =>
- break;
+ for (let n = 0; true; n += 1) match (client::next(&iter)) {
+ case let resp: query::query =>
+ if (flags & flag::ONE != 0 && n > 0) {
+ fmt::fatal("error: Ambiguous match");
};
- free(line);
+ write_item(os::stdout, &resp, field)!;
+ case let err: client::error =>
+ fmt::fatal("Error:", client::strerror(err));
+ case void =>
+ break;
};
-
- fmt::print(strio::string(&buf))!;
};
diff --git a/himitsu/client/client.ha b/himitsu/client/client.ha
index 12fbe4a..12e5f9e 100644
--- a/himitsu/client/client.ha
+++ b/himitsu/client/client.ha
@@ -81,7 +81,7 @@ export fn query(
fmt::fprint(&buf, "quit ")!;
};
- if (flags & flags::DECRYPT != 0) {
+ if (op == operation::QUERY && flags & flags::DECRYPT != 0) {
fmt::fprint(&buf, "-d ")!;
};
@@ -97,7 +97,7 @@ export fn query(
// Returns the next key from a key iterator, or void if no further keys are
// provided.
-export fn next(iter: *keyiter) (const str | void | error) = {
+export fn next(iter: *keyiter) (query::query | void | error) = {
strio::reset(&iter.buf);
match (bufio::scanline(iter.conn)?) {
case let buf: []u8 =>
@@ -117,7 +117,8 @@ export fn next(iter: *keyiter) (const str | void | error) = {
return strings::sub(string, 6, strings::end): hierror: error;
};
if (strings::hasprefix(string, "key ")) {
- return strings::sub(string, 4, strings::end);
+ return query::parse_string(
+ strings::sub(string, 4, strings::end))!;
};
abort("himitsu returned unexpected response");
};
diff --git a/himitsu/query/parse.ha b/himitsu/query/parse.ha
index 0bfec8c..ea11c2e 100644
--- a/himitsu/query/parse.ha
+++ b/himitsu/query/parse.ha
@@ -1,20 +1,9 @@
use bufio;
use encoding::utf8;
+use fmt;
use io;
-use regex;
-use shlex;
use strings;
-use fmt;
-
-let keyre: regex::regex = regex::regex { ... };
-
-@init fn init() void = {
- keyre = regex::compile(`^[-_A-Za-z]+$`)!;
-};
-
-@fini fn fini() void = {
- regex::finish(&keyre);
-};
+use strio;
// A parsed Himitsu query.
export type query = struct {
@@ -36,51 +25,198 @@ export type invalid = !void;
// return value to [[finish]] when they are done with it.
export fn parse(in: io::handle) (query | invalid | io::error) = {
const data = io::drain(in)?;
- const data = match (strings::fromutf8(data)) {
+ defer free(data);
+ match (strings::fromutf8(data)) {
case let data: str =>
- yield data;
+ return parse_string(data);
case utf8::invalid =>
return invalid;
};
+};
- const items = match (shlex::split(data)) {
- case let items: []str =>
- yield items;
- case shlex::syntaxerr =>
- return invalid;
+// Parses a query, returning its key/value pairs. The caller must pass the
+// return value to [[finish]] when they are done with it.
+export fn parse_string(in: str) (query | invalid) = {
+ let query = query { ... };
+ let iter = strings::iter(in);
+ for (true) {
+ match (parse_item(&iter, &query, false)?) {
+ case void =>
+ yield;
+ case io::EOF =>
+ break;
+ };
};
- defer strings::freeall(items);
- return parse_items(items);
+ return query;
};
// Parses a list of key/value pairs which has already been split with shlex (or
// a shell, for example when parsing a query from argv).
export fn parse_items(items: []str) (query | invalid) = {
- // XXX: Should do something about the case where the user specifies both
- // ? and !
let query = query { ... };
for (let i = 0z; i < len(items); i += 1) {
- const (key, value) = strings::cut(items[i], "=");
- let optional = false, private = false;
- if (strings::hassuffix(key, "!")) {
- private = true;
+ const item = strings::trim(items[i]);
+ if (len(item) == 0) {
+ continue;
};
- if (strings::hassuffix(key, "?")) {
+ let iter = strings::iter(item);
+ parse_item(&iter, &query, true)? as void;
+ };
+ return query;
+};
+
+fn parse_item(
+ iter: *strings::iterator,
+ q: *query,
+ split: bool,
+) (void | io::EOF | invalid) = {
+ // XXX: Should do something about the case where the user specifies both
+ // ? and !
+ const key = match (parse_value(iter, true, split)?) {
+ case let s: str =>
+ yield s;
+ case io::EOF =>
+ return io::EOF;
+ };
+ let optional = false, private = false;
+ match (strings::next(iter)) {
+ case let r: rune =>
+ switch (r) {
+ case '!' =>
+ private = true;
+ case '?' =>
optional = true;
+ case =>
+ strings::prev(iter);
+ };
+ case void =>
+ yield;
+ };
+ const value = match (strings::next(iter)) {
+ case let r: rune =>
+ yield if (r == '=') {
+ yield parse_value(iter, false, split)? as str;
+ } else {
+ strings::prev(iter);
+ yield "";
+ };
+ case void =>
+ yield "";
+ };
+
+ append(q.items, pair {
+ key = key,
+ value = value,
+ private = private,
+ optional = optional,
+ });
+};
+
+fn parse_value(
+ iter: *strings::iterator,
+ key: bool,
+ split: bool,
+) (str | io::EOF | invalid) = {
+ if (!split) for (true) match (strings::next(iter)) {
+ case let r: rune =>
+ if (r != ' ' && r != '\t') {
+ strings::prev(iter);
+ break;
+ };
+ case =>
+ break;
+ };
+
+ const buf = strio::dynamic();
+ defer io::close(&buf)!;
+ for (true) {
+ const r = match (strings::next(iter)) {
+ case let r: rune =>
+ yield r;
+ case void =>
+ if (key && len(strio::string(&buf)) == 0) {
+ return io::EOF;
+ };
+ break;
};
- key = strings::trim(key, '?', '!');
- if (!regex::test(&keyre, key)) {
+ switch (r) {
+ case '!', '?', '=' =>
+ if (key) {
+ strings::prev(iter);
+ break;
+ } else {
+ return invalid;
+ };
+ case ' ', '\t' =>
+ if (split) {
+ strio::appendrune(&buf, r)!;
+ } else {
+ break;
+ };
+ case '\\' =>
+ scan_backslash(&buf, iter)?;
+ case '"' =>
+ scan_double(&buf, iter)?;
+ case '\'' =>
+ scan_single(&buf, iter)?;
+ case =>
+ strio::appendrune(&buf, r)!;
+ };
+ };
+ if (key && len(strio::string(&buf)) == 0) {
+ return io::EOF;
+ };
+ return strings::dup(strio::string(&buf));
+};
+
+fn scan_double(out: io::handle, iter: *strings::iterator) (void | invalid) = {
+ for (true) {
+ const r = match (strings::next(iter)) {
+ case let r: rune =>
+ yield r;
+ case void =>
return invalid;
};
- append(query.items, pair {
- key = strings::dup(key),
- value = strings::dup(value),
- private = private,
- optional = optional,
- });
+ switch (r) {
+ case '"' =>
+ break;
+ case '\\' =>
+ scan_backslash(out, iter)?;
+ case =>
+ strio::appendrune(out, r)!;
+ };
+ };
+};
+
+fn scan_backslash(out: io::handle, iter: *strings::iterator) (void | invalid) = {
+ const r = match (strings::next(iter)) {
+ case let r: rune =>
+ yield r;
+ case void =>
+ return invalid;
+ };
+ if (r == 'n') {
+ strio::appendrune(out, '\n')!;
+ } else {
+ strio::appendrune(out, r)!;
+ };
+};
+
+fn scan_single(out: io::handle, iter: *strings::iterator) (void | invalid) = {
+ for (true) {
+ const r = match (strings::next(iter)) {
+ case let r: rune =>
+ yield r;
+ case void =>
+ return invalid;
+ };
+
+ if (r == '\'') {
+ break;
+ };
+ strio::appendrune(out, r)!;
};
- return query;
};
// Frees resources associated with this query.
@@ -102,21 +238,33 @@ export fn finish(q: *query) void = {
("foo", "", false, true),
("bar", "baz", true, false),
]),
+ (`query -d `, [
+ ("query", "", false, false),
+ ("-d", "", false, false),
+ ]),
+ (`user[email]=me@test.org 'user=password'!=hunter2`, [
+ ("user[email]", "me@test.org", false, false),
+ ("user=password", "hunter2", true, false),
+ ]),
+ (`user[email]=me@test.org 'user=password'!=hunter2`, [
+ ("user[email]", "me@test.org", false, false),
+ ("user=password", "hunter2", true, false),
+ ]),
+ (`Êmail²=hunter@example.org 'Späced password'!=nothunter2`, [
+ ("Êmail²", "hunter@example.org", false, false),
+ ("Späced password", "nothunter2", true, false),
+ ]),
];
for (let i = 0z; i < len(cases); i += 1) {
const expected = cases[i].1;
- const input = cases[i].0;
- const input = bufio::fixed(strings::toutf8(input),
- io::mode::READ);
-
- const result = parse(&input)!;
+ const result = parse_string(cases[i].0)!;
defer finish(&result);
assert(len(expected) == len(result.items));
for (let j = 0z; j < len(result.items); j += 1) {
- const got = result.items[i];
- const expect = &expected[i];
+ const got = result.items[j];
+ const expect = &expected[j];
assert(got.key == expect.0);
assert(got.value == expect.1);
assert(got.private == expect.2);
@@ -124,3 +272,14 @@ export fn finish(q: *query) void = {
};
};
};
+
+@test fn query_parse_split() void = {
+ const result = parse_items([`Spaced password!=nothunter2`])!;
+ defer finish(&result);
+
+ assert(len(result.items) == 1);
+ assert(result.items[0].key == "Spaced password");
+ assert(result.items[0].value == "nothunter2");
+ assert(result.items[0].private);
+ assert(!result.items[0].optional);
+};
diff --git a/himitsu/query/unparse.ha b/himitsu/query/unparse.ha
index d0a8c74..05167e5 100644
--- a/himitsu/query/unparse.ha
+++ b/himitsu/query/unparse.ha
@@ -1,6 +1,9 @@
+use ascii;
+use encoding::utf8;
use fmt;
use io;
-use shlex;
+use strings;
+use strio;
// Converts a Himitsu query into a string, including a newline, and writes it to
// the given I/O handle.
@@ -9,7 +12,7 @@ export fn unparse(sink: io::handle, q: *query) (size | io::error) = {
for (let i = 0z; i < len(q.items); i += 1) {
const pair = &q.items[i];
- z += fmt::fprintf(sink, "{}", pair.key)?;
+ z += quote(sink, pair.key)?;
if (pair.private) {
z += fmt::fprintf(sink, "!")?;
@@ -19,7 +22,7 @@ export fn unparse(sink: io::handle, q: *query) (size | io::error) = {
};
if (pair.value != "") {
z += fmt::fprintf(sink, "=")?;
- z += shlex::quote(sink, pair.value)?;
+ z += quote(sink, pair.value)?;
};
if (i + 1 < len(q.items)) {
@@ -29,3 +32,59 @@ export fn unparse(sink: io::handle, q: *query) (size | io::error) = {
z += fmt::fprintln(sink)?;
return z;
};
+
+// Quotes a string for [[parse]] and writes it to the provided [[io::handle]].
+export fn quote(sink: io::handle, s: str) (size | io::error) = {
+ if (len(s) == 0) {
+ return io::writeall(sink, strings::toutf8(`''`))?;
+ };
+ if (is_safe(s)) {
+ return io::writeall(sink, strings::toutf8(s))?;
+ };
+
+ let z = io::writeall(sink, ['\''])?;
+
+ const iter = strings::iter(s);
+ for (true) {
+ const rn = match (strings::next(&iter)) {
+ case let rn: rune =>
+ yield rn;
+ case void =>
+ break;
+ };
+
+ if (rn == '\'') {
+ z += io::writeall(sink, strings::toutf8(`'"'"'`))?;
+ } else if (rn == '\n') {
+ z += io::writeall(sink, strings::toutf8(`'"\n"'`))?;
+ } else {
+ z += io::writeall(sink, utf8::encoderune(rn))?;
+ };
+ };
+
+ z += io::writeall(sink, ['\''])?;
+ return z;
+};
+
+fn is_safe(s: str) bool = {
+ const iter = strings::iter(s);
+ for (true) {
+ const rn = match (strings::next(&iter)) {
+ case let rn: rune =>
+ yield rn;
+ case void =>
+ break;
+ };
+
+ switch (rn) {
+ case '@', '%', '+', '=', ':', ',', '.', '/', '-' =>
+ void;
+ case =>
+ if (!ascii::isalnum(rn) || ascii::isspace(rn)
+ || rn == '\n') {
+ return false;
+ };
+ };
+ };
+ return true;
+};
diff --git a/secstore/serialize.ha b/secstore/serialize.ha
index 99e6fa7..a9cc4b6 100644
--- a/secstore/serialize.ha
+++ b/secstore/serialize.ha
@@ -1,14 +1,14 @@
use bytes;
-use crypto;
use crypto::keystore;
+use crypto;
use encoding::base64;
use errors;
use fmt;
use fs;
+use himitsu::query;
use io;
use os;
use path;
-use shlex;
use strio;
use uuid;
@@ -28,11 +28,11 @@ export fn write(
// TODO: https://todo.sr.ht/~sircmpwn/hare/619
for (let i = 0z; i < len(ent.pairs); i += 1) {
const pair = &ent.pairs[i];
- shlex::quote(sink, pair.key)?;
+ query::quote(sink, pair.key)?;
match (pair.value) {
case let val: str =>
fmt::fprint(sink, "=")?;
- shlex::quote(sink, val)?;
+ query::quote(sink, val)?;
case let u: uuid::uuid =>
fmt::fprint(sink, "!")?;
if (private) {
@@ -42,7 +42,7 @@ export fn write(
let buf = strio::dynamic();
defer io::close(&buf)!;
write_private(store, &buf, u)?;
- shlex::quote(sink, strio::string(&buf))?;
+ query::quote(sink, strio::string(&buf))?;
};
};
if (i + 1 < len(ent.pairs)) {
--
2.40.1