Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 160 additions & 0 deletions src/core/install.zig
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,14 @@
fn init(self: *Release, kind: ReleaseKind) void {
self.* = .{
.kind = kind,
.version_buffer = undefined,

Check warning on line 54 in src/core/install.zig

View workflow job for this annotation

GitHub Actions / lint

unsafe-undefined

`undefined` is missing a safety comment
.version_len = 0,
.download_urls_buffer = undefined,

Check warning on line 56 in src/core/install.zig

View workflow job for this annotation

GitHub Actions / lint

unsafe-undefined

`undefined` is missing a safety comment
.download_urls_len = std.mem.zeroes([release_download_urls_max]u32),
.download_urls_count = 0,
.hash = null,
.size = 0,
.signature_url_buffer = undefined,

Check warning on line 61 in src/core/install.zig

View workflow job for this annotation

GitHub Actions / lint

unsafe-undefined

`undefined` is missing a safety comment
.signature_url_len = 0,
.extract_path_buffer = undefined,
.extract_path_len = 0,
Expand Down Expand Up @@ -379,6 +379,14 @@
}
const platform_str = platform_str_buffer[0..platform_str_temp.len];

// Stable releases live in the GitHub releases index. Master and pinned
// dev builds live behind the `select-version` endpoint, which returns a
// different JSON shape (per-platform tarball + shasum + size).
if (util_tool.is_master_like_version(version)) {
try resolve_zls_master_release(ctx, release, version, platform_str);
return;
}

const version_data = try fetch_zls_version_data(ctx, platform_str, version);

var version_path_buffer = try ctx.scratch(.path);
Expand All @@ -396,6 +404,35 @@
assert(release.signature_url() == null);
}

fn resolve_zls_master_release(
ctx: *context.CliContext,
release: *Release,
version: []const u8,
platform_str: []const u8,
) !void {
assert(version.len > 0);
assert(version.len < 100);
assert(platform_str.len > 0);
assert(util_tool.is_master_like_version(version));

const version_data = try fetch_zls_master_version_data(ctx, platform_str, version);

var version_path_buffer = try ctx.scratch(.path);
defer version_path_buffer.release();
const version_root = try util_data.get_zvm_zls_version(version_path_buffer);

release.init(.zls);
try release.set_version(version_data.version());
try release.set_extract_path_from_parts(ctx, version_root, release.version());
release.hash = version_data.shasum;
release.size = version_data.size;
try release.add_download_url(version_data.tarball());

assert(release.download_urls_count == 1);
assert(release.signature_url() == null);
assert(release.hash != null);
}

fn install_release(
ctx: *context.CliContext,
release: *const Release,
Expand Down Expand Up @@ -618,7 +655,7 @@
var stderr_buffer: [128]u8 = undefined;
var stderr_writer = std.Io.File.stderr().writer(ctx.io, &stderr_buffer);
stderr_writer.interface.writeAll("\ninterrupted, cleaning up...\n") catch {};
stderr_writer.interface.flush() catch {};

Check warning on line 658 in src/core/install.zig

View workflow job for this annotation

GitHub Actions / lint

suppressed-errors

`catch` statement suppresses errors

cleanup_delete_tree_with_timeout(ctx, extract_path) catch |err| {
log.warn("Interrupted cleanup failed for {s}: {s}", .{ extract_path, @errorName(err) });
Expand Down Expand Up @@ -924,3 +961,126 @@

return version_data;
}

/// Percent-encodes the unreserved subset needed for a Zig dev version pin
/// (`0.17.0-dev.261+3d1fb4fac`) when used as a URL query value. The literal
/// `+` would otherwise be decoded as a space by the server, so we encode it
/// as `%2B`. All RFC 3986 unreserved characters and the dev-pin separators
/// `.` and `-` are passed through; anything else is percent-encoded.
fn percent_encode_query_value(input: []const u8, output_buffer: []u8) ![]const u8 {
assert(input.len > 0);
assert(output_buffer.len >= input.len * 3);

var write_index: usize = 0;
for (input) |byte| {
const passthrough = (byte >= 'A' and byte <= 'Z') or
(byte >= 'a' and byte <= 'z') or
(byte >= '0' and byte <= '9') or
byte == '-' or byte == '.' or byte == '_' or byte == '~';
if (passthrough) {
output_buffer[write_index] = byte;
write_index += 1;
} else {
const hex_digits = "0123456789ABCDEF";
output_buffer[write_index] = '%';
output_buffer[write_index + 1] = hex_digits[(byte >> 4) & 0x0f];
output_buffer[write_index + 2] = hex_digits[byte & 0x0f];
write_index += 3;
}
}
assert(write_index >= input.len);
assert(write_index <= output_buffer.len);
return output_buffer[0..write_index];
}

/// Translates the user-facing `master` alias into the concrete Zig dev
/// version currently published in `index.json`. Pinned dev pins are copied
/// through verbatim. The returned slice points into `output_buffer`.
fn resolve_master_zig_version(
ctx: *context.CliContext,
version: []const u8,
output_buffer: *[limits.limits.version_string_length_maximum]u8,
) ![]const u8 {
assert(version.len > 0);
assert(util_tool.is_master_like_version(version));

if (util_tool.is_dev_version(version)) {
assert(version.len <= output_buffer.len);
@memcpy(output_buffer[0..version.len], version);
return output_buffer[0..version.len];
}

assert(util_tool.eql_str(version, "master"));
const res = try http_client.HttpClient.fetch(ctx, config.zig_url, .{});
assert(res.len > 0);

const resolved = try meta.Zig.get_master_version_string(res, output_buffer[0..]) orelse {
log.err("Zig index.json has no `master` entry; cannot resolve ZLS master build.", .{});
return error.UnsupportedVersion;
};
if (!util_tool.is_dev_version(resolved)) {
log.err("Zig master entry version '{s}' is not a dev pin.", .{resolved});
return error.UnsupportedVersion;
}
return resolved;
}

/// Fetches and parses the ZLS `select-version` payload for a master/dev
/// install. The endpoint requires a concrete Zig dev version as the
/// `zig_version` query parameter (the literal `master` is rejected with HTTP
/// 400), so the literal `master` alias is first resolved against Zig's
/// `index.json` to its current dev pin. Pinned dev versions are passed
/// through unchanged.
fn fetch_zls_master_version_data(
ctx: *context.CliContext,
platform_str: []const u8,
version: []const u8,
) !meta.Zls.MasterVersionData {
assert(platform_str.len > 0);
assert(version.len > 0);
assert(util_tool.is_master_like_version(version));

var resolved_buffer: [limits.limits.version_string_length_maximum]u8 = undefined;
const resolved_zig_version = try resolve_master_zig_version(ctx, version, &resolved_buffer);
assert(resolved_zig_version.len > 0);
assert(util_tool.is_dev_version(resolved_zig_version));

var encoded_version_buffer: [limits.limits.version_string_length_maximum * 3]u8 = undefined;
const encoded_zig_version = try percent_encode_query_value(
resolved_zig_version,
encoded_version_buffer[0..],
);

var url_buffer = try ctx.scratch(.path);
defer url_buffer.release();
const url = try url_buffer.set(try std.fmt.bufPrint(
url_buffer.slice(),
"{s}?zig_version={s}&compatibility=only-runtime",
.{ config.zls_select_version_url_base, encoded_zig_version },
));
assert(url.len > 0);
assert(url.len <= limits.limits.url_length_maximum);

const uri = std.Uri.parse(url) catch |err| {
log.err("Invalid ZLS select-version URL '{s}': {s}", .{ url, @errorName(err) });
return error.InvalidMetadataUrl;
};

const res = try http_client.HttpClient.fetch(ctx, uri, .{});
assert(res.len > 0);

const version_data = try meta.Zls.get_master_version_data(res, platform_str) orelse {
log.err("Unsupported ZLS master build for Zig '{s}' on platform '{s}'. The ZLS " ++
"select-version endpoint did not return an entry for this combination.", .{
version,
platform_str,
});
return error.UnsupportedVersion;
};

assert(version_data.version().len > 0);
assert(version_data.tarball().len > 0);
assert(version_data.size > 0);

return version_data;
}
67 changes: 67 additions & 0 deletions src/core/meta/zig.zig
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
const lookup_key: []const u8 = if (is_dev) "master" else version;
assert(lookup_key.len > 0);

var scanner: TokenScanner = undefined;

Check warning on line 63 in src/core/meta/zig.zig

View workflow job for this annotation

GitHub Actions / lint

unsafe-undefined

`undefined` is missing a safety comment
try scanner.init(raw);
defer scanner.deinit();

Expand Down Expand Up @@ -94,11 +94,51 @@
return error.IndexEntryLimitExceeded;
}

/// Reads the resolved dev version string from the `master` entry of the
/// Zig `index.json` payload (e.g. `0.16.0-dev.418+abcdef0`). Used by the
/// ZLS master install path to translate the user-facing alias `master`
/// into a concrete Zig version that the ZLS `select-version` endpoint
/// accepts.
pub fn get_master_version_string(
raw: []const u8,
target_buffer: []u8,
) !?[]const u8 {
assert(raw.len > 0);
assert(target_buffer.len > 0);

var scanner: TokenScanner = undefined;

Check warning on line 109 in src/core/meta/zig.zig

View workflow job for this annotation

GitHub Actions / lint

unsafe-undefined

`undefined` is missing a safety comment
try scanner.init(raw);
defer scanner.deinit();

try scanner.expect_object_begin();
const entries_max: u32 = 4096;
var entries_seen: u32 = 0;
while (entries_seen < entries_max) : (entries_seen += 1) {
switch (try scanner.peek_token_type()) {
.object_end => {
try scanner.expect_object_end();
return null;
},
.string => {
var key_buffer: [limits.limits.version_string_length_maximum]u8 = undefined;
const key = try scanner.next_string(key_buffer[0..]);
if (!util_tool.eql_str(key, "master")) {
try scanner.skip_value();
continue;
}
return try parse_master_version_field(&scanner, target_buffer);
},
else => return error.UnexpectedToken,
}
}
return error.IndexEntryLimitExceeded;
}

pub fn get_version_list(
raw: []const u8,
version_entries: []*object_pools.VersionEntry,
) !usize {
var scanner: TokenScanner = undefined;

Check warning on line 141 in src/core/meta/zig.zig

View workflow job for this annotation

GitHub Actions / lint

unsafe-undefined

`undefined` is missing a safety comment
try scanner.init(raw);
defer scanner.deinit();

Expand Down Expand Up @@ -175,6 +215,33 @@
return version_data;
}

fn parse_master_version_field(
scanner: *TokenScanner,
target_buffer: []u8,
) !?[]const u8 {
assert(target_buffer.len > 0);

try scanner.expect_object_begin();
var version_text: ?[]const u8 = null;
while (true) {
switch (try scanner.peek_token_type()) {
.object_end => break,
.string => {
var key_buffer: [limits.limits.path_length_maximum]u8 = undefined;
const key = try scanner.next_string(key_buffer[0..]);
if (util_tool.eql_str(key, "version")) {
version_text = try scanner.next_string(target_buffer);
} else {
try scanner.skip_value();
}
},
else => return error.UnexpectedToken,
}
}
try scanner.expect_object_end();
return version_text;
}

fn parse_platform_entry(
scanner: *TokenScanner,
version_data: *Zig.VersionData,
Expand Down
Loading
Loading