154 lines
5 KiB
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"));
|
|
}
|