aboutsummaryrefslogtreecommitdiff
path: root/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.zig')
-rw-r--r--src/main.zig238
1 files changed, 209 insertions, 29 deletions
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
}