From c7f4d3b179a8eac42b641bd584540a05e91c9bff Mon Sep 17 00:00:00 2001 From: jiacai2050 Date: Thu, 4 Jan 2024 22:34:42 +0800 Subject: [PATCH 1/7] refactor request type --- examples/advanced.zig | 12 +-- src/c.zig | 2 +- src/easy.zig | 231 +++++++++++++++++++----------------------- 3 files changed, 114 insertions(+), 131 deletions(-) diff --git a/examples/advanced.zig b/examples/advanced.zig index 03ce49d..c91b7f3 100644 --- a/examples/advanced.zig +++ b/examples/advanced.zig @@ -21,23 +21,23 @@ const Resposne = struct { }; fn put_with_custom_header(allocator: Allocator, easy: Easy) !void { - var stream = std.io.fixedBufferStream( + const body = \\ {"name": "John", "age": 15} - ); - const body = stream.reader(); + ; const header = blk: { - var h = curl.RequestHeader.init(allocator); + var h = curl.Request.Header.init(allocator); errdefer h.deinit(); try h.add(curl.HEADER_CONTENT_TYPE, "application/json"); try h.add("user-agent", UA); try h.add("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"); break :blk h; }; - var req = curl.Request(@TypeOf(body)).init("https://httpbin.org/anything/zig-curl", body, .{ + var req = curl.Request.init("https://httpbin.org/anything/zig-curl", .{ .method = .PUT, .header = header, .verbose = true, + .body = body, }); defer req.deinit(); @@ -89,7 +89,7 @@ fn post_mutli_part(easy: Easy) !void { try multi_part.add_part("build.zig", .{ .file = "build.zig" }); try multi_part.add_part("readme", .{ .file = "README.org" }); - var req = curl.Request(void).init("https://httpbin.org/anything/mp", {}, .{ + var req = curl.Request.init("https://httpbin.org/anything/mp", .{ .method = .PUT, .multi_part = multi_part, .verbose = true, diff --git a/src/c.zig b/src/c.zig index 2d509cf..22b55e6 100644 --- a/src/c.zig +++ b/src/c.zig @@ -56,7 +56,7 @@ comptime { } } -pub fn url_encode(string: []const u8) ?[]const u8 { +pub fn url_encode(string: [:0]const u8) ?[]const u8 { const r = c.curl_easy_escape(null, string.ptr, @intCast(string.len)); return std.mem.sliceTo(r.?, 0); } diff --git a/src/easy.zig b/src/easy.zig index 8bd2c4e..3181a53 100644 --- a/src/easy.zig +++ b/src/easy.zig @@ -14,9 +14,8 @@ const Self = @This(); allocator: Allocator, handle: *c.CURL, -/// The maximum time in milliseconds that the entire transfer operation to take. -timeout_ms: usize = 30_000, -default_user_agent: []const u8 = "zig-curl/0.1.0", +timeout_ms: usize, +user_agent: [:0]const u8, ca_bundle: ?[]const u8, pub const HEADER_CONTENT_TYPE: []const u8 = "Content-Type"; @@ -32,112 +31,101 @@ pub const Method = enum { PATCH, DELETE, - fn asString(self: @This()) [:0]const u8 { + fn asString(self: Method) [:0]const u8 { return @tagName(self); } }; -pub const RequestHeader = struct { - entries: std.StringHashMap([]const u8), - allocator: Allocator, - - pub fn init(allocator: Allocator) @This() { - return .{ - .entries = std.StringHashMap([]const u8).init(allocator), - .allocator = allocator, - }; - } - - pub fn deinit(self: *@This()) void { - self.entries.deinit(); - } +pub const Request = struct { + url: [:0]const u8, + /// body is io.Reader or void + opts: Options, + + pub const Options = struct { + method: Method = .GET, + header: ?Request.Header = null, + multi_part: ?MultiPart = null, + body: ?[]const u8 = null, + verbose: bool = false, + /// Redirection limit, 0 refuse any redirect, -1 for an infinite number of redirects. + redirects: i32 = 10, + /// Max body size, default 128M. + max_body_size: usize = 128 * 1024 * 1024, + }; - pub fn add(self: *@This(), k: []const u8, v: []const u8) !void { - try self.entries.put(k, v); - } + pub const Header = struct { + entries: std.StringHashMap([]const u8), + allocator: Allocator, - // Note: Caller should free returned list (after usage) with `freeCHeader`. - fn asCHeader(self: @This(), ua: []const u8) !?*c.struct_curl_slist { - if (self.entries.count() == 0) { - return null; + pub fn init(allocator: Allocator) @This() { + return .{ + .entries = std.StringHashMap([]const u8).init(allocator), + .allocator = allocator, + }; } - var lst: ?*c.struct_curl_slist = null; - var it = self.entries.iterator(); - var has_ua = false; - while (it.next()) |entry| { - if (!has_ua and std.ascii.eqlIgnoreCase(entry.key_ptr.*, HEADER_USER_AGENT)) { - has_ua = true; - } - - const kv = try fmt.allocPrintZ(self.allocator, "{s}: {s}", .{ entry.key_ptr.*, entry.value_ptr.* }); - defer self.allocator.free(kv); - - lst = c.curl_slist_append(lst, kv); + pub fn deinit(self: *@This()) void { + self.entries.deinit(); } - if (!has_ua) { - const kv = try fmt.allocPrintZ(self.allocator, "{s}: {s}", .{ HEADER_USER_AGENT, ua }); - defer self.allocator.free(kv); - lst = c.curl_slist_append(lst, kv); + pub fn add(self: *Header, k: []const u8, v: []const u8) !void { + try self.entries.put(k, v); } - return lst; - } - - fn freeCHeader(lst: *c.struct_curl_slist) void { - c.curl_slist_free_all(lst); - } -}; - -pub const RequestArgs = struct { - method: Method = .GET, - header: ?RequestHeader = null, - multi_part: ?MultiPart = null, - verbose: bool = false, - /// Redirection limit, 0 refuse any redirect, -1 for an infinite number of redirects. - redirects: i32 = 10, - /// Max body size, default 128M. - max_body_size: usize = 128 * 1024 * 1024, -}; + // Note: Caller should free returned list (after usage) with `freeCHeader`. + fn asCHeader(self: Header, ua: []const u8) !?*c.struct_curl_slist { + if (self.entries.count() == 0) { + return null; + } -pub fn Request(comptime ReaderType: type) type { - return struct { - url: []const u8, - /// body is io.Reader or void - body: ReaderType, - args: RequestArgs, + var lst: ?*c.struct_curl_slist = null; + var it = self.entries.iterator(); + var has_ua = false; + while (it.next()) |entry| { + if (!has_ua and std.ascii.eqlIgnoreCase(entry.key_ptr.*, HEADER_USER_AGENT)) { + has_ua = true; + } - pub fn init(url: []const u8, body: ReaderType, args: RequestArgs) @This() { - return .{ - .url = url, - .body = body, - .args = args, - }; - } + const kv = try fmt.allocPrintZ(self.allocator, "{s}: {s}", .{ entry.key_ptr.*, entry.value_ptr.* }); + defer self.allocator.free(kv); - pub fn deinit(self: *@This()) void { - if (self.args.header) |*h| { - h.deinit(); + lst = c.curl_slist_append(lst, kv); } - if (self.args.multi_part) |mp| { - mp.deinit(); + if (!has_ua) { + const kv = try fmt.allocPrintZ(self.allocator, "{s}: {s}", .{ HEADER_USER_AGENT, ua }); + defer self.allocator.free(kv); + + lst = c.curl_slist_append(lst, kv); } + + return lst; } - fn getVerbose(self: @This()) c_long { - return if (self.args.verbose) 1 else 0; + fn freeCHeader(lst: *c.struct_curl_slist) void { + c.curl_slist_free_all(lst); } + }; - fn getBody(self: @This(), allocator: Allocator) !?[]u8 { - if (@TypeOf(self.body) == void) { - return null; - } + pub fn init(url: [:0]const u8, opts: RequestOptions) @This() { + return .{ + .url = url, + .opts = opts, + }; + } - return try self.body.readAllAlloc(allocator, self.args.max_body_size); + pub fn deinit(self: *@This()) void { + if (self.opts.header) |*h| { + h.deinit(); } - }; -} + if (self.opts.multi_part) |mp| { + mp.deinit(); + } + } + + fn getVerbose(self: @This()) c_long { + return if (self.opts.verbose) 1 else 0; + } +}; pub const Buffer = std.ArrayList(u8); pub const Response = struct { @@ -157,20 +145,17 @@ pub const Response = struct { /// Get the first value associated with the given key. /// Applications need to copy the data if it wants to keep it around. - pub fn get(self: @This()) []const u8 { + pub fn get(self: Header) []const u8 { return mem.sliceTo(self.c_header.value, 0); } }; /// Gets the header associated with the given name. - pub fn get_header(self: @This(), name: []const u8) errors.HeaderError!?Header { + pub fn get_header(self: Response, name: [:0]const u8) errors.HeaderError!?Header { if (comptime !has_parse_header_support()) { return error.NoCurlHeaderSupport; } - const c_name = try fmt.allocPrintZ(self.allocator, "{s}", .{name}); - defer self.allocator.free(c_name); - var header: ?*c.struct_curl_header = null; const code = c.curl_easy_header(self.handle, name.ptr, 0, c.CURLH_HEADER, -1, &header); return if (errors.headerErrorFrom(code)) |err| @@ -179,7 +164,7 @@ pub const Response = struct { else => err, } else - Header{ + .{ .c_header = header.?, .name = name, }; @@ -196,31 +181,25 @@ pub const MultiPart = struct { /// Setting large data is memory consuming: one might consider using `data_callback` in such a case. data: []const u8, /// Set a mime part's body data from a file contents. - file: []const u8, + file: [:0]const u8, // TODO: https://curl.se/libcurl/c/curl_mime_data_cb.html // data_callback: u8, }; - pub fn deinit(self: @This()) void { + pub fn deinit(self: MultiPart) void { c.curl_mime_free(self.mime_handle); } - pub fn add_part(self: @This(), name: []const u8, source: DataSource) !void { + pub fn add_part(self: MultiPart, name: [:0]const u8, source: DataSource) !void { const part = if (c.curl_mime_addpart(self.mime_handle)) |part| part else return error.MimeAddPart; - const namez = try fmt.allocPrintZ(self.allocator, "{s}", .{name}); - defer self.allocator.free(namez); - - try checkCode(c.curl_mime_name(part, namez)); + try checkCode(c.curl_mime_name(part, name)); switch (source) { .data => |slice| { try checkCode(c.curl_mime_data(part, slice.ptr, slice.len)); }, .file => |filepath| { - const filepathz = try std.fmt.allocPrintZ(self.allocator, "{s}", .{filepath}); - defer self.allocator.free(filepathz); - - try checkCode(c.curl_mime_filedata(part, filepathz)); + try checkCode(c.curl_mime_filedata(part, filepath)); }, } } @@ -231,6 +210,9 @@ pub const EasyOptions = struct { /// Use zig's std.crypto.Certificate.Bundle for TLS instead of libcurl's default. /// Note that the builtin libcurl is compiled with mbedtls and does not include a CA bundle. use_std_crypto_ca_bundle: bool = true, + default_user_agent: [:0]const u8 = "zig-curl/0.1.0", + /// The maximum time in milliseconds that the entire transfer operation to take. + default_timeout_ms: usize = 30_000, }; pub fn init(allocator: Allocator, options: EasyOptions) !Self { @@ -240,7 +222,6 @@ pub fn init(allocator: Allocator, options: EasyOptions) !Self { defer bundle.deinit(allocator); try bundle.rescan(allocator); - var blob = std.ArrayList(u8).init(allocator); var iter = bundle.map.iterator(); while (iter.next()) |entry| { @@ -263,10 +244,12 @@ pub fn init(allocator: Allocator, options: EasyOptions) !Self { } }; - return if (c.curl_easy_init()) |h| + return if (c.curl_easy_init()) |handle| .{ .allocator = allocator, - .handle = h, + .handle = handle, + .timeout_ms = options.default_timeout_ms, + .user_agent = options.default_user_agent, .ca_bundle = ca_bundle, } else @@ -300,8 +283,8 @@ pub fn do(self: Self, req: anytype) !Response { const url = try fmt.allocPrintZ(self.allocator, "{s}", .{req.url}); defer self.allocator.free(url); try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_URL, url.ptr)); - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MAXREDIRS, @as(c_long, req.args.redirects))); - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CUSTOMREQUEST, req.args.method.asString().ptr)); + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MAXREDIRS, @as(c_long, req.opts.redirects))); + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CUSTOMREQUEST, req.opts.method.asString().ptr)); try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_VERBOSE, req.getVerbose())); if (self.ca_bundle) |bundle| { @@ -313,26 +296,22 @@ pub fn do(self: Self, req: anytype) !Response { try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CAINFO_BLOB, blob)); } - const body = try req.getBody(self.allocator); - defer if (body) |b| { - self.allocator.free(b); - }; - if (body) |b| { + if (req.opts.body) |b| { try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDS, b.ptr)); try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDSIZE, b.len)); } var mime_handle: ?*c.curl_mime = null; - if (req.args.multi_part) |mp| { + if (req.opts.multi_part) |mp| { mime_handle = mp.mime_handle; try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MIMEPOST, mime_handle)); } var header: ?*c.struct_curl_slist = null; - if (req.args.header) |h| { - header = try h.asCHeader(self.default_user_agent); + if (req.opts.header) |h| { + header = try h.asCHeader(self.user_agent); } - defer if (header) |h| RequestHeader.freeCHeader(h); + defer if (header) |h| Request.Header.freeCHeader(h); try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_HTTPHEADER, header)); var resp_buffer = Buffer.init(self.allocator); @@ -354,32 +333,36 @@ pub fn do(self: Self, req: anytype) !Response { } /// Get issues a GET to the specified URL. -pub fn get(self: Self, url: []const u8) !Response { - var req = Request(void).init(url, {}, .{}); +pub fn get(self: Self, url: [:0]const u8) !Response { + var req = Request.init(url, .{}); defer req.deinit(); return self.do(req); } /// Head issues a HEAD to the specified URL. -pub fn head(self: Self, url: []const u8) !Response { - var req = Request(void).init(url, {}, .{ .method = .HEAD }); +pub fn head(self: Self, url: [:0]const u8) !Response { + var req = Request.init(url, .{ .method = .HEAD }); defer req.deinit(); return self.do(req); } /// Post issues a POST to the specified URL. -pub fn post(self: Self, url: []const u8, content_type: []const u8, body: anytype) !Response { +pub fn post(self: Self, url: [:0]const u8, content_type: []const u8, body: anytype) !Response { const header = blk: { - var h = RequestHeader.init(self.allocator); + var h = Request.Header.init(self.allocator); errdefer h.deinit(); try h.add(HEADER_CONTENT_TYPE, content_type); break :blk h; }; - var req = Request(@TypeOf(body)).init(url, body, .{ + const payload = try body.readAllAlloc(self.allocator, 123); + defer self.allocator.free(payload); + + var req = Request.init(url, .{ .method = .POST, .header = header, + .body = payload, }); defer req.deinit(); From 192d2b67dd2985e86c77aeb476a60f7ed9de1e01 Mon Sep 17 00:00:00 2001 From: jiacai2050 Date: Wed, 10 Jan 2024 09:19:01 +0800 Subject: [PATCH 2/7] remove request type --- build.zig | 39 ++++---- build.zig.zon | 8 +- examples/basic.zig | 4 +- src/easy.zig | 243 ++++++++++++++++++--------------------------- 4 files changed, 120 insertions(+), 174 deletions(-) diff --git a/build.zig b/build.zig index e54c530..a5f7b76 100644 --- a/build.zig +++ b/build.zig @@ -9,34 +9,34 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const module = b.addModule(MODULE_NAME, .{ - .source_file = .{ .path = "src/main.zig" }, + .root_source_file = .{ .path = "src/main.zig" }, }); - const libcurl = b.dependency("libcurl", .{ .target = target, .optimize = optimize }); - b.installArtifact(libcurl.artifact("curl")); + // const libcurl = b.dependency("libcurl", .{ .target = target, .optimize = optimize }); + // b.installArtifact(libcurl.artifact("curl")); - try addExample(b, "basic", module, libcurl, target, optimize); - try addExample(b, "advanced", module, libcurl, target, optimize); + try addExample(b, "basic", module, target, optimize); + // try addExample(b, "advanced", module, libcurl, target, optimize); - const main_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/main.zig" }, - .target = target, - .optimize = optimize, - }); - main_tests.addModule(MODULE_NAME, module); - main_tests.linkLibrary(libcurl.artifact("curl")); + // const main_tests = b.addTest(.{ + // .root_source_file = .{ .path = "src/main.zig" }, + // .target = target, + // .optimize = optimize, + // }); + // main_tests.root_module.addImport(MODULE_NAME, module); + // main_tests.linkLibrary(libcurl.artifact("curl")); - const run_main_tests = b.addRunArtifact(main_tests); - const test_step = b.step("test", "Run library tests"); - test_step.dependOn(&run_main_tests.step); + // const run_main_tests = b.addRunArtifact(main_tests); + // const test_step = b.step("test", "Run library tests"); + // test_step.dependOn(&run_main_tests.step); } fn addExample( b: *std.Build, comptime name: []const u8, curl_module: *Module, - libcurl: *Build.Dependency, - target: std.zig.CrossTarget, + // libcurl: *Build.Dependency, + target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, ) !void { const exe = b.addExecutable(.{ @@ -47,8 +47,9 @@ fn addExample( }); b.installArtifact(exe); - exe.addModule(MODULE_NAME, curl_module); - exe.linkLibrary(libcurl.artifact("curl")); + exe.root_module.addImport(MODULE_NAME, curl_module); + // exe.linkLibrary(libcurl.artifact("curl")); + exe.linkSystemLibrary("curl"); const run_step = b.step("run-" ++ name, std.fmt.comptimePrint("Run {s} example", .{name})); run_step.dependOn(&b.addRunArtifact(exe).step); diff --git a/build.zig.zon b/build.zig.zon index a4a3cd2..326fc83 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -8,9 +8,9 @@ "LICENSE", }, .dependencies = .{ - .libcurl = .{ - .url = "https://github.com/Cloudef/zigcurl/archive/e9e726131d3eb80c02d20b7ee5ca6935324d6697.tar.gz", - .hash = "1220970ad7d9b96b3464fc12c1e67998881bc4d07c03b8b7d75a5367e4c9991ef099", - }, + // .libcurl = .{ + // .url = "https://github.com/star-tek-mb/zigcurl/archive/41379c8.tar.gz", + // .hash = "1220424bef91a3ffe97980b7a45062a2c1b26c3bbcefbda301419ad14c3641abcf57", + // }, }, } diff --git a/examples/basic.zig b/examples/basic.zig index 5c98168..0029e84 100644 --- a/examples/basic.zig +++ b/examples/basic.zig @@ -39,6 +39,6 @@ pub fn main() !void { println("GET demo"); try get(easy); - println("POST demo"); - try post(easy); + // println("POST demo"); + // try post(easy); } diff --git a/src/easy.zig b/src/easy.zig index 3181a53..bd410d7 100644 --- a/src/easy.zig +++ b/src/easy.zig @@ -14,6 +14,7 @@ const Self = @This(); allocator: Allocator, handle: *c.CURL, +headers: ?*c.struct_curl_slist, timeout_ms: usize, user_agent: [:0]const u8, ca_bundle: ?[]const u8, @@ -36,97 +37,6 @@ pub const Method = enum { } }; -pub const Request = struct { - url: [:0]const u8, - /// body is io.Reader or void - opts: Options, - - pub const Options = struct { - method: Method = .GET, - header: ?Request.Header = null, - multi_part: ?MultiPart = null, - body: ?[]const u8 = null, - verbose: bool = false, - /// Redirection limit, 0 refuse any redirect, -1 for an infinite number of redirects. - redirects: i32 = 10, - /// Max body size, default 128M. - max_body_size: usize = 128 * 1024 * 1024, - }; - - pub const Header = struct { - entries: std.StringHashMap([]const u8), - allocator: Allocator, - - pub fn init(allocator: Allocator) @This() { - return .{ - .entries = std.StringHashMap([]const u8).init(allocator), - .allocator = allocator, - }; - } - - pub fn deinit(self: *@This()) void { - self.entries.deinit(); - } - - pub fn add(self: *Header, k: []const u8, v: []const u8) !void { - try self.entries.put(k, v); - } - - // Note: Caller should free returned list (after usage) with `freeCHeader`. - fn asCHeader(self: Header, ua: []const u8) !?*c.struct_curl_slist { - if (self.entries.count() == 0) { - return null; - } - - var lst: ?*c.struct_curl_slist = null; - var it = self.entries.iterator(); - var has_ua = false; - while (it.next()) |entry| { - if (!has_ua and std.ascii.eqlIgnoreCase(entry.key_ptr.*, HEADER_USER_AGENT)) { - has_ua = true; - } - - const kv = try fmt.allocPrintZ(self.allocator, "{s}: {s}", .{ entry.key_ptr.*, entry.value_ptr.* }); - defer self.allocator.free(kv); - - lst = c.curl_slist_append(lst, kv); - } - if (!has_ua) { - const kv = try fmt.allocPrintZ(self.allocator, "{s}: {s}", .{ HEADER_USER_AGENT, ua }); - defer self.allocator.free(kv); - - lst = c.curl_slist_append(lst, kv); - } - - return lst; - } - - fn freeCHeader(lst: *c.struct_curl_slist) void { - c.curl_slist_free_all(lst); - } - }; - - pub fn init(url: [:0]const u8, opts: RequestOptions) @This() { - return .{ - .url = url, - .opts = opts, - }; - } - - pub fn deinit(self: *@This()) void { - if (self.opts.header) |*h| { - h.deinit(); - } - if (self.opts.multi_part) |mp| { - mp.deinit(); - } - } - - fn getVerbose(self: @This()) c_long { - return if (self.opts.verbose) 1 else 0; - } -}; - pub const Buffer = std.ArrayList(u8); pub const Response = struct { body: Buffer, @@ -251,6 +161,7 @@ pub fn init(allocator: Allocator, options: EasyOptions) !Self { .timeout_ms = options.default_timeout_ms, .user_agent = options.default_user_agent, .ca_bundle = ca_bundle, + .headers = null, } else error.CurlInit; @@ -261,6 +172,7 @@ pub fn deinit(self: Self) void { self.allocator.free(bundle); } + c.curl_slist_free_all(self.headers); c.curl_easy_cleanup(self.handle); } @@ -274,45 +186,72 @@ pub fn add_multi_part(self: Self) !MultiPart { error.MimeInit; } -/// Do sends an HTTP request and returns an HTTP response. -pub fn do(self: Self, req: anytype) !Response { - // Re-initializes all options previously set on a specified CURL handle to the default values. - defer c.curl_easy_reset(self.handle); - - try self.set_common_opts(); - const url = try fmt.allocPrintZ(self.allocator, "{s}", .{req.url}); - defer self.allocator.free(url); +pub fn set_url(self: Self, url: [:0]const u8) !void { try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_URL, url.ptr)); - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MAXREDIRS, @as(c_long, req.opts.redirects))); - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CUSTOMREQUEST, req.opts.method.asString().ptr)); - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_VERBOSE, req.getVerbose())); +} - if (self.ca_bundle) |bundle| { - const blob = c.curl_blob{ - .data = @constCast(bundle.ptr), - .len = bundle.len, - .flags = c.CURL_BLOB_NOCOPY, - }; - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CAINFO_BLOB, blob)); - } +pub fn set_max_redirects(self: Self, redirects: u32) !void { + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MAXREDIRS, @as(c_long, redirects))); +} - if (req.opts.body) |b| { - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDS, b.ptr)); - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDSIZE, b.len)); +pub fn set_method(self: Self, method: Method) !void { + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CUSTOMREQUEST, method.asString().ptr)); + return self; +} + +pub fn set_verbose(self: Self, verbose: bool) !void { + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_VERBOSE, verbose)); +} + +pub fn set_post_fields(self: Self, body: []const u8) !void { + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDS, body.ptr)); + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDSIZE, body.len)); + return self; +} + +pub fn reset(self: Self) void { + c.curl_easy_reset(self.handle); +} + +pub fn set_headers(self: Self, headers: std.StringHashMap([:0]const u8)) !Self { + if (headers.count() == 0) { + return null; } - var mime_handle: ?*c.curl_mime = null; - if (req.opts.multi_part) |mp| { - mime_handle = mp.mime_handle; - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MIMEPOST, mime_handle)); + var lst: ?*c.struct_curl_slist = null; + var it = headers.iterator(); + var has_ua = false; + while (it.next()) |entry| { + if (!has_ua and std.ascii.eqlIgnoreCase(entry.key_ptr.*, HEADER_USER_AGENT)) { + has_ua = true; + } + + const kv = try fmt.allocPrint(self.allocator, "{s}: {s}", .{ entry.key_ptr.*, entry.value_ptr.* }); + defer self.allocator.free(kv); + + lst = c.curl_slist_append(lst, kv); } + if (!has_ua) { + const kv = try fmt.allocPrintZ(self.allocator, "{s}: {s}", .{ HEADER_USER_AGENT, self.user_agent }); + defer self.allocator.free(kv); - var header: ?*c.struct_curl_slist = null; - if (req.opts.header) |h| { - header = try h.asCHeader(self.user_agent); + lst = c.curl_slist_append(lst, kv); } - defer if (header) |h| Request.Header.freeCHeader(h); - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_HTTPHEADER, header)); + + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_HTTPHEADER, lst)); + self.headers = lst; + return self; +} + +/// Perform sends an HTTP request and returns an HTTP response. +pub fn perform(self: Self) !Response { + try self.set_common_opts(); + + // var mime_handle: ?*c.curl_mime = null; + // if (req.opts.multi_part) |mp| { + // mime_handle = mp.mime_handle; + // try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MIMEPOST, mime_handle)); + // } var resp_buffer = Buffer.init(self.allocator); errdefer resp_buffer.deinit(); @@ -334,40 +273,38 @@ pub fn do(self: Self, req: anytype) !Response { /// Get issues a GET to the specified URL. pub fn get(self: Self, url: [:0]const u8) !Response { - var req = Request.init(url, .{}); - defer req.deinit(); - - return self.do(req); + try self.set_url(url); + return self.perform(); } /// Head issues a HEAD to the specified URL. pub fn head(self: Self, url: [:0]const u8) !Response { - var req = Request.init(url, .{ .method = .HEAD }); - defer req.deinit(); + try self.set_url(url); + try self.set_method(.HEAD); - return self.do(req); + return self.perform(); } -/// Post issues a POST to the specified URL. -pub fn post(self: Self, url: [:0]const u8, content_type: []const u8, body: anytype) !Response { - const header = blk: { - var h = Request.Header.init(self.allocator); - errdefer h.deinit(); - try h.add(HEADER_CONTENT_TYPE, content_type); - break :blk h; - }; - const payload = try body.readAllAlloc(self.allocator, 123); - defer self.allocator.free(payload); - - var req = Request.init(url, .{ - .method = .POST, - .header = header, - .body = payload, - }); - defer req.deinit(); - - return self.do(req); -} +// /// Post issues a POST to the specified URL. +// pub fn post(self: Self, url: [:0]const u8, content_type: []const u8, body: anytype) !Response { +// const header = blk: { +// var h = Request.Header.init(self.allocator); +// errdefer h.deinit(); +// try h.add(HEADER_CONTENT_TYPE, content_type); +// break :blk h; +// }; +// const payload = try body.readAllAlloc(self.allocator, 123); +// defer self.allocator.free(payload); + +// var req = Request.init(url, .{ +// .method = .POST, +// .header = header, +// .body = payload, +// }); +// defer req.deinit(); + +// return self.do(req); +// } /// Used for https://curl.se/libcurl/c/CURLOPT_WRITEFUNCTION.html // size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata); @@ -382,6 +319,14 @@ fn write_callback(ptr: [*c]c_char, size: c_uint, nmemb: c_uint, user_data: *anyo } fn set_common_opts(self: Self) !void { + if (self.ca_bundle) |bundle| { + const blob = c.curl_blob{ + .data = @constCast(bundle.ptr), + .len = bundle.len, + .flags = c.CURL_BLOB_NOCOPY, + }; + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CAINFO_BLOB, blob)); + } try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_TIMEOUT_MS, self.timeout_ms)); } From 46043f1a479060820df946c8aa235a4f00769081 Mon Sep 17 00:00:00 2001 From: jiacai2050 Date: Wed, 10 Jan 2024 22:20:26 +0800 Subject: [PATCH 3/7] assert basic run --- examples/basic.zig | 52 +++++++++++++++++++++++++++------ src/easy.zig | 71 +++++++++++----------------------------------- src/util.zig | 30 +++++++++++++++++++- 3 files changed, 90 insertions(+), 63 deletions(-) diff --git a/examples/basic.zig b/examples/basic.zig index 0029e84..c0911f9 100644 --- a/examples/basic.zig +++ b/examples/basic.zig @@ -5,7 +5,7 @@ const Allocator = mem.Allocator; const curl = @import("curl"); const Easy = curl.Easy; -fn get(easy: Easy) !void { +fn get(allocator: Allocator, easy: Easy) !void { const resp = try easy.get("https://httpbin.org/anything"); defer resp.deinit(); @@ -13,19 +13,55 @@ fn get(easy: Easy) !void { resp.status_code, resp.body.items, }); + + const Response = struct { + headers: struct { + Host: []const u8, + }, + method: []const u8, + }; + const parsed = try std.json.parseFromSlice(Response, allocator, resp.body.items, .{ + .ignore_unknown_fields = true, + }); + defer parsed.deinit(); + + try std.testing.expectEqualDeep(parsed.value, Response{ + .headers = .{ .Host = "httpbin.org" }, + .method = "GET", + }); } -fn post(easy: Easy) !void { - var payload = std.io.fixedBufferStream( +fn post(allocator: Allocator, easy: Easy) !void { + const payload = \\{"name": "John", "age": 15} - ); - const resp = try easy.post("https://httpbin.org/anything", "application/json", payload.reader()); + ; + const resp = try easy.post("https://httpbin.org/anything", "application/json", payload); defer resp.deinit(); std.debug.print("Status code: {d}\nBody: {s}\n", .{ resp.status_code, resp.body.items, }); + + const Response = struct { + headers: struct { + @"User-Agent": []const u8, + @"Content-Type": []const u8, + }, + json: struct { + name: []const u8, + age: u32, + }, + method: []const u8, + }; + const parsed = try std.json.parseFromSlice(Response, allocator, resp.body.items, .{ .ignore_unknown_fields = true }); + defer parsed.deinit(); + + try std.testing.expectEqualDeep(parsed.value, Response{ + .headers = .{ .@"User-Agent" = "zig-curl/0.1.0", .@"Content-Type" = "application/json" }, + .json = .{ .name = "John", .age = 15 }, + .method = "POST", + }); } pub fn main() !void { @@ -37,8 +73,8 @@ pub fn main() !void { defer easy.deinit(); println("GET demo"); - try get(easy); + try get(allocator, easy); - // println("POST demo"); - // try post(easy); + println("POST demo"); + try post(allocator, easy); } diff --git a/src/easy.zig b/src/easy.zig index bd410d7..0d7a27f 100644 --- a/src/easy.zig +++ b/src/easy.zig @@ -14,13 +14,10 @@ const Self = @This(); allocator: Allocator, handle: *c.CURL, -headers: ?*c.struct_curl_slist, timeout_ms: usize, user_agent: [:0]const u8, ca_bundle: ?[]const u8, -pub const HEADER_CONTENT_TYPE: []const u8 = "Content-Type"; -pub const HEADER_USER_AGENT: []const u8 = "User-Agent"; const CERT_MARKER_BEGIN = "-----BEGIN CERTIFICATE-----"; const CERT_MARKER_END = "\n-----END CERTIFICATE-----\n"; @@ -161,7 +158,6 @@ pub fn init(allocator: Allocator, options: EasyOptions) !Self { .timeout_ms = options.default_timeout_ms, .user_agent = options.default_user_agent, .ca_bundle = ca_bundle, - .headers = null, } else error.CurlInit; @@ -172,7 +168,6 @@ pub fn deinit(self: Self) void { self.allocator.free(bundle); } - c.curl_slist_free_all(self.headers); c.curl_easy_cleanup(self.handle); } @@ -206,41 +201,14 @@ pub fn set_verbose(self: Self, verbose: bool) !void { pub fn set_post_fields(self: Self, body: []const u8) !void { try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDS, body.ptr)); try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDSIZE, body.len)); - return self; } pub fn reset(self: Self) void { c.curl_easy_reset(self.handle); } -pub fn set_headers(self: Self, headers: std.StringHashMap([:0]const u8)) !Self { - if (headers.count() == 0) { - return null; - } - - var lst: ?*c.struct_curl_slist = null; - var it = headers.iterator(); - var has_ua = false; - while (it.next()) |entry| { - if (!has_ua and std.ascii.eqlIgnoreCase(entry.key_ptr.*, HEADER_USER_AGENT)) { - has_ua = true; - } - - const kv = try fmt.allocPrint(self.allocator, "{s}: {s}", .{ entry.key_ptr.*, entry.value_ptr.* }); - defer self.allocator.free(kv); - - lst = c.curl_slist_append(lst, kv); - } - if (!has_ua) { - const kv = try fmt.allocPrintZ(self.allocator, "{s}: {s}", .{ HEADER_USER_AGENT, self.user_agent }); - defer self.allocator.free(kv); - - lst = c.curl_slist_append(lst, kv); - } - - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_HTTPHEADER, lst)); - self.headers = lst; - return self; +pub fn set_headers(self: Self, headers: *c.struct_curl_slist) !void { + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_HTTPHEADER, headers)); } /// Perform sends an HTTP request and returns an HTTP response. @@ -264,7 +232,7 @@ pub fn perform(self: Self) !Response { try checkCode(c.curl_easy_getinfo(self.handle, c.CURLINFO_RESPONSE_CODE, &status_code)); return .{ - .status_code = @truncate(status_code), + .status_code = @intCast(status_code), .body = resp_buffer, .handle = self.handle, .allocator = self.allocator, @@ -286,25 +254,20 @@ pub fn head(self: Self, url: [:0]const u8) !Response { } // /// Post issues a POST to the specified URL. -// pub fn post(self: Self, url: [:0]const u8, content_type: []const u8, body: anytype) !Response { -// const header = blk: { -// var h = Request.Header.init(self.allocator); -// errdefer h.deinit(); -// try h.add(HEADER_CONTENT_TYPE, content_type); -// break :blk h; -// }; -// const payload = try body.readAllAlloc(self.allocator, 123); -// defer self.allocator.free(payload); - -// var req = Request.init(url, .{ -// .method = .POST, -// .header = header, -// .body = payload, -// }); -// defer req.deinit(); - -// return self.do(req); -// } +pub fn post(self: Self, url: [:0]const u8, content_type: []const u8, body: []const u8) !Response { + try self.set_url(url); + try self.set_post_fields(body); + + var headers = std.StringHashMap([]const u8).init(self.allocator); + try headers.put(util.HEADER_CONTENT_TYPE, content_type); + defer headers.deinit(); + + const c_headers = try util.map_to_headers(self.allocator, headers, self.user_agent); + defer c.curl_slist_free_all(c_headers); + + try self.set_headers(c_headers); + return self.perform(); +} /// Used for https://curl.se/libcurl/c/CURLOPT_WRITEFUNCTION.html // size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata); diff --git a/src/util.zig b/src/util.zig index b84436d..939394e 100644 --- a/src/util.zig +++ b/src/util.zig @@ -1,11 +1,39 @@ const std = @import("std"); +const c = @import("c.zig").c; const Allocator = std.mem.Allocator; - const Encoder = std.base64.standard.Encoder; +pub const HEADER_CONTENT_TYPE: []const u8 = "Content-Type"; +pub const HEADER_USER_AGENT: []const u8 = "User-Agent"; + pub fn encode_base64(allocator: Allocator, input: []const u8) ![]const u8 { const encoded_len = Encoder.calcSize(input.len); const dest = try allocator.alloc(u8, encoded_len); return Encoder.encode(dest, input); } + +pub fn map_to_headers(allocator: std.mem.Allocator, map: std.StringHashMap([]const u8), user_agent: []const u8) !*c.struct_curl_slist { + var headers: ?*c.struct_curl_slist = null; + var has_ua = false; + var iterator = map.iterator(); + while (iterator.next()) |item| { + const key = item.key_ptr.*; + const value = item.value_ptr.*; + const header = try std.fmt.allocPrintZ(allocator, "{s}: {s}", .{ key, value }); + defer allocator.free(header); + + headers = c.curl_slist_append(headers, header); + + if (!has_ua and std.ascii.eqlIgnoreCase(key, HEADER_USER_AGENT)) { + has_ua = true; + } + } + if (!has_ua) { + const kv = try std.fmt.allocPrintZ(allocator, "{s}: {s}", .{ HEADER_USER_AGENT, user_agent }); + defer allocator.free(kv); + + headers = c.curl_slist_append(headers, kv); + } + return headers.?; +} From 16a532d0c4fe4788e187f8b2763d1f066624f995 Mon Sep 17 00:00:00 2001 From: jiacai2050 Date: Tue, 16 Jan 2024 20:31:50 +0800 Subject: [PATCH 4/7] refactor examples --- .github/workflows/CI.yml | 4 +-- Makefile | 4 +-- build.zig | 2 +- examples/advanced.zig | 53 ++++++++++++++++++++-------------------- examples/basic.zig | 6 ++--- src/easy.zig | 32 ++++++++++++++++-------- src/main.zig | 1 + src/util.zig | 1 + 8 files changed, 58 insertions(+), 45 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 23777ff..309f0c1 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -34,7 +34,7 @@ jobs: make test - name: Run examples run: | - make run-examples + make run - name: Memory leak detect # Wait https://github.com/ziglang/zig/issues/15547 if: false @@ -61,4 +61,4 @@ jobs: version: master - name: Run examples run: | - make run-examples + make run diff --git a/Makefile b/Makefile index ae4867e..76c270a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -run-examples: +run: zig build run-basic -freference-trace zig build run-advanced -freference-trace @@ -9,4 +9,4 @@ test: docs: zig build-lib -femit-docs src/main.zig -.PHONY: test run-examples docs +.PHONY: test run docs diff --git a/build.zig b/build.zig index a5f7b76..6e09a36 100644 --- a/build.zig +++ b/build.zig @@ -16,7 +16,7 @@ pub fn build(b: *std.Build) void { // b.installArtifact(libcurl.artifact("curl")); try addExample(b, "basic", module, target, optimize); - // try addExample(b, "advanced", module, libcurl, target, optimize); + try addExample(b, "advanced", module, target, optimize); // const main_tests = b.addTest(.{ // .root_source_file = .{ .path = "src/main.zig" }, diff --git a/examples/advanced.zig b/examples/advanced.zig index c91b7f3..7de83ec 100644 --- a/examples/advanced.zig +++ b/examples/advanced.zig @@ -7,7 +7,7 @@ const Easy = curl.Easy; const UA = "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0"; -const Resposne = struct { +const Response = struct { headers: struct { @"User-Agent": []const u8, Authorization: []const u8, @@ -20,28 +20,28 @@ const Resposne = struct { url: []const u8, }; -fn put_with_custom_header(allocator: Allocator, easy: Easy) !void { +fn put_with_custom_header(allocator: Allocator, easy: *Easy) !void { const body = \\ {"name": "John", "age": 15} ; - const header = blk: { - var h = curl.Request.Header.init(allocator); + var headers = blk: { + var h = curl.Headers.init(allocator); errdefer h.deinit(); - try h.add(curl.HEADER_CONTENT_TYPE, "application/json"); - try h.add("user-agent", UA); - try h.add("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"); + try h.put(curl.HEADER_CONTENT_TYPE, "application/json"); + try h.put("user-agent", UA); + try h.put("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"); break :blk h; }; - var req = curl.Request.init("https://httpbin.org/anything/zig-curl", .{ - .method = .PUT, - .header = header, - .verbose = true, - .body = body, - }); - defer req.deinit(); + defer headers.deinit(); + + try easy.set_url("https://httpbin.org/anything/zig-curl"); + try easy.set_headers(headers); + try easy.set_method(.PUT); + try easy.set_verbose(true); + try easy.set_post_fields(body); - const resp = try easy.do(req); + const resp = try easy.perform(); defer resp.deinit(); std.debug.print("Status code: {d}\nBody: {s}\n", .{ @@ -49,14 +49,14 @@ fn put_with_custom_header(allocator: Allocator, easy: Easy) !void { resp.body.items, }); - const parsed = try std.json.parseFromSlice(Resposne, allocator, resp.body.items, .{ + const parsed = try std.json.parseFromSlice(Response, allocator, resp.body.items, .{ .ignore_unknown_fields = true, }); defer parsed.deinit(); try std.testing.expectEqualDeep( parsed.value, - .{ + Response{ .headers = .{ .@"User-Agent" = UA, .Authorization = "Basic YWxhZGRpbjpvcGVuc2VzYW1l", @@ -83,20 +83,19 @@ fn put_with_custom_header(allocator: Allocator, easy: Easy) !void { } fn post_mutli_part(easy: Easy) !void { - const multi_part = try easy.add_multi_part(); + const multi_part = try easy.create_multi_part(); try multi_part.add_part("foo", .{ .data = "hello foo" }); try multi_part.add_part("bar", .{ .data = "hello bar" }); try multi_part.add_part("build.zig", .{ .file = "build.zig" }); try multi_part.add_part("readme", .{ .file = "README.org" }); + defer multi_part.deinit(); - var req = curl.Request.init("https://httpbin.org/anything/mp", .{ - .method = .PUT, - .multi_part = multi_part, - .verbose = true, - }); - defer req.deinit(); + try easy.set_url("https://httpbin.org/anything/mp"); + try easy.set_method(.PUT); + try easy.set_multi_part(multi_part); + try easy.set_verbose(true); - const resp = try easy.do(req); + const resp = try easy.perform(); defer resp.deinit(); std.debug.print("resp:{s}\n", .{resp.body.items}); @@ -107,12 +106,12 @@ pub fn main() !void { defer if (gpa.deinit() != .ok) @panic("leak"); const allocator = gpa.allocator(); - const easy = try Easy.init(allocator, .{}); + var easy = try Easy.init(allocator, .{}); defer easy.deinit(); curl.print_libcurl_version(); println("PUT with custom header demo"); - try put_with_custom_header(allocator, easy); + try put_with_custom_header(allocator, &easy); try post_mutli_part(easy); } diff --git a/examples/basic.zig b/examples/basic.zig index c0911f9..cbd6d90 100644 --- a/examples/basic.zig +++ b/examples/basic.zig @@ -31,7 +31,7 @@ fn get(allocator: Allocator, easy: Easy) !void { }); } -fn post(allocator: Allocator, easy: Easy) !void { +fn post(allocator: Allocator, easy: *Easy) !void { const payload = \\{"name": "John", "age": 15} ; @@ -69,12 +69,12 @@ pub fn main() !void { defer if (gpa.deinit() != .ok) @panic("leak"); const allocator = gpa.allocator(); - const easy = try Easy.init(allocator, .{}); + var easy = try Easy.init(allocator, .{}); defer easy.deinit(); println("GET demo"); try get(allocator, easy); println("POST demo"); - try post(allocator, easy); + try post(allocator, &easy); } diff --git a/src/easy.zig b/src/easy.zig index 0d7a27f..51b577e 100644 --- a/src/easy.zig +++ b/src/easy.zig @@ -14,6 +14,7 @@ const Self = @This(); allocator: Allocator, handle: *c.CURL, +headers: ?*c.struct_curl_slist, timeout_ms: usize, user_agent: [:0]const u8, ca_bundle: ?[]const u8, @@ -34,6 +35,7 @@ pub const Method = enum { } }; +pub const Headers = std.StringHashMap([]const u8); pub const Buffer = std.ArrayList(u8); pub const Response = struct { body: Buffer, @@ -158,6 +160,7 @@ pub fn init(allocator: Allocator, options: EasyOptions) !Self { .timeout_ms = options.default_timeout_ms, .user_agent = options.default_user_agent, .ca_bundle = ca_bundle, + .headers = null, } else error.CurlInit; @@ -168,10 +171,14 @@ pub fn deinit(self: Self) void { self.allocator.free(bundle); } + if (self.headers) |h| { + c.curl_slist_free_all(h); + } + c.curl_easy_cleanup(self.handle); } -pub fn add_multi_part(self: Self) !MultiPart { +pub fn create_multi_part(self: Self) !MultiPart { return if (c.curl_mime_init(self.handle)) |h| .{ .allocator = self.allocator, @@ -191,7 +198,6 @@ pub fn set_max_redirects(self: Self, redirects: u32) !void { pub fn set_method(self: Self, method: Method) !void { try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_CUSTOMREQUEST, method.asString().ptr)); - return self; } pub fn set_verbose(self: Self, verbose: bool) !void { @@ -203,12 +209,21 @@ pub fn set_post_fields(self: Self, body: []const u8) !void { try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_POSTFIELDSIZE, body.len)); } +pub fn set_multi_part(self: Self, multi_part: MultiPart) !void { + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MIMEPOST, multi_part.mime_handle)); +} + pub fn reset(self: Self) void { c.curl_easy_reset(self.handle); } -pub fn set_headers(self: Self, headers: *c.struct_curl_slist) !void { - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_HTTPHEADER, headers)); +pub fn set_headers(self: *Self, headers: Headers) !void { + const c_headers = try util.map_to_headers(self.allocator, headers, self.user_agent); + if (self.headers) |h| { + c.curl_slist_free_all(h); + } + self.headers = c_headers; + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_HTTPHEADER, c_headers)); } /// Perform sends an HTTP request and returns an HTTP response. @@ -254,18 +269,15 @@ pub fn head(self: Self, url: [:0]const u8) !Response { } // /// Post issues a POST to the specified URL. -pub fn post(self: Self, url: [:0]const u8, content_type: []const u8, body: []const u8) !Response { +pub fn post(self: *Self, url: [:0]const u8, content_type: []const u8, body: []const u8) !Response { try self.set_url(url); try self.set_post_fields(body); - var headers = std.StringHashMap([]const u8).init(self.allocator); + var headers = Headers.init(self.allocator); try headers.put(util.HEADER_CONTENT_TYPE, content_type); defer headers.deinit(); - const c_headers = try util.map_to_headers(self.allocator, headers, self.user_agent); - defer c.curl_slist_free_all(c_headers); - - try self.set_headers(c_headers); + try self.set_headers(headers); return self.perform(); } diff --git a/src/main.zig b/src/main.zig index 3fb6ee3..5668b47 100644 --- a/src/main.zig +++ b/src/main.zig @@ -5,6 +5,7 @@ const checkCode = @import("errors.zig").checkCode; pub const Easy = @import("easy.zig"); pub usingnamespace Easy; pub usingnamespace @import("c.zig"); +pub usingnamespace @import("util.zig"); /// This function sets up the program environment that libcurl needs. /// Since this function is not thread safe before libcurl 7.84.0, this function diff --git a/src/util.zig b/src/util.zig index 939394e..e39deb5 100644 --- a/src/util.zig +++ b/src/util.zig @@ -29,6 +29,7 @@ pub fn map_to_headers(allocator: std.mem.Allocator, map: std.StringHashMap([]con has_ua = true; } } + if (!has_ua) { const kv = try std.fmt.allocPrintZ(allocator, "{s}: {s}", .{ HEADER_USER_AGENT, user_agent }); defer allocator.free(kv); From 45b6cc0961d969b33b3808ba07a204d19c588aa8 Mon Sep 17 00:00:00 2001 From: jiacai2050 Date: Tue, 16 Jan 2024 21:18:16 +0800 Subject: [PATCH 5/7] fix header --- examples/advanced.zig | 17 ++++++++----- examples/basic.zig | 7 +++-- src/easy.zig | 59 +++++++++++++++++++++++++------------------ 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/examples/advanced.zig b/examples/advanced.zig index 7de83ec..4f53b31 100644 --- a/examples/advanced.zig +++ b/examples/advanced.zig @@ -20,17 +20,17 @@ const Response = struct { url: []const u8, }; -fn put_with_custom_header(allocator: Allocator, easy: *Easy) !void { +fn put_with_custom_header(allocator: Allocator, easy: Easy) !void { const body = \\ {"name": "John", "age": 15} ; - var headers = blk: { - var h = curl.Headers.init(allocator); + const headers = blk: { + var h = try easy.create_headers(); errdefer h.deinit(); - try h.put(curl.HEADER_CONTENT_TYPE, "application/json"); - try h.put("user-agent", UA); - try h.put("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"); + try h.add(curl.HEADER_CONTENT_TYPE, "application/json"); + try h.add("user-agent", UA); + try h.add("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"); break :blk h; }; defer headers.deinit(); @@ -83,6 +83,9 @@ fn put_with_custom_header(allocator: Allocator, easy: *Easy) !void { } fn post_mutli_part(easy: Easy) !void { + // Reset old options, e.g. headers. + easy.reset(); + const multi_part = try easy.create_multi_part(); try multi_part.add_part("foo", .{ .data = "hello foo" }); try multi_part.add_part("bar", .{ .data = "hello bar" }); @@ -112,6 +115,6 @@ pub fn main() !void { curl.print_libcurl_version(); println("PUT with custom header demo"); - try put_with_custom_header(allocator, &easy); + try put_with_custom_header(allocator, easy); try post_mutli_part(easy); } diff --git a/examples/basic.zig b/examples/basic.zig index cbd6d90..0cf5756 100644 --- a/examples/basic.zig +++ b/examples/basic.zig @@ -31,7 +31,7 @@ fn get(allocator: Allocator, easy: Easy) !void { }); } -fn post(allocator: Allocator, easy: *Easy) !void { +fn post(allocator: Allocator, easy: Easy) !void { const payload = \\{"name": "John", "age": 15} ; @@ -45,7 +45,6 @@ fn post(allocator: Allocator, easy: *Easy) !void { const Response = struct { headers: struct { - @"User-Agent": []const u8, @"Content-Type": []const u8, }, json: struct { @@ -58,7 +57,7 @@ fn post(allocator: Allocator, easy: *Easy) !void { defer parsed.deinit(); try std.testing.expectEqualDeep(parsed.value, Response{ - .headers = .{ .@"User-Agent" = "zig-curl/0.1.0", .@"Content-Type" = "application/json" }, + .headers = .{ .@"Content-Type" = "application/json" }, .json = .{ .name = "John", .age = 15 }, .method = "POST", }); @@ -76,5 +75,5 @@ pub fn main() !void { try get(allocator, easy); println("POST demo"); - try post(allocator, &easy); + try post(allocator, easy); } diff --git a/src/easy.zig b/src/easy.zig index 51b577e..3cc6dc4 100644 --- a/src/easy.zig +++ b/src/easy.zig @@ -14,9 +14,7 @@ const Self = @This(); allocator: Allocator, handle: *c.CURL, -headers: ?*c.struct_curl_slist, timeout_ms: usize, -user_agent: [:0]const u8, ca_bundle: ?[]const u8, const CERT_MARKER_BEGIN = "-----BEGIN CERTIFICATE-----"; @@ -35,7 +33,29 @@ pub const Method = enum { } }; -pub const Headers = std.StringHashMap([]const u8); +pub const Headers = struct { + headers: ?*c.struct_curl_slist, + allocator: Allocator, + + pub fn init(allocator: Allocator) !Headers { + return .{ + .allocator = allocator, + .headers = null, + }; + } + + pub fn deinit(self: Headers) void { + c.curl_slist_free_all(self.headers); + } + + pub fn add(self: *Headers, name: []const u8, value: []const u8) !void { + const header = try std.fmt.allocPrintZ(self.allocator, "{s}: {s}", .{ name, value }); + defer self.allocator.free(header); + + self.headers = c.curl_slist_append(self.headers, header); + } +}; + pub const Buffer = std.ArrayList(u8); pub const Response = struct { body: Buffer, @@ -119,7 +139,6 @@ pub const EasyOptions = struct { /// Use zig's std.crypto.Certificate.Bundle for TLS instead of libcurl's default. /// Note that the builtin libcurl is compiled with mbedtls and does not include a CA bundle. use_std_crypto_ca_bundle: bool = true, - default_user_agent: [:0]const u8 = "zig-curl/0.1.0", /// The maximum time in milliseconds that the entire transfer operation to take. default_timeout_ms: usize = 30_000, }; @@ -158,9 +177,7 @@ pub fn init(allocator: Allocator, options: EasyOptions) !Self { .allocator = allocator, .handle = handle, .timeout_ms = options.default_timeout_ms, - .user_agent = options.default_user_agent, .ca_bundle = ca_bundle, - .headers = null, } else error.CurlInit; @@ -171,13 +188,13 @@ pub fn deinit(self: Self) void { self.allocator.free(bundle); } - if (self.headers) |h| { - c.curl_slist_free_all(h); - } - c.curl_easy_cleanup(self.handle); } +pub fn create_headers(self: Self) !Headers { + return Headers.init(self.allocator); +} + pub fn create_multi_part(self: Self) !MultiPart { return if (c.curl_mime_init(self.handle)) |h| .{ @@ -217,25 +234,16 @@ pub fn reset(self: Self) void { c.curl_easy_reset(self.handle); } -pub fn set_headers(self: *Self, headers: Headers) !void { - const c_headers = try util.map_to_headers(self.allocator, headers, self.user_agent); - if (self.headers) |h| { - c.curl_slist_free_all(h); +pub fn set_headers(self: Self, headers: Headers) !void { + if (headers.headers) |c_headers| { + try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_HTTPHEADER, c_headers)); } - self.headers = c_headers; - try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_HTTPHEADER, c_headers)); } /// Perform sends an HTTP request and returns an HTTP response. pub fn perform(self: Self) !Response { try self.set_common_opts(); - // var mime_handle: ?*c.curl_mime = null; - // if (req.opts.multi_part) |mp| { - // mime_handle = mp.mime_handle; - // try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_MIMEPOST, mime_handle)); - // } - var resp_buffer = Buffer.init(self.allocator); errdefer resp_buffer.deinit(); try checkCode(c.curl_easy_setopt(self.handle, c.CURLOPT_WRITEDATA, &resp_buffer)); @@ -269,14 +277,15 @@ pub fn head(self: Self, url: [:0]const u8) !Response { } // /// Post issues a POST to the specified URL. -pub fn post(self: *Self, url: [:0]const u8, content_type: []const u8, body: []const u8) !Response { +pub fn post(self: Self, url: [:0]const u8, content_type: []const u8, body: []const u8) !Response { try self.set_url(url); try self.set_post_fields(body); - var headers = Headers.init(self.allocator); - try headers.put(util.HEADER_CONTENT_TYPE, content_type); + var headers = try self.create_headers(); defer headers.deinit(); + try headers.add(util.HEADER_CONTENT_TYPE, content_type); + try self.set_headers(headers); return self.perform(); } From f1f560f662dbefee629bc56fca1e835aab96e433 Mon Sep 17 00:00:00 2001 From: jiacai2050 Date: Tue, 16 Jan 2024 21:30:09 +0800 Subject: [PATCH 6/7] fix ci --- build.zig | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/build.zig b/build.zig index 6e09a36..1e71416 100644 --- a/build.zig +++ b/build.zig @@ -12,23 +12,24 @@ pub fn build(b: *std.Build) void { .root_source_file = .{ .path = "src/main.zig" }, }); + // FIXME: libcurl doesn't work with zig master yet. // const libcurl = b.dependency("libcurl", .{ .target = target, .optimize = optimize }); // b.installArtifact(libcurl.artifact("curl")); try addExample(b, "basic", module, target, optimize); try addExample(b, "advanced", module, target, optimize); - // const main_tests = b.addTest(.{ - // .root_source_file = .{ .path = "src/main.zig" }, - // .target = target, - // .optimize = optimize, - // }); - // main_tests.root_module.addImport(MODULE_NAME, module); - // main_tests.linkLibrary(libcurl.artifact("curl")); - - // const run_main_tests = b.addRunArtifact(main_tests); - // const test_step = b.step("test", "Run library tests"); - // test_step.dependOn(&run_main_tests.step); + const main_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + main_tests.linkLibC(); + main_tests.linkSystemLibrary("curl"); + + const run_main_tests = b.addRunArtifact(main_tests); + const test_step = b.step("test", "Run library tests"); + test_step.dependOn(&run_main_tests.step); } fn addExample( @@ -50,6 +51,7 @@ fn addExample( exe.root_module.addImport(MODULE_NAME, curl_module); // exe.linkLibrary(libcurl.artifact("curl")); exe.linkSystemLibrary("curl"); + exe.linkLibC(); const run_step = b.step("run-" ++ name, std.fmt.comptimePrint("Run {s} example", .{name})); run_step.dependOn(&b.addRunArtifact(exe).step); From 227caa96affb5bde6cb734e29964c479076de671 Mon Sep 17 00:00:00 2001 From: jiacai2050 Date: Tue, 16 Jan 2024 22:25:55 +0800 Subject: [PATCH 7/7] tidy --- build.zig | 4 ++-- examples/advanced.zig | 4 ++-- examples/basic.zig | 2 +- src/easy.zig | 2 +- src/{main.zig => root.zig} | 1 - src/util.zig | 29 ----------------------------- 6 files changed, 6 insertions(+), 36 deletions(-) rename src/{main.zig => root.zig} (95%) diff --git a/build.zig b/build.zig index 1e71416..11d4001 100644 --- a/build.zig +++ b/build.zig @@ -9,7 +9,7 @@ pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); const module = b.addModule(MODULE_NAME, .{ - .root_source_file = .{ .path = "src/main.zig" }, + .root_source_file = .{ .path = "src/root.zig" }, }); // FIXME: libcurl doesn't work with zig master yet. @@ -20,7 +20,7 @@ pub fn build(b: *std.Build) void { try addExample(b, "advanced", module, target, optimize); const main_tests = b.addTest(.{ - .root_source_file = .{ .path = "src/main.zig" }, + .root_source_file = .{ .path = "src/root.zig" }, .target = target, .optimize = optimize, }); diff --git a/examples/advanced.zig b/examples/advanced.zig index 4f53b31..c958e74 100644 --- a/examples/advanced.zig +++ b/examples/advanced.zig @@ -28,7 +28,7 @@ fn put_with_custom_header(allocator: Allocator, easy: Easy) !void { const headers = blk: { var h = try easy.create_headers(); errdefer h.deinit(); - try h.add(curl.HEADER_CONTENT_TYPE, "application/json"); + try h.add("content-type", "application/json"); try h.add("user-agent", UA); try h.add("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"); break :blk h; @@ -109,7 +109,7 @@ pub fn main() !void { defer if (gpa.deinit() != .ok) @panic("leak"); const allocator = gpa.allocator(); - var easy = try Easy.init(allocator, .{}); + const easy = try Easy.init(allocator, .{}); defer easy.deinit(); curl.print_libcurl_version(); diff --git a/examples/basic.zig b/examples/basic.zig index 0cf5756..63fb239 100644 --- a/examples/basic.zig +++ b/examples/basic.zig @@ -68,7 +68,7 @@ pub fn main() !void { defer if (gpa.deinit() != .ok) @panic("leak"); const allocator = gpa.allocator(); - var easy = try Easy.init(allocator, .{}); + const easy = try Easy.init(allocator, .{}); defer easy.deinit(); println("GET demo"); diff --git a/src/easy.zig b/src/easy.zig index 3cc6dc4..eea5a76 100644 --- a/src/easy.zig +++ b/src/easy.zig @@ -284,7 +284,7 @@ pub fn post(self: Self, url: [:0]const u8, content_type: []const u8, body: []con var headers = try self.create_headers(); defer headers.deinit(); - try headers.add(util.HEADER_CONTENT_TYPE, content_type); + try headers.add("Content-Type", content_type); try self.set_headers(headers); return self.perform(); diff --git a/src/main.zig b/src/root.zig similarity index 95% rename from src/main.zig rename to src/root.zig index 5668b47..3fb6ee3 100644 --- a/src/main.zig +++ b/src/root.zig @@ -5,7 +5,6 @@ const checkCode = @import("errors.zig").checkCode; pub const Easy = @import("easy.zig"); pub usingnamespace Easy; pub usingnamespace @import("c.zig"); -pub usingnamespace @import("util.zig"); /// This function sets up the program environment that libcurl needs. /// Since this function is not thread safe before libcurl 7.84.0, this function diff --git a/src/util.zig b/src/util.zig index e39deb5..0939427 100644 --- a/src/util.zig +++ b/src/util.zig @@ -3,38 +3,9 @@ const c = @import("c.zig").c; const Allocator = std.mem.Allocator; const Encoder = std.base64.standard.Encoder; -pub const HEADER_CONTENT_TYPE: []const u8 = "Content-Type"; -pub const HEADER_USER_AGENT: []const u8 = "User-Agent"; - pub fn encode_base64(allocator: Allocator, input: []const u8) ![]const u8 { const encoded_len = Encoder.calcSize(input.len); const dest = try allocator.alloc(u8, encoded_len); return Encoder.encode(dest, input); } - -pub fn map_to_headers(allocator: std.mem.Allocator, map: std.StringHashMap([]const u8), user_agent: []const u8) !*c.struct_curl_slist { - var headers: ?*c.struct_curl_slist = null; - var has_ua = false; - var iterator = map.iterator(); - while (iterator.next()) |item| { - const key = item.key_ptr.*; - const value = item.value_ptr.*; - const header = try std.fmt.allocPrintZ(allocator, "{s}: {s}", .{ key, value }); - defer allocator.free(header); - - headers = c.curl_slist_append(headers, header); - - if (!has_ua and std.ascii.eqlIgnoreCase(key, HEADER_USER_AGENT)) { - has_ua = true; - } - } - - if (!has_ua) { - const kv = try std.fmt.allocPrintZ(allocator, "{s}: {s}", .{ HEADER_USER_AGENT, user_agent }); - defer allocator.free(kv); - - headers = c.curl_slist_append(headers, kv); - } - return headers.?; -}