const std = @import("std"); const builtin = @import("builtin"); const mustache = @import("mustache"); const datetime = @import("datetime"); const json = std.json; const Data = @import("Data.zig"); const Datetime = datetime.datetime.Datetime; const Timezone = datetime.timezones; pub const supports_symlinks = switch (builtin.target.os.tag) { .windows, .plan9, .fuchsia, .wasi, .hermit, .driverkit, .zos, .ps3, .ps4, .ps5, .uefi, .rtems, .contiki, .elfiamcu, .freestanding, .other, => false, else => true, }; pub fn relativeSymlink(from: []const u8, to: []const u8, allocator: std.mem.Allocator) !void { const to_dir = std.fs.path.dirname(to) orelse return error.CouldNotGetDirname; const relative_target = try std.fs.path.relative(allocator, to_dir, from); defer allocator.free(relative_target); var dir = try std.fs.openDirAbsolute(to_dir, .{}); defer dir.close(); const to_basename = std.fs.path.basename(to); try dir.symLink(relative_target, to_basename, .{}); } pub fn formatTwoDigitInt(value: u8, arena: std.mem.Allocator) ![]const u8 { return try std.fmt.allocPrint(arena, "{:0>2}", .{value}); } pub fn parseTwoDigitInt(s: []const u8) !u32 { if (s.len != 2) return error.InvalidFormat; return try std.fmt.parseInt(u32, s, 10); } pub fn parseSignedOffset(s: []const u8) !i32 { // Parse timezone offset in format +HH:mm or -HH:mm if (s.len != 6) return error.InvalidFormat; const sign: i32 = switch (s[0]) { '+' => 1, '-' => -1, else => return error.InvalidFormat, }; const hour = try parseTwoDigitInt(s[1..3]); if (s[3] != ':') return error.InvalidFormat; const minute = try parseTwoDigitInt(s[4..6]); return sign * @as(i32, @bitCast((hour * 60) + minute)); } pub fn parseIso8601(input: []const u8) !i64 { // Minimal length: 19 (YYYY-MM-DDTHH:mm:ss) if (input.len < 19) return error.InvalidFormat; const year = try std.fmt.parseInt(u16, input[0..4], 10); if (input[4] != '-') return error.InvalidFormat; const month = try parseTwoDigitInt(input[5..7]); if (input[7] != '-') return error.InvalidFormat; const day = try parseTwoDigitInt(input[8..10]); if (input[10] != 'T' and input[10] != 't') return error.InvalidFormat; const hour = try parseTwoDigitInt(input[11..13]); if (input[13] != ':') return error.InvalidFormat; const minute = try parseTwoDigitInt(input[14..16]); if (input[16] != ':') return error.InvalidFormat; const second = try parseTwoDigitInt(input[17..19]); var offset_minutes: i32 = 0; const rest = input[19..]; if (rest.len == 0) { // No timezone provided, assume local or UTC? Here assume UTC offset_minutes = 0; } else if (rest.len == 1 and rest[0] == 'Z') { offset_minutes = 0; } else if (rest.len == 6) { offset_minutes = try parseSignedOffset(rest); } else { return error.InvalidFormat; } const dt = try Datetime.create( year, month, day, hour, minute, second, 0, Timezone.UTC, ); return @truncate(dt.shiftMinutes(offset_minutes).toTimestamp()); } pub fn template( allocator: std.mem.Allocator, source: []const u8, data: anytype, ) ![]const u8 { if (mustache.allocRenderText( allocator, source, data, )) |output| { if (output.len > 0) return output; allocator.free(output); } else |_| {} return try allocator.dupe(u8, source); } pub inline fn toBytes(num: anytype) []const u8 { return &@as([@sizeOf(@TypeOf(num))]u8, @bitCast(num)); } pub fn compareText(_: void, lhs: []const u8, rhs: []const u8) bool { return std.mem.order(u8, lhs, rhs) == .lt; } fn parseGECOS(user: []const u8, allocator: std.mem.Allocator) ?[]const u8 { var file = std.fs.openFileAbsolute("/etc/passwd", .{}) catch return null; defer file.close(); var buf_reader = std.io.bufferedReader(file.reader()); var in_stream = buf_reader.reader(); var buf: [128]u8 = undefined; while (in_stream.readUntilDelimiterOrEof(&buf, '\n') catch return null) |line| { if (!std.mem.startsWith(u8, line, user)) continue; const s = std.mem.indexOfScalar(u8, line, ':') orelse continue; if (s != user.len) continue; var i: usize = 0; var token_iter = std.mem.splitScalar(u8, line, ':'); while (token_iter.next()) |token| { if (i == 4) { if (token.len == 0) return null; return allocator.dupe(u8, token) catch null; } i = i + 1; } } return null; } const fallback = "Mx. Makko"; pub fn getFullUserName(allocator: std.mem.Allocator) ![]const u8 { var envmap = try std.process.getEnvMap(allocator); defer envmap.deinit(); if (envmap.get("USER")) |user| { if (parseGECOS(user, allocator)) |full_name| return full_name; return try allocator.dupe(u8, user); } // Make sure that the caller owns the memory! return try allocator.dupe(u8, fallback); } pub fn exists(dir: std.fs.Dir, path: []const u8) bool { _ = dir.access(path, .{}) catch return false; return true; } pub fn isDir(dir: std.fs.Dir, path: []const u8) bool { var f = dir.openDir(path, .{}) catch return false; f.close(); return true; } pub fn extractFrontmatter(text: []const u8) ?[]const u8 { var start_pos: usize = 0; var end_pos: usize = 0; var in_frontmatter = false; var lines = std.mem.tokenizeScalar(u8, text, '\n'); var cursor: usize = 0; while (lines.next()) |line| { const trimmed = std.mem.trim(u8, line, " \t\r"); const line_start = cursor; cursor += line.len + 1; // +1 for '\n' if (!in_frontmatter) { if (std.mem.eql(u8, trimmed[0..3], "---")) { in_frontmatter = true; start_pos = cursor; } else if (trimmed.len != 0) return null; } else { if (std.mem.eql(u8, trimmed[0..3], "---")) { end_pos = line_start; return text[start_pos..end_pos]; } } } return null; } const hash_seed = 0x12345678; // don't you dare change this. const Hasher = std.hash.Wyhash; pub fn hashValue( hasher: *Hasher, obj: *std.json.Value, ) void { switch (obj.*) { .null => hasher.update("nil\x00"), .bool => |b| hasher.update(if (b) "tru\x00" else "fal\x00"), .float => |f| { hasher.update("flt\x00"); hasher.update(toBytes(f)); }, .integer => |i| { hasher.update("int\x00"); hasher.update(toBytes(i)); }, .string, .number_string => |s| { hasher.update("str\x00"); hasher.update(s); }, .array => |a| { hasher.update("arr\x00"); for (a.items) |*element| hashValue(hasher, element); hasher.update("arr\x01"); }, .object => |*o| { hasher.update("obj\x00"); const keys = o.keys(); const C = struct { keys: [][]const u8, pub fn lessThan(ctx: @This(), a: usize, b: usize) bool { return compareText({}, ctx.keys[a], ctx.keys[b]); } }; o.sort(C{ .keys = keys }); for (o.keys()) |name| { hasher.update("prp\x00"); hasher.update(name); const n = o.getPtr(name).?; hashValue(hasher, n); hasher.update("prp\x01"); } hasher.update("obj\x01"); }, } } pub fn hashJsonMap(map: std.json.ObjectMap) Data.Hash { var hasher = Hasher.init(hash_seed); var value: std.json.Value = .{ .object = map }; hashValue(&hasher, &value); const h: Data.Hash = @bitCast(hasher.final()); return h; } pub fn hashString(data: []const u8) Data.Hash { var hasher = Hasher.init(hash_seed); hasher.update(data); const h: Data.Hash = @bitCast(hasher.final()); return h; } pub fn hash(data: anytype) Data.Hash { return hashString(toBytes(data)); } // todo: implement this properly (it's very very very wasteful.) pub fn valueToJson( arena: std.mem.Allocator, value: anytype, ) !std.json.Parsed(json.Value) { const j = try std.json.stringifyAlloc( arena, value, .{ .whitespace = .minified }, ); defer arena.free(j); return try std.json.parseFromSlice( json.Value, arena, j, .{ .allocate = .alloc_always }, ); } const base_encoder = std.base64.Base64Encoder; const base_decoder = std.base64.Base64Decoder; const base_alphabet = std.base64.url_safe_alphabet_chars; pub fn i64ToBase64(i: i64, allocator: std.mem.Allocator) ![]u8 { var buffer: [@sizeOf(i64)]u8 = undefined; var dest: [32]u8 = undefined; std.mem.writeInt(i64, &buffer, i, .big); const encoder = base_encoder.init(base_alphabet, null); return try allocator.dupe(u8, encoder.encode(&dest, &buffer)); } pub fn base64ToI64(b64: []const u8) !i64 { const decoder = base_decoder.init(base_alphabet, null); var dest: [@sizeOf(i64)]u8 = undefined; try decoder.decode(&dest, b64); return std.mem.readInt(i64, &dest, .big); } pub fn isValidBase64(s: []const u8) bool { str: for (s) |c| { for (base_alphabet) |valid_char| if (c == valid_char) continue :str; return false; } return true; } pub fn toKebabWords(allocator: std.mem.Allocator, input: []const u8) ![]const []const u8 { var words = std.ArrayList([]const u8).init(allocator); var it = std.mem.splitAny(u8, input, " \t"); while (it.next()) |word| { const kebab = try toKebabCase(allocator, word); if (kebab.len == 0) { allocator.free(kebab); continue; } try words.append(kebab); } return try words.toOwnedSlice(); } // todo: handle utf8 fn toKebabCase(allocator: std.mem.Allocator, input: []const u8) ![]const u8 { var buf = std.ArrayList(u8).init(allocator); var i: usize = 0; while (i < input.len) { const c = input[i]; if (c == '_') { try appendDashIfNeeded(&buf); i += 1; continue; } if (std.ascii.isDigit(c)) { try appendDashIfNeeded(&buf); try buf.append(c); i += 1; continue; } if (std.ascii.isUpper(c)) { const next = if (i + 1 < input.len) input[i + 1] else 0; const is_acronym = (i + 1 < input.len and std.ascii.isUpper(next)) or (i + 1 == input.len); try appendDashIfNeeded(&buf); try buf.append(std.ascii.toLower(c)); i += 1; if (is_acronym) { while (i < input.len and std.ascii.isUpper(input[i])) { try buf.append(std.ascii.toLower(input[i])); i += 1; } } continue; } try buf.append(c); i += 1; } return buf.toOwnedSlice(); } fn appendDashIfNeeded(buf: *std.ArrayList(u8)) !void { if (buf.items.len > 0 and buf.items[buf.items.len - 1] != '-') { try buf.append('-'); } } pub fn freeStringList( allocator: std.mem.Allocator, list: []const []const u8, ) void { defer allocator.free(list); for (list) |str| allocator.free(str); } pub fn copyStringList( allocator: std.mem.Allocator, list: []const []const u8, ) ![]const []const u8 { const copy = try allocator.alloc([]const u8, list.len); for (list, 0..) |str, i| copy[i] = try allocator.dupe(u8, str); return copy; } pub fn fold64to32(h: u64) u32 { var k = h; k ^= k >> 33; k *= 0xff51afd7ed558ccd; k ^= k >> 33; k *= 0xc4ceb9fe1a85ec53; k ^= k >> 33; return @intCast(k ^ (k >> 32)); }