lib/hypothesis/src/artifact.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 
  3 const Sha256 = std.crypto.hash.sha2.Sha256;
  4 const fs_io = std.Options.debug_io;
  5 const magic = "JHMA";
  6 const format_version: u16 = 1;
  7 const header_bytes: usize = 48;
  8 
  9 pub const Metadata = struct {
 10     schema: []const u8,
 11     revision: []const u8,
 12 };
 13 
 14 pub const Entry = struct {
 15     metadata: Metadata,
 16     payload: []const u8,
 17 };
 18 
 19 pub fn encodedSize(
 20     metadata: Metadata,
 21     payload_bytes: usize,
 22 ) error{ArtifactTooLarge}!usize {
 23     if (metadata.schema.len == 0) return error.ArtifactTooLarge;
 24     if (metadata.revision.len == 0) return error.ArtifactTooLarge;
 25     if (metadata.schema.len > std.math.maxInt(u16)) return error.ArtifactTooLarge;
 26     if (metadata.revision.len > std.math.maxInt(u16)) return error.ArtifactTooLarge;
 27     if (payload_bytes > std.math.maxInt(u32)) return error.ArtifactTooLarge;
 28 
 29     const with_schema = std.math.add(usize, header_bytes, metadata.schema.len) catch
 30         return error.ArtifactTooLarge;
 31     const with_revision = std.math.add(usize, with_schema, metadata.revision.len) catch
 32         return error.ArtifactTooLarge;
 33     return std.math.add(usize, with_revision, payload_bytes) catch
 34         return error.ArtifactTooLarge;
 35 }
 36 
 37 pub fn save(path: []const u8, metadata: Metadata, payload: []const u8) !void {
 38     std.debug.assert(path.len > 0);
 39     std.debug.assert(metadata.schema.len > 0);
 40     std.debug.assert(metadata.revision.len > 0);
 41     _ = try encodedSize(metadata, payload.len);
 42 
 43     if (std.fs.path.dirname(path)) |parent| {
 44         try std.Io.Dir.cwd().createDirPath(fs_io, parent);
 45     }
 46     var header: [header_bytes]u8 = @splat(0);
 47     @memcpy(header[0..magic.len], magic);
 48     std.mem.writeInt(u16, header[4..6], format_version, .little);
 49     std.mem.writeInt(u16, header[8..10], @intCast(metadata.schema.len), .little);
 50     std.mem.writeInt(u16, header[10..12], @intCast(metadata.revision.len), .little);
 51     std.mem.writeInt(u32, header[12..16], @intCast(payload.len), .little);
 52     contentDigest(header[0..16], metadata, payload, header[16..48]);
 53 
 54     var atomic_file = try std.Io.Dir.cwd().createFileAtomic(fs_io, path, .{ .replace = true });
 55     defer atomic_file.deinit(fs_io);
 56     var offset: u64 = 0;
 57     try atomic_file.file.writePositionalAll(fs_io, &header, offset);
 58     offset += header.len;
 59     try atomic_file.file.writePositionalAll(fs_io, metadata.schema, offset);
 60     offset += metadata.schema.len;
 61     try atomic_file.file.writePositionalAll(fs_io, metadata.revision, offset);
 62     offset += metadata.revision.len;
 63     try atomic_file.file.writePositionalAll(fs_io, payload, offset);
 64     try atomic_file.replace(fs_io);
 65 }
 66 
 67 pub fn load(path: []const u8, storage: []u8) !?Entry {
 68     std.debug.assert(path.len > 0);
 69     var file = std.Io.Dir.cwd().openFile(fs_io, path, .{ .allow_directory = false }) catch |err| {
 70         if (err == error.FileNotFound) return null;
 71         return err;
 72     };
 73     defer file.close(fs_io);
 74     const stat = try file.stat(fs_io);
 75     if (stat.kind != .file) return error.InvalidArtifact;
 76     const file_len = std.math.cast(usize, stat.size) orelse return error.ArtifactTooLarge;
 77     if (file_len > storage.len) return error.ArtifactTooLarge;
 78     const data = storage[0..file_len];
 79     if (try file.readPositionalAll(fs_io, data, 0) != file_len) return error.InvalidArtifact;
 80     var trailing: [1]u8 = undefined;
 81     if (try file.readPositionalAll(fs_io, &trailing, file_len) != 0) {
 82         return error.InvalidArtifact;
 83     }
 84     return try decode(data);
 85 }
 86 
 87 fn decode(data: []const u8) !Entry {
 88     if (data.len < header_bytes) return error.InvalidArtifact;
 89     if (!std.mem.eql(u8, data[0..magic.len], magic)) return error.InvalidArtifact;
 90     if (std.mem.readInt(u16, data[4..6], .little) != format_version) {
 91         return error.UnsupportedArtifactVersion;
 92     }
 93     if (std.mem.readInt(u16, data[6..8], .little) != 0) return error.InvalidArtifact;
 94     const schema_len: usize = std.mem.readInt(u16, data[8..10], .little);
 95     const revision_len: usize = std.mem.readInt(u16, data[10..12], .little);
 96     const payload_len: usize = std.mem.readInt(u32, data[12..16], .little);
 97     const metadata_len = std.math.add(usize, schema_len, revision_len) catch
 98         return error.InvalidArtifact;
 99     const body_len = std.math.add(usize, metadata_len, payload_len) catch
100         return error.InvalidArtifact;
101     const expected_len = std.math.add(usize, header_bytes, body_len) catch
102         return error.InvalidArtifact;
103     if (data.len != expected_len) return error.InvalidArtifact;
104     if (schema_len == 0 or revision_len == 0) return error.InvalidArtifact;
105 
106     const schema = data[header_bytes..][0..schema_len];
107     const revision = data[header_bytes + schema_len ..][0..revision_len];
108     const payload = data[header_bytes + metadata_len ..][0..payload_len];
109     var digest: [Sha256.digest_length]u8 = undefined;
110     contentDigest(
111         data[0..16],
112         .{ .schema = schema, .revision = revision },
113         payload,
114         &digest,
115     );
116     if (!std.crypto.timing_safe.eql([Sha256.digest_length]u8, digest, data[16..48].*)) {
117         return error.InvalidArtifact;
118     }
119     return .{
120         .metadata = .{ .schema = schema, .revision = revision },
121         .payload = payload,
122     };
123 }
124 
125 fn contentDigest(
126     header: []const u8,
127     metadata: Metadata,
128     payload: []const u8,
129     output: *[Sha256.digest_length]u8,
130 ) void {
131     var hasher = Sha256.init(.{});
132     hasher.update(header);
133     hasher.update(metadata.schema);
134     hasher.update(metadata.revision);
135     hasher.update(payload);
136     hasher.final(output);
137 }
138 
139 test "artifact: materialized input retains schema revision and bytes" {
140     var tmp = std.testing.tmpDir(.{});
141     defer tmp.cleanup();
142     const dir_path = try tmp.dir.realPathFileAlloc(fs_io, ".", std.testing.allocator);
143     defer std.testing.allocator.free(dir_path);
144     var path_buffer: [std.fs.max_path_bytes]u8 = undefined;
145     const path = try std.fmt.bufPrint(&path_buffer, "{s}/case.jhma", .{dir_path});
146     const metadata = Metadata{
147         .schema = "example.state-machine.v1",
148         .revision = "generator-before-change",
149     };
150     try save(path, metadata, &.{ 3, 1, 4, 1, 5 });
151 
152     var storage: [256]u8 = undefined;
153     try std.testing.expectEqual(
154         header_bytes + metadata.schema.len + metadata.revision.len + 5,
155         try encodedSize(metadata, 5),
156     );
157     const loaded = (try load(path, &storage)).?;
158     try std.testing.expectEqualStrings(metadata.schema, loaded.metadata.schema);
159     try std.testing.expectEqualStrings(metadata.revision, loaded.metadata.revision);
160     try std.testing.expectEqualSlices(u8, &.{ 3, 1, 4, 1, 5 }, loaded.payload);
161 }
162 
163 test "artifact: payload corruption is rejected" {
164     var tmp = std.testing.tmpDir(.{});
165     defer tmp.cleanup();
166     const dir_path = try tmp.dir.realPathFileAlloc(fs_io, ".", std.testing.allocator);
167     defer std.testing.allocator.free(dir_path);
168     var path_buffer: [std.fs.max_path_bytes]u8 = undefined;
169     const path = try std.fmt.bufPrint(&path_buffer, "{s}/case.jhma", .{dir_path});
170     try save(path, .{ .schema = "example.v1", .revision = "one" }, "payload");
171     var file = try std.Io.Dir.cwd().openFile(fs_io, path, .{ .mode = .read_write });
172     defer file.close(fs_io);
173     try file.writePositionalAll(fs_io, "X", header_bytes + "example.v1".len + "one".len);
174 
175     var storage: [256]u8 = undefined;
176     try std.testing.expectError(error.InvalidArtifact, load(path, &storage));
177 }