const std = @import("std"); const ignore = @import("ignore.zig"); 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); errdefer allocator.free(out); 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 SHA256 hash of a file pub fn hashFile(allocator: std.mem.Allocator, file_path: []const u8) ![]u8 { var hasher = std.crypto.hash.sha2.Sha256.init(.{}); const file = try std.fs.cwd().openFile(file_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]); } var hash: [32]u8 = undefined; hasher.final(&hash); return encodeHexLower(allocator, &hash); } /// Calculate combined hash of multiple files (sorted by path) pub fn hashFiles(allocator: std.mem.Allocator, dir_path: []const u8, file_paths: []const []const u8) ![]u8 { var hasher = std.crypto.hash.sha2.Sha256.init(.{}); // Copy and sort paths for deterministic hashing var sorted_paths = std.ArrayList([]const u8).initCapacity(allocator, file_paths.len) catch |err| { return err; }; defer sorted_paths.deinit(allocator); for (file_paths) |path| { try sorted_paths.append(allocator, path); } std.sort.block([]const u8, sorted_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 for (sorted_paths.items) |path| { hasher.update(path); hasher.update(&[_]u8{0}); // Separator const full_path = try std.fs.path.join(allocator, &[_][]const u8{ dir_path, path }); defer allocator.free(full_path); const file_hash = try hashFile(allocator, full_path); defer allocator.free(file_hash); hasher.update(file_hash); hasher.update(&[_]u8{0}); // Separator } var hash: [32]u8 = undefined; hasher.final(&hash); return encodeHexLower(allocator, &hash); } /// Calculate commit ID for a directory (SHA256 of tree state, respecting .gitignore) 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(); // Load .gitignore and .mlignore patterns var gitignore = ignore.GitIgnore.init(allocator); defer gitignore.deinit(); try gitignore.loadFromDir(dir_path, ".gitignore"); try gitignore.loadFromDir(dir_path, ".mlignore"); var walker = try dir.walk(allocator); defer walker.deinit(allocator); // 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) { // Skip files matching default ignores if (ignore.matchesDefaultIgnore(entry.path)) continue; // Skip files matching .gitignore/.mlignore patterns if (gitignore.isIgnored(entry.path, false)) continue; 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")); }