use fmt; use getopt; use os; use os::exec; use io; use strings; use memio; use bufio; use format::ini; use encoding::utf8; use unix::tty; use errors; let verbose: bool = false; type account = struct { name: const str, user: str, pass: str, group: str, notes: str, url: str, //match }; type filter = struct { accnames: []str, urls: []str, groups: []str, notes: str, }; export fn main() void = { //testmap(); const cmd = getopt::parse(os::args, "imp", ('v', "Verbose mode"), ('l', "List the accounts"), ('p', "Display the account passwords"), ('c', "Print ini config from parsed accounts"), ('a', "Add an account"), ('f', "file", "The accounts file to use (IMP_FILE by default)"), ('g', "groups...", "Filters by a comma-separated list of groups"), ('m', "matches...", "Filters by a comma-separated list of matches"), ('n', "note", "Filters by notes containing the given keywords"), "accounts...", ); defer getopt::finish(&cmd); let file = match (os::getenv("IMP_FILE")) { case let f: str => yield f; case void => yield ""; }; let displaypass = false; let list = false; let accfilter = filter {...}; let add = false; let printconf = false; for (let i = 0z; i < len(cmd.opts); i += 1) { let opt = cmd.opts[i]; switch (opt.0) { case 'v' => verbose = true; case 'a' => add = true; case 'c' => printconf = true; case 'p' => displaypass = true; case 'l' => list = true; case 'g' => let spl = strings::split(opt.1, ","); defer free(spl); append(accfilter.groups, spl...)!; case 'm' => let spl = strings::split(opt.1, ","); defer free(spl); append(accfilter.urls, spl...)!; case 'n' => accfilter.notes = opt.1; case 'f' => file = opt.1; case => abort(); }; }; for (let i = 0z; i < len(cmd.args); i += 1) { append(accfilter.accnames, cmd.args[i])!; }; if (verbose) { printfilter(accfilter); }; if (file == "") { fmt::fatal("No file specified and the IMP_FILE environment variable isn't set"); }; let ini_data = match (decrypt(file)) { case let s: []u8 => yield s; case let e: os::exec::error => fmt::fatal(os::exec::strerror(e)); case let e: io::error => fmt::fatal(io::strerror(e)); }; defer free(ini_data); let accounts = match (parse(ini_data)) { case let e: []account => yield e; case let e: format::ini::error => fmt::fatal(format::ini::strerror(e)); }; if (add) { const acc = account { ... }; const n = getinput("name: ")!; const u = getinput("user: ")!; const p = getinput("pass: ", true)!; const cp = getinput("confirm pass: ", true)!; if (p != cp) { fmt::fatal("Passwords don't match"); }; if (n == "" || u == "" || p == "") { free(n); free(u); free(p); } else { acc.name = n; acc.user = u; acc.pass = p; append(accounts, acc)!; }; }; accounts = accs_filter(accounts, accfilter); defer accounts_free(accounts); if (printconf) { printaccs(os::stdout, accounts); } else if (list) { for (let i = 0z; i < len(accounts); i += 1) { let acc = accounts[i]; fmt::println(acc.name)!; }; } else { for (let i = 0z; i < len(accounts); i += 1) { let acc = accounts[i]; fmt::printfln("name\t{}", acc.name)!; fmt::printfln("user\t{}", acc.user)!; if (displaypass) { fmt::printfln("pass\t{}", acc.pass)!; }; if (len(acc.group) != 0) { fmt::printfln("group\t{}", acc.group)!; }; if (len(acc.url) != 0) { fmt::printfln("match\t{}", acc.url)!; }; if (len(acc.notes) != 0) { fmt::printfln("notes\t{}", acc.notes)!; }; if (i != len(accounts) - 1) { fmt::println()!; }; }; }; }; fn printfilter(f: filter) void = { fmt::fprintln(os::stderr, "printing filter:")!; fmt::fprint(os::stderr, "accs: ")!; let accnames = strings::join(", ", f.accnames...); defer free(accnames); fmt::fprintln(os::stderr, accnames)!; fmt::fprint(os::stderr, "groups: ")!; let groups = strings::join(", ", f.groups...); defer free(groups); fmt::fprintln(os::stderr, groups)!; fmt::fprint(os::stderr, "urls: ")!; let urls = strings::join(", ", f.urls...); defer free(urls); fmt::fprintln(os::stderr, urls)!; fmt::fprintfln(os::stderr, "notes: {}", f.notes)!; }; fn isemptyfilter(f: filter) bool = { return len(f.accnames) == 0 && len(f.groups) == 0 && len(f.urls) == 0 && len(f.notes) == 0; }; fn isfiltered(acc: account, f: filter) bool = { for (let i = 0z; i < len(f.accnames); i += 1) { if (strings::compare(f.accnames[i], acc.name) == 0) { return true; }; }; for (let i = 0z; i < len(f.groups); i += 1) { if (strings::hasprefix(acc.group, f.groups[i])) { return true; }; }; for (let i = 0z; i < len(f.urls); i += 1) { const url1 = strings::rtrim(f.urls[i], '/'); const url2 = strings::rtrim(acc.url, '/'); if (strings::compare(url1, url2) == 0) { return true; }; }; if (len(f.notes) != 0 && strings::contains(acc.notes, f.notes)) { return true; }; return false; }; fn accs_filter(accounts: []account, accfilter: filter) []account = { if (isemptyfilter(accfilter)) { return accounts; }; if (verbose) { fmt::fprintln(os::stderr, "filter not empty")!; }; let resaccs: []account = []; for (let i = 0z; i < len(accounts); i += 1) { let acc = accounts[i]; if (isfiltered(acc, accfilter)) { append(resaccs, acc)!; }; }; return resaccs; }; fn accounts_free(accounts: []account) void = { for (let i = 0z; i < len(accounts); i += 1) { account_free(accounts[i]); }; free(accounts); }; fn account_free(acc: account) void = { free(acc.name); // invalid metadata for small allocation on freelist (heap corruption?) //if (len(acc.pass) != 0) { // free(&acc.pass); //}; //if (len(acc.url) != 0) { // free(&acc.url); //}; //if (len(acc.notes) != 0) { // free(&acc.notes); //}; free(acc.group); }; fn parse(data: []u8) ([]account | format::ini::error) = { let scanner = format::ini::scan(&memio::fixed(data)); defer format::ini::finish(&scanner); //let entries: []format::ini::entry = []; let accounts: []account = []; for (true) { let entry = match (format::ini::next(&scanner)?) { case let e: format::ini::entry => yield e; case io::EOF => break; }; match (lookupaccount(accounts, entry.0)) { case void => let name = strings::dup(entry.0); let spl = strings::split(name, "/"); defer free(spl); let group = ""; if (len(spl) > 1) { group = strings::join("/", spl[..len(spl) - 1]...); }; let acc = account { name = name, group = group, ... }; setfieldaccount(&acc, strings::dup(entry.1), strings::dup(entry.2)); append(accounts, acc)!; case let acc: *account => setfieldaccount(acc, strings::dup(entry.1), strings::dup(entry.2)); }; }; return accounts; }; // Returns the accounts as an ini string. The return value must be freed. fn printaccs(h: io::handle, accs: []account) void = { for (let i = 0z; i < len(accs); i += 1) { const acc = accs[i]; fmt::fprintf(h, "[{}]\nuser={}\npass={}\n", acc.name, acc.user, acc.pass)!; if (acc.notes != "") { fmt::fprintf(h, "notes={}\n", acc.notes)!; }; if (acc.url != "") { fmt::fprintf(h, "match={}\n", acc.url)!; }; if (i < len(accs) - 1) { fmt::fprint(h, "\n")!; }; }; }; fn setfieldaccount(acc: *account, key: str, val: str) void = { key = strings::trim(key, ' '); defer free(key); val = strings::trim(val, ' '); //defer free(val); // Handle quoted values let valrunes = strings::torunes(val); defer free(valrunes); if (valrunes[0] == '"' && valrunes[len(valrunes)-1] == '"') { val = strings::trim(val, '"'); }; if (key == "user") { acc.user = val; } else if (key == "pass") { acc.pass = val; } else if (key == "notes") { acc.notes = val; } else if (key == "match") { acc.url = val; } else { free(val); }; }; fn lookupaccount(accounts: []account, name: str) (*account | void) = { for (let i = 0z; i < len(accounts); i += 1) { if (accounts[i].name == name) return &accounts[i]; }; return; }; fn decrypt(file: str) ([]u8 | os::exec::error | io::error) = { let cmd = os::exec::cmd("gpg", "-d", file)?; let pipe_stdout = os::exec::pipe(); os::exec::addfile(&cmd, os::stdout_file, pipe_stdout.1); os::exec::addfile(&cmd, os::stderr_file, os::exec::nullfd); let proc = os::exec::start(&cmd)?; io::close(pipe_stdout.1)?; let out = io::drain(pipe_stdout.0)?; io::close(pipe_stdout.0)?; let status = os::exec::wait(&proc)?; match (os::exec::check(&status)) { case let e: !os::exec::exit_status => fmt::fatal(os::exec::exitstr(e)); case void => yield; }; return out; }; fn readin(termf: io::file) (str | io::error | utf8::invalid) = { const scanner = bufio::newscanner(termf); defer bufio::finish(&scanner); const line = match (bufio::scan_line(&scanner)?) { case io::EOF => yield ""; case let line: const str => yield line; }; return strings::dup(line); }; // Gets user input from the tty (supports pipes) fn getinput(text: str = "", noecho: bool = false) (str | tty::error | io::error | utf8::invalid | errors::error) = { let termf = tty::open()?; defer io::close(termf)!; fmt::fprintf(termf, "{}", text)!; if (noecho) { const termios = tty::termios_query(termf)?; tty::noecho(&termios)?; defer tty::termios_restore(&termios); defer fmt::fprintln(termf)!; return readin(termf)?; }; return readin(termf)?; };