@@ -109,6 +109,17 @@ pub const Jar = struct {
109109 }
110110 }
111111
112+ // https://curl.se/docs/http-cookies.html
113+ pub fn populateFromCurl (self : * Jar , set_cookie : []const u8 ) ! void {
114+ const c = Cookie .parseCurl (self .allocator , set_cookie ) catch | err | {
115+ log .warn (.web_api , "cookie parse failed" , .{ .raw = set_cookie , .err = err });
116+ return ;
117+ };
118+
119+ const now = std .time .timestamp ();
120+ try self .add (c , now );
121+ }
122+
112123 pub fn populateFromResponse (self : * Jar , uri : * const Uri , set_cookie : []const u8 ) ! void {
113124 const c = Cookie .parse (self .allocator , uri , set_cookie ) catch | err | {
114125 log .warn (.web_api , "cookie parse failed" , .{ .raw = set_cookie , .err = err });
@@ -192,6 +203,62 @@ pub const Cookie = struct {
192203 self .arena .deinit ();
193204 }
194205
206+ // Parse curl's cookie file format
207+ // https://curl.se/docs/http-cookies.html
208+ pub fn parseCurl (allocator : Allocator , str : []const u8 ) ! Cookie {
209+ var c : Cookie = .{
210+ .arena = ArenaAllocator .init (allocator ),
211+ .name = undefined ,
212+ .value = undefined ,
213+ .path = undefined ,
214+ .same_site = undefined ,
215+ .secure = undefined ,
216+ .http_only = undefined ,
217+ .domain = undefined ,
218+ .expires = undefined ,
219+ };
220+ errdefer c .deinit ();
221+
222+ var aa = c .arena .allocator ();
223+
224+ var it = std .mem .splitScalar (u8 , str , '\t ' );
225+ var index : u8 = 0 ;
226+ while (it .next ()) | v | {
227+ defer index += 1 ;
228+
229+ switch (index ) {
230+ 0 = > {
231+ // domain name, can start with #HttpOnly_
232+ if (std .mem .indexOf (u8 , v , "#HttpOnly_" )) | pos | {
233+ c .http_only = true ;
234+ c .domain = try aa .dupe (u8 , v [pos + "#HttpOnly_" .len .. ]);
235+ } else {
236+ c .http_only = false ;
237+ c .domain = try aa .dupe (u8 , v );
238+ }
239+ },
240+ 1 = > c .same_site = .lax , // TODO
241+ 2 = > c .path = try aa .dupe (u8 , v ),
242+ 3 = > c .secure = std .mem .eql (u8 , "TRUE" , v ),
243+ 4 = > {
244+ if (std .mem .eql (u8 , "0" , v )) {
245+ c .expires = null ;
246+ } else {
247+ const i = try std .fmt .parseInt (i64 , v , 10 );
248+ c .expires = @floatFromInt (i );
249+ }
250+ },
251+ 5 = > c .name = try aa .dupe (u8 , v ),
252+ 6 = > c .value = try aa .dupe (u8 , v ),
253+ else = > return error .TooMuchCookieFields ,
254+ }
255+ }
256+
257+ if (index != 7 ) return error .MissingCookieFields ;
258+
259+ return c ;
260+ }
261+
195262 // There's https://datatracker.ietf.org/doc/html/rfc6265 but browsers are
196263 // far less strict. I only found 2 cases where browsers will reject a cookie:
197264 // - a byte 0...32 and 127..255 anywhere in the cookie (the HTTP header
@@ -912,6 +979,25 @@ test "Cookie: parse domain" {
912979 try expectError (error .InvalidDomain , "http://lightpanda.io/" , "b;domain=other.example.com" );
913980}
914981
982+ test "Cookie: parse curl" {
983+ try expectCookieCurl (.{
984+ .name = "cookie_key" ,
985+ .value = "cookie_value" ,
986+ .path = "/cookies/" ,
987+ .domain = "httpbin.io" ,
988+ .http_only = true ,
989+ }, "#HttpOnly_httpbin.io\t FALSE\t /cookies/\t FALSE\t 0\t cookie_key\t cookie_value" );
990+
991+ try expectCookieCurl (.{
992+ .name = "cookie_key" ,
993+ .value = "cookie_value" ,
994+ .path = "/cookies/" ,
995+ .domain = "httpbin.io" ,
996+ .expires = 10 ,
997+ .secure = true ,
998+ }, "httpbin.io\t TRUE\t /cookies/\t TRUE\t 10\t cookie_key\t cookie_value" );
999+ }
1000+
9151001const ExpectedCookie = struct {
9161002 name : []const u8 ,
9171003 value : []const u8 ,
@@ -939,6 +1025,21 @@ fn expectCookie(expected: ExpectedCookie, url: []const u8, set_cookie: []const u
9391025 try testing .expectDelta (expected .expires , cookie .expires , 2.0 );
9401026}
9411027
1028+ fn expectCookieCurl (expected : ExpectedCookie , set_cookie : []const u8 ) ! void {
1029+ var cookie = try Cookie .parseCurl (testing .allocator , set_cookie );
1030+ defer cookie .deinit ();
1031+
1032+ try testing .expectEqual (expected .name , cookie .name );
1033+ try testing .expectEqual (expected .value , cookie .value );
1034+ try testing .expectEqual (expected .secure , cookie .secure );
1035+ try testing .expectEqual (expected .http_only , cookie .http_only );
1036+ try testing .expectEqual (expected .same_site , cookie .same_site );
1037+ try testing .expectEqual (expected .path , cookie .path );
1038+ try testing .expectEqual (expected .domain , cookie .domain );
1039+
1040+ try testing .expectDelta (expected .expires , cookie .expires , 2.0 );
1041+ }
1042+
9421043fn expectAttribute (expected : anytype , url : ? []const u8 , set_cookie : []const u8 ) ! void {
9431044 const uri = if (url ) | u | try Uri .parse (u ) else test_uri ;
9441045 var cookie = try Cookie .parse (testing .allocator , & uri , set_cookie );
0 commit comments