const std = @import("std"); /// base64 encode a byte slice pub fn base64Encode(alloc: std.mem.Allocator, data: []const u8) ![]const u8 { const encoder = std.base64.standard.Encoder; const size = encoder.calcSize(data.len); const buf = try alloc.alloc(u8, size); return encoder.encode(buf, data); } /// base64 decode a string pub fn base64Decode(alloc: std.mem.Allocator, encoded: []const u8) ![]const u8 { const decoder = std.base64.standard.Decoder; // upper bound: 3 bytes per 4 chars const max_size = (encoded.len / 4 + 1) * 3; const buf = try alloc.alloc(u8, max_size); decoder.decode(buf, encoded) catch return error.InvalidBase64; // calculate actual decoded length (may be less due to padding) const actual_len = encoded.len / 4 * 3 - countPadding(encoded); return buf[0..actual_len]; } fn countPadding(encoded: []const u8) usize { var padding: usize = 0; if (encoded.len > 0 and encoded[encoded.len - 1] == '=') padding += 1; if (encoded.len > 1 and encoded[encoded.len - 2] == '=') padding += 1; return padding; } /// URL decode a percent-encoded string (handles %XX and + for space) pub fn urlDecode(alloc: std.mem.Allocator, encoded: []const u8) ![]const u8 { var result = std.ArrayListUnmanaged(u8){}; var i: usize = 0; while (i < encoded.len) { if (encoded[i] == '%' and i + 2 < encoded.len) { const hex = encoded[i + 1 .. i + 3]; const byte = std.fmt.parseInt(u8, hex, 16) catch { try result.append(alloc, encoded[i]); i += 1; continue; }; try result.append(alloc, byte); i += 3; } else if (encoded[i] == '+') { try result.append(alloc, ' '); i += 1; } else { try result.append(alloc, encoded[i]); i += 1; } } return result.toOwnedSlice(alloc); }