| 1 | const Manifest = @This(); |
| 2 | |
| 3 | const std = @import("std"); |
| 4 | const Io = std.Io; |
| 5 | const mem = std.mem; |
| 6 | const Allocator = std.mem.Allocator; |
| 7 | const assert = std.debug.assert; |
| 8 | const Ast = std.zig.Ast; |
| 9 | const testing = std.testing; |
| 10 | |
| 11 | const Package = @import("../Package.zig"); |
| 12 | |
| 13 | pub const max_bytes = 10 * 1024 * 1024; |
| 14 | pub const basename = "build.zig.zon"; |
| 15 | pub const max_name_len = 32; |
| 16 | pub const max_version_len = 32; |
| 17 | |
| 18 | pub const Dependency = struct { |
| 19 | location: Location, |
| 20 | location_tok: Ast.TokenIndex, |
| 21 | location_node: Ast.Node.Index, |
| 22 | hash: ?[]const u8, |
| 23 | hash_tok: Ast.OptionalTokenIndex, |
| 24 | hash_node: Ast.Node.OptionalIndex, |
| 25 | node: Ast.Node.Index, |
| 26 | name_tok: Ast.TokenIndex, |
| 27 | lazy: bool, |
| 28 | |
| 29 | pub const Location = union(enum) { |
| 30 | url: []const u8, |
| 31 | path: []const u8, |
| 32 | }; |
| 33 | }; |
| 34 | |
| 35 | pub const ErrorMessage = struct { |
| 36 | msg: []const u8, |
| 37 | tok: Ast.TokenIndex, |
| 38 | off: u32, |
| 39 | }; |
| 40 | |
| 41 | name: []const u8, |
| 42 | id: u32, |
| 43 | version: std.SemanticVersion, |
| 44 | version_node: Ast.Node.Index, |
| 45 | dependencies: std.array_hash_map.String(Dependency), |
| 46 | dependencies_node: Ast.Node.OptionalIndex, |
| 47 | paths: std.array_hash_map.String(void), |
| 48 | minimum_zig_version: ?std.SemanticVersion, |
| 49 | |
| 50 | errors: []ErrorMessage, |
| 51 | arena_state: std.heap.ArenaAllocator.State, |
| 52 | |
| 53 | pub const ParseOptions = struct { |
| 54 | allow_missing_paths_field: bool = false, |
| 55 | }; |
| 56 | |
| 57 | pub const Error = Allocator.Error; |
| 58 | |
| 59 | pub fn parse(gpa: Allocator, ast: *const Ast, rng: std.Random, options: ParseOptions) Error!Manifest { |
| 60 | const main_node_index = ast.nodeData(.root).node; |
| 61 | |
| 62 | var arena_instance = std.heap.ArenaAllocator.init(gpa); |
| 63 | errdefer arena_instance.deinit(); |
| 64 | |
| 65 | var p: Parse = .{ |
| 66 | .gpa = gpa, |
| 67 | .ast = ast.*, |
| 68 | .arena = arena_instance.allocator(), |
| 69 | .errors = .empty, |
| 70 | |
| 71 | .name = undefined, |
| 72 | .id = 0, |
| 73 | .version = undefined, |
| 74 | .version_node = undefined, |
| 75 | .dependencies = .{}, |
| 76 | .dependencies_node = .none, |
| 77 | .paths = .empty, |
| 78 | .allow_missing_paths_field = options.allow_missing_paths_field, |
| 79 | .minimum_zig_version = null, |
| 80 | .buf = .empty, |
| 81 | }; |
| 82 | defer p.buf.deinit(gpa); |
| 83 | defer p.errors.deinit(gpa); |
| 84 | defer p.dependencies.deinit(gpa); |
| 85 | defer p.paths.deinit(gpa); |
| 86 | |
| 87 | p.parseRoot(main_node_index, rng) catch |err| switch (err) { |
| 88 | error.ParseFailure => assert(p.errors.items.len > 0), |
| 89 | else => |e| return e, |
| 90 | }; |
| 91 | |
| 92 | return .{ |
| 93 | .name = p.name, |
| 94 | .id = p.id, |
| 95 | .version = p.version, |
| 96 | .version_node = p.version_node, |
| 97 | .dependencies = try p.dependencies.clone(p.arena), |
| 98 | .dependencies_node = p.dependencies_node, |
| 99 | .paths = try p.paths.clone(p.arena), |
| 100 | .minimum_zig_version = p.minimum_zig_version, |
| 101 | .errors = try p.arena.dupe(ErrorMessage, p.errors.items), |
| 102 | .arena_state = arena_instance.state, |
| 103 | }; |
| 104 | } |
| 105 | |
| 106 | pub fn deinit(man: *Manifest, gpa: Allocator) void { |
| 107 | man.arena_state.promote(gpa).deinit(); |
| 108 | man.* = undefined; |
| 109 | } |
| 110 | |
| 111 | pub fn copyErrorsIntoBundle( |
| 112 | man: Manifest, |
| 113 | ast: Ast, |
| 114 | /// ErrorBundle null-terminated string index |
| 115 | src_path: u32, |
| 116 | eb: *std.zig.ErrorBundle.Wip, |
| 117 | ) Allocator.Error!void { |
| 118 | for (man.errors) |msg| { |
| 119 | const start_loc = ast.tokenLocation(0, msg.tok); |
| 120 | |
| 121 | try eb.addRootErrorMessage(.{ |
| 122 | .msg = try eb.addString(msg.msg), |
| 123 | .src_loc = try eb.addSourceLocation(.{ |
| 124 | .src_path = src_path, |
| 125 | .span_start = ast.tokenStart(msg.tok), |
| 126 | .span_end = @intCast(ast.tokenStart(msg.tok) + ast.tokenSlice(msg.tok).len), |
| 127 | .span_main = ast.tokenStart(msg.tok) + msg.off, |
| 128 | .line = @intCast(start_loc.line), |
| 129 | .column = @intCast(start_loc.column), |
| 130 | .source_line = try eb.addString(ast.source[start_loc.line_start..start_loc.line_end]), |
| 131 | }), |
| 132 | }); |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | const Parse = struct { |
| 137 | gpa: Allocator, |
| 138 | ast: Ast, |
| 139 | arena: Allocator, |
| 140 | buf: std.ArrayList(u8), |
| 141 | errors: std.ArrayList(ErrorMessage), |
| 142 | |
| 143 | name: []const u8, |
| 144 | id: u32, |
| 145 | version: std.SemanticVersion, |
| 146 | version_node: Ast.Node.Index, |
| 147 | dependencies: std.array_hash_map.String(Dependency), |
| 148 | dependencies_node: Ast.Node.OptionalIndex, |
| 149 | paths: std.array_hash_map.String(void), |
| 150 | allow_missing_paths_field: bool, |
| 151 | minimum_zig_version: ?std.SemanticVersion, |
| 152 | |
| 153 | const InnerError = error{ ParseFailure, OutOfMemory }; |
| 154 | |
| 155 | fn parseRoot(p: *Parse, node: Ast.Node.Index, rng: std.Random) !void { |
| 156 | const ast = p.ast; |
| 157 | const main_token = ast.nodeMainToken(node); |
| 158 | |
| 159 | var buf: [2]Ast.Node.Index = undefined; |
| 160 | const struct_init = ast.fullStructInit(&buf, node) orelse { |
| 161 | return fail(p, main_token, "expected top level expression to be a struct", .{}); |
| 162 | }; |
| 163 | |
| 164 | var have_name = false; |
| 165 | var have_version = false; |
| 166 | var have_included_paths = false; |
| 167 | var fingerprint: ?Package.Fingerprint = null; |
| 168 | |
| 169 | for (struct_init.ast.fields) |field_init| { |
| 170 | const name_token = ast.firstToken(field_init) - 2; |
| 171 | const field_name = try identifierTokenString(p, name_token); |
| 172 | // We could get fancy with reflection and comptime logic here but doing |
| 173 | // things manually provides an opportunity to do any additional verification |
| 174 | // that is desirable on a per-field basis. |
| 175 | if (mem.eql(u8, field_name, "dependencies")) { |
| 176 | p.dependencies_node = field_init.toOptional(); |
| 177 | try parseDependencies(p, field_init); |
| 178 | } else if (mem.eql(u8, field_name, "paths")) { |
| 179 | have_included_paths = true; |
| 180 | try parseIncludedPaths(p, field_init); |
| 181 | } else if (mem.eql(u8, field_name, "name")) { |
| 182 | p.name = try parseName(p, field_init); |
| 183 | have_name = true; |
| 184 | } else if (mem.eql(u8, field_name, "fingerprint")) { |
| 185 | fingerprint = try parseFingerprint(p, field_init); |
| 186 | } else if (mem.eql(u8, field_name, "version")) { |
| 187 | p.version_node = field_init; |
| 188 | const version_text = try parseString(p, field_init); |
| 189 | if (version_text.len > max_version_len) { |
| 190 | try appendError(p, ast.nodeMainToken(field_init), "version string length {d} exceeds maximum of {d}", .{ version_text.len, max_version_len }); |
| 191 | } |
| 192 | p.version = std.SemanticVersion.parse(version_text) catch |err| v: { |
| 193 | try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)}); |
| 194 | break :v undefined; |
| 195 | }; |
| 196 | have_version = true; |
| 197 | } else if (mem.eql(u8, field_name, "minimum_zig_version")) { |
| 198 | const version_text = try parseString(p, field_init); |
| 199 | p.minimum_zig_version = std.SemanticVersion.parse(version_text) catch |err| v: { |
| 200 | try appendError(p, ast.nodeMainToken(field_init), "unable to parse semantic version: {s}", .{@errorName(err)}); |
| 201 | break :v null; |
| 202 | }; |
| 203 | } else { |
| 204 | // Ignore unknown fields so that we can add fields in future zig |
| 205 | // versions without breaking older zig versions. |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | if (!have_name) { |
| 210 | try appendError(p, main_token, "missing top-level 'name' field", .{}); |
| 211 | } else { |
| 212 | if (fingerprint) |n| { |
| 213 | if (!n.validate(p.name)) { |
| 214 | return fail(p, main_token, "invalid fingerprint: 0x{x}; if this is a new or forked package, use this value: 0x{x}", .{ |
| 215 | n.int(), Package.Fingerprint.generate(rng, p.name).int(), |
| 216 | }); |
| 217 | } |
| 218 | p.id = n.id; |
| 219 | } else { |
| 220 | try appendError(p, main_token, "missing top-level 'fingerprint' field; suggested value: 0x{x}", .{ |
| 221 | Package.Fingerprint.generate(rng, p.name).int(), |
| 222 | }); |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | if (!have_version) { |
| 227 | try appendError(p, main_token, "missing top-level 'version' field", .{}); |
| 228 | } |
| 229 | |
| 230 | if (!have_included_paths) { |
| 231 | if (p.allow_missing_paths_field) { |
| 232 | try p.paths.put(p.gpa, "", {}); |
| 233 | } else { |
| 234 | try appendError(p, main_token, "missing top-level 'paths' field", .{}); |
| 235 | } |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | fn parseDependencies(p: *Parse, node: Ast.Node.Index) !void { |
| 240 | const ast = p.ast; |
| 241 | |
| 242 | var buf: [2]Ast.Node.Index = undefined; |
| 243 | const struct_init = ast.fullStructInit(&buf, node) orelse { |
| 244 | const tok = ast.nodeMainToken(node); |
| 245 | return fail(p, tok, "expected dependencies expression to be a struct", .{}); |
| 246 | }; |
| 247 | |
| 248 | for (struct_init.ast.fields) |field_init| { |
| 249 | const name_token = ast.firstToken(field_init) - 2; |
| 250 | const dep_name = try identifierTokenString(p, name_token); |
| 251 | const dep = try parseDependency(p, field_init); |
| 252 | try p.dependencies.put(p.gpa, dep_name, dep); |
| 253 | } |
| 254 | } |
| 255 | |
| 256 | fn parseDependency(p: *Parse, node: Ast.Node.Index) !Dependency { |
| 257 | const ast = p.ast; |
| 258 | |
| 259 | var buf: [2]Ast.Node.Index = undefined; |
| 260 | const struct_init = ast.fullStructInit(&buf, node) orelse { |
| 261 | const tok = ast.nodeMainToken(node); |
| 262 | return fail(p, tok, "expected dependency expression to be a struct", .{}); |
| 263 | }; |
| 264 | |
| 265 | var dep: Dependency = .{ |
| 266 | .location = undefined, |
| 267 | .location_tok = undefined, |
| 268 | .location_node = undefined, |
| 269 | .hash = null, |
| 270 | .hash_tok = .none, |
| 271 | .hash_node = .none, |
| 272 | .node = node, |
| 273 | .name_tok = undefined, |
| 274 | .lazy = false, |
| 275 | }; |
| 276 | var has_location = false; |
| 277 | |
| 278 | for (struct_init.ast.fields) |field_init| { |
| 279 | const name_token = ast.firstToken(field_init) - 2; |
| 280 | dep.name_tok = name_token; |
| 281 | const field_name = try identifierTokenString(p, name_token); |
| 282 | // We could get fancy with reflection and comptime logic here but doing |
| 283 | // things manually provides an opportunity to do any additional verification |
| 284 | // that is desirable on a per-field basis. |
| 285 | if (mem.eql(u8, field_name, "url")) { |
| 286 | if (has_location) { |
| 287 | return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{}); |
| 288 | } |
| 289 | dep.location = .{ |
| 290 | .url = parseString(p, field_init) catch |err| switch (err) { |
| 291 | error.ParseFailure => continue, |
| 292 | else => |e| return e, |
| 293 | }, |
| 294 | }; |
| 295 | has_location = true; |
| 296 | dep.location_tok = ast.nodeMainToken(field_init); |
| 297 | dep.location_node = field_init; |
| 298 | } else if (mem.eql(u8, field_name, "path")) { |
| 299 | if (has_location) { |
| 300 | return fail(p, ast.nodeMainToken(field_init), "dependency should specify only one of 'url' and 'path' fields.", .{}); |
| 301 | } |
| 302 | dep.location = .{ |
| 303 | .path = parseString(p, field_init) catch |err| switch (err) { |
| 304 | error.ParseFailure => continue, |
| 305 | else => |e| return e, |
| 306 | }, |
| 307 | }; |
| 308 | has_location = true; |
| 309 | dep.location_tok = ast.nodeMainToken(field_init); |
| 310 | dep.location_node = field_init; |
| 311 | } else if (mem.eql(u8, field_name, "hash")) { |
| 312 | dep.hash = parseHash(p, field_init) catch |err| switch (err) { |
| 313 | error.ParseFailure => continue, |
| 314 | else => |e| return e, |
| 315 | }; |
| 316 | dep.hash_tok = .fromToken(ast.nodeMainToken(field_init)); |
| 317 | dep.hash_node = field_init.toOptional(); |
| 318 | } else if (mem.eql(u8, field_name, "lazy")) { |
| 319 | dep.lazy = parseBool(p, field_init) catch |err| switch (err) { |
| 320 | error.ParseFailure => continue, |
| 321 | else => |e| return e, |
| 322 | }; |
| 323 | } else { |
| 324 | // Ignore unknown fields so that we can add fields in future zig |
| 325 | // versions without breaking older zig versions. |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | if (!has_location) { |
| 330 | try appendError(p, ast.nodeMainToken(node), "dependency requires location field, one of 'url' or 'path'.", .{}); |
| 331 | } |
| 332 | |
| 333 | return dep; |
| 334 | } |
| 335 | |
| 336 | fn parseIncludedPaths(p: *Parse, node: Ast.Node.Index) !void { |
| 337 | const ast = p.ast; |
| 338 | |
| 339 | var buf: [2]Ast.Node.Index = undefined; |
| 340 | const array_init = ast.fullArrayInit(&buf, node) orelse { |
| 341 | const tok = ast.nodeMainToken(node); |
| 342 | return fail(p, tok, "expected paths expression to be a list of strings", .{}); |
| 343 | }; |
| 344 | |
| 345 | for (array_init.ast.elements) |elem_node| { |
| 346 | const path_string = try parseString(p, elem_node); |
| 347 | // This is normalized so that it can be used in string comparisons |
| 348 | // against file system paths. |
| 349 | const normalized = try std.fs.path.resolve(p.arena, &.{path_string}); |
| 350 | try p.paths.put(p.gpa, normalized, {}); |
| 351 | } |
| 352 | } |
| 353 | |
| 354 | fn parseBool(p: *Parse, node: Ast.Node.Index) !bool { |
| 355 | const ast = p.ast; |
| 356 | if (ast.nodeTag(node) != .identifier) { |
| 357 | return fail(p, ast.nodeMainToken(node), "expected identifier", .{}); |
| 358 | } |
| 359 | const ident_token = ast.nodeMainToken(node); |
| 360 | const token_bytes = ast.tokenSlice(ident_token); |
| 361 | if (mem.eql(u8, token_bytes, "true")) { |
| 362 | return true; |
| 363 | } else if (mem.eql(u8, token_bytes, "false")) { |
| 364 | return false; |
| 365 | } else { |
| 366 | return fail(p, ident_token, "expected boolean", .{}); |
| 367 | } |
| 368 | } |
| 369 | |
| 370 | fn parseFingerprint(p: *Parse, node: Ast.Node.Index) !Package.Fingerprint { |
| 371 | const ast = p.ast; |
| 372 | const main_token = ast.nodeMainToken(node); |
| 373 | if (ast.nodeTag(node) != .number_literal) { |
| 374 | return fail(p, main_token, "expected integer literal", .{}); |
| 375 | } |
| 376 | const token_bytes = ast.tokenSlice(main_token); |
| 377 | const parsed = std.zig.parseNumberLiteral(token_bytes); |
| 378 | switch (parsed) { |
| 379 | .int => |n| return @bitCast(n), |
| 380 | .big_int, .float => return fail(p, main_token, "expected u64 integer literal, found {s}", .{ |
| 381 | @tagName(parsed), |
| 382 | }), |
| 383 | .failure => |err| return fail(p, main_token, "bad integer literal: {s}", .{@tagName(err)}), |
| 384 | } |
| 385 | } |
| 386 | |
| 387 | fn parseName(p: *Parse, node: Ast.Node.Index) ![]const u8 { |
| 388 | const ast = p.ast; |
| 389 | const main_token = ast.nodeMainToken(node); |
| 390 | |
| 391 | if (ast.nodeTag(node) != .enum_literal) |
| 392 | return fail(p, main_token, "expected enum literal", .{}); |
| 393 | |
| 394 | const ident_name = ast.tokenSlice(main_token); |
| 395 | if (mem.startsWith(u8, ident_name, "@")) |
| 396 | return fail(p, main_token, "name must be a valid bare zig identifier", .{}); |
| 397 | |
| 398 | if (ident_name.len > max_name_len) |
| 399 | return fail(p, main_token, "name '{f}' exceeds max length of {d}", .{ |
| 400 | std.zig.fmtId(ident_name), max_name_len, |
| 401 | }); |
| 402 | |
| 403 | return ident_name; |
| 404 | } |
| 405 | |
| 406 | fn parseString(p: *Parse, node: Ast.Node.Index) ![]const u8 { |
| 407 | const ast = p.ast; |
| 408 | if (ast.nodeTag(node) != .string_literal) { |
| 409 | return fail(p, ast.nodeMainToken(node), "expected string literal", .{}); |
| 410 | } |
| 411 | const str_lit_token = ast.nodeMainToken(node); |
| 412 | const token_bytes = ast.tokenSlice(str_lit_token); |
| 413 | p.buf.clearRetainingCapacity(); |
| 414 | try parseStrLit(p, str_lit_token, &p.buf, token_bytes, 0); |
| 415 | const duped = try p.arena.dupe(u8, p.buf.items); |
| 416 | return duped; |
| 417 | } |
| 418 | |
| 419 | fn parseHash(p: *Parse, node: Ast.Node.Index) ![]const u8 { |
| 420 | const ast = p.ast; |
| 421 | const tok = ast.nodeMainToken(node); |
| 422 | const h = try parseString(p, node); |
| 423 | switch (Package.Hash.validate(h)) { |
| 424 | .ok => return h, |
| 425 | else => |t| return fail(p, tok, "invalid hash: {t}", .{t}), |
| 426 | } |
| 427 | } |
| 428 | |
| 429 | /// TODO: try to DRY this with AstGen.identifierTokenString |
| 430 | fn identifierTokenString(p: *Parse, token: Ast.TokenIndex) InnerError![]const u8 { |
| 431 | const ast = p.ast; |
| 432 | assert(ast.tokenTag(token) == .identifier); |
| 433 | const ident_name = ast.tokenSlice(token); |
| 434 | if (!mem.startsWith(u8, ident_name, "@")) { |
| 435 | return ident_name; |
| 436 | } |
| 437 | p.buf.clearRetainingCapacity(); |
| 438 | try parseStrLit(p, token, &p.buf, ident_name, 1); |
| 439 | const duped = try p.arena.dupe(u8, p.buf.items); |
| 440 | return duped; |
| 441 | } |
| 442 | |
| 443 | /// TODO: try to DRY this with AstGen.parseStrLit |
| 444 | fn parseStrLit( |
| 445 | p: *Parse, |
| 446 | token: Ast.TokenIndex, |
| 447 | buf: *std.ArrayList(u8), |
| 448 | bytes: []const u8, |
| 449 | offset: u32, |
| 450 | ) InnerError!void { |
| 451 | const raw_string = bytes[offset..]; |
| 452 | const result = r: { |
| 453 | var aw: std.Io.Writer.Allocating = .fromArrayList(p.gpa, buf); |
| 454 | defer buf.* = aw.toArrayList(); |
| 455 | break :r std.zig.string_literal.parseWrite(&aw.writer, raw_string) catch |err| switch (err) { |
| 456 | error.WriteFailed => return error.OutOfMemory, |
| 457 | }; |
| 458 | }; |
| 459 | switch (result) { |
| 460 | .success => {}, |
| 461 | .failure => |err| try p.appendStrLitError(err, token, bytes, offset), |
| 462 | } |
| 463 | } |
| 464 | |
| 465 | /// TODO: try to DRY this with AstGen.failWithStrLitError |
| 466 | fn appendStrLitError( |
| 467 | p: *Parse, |
| 468 | err: std.zig.string_literal.Error, |
| 469 | token: Ast.TokenIndex, |
| 470 | bytes: []const u8, |
| 471 | offset: u32, |
| 472 | ) Allocator.Error!void { |
| 473 | const raw_string = bytes[offset..]; |
| 474 | switch (err) { |
| 475 | .invalid_escape_character => |bad_index| { |
| 476 | try p.appendErrorOff( |
| 477 | token, |
| 478 | offset + @as(u32, @intCast(bad_index)), |
| 479 | "invalid escape character: '{c}'", |
| 480 | .{raw_string[bad_index]}, |
| 481 | ); |
| 482 | }, |
| 483 | .expected_hex_digit => |bad_index| { |
| 484 | try p.appendErrorOff( |
| 485 | token, |
| 486 | offset + @as(u32, @intCast(bad_index)), |
| 487 | "expected hex digit, found '{c}'", |
| 488 | .{raw_string[bad_index]}, |
| 489 | ); |
| 490 | }, |
| 491 | .empty_unicode_escape_sequence => |bad_index| { |
| 492 | try p.appendErrorOff( |
| 493 | token, |
| 494 | offset + @as(u32, @intCast(bad_index)), |
| 495 | "empty unicode escape sequence", |
| 496 | .{}, |
| 497 | ); |
| 498 | }, |
| 499 | .expected_hex_digit_or_rbrace => |bad_index| { |
| 500 | try p.appendErrorOff( |
| 501 | token, |
| 502 | offset + @as(u32, @intCast(bad_index)), |
| 503 | "expected hex digit or '}}', found '{c}'", |
| 504 | .{raw_string[bad_index]}, |
| 505 | ); |
| 506 | }, |
| 507 | .invalid_unicode_codepoint => |bad_index| { |
| 508 | try p.appendErrorOff( |
| 509 | token, |
| 510 | offset + @as(u32, @intCast(bad_index)), |
| 511 | "unicode escape does not correspond to a valid unicode scalar value", |
| 512 | .{}, |
| 513 | ); |
| 514 | }, |
| 515 | .expected_lbrace => |bad_index| { |
| 516 | try p.appendErrorOff( |
| 517 | token, |
| 518 | offset + @as(u32, @intCast(bad_index)), |
| 519 | "expected '{{', found '{c}", |
| 520 | .{raw_string[bad_index]}, |
| 521 | ); |
| 522 | }, |
| 523 | .expected_rbrace => |bad_index| { |
| 524 | try p.appendErrorOff( |
| 525 | token, |
| 526 | offset + @as(u32, @intCast(bad_index)), |
| 527 | "expected '}}', found '{c}", |
| 528 | .{raw_string[bad_index]}, |
| 529 | ); |
| 530 | }, |
| 531 | .expected_single_quote => |bad_index| { |
| 532 | try p.appendErrorOff( |
| 533 | token, |
| 534 | offset + @as(u32, @intCast(bad_index)), |
| 535 | "expected single quote ('), found '{c}", |
| 536 | .{raw_string[bad_index]}, |
| 537 | ); |
| 538 | }, |
| 539 | .invalid_character => |bad_index| { |
| 540 | try p.appendErrorOff( |
| 541 | token, |
| 542 | offset + @as(u32, @intCast(bad_index)), |
| 543 | "invalid byte in string or character literal: '{c}'", |
| 544 | .{raw_string[bad_index]}, |
| 545 | ); |
| 546 | }, |
| 547 | .empty_char_literal => { |
| 548 | try p.appendErrorOff(token, offset, "empty character literal", .{}); |
| 549 | }, |
| 550 | } |
| 551 | } |
| 552 | |
| 553 | fn fail( |
| 554 | p: *Parse, |
| 555 | tok: Ast.TokenIndex, |
| 556 | comptime fmt: []const u8, |
| 557 | args: anytype, |
| 558 | ) InnerError { |
| 559 | try appendError(p, tok, fmt, args); |
| 560 | return error.ParseFailure; |
| 561 | } |
| 562 | |
| 563 | fn appendError(p: *Parse, tok: Ast.TokenIndex, comptime fmt: []const u8, args: anytype) !void { |
| 564 | return appendErrorOff(p, tok, 0, fmt, args); |
| 565 | } |
| 566 | |
| 567 | fn appendErrorOff( |
| 568 | p: *Parse, |
| 569 | tok: Ast.TokenIndex, |
| 570 | byte_offset: u32, |
| 571 | comptime fmt: []const u8, |
| 572 | args: anytype, |
| 573 | ) Allocator.Error!void { |
| 574 | try p.errors.append(p.gpa, .{ |
| 575 | .msg = try std.fmt.allocPrint(p.arena, fmt, args), |
| 576 | .tok = tok, |
| 577 | .off = byte_offset, |
| 578 | }); |
| 579 | } |
| 580 | }; |
| 581 | |
| 582 | pub fn load( |
| 583 | io: Io, |
| 584 | arena: Allocator, |
| 585 | manifest_path: std.Build.Cache.Path, |
| 586 | ast: *std.zig.Ast, |
| 587 | error_bundle: *std.zig.ErrorBundle.Wip, |
| 588 | manifest: *Manifest, |
| 589 | allow_missing_paths_field: bool, |
| 590 | ) !void { |
| 591 | const manifest_bytes = try manifest_path.root_dir.handle.readFileAllocOptions( |
| 592 | io, |
| 593 | manifest_path.sub_path, |
| 594 | arena, |
| 595 | .limited(max_bytes), |
| 596 | .@"1", |
| 597 | 0, |
| 598 | ); |
| 599 | |
| 600 | ast.* = try std.zig.Ast.parse(arena, manifest_bytes, .{ .mode = .zon }); |
| 601 | |
| 602 | if (ast.errors.len > 0) { |
| 603 | const file_path = try manifest_path.joinString(arena, ""); |
| 604 | try std.zig.putAstErrorsIntoBundle(arena, ast.*, file_path, error_bundle); |
| 605 | return error.ErrorsBundled; |
| 606 | } |
| 607 | |
| 608 | const rng: std.Random.IoSource = .{ .io = io }; |
| 609 | |
| 610 | manifest.* = try parse(arena, ast, rng.interface(), .{ |
| 611 | .allow_missing_paths_field = allow_missing_paths_field, |
| 612 | }); |
| 613 | |
| 614 | if (manifest.errors.len > 0) { |
| 615 | const src_path = try error_bundle.printString("{f}", .{manifest_path}); |
| 616 | try manifest.copyErrorsIntoBundle(ast.*, src_path, error_bundle); |
| 617 | return error.ErrorsBundled; |
| 618 | } |
| 619 | } |
| 620 | |
| 621 | test "basic" { |
| 622 | const gpa = testing.allocator; |
| 623 | |
| 624 | const example = |
| 625 | \\.{ |
| 626 | \\ .name = .foo, |
| 627 | \\ .fingerprint = 0x8c736521490b23df, |
| 628 | \\ .version = "3.2.1", |
| 629 | \\ .paths = .{""}, |
| 630 | \\ .dependencies = .{ |
| 631 | \\ .bar = .{ |
| 632 | \\ .url = "https://example.com/baz.tar.gz", |
| 633 | \\ .hash = "libmp3lame-3.100.1-6-67wlF_KvEwDRCT3pTpcDzi5KGntWCEoM-WtvVPEWdlk5", |
| 634 | \\ }, |
| 635 | \\ }, |
| 636 | \\} |
| 637 | ; |
| 638 | |
| 639 | var ast = try Ast.parse(gpa, example, .{ .mode = .zon }); |
| 640 | defer ast.deinit(gpa); |
| 641 | |
| 642 | try testing.expect(ast.errors.len == 0); |
| 643 | |
| 644 | var rng = std.Random.DefaultPrng.init(0); |
| 645 | |
| 646 | var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{}); |
| 647 | defer manifest.deinit(gpa); |
| 648 | |
| 649 | try testing.expect(manifest.errors.len == 0); |
| 650 | try testing.expectEqualStrings("foo", manifest.name); |
| 651 | |
| 652 | try testing.expectEqual(@as(std.SemanticVersion, .{ |
| 653 | .major = 3, |
| 654 | .minor = 2, |
| 655 | .patch = 1, |
| 656 | }), manifest.version); |
| 657 | |
| 658 | try testing.expect(manifest.dependencies.count() == 1); |
| 659 | try testing.expectEqualStrings("bar", manifest.dependencies.keys()[0]); |
| 660 | try testing.expectEqualStrings( |
| 661 | "https://example.com/baz.tar.gz", |
| 662 | manifest.dependencies.values()[0].location.url, |
| 663 | ); |
| 664 | try testing.expectEqualStrings( |
| 665 | "libmp3lame-3.100.1-6-67wlF_KvEwDRCT3pTpcDzi5KGntWCEoM-WtvVPEWdlk5", |
| 666 | manifest.dependencies.values()[0].hash orelse return error.TestFailed, |
| 667 | ); |
| 668 | |
| 669 | try testing.expect(manifest.minimum_zig_version == null); |
| 670 | } |
| 671 | |
| 672 | test "minimum_zig_version" { |
| 673 | const gpa = testing.allocator; |
| 674 | |
| 675 | const example = |
| 676 | \\.{ |
| 677 | \\ .name = .foo, |
| 678 | \\ .fingerprint = 0x8c736521490b23df, |
| 679 | \\ .version = "3.2.1", |
| 680 | \\ .paths = .{""}, |
| 681 | \\ .minimum_zig_version = "0.11.1", |
| 682 | \\} |
| 683 | ; |
| 684 | |
| 685 | var ast = try Ast.parse(gpa, example, .{ .mode = .zon }); |
| 686 | defer ast.deinit(gpa); |
| 687 | |
| 688 | try testing.expect(ast.errors.len == 0); |
| 689 | |
| 690 | var rng = std.Random.DefaultPrng.init(0); |
| 691 | |
| 692 | var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{}); |
| 693 | defer manifest.deinit(gpa); |
| 694 | |
| 695 | try testing.expect(manifest.errors.len == 0); |
| 696 | try testing.expect(manifest.dependencies.count() == 0); |
| 697 | |
| 698 | try testing.expect(manifest.minimum_zig_version != null); |
| 699 | |
| 700 | try testing.expectEqual(@as(std.SemanticVersion, .{ |
| 701 | .major = 0, |
| 702 | .minor = 11, |
| 703 | .patch = 1, |
| 704 | }), manifest.minimum_zig_version.?); |
| 705 | } |
| 706 | |
| 707 | test "minimum_zig_version - invalid version" { |
| 708 | const gpa = testing.allocator; |
| 709 | |
| 710 | const example = |
| 711 | \\.{ |
| 712 | \\ .name = .foo, |
| 713 | \\ .fingerprint = 0x8c736521490b23df, |
| 714 | \\ .version = "3.2.1", |
| 715 | \\ .minimum_zig_version = "X.11.1", |
| 716 | \\ .paths = .{""}, |
| 717 | \\} |
| 718 | ; |
| 719 | |
| 720 | var ast = try Ast.parse(gpa, example, .{ .mode = .zon }); |
| 721 | defer ast.deinit(gpa); |
| 722 | |
| 723 | try testing.expect(ast.errors.len == 0); |
| 724 | |
| 725 | var rng = std.Random.DefaultPrng.init(0); |
| 726 | |
| 727 | var manifest = try Manifest.parse(gpa, &ast, rng.random(), .{}); |
| 728 | defer manifest.deinit(gpa); |
| 729 | |
| 730 | try testing.expect(manifest.errors.len == 1); |
| 731 | try testing.expect(manifest.dependencies.count() == 0); |
| 732 | |
| 733 | try testing.expect(manifest.minimum_zig_version == null); |
| 734 | } |