From fb802f3de42fe8d3efc3373073e92f91e27b5ad0 Mon Sep 17 00:00:00 2001 From: Martin Ashby Date: Sun, 10 Sep 2023 23:35:03 +0100 Subject: add test for manifest use libcurl instead of zig http client :'( Download all libraries --- src/main.zig | 238 +++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 209 insertions(+), 29 deletions(-) (limited to 'src/main.zig') diff --git a/src/main.zig b/src/main.zig index 4b6fdd5..773dad8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,17 +1,102 @@ const std = @import("std"); +const builtin = @import("builtin"); +const log = std.log.scoped(.mcl); + +/// Wrapper over http client impl +/// I'd love this to be the zig stdlig one but it doesn't support TLSv1.2 which +/// mojang are using (and not supporting v1.3 :( ). +const HttpClient = struct { + const curl = @cImport(@cInclude("curl/curl.h")); + + allocator: std.mem.Allocator, + c_curl: *curl.CURL, + errbuf: [curl.CURL_ERROR_SIZE]u8 = [_]u8{0}**curl.CURL_ERROR_SIZE, + + fn init(allocator: std.mem.Allocator) !HttpClient { + const c_curl = curl.curl_easy_init() orelse return error.CurlInitFailed; + return .{ + .allocator = allocator, + .c_curl = c_curl, + }; + } + fn deinit(self: *HttpClient) void { + curl.curl_easy_cleanup(self.c_curl); + } + + const Request = struct { + url: []const u8, + }; + const Response = struct { + response_buf: std.ArrayList(u8), + did_oom: bool = false, + status: std.http.Status = .teapot, + fn init(allocator: std.mem.Allocator) Response { + return .{ + .response_buf = std.ArrayList(u8).init(allocator), + }; + } + fn deinit(self: *Response) void { + self.response_buf.deinit(); + } + // size_t write_data(char *buffer, size_t size, size_t nmemb, void *userp); + // https://curl.se/libcurl/c/CURLOPT_WRITEDATA.html + fn write_data(buffer: [*c]u8, _: usize, nmemb: usize, userp: *anyopaque) callconv(.C) usize { + var self: *Response = @ptrCast(@alignCast(userp)); + self.response_buf.appendSlice(buffer[0..nmemb]) catch { + self.did_oom = true; + return 0; + }; + return nmemb; + } + }; + fn do(self: *HttpClient, request: Request) !Response { + const urlz = try self.allocator.dupeZ(u8, request.url); + defer self.allocator.free(urlz); + var response = Response.init(self.allocator); + errdefer response.deinit(); + if (curl.curl_easy_setopt(self.c_curl, curl.CURLOPT_ERRORBUFFER, &self.errbuf) != curl.CURLE_OK) { + log.err("cURL error: {s}", .{self.errbuf}); + return error.CurlError; + } + if (curl.curl_easy_setopt(self.c_curl, curl.CURLOPT_URL, urlz.ptr) != curl.CURLE_OK) { + log.err("cURL error: {s}", .{self.errbuf}); + return error.CurlError; + } + if (curl.curl_easy_setopt(self.c_curl, curl.CURLOPT_WRITEFUNCTION, Response.write_data) != curl.CURLE_OK) { + log.err("cURL error: {s}", .{self.errbuf}); + return error.CurlError; + } + if (curl.curl_easy_setopt(self.c_curl, curl.CURLOPT_WRITEDATA, &response) != curl.CURLE_OK) { + log.err("cURL error: {s}", .{self.errbuf}); + return error.CurlError; + } + if (curl.curl_easy_perform(self.c_curl) != curl.CURLE_OK) { + + if (response.did_oom) { + log.err("OOM receiving data from cURL", .{}); + return error.OutOfMemory; + } else { + log.err("cURL error: {s}", .{self.errbuf}); + return error.CurlError; + } + } + return response; + } +}; /// Minecraft launcher! pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{ .safety = false }){}; // TODO turn safety back on defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var client = std.http.Client{ .allocator = allocator }; + var client = try HttpClient.init(allocator); defer client.deinit(); + log.info("downloading manifest from {s}", .{Manifest.uri}); const manifest_wrap = try download_json(&client, Manifest, Manifest.uri); defer manifest_wrap.deinit(); const manifest = manifest_wrap.value; - std.log.info("release: {s} snapshot: {s}", .{ manifest.latest.release, manifest.latest.snapshot }); + log.info("release version: {s}", .{ manifest.latest.release }); // Find the release version const release_version_url = for (manifest.versions) |v| { @@ -22,13 +107,16 @@ pub fn main() !void { return error.NoReleaseVersionFound; }; + log.info("downloading release manifest from {s}", .{release_version_url}); var release_version_wrap = try download_json(&client, Version, release_version_url); defer release_version_wrap.deinit(); - const release_version = release_version_wrap.value; - _ = release_version; + const release_version: Version = release_version_wrap.value; - // Download all the bits + // Download all the required files + var root_dir = try get_minecraft_root_dir(allocator); + defer root_dir.close(); + try download_version(allocator, &client, &root_dir, release_version); // std.mem.replace(comptime T: type, input: []const T, needle: []const T, replacement: []const T, output: []T) // check the latest.release @@ -40,6 +128,75 @@ pub fn main() !void { // } +fn download_version(allocator: std.mem.Allocator, client: *HttpClient, root_dir: *const std.fs.Dir, version: Version) !void { + const dir_path = try std.fs.path.join(allocator, &[_][]const u8{"versions", version.id}); + defer allocator.free(dir_path); + log.info("downloading version {s} to {s}", .{version.id, dir_path}); + try root_dir.makePath(dir_path); + const dir = try root_dir.openDir(dir_path, .{}); + var client_jar = try dir.createFile("client.jar", .{}); + defer client_jar.close(); + try download_file(client, &client_jar, version.downloads.client.url); + + var client_map = try dir.createFile("client.map", .{}); + defer client_map.close(); + try download_file(client, &client_jar, version.downloads.client_mappings.url); + // TODO server + + // Dependencies... + lib: for (version.libraries) |library| { + if (library.rules) |rules| for (rules) |rule| { + // TODO rule.features + if (rule.os) |os| { + if (os.name) |osname| { + if (osname == .windows and builtin.os.tag != .windows) { + continue :lib; + } else if (osname == .osx and builtin.os.tag != .macos) { + continue :lib; + } else if (osname == .linux and builtin.os.tag != .linux) { + continue :lib; + } + } + + if (os.arch) |osarch| { + if (osarch == .x86 and builtin.cpu.arch != .x86) { + continue :lib; + } + } + } + }; + const file_path = std.fs.path.basename(library.downloads.artifact.path); + var file = try dir.createFile(file_path, .{}); + defer file.close(); + try download_file(client, &file, library.downloads.artifact.url); + } +} + +// Caller frees the result +fn fix_https(allocator: std.mem.Allocator, url_maybe_https: []const u8) ![]const u8 { + // This particular URL doesn't support TLSv1.3 :( + // and zig doesn't support TLSv1.2. Booooo. Hack it and use http for now. + if (std.mem.indexOf(u8, url_maybe_https, "piston-meta.mojang.com") != null + or std.mem.indexOf(u8, url_maybe_https, "piston-data.mojang.com") != null) { + const sz = std.mem.replacementSize(u8, url_maybe_https, "https://", "http://"); + const url = try allocator.alloc(u8, sz); + _ = std.mem.replace(u8, url_maybe_https, "https://", "http://", url); + return url; + } else { + return try allocator.dupe(u8, url_maybe_https); + } +} + +fn download_file(client: *HttpClient, file: *std.fs.File, url: []const u8) !void { + log.info("downloading file {s}", .{url}); + const a = client.allocator; + var r = try client.do(.{.url= url}); + defer r.deinit(); + const sl = try r.response_buf.toOwnedSlice(); + defer a.free(sl); + try file.writeAll(sl); +} + /// https://minecraft.fandom.com/wiki/Version_manifest.json const Manifest = struct { const uri = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"; @@ -68,8 +225,8 @@ const Version = struct { is_quick_play_realms: bool = false, }; const Os = struct { - name: ?[]const u8 = null, - arch: ?[]const u8 = null, + name: ?enum{osx,linux,windows} = null, + arch: ?enum{x86} = null, }; action: enum{allow}, features: ?Feature = null, @@ -181,36 +338,44 @@ const Version = struct { /// Caller is responsible for calling deinit on the resulting Parsed struct. -fn download_json(client: *std.http.Client, comptime T: type, url_maybe_https: []const u8) !std.json.Parsed(T) { - // TODO use https once zig supports TLSv1.2 or mojang support TLSv1.3 +fn download_json(client: *HttpClient, comptime T: type, url: []const u8) !std.json.Parsed(T) { const a = client.allocator; - const sz = std.mem.replacementSize(u8, url_maybe_https, "https://", "http://"); - const url = try a.alloc(u8, sz); - defer a.free(url); - _ = std.mem.replace(u8, url_maybe_https, "https://", "http://", url); - - var h = std.http.Headers.init(a); - defer h.deinit(); - try h.append("Accept", "application/json"); - const uri = try std.Uri.parse(url); - var r = try client.request(.GET, uri, h, .{}); - try r.start(); - try r.wait(); - if (r.response.status != .ok) { - std.log.err("request failed {}:{s}", .{ r.response.status, r.response.reason }); - return error.HttpStatusError; - } - var r2 = std.json.reader(a, r.reader()); + var r = try client.do(.{.url = url}); + var sl = try r.response_buf.toOwnedSlice(); + defer a.free(sl); + // TODO check status + // if (r.response.status != .ok) { + // log.err("request failed {}:{s}", .{ r.response.status, r.response.reason }); + // return error.HttpStatusError; + // } + var f = std.io.fixedBufferStream(sl); + var r2 = std.json.reader(a, f.reader()); var diags = std.json.Diagnostics{}; r2.enableDiagnostics(&diags); defer r2.deinit(); var res = std.json.parseFromTokenSource(T, a, &r2, .{ .ignore_unknown_fields = true, .allocate = .alloc_always }) catch |e| { - std.log.err("parse failed {} line {} col {}", .{ e, diags.getLine(), diags.getColumn() }); + log.err("parse failed {} line {} col {}", .{ e, diags.getLine(), diags.getColumn() }); return e; }; return res; } +fn get_minecraft_root_dir(allocator: std.mem.Allocator) !std.fs.Dir { + const home_dir = std.os.getenv("HOME") orelse { + return error.AppDataDirUnavailable; + }; + const root_dir_path = try std.fs.path.join(allocator, &[_][]const u8{home_dir, ".minecraft"}); + defer allocator.free(root_dir_path); + const root_dir = std.fs.openDirAbsolute(root_dir_path, .{}) catch |e| switch (e) { + error.FileNotFound => blk: { + try std.fs.makeDirAbsolute(root_dir_path); + break :blk try std.fs.openDirAbsolute(root_dir_path, .{}); + }, + else => return e, + }; + return root_dir; +} + test "heterogenous array" { const x = \\ { @@ -298,11 +463,26 @@ test "Version" { var d = std.json.Diagnostics{}; jr.enableDiagnostics(&d); var v_w = std.json.parseFromTokenSource(Version, std.testing.allocator, &jr, .{.ignore_unknown_fields = true}) catch |e| { - std.log.err("parse failed {} line {} col {}", .{ e, d.getLine(), d.getColumn() }); + log.err("parse failed {} line {} col {}", .{ e, d.getLine(), d.getColumn() }); + return e; + }; + defer v_w.deinit(); + const v = v_w.value; + _ = v; +} + +test "Manifest" { + const x = @embedFile("manifest_test.json"); + var f = std.io.fixedBufferStream(x); + var jr = std.json.reader(std.testing.allocator, f.reader()); + defer jr.deinit(); + var d = std.json.Diagnostics{}; + jr.enableDiagnostics(&d); + var v_w = std.json.parseFromTokenSource(Manifest, std.testing.allocator, &jr, .{.ignore_unknown_fields = true}) catch |e| { + log.err("parse failed {} line {} col {}", .{ e, d.getLine(), d.getColumn() }); return e; }; defer v_w.deinit(); const v = v_w.value; _ = v; - // v.arguments.game } -- cgit v1.2.3-ZIG