fetch_ml/cli/src/utils/crypto.zig

154 lines
5 KiB
Zig

const std = @import("std");
pub fn encodeHexLower(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
const hex = try allocator.alloc(u8, bytes.len * 2);
for (bytes, 0..) |byte, i| {
const hi: u8 = (byte >> 4) & 0xf;
const lo: u8 = byte & 0xf;
hex[i * 2] = if (hi < 10) '0' + hi else 'a' + (hi - 10);
hex[i * 2 + 1] = if (lo < 10) '0' + lo else 'a' + (lo - 10);
}
return hex;
}
fn hexNibble(c: u8) ?u8 {
return if (c >= '0' and c <= '9') c - '0' else if (c >= 'a' and c <= 'f') c - 'a' + 10 else if (c >= 'A' and c <= 'F') c - 'A' + 10 else null;
}
pub fn decodeHex(allocator: std.mem.Allocator, hex: []const u8) ![]u8 {
if ((hex.len % 2) != 0) return error.InvalidHex;
const out = try allocator.alloc(u8, hex.len / 2);
var i: usize = 0;
while (i < out.len) : (i += 1) {
const hi = hexNibble(hex[i * 2]) orelse return error.InvalidHex;
const lo = hexNibble(hex[i * 2 + 1]) orelse return error.InvalidHex;
out[i] = (hi << 4) | lo;
}
return out;
}
/// Hash a string using SHA256 and return lowercase hex string
pub fn hashString(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
var hash: [32]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(input, &hash, .{});
return encodeHexLower(allocator, &hash);
}
/// Hash an API key using SHA256 and return first 16 bytes (binary)
pub fn hashApiKey(allocator: std.mem.Allocator, api_key: []const u8) ![]u8 {
var hash: [32]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(api_key, &hash, .{});
// Return first 16 bytes
const result = try allocator.alloc(u8, 16);
@memcpy(result, hash[0..16]);
return result;
}
/// Calculate commit ID for a directory (SHA256 of tree state)
pub fn hashDirectory(allocator: std.mem.Allocator, dir_path: []const u8) ![]u8 {
var hasher = std.crypto.hash.sha2.Sha256.init(.{});
var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true });
defer dir.close();
var walker = try dir.walk(allocator);
defer walker.deinit();
// Collect and sort paths for deterministic hashing
var paths: std.ArrayList([]const u8) = .{};
defer {
for (paths.items) |path| allocator.free(path);
paths.deinit(allocator);
}
while (try walker.next()) |entry| {
if (entry.kind == .file) {
try paths.append(allocator, try allocator.dupe(u8, entry.path));
}
}
std.sort.block([]const u8, paths.items, {}, struct {
fn lessThan(_: void, a: []const u8, b: []const u8) bool {
return std.mem.order(u8, a, b) == .lt;
}
}.lessThan);
// Hash each file path and content
for (paths.items) |path| {
hasher.update(path);
hasher.update(&[_]u8{0}); // Separator
const file = try dir.openFile(path, .{});
defer file.close();
var buf: [4096]u8 = undefined;
while (true) {
const bytes_read = try file.read(&buf);
if (bytes_read == 0) break;
hasher.update(buf[0..bytes_read]);
}
hasher.update(&[_]u8{0}); // Separator
}
var hash: [32]u8 = undefined;
hasher.final(&hash);
return encodeHexLower(allocator, &hash);
}
test "hash string" {
const allocator = std.testing.allocator;
const hash = try hashString(allocator, "test");
defer allocator.free(hash);
// SHA256 of "test"
try std.testing.expectEqualStrings("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", hash);
}
test "hash empty string" {
const allocator = std.testing.allocator;
const hash = try hashString(allocator, "");
defer allocator.free(hash);
// SHA256 of empty string
try std.testing.expectEqualStrings("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", hash);
}
test "hash directory" {
const allocator = std.testing.allocator;
// For now, just test that we can hash the current directory
// This should work if there are files in the current directory
const hash = try hashDirectory(allocator, ".");
defer allocator.free(hash);
// Should produce a valid 64-character hex string
try std.testing.expectEqual(@as(usize, 64), hash.len);
// All characters should be valid hex
for (hash) |c| {
try std.testing.expect((c >= '0' and c <= '9') or (c >= 'a' and c <= 'f'));
}
}
test "hex encode/decode roundtrip" {
const allocator = std.testing.allocator;
const bytes = [_]u8{ 0x00, 0x01, 0x7f, 0x80, 0xfe, 0xff };
const enc = try encodeHexLower(allocator, &bytes);
defer allocator.free(enc);
try std.testing.expectEqualStrings("00017f80feff", enc);
const dec = try decodeHex(allocator, enc);
defer allocator.free(dec);
try std.testing.expectEqualSlices(u8, &bytes, dec);
}
test "hex decode rejects invalid" {
const allocator = std.testing.allocator;
try std.testing.expectError(error.InvalidHex, decodeHex(allocator, "0"));
try std.testing.expectError(error.InvalidHex, decodeHex(allocator, "zz"));
}