From ecb7bacfa1839a27a803bc24d3dee91fa21d9166 Mon Sep 17 00:00:00 2001 From: Andreas Huber Date: Tue, 27 Aug 2024 18:39:21 +0200 Subject: [PATCH] list command --- build.zig | 2 + src/main.zig | 168 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 153 insertions(+), 17 deletions(-) diff --git a/build.zig b/build.zig index 5149833..67b1bca 100644 --- a/build.zig +++ b/build.zig @@ -1,5 +1,7 @@ const std = @import("std"); +// TODO TODO + // Although this function looks imperative, note that its job is to // declaratively construct a build graph that will be executed by an external // runner. diff --git a/src/main.zig b/src/main.zig index bee4146..518e16c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,32 +1,75 @@ const std = @import("std"); const info = std.log.info; +const debug = std.log.debug; const print = std.debug.print; const Allocator = std.mem.Allocator; +const testing = std.testing; +const expect = testing.expect; // otpauth://totp/AWS+Dev?secret=47STA47VFCMMLLWOLHWO3KY7MYNC36MLCDTHOLIYKJCTTSSAMKVM7YA3VWT2AJEP&digits=6&icon=Amazon pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); - std.log.info("\n\nstart\n", .{}); const args = try std.process.argsAlloc(allocator); - std.debug.print("arguments: {s}\n", .{args}); - const x: Args = try parseArgs(args); - print("parsed Args: {?any}\n", x); - if (x.add != null) { + std.log.debug("arguments: {s}\n", .{args}); + const arg: Args = try parseArgs(allocator, args); + //print("parsed Args: {?any}\n", arg); + if (arg.add != null) { info("add:", .{}); - info("name: {s}", .{x.add.?.name}); - info("secret: {s}", .{x.add.?.secretEncoded}); + info("name: {s}", .{arg.add.?.name}); + info("secret: {s}", .{arg.add.?.secretEncoded}); const config_location = try configLocation(allocator); info("config location: {s}", .{config_location}); + + try readConfig(allocator); + } else if (arg.list) { + const names = try executeGetList(arg.config_location); + + for (0..names.items.len) |i| { + std.debug.print("{s}\n", .{names.items[i]}); + } + + //for (names, 0..) |name, index| { + // std.debug.print("{d} {s}", .{ index, name }); + //} + } else { + printHelp(); } + + //std.process.exit(returnValue); +} + +fn readConfig(allocator: Allocator) !void { + const config_location = try configLocation(allocator); + + var file = try std.fs.cwd().createFile(config_location, .{ .exclusive = true }); + defer file.close(); + + const bytes_read = try file.readToEndAlloc(allocator, 64 * 1024 * 1024); // read at most 64MB + info("config content: {s}", .{bytes_read}); + + //const content = try std.fs.openFileAbsolute(config_location, .{ .mode = .read_only }); + +} + +fn printHelp() void { + const msg = + \\Usage: zig-totp [options] + \\ + \\Options: + \\ --add otpauthUrl Add a new TOTP thingy with an otpauth URL. + ; + std.debug.print("{s}", .{msg}); } const Args = struct { add: ?OtpAuthUrl, + list: bool, + config_location: []const u8, }; const OtpAuthUrl = struct { @@ -35,11 +78,13 @@ const OtpAuthUrl = struct { url: []const u8, }; -const ArgumentError = error{InvalidOtpAuthUrl}; +const ArgumentError = error{ InvalidOtpAuthUrl, UnknownParameter, MissingConfigLocation }; -fn parseArgs(args: [][:0]u8) !Args { +fn parseArgs(allocator: Allocator, args: []const []const u8) !Args { var result = Args{ .add = null, + .list = false, + .config_location = try configLocation(allocator), }; var i: u17 = 1; while (i < args.len) : (i += 1) { @@ -51,7 +96,7 @@ fn parseArgs(args: [][:0]u8) !Args { return error.InvalidOtpAuthUrl; } - const otpauthUrlCandidate: ?[:0]const u8 = args[i]; + const otpauthUrlCandidate: ?[]const u8 = args[i]; if (otpauthUrlCandidate == null) { return error.InvalidOtpAuthUrl; @@ -79,31 +124,120 @@ fn parseArgs(args: [][:0]u8) !Args { //std.debug.print("add: {?s}\n", .{otpauthUrlCandidate}); //std.log.info("add: {?s}\n", .{otpauthUrlCandidate}); + } else if (std.mem.eql(u8, "--config", arg)) { + i += 1; + if (i >= args.len) { + return error.MissingConfigLocation; + } + const config_location = args[i]; + result.config_location = config_location; + } else if (std.mem.eql(u8, "list", arg)) { + result.list = true; } else { std.debug.print("unknown parameter: {s}\n", .{arg}); - std.process.exit(1); + return ArgumentError.UnknownParameter; } } return result; } -fn configLocation(allocator: Allocator) Allocator.Error![]const u8 { +fn parseOtpAuthUrl(url: []const u8) !OtpAuthUrl { + //std.debug.print("parsing >{s}<\n", .{url}); + const index = std.mem.indexOf(u8, url, "otpauth://totp/"); + if (index != 0) { + return error.InvalidOtpAuthUrl; + } + + var it = std.mem.splitSequence(u8, url[15..], "?secret="); + + const name = it.next(); + const secret = it.next(); + const empty = it.next(); + + if (name != null and secret != null and empty == null) { + return OtpAuthUrl{ .name = name.?, .secretEncoded = secret.?, .url = url }; + } else { + return ArgumentError.InvalidOtpAuthUrl; + } +} + +fn configLocation(allocator: Allocator) ![]const u8 { const xdg_config_home: ?[]const u8 = std.process.getEnvVarOwned(allocator, "XDG_CONFIG_HOME") catch null; if (xdg_config_home) |base| { const config_location = try std.mem.concat(allocator, u8, &[_][]const u8{ base, "/zig-totp" }); - info("config_location: {s}", .{config_location}); + debug("config_location: {s}", .{config_location}); return config_location; } const home = std.process.getEnvVarOwned(allocator, "HOME") catch null; const base = home orelse unreachable; const config_location = try std.mem.concat(allocator, u8, &[_][]const u8{ base, "/.zig-totp" }); - info("config_location: {s}", .{config_location}); + + debug("config_location: {s}", .{config_location}); return config_location; } -fn exit(returnValue: u8, message: []const u8) void { - std.log.warn("{s}", .{message}); - std.process.exit(returnValue); +fn executeGetList(config_location: []const u8) !std.ArrayList([]const u8) { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + const file = try std.fs.cwd().openFile(config_location, .{}); + defer file.close(); + + var buf_reader = std.io.bufferedReader(file.reader()); + var in_stream = buf_reader.reader(); + + //_ = try in_stream.readUntilDelimiterOrEofAlloc(allocator, '\n', 1024 * 10); + //std.debug.print("line: {s}", .{line1}); + // + //TODO print line + //TODO read all lines + //TODO parse the names + //TODO return list of names + // + + var names = std.ArrayList([]const u8).init(allocator); + + while (try in_stream.readUntilDelimiterOrEofAlloc(allocator, '\n', 1024 * 1024)) |line| { + //std.debug.print("line: {any}", .{line}); + if (line.len > 0 and std.mem.trim(u8, line, " \r").len > 0) { + const url = try parseOtpAuthUrl(line); + try names.append(url.name); + } + } + return names; +} + +test "bla" { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + const err = parseArgs(allocator, &[_][]const u8{ "path/of/executable", "--add", "not a totp url" }); + + try expect(err == ArgumentError.InvalidOtpAuthUrl); + //try expect(err == Allocator.Error.OutOfMemory); +} + +test "parse command line parameter: 'list'" { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + + const arg = try parseArgs(allocator, &[_][]const u8{ "path/of/executable", "list" }); + try expect(arg.list); +} + +test "read list of entries" { + try std.fs.cwd().makeDir("test-tmp"); + const file = try std.fs.cwd().createFile("test-tmp/zig-totp", .{ .read = true }); + defer { + file.close(); + std.fs.cwd().deleteTree("test-tmp") catch unreachable; + } + + _ = try file.write("otpauth://totp/token1?secret=c2VjcmV0Cg==\notpauth://totp/token2?secret=c2VjcmV0Cg==\n"); + + const list: std.ArrayList([]const u8) = try executeGetList("test-tmp/zig-totp"); + try std.testing.expectEqualStrings("token1", list.items[0]); + try std.testing.expectEqualStrings("token2", list.items[1]); + try std.testing.expectEqual(2, list.items.len); }