wreq 1.2.5 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.cargo/config.toml +4 -0
- data/Cargo.lock +135 -119
- data/Cargo.toml +11 -3
- data/Rakefile +6 -0
- data/crates/wreq-util/README.md +1 -1
- data/crates/wreq-util/examples/emulate.rs +3 -3
- data/crates/wreq-util/src/emulate/macros.rs +13 -11
- data/crates/wreq-util/src/emulate/profile/chrome/header.rs +18 -3
- data/crates/wreq-util/src/emulate/profile/firefox/header.rs +16 -0
- data/crates/wreq-util/src/emulate/profile/opera/header.rs +6 -7
- data/crates/wreq-util/tests/client.rs +103 -1
- data/docs/windows-gnu-tokio-crash.md +49 -3
- data/extconf.rb +4 -0
- data/lib/wreq.rb +70 -45
- data/lib/wreq_ruby/body.rb +29 -11
- data/lib/wreq_ruby/client.rb +88 -55
- data/lib/wreq_ruby/cookie.rb +79 -19
- data/lib/wreq_ruby/emulate.rb +74 -6
- data/lib/wreq_ruby/error.rb +5 -7
- data/lib/wreq_ruby/http.rb +74 -0
- data/lib/wreq_ruby/response.rb +3 -0
- data/script/rust_env.rb +85 -0
- data/src/arch.rs +22 -0
- data/src/client/body/form.rs +2 -0
- data/src/client/body/json.rs +47 -14
- data/src/client/body/stream.rs +147 -43
- data/src/client/body.rs +11 -15
- data/src/client/param.rs +7 -7
- data/src/client/req.rs +87 -47
- data/src/client/resp.rs +23 -24
- data/src/client.rs +310 -230
- data/src/cookie.rs +280 -87
- data/src/emulate.rs +85 -50
- data/src/error.rs +198 -41
- data/src/extractor.rs +16 -76
- data/src/header.rs +146 -23
- data/src/http.rs +62 -33
- data/src/lib.rs +26 -20
- data/src/macros.rs +71 -46
- data/src/options.rs +284 -0
- data/src/rt.rs +22 -11
- data/src/serde/de/array_deserializer.rs +48 -0
- data/src/serde/de/array_enumerator.rs +59 -0
- data/src/serde/de/deserializer.rs +307 -0
- data/src/serde/de/enum_deserializer.rs +47 -0
- data/src/serde/de/hash_deserializer.rs +89 -0
- data/src/serde/de/number_deserializer.rs +51 -0
- data/src/serde/de/variant_deserializer.rs +101 -0
- data/src/serde/de.rs +33 -0
- data/src/serde/error.rs +146 -0
- data/src/serde/ser/enums.rs +14 -0
- data/src/serde/ser/map_serializer.rs +56 -0
- data/src/serde/ser/seq_serializer.rs +71 -0
- data/src/serde/ser/serializer.rs +194 -0
- data/src/serde/ser/struct_serializer.rs +106 -0
- data/src/serde/ser/struct_variant_serializer.rs +44 -0
- data/src/serde/ser/tuple_variant_serializer.rs +41 -0
- data/src/serde/ser.rs +18 -0
- data/src/serde/tests.rs +237 -0
- data/src/serde.rs +202 -0
- data/test/body_sender_test.rb +246 -0
- data/test/cookie_test.rb +211 -10
- data/test/json_precision_test.rb +209 -0
- data/test/option_validation_test.rb +311 -0
- data/test/value_semantics_test.rb +238 -0
- data/wreq.gemspec +1 -1
- metadata +28 -4
- data/script/build_windows_gnu.ps1 +0 -264
- data/src/header/helper.rs +0 -112
data/src/cookie.rs
CHANGED
|
@@ -1,24 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
//! Ruby bindings for HTTP cookies and the shared cookie jar.
|
|
2
|
+
//!
|
|
3
|
+
//! Expiration follows [RFC 6265]. The binding accepts Ruby `Time` values and
|
|
4
|
+
//! finite Unix timestamps without converting them through unsigned
|
|
5
|
+
//! `SystemTime`. A non-positive Max-Age expires the cookie immediately.
|
|
6
|
+
//!
|
|
7
|
+
//! [RFC 6265]: https://www.rfc-editor.org/rfc/rfc6265.html
|
|
2
8
|
|
|
9
|
+
use std::{fmt, sync::Arc};
|
|
10
|
+
|
|
11
|
+
use ::serde::Deserialize;
|
|
3
12
|
use bytes::Bytes;
|
|
4
|
-
use cookie::{Cookie as RawCookie,
|
|
13
|
+
use cookie::{Cookie as RawCookie, ParseError, time::Duration};
|
|
5
14
|
use magnus::{
|
|
6
|
-
Error, Module, Object, RHash, RModule, RString, Ruby, TryConvert, Value, function,
|
|
7
|
-
r_hash::ForEach, typed_data::Obj, value::ReprValue,
|
|
15
|
+
Error, Module, Object, RHash, RModule, RString, Ruby, Time, TryConvert, Value, function,
|
|
16
|
+
method, r_hash::ForEach, typed_data::Obj, value::ReprValue,
|
|
8
17
|
};
|
|
9
18
|
use wreq::header::{self, HeaderMap, HeaderValue};
|
|
10
19
|
|
|
11
|
-
use crate::{
|
|
20
|
+
use crate::{
|
|
21
|
+
error::{header_value_error, type_error},
|
|
22
|
+
gvl,
|
|
23
|
+
options::{NativeOption, Options},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
use self::helper::{CookieExpiration, to_ruby_time, to_unix_timestamp};
|
|
12
27
|
|
|
28
|
+
// Defines constant registration, `into_ffi`/`from_ffi`, and handlers for
|
|
29
|
+
// Ruby's `to_s`, `to_sym`, `==`, `eql?`, and `hash` methods.
|
|
13
30
|
define_ruby_enum!(
|
|
14
31
|
/// The Cookie SameSite attribute.
|
|
15
|
-
const,
|
|
16
32
|
SameSite,
|
|
17
33
|
"Wreq::SameSite",
|
|
18
34
|
cookie::SameSite,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
symbols:
|
|
36
|
+
Strict => "strict",
|
|
37
|
+
Lax => "lax",
|
|
38
|
+
None => "none",
|
|
22
39
|
);
|
|
23
40
|
|
|
24
41
|
/// A single HTTP cookie.
|
|
@@ -30,80 +47,103 @@ pub struct Cookie(RawCookie<'static>);
|
|
|
30
47
|
#[derive(Default)]
|
|
31
48
|
pub struct Cookies(pub Vec<HeaderValue>);
|
|
32
49
|
|
|
33
|
-
///
|
|
50
|
+
/// Keyword options accepted by `Wreq::Cookie.new`.
|
|
51
|
+
#[derive(Deserialize)]
|
|
52
|
+
struct Builder {
|
|
53
|
+
/// The Domain attribute.
|
|
54
|
+
domain: Option<String>,
|
|
55
|
+
|
|
56
|
+
/// The Path attribute.
|
|
57
|
+
path: Option<String>,
|
|
58
|
+
|
|
59
|
+
/// The signed Max-Age lifetime in seconds.
|
|
60
|
+
#[serde(default)]
|
|
61
|
+
max_age: NativeOption<i64>,
|
|
62
|
+
|
|
63
|
+
/// The expiration converted from a Ruby `Time` or Numeric value.
|
|
64
|
+
#[serde(default)]
|
|
65
|
+
expires: NativeOption<CookieExpiration>,
|
|
66
|
+
|
|
67
|
+
/// Whether the cookie is inaccessible to client-side scripts.
|
|
68
|
+
http_only: Option<bool>,
|
|
69
|
+
|
|
70
|
+
/// Whether the cookie is restricted to secure connections.
|
|
71
|
+
secure: Option<bool>,
|
|
72
|
+
|
|
73
|
+
/// The SameSite policy.
|
|
74
|
+
#[serde(default)]
|
|
75
|
+
same_site: NativeOption<Obj<SameSite>>,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// A cookie jar that can be shared with a Ruby `Wreq::Client`.
|
|
34
79
|
///
|
|
35
|
-
///
|
|
36
|
-
/// This type is exposed to allow creating one and filling it with some
|
|
37
|
-
/// existing cookies more easily, before creating a `Client`.
|
|
80
|
+
/// Pass a populated jar as the client's `cookie_provider` option.
|
|
38
81
|
#[derive(Clone, Default)]
|
|
39
82
|
#[magnus::wrap(class = "Wreq::Jar", free_immediately, size)]
|
|
40
83
|
pub struct Jar(pub Arc<wreq::cookie::Jar>);
|
|
41
84
|
|
|
42
|
-
// ===== impl
|
|
43
|
-
|
|
44
|
-
impl
|
|
45
|
-
///
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
Option<bool>,
|
|
59
|
-
Option<Obj<SameSite>>,
|
|
60
|
-
),
|
|
61
|
-
(),
|
|
62
|
-
> = magnus::scan_args::get_kwargs(
|
|
63
|
-
args.keywords,
|
|
64
|
-
&[],
|
|
65
|
-
&[
|
|
66
|
-
"domain",
|
|
67
|
-
"path",
|
|
68
|
-
"max_age",
|
|
69
|
-
"expires",
|
|
70
|
-
"http_only",
|
|
71
|
-
"secure",
|
|
72
|
-
"same_site",
|
|
73
|
-
],
|
|
74
|
-
)?;
|
|
75
|
-
|
|
76
|
-
let (name, value) = args.required;
|
|
85
|
+
// ===== impl Builder =====
|
|
86
|
+
|
|
87
|
+
impl Builder {
|
|
88
|
+
/// Validates and deserializes a Ruby keyword options hash.
|
|
89
|
+
///
|
|
90
|
+
/// # Errors
|
|
91
|
+
///
|
|
92
|
+
/// Returns `ArgumentError` for unknown or duplicate keys. Invalid values
|
|
93
|
+
/// return the Ruby conversion error with the option name attached.
|
|
94
|
+
fn from_options(options: Options<'_>) -> Result<Self, Error> {
|
|
95
|
+
let mut builder = options.validate_keys::<Self>()?.deserialize::<Self>()?;
|
|
96
|
+
extract_native_option!(options, builder, max_age);
|
|
97
|
+
extract_native_option!(options, builder, expires);
|
|
98
|
+
extract_native_option!(options, builder, same_site);
|
|
99
|
+
Ok(builder)
|
|
100
|
+
}
|
|
77
101
|
|
|
102
|
+
/// Builds a native cookie from its name, value, and optional attributes.
|
|
103
|
+
fn build(mut self, name: String, value: String) -> Cookie {
|
|
78
104
|
let mut cookie = RawCookie::new(name, value);
|
|
79
105
|
|
|
80
|
-
if let Some(domain) =
|
|
106
|
+
if let Some(domain) = self.domain {
|
|
81
107
|
cookie.set_domain(domain);
|
|
82
108
|
}
|
|
83
109
|
|
|
84
|
-
if let Some(path) =
|
|
110
|
+
if let Some(path) = self.path {
|
|
85
111
|
cookie.set_path(path);
|
|
86
112
|
}
|
|
87
113
|
|
|
88
|
-
if let Some(max_age) =
|
|
89
|
-
cookie.set_max_age(Duration::seconds(max_age
|
|
114
|
+
if let Some(max_age) = self.max_age.take() {
|
|
115
|
+
cookie.set_max_age(Duration::seconds(max_age));
|
|
90
116
|
}
|
|
91
117
|
|
|
92
|
-
if let Some(expires) =
|
|
93
|
-
|
|
94
|
-
if let Some(system_time) = SystemTime::UNIX_EPOCH.checked_add(duration) {
|
|
95
|
-
cookie.set_expires(Expiration::DateTime(system_time.into()));
|
|
96
|
-
}
|
|
118
|
+
if let Some(expires) = self.expires.take() {
|
|
119
|
+
cookie.set_expires(expires.into_inner());
|
|
97
120
|
}
|
|
98
121
|
|
|
99
|
-
cookie.set_http_only(
|
|
100
|
-
cookie.set_secure(
|
|
122
|
+
cookie.set_http_only(self.http_only);
|
|
123
|
+
cookie.set_secure(self.secure);
|
|
101
124
|
|
|
102
|
-
if let Some(same_site) =
|
|
125
|
+
if let Some(same_site) = self.same_site.take() {
|
|
103
126
|
cookie.set_same_site(same_site.into_ffi());
|
|
104
127
|
}
|
|
105
128
|
|
|
106
|
-
|
|
129
|
+
Cookie(cookie)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ===== Ruby Cookie API =====
|
|
134
|
+
|
|
135
|
+
impl Cookie {
|
|
136
|
+
/// Creates a [`Cookie`] from its name, value, and keyword options.
|
|
137
|
+
///
|
|
138
|
+
/// # Errors
|
|
139
|
+
///
|
|
140
|
+
/// Returns `ArgumentError` for unknown or duplicate keys. Invalid values
|
|
141
|
+
/// return the Ruby conversion error with the option name attached.
|
|
142
|
+
pub fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
143
|
+
let args = magnus::scan_args::scan_args::<(String, String), (), (), (), RHash, ()>(args)?;
|
|
144
|
+
let (name, value) = args.required;
|
|
145
|
+
Builder::from_options(Options::new(ruby, args.keywords))
|
|
146
|
+
.map(|builder| builder.build(name, value))
|
|
107
147
|
}
|
|
108
148
|
|
|
109
149
|
/// The name of the cookie.
|
|
@@ -130,18 +170,24 @@ impl Cookie {
|
|
|
130
170
|
self.0.secure().unwrap_or(false)
|
|
131
171
|
}
|
|
132
172
|
|
|
133
|
-
/// Returns true
|
|
173
|
+
/// Returns true when the SameSite setting is Lax.
|
|
134
174
|
#[inline]
|
|
135
175
|
pub fn same_site_lax(&self) -> bool {
|
|
136
176
|
self.0.same_site() == Some(cookie::SameSite::Lax)
|
|
137
177
|
}
|
|
138
178
|
|
|
139
|
-
/// Returns true
|
|
179
|
+
/// Returns true when the SameSite setting is Strict.
|
|
140
180
|
#[inline]
|
|
141
181
|
pub fn same_site_strict(&self) -> bool {
|
|
142
182
|
self.0.same_site() == Some(cookie::SameSite::Strict)
|
|
143
183
|
}
|
|
144
184
|
|
|
185
|
+
/// Returns the SameSite setting, if present.
|
|
186
|
+
#[inline]
|
|
187
|
+
pub fn same_site(&self) -> Option<SameSite> {
|
|
188
|
+
self.0.same_site().map(SameSite::from_ffi)
|
|
189
|
+
}
|
|
190
|
+
|
|
145
191
|
/// Returns the path directive of the cookie, if set.
|
|
146
192
|
#[inline]
|
|
147
193
|
pub fn path(&self) -> Option<&str> {
|
|
@@ -154,41 +200,66 @@ impl Cookie {
|
|
|
154
200
|
self.0.domain()
|
|
155
201
|
}
|
|
156
202
|
|
|
157
|
-
///
|
|
203
|
+
/// Returns the signed Max-Age lifetime in seconds.
|
|
158
204
|
#[inline]
|
|
159
205
|
pub fn max_age(&self) -> Option<i64> {
|
|
160
206
|
self.0.max_age().map(|d| d.whole_seconds())
|
|
161
207
|
}
|
|
162
208
|
|
|
163
|
-
///
|
|
209
|
+
/// Returns the cookie expiration as a UTC Ruby `Time`.
|
|
210
|
+
pub fn expires_at(ruby: &Ruby, rb_self: &Self) -> Result<Option<Time>, Error> {
|
|
211
|
+
rb_self
|
|
212
|
+
.0
|
|
213
|
+
.expires_datetime()
|
|
214
|
+
.map(|value| to_ruby_time(ruby, value))
|
|
215
|
+
.transpose()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// Returns the cookie expiration as fractional Unix seconds.
|
|
164
219
|
#[inline]
|
|
165
220
|
pub fn expires(&self) -> Option<f64> {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
None | Some(Expiration::Session) => None,
|
|
175
|
-
}
|
|
221
|
+
self.0.expires_datetime().map(to_unix_timestamp)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// Formats the cookie as a Set-Cookie value.
|
|
225
|
+
#[inline]
|
|
226
|
+
pub fn to_s(&self) -> String {
|
|
227
|
+
self.to_string()
|
|
176
228
|
}
|
|
177
229
|
}
|
|
178
230
|
|
|
231
|
+
// ===== Native Cookie helpers =====
|
|
232
|
+
|
|
179
233
|
impl Cookie {
|
|
234
|
+
/// Clone this cookie for insertion into the native jar.
|
|
235
|
+
///
|
|
236
|
+
/// [RFC 6265 section 5.2.2] says that zero or a negative Max-Age expires a
|
|
237
|
+
/// cookie immediately. The native jar recognizes zero for deletion, so
|
|
238
|
+
/// this clone maps negative values to zero before insertion. The Ruby
|
|
239
|
+
/// object keeps its original signed value.
|
|
240
|
+
///
|
|
241
|
+
/// [RFC 6265 section 5.2.2]: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.2.2
|
|
242
|
+
fn clone_for_jar(&self) -> RawCookie<'static> {
|
|
243
|
+
let mut cookie = self.0.clone();
|
|
244
|
+
if cookie.max_age().is_some_and(Duration::is_negative) {
|
|
245
|
+
cookie.set_max_age(Duration::ZERO);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
cookie
|
|
249
|
+
}
|
|
250
|
+
|
|
180
251
|
/// Parse cookies from a `HeaderMap`.
|
|
181
|
-
pub fn extract_headers_cookies(headers: &HeaderMap) -> Vec<Cookie> {
|
|
252
|
+
pub(crate) fn extract_headers_cookies(headers: &HeaderMap) -> Vec<Cookie> {
|
|
182
253
|
headers
|
|
183
254
|
.get_all(header::SET_COOKIE)
|
|
184
255
|
.iter()
|
|
185
|
-
.
|
|
186
|
-
.flat_map(Result::ok)
|
|
256
|
+
.filter_map(|value| Self::parse(value).ok())
|
|
187
257
|
.map(RawCookie::into_owned)
|
|
188
258
|
.map(Cookie)
|
|
189
259
|
.collect()
|
|
190
260
|
}
|
|
191
261
|
|
|
262
|
+
/// Parse one Set-Cookie header value.
|
|
192
263
|
fn parse<'a>(value: &'a HeaderValue) -> Result<RawCookie<'a>, ParseError> {
|
|
193
264
|
std::str::from_utf8(value.as_bytes())
|
|
194
265
|
.map_err(cookie::ParseError::from)
|
|
@@ -206,13 +277,14 @@ impl fmt::Display for Cookie {
|
|
|
206
277
|
|
|
207
278
|
impl TryConvert for Cookies {
|
|
208
279
|
fn try_convert(value: magnus::Value) -> Result<Self, magnus::Error> {
|
|
280
|
+
let ruby = Ruby::get_with(value);
|
|
209
281
|
// try extract uncompressed cookies
|
|
210
282
|
if let Some(rhash) = RHash::from_value(value) {
|
|
211
283
|
let mut cookies = Vec::new();
|
|
212
284
|
rhash.foreach(|name: RString, value: RString| {
|
|
213
285
|
let cookie = format!("{name}={value}");
|
|
214
286
|
let header_value = HeaderValue::from_maybe_shared(Bytes::from(cookie))
|
|
215
|
-
.map_err(
|
|
287
|
+
.map_err(|err| header_value_error(&ruby, err))?;
|
|
216
288
|
cookies.push(header_value);
|
|
217
289
|
Ok(ForEach::Continue)
|
|
218
290
|
})?;
|
|
@@ -224,11 +296,11 @@ impl TryConvert for Cookies {
|
|
|
224
296
|
if let Some(cookies) = RString::from_value(value) {
|
|
225
297
|
return Ok(Self(vec![
|
|
226
298
|
HeaderValue::from_maybe_shared(cookies.to_bytes())
|
|
227
|
-
.map_err(
|
|
299
|
+
.map_err(|err| header_value_error(&ruby, err))?,
|
|
228
300
|
]));
|
|
229
301
|
}
|
|
230
302
|
|
|
231
|
-
|
|
303
|
+
Err(type_error(&ruby, "cookies must be a Hash or String"))
|
|
232
304
|
}
|
|
233
305
|
}
|
|
234
306
|
|
|
@@ -237,7 +309,7 @@ impl TryConvert for Cookies {
|
|
|
237
309
|
impl Jar {
|
|
238
310
|
/// Create a new [`Jar`] with an empty cookie store.
|
|
239
311
|
pub fn new() -> Self {
|
|
240
|
-
|
|
312
|
+
Self(Arc::new(wreq::cookie::Jar::default()))
|
|
241
313
|
}
|
|
242
314
|
|
|
243
315
|
/// Get all cookies.
|
|
@@ -256,14 +328,21 @@ impl Jar {
|
|
|
256
328
|
}
|
|
257
329
|
|
|
258
330
|
/// Add a cookie to this jar.
|
|
259
|
-
|
|
331
|
+
///
|
|
332
|
+
/// # Errors
|
|
333
|
+
///
|
|
334
|
+
/// Returns `TypeError` when `cookie` is neither a [`Cookie`] nor a String.
|
|
335
|
+
pub fn add(&self, cookie: Value, url: String) -> Result<(), Error> {
|
|
260
336
|
if let Ok(cookie) = Obj::<Cookie>::try_convert(cookie) {
|
|
261
|
-
gvl::nogvl(|| self.0.add(cookie.
|
|
337
|
+
gvl::nogvl(|| self.0.add(cookie.clone_for_jar(), &url));
|
|
338
|
+
return Ok(());
|
|
262
339
|
}
|
|
263
340
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
341
|
+
let ruby = Ruby::get_with(cookie);
|
|
342
|
+
let cookie = String::try_convert(cookie)
|
|
343
|
+
.map_err(|_| type_error(&ruby, "cookie must be a Wreq::Cookie or String"))?;
|
|
344
|
+
gvl::nogvl(|| self.0.add(cookie.as_ref(), &url));
|
|
345
|
+
Ok(())
|
|
267
346
|
}
|
|
268
347
|
|
|
269
348
|
/// Remove a cookie from this jar by name and URL.
|
|
@@ -277,10 +356,121 @@ impl Jar {
|
|
|
277
356
|
}
|
|
278
357
|
}
|
|
279
358
|
|
|
359
|
+
mod helper {
|
|
360
|
+
//! Converts between Ruby time values and native cookie timestamps.
|
|
361
|
+
|
|
362
|
+
use cookie::time::{Duration, OffsetDateTime, error::ComponentRange};
|
|
363
|
+
use magnus::{
|
|
364
|
+
Error, Integer, Ruby, Time, TryConvert, Value,
|
|
365
|
+
time::{Offset, Timespec},
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
use crate::error::{argument_error, range_error};
|
|
369
|
+
|
|
370
|
+
/// A validated cookie expiration converted from Ruby.
|
|
371
|
+
///
|
|
372
|
+
/// Ruby `Time` values keep nanosecond precision. Integer timestamps are
|
|
373
|
+
/// converted directly; other Numeric values use finite fractional Unix
|
|
374
|
+
/// seconds.
|
|
375
|
+
pub(super) struct CookieExpiration(OffsetDateTime);
|
|
376
|
+
|
|
377
|
+
impl CookieExpiration {
|
|
378
|
+
/// Unwraps the validated UTC expiration.
|
|
379
|
+
pub(super) fn into_inner(self) -> OffsetDateTime {
|
|
380
|
+
self.0
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
impl TryConvert for CookieExpiration {
|
|
385
|
+
fn try_convert(value: Value) -> Result<Self, Error> {
|
|
386
|
+
let ruby = Ruby::get_with(value);
|
|
387
|
+
expiration_from_value(&ruby, value).map(Self)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/// Converts a native cookie expiration into a UTC Ruby `Time`.
|
|
392
|
+
pub(super) fn to_ruby_time(ruby: &Ruby, value: OffsetDateTime) -> Result<Time, Error> {
|
|
393
|
+
ruby.time_timespec_new(
|
|
394
|
+
Timespec {
|
|
395
|
+
tv_sec: value.unix_timestamp(),
|
|
396
|
+
tv_nsec: i64::from(value.nanosecond()),
|
|
397
|
+
},
|
|
398
|
+
Offset::utc(),
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/// Converts a native cookie expiration into fractional Unix seconds.
|
|
403
|
+
pub(super) fn to_unix_timestamp(value: OffsetDateTime) -> f64 {
|
|
404
|
+
value.unix_timestamp() as f64 + f64::from(value.nanosecond()) / 1_000_000_000.0
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// Converts a Ruby `Time` or Numeric value into a native UTC timestamp.
|
|
408
|
+
fn expiration_from_value(ruby: &Ruby, value: Value) -> Result<OffsetDateTime, Error> {
|
|
409
|
+
if let Some(time) = Time::from_value(value) {
|
|
410
|
+
return expiration_from_time(ruby, time);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if let Some(integer) = Integer::from_value(value) {
|
|
414
|
+
return integer
|
|
415
|
+
.to_i64()
|
|
416
|
+
.and_then(|seconds| expiration_from_seconds(ruby, seconds));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
expiration_from_float(ruby, f64::try_convert(value)?)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// Converts a Ruby `Time` from its signed timespec without using `SystemTime`.
|
|
423
|
+
fn expiration_from_time(ruby: &Ruby, value: Time) -> Result<OffsetDateTime, Error> {
|
|
424
|
+
let timespec = value.timespec()?;
|
|
425
|
+
let nanosecond = u32::try_from(timespec.tv_nsec)
|
|
426
|
+
.ok()
|
|
427
|
+
.filter(|value| *value < 1_000_000_000)
|
|
428
|
+
.ok_or_else(|| {
|
|
429
|
+
argument_error(ruby, "time nanoseconds are outside the supported range")
|
|
430
|
+
})?;
|
|
431
|
+
|
|
432
|
+
expiration_from_seconds(ruby, timespec.tv_sec)?
|
|
433
|
+
.replace_nanosecond(nanosecond)
|
|
434
|
+
.map_err(|error| expiration_range_error(ruby, error))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/// Converts signed Unix seconds into the native cookie time range.
|
|
438
|
+
fn expiration_from_seconds(ruby: &Ruby, seconds: i64) -> Result<OffsetDateTime, Error> {
|
|
439
|
+
OffsetDateTime::from_unix_timestamp(seconds)
|
|
440
|
+
.map_err(|error| expiration_range_error(ruby, error))
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/// Converts a finite fractional Unix timestamp with checked arithmetic.
|
|
444
|
+
fn expiration_from_float(ruby: &Ruby, seconds: f64) -> Result<OffsetDateTime, Error> {
|
|
445
|
+
if !seconds.is_finite() {
|
|
446
|
+
return Err(argument_error(ruby, "timestamp must be finite"));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
let duration = Duration::checked_seconds_f64(seconds)
|
|
450
|
+
.ok_or_else(|| range_error(ruby, "timestamp is outside the supported range"))?;
|
|
451
|
+
OffsetDateTime::UNIX_EPOCH
|
|
452
|
+
.checked_add(duration)
|
|
453
|
+
.ok_or_else(|| range_error(ruby, "timestamp is outside the supported range"))
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/// Maps a native timestamp range error to Ruby's `RangeError`.
|
|
457
|
+
fn expiration_range_error(ruby: &Ruby, error: ComponentRange) -> Error {
|
|
458
|
+
range_error(
|
|
459
|
+
ruby,
|
|
460
|
+
format!("timestamp is outside the supported range: {error}"),
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
280
465
|
pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
|
|
281
466
|
// SameSite enum
|
|
282
467
|
let same_site_class = gem_module.define_class("SameSite", ruby.class_object())?;
|
|
283
468
|
SameSite::define_constants(same_site_class)?;
|
|
469
|
+
same_site_class.define_method("to_s", method!(SameSite::to_s, 0))?;
|
|
470
|
+
same_site_class.define_method("to_sym", method!(SameSite::to_sym, 0))?;
|
|
471
|
+
same_site_class.define_method("==", method!(SameSite::equals, 1))?;
|
|
472
|
+
same_site_class.define_method("eql?", method!(SameSite::is_eql, 1))?;
|
|
473
|
+
same_site_class.define_method("hash", method!(SameSite::hash_value, 0))?;
|
|
284
474
|
|
|
285
475
|
// Cookie class
|
|
286
476
|
let cookie_class = gem_module.define_class("Cookie", ruby.class_object())?;
|
|
@@ -293,10 +483,13 @@ pub fn include(ruby: &Ruby, gem_module: &RModule) -> Result<(), Error> {
|
|
|
293
483
|
cookie_class.define_method("secure?", method!(Cookie::secure, 0))?;
|
|
294
484
|
cookie_class.define_method("same_site_lax?", method!(Cookie::same_site_lax, 0))?;
|
|
295
485
|
cookie_class.define_method("same_site_strict?", method!(Cookie::same_site_strict, 0))?;
|
|
486
|
+
cookie_class.define_method("same_site", method!(Cookie::same_site, 0))?;
|
|
296
487
|
cookie_class.define_method("path", method!(Cookie::path, 0))?;
|
|
297
488
|
cookie_class.define_method("domain", method!(Cookie::domain, 0))?;
|
|
298
489
|
cookie_class.define_method("max_age", method!(Cookie::max_age, 0))?;
|
|
490
|
+
cookie_class.define_method("expires_at", method!(Cookie::expires_at, 0))?;
|
|
299
491
|
cookie_class.define_method("expires", method!(Cookie::expires, 0))?;
|
|
492
|
+
cookie_class.define_method("to_s", method!(Cookie::to_s, 0))?;
|
|
300
493
|
|
|
301
494
|
// Jar class
|
|
302
495
|
let jar_class = gem_module.define_class("Jar", ruby.class_object())?;
|