gpower2/src/config.zig

235 lines
7.5 KiB
Zig

const std = @import("std");
const kf = @import("known-folders");
const ini = @import("ini");
const c = @import("ffi.zig").c;
pub var global_config: ?*Config = null;
pub const Config = struct {
shutdown_command: []const []const u8 = &[_][]const u8{ "systemctl", "poweroff" },
reboot_command: []const []const u8 = &[_][]const u8{ "systemctl", "reboot" },
suspend_command: []const []const u8 = &[_][]const u8{ "systemctl", "suspend" },
hibernate_command: []const []const u8 = &[_][]const u8{ "systemctl", "hibernate" },
alloc: std.mem.Allocator,
command_arena: std.heap.ArenaAllocator,
pub fn parse(alloc: std.mem.Allocator) !Config {
var self = Config{
.alloc = alloc,
.command_arena = std.heap.ArenaAllocator.init(alloc),
};
errdefer self.command_arena.deinit();
var config_dir = (kf.open(
alloc,
.roaming_configuration,
.{},
) catch return self) orelse return self;
defer config_dir.close();
const file = config_dir.openFile("gpower2/config.ini", .{}) catch |err| {
std.log.warn("Failed to open config file ({e}). Skipping config!", .{err});
return self;
};
defer file.close();
var parser = ini.parse(std.heap.c_allocator, file.reader());
defer parser.deinit();
var current_section: ?[]const u8 = null;
while (try parser.next()) |rec| {
switch (rec) {
.section => |sec| {
if (current_section) |prev| {
std.heap.c_allocator.free(prev);
}
current_section = try std.heap.c_allocator.dupe(u8, sec);
},
.property => |kv| {
if (current_section == null or
!std.mem.eql(u8, "commands", current_section.?))
{
std.log.err(
\\Config contained property outside of 'commands' section!
,
.{},
);
return error.PropertyOutsideCommandSection;
}
if (std.mem.eql(u8, "shutdown", kv.key)) {
self.shutdown_command = try CommandParser.parse(
kv.value,
self.command_arena.allocator(),
);
} else if (std.mem.eql(u8, "reboot", kv.key)) {
self.reboot_command = try CommandParser.parse(
kv.value,
self.command_arena.allocator(),
);
} else if (std.mem.eql(u8, "suspend", kv.key)) {
self.suspend_command = try CommandParser.parse(
kv.value,
self.command_arena.allocator(),
);
} else if (std.mem.eql(u8, "hibernate", kv.key)) {
self.hibernate_command = try CommandParser.parse(
kv.value,
self.command_arena.allocator(),
);
} else {
std.log.err("Unknown config property '{s}'!", .{kv.key});
return error.UnknownProperty;
}
},
.enumeration => {
std.log.err(
\\Found enumeration in config file!
\\Only sections and properties are allowed!
,
.{},
);
return error.EnumerationInConfig;
},
}
}
return self;
}
pub fn deinit(self: *Config) void {
self.command_arena.deinit();
self.* = undefined;
}
};
const CommandParser = struct {
const State = enum {
default,
single_quote,
double_quote,
};
str: []const u8,
offset: usize = 0,
state: State = .default,
component: std.ArrayList(u8),
/// Parses a command and returns the argv array, allocated using alloc.
fn parse(str: []const u8, alloc: std.mem.Allocator) ![][]u8 {
var parser = init(str, alloc);
defer parser.deinit();
var out = std.ArrayList([]u8).init(alloc);
errdefer out.deinit();
while (try parser.next()) |part| {
try out.append(part);
}
return out.toOwnedSlice();
}
fn init(str: []const u8, alloc: std.mem.Allocator) CommandParser {
return .{
.str = str,
.component = std.ArrayList(u8).init(alloc),
};
}
fn deinit(self: *CommandParser) void {
self.component.deinit();
self.* = undefined;
}
/// Parses the next argv item.
/// The returned slice will be owned by the provided alloc.
/// Returns null if there are no more items left.
fn next(self: *CommandParser) !?[]u8 {
if (self.offset >= self.str.len)
return null;
self.component.clearRetainingCapacity();
while (self.offset < self.str.len) {
const ch = self.str[self.offset];
self.offset += 1;
switch (self.state) {
.default => {
if (std.ascii.isSpace(ch)) {
// double space safety
if (self.component.items.len > 0)
break;
continue;
}
switch (ch) {
'\'' => self.state = .single_quote,
'\"' => self.state = .double_quote,
'\\' => try self.escapeSeq(),
else => try self.component.append(ch),
}
},
.single_quote => switch (ch) {
'\'' => self.state = .default,
'\\' => try self.escapeSeq(),
else => try self.component.append(ch),
},
.double_quote => switch (ch) {
'\"' => self.state = .default,
'\\' => try self.escapeSeq(),
else => try self.component.append(ch),
},
}
}
if (self.state != .default) {
return error.UnclosedDelimeter;
}
return try self.component.allocator.dupe(u8, self.component.items);
}
fn escapeSeq(self: *CommandParser) !void {
if (self.offset >= self.str.len)
return error.UnfinishedEscape;
const ch = self.str[self.offset];
self.offset += 1;
if (self.state == .single_quote) {
if (ch != '\'')
try self.component.append('\\');
try self.component.append(ch);
} else {
try self.component.append(ch);
}
}
};
fn assertParserNext(p: *CommandParser, expected: []const u8) !void {
const n = try p.next();
try std.testing.expect(n != null);
defer p.component.allocator.free(n.?);
try std.testing.expectEqualStrings(expected, n.?);
}
test "CommandParser" {
var p1 = CommandParser.init(
\\"foo\" "bar 'baz\'' \\
,
std.testing.allocator,
);
defer p1.deinit();
try assertParserNext(&p1, "foo\" bar");
try assertParserNext(&p1, "baz'");
try assertParserNext(&p1, "\\");
}