tools

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

main.zig (9264B)


      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 selected = try fzf(init, alloc, paths.items) orelse return;
     36     defer alloc.free(selected);
     37 
     38     const exists = blk: {
     39         for (passwords.items) |item| {
     40             if (std.mem.eql(u8, item.path, selected)) break :blk true;
     41         }
     42         break :blk false;
     43     };
     44     const basename_start = if (std.mem.lastIndexOfScalar(u8, selected, '/')) |idx| idx + 1 else 0;
     45     const has_otp = std.mem.find(u8, selected[basename_start..], "otp") != null;
     46 
     47     const secret = if (has_otp and exists)
     48         try run(init, alloc, &.{ "pass", "otp", "code", selected })
     49     else if (!exists)
     50         try store(init, alloc, selected)
     51     else
     52         run(init, alloc, &.{ "pass", "show", selected }) catch try store(init, alloc, selected);
     53 
     54     try clip(init, secret);
     55     alloc.free(secret);
     56 }
     57 
     58 fn dir(alloc: std.mem.Allocator, environ: *std.process.Environ.Map) ![]const u8 {
     59     if (environ.get("PASSWORD_STORE_DIR")) |p| {
     60         return try alloc.dupe(u8, p);
     61     }
     62 
     63     const home = environ.get("HOME") orelse return error.NoHome;
     64     return try std.fs.path.join(alloc, &.{ home, ".password-store" });
     65 }
     66 
     67 fn collect(alloc: std.mem.Allocator, io: std.Io, base_dir: []const u8, sub_path: []const u8, list: *std.ArrayListUnmanaged(Entry)) anyerror!void {
     68     const full_path = try std.fs.path.join(alloc, &.{ base_dir, sub_path });
     69     defer alloc.free(full_path);
     70 
     71     var d = std.Io.Dir.openDirAbsolute(io, full_path, .{ .iterate = true }) catch return;
     72     defer d.close(io);
     73 
     74     var it = d.iterate();
     75     while (try it.next(io)) |entry| {
     76         if (entry.name.len > 0 and entry.name[0] == '.') continue;
     77 
     78         const entry_sub_path = if (sub_path.len == 0)
     79             try alloc.dupe(u8, entry.name)
     80         else
     81             try std.fs.path.join(alloc, &.{ sub_path, entry.name });
     82         errdefer alloc.free(entry_sub_path);
     83 
     84         if (entry.kind == .directory) {
     85             try collect(alloc, io, base_dir, entry_sub_path, list);
     86             alloc.free(entry_sub_path);
     87         } else if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".gpg")) {
     88             const name = entry_sub_path[0 .. entry_sub_path.len - 4];
     89             const file_path = try std.fs.path.join(alloc, &.{ base_dir, entry_sub_path });
     90             defer alloc.free(file_path);
     91 
     92             const accessed = blk: {
     93                 const file = std.Io.Dir.openFileAbsolute(io, file_path, .{}) catch |err| {
     94                     std.debug.print("Failed to stat {s}: {}\n", .{ file_path, err });
     95                     break :blk @as(i128, 0);
     96                 };
     97                 defer file.close(io);
     98 
     99                 const file_stat = file.stat(io) catch |err| {
    100                     std.debug.print("Failed to stat {s}: {}\n", .{ file_path, err });
    101                     break :blk @as(i128, 0);
    102                 };
    103 
    104                 const atime_ts = file_stat.atime orelse break :blk @as(i128, 0);
    105                 break :blk @as(i128, @intCast(atime_ts.nanoseconds));
    106             };
    107 
    108             try list.append(alloc, Entry{
    109                 .path = try alloc.dupe(u8, name),
    110                 .accessed = accessed,
    111             });
    112             alloc.free(entry_sub_path);
    113         } else {
    114             alloc.free(entry_sub_path);
    115         }
    116     }
    117 }
    118 
    119 fn fzf(init: std.process.Init, alloc: std.mem.Allocator, items: []const []const u8) !?[]u8 {
    120     var child = try std.process.spawn(init.io, .{
    121         .argv = &.{ "fzf", "--print-query" },
    122         .stdin = .pipe,
    123         .stdout = .pipe,
    124         .stderr = .inherit,
    125     });
    126 
    127     if (child.stdin) |stdin| {
    128         var buf: [4096]u8 = undefined;
    129         var fw = stdin.writer(init.io, &buf);
    130         for (items) |item| {
    131             try fw.interface.writeAll(item);
    132             try fw.interface.writeAll("\n");
    133         }
    134         try fw.flush();
    135         stdin.close(init.io);
    136         child.stdin = null;
    137     }
    138 
    139     const output = try read(init.io, alloc, &child);
    140     defer alloc.free(output);
    141 
    142     const term = try child.wait(init.io);
    143     if (term != .exited) return null;
    144 
    145     const trimmed = std.mem.trim(u8, output, " \n\r\t");
    146     if (trimmed.len == 0) return null;
    147 
    148     if (std.mem.findScalar(u8, trimmed, '\n')) |idx| {
    149         const selected = std.mem.trim(u8, trimmed[idx + 1 ..], " \n\r\t");
    150         if (selected.len > 0) return try alloc.dupe(u8, selected);
    151         const query = std.mem.trim(u8, trimmed[0..idx], " \n\r\t");
    152         if (query.len > 0) return try alloc.dupe(u8, query);
    153     }
    154 
    155     return try alloc.dupe(u8, trimmed);
    156 }
    157 
    158 fn run(init: std.process.Init, alloc: std.mem.Allocator, args: []const []const u8) ![]u8 {
    159     var child = try std.process.spawn(init.io, .{
    160         .argv = args,
    161         .stdout = .pipe,
    162         .stderr = .inherit,
    163     });
    164 
    165     const output = try read(init.io, alloc, &child);
    166 
    167     const term = try child.wait(init.io);
    168     if (term != .exited or term.exited != 0) {
    169         alloc.free(output);
    170         return error.FailedExit;
    171     }
    172 
    173     if (std.mem.findScalar(u8, output, '\n')) |idx| {
    174         const result = try alloc.dupe(u8, output[0..idx]);
    175         alloc.free(output);
    176         return result;
    177     }
    178 
    179     return output;
    180 }
    181 
    182 fn read(io: std.Io, alloc: std.mem.Allocator, child: *std.process.Child) ![]u8 {
    183     if (child.stdout) |file| {
    184         var buf: [4096]u8 = undefined;
    185         var fr = file.reader(io, &buf);
    186         return try fr.interface.allocRemaining(alloc, .unlimited);
    187     }
    188     return try alloc.dupe(u8, "");
    189 }
    190 
    191 fn gen(init: std.process.Init, alloc: std.mem.Allocator) ![]u8 {
    192     const charset = std.ascii.letters ++ "0123456789";
    193     const special_chars = "!@#$%^&*()-_=+[]{}|;:,.<>?";
    194 
    195     var password = try alloc.alloc(u8, 16);
    196     errdefer alloc.free(password);
    197 
    198     init.io.random(password);
    199     for (password) |*c| {
    200         c.* = charset[c.* % charset.len];
    201     }
    202 
    203     var idx_bytes: [8]u8 = undefined;
    204     init.io.random(&idx_bytes);
    205     const pos = std.mem.readInt(u64, idx_bytes[0..8], .little) % password.len;
    206     init.io.random(&idx_bytes);
    207     const idx = std.mem.readInt(u64, idx_bytes[0..8], .little) % special_chars.len;
    208     password[pos] = special_chars[idx];
    209 
    210     return password;
    211 }
    212 
    213 fn store(init: std.process.Init, alloc: std.mem.Allocator, name: []const u8) ![]u8 {
    214     const password = try gen(init, alloc);
    215     errdefer alloc.free(password);
    216 
    217     inner(init, alloc, name, password) catch |err| {
    218         std.debug.print("Failed to store password: {}\n", .{err});
    219         return err;
    220     };
    221     return password;
    222 }
    223 
    224 fn inner(init: std.process.Init, alloc: std.mem.Allocator, name: []const u8, password: []const u8) !void {
    225     const pass_dir = try dir(alloc, init.environ_map);
    226     defer alloc.free(pass_dir);
    227 
    228     const full_path = try std.fs.path.join(alloc, &.{ pass_dir, name });
    229     defer alloc.free(full_path);
    230 
    231     if (std.fs.path.dirname(full_path)) |dir_path| {
    232         std.Io.Dir.createDirAbsolute(init.io, dir_path, .default_dir) catch |err| {
    233             if (err != error.PathAlreadyExists) return err;
    234         };
    235     }
    236 
    237     var child = try std.process.spawn(init.io, .{
    238         .argv = &.{ "pass", "insert", "-e", name },
    239         .stdin = .pipe,
    240         .stderr = .inherit,
    241     });
    242 
    243     if (child.stdin) |stdin| {
    244         var buf: [4096]u8 = undefined;
    245         var fw = stdin.writer(init.io, &buf);
    246         try fw.interface.writeAll(password);
    247         try fw.interface.writeAll("\n");
    248         try fw.flush();
    249         stdin.close(init.io);
    250         child.stdin = null;
    251     }
    252 
    253     const term = try child.wait(init.io);
    254     if (term != .exited or term.exited != 0) return error.FailedExit;
    255 }
    256 
    257 fn clip(init: std.process.Init, content: []const u8) !void {
    258     const args: []const []const u8 = switch (builtin.os.tag) {
    259         .macos => &.{"pbcopy"},
    260         .linux => args: {
    261             if (init.environ_map.get("WAYLAND_DISPLAY")) |_| {
    262                 break :args &.{"wl-copy"};
    263             } else break :args &.{ "xclip", "-selection", "clipboard" };
    264         },
    265         else => return,
    266     };
    267 
    268     var child = try std.process.spawn(init.io, .{
    269         .argv = args,
    270         .stdin = .pipe,
    271     });
    272 
    273     if (child.stdin) |stdin| {
    274         var buf: [4096]u8 = undefined;
    275         var fw = stdin.writer(init.io, &buf);
    276         try fw.interface.writeAll(content);
    277         try fw.flush();
    278         stdin.close(init.io);
    279         child.stdin = null;
    280     }
    281 
    282     const term = try child.wait(init.io);
    283     if (term != .exited) return error.ClipboardFailed;
    284 }