tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

main.zig (12093B)


      1 const std = @import("std");
      2 const builtin = @import("builtin");
      3 
      4 const Entry = struct {
      5     path: []const u8,
      6     accessed: i128,
      7 };
      8 
      9 pub fn main(init: std.process.Init) !void {
     10     const alloc = init.gpa;
     11 
     12     const pass_dir = try dir(alloc, init.environ_map);
     13     defer alloc.free(pass_dir);
     14 
     15     var passwords = std.ArrayListUnmanaged(Entry).empty;
     16     defer {
     17         for (passwords.items) |p| alloc.free(p.path);
     18         passwords.deinit(alloc);
     19     }
     20 
     21     try collect(alloc, init.io, pass_dir, "", &passwords);
     22 
     23     std.mem.sort(Entry, passwords.items, {}, struct {
     24         fn lessThan(_: void, a: Entry, b: Entry) bool {
     25             return a.accessed > b.accessed;
     26         }
     27     }.lessThan);
     28 
     29     var paths = std.ArrayListUnmanaged([]const u8).empty;
     30     defer paths.deinit(alloc);
     31     for (passwords.items) |entry| {
     32         try paths.append(alloc, entry.path);
     33     }
     34 
     35     const pick = try fzf(init, alloc, paths.items) orelse return;
     36     defer pick.deinit(alloc);
     37 
     38     if (std.mem.eql(u8, pick.key, "ctrl-e")) {
     39         const target = if (pick.selected.len > 0) pick.selected else pick.query;
     40         if (target.len == 0) return;
     41         try edit(init, target);
     42         return;
     43     }
     44 
     45     const selected = if (pick.selected.len > 0) pick.selected else pick.query;
     46     if (selected.len == 0) return;
     47 
     48     const exists = blk: {
     49         for (passwords.items) |item| {
     50             if (std.mem.eql(u8, item.path, selected)) break :blk true;
     51         }
     52         break :blk false;
     53     };
     54     const basename_start = if (std.mem.lastIndexOfScalar(u8, selected, '/')) |idx| idx + 1 else 0;
     55     const has_otp = std.mem.find(u8, selected[basename_start..], "otp") != null;
     56 
     57     const secret = if (has_otp and exists)
     58         try run(init, alloc, &.{ "pass", "otp", "code", selected })
     59     else if (!exists)
     60         try store(init, alloc, selected)
     61     else
     62         run(init, alloc, &.{ "pass", "show", selected }) catch try store(init, alloc, selected);
     63 
     64     try clip(init, secret);
     65     alloc.free(secret);
     66 }
     67 
     68 fn dir(alloc: std.mem.Allocator, environ: *std.process.Environ.Map) ![]const u8 {
     69     if (environ.get("PASSWORD_STORE_DIR")) |p| {
     70         return try alloc.dupe(u8, p);
     71     }
     72 
     73     const home = environ.get("HOME") orelse return error.NoHome;
     74     return try std.fs.path.join(alloc, &.{ home, ".password-store" });
     75 }
     76 
     77 fn collect(alloc: std.mem.Allocator, io: std.Io, base_dir: []const u8, sub_path: []const u8, list: *std.ArrayListUnmanaged(Entry)) anyerror!void {
     78     const full_path = try std.fs.path.join(alloc, &.{ base_dir, sub_path });
     79     defer alloc.free(full_path);
     80 
     81     var d = std.Io.Dir.openDirAbsolute(io, full_path, .{ .iterate = true }) catch return;
     82     defer d.close(io);
     83 
     84     var it = d.iterate();
     85     while (try it.next(io)) |entry| {
     86         if (entry.name.len > 0 and entry.name[0] == '.') continue;
     87 
     88         const entry_sub_path = if (sub_path.len == 0)
     89             try alloc.dupe(u8, entry.name)
     90         else
     91             try std.fs.path.join(alloc, &.{ sub_path, entry.name });
     92         errdefer alloc.free(entry_sub_path);
     93 
     94         if (entry.kind == .directory) {
     95             try collect(alloc, io, base_dir, entry_sub_path, list);
     96             alloc.free(entry_sub_path);
     97         } else if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".gpg")) {
     98             const name = entry_sub_path[0 .. entry_sub_path.len - 4];
     99             const file_path = try std.fs.path.join(alloc, &.{ base_dir, entry_sub_path });
    100             defer alloc.free(file_path);
    101 
    102             const accessed = blk: {
    103                 const file = std.Io.Dir.openFileAbsolute(io, file_path, .{}) catch |err| {
    104                     std.log.warn("failed to open {s}: {}", .{ file_path, err });
    105                     break :blk @as(i128, 0);
    106                 };
    107                 defer file.close(io);
    108 
    109                 const file_stat = file.stat(io) catch |err| {
    110                     std.log.warn("failed to stat {s}: {}", .{ file_path, err });
    111                     break :blk @as(i128, 0);
    112                 };
    113 
    114                 const atime_ts = file_stat.atime orelse break :blk @as(i128, 0);
    115                 break :blk @as(i128, @intCast(atime_ts.nanoseconds));
    116             };
    117 
    118             try list.append(alloc, Entry{
    119                 .path = try alloc.dupe(u8, name),
    120                 .accessed = accessed,
    121             });
    122             alloc.free(entry_sub_path);
    123         } else {
    124             alloc.free(entry_sub_path);
    125         }
    126     }
    127 }
    128 
    129 const Pick = struct {
    130     query: []const u8,
    131     key: []const u8,
    132     selected: []const u8,
    133     raw: []u8,
    134 
    135     fn deinit(self: Pick, alloc: std.mem.Allocator) void {
    136         alloc.free(self.raw);
    137     }
    138 };
    139 
    140 const PickParts = struct {
    141     query: []const u8,
    142     key: []const u8,
    143     selected: []const u8,
    144 };
    145 
    146 fn parseFzfOutput(output: []const u8) ?PickParts {
    147     var lines = [3][]const u8{ "", "", "" };
    148     var it = std.mem.splitScalar(u8, output, '\n');
    149     var i: usize = 0;
    150     while (i < lines.len) : (i += 1) {
    151         const line = it.next() orelse break;
    152         lines[i] = std.mem.trim(u8, line, " \n\r\t");
    153     }
    154 
    155     if (lines[0].len == 0 and lines[1].len == 0 and lines[2].len == 0) return null;
    156 
    157     return .{ .query = lines[0], .key = lines[1], .selected = lines[2] };
    158 }
    159 
    160 fn fzf(init: std.process.Init, alloc: std.mem.Allocator, items: []const []const u8) !?Pick {
    161     var child = try std.process.spawn(init.io, .{
    162         .argv = &.{ "fzf", "--exact", "--print-query", "--expect=ctrl-e" },
    163         .stdin = .pipe,
    164         .stdout = .pipe,
    165         .stderr = .inherit,
    166     });
    167 
    168     if (child.stdin) |stdin| {
    169         var buf: [4096]u8 = undefined;
    170         var fw = stdin.writer(init.io, &buf);
    171         for (items) |item| {
    172             try fw.interface.writeAll(item);
    173             try fw.interface.writeAll("\n");
    174         }
    175         try fw.flush();
    176         stdin.close(init.io);
    177         child.stdin = null;
    178     }
    179 
    180     const output = try read(init.io, alloc, &child);
    181     errdefer alloc.free(output);
    182 
    183     const term = try child.wait(init.io);
    184     if (term != .exited) {
    185         alloc.free(output);
    186         return null;
    187     }
    188 
    189     const parts = parseFzfOutput(output) orelse {
    190         alloc.free(output);
    191         return null;
    192     };
    193 
    194     return .{
    195         .query = parts.query,
    196         .key = parts.key,
    197         .selected = parts.selected,
    198         .raw = output,
    199     };
    200 }
    201 
    202 fn edit(init: std.process.Init, target: []const u8) !void {
    203     var child = try std.process.spawn(init.io, .{
    204         .argv = &.{ "pass", "edit", target },
    205         .stdin = .inherit,
    206         .stdout = .inherit,
    207         .stderr = .inherit,
    208     });
    209 
    210     const term = try child.wait(init.io);
    211     // `pass edit` exits non-zero with "Password unchanged." when the editor
    212     // saved no changes. That is not a failure here.
    213     if (term != .exited) return error.FailedExit;
    214 }
    215 
    216 test "parseFzfOutput query only" {
    217     const parts = parseFzfOutput("foo\n") orelse return error.TestUnexpectedResult;
    218     try std.testing.expectEqualStrings("foo", parts.query);
    219     try std.testing.expectEqualStrings("", parts.key);
    220     try std.testing.expectEqualStrings("", parts.selected);
    221 }
    222 
    223 test "parseFzfOutput query key selection" {
    224     const parts = parseFzfOutput("foo\nctrl-e\nbar\n") orelse return error.TestUnexpectedResult;
    225     try std.testing.expectEqualStrings("foo", parts.query);
    226     try std.testing.expectEqualStrings("ctrl-e", parts.key);
    227     try std.testing.expectEqualStrings("bar", parts.selected);
    228 }
    229 
    230 test "parseFzfOutput enter with selection" {
    231     const parts = parseFzfOutput("foo\n\nbar\n") orelse return error.TestUnexpectedResult;
    232     try std.testing.expectEqualStrings("foo", parts.query);
    233     try std.testing.expectEqualStrings("", parts.key);
    234     try std.testing.expectEqualStrings("bar", parts.selected);
    235 }
    236 
    237 test "parseFzfOutput empty query ctrl-e" {
    238     const parts = parseFzfOutput("\nctrl-e\n\n") orelse return error.TestUnexpectedResult;
    239     try std.testing.expectEqualStrings("", parts.query);
    240     try std.testing.expectEqualStrings("ctrl-e", parts.key);
    241     try std.testing.expectEqualStrings("", parts.selected);
    242 }
    243 
    244 test "parseFzfOutput empty" {
    245     try std.testing.expect(parseFzfOutput("\n\n\n") == null);
    246     try std.testing.expect(parseFzfOutput("") == null);
    247 }
    248 
    249 fn run(init: std.process.Init, alloc: std.mem.Allocator, args: []const []const u8) ![]u8 {
    250     var child = try std.process.spawn(init.io, .{
    251         .argv = args,
    252         .stdout = .pipe,
    253         .stderr = .inherit,
    254     });
    255 
    256     const output = try read(init.io, alloc, &child);
    257 
    258     const term = try child.wait(init.io);
    259     if (term != .exited or term.exited != 0) {
    260         alloc.free(output);
    261         return error.FailedExit;
    262     }
    263 
    264     if (std.mem.findScalar(u8, output, '\n')) |idx| {
    265         const result = try alloc.dupe(u8, output[0..idx]);
    266         alloc.free(output);
    267         return result;
    268     }
    269 
    270     return output;
    271 }
    272 
    273 fn read(io: std.Io, alloc: std.mem.Allocator, child: *std.process.Child) ![]u8 {
    274     if (child.stdout) |file| {
    275         var buf: [4096]u8 = undefined;
    276         var fr = file.reader(io, &buf);
    277         return try fr.interface.allocRemaining(alloc, .unlimited);
    278     }
    279     return try alloc.dupe(u8, "");
    280 }
    281 
    282 fn gen(init: std.process.Init, alloc: std.mem.Allocator) ![]u8 {
    283     const charset = std.ascii.letters ++ "0123456789";
    284     const special_chars = "!@#$%^&*()-_=+[]{}|;:,.<>?";
    285 
    286     var password = try alloc.alloc(u8, 16);
    287     errdefer alloc.free(password);
    288 
    289     init.io.random(password);
    290     for (password) |*c| {
    291         c.* = charset[c.* % charset.len];
    292     }
    293 
    294     var idx_bytes: [8]u8 = undefined;
    295     init.io.random(&idx_bytes);
    296     const pos = std.mem.readInt(u64, idx_bytes[0..8], .little) % password.len;
    297     init.io.random(&idx_bytes);
    298     const idx = std.mem.readInt(u64, idx_bytes[0..8], .little) % special_chars.len;
    299     password[pos] = special_chars[idx];
    300 
    301     return password;
    302 }
    303 
    304 fn store(init: std.process.Init, alloc: std.mem.Allocator, name: []const u8) ![]u8 {
    305     const password = try gen(init, alloc);
    306     errdefer alloc.free(password);
    307 
    308     inner(init, alloc, name, password) catch |err| {
    309         std.log.err("failed to store password: {}", .{err});
    310         return err;
    311     };
    312     return password;
    313 }
    314 
    315 fn inner(init: std.process.Init, alloc: std.mem.Allocator, name: []const u8, password: []const u8) !void {
    316     const pass_dir = try dir(alloc, init.environ_map);
    317     defer alloc.free(pass_dir);
    318 
    319     const full_path = try std.fs.path.join(alloc, &.{ pass_dir, name });
    320     defer alloc.free(full_path);
    321 
    322     if (std.fs.path.dirname(full_path)) |dir_path| {
    323         std.Io.Dir.createDirAbsolute(init.io, dir_path, .default_dir) catch |err| {
    324             if (err != error.PathAlreadyExists) return err;
    325         };
    326     }
    327 
    328     var child = try std.process.spawn(init.io, .{
    329         .argv = &.{ "pass", "insert", "-e", name },
    330         .stdin = .pipe,
    331         .stderr = .inherit,
    332     });
    333 
    334     if (child.stdin) |stdin| {
    335         var buf: [4096]u8 = undefined;
    336         var fw = stdin.writer(init.io, &buf);
    337         try fw.interface.writeAll(password);
    338         try fw.interface.writeAll("\n");
    339         try fw.flush();
    340         stdin.close(init.io);
    341         child.stdin = null;
    342     }
    343 
    344     const term = try child.wait(init.io);
    345     if (term != .exited or term.exited != 0) return error.FailedExit;
    346 }
    347 
    348 fn clip(init: std.process.Init, content: []const u8) !void {
    349     const args: []const []const u8 = switch (builtin.os.tag) {
    350         .macos => &.{"pbcopy"},
    351         .linux => args: {
    352             if (init.environ_map.get("WAYLAND_DISPLAY")) |_| {
    353                 break :args &.{"wl-copy"};
    354             } else break :args &.{ "xclip", "-selection", "clipboard" };
    355         },
    356         else => return,
    357     };
    358 
    359     var child = try std.process.spawn(init.io, .{
    360         .argv = args,
    361         .stdin = .pipe,
    362     });
    363 
    364     if (child.stdin) |stdin| {
    365         var buf: [4096]u8 = undefined;
    366         var fw = stdin.writer(init.io, &buf);
    367         try fw.interface.writeAll(content);
    368         try fw.flush();
    369         stdin.close(init.io);
    370         child.stdin = null;
    371     }
    372 
    373     const term = try child.wait(init.io);
    374     if (term != .exited) return error.ClipboardFailed;
    375 }