зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1710421 - [webdriver] Update time and cookie dependencies. r=webdriver-reviewers,glandium,whimboo
Differential Revision: https://phabricator.services.mozilla.com/D147000
This commit is contained in:
Родитель
5ff09263c0
Коммит
2edc1ca2ed
|
@ -607,7 +607,7 @@ dependencies = [
|
|||
"storage_variant",
|
||||
"tempfile",
|
||||
"thin-vec",
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
"wr_malloc_size_of",
|
||||
"xpcom",
|
||||
]
|
||||
|
@ -675,7 +675,7 @@ dependencies = [
|
|||
"num-integer",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
|
@ -745,11 +745,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.12.0"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5"
|
||||
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
|
||||
dependencies = [
|
||||
"time",
|
||||
"time 0.3.9",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2125,7 +2126,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
"uuid",
|
||||
"whatsys",
|
||||
]
|
||||
|
@ -2145,7 +2146,7 @@ dependencies = [
|
|||
"rkv",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
"uuid",
|
||||
"zeitstempel",
|
||||
]
|
||||
|
@ -2467,7 +2468,7 @@ dependencies = [
|
|||
"log",
|
||||
"pin-project",
|
||||
"socket2",
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
"tokio 0.2.25",
|
||||
"tower-service",
|
||||
"want",
|
||||
|
@ -3425,7 +3426,7 @@ version = "0.1.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aad9dfe950c057b1bfe9c1f2aa51583a8468ef2a5baba2ebbe06d775efeb7729"
|
||||
dependencies = [
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
|
@ -3685,6 +3686,15 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
|
@ -4871,7 +4881,7 @@ dependencies = [
|
|||
"static_prefs",
|
||||
"style_derive",
|
||||
"style_traits",
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
"to_shmem",
|
||||
"to_shmem_derive",
|
||||
"toml",
|
||||
|
@ -5074,6 +5084,24 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
|
||||
dependencies = [
|
||||
"itoa 1.0.2",
|
||||
"libc",
|
||||
"num_threads",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.3.4"
|
||||
|
@ -5675,7 +5703,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"time 0.3.9",
|
||||
"tokio 0.2.25",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
|
@ -5758,7 +5786,7 @@ dependencies = [
|
|||
"smallvec",
|
||||
"svg_fmt",
|
||||
"swgl",
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
"topological-sort",
|
||||
"tracy-rs",
|
||||
"webrender_api",
|
||||
|
@ -5781,7 +5809,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_bytes",
|
||||
"serde_derive",
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
"wr_malloc_size_of",
|
||||
]
|
||||
|
||||
|
@ -6090,5 +6118,5 @@ dependencies = [
|
|||
"flate2",
|
||||
"msdos_time",
|
||||
"podio",
|
||||
"time",
|
||||
"time 0.1.43",
|
||||
]
|
||||
|
|
|
@ -83,6 +83,10 @@ TOLERATED_DUPES = {
|
|||
"memoffset": 2,
|
||||
"mio": 2,
|
||||
"pin-project-lite": 2,
|
||||
# Transition from time 0.1 to 0.3 underway, but chrono is stuck on 0.1
|
||||
# and hasn't been updated in 1.5 years (an hypothetical update is
|
||||
# expected to remove the dependency on time altogether).
|
||||
"time": 2,
|
||||
"tokio": 3,
|
||||
}
|
||||
|
||||
|
|
|
@ -17,13 +17,13 @@ server = ["tokio", "warp"]
|
|||
[dependencies]
|
||||
base64 = "0.12"
|
||||
bytes = "0.5"
|
||||
cookie = { version = "0.12", default-features = false }
|
||||
cookie = { version = "0.16", default-features = false }
|
||||
http = "0.2"
|
||||
log = "0.4"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
time = "0.1"
|
||||
time = "0.3"
|
||||
tokio = { version = "0.2", features = ["rt-core"], optional = true}
|
||||
unicode-segmentation = "1.2"
|
||||
url = "2.0"
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"files":{"Cargo.toml":"2c65ccbf56c2640abee1927d35423ade48e20588630ac31e094d434595ee80d8","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"378f5840b258e2779c39418f3f2d7b2ba96f1c7917dd6be0713f88305dbda397","README.md":"3fcac69759e004d729a868ed5ef248c84d86fb0f3ab5c7a93abb547b91a0eb4e","src/builder.rs":"4200963d44d1a59f1268965b77407ba977eb5a777875cb76ea927ddc829be3d8","src/delta.rs":"510fc3dbf0a70d635d0488c5a5a32a2ba8e1490ce05bee39d944ea8c02189bbc","src/draft.rs":"950b43b3f6e1c4c13b1e90220c71defe02713170807b41e5ffde9a1327688f48","src/jar.rs":"8cc6531203e2a9422bfe1b2a00aeb96beb57d4676fa147a66f28f2d7c3129b57","src/lib.rs":"6a267d63ad90998f4a463c726be6a93fc33979eb8a72bfb53cae9f5b7a13fae0","src/parse.rs":"549844993601f20f5de3f5d5f8bea0fce3fe4f09d72e343aff9e433948a4ec5c","src/secure/key.rs":"734f35ef4b0d6b63174befdcb970f0304ac63f0895871b7c2f267fefdd43b648","src/secure/macros.rs":"83d770e5c4eb7fbd3c3d86973b69042e9e2bb9fafb72a4456598e2ae78638d5f","src/secure/mod.rs":"5d7fecb62295827d474ed1ce6b7628fe93d4a09eb14babfde036d64e8e4a04f8","src/secure/private.rs":"81d782cd4fa4b1415795710ad9e2e77eca3f4326e20ef96675093db9a378da32","src/secure/signed.rs":"26c46c2d561ea14d1d8d79f85342a98b4bd749df776677dde91dd9b928e91fbe"},"package":"888604f00b3db336d2af898ec3c1d5d0ddf5e6d462220f2ededc33a87ac4bbd5"}
|
||||
{"files":{"CHANGELOG.md":"f28b8766c2e83ce8631e17bdeaea680c3a4306cc3a9a464e75c4e144bf7b48d4","Cargo.toml":"ea7621bda57463a85c1302db729ea460b19a6a97a36d50ac058c45ef4d8ce7e7","LICENSE-APACHE":"2773e20df8f4c52a026b5b578c7f2457341f5aa3fb6612fd87e1d2e1bd8f48ad","LICENSE-MIT":"f28420c1906af38be726cd7842798f0194b27c41d6051c0d1e9ee348b8a4a0ea","README.md":"c40f9fb713d6e7d471a1725315cc45fccfb62c6690ac32095351a4722b2f0c84","build.rs":"42c12800b13ac5ad41021f04a1bdb94de9d782d853ff5f54d8734bef1491e9d1","scripts/test.sh":"a76191d56d96c32efcb6883e0983e86beb4c6842e6e5c5a8bfded4c8183ff6f6","src/builder.rs":"238884aebf7fa2e4ab940faee4a5a2f5d6add35d2214ac644c36ec0ec64d7829","src/delta.rs":"4232e3acf98a70b111329c92cd823ba0419d4a12c3894091a318ae823dfdb225","src/draft.rs":"4f39d7acf3d3e868c013bab9f97ac570b983eed94d64fd59dba39343b870e4e0","src/expiration.rs":"188be15a5dd20d3471daa483609377ab66d876eb77afad1fc44891c18f251efd","src/jar.rs":"e91ba170610cc5b73a5cce8de1f6d960b26303ee5dff49822728e86a5fc64218","src/lib.rs":"e5a4bd66cf864e529ce81b33b02fe05fa807ad9ec1b368a062a05ad131ffdbba","src/parse.rs":"b37bd10631edc373ac82d9c9e5968416b04b3b705630ea4d4f9b1d7db9e695f6","src/secure/key.rs":"a8154b55c5435acba18132b90ab47f6d8ef229eb81240180079a99ae7885d8ca","src/secure/macros.rs":"18377b3fffdb2a1ad754f98f65a69d40a31fb69185fed231a4808ed4319985e4","src/secure/mod.rs":"6b2cf8486244545d51ecc5bd880845ae3f8d681ed86592e93cc4706b34704dcc","src/secure/private.rs":"0bd1b986d41b23fbb9c38836aa957b3db1f534c41dc205085a36d38099ad2f36","src/secure/signed.rs":"2375c29ca816e093fbee1db5631630f0ea47d92b1d16db458b7be14f30765117"},"package":"94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"}
|
|
@ -0,0 +1,142 @@
|
|||
# Version 0.16
|
||||
|
||||
## Version 0.16.0 (Dec 28, 2021)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The MSRV is now `1.53`, up from `1.41` in `0.15`.
|
||||
* `time` has been updated to `0.3` and is reexported from the crate root.
|
||||
|
||||
### General Changes
|
||||
|
||||
* `rust-crypto` dependencies were updated to their latest versions.
|
||||
|
||||
# Version 0.15
|
||||
|
||||
## Version 0.15.1 (Jul 14, 2021)
|
||||
|
||||
### Changes and Fixes
|
||||
|
||||
* A panic that could result from non-char boundary indexing was fixed.
|
||||
* Stale doc references to version `0.14` were updated.
|
||||
|
||||
## Version 0.15.0 (Feb 25, 2021)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* `Cookie::force_remove()` takes `&Cookie` instead of `Cookie`.
|
||||
* Child jar methods split into immutable and mutable versions
|
||||
(`Cookie::{private{_mut}, signed{_mut}}`).
|
||||
* `Cookie::encoded()` returns a new `Display` struct.
|
||||
* Dates with year `<= 99` are handled like Chrome: range `0..=68` maps to
|
||||
`2000..=2068`, `69..=99` to `1969..=1999`.
|
||||
* `Cookie::{set_}expires()` operates on a new `Expiration` enum.
|
||||
|
||||
### New Features
|
||||
|
||||
* Added `Cookie::make_removal()` to manually create expired cookies.
|
||||
* Added `Cookie::stripped()` display variant to print only the `name` and
|
||||
`value` of a cookie.
|
||||
* `Key` implements a constant-time `PartialEq`.
|
||||
* Added `Key::master()` to retrieve the full 512-bit master key.
|
||||
* Added `PrivateJar::decrypt()` to manually decrypt an encrypted `Cookie`.
|
||||
* Added `SignedJar::verify()` to manually verify a signed `Cookie`.
|
||||
* `Cookie::expires()` returns an `Option<Expiration>` to allow distinguishing
|
||||
between unset and `None` expirations.
|
||||
* Added `Cookie::expires_datetime()` to retrieve the expiration as an
|
||||
`OffsetDateTime`.
|
||||
* Added `Cookie::unset_expires()` to unset expirations.
|
||||
|
||||
### General Changes and Fixes
|
||||
|
||||
* MSRV is 1.41.
|
||||
|
||||
# Version 0.14
|
||||
|
||||
## Version 0.14.3 (Nov 5, 2020)
|
||||
|
||||
### Changes and Fixes
|
||||
|
||||
* `rust-crypto` dependencies were updated to their latest versions.
|
||||
|
||||
## Version 0.14.2 (Jul 22, 2020)
|
||||
|
||||
### Changes and Fixes
|
||||
|
||||
* Documentation now builds on the stable channel.
|
||||
* `rust-crypto` dependencies were updated to their latest versions.
|
||||
* Fixed 'interator' -> 'iterator' documentation typo.
|
||||
|
||||
## Version 0.14.1 (Jun 5, 2020)
|
||||
|
||||
### Changes and Fixes
|
||||
|
||||
* Updated `base64` dependency to 0.12.
|
||||
* Updated minimum `time` dependency to correct version: 0.2.11.
|
||||
* Added `readme` key to `Cargo.toml`, updated `license` field.
|
||||
|
||||
## Version 0.14.0 (May 29, 2020)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* The `Key::from_master()` method was deprecated in favor of the more aptly
|
||||
named `Key::derive_from()`.
|
||||
* The deprecated `CookieJar::clear()` method was removed.
|
||||
|
||||
### New Features
|
||||
|
||||
* Added `Key::from()` to create a `Key` structure from a full-length key.
|
||||
* Signed and private cookie jars can be individually enabled via the new
|
||||
`signed` and `private` features, respectively.
|
||||
* Key derivation via key expansion can be individually enabled via the new
|
||||
`key-expansion` feature.
|
||||
|
||||
### General Changes and Fixes
|
||||
|
||||
* `ring` is no longer a dependency: `RustCrypto`-based cryptography is used in
|
||||
lieu of `ring`. Prior to their inclusion here, the `hmac` and `hkdf` crates
|
||||
were audited.
|
||||
* Quotes, if present, are stripped from cookie values when parsing.
|
||||
|
||||
# Version 0.13
|
||||
|
||||
## Version 0.13.3 (Feb 3, 2020)
|
||||
|
||||
### Changes
|
||||
|
||||
* The `time` dependency was unpinned from `0.2.4`, allowing any `0.2.x`
|
||||
version of `time` where `x >= 6`.
|
||||
|
||||
## Version 0.13.2 (Jan 28, 2020)
|
||||
|
||||
### Changes
|
||||
|
||||
* The `time` dependency was pinned to `0.2.4` due to upstream breaking changes
|
||||
in `0.2.5`.
|
||||
|
||||
## Version 0.13.1 (Jan 23, 2020)
|
||||
|
||||
### New Features
|
||||
|
||||
* Added the `CookieJar::reset_delta()` method, which reverts all _delta_
|
||||
changes to a `CookieJar`.
|
||||
|
||||
## Version 0.13.0 (Jan 21, 2020)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* `time` was updated from 0.1 to 0.2.
|
||||
* `ring` was updated from 0.14 to 0.16.
|
||||
* `SameSite::None` now writes `SameSite=None` to correspond with updated
|
||||
`SameSite` draft. `SameSite` can be unset by passing `None` to
|
||||
`Cookie::set_same_site()`.
|
||||
* `CookieBuilder` gained a lifetime: `CookieBuilder<'c>`.
|
||||
|
||||
### General Changes and Fixes
|
||||
|
||||
* Added a CHANGELOG.
|
||||
* `expires`, `max_age`, `path`, and `domain` can be unset by passing `None` to
|
||||
the respective `Cookie::set_{field}()` method.
|
||||
* The "Expires" field is limited to a date-time of Dec 31, 9999, 23:59:59.
|
||||
* The `%` character is now properly encoded and decoded.
|
||||
* Constructor methods on `CookieBuilder` allow non-static lifetimes.
|
|
@ -11,30 +11,60 @@
|
|||
# will likely look very different (and much more reasonable)
|
||||
|
||||
[package]
|
||||
edition = "2018"
|
||||
name = "cookie"
|
||||
version = "0.12.0"
|
||||
authors = ["Alex Crichton <alex@alexcrichton.com>", "Sergio Benitez <sb@sergio.bz>"]
|
||||
description = "Crate for parsing HTTP cookie headers and managing a cookie jar. Supports signed\nand private (encrypted + signed) jars.\n"
|
||||
version = "0.16.0"
|
||||
authors = ["Sergio Benitez <sb@sergio.bz>", "Alex Crichton <alex@alexcrichton.com>"]
|
||||
build = "build.rs"
|
||||
description = "HTTP cookie parsing and cookie jar management. Supports signed and private\n(encrypted, authenticated) jars.\n"
|
||||
documentation = "https://docs.rs/cookie"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/alexcrichton/cookie-rs"
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/SergioBenitez/cookie-rs"
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
[dependencies.aes-gcm]
|
||||
version = "0.9.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.base64]
|
||||
version = "0.13"
|
||||
optional = true
|
||||
|
||||
[dependencies.hkdf]
|
||||
version = "0.12.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.hmac]
|
||||
version = "0.12.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.percent-encoding]
|
||||
version = "2.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.8"
|
||||
optional = true
|
||||
|
||||
[dependencies.sha2]
|
||||
version = "0.10.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.ring]
|
||||
version = "0.14.0"
|
||||
[dependencies.subtle]
|
||||
version = "2.3"
|
||||
optional = true
|
||||
|
||||
[dependencies.time]
|
||||
version = "0.1"
|
||||
|
||||
[dependencies.url]
|
||||
version = "1.0"
|
||||
optional = true
|
||||
version = "0.3"
|
||||
features = ["std", "parsing", "formatting", "macros"]
|
||||
default-features = false
|
||||
[build-dependencies.version_check]
|
||||
version = "0.9"
|
||||
|
||||
[features]
|
||||
percent-encode = ["url"]
|
||||
secure = ["ring", "base64"]
|
||||
key-expansion = ["sha2", "hkdf"]
|
||||
percent-encode = ["percent-encoding"]
|
||||
private = ["aes-gcm", "base64", "rand", "subtle"]
|
||||
secure = ["private", "signed", "key-expansion"]
|
||||
signed = ["hmac", "sha2", "base64", "rand", "subtle"]
|
||||
|
|
|
@ -186,7 +186,8 @@ APPENDIX: How to apply the Apache License to your work.
|
|||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
Copyright 2017 Sergio Benitez
|
||||
Copyright 2014 Alex Chricton
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
Copyright (c) 2017 Sergio Benitez
|
||||
Copyright (c) 2014 Alex Crichton
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# cookie-rs
|
||||
# Cookie
|
||||
|
||||
[![Build Status](https://travis-ci.com/SergioBenitez/cookie-rs.svg?branch=master)](https://travis-ci.com/SergioBenitez/cookie-rs)
|
||||
[![CI Status](https://github.com/SergioBenitez/cookie-rs/workflows/CI/badge.svg)](https://github.com/SergioBenitez/cookie-rs/actions)
|
||||
[![Current Crates.io Version](https://img.shields.io/crates/v/cookie.svg)](https://crates.io/crates/cookie)
|
||||
[![Documentation](https://docs.rs/cookie/badge.svg)](https://docs.rs/cookie)
|
||||
|
||||
A library for parsing HTTP cookies and managing cookie jars.
|
||||
A Rust library for parsing HTTP cookies and managing cookie jars.
|
||||
|
||||
# Usage
|
||||
|
||||
|
@ -11,11 +12,17 @@ Add the following to your `Cargo.toml`:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
cookie = "0.12"
|
||||
cookie = "0.16"
|
||||
```
|
||||
|
||||
See the [documentation](http://docs.rs/cookie) for detailed usage information.
|
||||
|
||||
# MSRV
|
||||
|
||||
khe minimum supported `rustc` version for cookie `0.16` is `1.53`.
|
||||
|
||||
The minimum supported `rustc` version for cookie `0.15` is `1.41`.
|
||||
|
||||
# License
|
||||
|
||||
This project is licensed under either of
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
fn main() {
|
||||
if let Some(true) = version_check::is_feature_flaggable() {
|
||||
println!("cargo:rustc-cfg=nightly");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cargo build --verbose
|
||||
|
||||
cargo test --verbose --features percent-encode
|
||||
cargo test --verbose --features private
|
||||
cargo test --verbose --features signed
|
||||
cargo test --verbose --features secure
|
||||
cargo test --verbose --features 'private,key-expansion'
|
||||
cargo test --verbose --features 'signed,key-expansion'
|
||||
cargo test --verbose --features 'secure,percent-encode'
|
||||
|
||||
cargo test --verbose
|
||||
cargo test --verbose --no-default-features
|
||||
cargo test --verbose --all-features
|
||||
|
||||
rustdoc --test README.md -L target
|
|
@ -1,25 +1,21 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use time::{Tm, Duration};
|
||||
|
||||
use ::{Cookie, SameSite};
|
||||
use crate::{Cookie, SameSite, Expiration};
|
||||
|
||||
/// Structure that follows the builder pattern for building `Cookie` structs.
|
||||
///
|
||||
/// To construct a cookie:
|
||||
///
|
||||
/// 1. Call [`Cookie::build`](struct.Cookie.html#method.build) to start building.
|
||||
/// 1. Call [`Cookie::build`] to start building.
|
||||
/// 2. Use any of the builder methods to set fields in the cookie.
|
||||
/// 3. Call [finish](#method.finish) to retrieve the built cookie.
|
||||
/// 3. Call [`CookieBuilder::finish()`] to retrieve the built cookie.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate cookie;
|
||||
/// extern crate time;
|
||||
///
|
||||
/// use cookie::Cookie;
|
||||
/// use time::Duration;
|
||||
/// use cookie::time::Duration;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let cookie: Cookie = Cookie::build("name", "value")
|
||||
|
@ -32,16 +28,15 @@ use ::{Cookie, SameSite};
|
|||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CookieBuilder {
|
||||
pub struct CookieBuilder<'c> {
|
||||
/// The cookie being built.
|
||||
cookie: Cookie<'static>,
|
||||
cookie: Cookie<'c>,
|
||||
}
|
||||
|
||||
impl CookieBuilder {
|
||||
impl<'c> CookieBuilder<'c> {
|
||||
/// Creates a new `CookieBuilder` instance from the given name and value.
|
||||
///
|
||||
/// This method is typically called indirectly via
|
||||
/// [Cookie::build](struct.Cookie.html#method.build).
|
||||
/// This method is typically called indirectly via [`Cookie::build()`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
@ -51,9 +46,9 @@ impl CookieBuilder {
|
|||
/// let c = Cookie::build("foo", "bar").finish();
|
||||
/// assert_eq!(c.name_value(), ("foo", "bar"));
|
||||
/// ```
|
||||
pub fn new<N, V>(name: N, value: V) -> CookieBuilder
|
||||
where N: Into<Cow<'static, str>>,
|
||||
V: Into<Cow<'static, str>>
|
||||
pub fn new<N, V>(name: N, value: V) -> Self
|
||||
where N: Into<Cow<'c, str>>,
|
||||
V: Into<Cow<'c, str>>
|
||||
{
|
||||
CookieBuilder { cookie: Cookie::new(name, value) }
|
||||
}
|
||||
|
@ -64,20 +59,25 @@ impl CookieBuilder {
|
|||
///
|
||||
/// ```rust
|
||||
/// # extern crate cookie;
|
||||
/// extern crate time;
|
||||
///
|
||||
/// use cookie::Cookie;
|
||||
/// use cookie::{Cookie, Expiration};
|
||||
/// use cookie::time::OffsetDateTime;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .expires(time::now())
|
||||
/// .expires(OffsetDateTime::now_utc())
|
||||
/// .finish();
|
||||
///
|
||||
/// assert!(c.expires().is_some());
|
||||
///
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
/// .expires(None)
|
||||
/// .finish();
|
||||
///
|
||||
/// assert_eq!(c.expires(), Some(Expiration::Session));
|
||||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn expires(mut self, when: Tm) -> CookieBuilder {
|
||||
pub fn expires<E: Into<Expiration>>(mut self, when: E) -> Self {
|
||||
self.cookie.set_expires(when);
|
||||
self
|
||||
}
|
||||
|
@ -88,10 +88,8 @@ impl CookieBuilder {
|
|||
///
|
||||
/// ```rust
|
||||
/// # extern crate cookie;
|
||||
/// extern crate time;
|
||||
/// use time::Duration;
|
||||
///
|
||||
/// use cookie::Cookie;
|
||||
/// use cookie::time::Duration;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
|
@ -102,7 +100,7 @@ impl CookieBuilder {
|
|||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn max_age(mut self, value: Duration) -> CookieBuilder {
|
||||
pub fn max_age(mut self, value: time::Duration) -> Self {
|
||||
self.cookie.set_max_age(value);
|
||||
self
|
||||
}
|
||||
|
@ -120,7 +118,7 @@ impl CookieBuilder {
|
|||
///
|
||||
/// assert_eq!(c.domain(), Some("www.rust-lang.org"));
|
||||
/// ```
|
||||
pub fn domain<D: Into<Cow<'static, str>>>(mut self, value: D) -> CookieBuilder {
|
||||
pub fn domain<D: Into<Cow<'c, str>>>(mut self, value: D) -> Self {
|
||||
self.cookie.set_domain(value);
|
||||
self
|
||||
}
|
||||
|
@ -138,7 +136,7 @@ impl CookieBuilder {
|
|||
///
|
||||
/// assert_eq!(c.path(), Some("/"));
|
||||
/// ```
|
||||
pub fn path<P: Into<Cow<'static, str>>>(mut self, path: P) -> CookieBuilder {
|
||||
pub fn path<P: Into<Cow<'c, str>>>(mut self, path: P) -> Self {
|
||||
self.cookie.set_path(path);
|
||||
self
|
||||
}
|
||||
|
@ -157,7 +155,7 @@ impl CookieBuilder {
|
|||
/// assert_eq!(c.secure(), Some(true));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn secure(mut self, value: bool) -> CookieBuilder {
|
||||
pub fn secure(mut self, value: bool) -> Self {
|
||||
self.cookie.set_secure(value);
|
||||
self
|
||||
}
|
||||
|
@ -176,7 +174,7 @@ impl CookieBuilder {
|
|||
/// assert_eq!(c.http_only(), Some(true));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn http_only(mut self, value: bool) -> CookieBuilder {
|
||||
pub fn http_only(mut self, value: bool) -> Self {
|
||||
self.cookie.set_http_only(value);
|
||||
self
|
||||
}
|
||||
|
@ -195,7 +193,7 @@ impl CookieBuilder {
|
|||
/// assert_eq!(c.same_site(), Some(SameSite::Strict));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn same_site(mut self, value: SameSite) -> CookieBuilder {
|
||||
pub fn same_site(mut self, value: SameSite) -> Self {
|
||||
self.cookie.set_same_site(value);
|
||||
self
|
||||
}
|
||||
|
@ -207,10 +205,8 @@ impl CookieBuilder {
|
|||
///
|
||||
/// ```rust
|
||||
/// # extern crate cookie;
|
||||
/// extern crate time;
|
||||
///
|
||||
/// use cookie::Cookie;
|
||||
/// use time::Duration;
|
||||
/// use cookie::time::Duration;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let c = Cookie::build("foo", "bar")
|
||||
|
@ -222,7 +218,7 @@ impl CookieBuilder {
|
|||
/// # }
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn permanent(mut self) -> CookieBuilder {
|
||||
pub fn permanent(mut self) -> Self {
|
||||
self.cookie.make_permanent();
|
||||
self
|
||||
}
|
||||
|
@ -244,7 +240,7 @@ impl CookieBuilder {
|
|||
/// assert_eq!(c.path(), Some("/"));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn finish(self) -> Cookie<'static> {
|
||||
pub fn finish(self) -> Cookie<'c> {
|
||||
self.cookie
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::ops::{Deref, DerefMut};
|
|||
use std::hash::{Hash, Hasher};
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use Cookie;
|
||||
use crate::Cookie;
|
||||
|
||||
/// A `DeltaCookie` is a helper structure used in a cookie jar. It wraps a
|
||||
/// `Cookie` so that it can be hashed and compared purely by name. It further
|
||||
|
@ -19,20 +19,14 @@ impl DeltaCookie {
|
|||
/// Create a new `DeltaCookie` that is being added to a jar.
|
||||
#[inline]
|
||||
pub fn added(cookie: Cookie<'static>) -> DeltaCookie {
|
||||
DeltaCookie {
|
||||
cookie: cookie,
|
||||
removed: false,
|
||||
}
|
||||
DeltaCookie { cookie, removed: false, }
|
||||
}
|
||||
|
||||
/// Create a new `DeltaCookie` that is being removed from a jar. The
|
||||
/// `cookie` should be a "removal" cookie.
|
||||
#[inline]
|
||||
pub fn removed(cookie: Cookie<'static>) -> DeltaCookie {
|
||||
DeltaCookie {
|
||||
cookie: cookie,
|
||||
removed: true,
|
||||
}
|
||||
DeltaCookie { cookie, removed: true, }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,18 +10,28 @@ use std::fmt;
|
|||
/// attribute is "Strict", then the cookie is never sent in cross-site requests.
|
||||
/// If the `SameSite` attribute is "Lax", the cookie is only sent in cross-site
|
||||
/// requests with "safe" HTTP methods, i.e, `GET`, `HEAD`, `OPTIONS`, `TRACE`.
|
||||
/// If the `SameSite` attribute is not present (made explicit via the
|
||||
/// `SameSite::None` variant), then the cookie will be sent as normal.
|
||||
/// If the `SameSite` attribute is "None", the cookie is sent in all cross-site
|
||||
/// requests if the "Secure" flag is also set, otherwise the cookie is ignored.
|
||||
/// This library automatically sets the "Secure" flag on cookies when
|
||||
/// `same_site` is set to `SameSite::None` as long as `secure` is not explicitly
|
||||
/// set to `false`.
|
||||
///
|
||||
/// **Note:** This cookie attribute is an HTTP draft! Its meaning and definition
|
||||
/// are subject to change.
|
||||
/// If the `SameSite` attribute is not present (by not setting `SameSite`
|
||||
/// initally or passing `None` to [`Cookie::set_same_site()`]), then the cookie
|
||||
/// will be sent as normal.
|
||||
///
|
||||
/// **Note:** This cookie attribute is an [HTTP draft]! Its meaning and
|
||||
/// definition are subject to change.
|
||||
///
|
||||
/// [`Cookie::set_same_site()`]: crate::Cookie::set_same_site()
|
||||
/// [HTTP draft]: https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SameSite {
|
||||
/// The "Strict" `SameSite` attribute.
|
||||
Strict,
|
||||
/// The "Lax" `SameSite` attribute.
|
||||
Lax,
|
||||
/// No `SameSite` attribute.
|
||||
/// The "None" `SameSite` attribute.
|
||||
None
|
||||
}
|
||||
|
||||
|
@ -92,7 +102,7 @@ impl fmt::Display for SameSite {
|
|||
match *self {
|
||||
SameSite::Strict => write!(f, "Strict"),
|
||||
SameSite::Lax => write!(f, "Lax"),
|
||||
SameSite::None => Ok(()),
|
||||
SameSite::None => write!(f, "None"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
use time::OffsetDateTime;
|
||||
|
||||
/// A cookie's expiration: either session or a date-time.
|
||||
///
|
||||
/// An `Expiration` is constructible via `Expiration::from()` with an
|
||||
/// `Option<OffsetDateTime>` or an `OffsetDateTime`:
|
||||
///
|
||||
/// * `None` -> `Expiration::Session`
|
||||
/// * `Some(OffsetDateTime)` -> `Expiration::DateTime`
|
||||
/// * `OffsetDateTime` -> `Expiration::DateTime`
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::Expiration;
|
||||
/// use time::OffsetDateTime;
|
||||
///
|
||||
/// let expires = Expiration::from(None);
|
||||
/// assert_eq!(expires, Expiration::Session);
|
||||
///
|
||||
/// let now = OffsetDateTime::now_utc();
|
||||
/// let expires = Expiration::from(now);
|
||||
/// assert_eq!(expires, Expiration::DateTime(now));
|
||||
///
|
||||
/// let expires = Expiration::from(Some(now));
|
||||
/// assert_eq!(expires, Expiration::DateTime(now));
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Expiration {
|
||||
/// Expiration for a "permanent" cookie at a specific date-time.
|
||||
DateTime(OffsetDateTime),
|
||||
/// Expiration for a "session" cookie. Browsers define the notion of a
|
||||
/// "session" and will automatically expire session cookies when they deem
|
||||
/// the "session" to be over. This is typically, but need not be, when the
|
||||
/// browser is closed.
|
||||
Session,
|
||||
}
|
||||
|
||||
impl Expiration {
|
||||
/// Returns `true` if `self` is an `Expiration::DateTime`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::Expiration;
|
||||
/// use time::OffsetDateTime;
|
||||
///
|
||||
/// let expires = Expiration::from(None);
|
||||
/// assert!(!expires.is_datetime());
|
||||
///
|
||||
/// let expires = Expiration::from(OffsetDateTime::now_utc());
|
||||
/// assert!(expires.is_datetime());
|
||||
/// ```
|
||||
pub fn is_datetime(&self) -> bool {
|
||||
match self {
|
||||
Expiration::DateTime(_) => true,
|
||||
Expiration::Session => false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` is an `Expiration::Session`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::Expiration;
|
||||
/// use time::OffsetDateTime;
|
||||
///
|
||||
/// let expires = Expiration::from(None);
|
||||
/// assert!(expires.is_session());
|
||||
///
|
||||
/// let expires = Expiration::from(OffsetDateTime::now_utc());
|
||||
/// assert!(!expires.is_session());
|
||||
/// ```
|
||||
pub fn is_session(&self) -> bool {
|
||||
match self {
|
||||
Expiration::DateTime(_) => false,
|
||||
Expiration::Session => true
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the inner `OffsetDateTime` if `self` is a `DateTime`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::Expiration;
|
||||
/// use time::OffsetDateTime;
|
||||
///
|
||||
/// let expires = Expiration::from(None);
|
||||
/// assert!(expires.datetime().is_none());
|
||||
///
|
||||
/// let now = OffsetDateTime::now_utc();
|
||||
/// let expires = Expiration::from(now);
|
||||
/// assert_eq!(expires.datetime(), Some(now));
|
||||
/// ```
|
||||
pub fn datetime(self) -> Option<OffsetDateTime> {
|
||||
match self {
|
||||
Expiration::Session => None,
|
||||
Expiration::DateTime(v) => Some(v)
|
||||
}
|
||||
}
|
||||
|
||||
/// Applied `f` to the inner `OffsetDateTime` if `self` is a `DateTime` and
|
||||
/// returns the mapped `Expiration`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::Expiration;
|
||||
/// use time::{OffsetDateTime, Duration};
|
||||
///
|
||||
/// let now = OffsetDateTime::now_utc();
|
||||
/// let one_week = Duration::weeks(1);
|
||||
///
|
||||
/// let expires = Expiration::from(now);
|
||||
/// assert_eq!(expires.map(|t| t + one_week).datetime(), Some(now + one_week));
|
||||
///
|
||||
/// let expires = Expiration::from(None);
|
||||
/// assert_eq!(expires.map(|t| t + one_week).datetime(), None);
|
||||
/// ```
|
||||
pub fn map<F>(self, f: F) -> Self
|
||||
where F: FnOnce(OffsetDateTime) -> OffsetDateTime
|
||||
{
|
||||
match self {
|
||||
Expiration::Session => Expiration::Session,
|
||||
Expiration::DateTime(v) => Expiration::DateTime(f(v)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Option<OffsetDateTime>>> From<T> for Expiration {
|
||||
fn from(option: T) -> Self {
|
||||
match option.into() {
|
||||
Some(value) => Expiration::DateTime(value),
|
||||
None => Expiration::Session
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,18 +1,17 @@
|
|||
use std::collections::HashSet;
|
||||
use std::mem::replace;
|
||||
|
||||
use time::{self, Duration};
|
||||
#[cfg(feature = "signed")] use crate::secure::SignedJar;
|
||||
#[cfg(feature = "private")] use crate::secure::PrivateJar;
|
||||
#[cfg(any(feature = "signed", feature = "private"))] use crate::secure::Key;
|
||||
|
||||
#[cfg(feature = "secure")]
|
||||
use secure::{PrivateJar, SignedJar, Key};
|
||||
use delta::DeltaCookie;
|
||||
use Cookie;
|
||||
use crate::delta::DeltaCookie;
|
||||
use crate::Cookie;
|
||||
|
||||
/// A collection of cookies that tracks its modifications.
|
||||
///
|
||||
/// A `CookieJar` provides storage for any number of cookies. Any changes made
|
||||
/// to the jar are tracked; the changes can be retrieved via the
|
||||
/// [delta](#method.delta) method which returns an interator over the changes.
|
||||
/// [delta](#method.delta) method which returns an iterator over the changes.
|
||||
///
|
||||
/// # Usage
|
||||
///
|
||||
|
@ -118,7 +117,7 @@ impl CookieJar {
|
|||
self.delta_cookies
|
||||
.get(name)
|
||||
.or_else(|| self.original_cookies.get(name))
|
||||
.and_then(|c| if !c.removed { Some(&c.cookie) } else { None })
|
||||
.and_then(|c| if c.removed { None } else { Some(&c.cookie) })
|
||||
}
|
||||
|
||||
/// Adds an "original" `cookie` to this jar. If an original cookie with the
|
||||
|
@ -179,7 +178,7 @@ impl CookieJar {
|
|||
///
|
||||
/// A "removal" cookie is a cookie that has the same name as the original
|
||||
/// cookie but has an empty value, a max-age of 0, and an expiration date
|
||||
/// far in the past.
|
||||
/// far in the past. See also [`Cookie::make_removal()`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
@ -187,10 +186,8 @@ impl CookieJar {
|
|||
///
|
||||
/// ```rust
|
||||
/// # extern crate cookie;
|
||||
/// extern crate time;
|
||||
///
|
||||
/// use cookie::{CookieJar, Cookie};
|
||||
/// use time::Duration;
|
||||
/// use cookie::time::Duration;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut jar = CookieJar::new();
|
||||
|
@ -209,7 +206,8 @@ impl CookieJar {
|
|||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Removing a new cookie does not result in a _removal_ cookie:
|
||||
/// Removing a new cookie does not result in a _removal_ cookie unless
|
||||
/// there's an original cookie with the same name:
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::{CookieJar, Cookie};
|
||||
|
@ -220,12 +218,17 @@ impl CookieJar {
|
|||
///
|
||||
/// jar.remove(Cookie::named("name"));
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
///
|
||||
/// jar.add_original(Cookie::new("name", "value"));
|
||||
/// jar.add(Cookie::new("name", "value"));
|
||||
/// assert_eq!(jar.delta().count(), 1);
|
||||
///
|
||||
/// jar.remove(Cookie::named("name"));
|
||||
/// assert_eq!(jar.delta().count(), 1);
|
||||
/// ```
|
||||
pub fn remove(&mut self, mut cookie: Cookie<'static>) {
|
||||
if self.original_cookies.contains(cookie.name()) {
|
||||
cookie.set_value("");
|
||||
cookie.set_max_age(Duration::seconds(0));
|
||||
cookie.set_expires(time::now() - Duration::days(365));
|
||||
cookie.make_removal();
|
||||
self.delta_cookies.replace(DeltaCookie::removed(cookie));
|
||||
} else {
|
||||
self.delta_cookies.remove(cookie.name());
|
||||
|
@ -243,10 +246,8 @@ impl CookieJar {
|
|||
///
|
||||
/// ```rust
|
||||
/// # extern crate cookie;
|
||||
/// extern crate time;
|
||||
///
|
||||
/// use cookie::{CookieJar, Cookie};
|
||||
/// use time::Duration;
|
||||
/// use cookie::time::Duration;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut jar = CookieJar::new();
|
||||
|
@ -258,30 +259,56 @@ impl CookieJar {
|
|||
/// assert_eq!(jar.iter().count(), 2);
|
||||
///
|
||||
/// // Now force remove the original cookie.
|
||||
/// jar.force_remove(Cookie::new("name", "value"));
|
||||
/// jar.force_remove(&Cookie::named("name"));
|
||||
/// assert_eq!(jar.delta().count(), 1);
|
||||
/// assert_eq!(jar.iter().count(), 1);
|
||||
///
|
||||
/// // Now force remove the new cookie.
|
||||
/// jar.force_remove(Cookie::new("key", "value"));
|
||||
/// jar.force_remove(&Cookie::named("key"));
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
/// assert_eq!(jar.iter().count(), 0);
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn force_remove<'a>(&mut self, cookie: Cookie<'a>) {
|
||||
pub fn force_remove<'a>(&mut self, cookie: &Cookie<'a>) {
|
||||
self.original_cookies.remove(cookie.name());
|
||||
self.delta_cookies.remove(cookie.name());
|
||||
}
|
||||
|
||||
/// Removes all cookies from this cookie jar.
|
||||
#[deprecated(since = "0.7.0", note = "calling this method may not remove \
|
||||
all cookies since the path and domain are not specified; use \
|
||||
`remove` instead")]
|
||||
pub fn clear(&mut self) {
|
||||
self.delta_cookies.clear();
|
||||
for delta in replace(&mut self.original_cookies, HashSet::new()) {
|
||||
self.remove(delta.cookie);
|
||||
}
|
||||
/// Removes all delta cookies, i.e. all cookies not added via
|
||||
/// [`CookieJar::add_original()`], from this `CookieJar`. This undoes any
|
||||
/// changes from [`CookieJar::add()`] and [`CookieJar::remove()`]
|
||||
/// operations.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::{CookieJar, Cookie};
|
||||
///
|
||||
/// let mut jar = CookieJar::new();
|
||||
///
|
||||
/// // Only original cookies will remain after calling `reset_delta`.
|
||||
/// jar.add_original(Cookie::new("name", "value"));
|
||||
/// jar.add_original(Cookie::new("language", "Rust"));
|
||||
///
|
||||
/// // These operations, represented by delta cookies, will be reset.
|
||||
/// jar.add(Cookie::new("language", "C++"));
|
||||
/// jar.remove(Cookie::named("name"));
|
||||
///
|
||||
/// // All is normal.
|
||||
/// assert_eq!(jar.get("name"), None);
|
||||
/// assert_eq!(jar.get("language").map(Cookie::value), Some("C++"));
|
||||
/// assert_eq!(jar.iter().count(), 1);
|
||||
/// assert_eq!(jar.delta().count(), 2);
|
||||
///
|
||||
/// // Resetting undoes delta operations.
|
||||
/// jar.reset_delta();
|
||||
/// assert_eq!(jar.get("name").map(Cookie::value), Some("value"));
|
||||
/// assert_eq!(jar.get("language").map(Cookie::value), Some("Rust"));
|
||||
/// assert_eq!(jar.iter().count(), 2);
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
/// ```
|
||||
pub fn reset_delta(&mut self) {
|
||||
self.delta_cookies = HashSet::new();
|
||||
}
|
||||
|
||||
/// Returns an iterator over cookies that represent the changes to this jar
|
||||
|
@ -350,14 +377,9 @@ impl CookieJar {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a `PrivateJar` with `self` as its parent jar using the key `key`
|
||||
/// to sign/encrypt and verify/decrypt cookies added/retrieved from the
|
||||
/// child jar.
|
||||
///
|
||||
/// Any modifications to the child jar will be reflected on the parent jar,
|
||||
/// and any retrievals from the child jar will be made from the parent jar.
|
||||
///
|
||||
/// This method is only available when the `secure` feature is enabled.
|
||||
/// Returns a read-only `PrivateJar` with `self` as its parent jar using the
|
||||
/// key `key` to verify/decrypt cookies retrieved from the child jar. Any
|
||||
/// retrievals from the child jar will be made from the parent jar.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
@ -369,7 +391,7 @@ impl CookieJar {
|
|||
///
|
||||
/// // Add a private (signed + encrypted) cookie.
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.private(&key).add(Cookie::new("private", "text"));
|
||||
/// jar.private_mut(&key).add(Cookie::new("private", "text"));
|
||||
///
|
||||
/// // The cookie's contents are encrypted.
|
||||
/// assert_ne!(jar.get("private").unwrap().value(), "text");
|
||||
|
@ -383,18 +405,43 @@ impl CookieJar {
|
|||
/// assert!(jar.private(&key).get("private").is_none());
|
||||
/// assert!(jar.get("private").is_some());
|
||||
/// ```
|
||||
#[cfg(feature = "secure")]
|
||||
pub fn private(&mut self, key: &Key) -> PrivateJar {
|
||||
#[cfg(feature = "private")]
|
||||
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))]
|
||||
pub fn private<'a>(&'a self, key: &Key) -> PrivateJar<&'a Self> {
|
||||
PrivateJar::new(self, key)
|
||||
}
|
||||
|
||||
/// Returns a `SignedJar` with `self` as its parent jar using the key `key`
|
||||
/// to sign/verify cookies added/retrieved from the child jar.
|
||||
/// Returns a read/write `PrivateJar` with `self` as its parent jar using
|
||||
/// the key `key` to sign/encrypt and verify/decrypt cookies added/retrieved
|
||||
/// from the child jar.
|
||||
///
|
||||
/// Any modifications to the child jar will be reflected on the parent jar,
|
||||
/// and any retrievals from the child jar will be made from the parent jar.
|
||||
///
|
||||
/// This method is only available when the `secure` feature is enabled.
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::{Cookie, CookieJar, Key};
|
||||
///
|
||||
/// // Generate a secure key.
|
||||
/// let key = Key::generate();
|
||||
///
|
||||
/// // Add a private (signed + encrypted) cookie.
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.private_mut(&key).add(Cookie::new("private", "text"));
|
||||
///
|
||||
/// // Remove a cookie using the child jar.
|
||||
/// jar.private_mut(&key).remove(Cookie::named("private"));
|
||||
/// ```
|
||||
#[cfg(feature = "private")]
|
||||
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))]
|
||||
pub fn private_mut<'a>(&'a mut self, key: &Key) -> PrivateJar<&'a mut Self> {
|
||||
PrivateJar::new(self, key)
|
||||
}
|
||||
|
||||
/// Returns a read-only `SignedJar` with `self` as its parent jar using the
|
||||
/// key `key` to verify cookies retrieved from the child jar. Any retrievals
|
||||
/// from the child jar will be made from the parent jar.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
@ -406,7 +453,7 @@ impl CookieJar {
|
|||
///
|
||||
/// // Add a signed cookie.
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.signed(&key).add(Cookie::new("signed", "text"));
|
||||
/// jar.signed_mut(&key).add(Cookie::new("signed", "text"));
|
||||
///
|
||||
/// // The cookie's contents are signed but still in plaintext.
|
||||
/// assert_ne!(jar.get("signed").unwrap().value(), "text");
|
||||
|
@ -421,8 +468,36 @@ impl CookieJar {
|
|||
/// assert!(jar.signed(&key).get("signed").is_none());
|
||||
/// assert!(jar.get("signed").is_some());
|
||||
/// ```
|
||||
#[cfg(feature = "secure")]
|
||||
pub fn signed(&mut self, key: &Key) -> SignedJar {
|
||||
#[cfg(feature = "signed")]
|
||||
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))]
|
||||
pub fn signed<'a>(&'a self, key: &Key) -> SignedJar<&'a Self> {
|
||||
SignedJar::new(self, key)
|
||||
}
|
||||
|
||||
/// Returns a read/write `SignedJar` with `self` as its parent jar using the
|
||||
/// key `key` to sign/verify cookies added/retrieved from the child jar.
|
||||
///
|
||||
/// Any modifications to the child jar will be reflected on the parent jar,
|
||||
/// and any retrievals from the child jar will be made from the parent jar.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::{Cookie, CookieJar, Key};
|
||||
///
|
||||
/// // Generate a secure key.
|
||||
/// let key = Key::generate();
|
||||
///
|
||||
/// // Add a signed cookie.
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.signed_mut(&key).add(Cookie::new("signed", "text"));
|
||||
///
|
||||
/// // Remove a cookie.
|
||||
/// jar.signed_mut(&key).remove(Cookie::named("signed"));
|
||||
/// ```
|
||||
#[cfg(feature = "signed")]
|
||||
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))]
|
||||
pub fn signed_mut<'a>(&'a mut self, key: &Key) -> SignedJar<&'a mut Self> {
|
||||
SignedJar::new(self, key)
|
||||
}
|
||||
}
|
||||
|
@ -468,7 +543,7 @@ impl<'a> Iterator for Iter<'a> {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::CookieJar;
|
||||
use Cookie;
|
||||
use crate::Cookie;
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
|
@ -483,7 +558,8 @@ mod test {
|
|||
assert!(c.get("test2").is_some());
|
||||
|
||||
c.add(Cookie::new("test3", ""));
|
||||
c.clear();
|
||||
c.remove(Cookie::named("test2"));
|
||||
c.remove(Cookie::named("test3"));
|
||||
|
||||
assert!(c.get("test").is_none());
|
||||
assert!(c.get("test2").is_none());
|
||||
|
@ -500,9 +576,9 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "secure")]
|
||||
#[cfg(all(feature = "signed", feature = "private"))]
|
||||
fn iter() {
|
||||
let key = ::Key::generate();
|
||||
let key = crate::Key::generate();
|
||||
let mut c = CookieJar::new();
|
||||
|
||||
c.add_original(Cookie::new("original", "original"));
|
||||
|
@ -512,8 +588,8 @@ mod test {
|
|||
c.add(Cookie::new("test3", "test3"));
|
||||
assert_eq!(c.iter().count(), 4);
|
||||
|
||||
c.signed(&key).add(Cookie::new("signed", "signed"));
|
||||
c.private(&key).add(Cookie::new("encrypted", "encrypted"));
|
||||
c.signed_mut(&key).add(Cookie::new("signed", "signed"));
|
||||
c.private_mut(&key).add(Cookie::new("encrypted", "encrypted"));
|
||||
assert_eq!(c.iter().count(), 6);
|
||||
|
||||
c.remove(Cookie::named("test"));
|
||||
|
@ -531,7 +607,6 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "secure")]
|
||||
fn delta() {
|
||||
use std::collections::HashMap;
|
||||
use time::Duration;
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,21 +1,29 @@
|
|||
use std::borrow::Cow;
|
||||
use std::cmp;
|
||||
use std::error::Error;
|
||||
use std::convert::{From, TryFrom};
|
||||
use std::str::Utf8Error;
|
||||
use std::fmt;
|
||||
use std::convert::From;
|
||||
|
||||
#[allow(unused_imports, deprecated)]
|
||||
use std::ascii::AsciiExt;
|
||||
|
||||
#[cfg(feature = "percent-encode")]
|
||||
use url::percent_encoding::percent_decode;
|
||||
use time::{self, Duration};
|
||||
use percent_encoding::percent_decode;
|
||||
use time::{PrimitiveDateTime, Duration, OffsetDateTime};
|
||||
use time::{parsing::Parsable, macros::format_description, format_description::FormatItem};
|
||||
|
||||
use ::{Cookie, SameSite, CookieStr};
|
||||
use crate::{Cookie, SameSite, CookieStr};
|
||||
|
||||
// The three formats spec'd in http://tools.ietf.org/html/rfc2616#section-3.3.1.
|
||||
// Additional ones as encountered in the real world.
|
||||
pub static FMT1: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day] [month repr:short] [year padding:none] [hour]:[minute]:[second] GMT");
|
||||
pub static FMT2: &[FormatItem<'_>] = format_description!("[weekday], [day]-[month repr:short]-[year repr:last_two] [hour]:[minute]:[second] GMT");
|
||||
pub static FMT3: &[FormatItem<'_>] = format_description!("[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]:[second] [year padding:none]");
|
||||
pub static FMT4: &[FormatItem<'_>] = format_description!("[weekday repr:short], [day]-[month repr:short]-[year padding:none] [hour]:[minute]:[second] GMT");
|
||||
|
||||
/// Enum corresponding to a parsing error.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
pub enum ParseError {
|
||||
/// The cookie did not contain a name/value pair.
|
||||
MissingPair,
|
||||
|
@ -23,10 +31,6 @@ pub enum ParseError {
|
|||
EmptyName,
|
||||
/// Decoding the cookie's name or value resulted in invalid UTF-8.
|
||||
Utf8Error(Utf8Error),
|
||||
/// It is discouraged to exhaustively match on this enum as its variants may
|
||||
/// grow without a breaking-change bump in version numbers.
|
||||
#[doc(hidden)]
|
||||
__Nonexhasutive,
|
||||
}
|
||||
|
||||
impl ParseError {
|
||||
|
@ -38,7 +42,6 @@ impl ParseError {
|
|||
ParseError::Utf8Error(_) => {
|
||||
"decoding the cookie's name or value resulted in invalid UTF-8"
|
||||
}
|
||||
ParseError::__Nonexhasutive => unreachable!("__Nonexhasutive ParseError"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,18 +82,39 @@ fn indexes_of(needle: &str, haystack: &str) -> Option<(usize, usize)> {
|
|||
}
|
||||
|
||||
#[cfg(feature = "percent-encode")]
|
||||
fn name_val_decoded(name: &str, val: &str) -> Result<(CookieStr, CookieStr), ParseError> {
|
||||
fn name_val_decoded(
|
||||
name: &str,
|
||||
val: &str
|
||||
) -> Result<Option<(CookieStr<'static>, CookieStr<'static>)>, ParseError> {
|
||||
let decoded_name = percent_decode(name.as_bytes()).decode_utf8()?;
|
||||
let decoded_value = percent_decode(val.as_bytes()).decode_utf8()?;
|
||||
let name = CookieStr::Concrete(Cow::Owned(decoded_name.into_owned()));
|
||||
let val = CookieStr::Concrete(Cow::Owned(decoded_value.into_owned()));
|
||||
|
||||
Ok((name, val))
|
||||
if let (&Cow::Borrowed(_), &Cow::Borrowed(_)) = (&decoded_name, &decoded_value) {
|
||||
Ok(None)
|
||||
} else {
|
||||
let name = CookieStr::Concrete(Cow::Owned(decoded_name.into()));
|
||||
let val = CookieStr::Concrete(Cow::Owned(decoded_value.into()));
|
||||
Ok(Some((name, val)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "percent-encode"))]
|
||||
fn name_val_decoded(_: &str, _: &str) -> Result<(CookieStr, CookieStr), ParseError> {
|
||||
unreachable!("This function should never be called when the feature is disabled!")
|
||||
fn name_val_decoded(
|
||||
_: &str,
|
||||
_: &str
|
||||
) -> Result<Option<(CookieStr<'static>, CookieStr<'static>)>, ParseError> {
|
||||
unreachable!("This function should never be called with 'percent-encode' disabled!")
|
||||
}
|
||||
|
||||
fn trim_quotes(s: &str) -> &str {
|
||||
if s.len() < 2 {
|
||||
return s;
|
||||
}
|
||||
|
||||
match (s.chars().next(), s.chars().last()) {
|
||||
(Some('"'), Some('"')) => &s[1..(s.len() - 1)],
|
||||
_ => s
|
||||
}
|
||||
}
|
||||
|
||||
// This function does the real parsing but _does not_ set the `cookie_string` in
|
||||
|
@ -99,14 +123,14 @@ fn name_val_decoded(_: &str, _: &str) -> Result<(CookieStr, CookieStr), ParseErr
|
|||
// set in the outer `parse` function.
|
||||
fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
|
||||
let mut attributes = s.split(';');
|
||||
let key_value = match attributes.next() {
|
||||
Some(s) => s,
|
||||
_ => panic!(),
|
||||
};
|
||||
|
||||
// Determine the name = val.
|
||||
let key_value = attributes.next().expect("first str::split().next() returns Some");
|
||||
let (name, value) = match key_value.find('=') {
|
||||
Some(i) => (key_value[..i].trim(), key_value[(i + 1)..].trim()),
|
||||
Some(i) => {
|
||||
let (key, value) = (key_value[..i].trim(), key_value[(i + 1)..].trim());
|
||||
(key, trim_quotes(value).trim())
|
||||
},
|
||||
None => return Err(ParseError::MissingPair)
|
||||
};
|
||||
|
||||
|
@ -114,23 +138,29 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
|
|||
return Err(ParseError::EmptyName);
|
||||
}
|
||||
|
||||
// Create a cookie with all of the defaults. We'll fill things in while we
|
||||
// iterate through the parameters below.
|
||||
let (name, value) = if decode {
|
||||
name_val_decoded(name, value)?
|
||||
} else {
|
||||
// If there is nothing to decode, or we're not decoding, use indexes.
|
||||
let indexed_names = |s, name, value| {
|
||||
let name_indexes = indexes_of(name, s).expect("name sub");
|
||||
let value_indexes = indexes_of(value, s).expect("value sub");
|
||||
let name = CookieStr::Indexed(name_indexes.0, name_indexes.1);
|
||||
let value = CookieStr::Indexed(value_indexes.0, value_indexes.1);
|
||||
|
||||
(name, value)
|
||||
};
|
||||
|
||||
let mut cookie = Cookie {
|
||||
// Create a cookie with all of the defaults. We'll fill things in while we
|
||||
// iterate through the parameters below.
|
||||
let (name, value) = if decode {
|
||||
match name_val_decoded(name, value)? {
|
||||
Some((name, value)) => (name, value),
|
||||
None => indexed_names(s, name, value)
|
||||
}
|
||||
} else {
|
||||
indexed_names(s, name, value)
|
||||
};
|
||||
|
||||
let mut cookie: Cookie<'c> = Cookie {
|
||||
name, value,
|
||||
cookie_string: None,
|
||||
name: name,
|
||||
value: value,
|
||||
expires: None,
|
||||
max_age: None,
|
||||
domain: None,
|
||||
|
@ -149,21 +179,26 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
|
|||
match (&*key.to_ascii_lowercase(), value) {
|
||||
("secure", _) => cookie.secure = Some(true),
|
||||
("httponly", _) => cookie.http_only = Some(true),
|
||||
("max-age", Some(v)) => {
|
||||
// See RFC 6265 Section 5.2.2, negative values indicate that the
|
||||
// earliest possible expiration time should be used, so set the
|
||||
// max age as 0 seconds.
|
||||
cookie.max_age = match v.parse() {
|
||||
Ok(val) if val <= 0 => Some(Duration::zero()),
|
||||
Ok(val) => {
|
||||
// Don't panic if the max age seconds is greater than what's supported by
|
||||
// `Duration`.
|
||||
let val = cmp::min(val, Duration::max_value().num_seconds());
|
||||
Some(Duration::seconds(val))
|
||||
}
|
||||
Err(_) => continue,
|
||||
};
|
||||
}
|
||||
("max-age", Some(mut v)) => cookie.max_age = {
|
||||
let is_negative = v.starts_with('-');
|
||||
if is_negative {
|
||||
v = &v[1..];
|
||||
}
|
||||
|
||||
if !v.chars().all(|d| d.is_digit(10)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// From RFC 6265 5.2.2: neg values indicate that the earliest
|
||||
// expiration should be used, so set the max age to 0 seconds.
|
||||
if is_negative {
|
||||
Some(Duration::ZERO)
|
||||
} else {
|
||||
Some(v.parse::<i64>()
|
||||
.map(Duration::seconds)
|
||||
.unwrap_or_else(|_| Duration::seconds(i64::max_value())))
|
||||
}
|
||||
},
|
||||
("domain", Some(mut domain)) if !domain.is_empty() => {
|
||||
if domain.starts_with('.') {
|
||||
domain = &domain[1..];
|
||||
|
@ -181,6 +216,8 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
|
|||
cookie.same_site = Some(SameSite::Strict);
|
||||
} else if v.eq_ignore_ascii_case("lax") {
|
||||
cookie.same_site = Some(SameSite::Lax);
|
||||
} else if v.eq_ignore_ascii_case("none") {
|
||||
cookie.same_site = Some(SameSite::None);
|
||||
} else {
|
||||
// We do nothing here, for now. When/if the `SameSite`
|
||||
// attribute becomes standard, the spec says that we should
|
||||
|
@ -190,16 +227,14 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
|
|||
}
|
||||
}
|
||||
("expires", Some(v)) => {
|
||||
// Try strptime with three date formats according to
|
||||
// http://tools.ietf.org/html/rfc2616#section-3.3.1. Try
|
||||
// additional ones as encountered in the real world.
|
||||
let tm = time::strptime(v, "%a, %d %b %Y %H:%M:%S %Z")
|
||||
.or_else(|_| time::strptime(v, "%A, %d-%b-%y %H:%M:%S %Z"))
|
||||
.or_else(|_| time::strptime(v, "%a, %d-%b-%Y %H:%M:%S %Z"))
|
||||
.or_else(|_| time::strptime(v, "%a %b %d %H:%M:%S %Y"));
|
||||
let tm = parse_date(v, &FMT1)
|
||||
.or_else(|_| parse_date(v, &FMT2))
|
||||
.or_else(|_| parse_date(v, &FMT3))
|
||||
.or_else(|_| parse_date(v, &FMT4));
|
||||
// .or_else(|_| parse_date(v, &FMT5));
|
||||
|
||||
if let Ok(time) = tm {
|
||||
cookie.expires = Some(time)
|
||||
cookie.expires = Some(time.into())
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
@ -214,7 +249,7 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result<Cookie<'c>, ParseError> {
|
|||
Ok(cookie)
|
||||
}
|
||||
|
||||
pub fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result<Cookie<'c>, ParseError>
|
||||
pub(crate) fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result<Cookie<'c>, ParseError>
|
||||
where S: Into<Cow<'c, str>>
|
||||
{
|
||||
let s = cow.into();
|
||||
|
@ -223,10 +258,27 @@ pub fn parse_cookie<'c, S>(cow: S, decode: bool) -> Result<Cookie<'c>, ParseErro
|
|||
Ok(cookie)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_date(s: &str, format: &impl Parsable) -> Result<OffsetDateTime, time::Error> {
|
||||
// Parse. Handle "abbreviated" dates like Chromium. See cookie#162.
|
||||
let mut date = format.parse(s.as_bytes())?;
|
||||
if let Some(y) = date.year().or_else(|| date.year_last_two().map(|v| v as i32)) {
|
||||
let offset = match y {
|
||||
0..=68 => 2000,
|
||||
69..=99 => 1900,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
date.set_year(y + offset);
|
||||
}
|
||||
|
||||
Ok(PrimitiveDateTime::try_from(date)?.assume_utc())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ::{Cookie, SameSite};
|
||||
use ::time::{strptime, Duration};
|
||||
use super::parse_date;
|
||||
use crate::{Cookie, SameSite};
|
||||
use time::Duration;
|
||||
|
||||
macro_rules! assert_eq_parse {
|
||||
($string:expr, $expected:expr) => (
|
||||
|
@ -271,6 +323,15 @@ mod tests {
|
|||
assert_eq_parse!("foo=bar; SameSite=strict", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=STrICT", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=STRICT", expected);
|
||||
|
||||
let expected = Cookie::build("foo", "bar")
|
||||
.same_site(SameSite::None)
|
||||
.finish();
|
||||
|
||||
assert_eq_parse!("foo=bar; SameSite=None", expected);
|
||||
assert_eq_parse!("foo=bar; SameSITE=none", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=NOne", expected);
|
||||
assert_eq_parse!("foo=bar; SameSite=nOne", expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -283,10 +344,28 @@ mod tests {
|
|||
let expected = Cookie::build("foo", "bar=baz").finish();
|
||||
assert_eq_parse!("foo=bar=baz", expected);
|
||||
|
||||
let expected = Cookie::build("foo", "\"bar\"").finish();
|
||||
assert_eq_parse!("foo=\"\"bar\"\"", expected);
|
||||
|
||||
let expected = Cookie::build("foo", "\"bar").finish();
|
||||
assert_eq_parse!("foo= \"bar", expected);
|
||||
assert_eq_parse!("foo=\"bar ", expected);
|
||||
assert_eq_parse!("foo=\"\"bar\"", expected);
|
||||
assert_eq_parse!("foo=\"\"bar \"", expected);
|
||||
assert_eq_parse!("foo=\"\"bar \" ", expected);
|
||||
|
||||
let expected = Cookie::build("foo", "bar\"").finish();
|
||||
assert_eq_parse!("foo=bar\"", expected);
|
||||
assert_eq_parse!("foo=\"bar\"\"", expected);
|
||||
assert_eq_parse!("foo=\" bar\"\"", expected);
|
||||
assert_eq_parse!("foo=\" bar\" \" ", expected);
|
||||
|
||||
let mut expected = Cookie::build("foo", "bar").finish();
|
||||
assert_eq_parse!("foo=bar", expected);
|
||||
assert_eq_parse!("foo = bar", expected);
|
||||
assert_eq_parse!("foo=\"bar\"", expected);
|
||||
assert_eq_parse!(" foo=bar ", expected);
|
||||
assert_eq_parse!(" foo=\"bar \" ", expected);
|
||||
assert_eq_parse!(" foo=bar ;Domain=", expected);
|
||||
assert_eq_parse!(" foo=bar ;Domain= ", expected);
|
||||
assert_eq_parse!(" foo=bar ;Ignored", expected);
|
||||
|
@ -316,7 +395,7 @@ mod tests {
|
|||
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; secure", unexpected);
|
||||
|
||||
expected.set_max_age(Duration::zero());
|
||||
expected.set_max_age(Duration::ZERO);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=0", expected);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age = 0 ", expected);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=-1", expected);
|
||||
|
@ -371,18 +450,76 @@ mod tests {
|
|||
Domain=FOO.COM", unexpected);
|
||||
|
||||
let time_str = "Wed, 21 Oct 2015 07:28:00 GMT";
|
||||
let expires = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap();
|
||||
let expires = parse_date(time_str, &super::FMT1).unwrap();
|
||||
expected.set_expires(expires);
|
||||
assert_eq_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", expected);
|
||||
|
||||
unexpected.set_domain("foo.com");
|
||||
let bad_expires = strptime(time_str, "%a, %d %b %Y %H:%S:%M %Z").unwrap();
|
||||
let bad_expires = parse_date(time_str, &super::FMT1).unwrap();
|
||||
expected.set_expires(bad_expires);
|
||||
assert_ne_parse!(" foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \
|
||||
Domain=foo.com; Expires=Wed, 21 Oct 2015 07:28:00 GMT", unexpected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_abbreviated_years() {
|
||||
let cookie_str = "foo=bar; expires=Thu, 10-Sep-20 20:00:00 GMT";
|
||||
let cookie = Cookie::parse(cookie_str).unwrap();
|
||||
assert_eq!(cookie.expires_datetime().unwrap().year(), 2020);
|
||||
|
||||
let cookie_str = "foo=bar; expires=Thu, 10-Sep-68 20:00:00 GMT";
|
||||
let cookie = Cookie::parse(cookie_str).unwrap();
|
||||
assert_eq!(cookie.expires_datetime().unwrap().year(), 2068);
|
||||
|
||||
let cookie_str = "foo=bar; expires=Thu, 10-Sep-69 20:00:00 GMT";
|
||||
let cookie = Cookie::parse(cookie_str).unwrap();
|
||||
assert_eq!(cookie.expires_datetime().unwrap().year(), 1969);
|
||||
|
||||
let cookie_str = "foo=bar; expires=Thu, 10-Sep-99 20:00:00 GMT";
|
||||
let cookie = Cookie::parse(cookie_str).unwrap();
|
||||
assert_eq!(cookie.expires_datetime().unwrap().year(), 1999);
|
||||
|
||||
let cookie_str = "foo=bar; expires=Thu, 10-Sep-2069 20:00:00 GMT";
|
||||
let cookie = Cookie::parse(cookie_str).unwrap();
|
||||
assert_eq!(cookie.expires_datetime().unwrap().year(), 2069);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_variant_date_fmts() {
|
||||
let cookie_str = "foo=bar; expires=Sun, 06 Nov 1994 08:49:37 GMT";
|
||||
Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap();
|
||||
|
||||
let cookie_str = "foo=bar; expires=Sunday, 06-Nov-94 08:49:37 GMT";
|
||||
Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap();
|
||||
|
||||
let cookie_str = "foo=bar; expires=Sun Nov 6 08:49:37 1994";
|
||||
Cookie::parse(cookie_str).unwrap().expires_datetime().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_very_large_max_ages() {
|
||||
let mut expected = Cookie::build("foo", "bar")
|
||||
.max_age(Duration::seconds(i64::max_value()))
|
||||
.finish();
|
||||
|
||||
let string = format!("foo=bar; Max-Age={}", 1u128 << 100);
|
||||
assert_eq_parse!(&string, expected);
|
||||
|
||||
expected.set_max_age(Duration::seconds(0));
|
||||
assert_eq_parse!("foo=bar; Max-Age=-129", expected);
|
||||
|
||||
let string = format!("foo=bar; Max-Age=-{}", 1u128 << 100);
|
||||
assert_eq_parse!(&string, expected);
|
||||
|
||||
let string = format!("foo=bar; Max-Age=-{}", i64::max_value());
|
||||
assert_eq_parse!(&string, expected);
|
||||
|
||||
let string = format!("foo=bar; Max-Age={}", i64::max_value());
|
||||
expected.set_max_age(Duration::seconds(i64::max_value()));
|
||||
assert_eq_parse!(&string, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_characters() {
|
||||
let expected = Cookie::new("foo", "b%2Fr");
|
||||
|
@ -403,10 +540,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn do_not_panic_on_large_max_ages() {
|
||||
let max_seconds = Duration::max_value().num_seconds();
|
||||
let max_seconds = Duration::MAX.whole_seconds();
|
||||
let expected = Cookie::build("foo", "bar")
|
||||
.max_age(Duration::seconds(max_seconds))
|
||||
.finish();
|
||||
assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", max_seconds + 1), expected);
|
||||
let too_many_seconds = (max_seconds as u64) + 1;
|
||||
assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", too_many_seconds), expected);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,68 @@
|
|||
use secure::ring::hkdf::expand;
|
||||
use secure::ring::digest::{SHA256, Algorithm};
|
||||
use secure::ring::hmac::SigningKey;
|
||||
use secure::ring::rand::{SecureRandom, SystemRandom};
|
||||
const SIGNING_KEY_LEN: usize = 32;
|
||||
const ENCRYPTION_KEY_LEN: usize = 32;
|
||||
const COMBINED_KEY_LENGTH: usize = SIGNING_KEY_LEN + ENCRYPTION_KEY_LEN;
|
||||
|
||||
use secure::private::KEY_LEN as PRIVATE_KEY_LEN;
|
||||
use secure::signed::KEY_LEN as SIGNED_KEY_LEN;
|
||||
|
||||
static HKDF_DIGEST: &'static Algorithm = &SHA256;
|
||||
const KEYS_INFO: &'static str = "COOKIE;SIGNED:HMAC-SHA256;PRIVATE:AEAD-AES-256-GCM";
|
||||
// Statically ensure the numbers above are in-sync.
|
||||
#[cfg(feature = "signed")]
|
||||
const_assert!(crate::secure::signed::KEY_LEN == SIGNING_KEY_LEN);
|
||||
#[cfg(feature = "private")]
|
||||
const_assert!(crate::secure::private::KEY_LEN == ENCRYPTION_KEY_LEN);
|
||||
|
||||
/// A cryptographic master key for use with `Signed` and/or `Private` jars.
|
||||
///
|
||||
/// This structure encapsulates secure, cryptographic keys for use with both
|
||||
/// [PrivateJar](struct.PrivateJar.html) and [SignedJar](struct.SignedJar.html).
|
||||
/// It can be derived from a single master key via
|
||||
/// [from_master](#method.from_master) or generated from a secure random source
|
||||
/// via [generate](#method.generate). A single instance of `Key` can be used for
|
||||
/// both a `PrivateJar` and a `SignedJar`.
|
||||
///
|
||||
/// This type is only available when the `secure` feature is enabled.
|
||||
/// [`PrivateJar`](crate::PrivateJar) and [`SignedJar`](crate::SignedJar). A
|
||||
/// single instance of a `Key` can be used for both a `PrivateJar` and a
|
||||
/// `SignedJar` simultaneously with no notable security implications.
|
||||
#[cfg_attr(all(nightly, doc), doc(cfg(any(feature = "private", feature = "signed"))))]
|
||||
#[derive(Clone)]
|
||||
pub struct Key {
|
||||
signing_key: [u8; SIGNED_KEY_LEN],
|
||||
encryption_key: [u8; PRIVATE_KEY_LEN]
|
||||
pub struct Key([u8; COMBINED_KEY_LENGTH /* SIGNING | ENCRYPTION */]);
|
||||
|
||||
impl PartialEq for Key {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
use subtle::ConstantTimeEq;
|
||||
|
||||
self.0.ct_eq(&other.0).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Key {
|
||||
// An empty key structure, to be filled.
|
||||
const fn zero() -> Self {
|
||||
Key([0; COMBINED_KEY_LENGTH])
|
||||
}
|
||||
|
||||
/// Creates a new `Key` from a 512-bit cryptographically random string.
|
||||
///
|
||||
/// The supplied key must be at least 512-bits (64 bytes). For security, the
|
||||
/// master key _must_ be cryptographically random.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `key` is less than 64 bytes in length.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::Key;
|
||||
///
|
||||
/// # /*
|
||||
/// let key = { /* a cryptographically random key >= 64 bytes */ };
|
||||
/// # */
|
||||
/// # let key: &Vec<u8> = &(0..64).collect();
|
||||
///
|
||||
/// let key = Key::from(key);
|
||||
/// ```
|
||||
pub fn from(key: &[u8]) -> Key {
|
||||
if key.len() < 64 {
|
||||
panic!("bad key length: expected >= 64 bytes, found {}", key.len());
|
||||
}
|
||||
|
||||
let mut output = Key::zero();
|
||||
output.0.copy_from_slice(&key[..COMBINED_KEY_LENGTH]);
|
||||
output
|
||||
}
|
||||
|
||||
/// Derives new signing/encryption keys from a master key.
|
||||
///
|
||||
/// The master key must be at least 256-bits (32 bytes). For security, the
|
||||
|
@ -46,28 +83,21 @@ impl Key {
|
|||
/// # */
|
||||
/// # let master_key: &Vec<u8> = &(0..32).collect();
|
||||
///
|
||||
/// let key = Key::from_master(master_key);
|
||||
/// let key = Key::derive_from(master_key);
|
||||
/// ```
|
||||
pub fn from_master(key: &[u8]) -> Key {
|
||||
if key.len() < 32 {
|
||||
panic!("bad master key length: expected at least 32 bytes, found {}", key.len());
|
||||
#[cfg(feature = "key-expansion")]
|
||||
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "key-expansion")))]
|
||||
pub fn derive_from(master_key: &[u8]) -> Self {
|
||||
if master_key.len() < 32 {
|
||||
panic!("bad master key length: expected >= 32 bytes, found {}", master_key.len());
|
||||
}
|
||||
|
||||
// Expand the user's key into two.
|
||||
let prk = SigningKey::new(HKDF_DIGEST, key);
|
||||
let mut both_keys = [0; SIGNED_KEY_LEN + PRIVATE_KEY_LEN];
|
||||
expand(&prk, KEYS_INFO.as_bytes(), &mut both_keys);
|
||||
|
||||
// Copy the keys into their respective arrays.
|
||||
let mut signing_key = [0; SIGNED_KEY_LEN];
|
||||
let mut encryption_key = [0; PRIVATE_KEY_LEN];
|
||||
signing_key.copy_from_slice(&both_keys[..SIGNED_KEY_LEN]);
|
||||
encryption_key.copy_from_slice(&both_keys[SIGNED_KEY_LEN..]);
|
||||
|
||||
Key {
|
||||
signing_key: signing_key,
|
||||
encryption_key: encryption_key
|
||||
}
|
||||
// Expand the master key into two HKDF generated keys.
|
||||
const KEYS_INFO: &[u8] = b"COOKIE;SIGNED:HMAC-SHA256;PRIVATE:AEAD-AES-256-GCM";
|
||||
let mut both_keys = [0; COMBINED_KEY_LENGTH];
|
||||
let hk = hkdf::Hkdf::<sha2::Sha256>::from_prk(master_key).expect("key length prechecked");
|
||||
hk.expand(KEYS_INFO, &mut both_keys).expect("expand into keys");
|
||||
Key::from(&both_keys)
|
||||
}
|
||||
|
||||
/// Generates signing/encryption keys from a secure, random source. Keys are
|
||||
|
@ -76,7 +106,7 @@ impl Key {
|
|||
/// # Panics
|
||||
///
|
||||
/// Panics if randomness cannot be retrieved from the operating system. See
|
||||
/// [try_generate](#method.try_generate) for a non-panicking version.
|
||||
/// [`Key::try_generate()`] for a non-panicking version.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
@ -101,18 +131,16 @@ impl Key {
|
|||
/// let key = Key::try_generate();
|
||||
/// ```
|
||||
pub fn try_generate() -> Option<Key> {
|
||||
let mut sign_key = [0; SIGNED_KEY_LEN];
|
||||
let mut enc_key = [0; PRIVATE_KEY_LEN];
|
||||
use crate::secure::rand::RngCore;
|
||||
|
||||
let rng = SystemRandom::new();
|
||||
if rng.fill(&mut sign_key).is_err() || rng.fill(&mut enc_key).is_err() {
|
||||
return None
|
||||
}
|
||||
|
||||
Some(Key { signing_key: sign_key, encryption_key: enc_key })
|
||||
let mut rng = crate::secure::rand::thread_rng();
|
||||
let mut key = Key::zero();
|
||||
rng.try_fill_bytes(&mut key.0).ok()?;
|
||||
Some(key)
|
||||
}
|
||||
|
||||
/// Returns the raw bytes of a key suitable for signing cookies.
|
||||
/// Returns the raw bytes of a key suitable for signing cookies. Guaranteed
|
||||
/// to be at least 32 bytes.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
@ -123,10 +151,11 @@ impl Key {
|
|||
/// let signing_key = key.signing();
|
||||
/// ```
|
||||
pub fn signing(&self) -> &[u8] {
|
||||
&self.signing_key[..]
|
||||
&self.0[..SIGNING_KEY_LEN]
|
||||
}
|
||||
|
||||
/// Returns the raw bytes of a key suitable for encrypting cookies.
|
||||
/// Guaranteed to be at least 32 bytes.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
@ -137,7 +166,22 @@ impl Key {
|
|||
/// let encryption_key = key.encryption();
|
||||
/// ```
|
||||
pub fn encryption(&self) -> &[u8] {
|
||||
&self.encryption_key[..]
|
||||
&self.0[SIGNING_KEY_LEN..]
|
||||
}
|
||||
|
||||
/// Returns the raw bytes of the master key. Guaranteed to be at least 64
|
||||
/// bytes.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::Key;
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let master_key = key.master();
|
||||
/// ```
|
||||
pub fn master(&self) -> &[u8] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,18 +190,30 @@ mod test {
|
|||
use super::Key;
|
||||
|
||||
#[test]
|
||||
fn deterministic_from_master() {
|
||||
fn from_works() {
|
||||
let key = Key::from(&(0..64).collect::<Vec<_>>());
|
||||
|
||||
let signing: Vec<u8> = (0..32).collect();
|
||||
assert_eq!(key.signing(), &*signing);
|
||||
|
||||
let encryption: Vec<u8> = (32..64).collect();
|
||||
assert_eq!(key.encryption(), &*encryption);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "key-expansion")]
|
||||
fn deterministic_derive() {
|
||||
let master_key: Vec<u8> = (0..32).collect();
|
||||
|
||||
let key_a = Key::from_master(&master_key);
|
||||
let key_b = Key::from_master(&master_key);
|
||||
let key_a = Key::derive_from(&master_key);
|
||||
let key_b = Key::derive_from(&master_key);
|
||||
|
||||
assert_eq!(key_a.signing(), key_b.signing());
|
||||
assert_eq!(key_a.encryption(), key_b.encryption());
|
||||
assert_ne!(key_a.encryption(), key_a.signing());
|
||||
|
||||
let master_key_2: Vec<u8> = (32..64).collect();
|
||||
let key_2 = Key::from_master(&master_key_2);
|
||||
let key_2 = Key::derive_from(&master_key_2);
|
||||
|
||||
assert_ne!(key_2.signing(), key_a.signing());
|
||||
assert_ne!(key_2.encryption(), key_a.encryption());
|
||||
|
|
|
@ -39,3 +39,11 @@ macro_rules! assert_secure_behaviour {
|
|||
})
|
||||
}
|
||||
|
||||
// This is courtesty of `static_assertions`. That library is Copyright (c) 2017
|
||||
// Nikolai Vazquez. See https://github.com/nvzqz/static-assertions-rs for more.
|
||||
macro_rules! const_assert {
|
||||
($x:expr $(,)?) => {
|
||||
#[allow(unknown_lints, clippy::eq_op)]
|
||||
const _: [(); 0 - !{ const ASSERT: bool = $x; ASSERT } as usize] = [];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
extern crate ring;
|
||||
extern crate rand;
|
||||
extern crate base64;
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod private;
|
||||
mod signed;
|
||||
mod key;
|
||||
|
||||
pub use self::private::*;
|
||||
pub use self::signed::*;
|
||||
pub use self::key::*;
|
||||
|
||||
#[cfg(feature = "private")] mod private;
|
||||
#[cfg(feature = "private")] pub use self::private::*;
|
||||
|
||||
#[cfg(feature = "signed")] mod signed;
|
||||
#[cfg(feature = "signed")] pub use self::signed::*;
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
use secure::ring::aead::{seal_in_place, open_in_place, Aad, Algorithm, Nonce, AES_256_GCM};
|
||||
use secure::ring::aead::{OpeningKey, SealingKey};
|
||||
use secure::ring::rand::{SecureRandom, SystemRandom};
|
||||
use secure::{base64, Key};
|
||||
extern crate aes_gcm;
|
||||
|
||||
use {Cookie, CookieJar};
|
||||
use std::convert::TryInto;
|
||||
use std::borrow::{Borrow, BorrowMut};
|
||||
|
||||
use crate::secure::{base64, rand, Key};
|
||||
use crate::{Cookie, CookieJar};
|
||||
|
||||
use self::aes_gcm::Aes256Gcm;
|
||||
use self::aes_gcm::aead::{Aead, AeadInPlace, NewAead, generic_array::GenericArray, Payload};
|
||||
use self::rand::RngCore;
|
||||
|
||||
// Keep these in sync, and keep the key len synced with the `private` docs as
|
||||
// well as the `KEYS_INFO` const in secure::Key.
|
||||
static ALGO: &'static Algorithm = &AES_256_GCM;
|
||||
const NONCE_LEN: usize = 12;
|
||||
pub const KEY_LEN: usize = 32;
|
||||
pub(crate) const NONCE_LEN: usize = 12;
|
||||
pub(crate) const TAG_LEN: usize = 16;
|
||||
pub(crate) const KEY_LEN: usize = 32;
|
||||
|
||||
/// A child cookie jar that provides authenticated encryption for its cookies.
|
||||
///
|
||||
|
@ -18,22 +23,49 @@ pub const KEY_LEN: usize = 32;
|
|||
/// `PrivateJar` are simultaneously assured confidentiality, integrity, and
|
||||
/// authenticity. In other words, clients cannot discover nor tamper with the
|
||||
/// contents of a cookie, nor can they fabricate cookie data.
|
||||
///
|
||||
/// This type is only available when the `secure` feature is enabled.
|
||||
pub struct PrivateJar<'a> {
|
||||
parent: &'a mut CookieJar,
|
||||
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "private")))]
|
||||
pub struct PrivateJar<J> {
|
||||
parent: J,
|
||||
key: [u8; KEY_LEN]
|
||||
}
|
||||
|
||||
impl<'a> PrivateJar<'a> {
|
||||
impl<J> PrivateJar<J> {
|
||||
/// Creates a new child `PrivateJar` with parent `parent` and key `key`.
|
||||
/// This method is typically called indirectly via the `signed` method of
|
||||
/// `CookieJar`.
|
||||
#[doc(hidden)]
|
||||
pub fn new(parent: &'a mut CookieJar, key: &Key) -> PrivateJar<'a> {
|
||||
let mut key_array = [0u8; KEY_LEN];
|
||||
key_array.copy_from_slice(key.encryption());
|
||||
PrivateJar { parent: parent, key: key_array }
|
||||
pub(crate) fn new(parent: J, key: &Key) -> PrivateJar<J> {
|
||||
PrivateJar { parent, key: key.encryption().try_into().expect("enc key len") }
|
||||
}
|
||||
|
||||
/// Encrypts the cookie's value with authenticated encryption providing
|
||||
/// confidentiality, integrity, and authenticity.
|
||||
fn encrypt_cookie(&self, cookie: &mut Cookie) {
|
||||
// Create a vec to hold the [nonce | cookie value | tag].
|
||||
let cookie_val = cookie.value().as_bytes();
|
||||
let mut data = vec![0; NONCE_LEN + cookie_val.len() + TAG_LEN];
|
||||
|
||||
// Split data into three: nonce, input/output, tag. Copy input.
|
||||
let (nonce, in_out) = data.split_at_mut(NONCE_LEN);
|
||||
let (in_out, tag) = in_out.split_at_mut(cookie_val.len());
|
||||
in_out.copy_from_slice(cookie_val);
|
||||
|
||||
// Fill nonce piece with random data.
|
||||
let mut rng = self::rand::thread_rng();
|
||||
rng.try_fill_bytes(nonce).expect("couldn't random fill nonce");
|
||||
let nonce = GenericArray::clone_from_slice(nonce);
|
||||
|
||||
// Perform the actual sealing operation, using the cookie's name as
|
||||
// associated data to prevent value swapping.
|
||||
let aad = cookie.name().as_bytes();
|
||||
let aead = Aes256Gcm::new(GenericArray::from_slice(&self.key));
|
||||
let aad_tag = aead.encrypt_in_place_detached(&nonce, aad, in_out)
|
||||
.expect("encryption failure!");
|
||||
|
||||
// Copy the tag into the tag piece.
|
||||
tag.copy_from_slice(&aad_tag);
|
||||
|
||||
// Base64 encode [nonce | encrypted value | tag].
|
||||
cookie.set_value(base64::encode(&data));
|
||||
}
|
||||
|
||||
/// Given a sealed value `str` and a key name `name`, where the nonce is
|
||||
|
@ -41,24 +73,56 @@ impl<'a> PrivateJar<'a> {
|
|||
/// verifies and decrypts the sealed value and returns it. If there's a
|
||||
/// problem, returns an `Err` with a string describing the issue.
|
||||
fn unseal(&self, name: &str, value: &str) -> Result<String, &'static str> {
|
||||
let mut data = base64::decode(value).map_err(|_| "bad base64 value")?;
|
||||
let data = base64::decode(value).map_err(|_| "bad base64 value")?;
|
||||
if data.len() <= NONCE_LEN {
|
||||
return Err("length of decoded data is <= NONCE_LEN");
|
||||
}
|
||||
|
||||
let ad = Aad::from(name.as_bytes());
|
||||
let key = OpeningKey::new(ALGO, &self.key).expect("opening key");
|
||||
let (nonce, sealed) = data.split_at_mut(NONCE_LEN);
|
||||
let nonce = Nonce::try_assume_unique_for_key(nonce)
|
||||
.expect("invalid length of `nonce`");
|
||||
let unsealed = open_in_place(&key, nonce, ad, 0, sealed)
|
||||
.map_err(|_| "invalid key/nonce/value: bad seal")?;
|
||||
let (nonce, cipher) = data.split_at(NONCE_LEN);
|
||||
let payload = Payload { msg: cipher, aad: name.as_bytes() };
|
||||
|
||||
::std::str::from_utf8(unsealed)
|
||||
.map(|s| s.to_string())
|
||||
.map_err(|_| "bad unsealed utf8")
|
||||
let aead = Aes256Gcm::new(GenericArray::from_slice(&self.key));
|
||||
aead.decrypt(GenericArray::from_slice(nonce), payload)
|
||||
.map_err(|_| "invalid key/nonce/value: bad seal")
|
||||
.and_then(|s| String::from_utf8(s).map_err(|_| "bad unsealed utf8"))
|
||||
}
|
||||
|
||||
/// Authenticates and decrypts `cookie`, returning the plaintext version if
|
||||
/// decryption succeeds or `None` otherwise. Authenticatation and decryption
|
||||
/// _always_ succeeds if `cookie` was generated by a `PrivateJar` with the
|
||||
/// same key as `self`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// assert!(jar.private(&key).get("name").is_none());
|
||||
///
|
||||
/// jar.private_mut(&key).add(Cookie::new("name", "value"));
|
||||
/// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value");
|
||||
///
|
||||
/// let plain = jar.get("name").cloned().unwrap();
|
||||
/// assert_ne!(plain.value(), "value");
|
||||
/// let decrypted = jar.private(&key).decrypt(plain).unwrap();
|
||||
/// assert_eq!(decrypted.value(), "value");
|
||||
///
|
||||
/// let plain = Cookie::new("plaintext", "hello");
|
||||
/// assert!(jar.private(&key).decrypt(plain).is_none());
|
||||
/// ```
|
||||
pub fn decrypt(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> {
|
||||
if let Ok(value) = self.unseal(cookie.name(), cookie.value()) {
|
||||
cookie.set_value(value);
|
||||
return Some(cookie);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<J: Borrow<CookieJar>> PrivateJar<J> {
|
||||
/// Returns a reference to the `Cookie` inside this jar with the name `name`
|
||||
/// and authenticates and decrypts the cookie's value, returning a `Cookie`
|
||||
/// with the decrypted value. If the cookie cannot be found, or the cookie
|
||||
|
@ -70,25 +134,20 @@ impl<'a> PrivateJar<'a> {
|
|||
/// use cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// let mut private_jar = jar.private(&key);
|
||||
/// assert!(private_jar.get("name").is_none());
|
||||
/// let jar = CookieJar::new();
|
||||
/// assert!(jar.private(&key).get("name").is_none());
|
||||
///
|
||||
/// let mut jar = jar;
|
||||
/// let mut private_jar = jar.private_mut(&key);
|
||||
/// private_jar.add(Cookie::new("name", "value"));
|
||||
/// assert_eq!(private_jar.get("name").unwrap().value(), "value");
|
||||
/// ```
|
||||
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
|
||||
if let Some(cookie_ref) = self.parent.get(name) {
|
||||
let mut cookie = cookie_ref.clone();
|
||||
if let Ok(value) = self.unseal(name, cookie.value()) {
|
||||
cookie.set_value(value);
|
||||
return Some(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
self.parent.borrow().get(name).and_then(|c| self.decrypt(c.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<J: BorrowMut<CookieJar>> PrivateJar<J> {
|
||||
/// Adds `cookie` to the parent jar. The cookie's value is encrypted with
|
||||
/// authenticated encryption assuring confidentiality, integrity, and
|
||||
/// authenticity.
|
||||
|
@ -100,24 +159,22 @@ impl<'a> PrivateJar<'a> {
|
|||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.private(&key).add(Cookie::new("name", "value"));
|
||||
/// jar.private_mut(&key).add(Cookie::new("name", "value"));
|
||||
///
|
||||
/// assert_ne!(jar.get("name").unwrap().value(), "value");
|
||||
/// assert_eq!(jar.private(&key).get("name").unwrap().value(), "value");
|
||||
/// ```
|
||||
pub fn add(&mut self, mut cookie: Cookie<'static>) {
|
||||
self.encrypt_cookie(&mut cookie);
|
||||
|
||||
// Add the sealed cookie to the parent.
|
||||
self.parent.add(cookie);
|
||||
self.parent.borrow_mut().add(cookie);
|
||||
}
|
||||
|
||||
/// Adds an "original" `cookie` to parent jar. The cookie's value is
|
||||
/// encrypted with authenticated encryption assuring confidentiality,
|
||||
/// integrity, and authenticity. Adding an original cookie does not affect
|
||||
/// the [`CookieJar::delta()`](struct.CookieJar.html#method.delta)
|
||||
/// computation. This method is intended to be used to seed the cookie jar
|
||||
/// with cookies received from a client's HTTP message.
|
||||
/// the [`CookieJar::delta()`] computation. This method is intended to be
|
||||
/// used to seed the cookie jar with cookies received from a client's HTTP
|
||||
/// message.
|
||||
///
|
||||
/// For accurate `delta` computations, this method should not be called
|
||||
/// after calling `remove`.
|
||||
|
@ -129,48 +186,14 @@ impl<'a> PrivateJar<'a> {
|
|||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.private(&key).add_original(Cookie::new("name", "value"));
|
||||
/// jar.private_mut(&key).add_original(Cookie::new("name", "value"));
|
||||
///
|
||||
/// assert_eq!(jar.iter().count(), 1);
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
/// ```
|
||||
pub fn add_original(&mut self, mut cookie: Cookie<'static>) {
|
||||
self.encrypt_cookie(&mut cookie);
|
||||
|
||||
// Add the sealed cookie to the parent.
|
||||
self.parent.add_original(cookie);
|
||||
}
|
||||
|
||||
/// Encrypts the cookie's value with
|
||||
/// authenticated encryption assuring confidentiality, integrity, and authenticity.
|
||||
fn encrypt_cookie(&self, cookie: &mut Cookie) {
|
||||
let mut data;
|
||||
let output_len = {
|
||||
// Create the `SealingKey` structure.
|
||||
let key = SealingKey::new(ALGO, &self.key).expect("sealing key creation");
|
||||
|
||||
// Create a vec to hold the [nonce | cookie value | overhead].
|
||||
let overhead = ALGO.tag_len();
|
||||
let cookie_val = cookie.value().as_bytes();
|
||||
data = vec![0; NONCE_LEN + cookie_val.len() + overhead];
|
||||
|
||||
// Randomly generate the nonce, then copy the cookie value as input.
|
||||
let (nonce, in_out) = data.split_at_mut(NONCE_LEN);
|
||||
SystemRandom::new().fill(nonce).expect("couldn't random fill nonce");
|
||||
in_out[..cookie_val.len()].copy_from_slice(cookie_val);
|
||||
let nonce = Nonce::try_assume_unique_for_key(nonce)
|
||||
.expect("invalid length of `nonce`");
|
||||
|
||||
// Use cookie's name as associated data to prevent value swapping.
|
||||
let ad = Aad::from(cookie.name().as_bytes());
|
||||
|
||||
// Perform the actual sealing operation and get the output length.
|
||||
seal_in_place(&key, nonce, ad, in_out, overhead).expect("in-place seal")
|
||||
};
|
||||
|
||||
// Base64 encode the nonce and encrypted value.
|
||||
let sealed_value = base64::encode(&data[..(NONCE_LEN + output_len)]);
|
||||
cookie.set_value(sealed_value);
|
||||
self.parent.borrow_mut().add_original(cookie);
|
||||
}
|
||||
|
||||
/// Removes `cookie` from the parent jar.
|
||||
|
@ -178,8 +201,8 @@ impl<'a> PrivateJar<'a> {
|
|||
/// For correct removal, the passed in `cookie` must contain the same `path`
|
||||
/// and `domain` as the cookie that was initially set.
|
||||
///
|
||||
/// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more
|
||||
/// details.
|
||||
/// This is identical to [`CookieJar::remove()`]. See the method's
|
||||
/// documentation for more details.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
@ -188,7 +211,7 @@ impl<'a> PrivateJar<'a> {
|
|||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// let mut private_jar = jar.private(&key);
|
||||
/// let mut private_jar = jar.private_mut(&key);
|
||||
///
|
||||
/// private_jar.add(Cookie::new("name", "value"));
|
||||
/// assert!(private_jar.get("name").is_some());
|
||||
|
@ -197,25 +220,45 @@ impl<'a> PrivateJar<'a> {
|
|||
/// assert!(private_jar.get("name").is_none());
|
||||
/// ```
|
||||
pub fn remove(&mut self, cookie: Cookie<'static>) {
|
||||
self.parent.remove(cookie);
|
||||
self.parent.borrow_mut().remove(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {CookieJar, Cookie, Key};
|
||||
use crate::{CookieJar, Cookie, Key};
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let key = Key::generate();
|
||||
let mut jar = CookieJar::new();
|
||||
assert_simple_behaviour!(jar, jar.private(&key));
|
||||
assert_simple_behaviour!(jar, jar.private_mut(&key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private() {
|
||||
fn secure() {
|
||||
let key = Key::generate();
|
||||
let mut jar = CookieJar::new();
|
||||
assert_secure_behaviour!(jar, jar.private(&key));
|
||||
assert_secure_behaviour!(jar, jar.private_mut(&key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
// Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256.
|
||||
let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249,
|
||||
34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254,
|
||||
24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16,
|
||||
198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233,
|
||||
10, 180, 170, 187, 89, 252, 137, 110, 107]);
|
||||
|
||||
let mut jar = CookieJar::new();
|
||||
jar.add(Cookie::new("encrypted_with_ring014",
|
||||
"lObeZJorGVyeSWUA8khTO/8UCzFVBY9g0MGU6/J3NN1R5x11dn2JIA=="));
|
||||
jar.add(Cookie::new("encrypted_with_ring016",
|
||||
"SU1ujceILyMBg3fReqRmA9HUtAIoSPZceOM/CUpObROHEujXIjonkA=="));
|
||||
|
||||
let private = jar.private(&key);
|
||||
assert_eq!(private.get("encrypted_with_ring014").unwrap().value(), "Tamper-proof");
|
||||
assert_eq!(private.get("encrypted_with_ring016").unwrap().value(), "Tamper-proof");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,54 +1,106 @@
|
|||
use secure::ring::digest::{SHA256, Algorithm};
|
||||
use secure::ring::hmac::{SigningKey, sign, verify_with_own_key as verify};
|
||||
use secure::{base64, Key};
|
||||
use std::convert::TryInto;
|
||||
use std::borrow::{Borrow, BorrowMut};
|
||||
|
||||
use {Cookie, CookieJar};
|
||||
use sha2::Sha256;
|
||||
use hmac::{Hmac, Mac};
|
||||
|
||||
use crate::secure::{base64, Key};
|
||||
use crate::{Cookie, CookieJar};
|
||||
|
||||
// Keep these in sync, and keep the key len synced with the `signed` docs as
|
||||
// well as the `KEYS_INFO` const in secure::Key.
|
||||
static HMAC_DIGEST: &'static Algorithm = &SHA256;
|
||||
const BASE64_DIGEST_LEN: usize = 44;
|
||||
pub const KEY_LEN: usize = 32;
|
||||
pub(crate) const BASE64_DIGEST_LEN: usize = 44;
|
||||
pub(crate) const KEY_LEN: usize = 32;
|
||||
|
||||
/// A child cookie jar that authenticates its cookies.
|
||||
///
|
||||
/// A _signed_ child jar signs all the cookies added to it and verifies cookies
|
||||
/// retrieved from it. Any cookies stored in a `SignedJar` are assured integrity
|
||||
/// and authenticity. In other words, clients cannot tamper with the contents of
|
||||
/// a cookie nor can they fabricate cookie values, but the data is visible in
|
||||
/// plaintext.
|
||||
///
|
||||
/// This type is only available when the `secure` feature is enabled.
|
||||
pub struct SignedJar<'a> {
|
||||
parent: &'a mut CookieJar,
|
||||
key: SigningKey
|
||||
/// retrieved from it. Any cookies stored in a `SignedJar` are provided
|
||||
/// integrity and authenticity. In other words, clients cannot tamper with the
|
||||
/// contents of a cookie nor can they fabricate cookie values, but the data is
|
||||
/// visible in plaintext.
|
||||
#[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))]
|
||||
pub struct SignedJar<J> {
|
||||
parent: J,
|
||||
key: [u8; KEY_LEN],
|
||||
}
|
||||
|
||||
impl<'a> SignedJar<'a> {
|
||||
impl<J> SignedJar<J> {
|
||||
/// Creates a new child `SignedJar` with parent `parent` and key `key`. This
|
||||
/// method is typically called indirectly via the `signed` method of
|
||||
/// method is typically called indirectly via the `signed{_mut}` methods of
|
||||
/// `CookieJar`.
|
||||
#[doc(hidden)]
|
||||
pub fn new(parent: &'a mut CookieJar, key: &Key) -> SignedJar<'a> {
|
||||
SignedJar { parent: parent, key: SigningKey::new(HMAC_DIGEST, key.signing()) }
|
||||
pub(crate) fn new(parent: J, key: &Key) -> SignedJar<J> {
|
||||
SignedJar { parent, key: key.signing().try_into().expect("sign key len") }
|
||||
}
|
||||
|
||||
/// Signs the cookie's value providing integrity and authenticity.
|
||||
fn sign_cookie(&self, cookie: &mut Cookie) {
|
||||
// Compute HMAC-SHA256 of the cookie's value.
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("good key");
|
||||
mac.update(cookie.value().as_bytes());
|
||||
|
||||
// Cookie's new value is [MAC | original-value].
|
||||
let mut new_value = base64::encode(&mac.finalize().into_bytes());
|
||||
new_value.push_str(cookie.value());
|
||||
cookie.set_value(new_value);
|
||||
}
|
||||
|
||||
/// Given a signed value `str` where the signature is prepended to `value`,
|
||||
/// verifies the signed value and returns it. If there's a problem, returns
|
||||
/// an `Err` with a string describing the issue.
|
||||
fn verify(&self, cookie_value: &str) -> Result<String, &'static str> {
|
||||
if cookie_value.len() < BASE64_DIGEST_LEN {
|
||||
return Err("length of value is <= BASE64_DIGEST_LEN");
|
||||
fn _verify(&self, cookie_value: &str) -> Result<String, &'static str> {
|
||||
if !cookie_value.is_char_boundary(BASE64_DIGEST_LEN) {
|
||||
return Err("missing or invalid digest");
|
||||
}
|
||||
|
||||
// Split [MAC | original-value] into its two parts.
|
||||
let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
|
||||
let sig = base64::decode(digest_str).map_err(|_| "bad base64 digest")?;
|
||||
let digest = base64::decode(digest_str).map_err(|_| "bad base64 digest")?;
|
||||
|
||||
verify(&self.key, value.as_bytes(), &sig)
|
||||
// Perform the verification.
|
||||
let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("good key");
|
||||
mac.update(value.as_bytes());
|
||||
mac.verify_slice(&digest)
|
||||
.map(|_| value.to_string())
|
||||
.map_err(|_| "value did not verify")
|
||||
}
|
||||
|
||||
/// Verifies the authenticity and integrity of `cookie`, returning the
|
||||
/// plaintext version if verification succeeds or `None` otherwise.
|
||||
/// Verification _always_ succeeds if `cookie` was generated by a
|
||||
/// `SignedJar` with the same key as `self`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// assert!(jar.signed(&key).get("name").is_none());
|
||||
///
|
||||
/// jar.signed_mut(&key).add(Cookie::new("name", "value"));
|
||||
/// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value");
|
||||
///
|
||||
/// let plain = jar.get("name").cloned().unwrap();
|
||||
/// assert_ne!(plain.value(), "value");
|
||||
/// let verified = jar.signed(&key).verify(plain).unwrap();
|
||||
/// assert_eq!(verified.value(), "value");
|
||||
///
|
||||
/// let plain = Cookie::new("plaintext", "hello");
|
||||
/// assert!(jar.signed(&key).verify(plain).is_none());
|
||||
/// ```
|
||||
pub fn verify(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> {
|
||||
if let Ok(value) = self._verify(cookie.value()) {
|
||||
cookie.set_value(value);
|
||||
return Some(cookie);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<J: Borrow<CookieJar>> SignedJar<J> {
|
||||
/// Returns a reference to the `Cookie` inside this jar with the name `name`
|
||||
/// and verifies the authenticity and integrity of the cookie's value,
|
||||
/// returning a `Cookie` with the authenticated value. If the cookie cannot
|
||||
|
@ -60,25 +112,20 @@ impl<'a> SignedJar<'a> {
|
|||
/// use cookie::{CookieJar, Cookie, Key};
|
||||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// let mut signed_jar = jar.signed(&key);
|
||||
/// assert!(signed_jar.get("name").is_none());
|
||||
/// let jar = CookieJar::new();
|
||||
/// assert!(jar.signed(&key).get("name").is_none());
|
||||
///
|
||||
/// let mut jar = jar;
|
||||
/// let mut signed_jar = jar.signed_mut(&key);
|
||||
/// signed_jar.add(Cookie::new("name", "value"));
|
||||
/// assert_eq!(signed_jar.get("name").unwrap().value(), "value");
|
||||
/// ```
|
||||
pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
|
||||
if let Some(cookie_ref) = self.parent.get(name) {
|
||||
let mut cookie = cookie_ref.clone();
|
||||
if let Ok(value) = self.verify(cookie.value()) {
|
||||
cookie.set_value(value);
|
||||
return Some(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
self.parent.borrow().get(name).and_then(|c| self.verify(c.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<J: BorrowMut<CookieJar>> SignedJar<J> {
|
||||
/// Adds `cookie` to the parent jar. The cookie's value is signed assuring
|
||||
/// integrity and authenticity.
|
||||
///
|
||||
|
@ -89,7 +136,7 @@ impl<'a> SignedJar<'a> {
|
|||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.signed(&key).add(Cookie::new("name", "value"));
|
||||
/// jar.signed_mut(&key).add(Cookie::new("name", "value"));
|
||||
///
|
||||
/// assert_ne!(jar.get("name").unwrap().value(), "value");
|
||||
/// assert!(jar.get("name").unwrap().value().contains("value"));
|
||||
|
@ -97,14 +144,14 @@ impl<'a> SignedJar<'a> {
|
|||
/// ```
|
||||
pub fn add(&mut self, mut cookie: Cookie<'static>) {
|
||||
self.sign_cookie(&mut cookie);
|
||||
self.parent.add(cookie);
|
||||
self.parent.borrow_mut().add(cookie);
|
||||
}
|
||||
|
||||
/// Adds an "original" `cookie` to this jar. The cookie's value is signed
|
||||
/// assuring integrity and authenticity. Adding an original cookie does not
|
||||
/// affect the [`CookieJar::delta()`](struct.CookieJar.html#method.delta)
|
||||
/// computation. This method is intended to be used to seed the cookie jar
|
||||
/// with cookies received from a client's HTTP message.
|
||||
/// affect the [`CookieJar::delta()`] computation. This method is intended
|
||||
/// to be used to seed the cookie jar with cookies received from a client's
|
||||
/// HTTP message.
|
||||
///
|
||||
/// For accurate `delta` computations, this method should not be called
|
||||
/// after calling `remove`.
|
||||
|
@ -116,22 +163,14 @@ impl<'a> SignedJar<'a> {
|
|||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// jar.signed(&key).add_original(Cookie::new("name", "value"));
|
||||
/// jar.signed_mut(&key).add_original(Cookie::new("name", "value"));
|
||||
///
|
||||
/// assert_eq!(jar.iter().count(), 1);
|
||||
/// assert_eq!(jar.delta().count(), 0);
|
||||
/// ```
|
||||
pub fn add_original(&mut self, mut cookie: Cookie<'static>) {
|
||||
self.sign_cookie(&mut cookie);
|
||||
self.parent.add_original(cookie);
|
||||
}
|
||||
|
||||
/// Signs the cookie's value assuring integrity and authenticity.
|
||||
fn sign_cookie(&self, cookie: &mut Cookie) {
|
||||
let digest = sign(&self.key, cookie.value().as_bytes());
|
||||
let mut new_value = base64::encode(digest.as_ref());
|
||||
new_value.push_str(cookie.value());
|
||||
cookie.set_value(new_value);
|
||||
self.parent.borrow_mut().add_original(cookie);
|
||||
}
|
||||
|
||||
/// Removes `cookie` from the parent jar.
|
||||
|
@ -139,8 +178,8 @@ impl<'a> SignedJar<'a> {
|
|||
/// For correct removal, the passed in `cookie` must contain the same `path`
|
||||
/// and `domain` as the cookie that was initially set.
|
||||
///
|
||||
/// See [CookieJar::remove](struct.CookieJar.html#method.remove) for more
|
||||
/// details.
|
||||
/// This is identical to [`CookieJar::remove()`]. See the method's
|
||||
/// documentation for more details.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
|
@ -149,7 +188,7 @@ impl<'a> SignedJar<'a> {
|
|||
///
|
||||
/// let key = Key::generate();
|
||||
/// let mut jar = CookieJar::new();
|
||||
/// let mut signed_jar = jar.signed(&key);
|
||||
/// let mut signed_jar = jar.signed_mut(&key);
|
||||
///
|
||||
/// signed_jar.add(Cookie::new("name", "value"));
|
||||
/// assert!(signed_jar.get("name").is_some());
|
||||
|
@ -158,25 +197,55 @@ impl<'a> SignedJar<'a> {
|
|||
/// assert!(signed_jar.get("name").is_none());
|
||||
/// ```
|
||||
pub fn remove(&mut self, cookie: Cookie<'static>) {
|
||||
self.parent.remove(cookie);
|
||||
self.parent.borrow_mut().remove(cookie);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {CookieJar, Cookie, Key};
|
||||
use crate::{CookieJar, Cookie, Key};
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let key = Key::generate();
|
||||
let mut jar = CookieJar::new();
|
||||
assert_simple_behaviour!(jar, jar.signed(&key));
|
||||
assert_simple_behaviour!(jar, jar.signed_mut(&key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn private() {
|
||||
let key = Key::generate();
|
||||
let mut jar = CookieJar::new();
|
||||
assert_secure_behaviour!(jar, jar.signed(&key));
|
||||
assert_secure_behaviour!(jar, jar.signed_mut(&key));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
// Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256.
|
||||
let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249,
|
||||
34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254,
|
||||
24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16,
|
||||
198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233,
|
||||
10, 180, 170, 187, 89, 252, 137, 110, 107]);
|
||||
|
||||
let mut jar = CookieJar::new();
|
||||
jar.add(Cookie::new("signed_with_ring014",
|
||||
"3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof"));
|
||||
jar.add(Cookie::new("signed_with_ring016",
|
||||
"3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof"));
|
||||
|
||||
let signed = jar.signed(&key);
|
||||
assert_eq!(signed.get("signed_with_ring014").unwrap().value(), "Tamper-proof");
|
||||
assert_eq!(signed.get("signed_with_ring016").unwrap().value(), "Tamper-proof");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_178() {
|
||||
let data = "x=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy£";
|
||||
let c = Cookie::parse(data).expect("failed to parse cookie");
|
||||
let key = Key::from(&[0u8; 64]);
|
||||
let mut jar = CookieJar::new();
|
||||
let signed = jar.signed_mut(&key);
|
||||
assert!(signed.verify(c).is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
{"files":{"Cargo.toml":"ed1dbfb8f9eb836549858fb0ca704eddaa480cbe576e8dd50ebca1529dca21b3","LICENSE-Apache":"c69b72788ec261765e460532016b4bb78372f238e449b5e70e3268ec1e18fd15","LICENSE-MIT":"b4bf94a9fceb8846320fda938ee53fc16b506572609da5cf1d2d289a5597a8f8","src/apple.rs":"018e729ea67e8e17428d2e8d93f28ed0f7e7c506fcf78b56f56113235ce7dfcf","src/freebsd.rs":"683636294a62d6b958a5de800b52ddfea609234921e0583906272aacc71e18e5","src/imp.rs":"8cc9d07f05b0aa70e9997648abce7f79bd758c6fc76040d1c8f7beb7bf551e9d","src/lib.rs":"c53382612069b9552846414d1508cbb1401c500a4f34e21addd336001ebd8b7e","src/linux.rs":"67e02ecd105b8a421227bf72814e8388a4f77df1d8a44b8902bc8926f7e6698c"},"package":"2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"}
|
|
@ -0,0 +1,36 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
name = "num_threads"
|
||||
version = "0.1.6"
|
||||
authors = ["Jacob Pratt <open-source@jhpratt.dev>"]
|
||||
include = [
|
||||
"src/**/*",
|
||||
"LICENSE-*",
|
||||
]
|
||||
description = "A minimal library that determines the number of running threads for the current process."
|
||||
categories = [
|
||||
"api-bindings",
|
||||
"hardware-support",
|
||||
"os",
|
||||
]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/jhpratt/num_threads"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
[target."cfg(any(target_os = \"macos\", target_os = \"ios\", target_os = \"freebsd\"))".dependencies.libc]
|
||||
version = "0.2.107"
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021 Jacob Pratt
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2021 Jacob Pratt
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,45 @@
|
|||
extern crate libc;
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use self::libc::{kern_return_t, mach_msg_type_number_t, mach_port_t, thread_t};
|
||||
|
||||
// This constant is from
|
||||
// /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/
|
||||
// usr/include/mach/machine/thread_state.h.
|
||||
//
|
||||
// It has not been updated since Apple devices started to support 64-bit ARM (iOS), so it
|
||||
// should be very stable.
|
||||
const THREAD_STATE_MAX: i32 = 1296;
|
||||
#[allow(non_camel_case_types)]
|
||||
// https://github.com/apple/darwin-xnu/blob/a1babec6b135d1f35b2590a1990af3c5c5393479/osfmk/mach/mach_types.defs#L155
|
||||
type task_inspect_t = mach_port_t;
|
||||
#[allow(non_camel_case_types)]
|
||||
// https://github.com/apple/darwin-xnu/blob/a1babec6b135d1f35b2590a1990af3c5c5393479/osfmk/mach/mach_types.defs#L238
|
||||
type thread_array_t = [thread_t; THREAD_STATE_MAX as usize];
|
||||
|
||||
extern "C" {
|
||||
// https://developer.apple.com/documentation/kernel/1537751-task_threads/
|
||||
fn task_threads(
|
||||
target_task: task_inspect_t,
|
||||
act_list: *mut thread_array_t,
|
||||
act_listCnt: *mut mach_msg_type_number_t,
|
||||
) -> kern_return_t;
|
||||
}
|
||||
|
||||
pub(crate) fn num_threads() -> Option<NonZeroUsize> {
|
||||
// http://web.mit.edu/darwin/src/modules/xnu/osfmk/man/task_threads.html
|
||||
let mut thread_state = [0u32; THREAD_STATE_MAX as usize];
|
||||
let mut thread_count = 0;
|
||||
|
||||
// Safety: `mach_task_self` always returns a valid value, `thread_state` is large enough, and
|
||||
// both it and `thread_count` are writable.
|
||||
let result =
|
||||
unsafe { task_threads(libc::mach_task_self(), &mut thread_state, &mut thread_count) };
|
||||
|
||||
if result == libc::KERN_SUCCESS {
|
||||
NonZeroUsize::new(thread_count as usize)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
extern crate libc;
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
use std::{mem, ptr};
|
||||
|
||||
pub(crate) fn num_threads() -> Option<NonZeroUsize> {
|
||||
// Safety: `sysctl` and `getpid` are both thread-safe.
|
||||
// `kip` is only accessed if sysctl() succeeds and agrees with the expected size,
|
||||
// and the data only trusted if both its embedded size and pid match expectations
|
||||
unsafe {
|
||||
let pid = libc::getpid();
|
||||
let mib: [libc::c_int; 4] = [libc::CTL_KERN, libc::KERN_PROC, libc::KERN_PROC_PID, pid];
|
||||
let mut kip: libc::kinfo_proc = mem::zeroed();
|
||||
let expected_kip_len = mem::size_of_val(&kip);
|
||||
let mut kip_len = expected_kip_len;
|
||||
|
||||
let ret = libc::sysctl(
|
||||
mib.as_ptr(),
|
||||
mib.len() as u32,
|
||||
&mut kip as *mut _ as *mut libc::c_void,
|
||||
&mut kip_len,
|
||||
ptr::null(),
|
||||
0,
|
||||
);
|
||||
|
||||
if ret == 0
|
||||
&& kip_len == expected_kip_len
|
||||
&& kip.ki_structsize == expected_kip_len as i32
|
||||
&& kip.ki_pid == pid
|
||||
{
|
||||
NonZeroUsize::new(kip.ki_numthreads as usize)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
//! Fallback if no OS matches.
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
pub(crate) fn num_threads() -> Option<NonZeroUsize> {
|
||||
None
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
//! Minimum supported Rust version: 1.28
|
||||
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
#[cfg_attr(any(target_os = "linux", target_os = "android"), path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "freebsd", path = "freebsd.rs")]
|
||||
#[cfg_attr(any(target_os = "macos", target_os = "ios"), path = "apple.rs")]
|
||||
mod imp;
|
||||
|
||||
/// Obtain the number of threads currently part of the active process. Returns `None` if the number
|
||||
/// of threads cannot be determined.
|
||||
pub fn num_threads() -> Option<NonZeroUsize> {
|
||||
imp::num_threads()
|
||||
}
|
||||
|
||||
/// Determine if the current process is single-threaded. Returns `None` if the number of threads
|
||||
/// cannot be determined.
|
||||
pub fn is_single_threaded() -> Option<bool> {
|
||||
num_threads().map(|n| n.get() == 1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
// Run each expression in its own thread.
|
||||
macro_rules! threaded {
|
||||
($first:expr;) => {
|
||||
$first;
|
||||
};
|
||||
($first:expr; $($rest:expr;)*) => {
|
||||
$first;
|
||||
::std::thread::spawn(|| {
|
||||
threaded!($($rest;)*);
|
||||
})
|
||||
.join()
|
||||
.unwrap();
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_threads() {
|
||||
threaded! {
|
||||
assert_eq!(super::num_threads().map(NonZeroUsize::get), Some(1));
|
||||
assert_eq!(super::num_threads().map(NonZeroUsize::get), Some(2));
|
||||
assert_eq!(super::num_threads().map(NonZeroUsize::get), Some(3));
|
||||
assert_eq!(super::num_threads().map(NonZeroUsize::get), Some(4));
|
||||
assert_eq!(super::num_threads().map(NonZeroUsize::get), Some(5));
|
||||
assert_eq!(super::num_threads().map(NonZeroUsize::get), Some(6));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_single_threaded() {
|
||||
threaded! {
|
||||
assert_eq!(super::is_single_threaded(), Some(true));
|
||||
assert_eq!(super::is_single_threaded(), Some(false));
|
||||
assert_eq!(super::is_single_threaded(), Some(false));
|
||||
assert_eq!(super::is_single_threaded(), Some(false));
|
||||
assert_eq!(super::is_single_threaded(), Some(false));
|
||||
assert_eq!(super::is_single_threaded(), Some(false));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
use std::fs;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
pub(crate) fn num_threads() -> Option<NonZeroUsize> {
|
||||
fs::read_to_string("/proc/self/stat")
|
||||
.ok()
|
||||
.as_ref()
|
||||
// Skip past the pid and (process name) fields
|
||||
.and_then(|stat| stat.rsplit(')').next())
|
||||
// 20th field, less the two we skipped
|
||||
.and_then(|rstat| rstat.split_whitespace().nth(17))
|
||||
.and_then(|num_threads| num_threads.parse::<usize>().ok())
|
||||
.and_then(NonZeroUsize::new)
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"files":{"Cargo.toml":"ddebccd9128093c3fdf84b4f6a131fe634c85e8fc2059afed9ea30e02fe22c58","LICENSE-APACHE":"a60eea817514531668d7e00765731449fe14d059d3249e0bc93b36de45f759f2","LICENSE-MIT":"6485b8ed310d3f0340bf1ad1f47645069ce4069dcc6bb46c7d5c6faf41de1fdb","README.md":"35b591c7481ec3ae59210fa4f9b7cecb1b16e632bfd6193b032239e74f9bfdb8","src/display.rs":"52d16abaa37b3ab577747c7d9d2ed6ded1b126458e980dc3e1a571fa6e1f9fda","src/duration.rs":"c706d392bdb7f65b23fcc20189a9a77c50b765b9c548e247238424ed6fb56a46","src/lib.rs":"720e380829cda276466bce34d6a29a2f7aec1f750e4b4522b217c57930380545","src/parse.rs":"65bd9142d8c15eb54a8d4db6e2c48bf1adbcc875953141c17e07ba58f356a027","src/sys.rs":"851994516ff29dd9f5749fa19b1db13b7afe484e4bd1d279b420fda7ed8404ab"},"package":"ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"}
|
|
@ -0,0 +1,38 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies
|
||||
#
|
||||
# If you believe there's an error in this file please file an
|
||||
# issue against the rust-lang/cargo repository. If you're
|
||||
# editing this file be aware that the upstream Cargo.toml
|
||||
# will likely look very different (and much more reasonable)
|
||||
|
||||
[package]
|
||||
name = "time"
|
||||
version = "0.1.43"
|
||||
authors = ["The Rust Project Developers"]
|
||||
exclude = [".github", "benches"]
|
||||
description = "Utilities for working with time-related functions in Rust.\n"
|
||||
homepage = "https://github.com/time-rs/time"
|
||||
documentation = "https://docs.rs/time/~0.1"
|
||||
readme = "README.md"
|
||||
license = "MIT/Apache-2.0"
|
||||
repository = "https://github.com/time-rs/time"
|
||||
[dependencies.libc]
|
||||
version = "0.2.69"
|
||||
|
||||
[dependencies.rustc-serialize]
|
||||
version = "0.3"
|
||||
optional = true
|
||||
[dev-dependencies.log]
|
||||
version = "0.4"
|
||||
|
||||
[dev-dependencies.winapi]
|
||||
version = "0.3.0"
|
||||
features = ["std", "processthreadsapi", "winbase"]
|
||||
[target."cfg(windows)".dependencies.winapi]
|
||||
version = "0.3.0"
|
||||
features = ["std", "minwinbase", "minwindef", "ntdef", "profileapi", "sysinfoapi", "timezoneapi"]
|
|
@ -0,0 +1,25 @@
|
|||
Copyright (c) 2014 The Rust Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,31 @@
|
|||
time
|
||||
====
|
||||
|
||||
Utilities for working with time-related functions in Rust
|
||||
|
||||
[![build status](https://github.com/time-rs/time/workflows/Build/badge.svg?branch=v0.1)](https://github.com/time-rs/time/actions?query=branch%3Av0.1)
|
||||
[![Documentation](https://docs.rs/time/badge.svg?version=0.1)](https://docs.rs/time/~0.1)
|
||||
![rustc 1.21.0](https://img.shields.io/badge/rustc-1.21.0-blue)
|
||||
|
||||
## time v0.1.x is Deprecated
|
||||
|
||||
The 0.1.x series of this library is deprecated and in maintenance mode. No new
|
||||
features will be added. Active development now occurs in the 0.2.x series.
|
||||
|
||||
If you need additional functionality that this crate does not provide, check
|
||||
out the [`chrono`](https://github.com/chronotope/chrono) crate.
|
||||
|
||||
## Usage
|
||||
|
||||
Put this in your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
time = "0.1"
|
||||
```
|
||||
|
||||
And this in your crate root:
|
||||
|
||||
```rust
|
||||
extern crate time;
|
||||
```
|
|
@ -0,0 +1,655 @@
|
|||
// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution and at
|
||||
// http://rust-lang.org/COPYRIGHT.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
//! Temporal quantification
|
||||
|
||||
use std::{fmt, i64};
|
||||
use std::error::Error;
|
||||
use std::ops::{Add, Sub, Mul, Div, Neg, FnOnce};
|
||||
use std::time::Duration as StdDuration;
|
||||
|
||||
/// The number of nanoseconds in a microsecond.
|
||||
const NANOS_PER_MICRO: i32 = 1000;
|
||||
/// The number of nanoseconds in a millisecond.
|
||||
const NANOS_PER_MILLI: i32 = 1000_000;
|
||||
/// The number of nanoseconds in seconds.
|
||||
const NANOS_PER_SEC: i32 = 1_000_000_000;
|
||||
/// The number of microseconds per second.
|
||||
const MICROS_PER_SEC: i64 = 1000_000;
|
||||
/// The number of milliseconds per second.
|
||||
const MILLIS_PER_SEC: i64 = 1000;
|
||||
/// The number of seconds in a minute.
|
||||
const SECS_PER_MINUTE: i64 = 60;
|
||||
/// The number of seconds in an hour.
|
||||
const SECS_PER_HOUR: i64 = 3600;
|
||||
/// The number of (non-leap) seconds in days.
|
||||
const SECS_PER_DAY: i64 = 86400;
|
||||
/// The number of (non-leap) seconds in a week.
|
||||
const SECS_PER_WEEK: i64 = 604800;
|
||||
|
||||
macro_rules! try_opt {
|
||||
($e:expr) => (match $e { Some(v) => v, None => return None })
|
||||
}
|
||||
|
||||
|
||||
/// ISO 8601 time duration with nanosecond precision.
|
||||
/// This also allows for the negative duration; see individual methods for details.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
|
||||
pub struct Duration {
|
||||
secs: i64,
|
||||
nanos: i32, // Always 0 <= nanos < NANOS_PER_SEC
|
||||
}
|
||||
|
||||
/// The minimum possible `Duration`: `i64::MIN` milliseconds.
|
||||
pub const MIN: Duration = Duration {
|
||||
secs: i64::MIN / MILLIS_PER_SEC - 1,
|
||||
nanos: NANOS_PER_SEC + (i64::MIN % MILLIS_PER_SEC) as i32 * NANOS_PER_MILLI
|
||||
};
|
||||
|
||||
/// The maximum possible `Duration`: `i64::MAX` milliseconds.
|
||||
pub const MAX: Duration = Duration {
|
||||
secs: i64::MAX / MILLIS_PER_SEC,
|
||||
nanos: (i64::MAX % MILLIS_PER_SEC) as i32 * NANOS_PER_MILLI
|
||||
};
|
||||
|
||||
impl Duration {
|
||||
/// Makes a new `Duration` with given number of weeks.
|
||||
/// Equivalent to `Duration::seconds(weeks * 7 * 24 * 60 * 60)` with overflow checks.
|
||||
/// Panics when the duration is out of bounds.
|
||||
#[inline]
|
||||
pub fn weeks(weeks: i64) -> Duration {
|
||||
let secs = weeks.checked_mul(SECS_PER_WEEK).expect("Duration::weeks out of bounds");
|
||||
Duration::seconds(secs)
|
||||
}
|
||||
|
||||
/// Makes a new `Duration` with given number of days.
|
||||
/// Equivalent to `Duration::seconds(days * 24 * 60 * 60)` with overflow checks.
|
||||
/// Panics when the duration is out of bounds.
|
||||
#[inline]
|
||||
pub fn days(days: i64) -> Duration {
|
||||
let secs = days.checked_mul(SECS_PER_DAY).expect("Duration::days out of bounds");
|
||||
Duration::seconds(secs)
|
||||
}
|
||||
|
||||
/// Makes a new `Duration` with given number of hours.
|
||||
/// Equivalent to `Duration::seconds(hours * 60 * 60)` with overflow checks.
|
||||
/// Panics when the duration is out of bounds.
|
||||
#[inline]
|
||||
pub fn hours(hours: i64) -> Duration {
|
||||
let secs = hours.checked_mul(SECS_PER_HOUR).expect("Duration::hours out of bounds");
|
||||
Duration::seconds(secs)
|
||||
}
|
||||
|
||||
/// Makes a new `Duration` with given number of minutes.
|
||||
/// Equivalent to `Duration::seconds(minutes * 60)` with overflow checks.
|
||||
/// Panics when the duration is out of bounds.
|
||||
#[inline]
|
||||
pub fn minutes(minutes: i64) -> Duration {
|
||||
let secs = minutes.checked_mul(SECS_PER_MINUTE).expect("Duration::minutes out of bounds");
|
||||
Duration::seconds(secs)
|
||||
}
|
||||
|
||||
/// Makes a new `Duration` with given number of seconds.
|
||||
/// Panics when the duration is more than `i64::MAX` milliseconds
|
||||
/// or less than `i64::MIN` milliseconds.
|
||||
#[inline]
|
||||
pub fn seconds(seconds: i64) -> Duration {
|
||||
let d = Duration { secs: seconds, nanos: 0 };
|
||||
if d < MIN || d > MAX {
|
||||
panic!("Duration::seconds out of bounds");
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// Makes a new `Duration` with given number of milliseconds.
|
||||
#[inline]
|
||||
pub fn milliseconds(milliseconds: i64) -> Duration {
|
||||
let (secs, millis) = div_mod_floor_64(milliseconds, MILLIS_PER_SEC);
|
||||
let nanos = millis as i32 * NANOS_PER_MILLI;
|
||||
Duration { secs: secs, nanos: nanos }
|
||||
}
|
||||
|
||||
/// Makes a new `Duration` with given number of microseconds.
|
||||
#[inline]
|
||||
pub fn microseconds(microseconds: i64) -> Duration {
|
||||
let (secs, micros) = div_mod_floor_64(microseconds, MICROS_PER_SEC);
|
||||
let nanos = micros as i32 * NANOS_PER_MICRO;
|
||||
Duration { secs: secs, nanos: nanos }
|
||||
}
|
||||
|
||||
/// Makes a new `Duration` with given number of nanoseconds.
|
||||
#[inline]
|
||||
pub fn nanoseconds(nanos: i64) -> Duration {
|
||||
let (secs, nanos) = div_mod_floor_64(nanos, NANOS_PER_SEC as i64);
|
||||
Duration { secs: secs, nanos: nanos as i32 }
|
||||
}
|
||||
|
||||
/// Runs a closure, returning the duration of time it took to run the
|
||||
/// closure.
|
||||
pub fn span<F>(f: F) -> Duration where F: FnOnce() {
|
||||
let before = super::precise_time_ns();
|
||||
f();
|
||||
Duration::nanoseconds((super::precise_time_ns() - before) as i64)
|
||||
}
|
||||
|
||||
/// Returns the total number of whole weeks in the duration.
|
||||
#[inline]
|
||||
pub fn num_weeks(&self) -> i64 {
|
||||
self.num_days() / 7
|
||||
}
|
||||
|
||||
/// Returns the total number of whole days in the duration.
|
||||
pub fn num_days(&self) -> i64 {
|
||||
self.num_seconds() / SECS_PER_DAY
|
||||
}
|
||||
|
||||
/// Returns the total number of whole hours in the duration.
|
||||
#[inline]
|
||||
pub fn num_hours(&self) -> i64 {
|
||||
self.num_seconds() / SECS_PER_HOUR
|
||||
}
|
||||
|
||||
/// Returns the total number of whole minutes in the duration.
|
||||
#[inline]
|
||||
pub fn num_minutes(&self) -> i64 {
|
||||
self.num_seconds() / SECS_PER_MINUTE
|
||||
}
|
||||
|
||||
/// Returns the total number of whole seconds in the duration.
|
||||
pub fn num_seconds(&self) -> i64 {
|
||||
// If secs is negative, nanos should be subtracted from the duration.
|
||||
if self.secs < 0 && self.nanos > 0 {
|
||||
self.secs + 1
|
||||
} else {
|
||||
self.secs
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of nanoseconds such that
|
||||
/// `nanos_mod_sec() + num_seconds() * NANOS_PER_SEC` is the total number of
|
||||
/// nanoseconds in the duration.
|
||||
fn nanos_mod_sec(&self) -> i32 {
|
||||
if self.secs < 0 && self.nanos > 0 {
|
||||
self.nanos - NANOS_PER_SEC
|
||||
} else {
|
||||
self.nanos
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the total number of whole milliseconds in the duration,
|
||||
pub fn num_milliseconds(&self) -> i64 {
|
||||
// A proper Duration will not overflow, because MIN and MAX are defined
|
||||
// such that the range is exactly i64 milliseconds.
|
||||
let secs_part = self.num_seconds() * MILLIS_PER_SEC;
|
||||
let nanos_part = self.nanos_mod_sec() / NANOS_PER_MILLI;
|
||||
secs_part + nanos_part as i64
|
||||
}
|
||||
|
||||
/// Returns the total number of whole microseconds in the duration,
|
||||
/// or `None` on overflow (exceeding 2<sup>63</sup> microseconds in either direction).
|
||||
pub fn num_microseconds(&self) -> Option<i64> {
|
||||
let secs_part = try_opt!(self.num_seconds().checked_mul(MICROS_PER_SEC));
|
||||
let nanos_part = self.nanos_mod_sec() / NANOS_PER_MICRO;
|
||||
secs_part.checked_add(nanos_part as i64)
|
||||
}
|
||||
|
||||
/// Returns the total number of whole nanoseconds in the duration,
|
||||
/// or `None` on overflow (exceeding 2<sup>63</sup> nanoseconds in either direction).
|
||||
pub fn num_nanoseconds(&self) -> Option<i64> {
|
||||
let secs_part = try_opt!(self.num_seconds().checked_mul(NANOS_PER_SEC as i64));
|
||||
let nanos_part = self.nanos_mod_sec();
|
||||
secs_part.checked_add(nanos_part as i64)
|
||||
}
|
||||
|
||||
/// Add two durations, returning `None` if overflow occurred.
|
||||
pub fn checked_add(&self, rhs: &Duration) -> Option<Duration> {
|
||||
let mut secs = try_opt!(self.secs.checked_add(rhs.secs));
|
||||
let mut nanos = self.nanos + rhs.nanos;
|
||||
if nanos >= NANOS_PER_SEC {
|
||||
nanos -= NANOS_PER_SEC;
|
||||
secs = try_opt!(secs.checked_add(1));
|
||||
}
|
||||
let d = Duration { secs: secs, nanos: nanos };
|
||||
// Even if d is within the bounds of i64 seconds,
|
||||
// it might still overflow i64 milliseconds.
|
||||
if d < MIN || d > MAX { None } else { Some(d) }
|
||||
}
|
||||
|
||||
/// Subtract two durations, returning `None` if overflow occurred.
|
||||
pub fn checked_sub(&self, rhs: &Duration) -> Option<Duration> {
|
||||
let mut secs = try_opt!(self.secs.checked_sub(rhs.secs));
|
||||
let mut nanos = self.nanos - rhs.nanos;
|
||||
if nanos < 0 {
|
||||
nanos += NANOS_PER_SEC;
|
||||
secs = try_opt!(secs.checked_sub(1));
|
||||
}
|
||||
let d = Duration { secs: secs, nanos: nanos };
|
||||
// Even if d is within the bounds of i64 seconds,
|
||||
// it might still overflow i64 milliseconds.
|
||||
if d < MIN || d > MAX { None } else { Some(d) }
|
||||
}
|
||||
|
||||
/// The minimum possible `Duration`: `i64::MIN` milliseconds.
|
||||
#[inline]
|
||||
pub fn min_value() -> Duration { MIN }
|
||||
|
||||
/// The maximum possible `Duration`: `i64::MAX` milliseconds.
|
||||
#[inline]
|
||||
pub fn max_value() -> Duration { MAX }
|
||||
|
||||
/// A duration where the stored seconds and nanoseconds are equal to zero.
|
||||
#[inline]
|
||||
pub fn zero() -> Duration {
|
||||
Duration { secs: 0, nanos: 0 }
|
||||
}
|
||||
|
||||
/// Returns `true` if the duration equals `Duration::zero()`.
|
||||
#[inline]
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.secs == 0 && self.nanos == 0
|
||||
}
|
||||
|
||||
/// Creates a `time::Duration` object from `std::time::Duration`
|
||||
///
|
||||
/// This function errors when original duration is larger than the maximum
|
||||
/// value supported for this type.
|
||||
pub fn from_std(duration: StdDuration) -> Result<Duration, OutOfRangeError> {
|
||||
// We need to check secs as u64 before coercing to i64
|
||||
if duration.as_secs() > MAX.secs as u64 {
|
||||
return Err(OutOfRangeError(()));
|
||||
}
|
||||
let d = Duration {
|
||||
secs: duration.as_secs() as i64,
|
||||
nanos: duration.subsec_nanos() as i32,
|
||||
};
|
||||
if d > MAX {
|
||||
return Err(OutOfRangeError(()));
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// Creates a `std::time::Duration` object from `time::Duration`
|
||||
///
|
||||
/// This function errors when duration is less than zero. As standard
|
||||
/// library implementation is limited to non-negative values.
|
||||
pub fn to_std(&self) -> Result<StdDuration, OutOfRangeError> {
|
||||
if self.secs < 0 {
|
||||
return Err(OutOfRangeError(()));
|
||||
}
|
||||
Ok(StdDuration::new(self.secs as u64, self.nanos as u32))
|
||||
}
|
||||
|
||||
/// Returns the raw value of duration.
|
||||
#[cfg(target_env = "sgx")]
|
||||
pub(crate) fn raw(&self) -> (i64, i32) {
|
||||
(self.secs, self.nanos)
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
#[inline]
|
||||
fn neg(self) -> Duration {
|
||||
if self.nanos == 0 {
|
||||
Duration { secs: -self.secs, nanos: 0 }
|
||||
} else {
|
||||
Duration { secs: -self.secs - 1, nanos: NANOS_PER_SEC - self.nanos }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn add(self, rhs: Duration) -> Duration {
|
||||
let mut secs = self.secs + rhs.secs;
|
||||
let mut nanos = self.nanos + rhs.nanos;
|
||||
if nanos >= NANOS_PER_SEC {
|
||||
nanos -= NANOS_PER_SEC;
|
||||
secs += 1;
|
||||
}
|
||||
Duration { secs: secs, nanos: nanos }
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn sub(self, rhs: Duration) -> Duration {
|
||||
let mut secs = self.secs - rhs.secs;
|
||||
let mut nanos = self.nanos - rhs.nanos;
|
||||
if nanos < 0 {
|
||||
nanos += NANOS_PER_SEC;
|
||||
secs -= 1;
|
||||
}
|
||||
Duration { secs: secs, nanos: nanos }
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<i32> for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn mul(self, rhs: i32) -> Duration {
|
||||
// Multiply nanoseconds as i64, because it cannot overflow that way.
|
||||
let total_nanos = self.nanos as i64 * rhs as i64;
|
||||
let (extra_secs, nanos) = div_mod_floor_64(total_nanos, NANOS_PER_SEC as i64);
|
||||
let secs = self.secs * rhs as i64 + extra_secs;
|
||||
Duration { secs: secs, nanos: nanos as i32 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Div<i32> for Duration {
|
||||
type Output = Duration;
|
||||
|
||||
fn div(self, rhs: i32) -> Duration {
|
||||
let mut secs = self.secs / rhs as i64;
|
||||
let carry = self.secs - secs * rhs as i64;
|
||||
let extra_nanos = carry * NANOS_PER_SEC as i64 / rhs as i64;
|
||||
let mut nanos = self.nanos / rhs + extra_nanos as i32;
|
||||
if nanos >= NANOS_PER_SEC {
|
||||
nanos -= NANOS_PER_SEC;
|
||||
secs += 1;
|
||||
}
|
||||
if nanos < 0 {
|
||||
nanos += NANOS_PER_SEC;
|
||||
secs -= 1;
|
||||
}
|
||||
Duration { secs: secs, nanos: nanos }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Duration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
// technically speaking, negative duration is not valid ISO 8601,
|
||||
// but we need to print it anyway.
|
||||
let (abs, sign) = if self.secs < 0 { (-*self, "-") } else { (*self, "") };
|
||||
|
||||
let days = abs.secs / SECS_PER_DAY;
|
||||
let secs = abs.secs - days * SECS_PER_DAY;
|
||||
let hasdate = days != 0;
|
||||
let hastime = (secs != 0 || abs.nanos != 0) || !hasdate;
|
||||
|
||||
write!(f, "{}P", sign)?;
|
||||
|
||||
if hasdate {
|
||||
write!(f, "{}D", days)?;
|
||||
}
|
||||
if hastime {
|
||||
if abs.nanos == 0 {
|
||||
write!(f, "T{}S", secs)?;
|
||||
} else if abs.nanos % NANOS_PER_MILLI == 0 {
|
||||
write!(f, "T{}.{:03}S", secs, abs.nanos / NANOS_PER_MILLI)?;
|
||||
} else if abs.nanos % NANOS_PER_MICRO == 0 {
|
||||
write!(f, "T{}.{:06}S", secs, abs.nanos / NANOS_PER_MICRO)?;
|
||||
} else {
|
||||
write!(f, "T{}.{:09}S", secs, abs.nanos)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents error when converting `Duration` to/from a standard library
|
||||
/// implementation
|
||||
///
|
||||
/// The `std::time::Duration` supports a range from zero to `u64::MAX`
|
||||
/// *seconds*, while this module supports signed range of up to
|
||||
/// `i64::MAX` of *milliseconds*.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OutOfRangeError(());
|
||||
|
||||
impl fmt::Display for OutOfRangeError {
|
||||
#[allow(deprecated)]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for OutOfRangeError {
|
||||
fn description(&self) -> &str {
|
||||
"Source duration value is out of range for the target type"
|
||||
}
|
||||
}
|
||||
|
||||
// Copied from libnum
|
||||
#[inline]
|
||||
fn div_mod_floor_64(this: i64, other: i64) -> (i64, i64) {
|
||||
(div_floor_64(this, other), mod_floor_64(this, other))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn div_floor_64(this: i64, other: i64) -> i64 {
|
||||
match div_rem_64(this, other) {
|
||||
(d, r) if (r > 0 && other < 0)
|
||||
|| (r < 0 && other > 0) => d - 1,
|
||||
(d, _) => d,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn mod_floor_64(this: i64, other: i64) -> i64 {
|
||||
match this % other {
|
||||
r if (r > 0 && other < 0)
|
||||
|| (r < 0 && other > 0) => r + other,
|
||||
r => r,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn div_rem_64(this: i64, other: i64) -> (i64, i64) {
|
||||
(this / other, this % other)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Duration, MIN, MAX, OutOfRangeError};
|
||||
use std::{i32, i64};
|
||||
use std::time::Duration as StdDuration;
|
||||
|
||||
#[test]
|
||||
fn test_duration() {
|
||||
assert!(Duration::seconds(1) != Duration::zero());
|
||||
assert_eq!(Duration::seconds(1) + Duration::seconds(2), Duration::seconds(3));
|
||||
assert_eq!(Duration::seconds(86399) + Duration::seconds(4),
|
||||
Duration::days(1) + Duration::seconds(3));
|
||||
assert_eq!(Duration::days(10) - Duration::seconds(1000), Duration::seconds(863000));
|
||||
assert_eq!(Duration::days(10) - Duration::seconds(1000000), Duration::seconds(-136000));
|
||||
assert_eq!(Duration::days(2) + Duration::seconds(86399) +
|
||||
Duration::nanoseconds(1234567890),
|
||||
Duration::days(3) + Duration::nanoseconds(234567890));
|
||||
assert_eq!(-Duration::days(3), Duration::days(-3));
|
||||
assert_eq!(-(Duration::days(3) + Duration::seconds(70)),
|
||||
Duration::days(-4) + Duration::seconds(86400-70));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_num_days() {
|
||||
assert_eq!(Duration::zero().num_days(), 0);
|
||||
assert_eq!(Duration::days(1).num_days(), 1);
|
||||
assert_eq!(Duration::days(-1).num_days(), -1);
|
||||
assert_eq!(Duration::seconds(86399).num_days(), 0);
|
||||
assert_eq!(Duration::seconds(86401).num_days(), 1);
|
||||
assert_eq!(Duration::seconds(-86399).num_days(), 0);
|
||||
assert_eq!(Duration::seconds(-86401).num_days(), -1);
|
||||
assert_eq!(Duration::days(i32::MAX as i64).num_days(), i32::MAX as i64);
|
||||
assert_eq!(Duration::days(i32::MIN as i64).num_days(), i32::MIN as i64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_num_seconds() {
|
||||
assert_eq!(Duration::zero().num_seconds(), 0);
|
||||
assert_eq!(Duration::seconds(1).num_seconds(), 1);
|
||||
assert_eq!(Duration::seconds(-1).num_seconds(), -1);
|
||||
assert_eq!(Duration::milliseconds(999).num_seconds(), 0);
|
||||
assert_eq!(Duration::milliseconds(1001).num_seconds(), 1);
|
||||
assert_eq!(Duration::milliseconds(-999).num_seconds(), 0);
|
||||
assert_eq!(Duration::milliseconds(-1001).num_seconds(), -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_num_milliseconds() {
|
||||
assert_eq!(Duration::zero().num_milliseconds(), 0);
|
||||
assert_eq!(Duration::milliseconds(1).num_milliseconds(), 1);
|
||||
assert_eq!(Duration::milliseconds(-1).num_milliseconds(), -1);
|
||||
assert_eq!(Duration::microseconds(999).num_milliseconds(), 0);
|
||||
assert_eq!(Duration::microseconds(1001).num_milliseconds(), 1);
|
||||
assert_eq!(Duration::microseconds(-999).num_milliseconds(), 0);
|
||||
assert_eq!(Duration::microseconds(-1001).num_milliseconds(), -1);
|
||||
assert_eq!(Duration::milliseconds(i64::MAX).num_milliseconds(), i64::MAX);
|
||||
assert_eq!(Duration::milliseconds(i64::MIN).num_milliseconds(), i64::MIN);
|
||||
assert_eq!(MAX.num_milliseconds(), i64::MAX);
|
||||
assert_eq!(MIN.num_milliseconds(), i64::MIN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_num_microseconds() {
|
||||
assert_eq!(Duration::zero().num_microseconds(), Some(0));
|
||||
assert_eq!(Duration::microseconds(1).num_microseconds(), Some(1));
|
||||
assert_eq!(Duration::microseconds(-1).num_microseconds(), Some(-1));
|
||||
assert_eq!(Duration::nanoseconds(999).num_microseconds(), Some(0));
|
||||
assert_eq!(Duration::nanoseconds(1001).num_microseconds(), Some(1));
|
||||
assert_eq!(Duration::nanoseconds(-999).num_microseconds(), Some(0));
|
||||
assert_eq!(Duration::nanoseconds(-1001).num_microseconds(), Some(-1));
|
||||
assert_eq!(Duration::microseconds(i64::MAX).num_microseconds(), Some(i64::MAX));
|
||||
assert_eq!(Duration::microseconds(i64::MIN).num_microseconds(), Some(i64::MIN));
|
||||
assert_eq!(MAX.num_microseconds(), None);
|
||||
assert_eq!(MIN.num_microseconds(), None);
|
||||
|
||||
// overflow checks
|
||||
const MICROS_PER_DAY: i64 = 86400_000_000;
|
||||
assert_eq!(Duration::days(i64::MAX / MICROS_PER_DAY).num_microseconds(),
|
||||
Some(i64::MAX / MICROS_PER_DAY * MICROS_PER_DAY));
|
||||
assert_eq!(Duration::days(i64::MIN / MICROS_PER_DAY).num_microseconds(),
|
||||
Some(i64::MIN / MICROS_PER_DAY * MICROS_PER_DAY));
|
||||
assert_eq!(Duration::days(i64::MAX / MICROS_PER_DAY + 1).num_microseconds(), None);
|
||||
assert_eq!(Duration::days(i64::MIN / MICROS_PER_DAY - 1).num_microseconds(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_num_nanoseconds() {
|
||||
assert_eq!(Duration::zero().num_nanoseconds(), Some(0));
|
||||
assert_eq!(Duration::nanoseconds(1).num_nanoseconds(), Some(1));
|
||||
assert_eq!(Duration::nanoseconds(-1).num_nanoseconds(), Some(-1));
|
||||
assert_eq!(Duration::nanoseconds(i64::MAX).num_nanoseconds(), Some(i64::MAX));
|
||||
assert_eq!(Duration::nanoseconds(i64::MIN).num_nanoseconds(), Some(i64::MIN));
|
||||
assert_eq!(MAX.num_nanoseconds(), None);
|
||||
assert_eq!(MIN.num_nanoseconds(), None);
|
||||
|
||||
// overflow checks
|
||||
const NANOS_PER_DAY: i64 = 86400_000_000_000;
|
||||
assert_eq!(Duration::days(i64::MAX / NANOS_PER_DAY).num_nanoseconds(),
|
||||
Some(i64::MAX / NANOS_PER_DAY * NANOS_PER_DAY));
|
||||
assert_eq!(Duration::days(i64::MIN / NANOS_PER_DAY).num_nanoseconds(),
|
||||
Some(i64::MIN / NANOS_PER_DAY * NANOS_PER_DAY));
|
||||
assert_eq!(Duration::days(i64::MAX / NANOS_PER_DAY + 1).num_nanoseconds(), None);
|
||||
assert_eq!(Duration::days(i64::MIN / NANOS_PER_DAY - 1).num_nanoseconds(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_checked_ops() {
|
||||
assert_eq!(Duration::milliseconds(i64::MAX - 1).checked_add(&Duration::microseconds(999)),
|
||||
Some(Duration::milliseconds(i64::MAX - 2) + Duration::microseconds(1999)));
|
||||
assert!(Duration::milliseconds(i64::MAX).checked_add(&Duration::microseconds(1000))
|
||||
.is_none());
|
||||
|
||||
assert_eq!(Duration::milliseconds(i64::MIN).checked_sub(&Duration::milliseconds(0)),
|
||||
Some(Duration::milliseconds(i64::MIN)));
|
||||
assert!(Duration::milliseconds(i64::MIN).checked_sub(&Duration::milliseconds(1))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_mul() {
|
||||
assert_eq!(Duration::zero() * i32::MAX, Duration::zero());
|
||||
assert_eq!(Duration::zero() * i32::MIN, Duration::zero());
|
||||
assert_eq!(Duration::nanoseconds(1) * 0, Duration::zero());
|
||||
assert_eq!(Duration::nanoseconds(1) * 1, Duration::nanoseconds(1));
|
||||
assert_eq!(Duration::nanoseconds(1) * 1_000_000_000, Duration::seconds(1));
|
||||
assert_eq!(Duration::nanoseconds(1) * -1_000_000_000, -Duration::seconds(1));
|
||||
assert_eq!(-Duration::nanoseconds(1) * 1_000_000_000, -Duration::seconds(1));
|
||||
assert_eq!(Duration::nanoseconds(30) * 333_333_333,
|
||||
Duration::seconds(10) - Duration::nanoseconds(10));
|
||||
assert_eq!((Duration::nanoseconds(1) + Duration::seconds(1) + Duration::days(1)) * 3,
|
||||
Duration::nanoseconds(3) + Duration::seconds(3) + Duration::days(3));
|
||||
assert_eq!(Duration::milliseconds(1500) * -2, Duration::seconds(-3));
|
||||
assert_eq!(Duration::milliseconds(-1500) * 2, Duration::seconds(-3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_div() {
|
||||
assert_eq!(Duration::zero() / i32::MAX, Duration::zero());
|
||||
assert_eq!(Duration::zero() / i32::MIN, Duration::zero());
|
||||
assert_eq!(Duration::nanoseconds(123_456_789) / 1, Duration::nanoseconds(123_456_789));
|
||||
assert_eq!(Duration::nanoseconds(123_456_789) / -1, -Duration::nanoseconds(123_456_789));
|
||||
assert_eq!(-Duration::nanoseconds(123_456_789) / -1, Duration::nanoseconds(123_456_789));
|
||||
assert_eq!(-Duration::nanoseconds(123_456_789) / 1, -Duration::nanoseconds(123_456_789));
|
||||
assert_eq!(Duration::seconds(1) / 3, Duration::nanoseconds(333_333_333));
|
||||
assert_eq!(Duration::seconds(4) / 3, Duration::nanoseconds(1_333_333_333));
|
||||
assert_eq!(Duration::seconds(-1) / 2, Duration::milliseconds(-500));
|
||||
assert_eq!(Duration::seconds(1) / -2, Duration::milliseconds(-500));
|
||||
assert_eq!(Duration::seconds(-1) / -2, Duration::milliseconds(500));
|
||||
assert_eq!(Duration::seconds(-4) / 3, Duration::nanoseconds(-1_333_333_333));
|
||||
assert_eq!(Duration::seconds(-4) / -3, Duration::nanoseconds(1_333_333_333));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_duration_fmt() {
|
||||
assert_eq!(Duration::zero().to_string(), "PT0S");
|
||||
assert_eq!(Duration::days(42).to_string(), "P42D");
|
||||
assert_eq!(Duration::days(-42).to_string(), "-P42D");
|
||||
assert_eq!(Duration::seconds(42).to_string(), "PT42S");
|
||||
assert_eq!(Duration::milliseconds(42).to_string(), "PT0.042S");
|
||||
assert_eq!(Duration::microseconds(42).to_string(), "PT0.000042S");
|
||||
assert_eq!(Duration::nanoseconds(42).to_string(), "PT0.000000042S");
|
||||
assert_eq!((Duration::days(7) + Duration::milliseconds(6543)).to_string(),
|
||||
"P7DT6.543S");
|
||||
assert_eq!(Duration::seconds(-86401).to_string(), "-P1DT1S");
|
||||
assert_eq!(Duration::nanoseconds(-1).to_string(), "-PT0.000000001S");
|
||||
|
||||
// the format specifier should have no effect on `Duration`
|
||||
assert_eq!(format!("{:30}", Duration::days(1) + Duration::milliseconds(2345)),
|
||||
"P1DT2.345S");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_std() {
|
||||
assert_eq!(Duration::seconds(1).to_std(), Ok(StdDuration::new(1, 0)));
|
||||
assert_eq!(Duration::seconds(86401).to_std(), Ok(StdDuration::new(86401, 0)));
|
||||
assert_eq!(Duration::milliseconds(123).to_std(), Ok(StdDuration::new(0, 123000000)));
|
||||
assert_eq!(Duration::milliseconds(123765).to_std(), Ok(StdDuration::new(123, 765000000)));
|
||||
assert_eq!(Duration::nanoseconds(777).to_std(), Ok(StdDuration::new(0, 777)));
|
||||
assert_eq!(MAX.to_std(), Ok(StdDuration::new(9223372036854775, 807000000)));
|
||||
assert_eq!(Duration::seconds(-1).to_std(),
|
||||
Err(OutOfRangeError(())));
|
||||
assert_eq!(Duration::milliseconds(-1).to_std(),
|
||||
Err(OutOfRangeError(())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_std() {
|
||||
assert_eq!(Ok(Duration::seconds(1)),
|
||||
Duration::from_std(StdDuration::new(1, 0)));
|
||||
assert_eq!(Ok(Duration::seconds(86401)),
|
||||
Duration::from_std(StdDuration::new(86401, 0)));
|
||||
assert_eq!(Ok(Duration::milliseconds(123)),
|
||||
Duration::from_std(StdDuration::new(0, 123000000)));
|
||||
assert_eq!(Ok(Duration::milliseconds(123765)),
|
||||
Duration::from_std(StdDuration::new(123, 765000000)));
|
||||
assert_eq!(Ok(Duration::nanoseconds(777)),
|
||||
Duration::from_std(StdDuration::new(0, 777)));
|
||||
assert_eq!(Ok(MAX),
|
||||
Duration::from_std(StdDuration::new(9223372036854775, 807000000)));
|
||||
assert_eq!(Duration::from_std(StdDuration::new(9223372036854776, 0)),
|
||||
Err(OutOfRangeError(())));
|
||||
assert_eq!(Duration::from_std(StdDuration::new(9223372036854775, 807000001)),
|
||||
Err(OutOfRangeError(())));
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1 @@
|
|||
{"files":{"Cargo.toml":"c993889735eed89f265010c2ff35ffdd33c95077f53b012678d7330b7662ba9e","LICENSE-Apache":"b8929fea28678da67251fb2daf9438f67503814211051861612441806d8edb05","LICENSE-MIT":"04620bf27e4a643dd47bf27652320c205acdb776c1f9f24bb8c3bfaba10804c5","src/date.rs":"ef9432de8c7250b6ffe82fe7cd4332018ce22e32b0a414d3a7d60eb18d9b6eca","src/datetime.rs":"5c7f6e07dc2f0dcfcd86216664df53bc008dbc86f346df57a9ff57f52fe43bc6","src/error.rs":"a1de0680c8a51a05f341352e9707fe50d2c29b00c36600c2b642879cea1e89c6","src/format_description/component.rs":"a05e7549db9bab4f3836f5fd5af18cacbfa6b323d0106b027e21bf438a5885e5","src/format_description/error.rs":"f2085c5914c21bce09e8eb73b2b96584ac28a829f151e339b3f4ca7b5d5405a3","src/format_description/mod.rs":"da47af329408e9428753ad98ce433eaf026cfdd6e73e3142b23285251d32d0dd","src/format_description/modifier.rs":"c252c8a7d6608b594a6f715210ff67e804ae2f308025f62c8dd99d707627e4a9","src/format_description/parse.rs":"e131ccb7e7ad28e851a01f1b4ce8a6febcc2093e21c0a18126f15e8c17044625","src/helpers/mod.rs":"d3283903b66388efced79a4b1eca3a6b4644031d1ae211034443df5b6b78dc51","src/helpers/string.rs":"ba5699a4df344cbd71c4143f642f6bc07591f53978a9800d4b49ca1f461f87d9","src/lib.rs":"fecf82d1c8b1287fda03c3728741e477731534cf9c2609def2b525ad29683119","src/offset.rs":"fc9341648e091b4d8f7bec47006c01c21cb038c7ef98bd36a492cf78e7533023","src/quote.rs":"058ae621d7c6951c6289a3e855c74d924d9434c1f3031438587aedeb718be283","src/serde_format_description.rs":"5750a1ecf75ae80dc88d4242a6e56b39f3e25c77fdbcc2e88ec3a91ad8511434","src/time.rs":"3c06562358aed7ef624319c96e3f9c150a069606ab930de98ac379ef16b08100","src/to_tokens.rs":"825150a92396a019fee44f21da0bd257349e276d5e75a23ff86cfc625bef6f10"},"package":"42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"}
|
|
@ -0,0 +1,29 @@
|
|||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2018"
|
||||
name = "time-macros"
|
||||
version = "0.2.4"
|
||||
authors = ["Jacob Pratt <open-source@jhpratt.dev>", "Time contributors"]
|
||||
description = "Procedural macros for the time crate."
|
||||
readme = "../README.md"
|
||||
keywords = ["date", "time", "calendar", "duration"]
|
||||
categories = ["date-and-time"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/time-rs/time"
|
||||
resolver = "2"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[features]
|
||||
large-dates = []
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022 Jacob Pratt et al.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2022 Jacob Pratt et al.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,137 @@
|
|||
use std::iter::Peekable;
|
||||
|
||||
use proc_macro::{token_stream, TokenTree};
|
||||
|
||||
use crate::helpers::{
|
||||
consume_any_ident, consume_number, consume_punct, days_in_year, days_in_year_month,
|
||||
weeks_in_year, ymd_to_yo, ywd_to_yo,
|
||||
};
|
||||
use crate::to_tokens::ToTokenTree;
|
||||
use crate::Error;
|
||||
|
||||
#[cfg(feature = "large-dates")]
|
||||
const MAX_YEAR: i32 = 999_999;
|
||||
#[cfg(not(feature = "large-dates"))]
|
||||
const MAX_YEAR: i32 = 9_999;
|
||||
|
||||
pub(crate) struct Date {
|
||||
pub(crate) year: i32,
|
||||
pub(crate) ordinal: u16,
|
||||
}
|
||||
|
||||
pub(crate) fn parse(chars: &mut Peekable<token_stream::IntoIter>) -> Result<Date, Error> {
|
||||
let (year_sign_span, year_sign, explicit_sign) = if let Ok(span) = consume_punct('-', chars) {
|
||||
(Some(span), -1, true)
|
||||
} else if let Ok(span) = consume_punct('+', chars) {
|
||||
(Some(span), 1, true)
|
||||
} else {
|
||||
(None, 1, false)
|
||||
};
|
||||
let (year_span, mut year) = consume_number::<i32>("year", chars)?;
|
||||
year *= year_sign;
|
||||
if year.abs() > MAX_YEAR {
|
||||
return Err(Error::InvalidComponent {
|
||||
name: "year",
|
||||
value: year.to_string(),
|
||||
span_start: Some(year_sign_span.unwrap_or(year_span)),
|
||||
span_end: Some(year_span),
|
||||
});
|
||||
}
|
||||
if !explicit_sign && year.abs() >= 10_000 {
|
||||
return Err(Error::Custom {
|
||||
message: "years with more than four digits must have an explicit sign".into(),
|
||||
span_start: Some(year_sign_span.unwrap_or(year_span)),
|
||||
span_end: Some(year_span),
|
||||
});
|
||||
}
|
||||
|
||||
consume_punct('-', chars)?;
|
||||
|
||||
// year-week-day
|
||||
if let Ok(w_span) = consume_any_ident(&["W"], chars) {
|
||||
let (week_span, week) = consume_number::<u8>("week", chars)?;
|
||||
consume_punct('-', chars)?;
|
||||
let (day_span, day) = consume_number::<u8>("day", chars)?;
|
||||
|
||||
if week > weeks_in_year(year) {
|
||||
return Err(Error::InvalidComponent {
|
||||
name: "week",
|
||||
value: week.to_string(),
|
||||
span_start: Some(w_span),
|
||||
span_end: Some(week_span),
|
||||
});
|
||||
}
|
||||
if day == 0 || day > 7 {
|
||||
return Err(Error::InvalidComponent {
|
||||
name: "day",
|
||||
value: day.to_string(),
|
||||
span_start: Some(day_span),
|
||||
span_end: Some(day_span),
|
||||
});
|
||||
}
|
||||
|
||||
let (year, ordinal) = ywd_to_yo(year, week, day);
|
||||
|
||||
return Ok(Date { year, ordinal });
|
||||
}
|
||||
|
||||
// We don't yet know whether it's year-month-day or year-ordinal.
|
||||
let (month_or_ordinal_span, month_or_ordinal) =
|
||||
consume_number::<u16>("month or ordinal", chars)?;
|
||||
|
||||
// year-month-day
|
||||
#[allow(clippy::branches_sharing_code)] // clarity
|
||||
if consume_punct('-', chars).is_ok() {
|
||||
let (month_span, month) = (month_or_ordinal_span, month_or_ordinal);
|
||||
let (day_span, day) = consume_number::<u8>("day", chars)?;
|
||||
|
||||
if month == 0 || month > 12 {
|
||||
return Err(Error::InvalidComponent {
|
||||
name: "month",
|
||||
value: month.to_string(),
|
||||
span_start: Some(month_span),
|
||||
span_end: Some(month_span),
|
||||
});
|
||||
}
|
||||
let month = month as _;
|
||||
if day == 0 || day > days_in_year_month(year, month) {
|
||||
return Err(Error::InvalidComponent {
|
||||
name: "day",
|
||||
value: day.to_string(),
|
||||
span_start: Some(day_span),
|
||||
span_end: Some(day_span),
|
||||
});
|
||||
}
|
||||
|
||||
let (year, ordinal) = ymd_to_yo(year, month, day);
|
||||
|
||||
Ok(Date { year, ordinal })
|
||||
}
|
||||
// year-ordinal
|
||||
else {
|
||||
let (ordinal_span, ordinal) = (month_or_ordinal_span, month_or_ordinal);
|
||||
|
||||
if ordinal == 0 || ordinal > days_in_year(year) {
|
||||
return Err(Error::InvalidComponent {
|
||||
name: "ordinal",
|
||||
value: ordinal.to_string(),
|
||||
span_start: Some(ordinal_span),
|
||||
span_end: Some(ordinal_span),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Date { year, ordinal })
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokenTree for Date {
|
||||
fn into_token_tree(self) -> TokenTree {
|
||||
quote_group! {{
|
||||
const DATE: ::time::Date = ::time::Date::__from_ordinal_date_unchecked(
|
||||
#(self.year),
|
||||
#(self.ordinal),
|
||||
);
|
||||
DATE
|
||||
}}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
use std::iter::Peekable;
|
||||
|
||||
use proc_macro::{token_stream, Ident, Span, TokenTree};
|
||||
|
||||
use crate::date::Date;
|
||||
use crate::error::Error;
|
||||
use crate::offset::Offset;
|
||||
use crate::time::Time;
|
||||
use crate::to_tokens::ToTokenTree;
|
||||
use crate::{date, offset, time};
|
||||
|
||||
pub(crate) struct DateTime {
|
||||
date: Date,
|
||||
time: Time,
|
||||
offset: Option<Offset>,
|
||||
}
|
||||
|
||||
pub(crate) fn parse(chars: &mut Peekable<token_stream::IntoIter>) -> Result<DateTime, Error> {
|
||||
let date = date::parse(chars)?;
|
||||
let time = time::parse(chars)?;
|
||||
let offset = match offset::parse(chars) {
|
||||
Ok(offset) => Some(offset),
|
||||
Err(Error::UnexpectedEndOfInput | Error::MissingComponent { name: "sign", .. }) => None,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
if let Some(token) = chars.peek() {
|
||||
return Err(Error::UnexpectedToken {
|
||||
tree: token.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(DateTime { date, time, offset })
|
||||
}
|
||||
|
||||
impl ToTokenTree for DateTime {
|
||||
fn into_token_tree(self) -> TokenTree {
|
||||
let (type_name, maybe_offset) = match self.offset {
|
||||
Some(offset) => (
|
||||
Ident::new("OffsetDateTime", Span::mixed_site()),
|
||||
quote!(.assume_offset(#(offset))),
|
||||
),
|
||||
None => (
|
||||
Ident::new("PrimitiveDateTime", Span::mixed_site()),
|
||||
quote!(),
|
||||
),
|
||||
};
|
||||
|
||||
quote_group! {{
|
||||
const DATE_TIME: ::time::#(type_name) = ::time::PrimitiveDateTime::new(
|
||||
#(self.date),
|
||||
#(self.time),
|
||||
) #S(maybe_offset);
|
||||
DATE_TIME
|
||||
}}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::iter::once;
|
||||
|
||||
use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
|
||||
|
||||
use crate::format_description::error::InvalidFormatDescription;
|
||||
|
||||
trait WithSpan {
|
||||
fn with_span(self, span: Span) -> Self;
|
||||
}
|
||||
|
||||
impl WithSpan for TokenTree {
|
||||
fn with_span(mut self, span: Span) -> Self {
|
||||
self.set_span(span);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Error {
|
||||
MissingComponent {
|
||||
name: &'static str,
|
||||
span_start: Option<Span>,
|
||||
span_end: Option<Span>,
|
||||
},
|
||||
InvalidComponent {
|
||||
name: &'static str,
|
||||
value: String,
|
||||
span_start: Option<Span>,
|
||||
span_end: Option<Span>,
|
||||
},
|
||||
ExpectedString {
|
||||
span_start: Option<Span>,
|
||||
span_end: Option<Span>,
|
||||
},
|
||||
UnexpectedToken {
|
||||
tree: TokenTree,
|
||||
},
|
||||
UnexpectedEndOfInput,
|
||||
InvalidFormatDescription {
|
||||
error: InvalidFormatDescription,
|
||||
span_start: Option<Span>,
|
||||
span_end: Option<Span>,
|
||||
},
|
||||
Custom {
|
||||
message: Cow<'static, str>,
|
||||
span_start: Option<Span>,
|
||||
span_end: Option<Span>,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingComponent { name, .. } => write!(f, "missing component: {}", name),
|
||||
Self::InvalidComponent { name, value, .. } => {
|
||||
write!(f, "invalid component: {} was {}", name, value)
|
||||
}
|
||||
Self::ExpectedString { .. } => f.write_str("expected string"),
|
||||
Self::UnexpectedToken { tree } => write!(f, "unexpected token: {}", tree),
|
||||
Self::UnexpectedEndOfInput => f.write_str("unexpected end of input"),
|
||||
Self::InvalidFormatDescription { error, .. } => error.fmt(f),
|
||||
Self::Custom { message, .. } => f.write_str(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
fn span_start(&self) -> Span {
|
||||
match self {
|
||||
Self::MissingComponent { span_start, .. }
|
||||
| Self::InvalidComponent { span_start, .. }
|
||||
| Self::ExpectedString { span_start, .. }
|
||||
| Self::InvalidFormatDescription { span_start, .. }
|
||||
| Self::Custom { span_start, .. } => *span_start,
|
||||
Self::UnexpectedToken { tree } => Some(tree.span()),
|
||||
Self::UnexpectedEndOfInput => Some(Span::mixed_site()),
|
||||
}
|
||||
.unwrap_or_else(Span::mixed_site)
|
||||
}
|
||||
|
||||
fn span_end(&self) -> Span {
|
||||
match self {
|
||||
Self::MissingComponent { span_end, .. }
|
||||
| Self::InvalidComponent { span_end, .. }
|
||||
| Self::ExpectedString { span_end, .. }
|
||||
| Self::InvalidFormatDescription { span_end, .. }
|
||||
| Self::Custom { span_end, .. } => *span_end,
|
||||
Self::UnexpectedToken { tree, .. } => Some(tree.span()),
|
||||
Self::UnexpectedEndOfInput => Some(Span::mixed_site()),
|
||||
}
|
||||
.unwrap_or_else(|| self.span_start())
|
||||
}
|
||||
|
||||
pub(crate) fn to_compile_error(&self) -> TokenStream {
|
||||
let (start, end) = (self.span_start(), self.span_end());
|
||||
|
||||
[
|
||||
TokenTree::from(Punct::new(':', Spacing::Joint)).with_span(start),
|
||||
TokenTree::from(Punct::new(':', Spacing::Alone)).with_span(start),
|
||||
TokenTree::from(Ident::new("core", start)),
|
||||
TokenTree::from(Punct::new(':', Spacing::Joint)).with_span(start),
|
||||
TokenTree::from(Punct::new(':', Spacing::Alone)).with_span(start),
|
||||
TokenTree::from(Ident::new("compile_error", start)),
|
||||
TokenTree::from(Punct::new('!', Spacing::Alone)).with_span(start),
|
||||
TokenTree::from(Group::new(
|
||||
Delimiter::Parenthesis,
|
||||
TokenStream::from(
|
||||
TokenTree::from(Literal::string(&self.to_string())).with_span(end),
|
||||
),
|
||||
))
|
||||
.with_span(end),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Like `to_compile_error`, but for use in macros that produce items.
|
||||
pub(crate) fn to_compile_error_standalone(&self) -> TokenStream {
|
||||
let end = self.span_end();
|
||||
self.to_compile_error()
|
||||
.into_iter()
|
||||
.chain(once(
|
||||
TokenTree::from(Punct::new(';', Spacing::Alone)).with_span(end),
|
||||
))
|
||||
.collect()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,168 @@
|
|||
use proc_macro::{Ident, Span, TokenStream};
|
||||
|
||||
use crate::format_description::error::InvalidFormatDescription;
|
||||
use crate::format_description::modifier;
|
||||
use crate::format_description::modifier::Modifiers;
|
||||
use crate::to_tokens::ToTokenStream;
|
||||
|
||||
pub(crate) enum Component {
|
||||
Day(modifier::Day),
|
||||
Month(modifier::Month),
|
||||
Ordinal(modifier::Ordinal),
|
||||
Weekday(modifier::Weekday),
|
||||
WeekNumber(modifier::WeekNumber),
|
||||
Year(modifier::Year),
|
||||
Hour(modifier::Hour),
|
||||
Minute(modifier::Minute),
|
||||
Period(modifier::Period),
|
||||
Second(modifier::Second),
|
||||
Subsecond(modifier::Subsecond),
|
||||
OffsetHour(modifier::OffsetHour),
|
||||
OffsetMinute(modifier::OffsetMinute),
|
||||
OffsetSecond(modifier::OffsetSecond),
|
||||
}
|
||||
|
||||
impl ToTokenStream for Component {
|
||||
fn append_to(self, ts: &mut TokenStream) {
|
||||
let mut mts = TokenStream::new();
|
||||
|
||||
macro_rules! component_name_and_append {
|
||||
($($name:ident)*) => {
|
||||
match self {
|
||||
$(Self::$name(modifier) => {
|
||||
modifier.append_to(&mut mts);
|
||||
stringify!($name)
|
||||
})*
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let component = component_name_and_append![
|
||||
Day
|
||||
Month
|
||||
Ordinal
|
||||
Weekday
|
||||
WeekNumber
|
||||
Year
|
||||
Hour
|
||||
Minute
|
||||
Period
|
||||
Second
|
||||
Subsecond
|
||||
OffsetHour
|
||||
OffsetMinute
|
||||
OffsetSecond
|
||||
];
|
||||
let component = Ident::new(component, Span::mixed_site());
|
||||
|
||||
quote_append! { ts
|
||||
::time::format_description::Component::#(component)(#S(mts))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum NakedComponent {
|
||||
Day,
|
||||
Month,
|
||||
Ordinal,
|
||||
Weekday,
|
||||
WeekNumber,
|
||||
Year,
|
||||
Hour,
|
||||
Minute,
|
||||
Period,
|
||||
Second,
|
||||
Subsecond,
|
||||
OffsetHour,
|
||||
OffsetMinute,
|
||||
OffsetSecond,
|
||||
}
|
||||
|
||||
impl NakedComponent {
|
||||
pub(crate) fn parse(
|
||||
component_name: &[u8],
|
||||
component_index: usize,
|
||||
) -> Result<Self, InvalidFormatDescription> {
|
||||
match component_name {
|
||||
b"day" => Ok(Self::Day),
|
||||
b"month" => Ok(Self::Month),
|
||||
b"ordinal" => Ok(Self::Ordinal),
|
||||
b"weekday" => Ok(Self::Weekday),
|
||||
b"week_number" => Ok(Self::WeekNumber),
|
||||
b"year" => Ok(Self::Year),
|
||||
b"hour" => Ok(Self::Hour),
|
||||
b"minute" => Ok(Self::Minute),
|
||||
b"period" => Ok(Self::Period),
|
||||
b"second" => Ok(Self::Second),
|
||||
b"subsecond" => Ok(Self::Subsecond),
|
||||
b"offset_hour" => Ok(Self::OffsetHour),
|
||||
b"offset_minute" => Ok(Self::OffsetMinute),
|
||||
b"offset_second" => Ok(Self::OffsetSecond),
|
||||
b"" => Err(InvalidFormatDescription::MissingComponentName {
|
||||
index: component_index,
|
||||
}),
|
||||
_ => Err(InvalidFormatDescription::InvalidComponentName {
|
||||
name: String::from_utf8_lossy(component_name).into_owned(),
|
||||
index: component_index,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attach_modifiers(self, modifiers: Modifiers) -> Component {
|
||||
match self {
|
||||
Self::Day => Component::Day(modifier::Day {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::Month => Component::Month(modifier::Month {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
repr: modifiers.month_repr.unwrap_or_default(),
|
||||
case_sensitive: modifiers.case_sensitive.unwrap_or(true),
|
||||
}),
|
||||
Self::Ordinal => Component::Ordinal(modifier::Ordinal {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::Weekday => Component::Weekday(modifier::Weekday {
|
||||
repr: modifiers.weekday_repr.unwrap_or_default(),
|
||||
one_indexed: modifiers.weekday_is_one_indexed.unwrap_or(true),
|
||||
case_sensitive: modifiers.case_sensitive.unwrap_or(true),
|
||||
}),
|
||||
Self::WeekNumber => Component::WeekNumber(modifier::WeekNumber {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
repr: modifiers.week_number_repr.unwrap_or_default(),
|
||||
}),
|
||||
Self::Year => Component::Year(modifier::Year {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
repr: modifiers.year_repr.unwrap_or_default(),
|
||||
iso_week_based: modifiers.year_is_iso_week_based.unwrap_or_default(),
|
||||
sign_is_mandatory: modifiers.sign_is_mandatory.unwrap_or_default(),
|
||||
}),
|
||||
Self::Hour => Component::Hour(modifier::Hour {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
is_12_hour_clock: modifiers.hour_is_12_hour_clock.unwrap_or_default(),
|
||||
}),
|
||||
Self::Minute => Component::Minute(modifier::Minute {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::Period => Component::Period(modifier::Period {
|
||||
is_uppercase: modifiers.period_is_uppercase.unwrap_or(true),
|
||||
case_sensitive: modifiers.case_sensitive.unwrap_or(true),
|
||||
}),
|
||||
Self::Second => Component::Second(modifier::Second {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::Subsecond => Component::Subsecond(modifier::Subsecond {
|
||||
digits: modifiers.subsecond_digits.unwrap_or_default(),
|
||||
}),
|
||||
Self::OffsetHour => Component::OffsetHour(modifier::OffsetHour {
|
||||
sign_is_mandatory: modifiers.sign_is_mandatory.unwrap_or_default(),
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::OffsetMinute => Component::OffsetMinute(modifier::OffsetMinute {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::OffsetSecond => Component::OffsetSecond(modifier::OffsetSecond {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
use std::fmt;
|
||||
|
||||
pub(crate) enum InvalidFormatDescription {
|
||||
UnclosedOpeningBracket { index: usize },
|
||||
InvalidComponentName { name: String, index: usize },
|
||||
InvalidModifier { value: String, index: usize },
|
||||
MissingComponentName { index: usize },
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidFormatDescription {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use InvalidFormatDescription::*;
|
||||
match self {
|
||||
UnclosedOpeningBracket { index } => {
|
||||
write!(f, "unclosed opening bracket at byte index {}", index)
|
||||
}
|
||||
InvalidComponentName { name, index } => write!(
|
||||
f,
|
||||
"invalid component name `{}` at byte index {}",
|
||||
name, index
|
||||
),
|
||||
InvalidModifier { value, index } => {
|
||||
write!(f, "invalid modifier `{}` at byte index {}", value, index)
|
||||
}
|
||||
MissingComponentName { index } => {
|
||||
write!(f, "missing component name at byte index {}", index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
mod component;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod modifier;
|
||||
pub(crate) mod parse;
|
||||
|
||||
use proc_macro::{Literal, TokenStream};
|
||||
|
||||
pub(crate) use self::component::Component;
|
||||
pub(crate) use self::parse::parse;
|
||||
use crate::to_tokens::ToTokenStream;
|
||||
|
||||
mod helper {
|
||||
#[must_use = "This does not modify the original slice."]
|
||||
pub(crate) fn consume_whitespace<'a>(bytes: &'a [u8], index: &mut usize) -> &'a [u8] {
|
||||
let first_non_whitespace = bytes
|
||||
.iter()
|
||||
.position(|c| !c.is_ascii_whitespace())
|
||||
.unwrap_or(bytes.len());
|
||||
*index += first_non_whitespace;
|
||||
&bytes[first_non_whitespace..]
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(single_use_lifetimes)] // false positive
|
||||
#[allow(variant_size_differences)]
|
||||
pub(crate) enum FormatItem<'a> {
|
||||
Literal(&'a [u8]),
|
||||
Component(Component),
|
||||
}
|
||||
|
||||
impl ToTokenStream for FormatItem<'_> {
|
||||
fn append_to(self, ts: &mut TokenStream) {
|
||||
quote_append! { ts
|
||||
::time::format_description::FormatItem::#S(match self {
|
||||
FormatItem::Literal(bytes) => quote! { Literal(#(Literal::byte_string(bytes))) },
|
||||
FormatItem::Component(component) => quote! { Component(#S(component)) },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,417 @@
|
|||
use core::mem;
|
||||
|
||||
use proc_macro::{Ident, Span, TokenStream, TokenTree};
|
||||
|
||||
use crate::format_description::error::InvalidFormatDescription;
|
||||
use crate::format_description::helper;
|
||||
use crate::to_tokens::{ToTokenStream, ToTokenTree};
|
||||
|
||||
macro_rules! to_tokens {
|
||||
(
|
||||
$(#[$struct_attr:meta])*
|
||||
$struct_vis:vis struct $struct_name:ident {$(
|
||||
$(#[$field_attr:meta])*
|
||||
$field_vis:vis $field_name:ident : $field_ty:ty
|
||||
),+ $(,)?}
|
||||
) => {
|
||||
$(#[$struct_attr])*
|
||||
$struct_vis struct $struct_name {$(
|
||||
$(#[$field_attr])*
|
||||
$field_vis $field_name: $field_ty
|
||||
),+}
|
||||
|
||||
impl ToTokenTree for $struct_name {
|
||||
fn into_token_tree(self) -> TokenTree {
|
||||
let mut tokens = TokenStream::new();
|
||||
let Self {$($field_name),+} = self;
|
||||
|
||||
quote_append! { tokens
|
||||
let mut value = ::time::format_description::modifier::$struct_name::default();
|
||||
};
|
||||
$(
|
||||
quote_append!(tokens value.$field_name =);
|
||||
$field_name.append_to(&mut tokens);
|
||||
quote_append!(tokens ;);
|
||||
)+
|
||||
quote_append!(tokens value);
|
||||
|
||||
proc_macro::TokenTree::Group(proc_macro::Group::new(
|
||||
proc_macro::Delimiter::Brace,
|
||||
tokens,
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
$(#[$enum_attr:meta])*
|
||||
$enum_vis:vis enum $enum_name:ident {$(
|
||||
$(#[$variant_attr:meta])*
|
||||
$variant_name:ident
|
||||
),+ $(,)?}
|
||||
) => {
|
||||
$(#[$enum_attr])*
|
||||
$enum_vis enum $enum_name {$(
|
||||
$(#[$variant_attr])*
|
||||
$variant_name
|
||||
),+}
|
||||
|
||||
impl ToTokenStream for $enum_name {
|
||||
fn append_to(self, ts: &mut TokenStream) {
|
||||
quote_append! { ts
|
||||
::time::format_description::modifier::$enum_name::
|
||||
};
|
||||
let name = match self {
|
||||
$(Self::$variant_name => stringify!($variant_name)),+
|
||||
};
|
||||
ts.extend([TokenTree::Ident(Ident::new(name, Span::mixed_site()))]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct Day {
|
||||
pub(crate) padding: Padding,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) enum MonthRepr {
|
||||
Numerical,
|
||||
Long,
|
||||
Short,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct Month {
|
||||
pub(crate) padding: Padding,
|
||||
pub(crate) repr: MonthRepr,
|
||||
pub(crate) case_sensitive: bool,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct Ordinal {
|
||||
pub(crate) padding: Padding,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) enum WeekdayRepr {
|
||||
Short,
|
||||
Long,
|
||||
Sunday,
|
||||
Monday,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct Weekday {
|
||||
pub(crate) repr: WeekdayRepr,
|
||||
pub(crate) one_indexed: bool,
|
||||
pub(crate) case_sensitive: bool,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) enum WeekNumberRepr {
|
||||
Iso,
|
||||
Sunday,
|
||||
Monday,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct WeekNumber {
|
||||
pub(crate) padding: Padding,
|
||||
pub(crate) repr: WeekNumberRepr,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) enum YearRepr {
|
||||
Full,
|
||||
LastTwo,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct Year {
|
||||
pub(crate) padding: Padding,
|
||||
pub(crate) repr: YearRepr,
|
||||
pub(crate) iso_week_based: bool,
|
||||
pub(crate) sign_is_mandatory: bool,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct Hour {
|
||||
pub(crate) padding: Padding,
|
||||
pub(crate) is_12_hour_clock: bool,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct Minute {
|
||||
pub(crate) padding: Padding,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct Period {
|
||||
pub(crate) is_uppercase: bool,
|
||||
pub(crate) case_sensitive: bool,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct Second {
|
||||
pub(crate) padding: Padding,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) enum SubsecondDigits {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
Five,
|
||||
Six,
|
||||
Seven,
|
||||
Eight,
|
||||
Nine,
|
||||
OneOrMore,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct Subsecond {
|
||||
pub(crate) digits: SubsecondDigits,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct OffsetHour {
|
||||
pub(crate) sign_is_mandatory: bool,
|
||||
pub(crate) padding: Padding,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct OffsetMinute {
|
||||
pub(crate) padding: Padding,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) struct OffsetSecond {
|
||||
pub(crate) padding: Padding,
|
||||
}
|
||||
}
|
||||
|
||||
to_tokens! {
|
||||
pub(crate) enum Padding {
|
||||
Space,
|
||||
Zero,
|
||||
None,
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_default {
|
||||
($($type:ty => $default:expr;)*) => {$(
|
||||
impl Default for $type {
|
||||
fn default() -> Self {
|
||||
$default
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
impl_default! {
|
||||
Day => Self { padding: Padding::default() };
|
||||
MonthRepr => Self::Numerical;
|
||||
Month => Self {
|
||||
padding: Padding::default(),
|
||||
repr: MonthRepr::default(),
|
||||
case_sensitive: true,
|
||||
};
|
||||
Ordinal => Self { padding: Padding::default() };
|
||||
WeekdayRepr => Self::Long;
|
||||
Weekday => Self {
|
||||
repr: WeekdayRepr::default(),
|
||||
one_indexed: true,
|
||||
case_sensitive: true,
|
||||
};
|
||||
WeekNumberRepr => Self::Iso;
|
||||
WeekNumber => Self {
|
||||
padding: Padding::default(),
|
||||
repr: WeekNumberRepr::default(),
|
||||
};
|
||||
YearRepr => Self::Full;
|
||||
Year => Self {
|
||||
padding: Padding::default(),
|
||||
repr: YearRepr::default(),
|
||||
iso_week_based: false,
|
||||
sign_is_mandatory: false,
|
||||
};
|
||||
Hour => Self {
|
||||
padding: Padding::default(),
|
||||
is_12_hour_clock: false,
|
||||
};
|
||||
Minute => Self { padding: Padding::default() };
|
||||
Period => Self { is_uppercase: true, case_sensitive: true };
|
||||
Second => Self { padding: Padding::default() };
|
||||
SubsecondDigits => Self::OneOrMore;
|
||||
Subsecond => Self { digits: SubsecondDigits::default() };
|
||||
OffsetHour => Self {
|
||||
sign_is_mandatory: true,
|
||||
padding: Padding::default(),
|
||||
};
|
||||
OffsetMinute => Self { padding: Padding::default() };
|
||||
OffsetSecond => Self { padding: Padding::default() };
|
||||
Padding => Self::Zero;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Modifiers {
|
||||
pub(crate) padding: Option<Padding>,
|
||||
pub(crate) hour_is_12_hour_clock: Option<bool>,
|
||||
pub(crate) period_is_uppercase: Option<bool>,
|
||||
pub(crate) month_repr: Option<MonthRepr>,
|
||||
pub(crate) subsecond_digits: Option<SubsecondDigits>,
|
||||
pub(crate) weekday_repr: Option<WeekdayRepr>,
|
||||
pub(crate) weekday_is_one_indexed: Option<bool>,
|
||||
pub(crate) week_number_repr: Option<WeekNumberRepr>,
|
||||
pub(crate) year_repr: Option<YearRepr>,
|
||||
pub(crate) year_is_iso_week_based: Option<bool>,
|
||||
pub(crate) sign_is_mandatory: Option<bool>,
|
||||
pub(crate) case_sensitive: Option<bool>,
|
||||
}
|
||||
|
||||
impl Modifiers {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub(crate) fn parse(
|
||||
component_name: &[u8],
|
||||
mut bytes: &[u8],
|
||||
index: &mut usize,
|
||||
) -> Result<Self, InvalidFormatDescription> {
|
||||
let mut modifiers = Self::default();
|
||||
|
||||
while !bytes.is_empty() {
|
||||
// Trim any whitespace between modifiers.
|
||||
bytes = helper::consume_whitespace(bytes, index);
|
||||
|
||||
let modifier;
|
||||
if let Some(whitespace_loc) = bytes.iter().position(u8::is_ascii_whitespace) {
|
||||
*index += whitespace_loc;
|
||||
modifier = &bytes[..whitespace_loc];
|
||||
bytes = &bytes[whitespace_loc..];
|
||||
} else {
|
||||
modifier = mem::take(&mut bytes);
|
||||
}
|
||||
|
||||
if modifier.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
match (component_name, modifier) {
|
||||
(
|
||||
b"day" | b"hour" | b"minute" | b"month" | b"offset_hour" | b"offset_minute"
|
||||
| b"offset_second" | b"ordinal" | b"second" | b"week_number" | b"year",
|
||||
b"padding:space",
|
||||
) => modifiers.padding = Some(Padding::Space),
|
||||
(
|
||||
b"day" | b"hour" | b"minute" | b"month" | b"offset_hour" | b"offset_minute"
|
||||
| b"offset_second" | b"ordinal" | b"second" | b"week_number" | b"year",
|
||||
b"padding:zero",
|
||||
) => modifiers.padding = Some(Padding::Zero),
|
||||
(
|
||||
b"day" | b"hour" | b"minute" | b"month" | b"offset_hour" | b"offset_minute"
|
||||
| b"offset_second" | b"ordinal" | b"second" | b"week_number" | b"year",
|
||||
b"padding:none",
|
||||
) => modifiers.padding = Some(Padding::None),
|
||||
(b"hour", b"repr:24") => modifiers.hour_is_12_hour_clock = Some(false),
|
||||
(b"hour", b"repr:12") => modifiers.hour_is_12_hour_clock = Some(true),
|
||||
(b"month" | b"period" | b"weekday", b"case_sensitive:true") => {
|
||||
modifiers.case_sensitive = Some(true)
|
||||
}
|
||||
(b"month" | b"period" | b"weekday", b"case_sensitive:false") => {
|
||||
modifiers.case_sensitive = Some(false)
|
||||
}
|
||||
(b"month", b"repr:numerical") => modifiers.month_repr = Some(MonthRepr::Numerical),
|
||||
(b"month", b"repr:long") => modifiers.month_repr = Some(MonthRepr::Long),
|
||||
(b"month", b"repr:short") => modifiers.month_repr = Some(MonthRepr::Short),
|
||||
(b"offset_hour" | b"year", b"sign:automatic") => {
|
||||
modifiers.sign_is_mandatory = Some(false);
|
||||
}
|
||||
(b"offset_hour" | b"year", b"sign:mandatory") => {
|
||||
modifiers.sign_is_mandatory = Some(true);
|
||||
}
|
||||
(b"period", b"case:upper") => modifiers.period_is_uppercase = Some(true),
|
||||
(b"period", b"case:lower") => modifiers.period_is_uppercase = Some(false),
|
||||
(b"subsecond", b"digits:1") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::One);
|
||||
}
|
||||
(b"subsecond", b"digits:2") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Two);
|
||||
}
|
||||
(b"subsecond", b"digits:3") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Three);
|
||||
}
|
||||
(b"subsecond", b"digits:4") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Four);
|
||||
}
|
||||
(b"subsecond", b"digits:5") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Five);
|
||||
}
|
||||
(b"subsecond", b"digits:6") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Six);
|
||||
}
|
||||
(b"subsecond", b"digits:7") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Seven);
|
||||
}
|
||||
(b"subsecond", b"digits:8") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Eight);
|
||||
}
|
||||
(b"subsecond", b"digits:9") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Nine);
|
||||
}
|
||||
(b"subsecond", b"digits:1+") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::OneOrMore);
|
||||
}
|
||||
(b"weekday", b"repr:short") => modifiers.weekday_repr = Some(WeekdayRepr::Short),
|
||||
(b"weekday", b"repr:long") => modifiers.weekday_repr = Some(WeekdayRepr::Long),
|
||||
(b"weekday", b"repr:sunday") => modifiers.weekday_repr = Some(WeekdayRepr::Sunday),
|
||||
(b"weekday", b"repr:monday") => modifiers.weekday_repr = Some(WeekdayRepr::Monday),
|
||||
(b"weekday", b"one_indexed:true") => modifiers.weekday_is_one_indexed = Some(true),
|
||||
(b"weekday", b"one_indexed:false") => {
|
||||
modifiers.weekday_is_one_indexed = Some(false);
|
||||
}
|
||||
(b"week_number", b"repr:iso") => {
|
||||
modifiers.week_number_repr = Some(WeekNumberRepr::Iso);
|
||||
}
|
||||
(b"week_number", b"repr:sunday") => {
|
||||
modifiers.week_number_repr = Some(WeekNumberRepr::Sunday);
|
||||
}
|
||||
(b"week_number", b"repr:monday") => {
|
||||
modifiers.week_number_repr = Some(WeekNumberRepr::Monday);
|
||||
}
|
||||
(b"year", b"repr:full") => modifiers.year_repr = Some(YearRepr::Full),
|
||||
(b"year", b"repr:last_two") => modifiers.year_repr = Some(YearRepr::LastTwo),
|
||||
(b"year", b"base:calendar") => modifiers.year_is_iso_week_based = Some(false),
|
||||
(b"year", b"base:iso_week") => modifiers.year_is_iso_week_based = Some(true),
|
||||
_ => {
|
||||
return Err(InvalidFormatDescription::InvalidModifier {
|
||||
value: String::from_utf8_lossy(modifier).into_owned(),
|
||||
index: *index,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(modifiers)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
use proc_macro::Span;
|
||||
|
||||
use crate::format_description::component::{Component, NakedComponent};
|
||||
use crate::format_description::error::InvalidFormatDescription;
|
||||
use crate::format_description::{helper, modifier, FormatItem};
|
||||
use crate::Error;
|
||||
|
||||
struct ParsedItem<'a> {
|
||||
item: FormatItem<'a>,
|
||||
remaining: &'a [u8],
|
||||
}
|
||||
|
||||
fn parse_component(mut s: &[u8], index: &mut usize) -> Result<Component, InvalidFormatDescription> {
|
||||
s = helper::consume_whitespace(s, index);
|
||||
|
||||
let component_index = *index;
|
||||
let whitespace_loc = s
|
||||
.iter()
|
||||
.position(u8::is_ascii_whitespace)
|
||||
.unwrap_or(s.len());
|
||||
*index += whitespace_loc;
|
||||
let component_name = &s[..whitespace_loc];
|
||||
s = &s[whitespace_loc..];
|
||||
s = helper::consume_whitespace(s, index);
|
||||
|
||||
Ok(NakedComponent::parse(component_name, component_index)?
|
||||
.attach_modifiers(modifier::Modifiers::parse(component_name, s, index)?))
|
||||
}
|
||||
|
||||
fn parse_literal<'a>(s: &'a [u8], index: &mut usize) -> ParsedItem<'a> {
|
||||
let loc = s.iter().position(|&c| c == b'[').unwrap_or(s.len());
|
||||
*index += loc;
|
||||
ParsedItem {
|
||||
item: FormatItem::Literal(&s[..loc]),
|
||||
remaining: &s[loc..],
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_item<'a>(
|
||||
s: &'a [u8],
|
||||
index: &mut usize,
|
||||
) -> Result<ParsedItem<'a>, InvalidFormatDescription> {
|
||||
if let [b'[', b'[', remaining @ ..] = s {
|
||||
*index += 2;
|
||||
return Ok(ParsedItem {
|
||||
item: FormatItem::Literal(&[b'[']),
|
||||
remaining,
|
||||
});
|
||||
};
|
||||
|
||||
if s.starts_with(&[b'[']) {
|
||||
if let Some(bracket_index) = s.iter().position(|&c| c == b']') {
|
||||
*index += 1; // opening bracket
|
||||
let ret_val = ParsedItem {
|
||||
item: FormatItem::Component(parse_component(&s[1..bracket_index], index)?),
|
||||
remaining: &s[bracket_index + 1..],
|
||||
};
|
||||
*index += 1; // closing bracket
|
||||
Ok(ret_val)
|
||||
} else {
|
||||
Err(InvalidFormatDescription::UnclosedOpeningBracket { index: *index })
|
||||
}
|
||||
} else {
|
||||
Ok(parse_literal(s, index))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse(mut s: &[u8], span: Span) -> Result<Vec<FormatItem<'_>>, Error> {
|
||||
let mut compound = Vec::new();
|
||||
let mut loc = 0;
|
||||
|
||||
while !s.is_empty() {
|
||||
let ParsedItem { item, remaining } =
|
||||
parse_item(s, &mut loc).map_err(|error| Error::InvalidFormatDescription {
|
||||
error,
|
||||
span_start: Some(span),
|
||||
span_end: Some(span),
|
||||
})?;
|
||||
s = remaining;
|
||||
compound.push(item);
|
||||
}
|
||||
|
||||
Ok(compound)
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
mod string;
|
||||
|
||||
use std::iter::Peekable;
|
||||
use std::str::FromStr;
|
||||
|
||||
use proc_macro::{token_stream, Span, TokenStream, TokenTree};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
pub(crate) fn get_string_literal(tokens: TokenStream) -> Result<(Span, Vec<u8>), Error> {
|
||||
let mut tokens = tokens.into_iter();
|
||||
|
||||
match (tokens.next(), tokens.next()) {
|
||||
(Some(TokenTree::Literal(literal)), None) => string::parse(&literal),
|
||||
(Some(tree), None) => Err(Error::ExpectedString {
|
||||
span_start: Some(tree.span()),
|
||||
span_end: Some(tree.span()),
|
||||
}),
|
||||
(_, Some(tree)) => Err(Error::UnexpectedToken { tree }),
|
||||
(None, None) => Err(Error::ExpectedString {
|
||||
span_start: None,
|
||||
span_end: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn consume_number<T: FromStr>(
|
||||
component_name: &'static str,
|
||||
chars: &mut Peekable<token_stream::IntoIter>,
|
||||
) -> Result<(Span, T), Error> {
|
||||
let (span, digits) = match chars.next() {
|
||||
Some(TokenTree::Literal(literal)) => (literal.span(), literal.to_string()),
|
||||
Some(tree) => return Err(Error::UnexpectedToken { tree }),
|
||||
None => return Err(Error::UnexpectedEndOfInput),
|
||||
};
|
||||
|
||||
if let Ok(value) = digits.replace('_', "").parse() {
|
||||
Ok((span, value))
|
||||
} else {
|
||||
Err(Error::InvalidComponent {
|
||||
name: component_name,
|
||||
value: digits,
|
||||
span_start: Some(span),
|
||||
span_end: Some(span),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn consume_any_ident(
|
||||
idents: &[&str],
|
||||
chars: &mut Peekable<token_stream::IntoIter>,
|
||||
) -> Result<Span, Error> {
|
||||
match chars.peek() {
|
||||
Some(TokenTree::Ident(char)) if idents.contains(&char.to_string().as_str()) => {
|
||||
let ret = Ok(char.span());
|
||||
drop(chars.next());
|
||||
ret
|
||||
}
|
||||
Some(tree) => Err(Error::UnexpectedToken { tree: tree.clone() }),
|
||||
None => Err(Error::UnexpectedEndOfInput),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn consume_punct(
|
||||
c: char,
|
||||
chars: &mut Peekable<token_stream::IntoIter>,
|
||||
) -> Result<Span, Error> {
|
||||
match chars.peek() {
|
||||
Some(TokenTree::Punct(punct)) if *punct == c => {
|
||||
let ret = Ok(punct.span());
|
||||
drop(chars.next());
|
||||
ret
|
||||
}
|
||||
Some(tree) => Err(Error::UnexpectedToken { tree: tree.clone() }),
|
||||
None => Err(Error::UnexpectedEndOfInput),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_leap_year(year: i32) -> bool {
|
||||
(year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0))
|
||||
}
|
||||
|
||||
fn jan_weekday(year: i32, ordinal: i32) -> u8 {
|
||||
macro_rules! div_floor {
|
||||
($a:expr, $b:expr) => {{
|
||||
let (_quotient, _remainder) = ($a / $b, $a % $b);
|
||||
if (_remainder > 0 && $b < 0) || (_remainder < 0 && $b > 0) {
|
||||
_quotient - 1
|
||||
} else {
|
||||
_quotient
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
let adj_year = year - 1;
|
||||
((ordinal + adj_year + div_floor!(adj_year, 4) - div_floor!(adj_year, 100)
|
||||
+ div_floor!(adj_year, 400)
|
||||
+ 6)
|
||||
.rem_euclid(7)) as _
|
||||
}
|
||||
|
||||
pub(crate) fn days_in_year(year: i32) -> u16 {
|
||||
365 + is_leap_year(year) as u16
|
||||
}
|
||||
|
||||
pub(crate) fn days_in_year_month(year: i32, month: u8) -> u8 {
|
||||
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month as usize - 1]
|
||||
+ (month == 2 && is_leap_year(year)) as u8
|
||||
}
|
||||
|
||||
pub(crate) fn weeks_in_year(year: i32) -> u8 {
|
||||
52 + (jan_weekday(year, 1) + is_leap_year(year) as u8 == 3) as u8
|
||||
}
|
||||
|
||||
pub(crate) fn ywd_to_yo(year: i32, week: u8, iso_weekday_number: u8) -> (i32, u16) {
|
||||
let (ordinal, overflow) = (u16::from(week) * 7 + u16::from(iso_weekday_number))
|
||||
.overflowing_sub(u16::from(jan_weekday(year, 4)) + 4);
|
||||
|
||||
if overflow || ordinal == 0 {
|
||||
return (year - 1, (ordinal.wrapping_add(days_in_year(year - 1))));
|
||||
}
|
||||
|
||||
let days_in_cur_year = days_in_year(year);
|
||||
if ordinal > days_in_cur_year {
|
||||
(year + 1, ordinal - days_in_cur_year)
|
||||
} else {
|
||||
(year, ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ymd_to_yo(year: i32, month: u8, day: u8) -> (i32, u16) {
|
||||
let ordinal = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334][month as usize - 1]
|
||||
+ (month > 2 && is_leap_year(year)) as u16;
|
||||
|
||||
(year, ordinal + u16::from(day))
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
use std::ops::{Index, RangeFrom};
|
||||
|
||||
use proc_macro::Span;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
pub(crate) fn parse(token: &proc_macro::Literal) -> Result<(Span, Vec<u8>), Error> {
|
||||
let span = token.span();
|
||||
let repr = token.to_string();
|
||||
|
||||
match repr.as_bytes() {
|
||||
[b'"', ..] => Ok((span, parse_lit_str_cooked(&repr[1..]))),
|
||||
[b'b', b'"', rest @ ..] => Ok((span, parse_lit_byte_str_cooked(rest))),
|
||||
[b'r', rest @ ..] | [b'b', b'r', rest @ ..] => Ok((span, parse_lit_str_raw(rest))),
|
||||
_ => Err(Error::ExpectedString {
|
||||
span_start: Some(span),
|
||||
span_end: Some(span),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn byte(s: impl AsRef<[u8]>, idx: usize) -> u8 {
|
||||
s.as_ref().get(idx).copied().unwrap_or_default()
|
||||
}
|
||||
|
||||
fn parse_lit_str_cooked(mut s: &str) -> Vec<u8> {
|
||||
let mut content = String::new();
|
||||
'outer: loop {
|
||||
let ch = match byte(s, 0) {
|
||||
b'"' => break,
|
||||
b'\\' => {
|
||||
let b = byte(s, 1);
|
||||
s = &s[2..];
|
||||
match b {
|
||||
b'x' => {
|
||||
let (byte, rest) = backslash_x(s);
|
||||
s = rest;
|
||||
char::from_u32(u32::from(byte)).expect("byte was just validated")
|
||||
}
|
||||
b'u' => {
|
||||
let (chr, rest) = backslash_u(s);
|
||||
s = rest;
|
||||
chr
|
||||
}
|
||||
b'n' => '\n',
|
||||
b'r' => '\r',
|
||||
b't' => '\t',
|
||||
b'\\' => '\\',
|
||||
b'0' => '\0',
|
||||
b'\'' => '\'',
|
||||
b'"' => '"',
|
||||
b'\r' | b'\n' => loop {
|
||||
let ch = s.chars().next().unwrap_or_default();
|
||||
if ch.is_whitespace() {
|
||||
s = &s[ch.len_utf8()..];
|
||||
} else {
|
||||
continue 'outer;
|
||||
}
|
||||
},
|
||||
_ => unreachable!("invalid escape"),
|
||||
}
|
||||
}
|
||||
b'\r' => {
|
||||
// bare CR not permitted
|
||||
s = &s[2..];
|
||||
'\n'
|
||||
}
|
||||
_ => {
|
||||
let ch = s.chars().next().unwrap_or_default();
|
||||
s = &s[ch.len_utf8()..];
|
||||
ch
|
||||
}
|
||||
};
|
||||
content.push(ch);
|
||||
}
|
||||
|
||||
content.into_bytes()
|
||||
}
|
||||
|
||||
fn parse_lit_str_raw(s: &[u8]) -> Vec<u8> {
|
||||
let mut pounds = 0;
|
||||
while byte(s, pounds) == b'#' {
|
||||
pounds += 1;
|
||||
}
|
||||
let close = s
|
||||
.iter()
|
||||
.rposition(|&b| b == b'"')
|
||||
.expect("had a string without trailing \"");
|
||||
|
||||
s[pounds + 1..close].to_owned()
|
||||
}
|
||||
|
||||
fn parse_lit_byte_str_cooked(mut v: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
'outer: loop {
|
||||
let byte = match byte(v, 0) {
|
||||
b'"' => break,
|
||||
b'\\' => {
|
||||
let b = byte(v, 1);
|
||||
v = &v[2..];
|
||||
match b {
|
||||
b'x' => {
|
||||
let (byte, rest) = backslash_x(v);
|
||||
v = rest;
|
||||
byte
|
||||
}
|
||||
b'n' => b'\n',
|
||||
b'r' => b'\r',
|
||||
b't' => b'\t',
|
||||
b'\\' => b'\\',
|
||||
b'0' => b'\0',
|
||||
b'\'' => b'\'',
|
||||
b'"' => b'"',
|
||||
b'\r' | b'\n' => loop {
|
||||
let byte = byte(v, 0);
|
||||
let ch = char::from_u32(u32::from(byte)).expect("invalid byte");
|
||||
if ch.is_whitespace() {
|
||||
v = &v[1..];
|
||||
} else {
|
||||
continue 'outer;
|
||||
}
|
||||
},
|
||||
_ => unreachable!("invalid escape"),
|
||||
}
|
||||
}
|
||||
b'\r' => {
|
||||
// bare CR not permitted
|
||||
v = &v[2..];
|
||||
b'\n'
|
||||
}
|
||||
b => {
|
||||
v = &v[1..];
|
||||
b
|
||||
}
|
||||
};
|
||||
out.push(byte);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn backslash_x<S>(s: &S) -> (u8, &S)
|
||||
where
|
||||
S: Index<RangeFrom<usize>, Output = S> + AsRef<[u8]> + ?Sized,
|
||||
{
|
||||
let mut ch = 0;
|
||||
let b0 = byte(s, 0);
|
||||
let b1 = byte(s, 1);
|
||||
ch += 0x10 * (b0 - b'0');
|
||||
ch += match b1 {
|
||||
b'0'..=b'9' => b1 - b'0',
|
||||
b'a'..=b'f' => 10 + (b1 - b'a'),
|
||||
b'A'..=b'F' => 10 + (b1 - b'A'),
|
||||
_ => unreachable!("invalid hex escape"),
|
||||
};
|
||||
(ch, &s[2..])
|
||||
}
|
||||
|
||||
fn backslash_u(mut s: &str) -> (char, &str) {
|
||||
s = &s[1..];
|
||||
|
||||
let mut ch = 0;
|
||||
let mut digits = 0;
|
||||
loop {
|
||||
let b = byte(s, 0);
|
||||
let digit = match b {
|
||||
b'0'..=b'9' => b - b'0',
|
||||
b'a'..=b'f' => 10 + b - b'a',
|
||||
b'A'..=b'F' => 10 + b - b'A',
|
||||
b'_' if digits > 0 => {
|
||||
s = &s[1..];
|
||||
continue;
|
||||
}
|
||||
b'}' if digits != 0 => break,
|
||||
_ => unreachable!("invalid unicode escape"),
|
||||
};
|
||||
ch *= 0x10;
|
||||
ch += u32::from(digit);
|
||||
digits += 1;
|
||||
s = &s[1..];
|
||||
}
|
||||
s = &s[1..];
|
||||
|
||||
(
|
||||
char::from_u32(ch).expect("invalid unicode escape passed by compiler"),
|
||||
s,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
#![deny(
|
||||
anonymous_parameters,
|
||||
clippy::all,
|
||||
const_err,
|
||||
illegal_floating_point_literal_pattern,
|
||||
late_bound_lifetime_arguments,
|
||||
path_statements,
|
||||
patterns_in_fns_without_body,
|
||||
rust_2018_idioms,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unreachable_pub,
|
||||
unsafe_code,
|
||||
unused_extern_crates
|
||||
)]
|
||||
#![warn(
|
||||
clippy::dbg_macro,
|
||||
clippy::decimal_literal_representation,
|
||||
clippy::get_unwrap,
|
||||
clippy::nursery,
|
||||
clippy::print_stdout,
|
||||
clippy::todo,
|
||||
clippy::unimplemented,
|
||||
clippy::unnested_or_patterns,
|
||||
clippy::unwrap_used,
|
||||
clippy::use_debug,
|
||||
single_use_lifetimes,
|
||||
unused_qualifications,
|
||||
variant_size_differences
|
||||
)]
|
||||
#![allow(clippy::missing_const_for_fn, clippy::redundant_pub_crate)]
|
||||
|
||||
#[macro_use]
|
||||
mod quote;
|
||||
|
||||
mod date;
|
||||
mod datetime;
|
||||
mod error;
|
||||
mod format_description;
|
||||
mod helpers;
|
||||
mod offset;
|
||||
mod serde_format_description;
|
||||
mod time;
|
||||
mod to_tokens;
|
||||
|
||||
use proc_macro::{TokenStream, TokenTree};
|
||||
|
||||
use self::error::Error;
|
||||
|
||||
macro_rules! impl_macros {
|
||||
($($name:ident)*) => {$(
|
||||
#[proc_macro]
|
||||
pub fn $name(input: TokenStream) -> TokenStream {
|
||||
use crate::to_tokens::ToTokenTree;
|
||||
|
||||
let mut iter = input.into_iter().peekable();
|
||||
match $name::parse(&mut iter) {
|
||||
Ok(value) => match iter.peek() {
|
||||
Some(tree) => Error::UnexpectedToken { tree: tree.clone() }.to_compile_error(),
|
||||
None => TokenStream::from(value.into_token_tree()),
|
||||
},
|
||||
Err(err) => err.to_compile_error(),
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
impl_macros![date datetime offset time];
|
||||
|
||||
// TODO Gate this behind the the `formatting` or `parsing` feature flag when weak dependency
|
||||
// features land.
|
||||
#[proc_macro]
|
||||
pub fn format_description(input: TokenStream) -> TokenStream {
|
||||
(|| {
|
||||
let (span, string) = helpers::get_string_literal(input)?;
|
||||
let items = format_description::parse(&string, span)?;
|
||||
|
||||
Ok(quote! {{
|
||||
const DESCRIPTION: &[::time::format_description::FormatItem<'_>] = &[#S(
|
||||
items
|
||||
.into_iter()
|
||||
.map(|item| quote! { #S(item), })
|
||||
.collect::<TokenStream>()
|
||||
)];
|
||||
DESCRIPTION
|
||||
}})
|
||||
})()
|
||||
.unwrap_or_else(|err: Error| err.to_compile_error())
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn serde_format_description(input: TokenStream) -> TokenStream {
|
||||
(|| {
|
||||
let mut tokens = input.into_iter().peekable();
|
||||
// First, an identifier (the desired module name)
|
||||
let mod_name = match tokens.next() {
|
||||
Some(TokenTree::Ident(ident)) => Ok(ident),
|
||||
Some(tree) => Err(Error::UnexpectedToken { tree }),
|
||||
None => Err(Error::UnexpectedEndOfInput),
|
||||
}?;
|
||||
|
||||
// Followed by a comma
|
||||
helpers::consume_punct(',', &mut tokens)?;
|
||||
|
||||
// Then, the type to create serde serializers for (e.g., `OffsetDateTime`).
|
||||
let formattable = match tokens.next() {
|
||||
Some(tree @ TokenTree::Ident(_)) => Ok(tree),
|
||||
Some(tree) => Err(Error::UnexpectedToken { tree }),
|
||||
None => Err(Error::UnexpectedEndOfInput),
|
||||
}?;
|
||||
|
||||
// Another comma
|
||||
helpers::consume_punct(',', &mut tokens)?;
|
||||
|
||||
// Then, a string literal.
|
||||
let (span, format_string) = helpers::get_string_literal(tokens.collect())?;
|
||||
|
||||
let items = format_description::parse(&format_string, span)?;
|
||||
let items: TokenStream = items.into_iter().map(|item| quote! { #S(item), }).collect();
|
||||
|
||||
Ok(serde_format_description::build(
|
||||
mod_name,
|
||||
items,
|
||||
formattable,
|
||||
&String::from_utf8_lossy(&format_string),
|
||||
))
|
||||
})()
|
||||
.unwrap_or_else(|err: Error| err.to_compile_error_standalone())
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
use std::iter::Peekable;
|
||||
|
||||
use proc_macro::{token_stream, Span, TokenTree};
|
||||
|
||||
use crate::helpers::{consume_any_ident, consume_number, consume_punct};
|
||||
use crate::to_tokens::ToTokenTree;
|
||||
use crate::Error;
|
||||
|
||||
pub(crate) struct Offset {
|
||||
pub(crate) hours: i8,
|
||||
pub(crate) minutes: i8,
|
||||
pub(crate) seconds: i8,
|
||||
}
|
||||
|
||||
pub(crate) fn parse(chars: &mut Peekable<token_stream::IntoIter>) -> Result<Offset, Error> {
|
||||
if consume_any_ident(&["utc", "UTC"], chars).is_ok() {
|
||||
return Ok(Offset {
|
||||
hours: 0,
|
||||
minutes: 0,
|
||||
seconds: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let sign = if consume_punct('+', chars).is_ok() {
|
||||
1
|
||||
} else if consume_punct('-', chars).is_ok() {
|
||||
-1
|
||||
} else if let Some(tree) = chars.next() {
|
||||
return Err(Error::UnexpectedToken { tree });
|
||||
} else {
|
||||
return Err(Error::MissingComponent {
|
||||
name: "sign",
|
||||
span_start: None,
|
||||
span_end: None,
|
||||
});
|
||||
};
|
||||
|
||||
let (hours_span, hours) = consume_number::<i8>("hour", chars)?;
|
||||
let (mut minutes_span, mut minutes) = (Span::mixed_site(), 0);
|
||||
let (mut seconds_span, mut seconds) = (Span::mixed_site(), 0);
|
||||
|
||||
if consume_punct(':', chars).is_ok() {
|
||||
let min = consume_number::<i8>("minute", chars)?;
|
||||
minutes_span = min.0;
|
||||
minutes = min.1;
|
||||
|
||||
if consume_punct(':', chars).is_ok() {
|
||||
let sec = consume_number::<i8>("second", chars)?;
|
||||
seconds_span = sec.0;
|
||||
seconds = sec.1;
|
||||
}
|
||||
}
|
||||
|
||||
if hours >= 24 {
|
||||
Err(Error::InvalidComponent {
|
||||
name: "hour",
|
||||
value: hours.to_string(),
|
||||
span_start: Some(hours_span),
|
||||
span_end: Some(hours_span),
|
||||
})
|
||||
} else if minutes >= 60 {
|
||||
Err(Error::InvalidComponent {
|
||||
name: "minute",
|
||||
value: minutes.to_string(),
|
||||
span_start: Some(minutes_span),
|
||||
span_end: Some(minutes_span),
|
||||
})
|
||||
} else if seconds >= 60 {
|
||||
Err(Error::InvalidComponent {
|
||||
name: "second",
|
||||
value: seconds.to_string(),
|
||||
span_start: Some(seconds_span),
|
||||
span_end: Some(seconds_span),
|
||||
})
|
||||
} else {
|
||||
Ok(Offset {
|
||||
hours: sign * hours,
|
||||
minutes: sign * minutes,
|
||||
seconds: sign * seconds,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokenTree for Offset {
|
||||
fn into_token_tree(self) -> TokenTree {
|
||||
quote_group! {{
|
||||
const OFFSET: ::time::UtcOffset = ::time::UtcOffset::__from_hms_unchecked(
|
||||
#(self.hours),
|
||||
#(self.minutes),
|
||||
#(self.seconds),
|
||||
);
|
||||
OFFSET
|
||||
}}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
macro_rules! quote {
|
||||
() => (::proc_macro::TokenStream::new());
|
||||
($($x:tt)*) => {{
|
||||
let mut ts = ::proc_macro::TokenStream::new();
|
||||
let ts_mut = &mut ts;
|
||||
quote_inner!(ts_mut $($x)*);
|
||||
ts
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! quote_append {
|
||||
($ts:ident $($x:tt)*) => {{
|
||||
quote_inner!($ts $($x)*);
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! quote_group {
|
||||
({ $($x:tt)* }) => {
|
||||
::proc_macro::TokenTree::Group(::proc_macro::Group::new(
|
||||
::proc_macro::Delimiter::Brace,
|
||||
quote!($($x)*)
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! sym {
|
||||
($ts:ident $x:tt $y:tt) => {
|
||||
$ts.extend([
|
||||
::proc_macro::TokenTree::from(::proc_macro::Punct::new(
|
||||
$x,
|
||||
::proc_macro::Spacing::Joint,
|
||||
)),
|
||||
::proc_macro::TokenTree::from(::proc_macro::Punct::new(
|
||||
$y,
|
||||
::proc_macro::Spacing::Alone,
|
||||
)),
|
||||
]);
|
||||
};
|
||||
($ts:ident $x:tt) => {
|
||||
$ts.extend([::proc_macro::TokenTree::from(::proc_macro::Punct::new(
|
||||
$x,
|
||||
::proc_macro::Spacing::Alone,
|
||||
))]);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! quote_inner {
|
||||
// Base case
|
||||
($ts:ident) => {};
|
||||
|
||||
// Single or double symbols
|
||||
($ts:ident :: $($tail:tt)*) => { sym!($ts ':' ':'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident .. $($tail:tt)*) => { sym!($ts '.' '.'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident : $($tail:tt)*) => { sym!($ts ':'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident = $($tail:tt)*) => { sym!($ts '='); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident ; $($tail:tt)*) => { sym!($ts ';'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident , $($tail:tt)*) => { sym!($ts ','); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident . $($tail:tt)*) => { sym!($ts '.'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident & $($tail:tt)*) => { sym!($ts '&'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident << $($tail:tt)*) => { sym!($ts '<' '<'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident < $($tail:tt)*) => { sym!($ts '<'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident >> $($tail:tt)*) => { sym!($ts '>' '>'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident > $($tail:tt)*) => { sym!($ts '>'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident -> $($tail:tt)*) => { sym!($ts '-' '>'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident ? $($tail:tt)*) => { sym!($ts '?'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident ! $($tail:tt)*) => { sym!($ts '!'); quote_inner!($ts $($tail)*); };
|
||||
($ts:ident | $($tail:tt)*) => { sym!($ts '|'); quote_inner!($ts $($tail)*); };
|
||||
|
||||
// Identifier
|
||||
($ts:ident $i:ident $($tail:tt)*) => {
|
||||
$ts.extend([::proc_macro::TokenTree::from(::proc_macro::Ident::new(
|
||||
&stringify!($i),
|
||||
::proc_macro::Span::mixed_site(),
|
||||
))]);
|
||||
quote_inner!($ts $($tail)*);
|
||||
};
|
||||
|
||||
// Literal
|
||||
($ts:ident $l:literal $($tail:tt)*) => {
|
||||
$ts.extend([::proc_macro::TokenTree::from(::proc_macro::Literal::string(&$l))]);
|
||||
quote_inner!($ts $($tail)*);
|
||||
};
|
||||
|
||||
// Lifetime
|
||||
($ts:ident $l:lifetime $($tail:tt)*) => {
|
||||
$ts.extend([
|
||||
::proc_macro::TokenTree::from(
|
||||
::proc_macro::Punct::new('\'', ::proc_macro::Spacing::Joint)
|
||||
),
|
||||
::proc_macro::TokenTree::from(::proc_macro::Ident::new(
|
||||
stringify!($l).trim_start_matches(|c| c == '\''),
|
||||
::proc_macro::Span::mixed_site(),
|
||||
)),
|
||||
]);
|
||||
quote_inner!($ts $($tail)*);
|
||||
};
|
||||
|
||||
// Groups
|
||||
($ts:ident ($($inner:tt)*) $($tail:tt)*) => {
|
||||
$ts.extend([::proc_macro::TokenTree::Group(::proc_macro::Group::new(
|
||||
::proc_macro::Delimiter::Parenthesis,
|
||||
quote!($($inner)*)
|
||||
))]);
|
||||
quote_inner!($ts $($tail)*);
|
||||
};
|
||||
($ts:ident [$($inner:tt)*] $($tail:tt)*) => {
|
||||
$ts.extend([::proc_macro::TokenTree::Group(::proc_macro::Group::new(
|
||||
::proc_macro::Delimiter::Bracket,
|
||||
quote!($($inner)*)
|
||||
))]);
|
||||
quote_inner!($ts $($tail)*);
|
||||
};
|
||||
($ts:ident {$($inner:tt)*} $($tail:tt)*) => {
|
||||
$ts.extend([::proc_macro::TokenTree::Group(::proc_macro::Group::new(
|
||||
::proc_macro::Delimiter::Brace,
|
||||
quote!($($inner)*)
|
||||
))]);
|
||||
quote_inner!($ts $($tail)*);
|
||||
};
|
||||
|
||||
// Interpolated values
|
||||
// TokenTree by default
|
||||
($ts:ident #($e:expr) $($tail:tt)*) => {
|
||||
$ts.extend([$crate::to_tokens::ToTokenTree::into_token_tree($e)]);
|
||||
quote_inner!($ts $($tail)*);
|
||||
};
|
||||
// Allow a TokenStream by request. It's more expensive, so avoid if possible.
|
||||
($ts:ident #S($e:expr) $($tail:tt)*) => {
|
||||
$crate::to_tokens::ToTokenStream::append_to($e, $ts);
|
||||
quote_inner!($ts $($tail)*);
|
||||
};
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
use proc_macro::{Ident, TokenStream, TokenTree};
|
||||
|
||||
use crate::to_tokens;
|
||||
|
||||
pub(crate) fn build(
|
||||
mod_name: Ident,
|
||||
items: impl to_tokens::ToTokenStream,
|
||||
ty: TokenTree,
|
||||
format_string: &str,
|
||||
) -> TokenStream {
|
||||
let ty_s = &*ty.to_string();
|
||||
|
||||
let visitor = quote! {
|
||||
struct Visitor;
|
||||
struct OptionVisitor;
|
||||
|
||||
impl<'a> ::serde::de::Visitor<'a> for Visitor {
|
||||
type Value = __TimeSerdeType;
|
||||
|
||||
fn expecting(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
concat!(
|
||||
"a(n) `",
|
||||
#(ty_s),
|
||||
"` in the format \"",
|
||||
#(format_string),
|
||||
"\"",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_str<E: ::serde::de::Error>(
|
||||
self,
|
||||
value: &str
|
||||
) -> Result<__TimeSerdeType, E> {
|
||||
__TimeSerdeType::parse(value, &DESCRIPTION).map_err(E::custom)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ::serde::de::Visitor<'a> for OptionVisitor {
|
||||
type Value = Option<__TimeSerdeType>;
|
||||
|
||||
fn expecting(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
concat!(
|
||||
"an `Option<",
|
||||
#(ty_s),
|
||||
">` in the format \"",
|
||||
#(format_string),
|
||||
"\"",
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_some<D: ::serde::de::Deserializer<'a>>(
|
||||
self,
|
||||
deserializer: D
|
||||
) -> Result<Option<__TimeSerdeType>, D::Error> {
|
||||
deserializer
|
||||
.deserialize_any(Visitor)
|
||||
.map(Some)
|
||||
}
|
||||
|
||||
fn visit_none<E: ::serde::de::Error>(
|
||||
self
|
||||
) -> Result<Option<__TimeSerdeType>, E> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
let primary_fns = quote! {
|
||||
pub fn serialize<S: ::serde::Serializer>(
|
||||
datetime: &__TimeSerdeType,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
use ::serde::Serialize;
|
||||
datetime
|
||||
.format(&DESCRIPTION)
|
||||
.map_err(::time::error::Format::into_invalid_serde_value::<S>)?
|
||||
.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'a, D: ::serde::Deserializer<'a>>(
|
||||
deserializer: D
|
||||
) -> Result<__TimeSerdeType, D::Error> {
|
||||
use ::serde::Deserialize;
|
||||
deserializer.deserialize_any(Visitor)
|
||||
}
|
||||
};
|
||||
|
||||
let options_fns = quote! {
|
||||
pub fn serialize<S: ::serde::Serializer>(
|
||||
option: &Option<__TimeSerdeType>,
|
||||
serializer: S,
|
||||
) -> Result<S::Ok, S::Error> {
|
||||
use ::serde::Serialize;
|
||||
option.map(|datetime| datetime.format(&DESCRIPTION))
|
||||
.transpose()
|
||||
.map_err(::time::error::Format::into_invalid_serde_value::<S>)?
|
||||
.serialize(serializer)
|
||||
}
|
||||
|
||||
pub fn deserialize<'a, D: ::serde::Deserializer<'a>>(
|
||||
deserializer: D
|
||||
) -> Result<Option<__TimeSerdeType>, D::Error> {
|
||||
use ::serde::Deserialize;
|
||||
deserializer.deserialize_option(OptionVisitor)
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
mod #(mod_name) {
|
||||
use ::time::#(ty) as __TimeSerdeType;
|
||||
|
||||
const DESCRIPTION: &[::time::format_description::FormatItem<'_>] = &[#S(items)];
|
||||
|
||||
#S(visitor)
|
||||
#S(primary_fns)
|
||||
|
||||
pub(super) mod option {
|
||||
use super::{DESCRIPTION, OptionVisitor, Visitor, __TimeSerdeType};
|
||||
|
||||
#S(options_fns)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
use std::iter::Peekable;
|
||||
|
||||
use proc_macro::{token_stream, Span, TokenTree};
|
||||
|
||||
use crate::helpers::{consume_any_ident, consume_number, consume_punct};
|
||||
use crate::to_tokens::ToTokenTree;
|
||||
use crate::Error;
|
||||
|
||||
enum Period {
|
||||
Am,
|
||||
Pm,
|
||||
_24,
|
||||
}
|
||||
|
||||
pub(crate) struct Time {
|
||||
pub(crate) hour: u8,
|
||||
pub(crate) minute: u8,
|
||||
pub(crate) second: u8,
|
||||
pub(crate) nanosecond: u32,
|
||||
}
|
||||
|
||||
pub(crate) fn parse(chars: &mut Peekable<token_stream::IntoIter>) -> Result<Time, Error> {
|
||||
fn consume_period(chars: &mut Peekable<token_stream::IntoIter>) -> (Option<Span>, Period) {
|
||||
if let Ok(span) = consume_any_ident(&["am", "AM"], chars) {
|
||||
(Some(span), Period::Am)
|
||||
} else if let Ok(span) = consume_any_ident(&["pm", "PM"], chars) {
|
||||
(Some(span), Period::Pm)
|
||||
} else {
|
||||
(None, Period::_24)
|
||||
}
|
||||
}
|
||||
|
||||
let (hour_span, hour) = consume_number("hour", chars)?;
|
||||
|
||||
let ((minute_span, minute), (second_span, second), (period_span, period)) =
|
||||
match consume_period(chars) {
|
||||
// Nothing but the 12-hour clock hour and AM/PM
|
||||
(period_span @ Some(_), period) => (
|
||||
(Span::mixed_site(), 0),
|
||||
(Span::mixed_site(), 0.),
|
||||
(period_span, period),
|
||||
),
|
||||
(None, _) => {
|
||||
consume_punct(':', chars)?;
|
||||
let (minute_span, minute) = consume_number::<u8>("minute", chars)?;
|
||||
let (second_span, second): (_, f64) = if consume_punct(':', chars).is_ok() {
|
||||
consume_number("second", chars)?
|
||||
} else {
|
||||
(Span::mixed_site(), 0.)
|
||||
};
|
||||
let (period_span, period) = consume_period(chars);
|
||||
(
|
||||
(minute_span, minute),
|
||||
(second_span, second),
|
||||
(period_span, period),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let hour = match (hour, period) {
|
||||
(0, Period::Am | Period::Pm) => {
|
||||
return Err(Error::InvalidComponent {
|
||||
name: "hour",
|
||||
value: hour.to_string(),
|
||||
span_start: Some(hour_span),
|
||||
span_end: Some(period_span.unwrap_or(hour_span)),
|
||||
});
|
||||
}
|
||||
(12, Period::Am) => 0,
|
||||
(12, Period::Pm) => 12,
|
||||
(hour, Period::Am | Period::_24) => hour,
|
||||
(hour, Period::Pm) => hour + 12,
|
||||
};
|
||||
|
||||
if hour >= 24 {
|
||||
Err(Error::InvalidComponent {
|
||||
name: "hour",
|
||||
value: hour.to_string(),
|
||||
span_start: Some(hour_span),
|
||||
span_end: Some(period_span.unwrap_or(hour_span)),
|
||||
})
|
||||
} else if minute >= 60 {
|
||||
Err(Error::InvalidComponent {
|
||||
name: "minute",
|
||||
value: minute.to_string(),
|
||||
span_start: Some(minute_span),
|
||||
span_end: Some(minute_span),
|
||||
})
|
||||
} else if second >= 60. {
|
||||
Err(Error::InvalidComponent {
|
||||
name: "second",
|
||||
value: second.to_string(),
|
||||
span_start: Some(second_span),
|
||||
span_end: Some(second_span),
|
||||
})
|
||||
} else {
|
||||
Ok(Time {
|
||||
hour,
|
||||
minute,
|
||||
second: second.trunc() as _,
|
||||
nanosecond: (second.fract() * 1_000_000_000.).round() as _,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokenTree for Time {
|
||||
fn into_token_tree(self) -> TokenTree {
|
||||
quote_group! {{
|
||||
const TIME: ::time::Time = ::time::Time::__from_hms_nanos_unchecked(
|
||||
#(self.hour),
|
||||
#(self.minute),
|
||||
#(self.second),
|
||||
#(self.nanosecond),
|
||||
);
|
||||
TIME
|
||||
}}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
use proc_macro::{Group, Ident, Literal, Punct, Span, TokenStream, TokenTree};
|
||||
|
||||
pub(crate) trait ToTokenStream: Sized {
|
||||
fn append_to(self, ts: &mut TokenStream);
|
||||
}
|
||||
|
||||
pub(crate) trait ToTokenTree: Sized {
|
||||
fn into_token_tree(self) -> TokenTree;
|
||||
}
|
||||
|
||||
impl<T: ToTokenTree> ToTokenStream for T {
|
||||
fn append_to(self, ts: &mut TokenStream) {
|
||||
ts.extend([self.into_token_tree()])
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokenTree for bool {
|
||||
fn into_token_tree(self) -> TokenTree {
|
||||
let lit = if self { "true" } else { "false" };
|
||||
TokenTree::Ident(Ident::new(lit, Span::mixed_site()))
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokenStream for TokenStream {
|
||||
fn append_to(self, ts: &mut TokenStream) {
|
||||
ts.extend(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokenTree for TokenTree {
|
||||
fn into_token_tree(self) -> TokenTree {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokenTree for &str {
|
||||
fn into_token_tree(self) -> TokenTree {
|
||||
TokenTree::Literal(Literal::string(self))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_for_tree_types {
|
||||
($($type:ty)*) => {$(
|
||||
impl ToTokenTree for $type {
|
||||
fn into_token_tree(self) -> TokenTree {
|
||||
TokenTree::from(self)
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
impl_for_tree_types![Ident Literal Group Punct];
|
||||
|
||||
macro_rules! impl_for_int {
|
||||
($($type:ty => $method:ident)*) => {$(
|
||||
impl ToTokenTree for $type {
|
||||
fn into_token_tree(self) -> TokenTree {
|
||||
TokenTree::from(Literal::$method(self))
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
impl_for_int! {
|
||||
i8 => i8_unsuffixed
|
||||
u8 => u8_unsuffixed
|
||||
u16 => u16_unsuffixed
|
||||
i32 => i32_unsuffixed
|
||||
u32 => u32_unsuffixed
|
||||
}
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -3,36 +3,102 @@
|
|||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you believe there's an error in this file please file an
|
||||
# issue against the rust-lang/cargo repository. If you're
|
||||
# editing this file be aware that the upstream Cargo.toml
|
||||
# will likely look very different (and much more reasonable)
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2018"
|
||||
name = "time"
|
||||
version = "0.1.43"
|
||||
authors = ["The Rust Project Developers"]
|
||||
exclude = [".github", "benches"]
|
||||
description = "Utilities for working with time-related functions in Rust.\n"
|
||||
homepage = "https://github.com/time-rs/time"
|
||||
documentation = "https://docs.rs/time/~0.1"
|
||||
version = "0.3.9"
|
||||
authors = ["Jacob Pratt <open-source@jhpratt.dev>", "Time contributors"]
|
||||
include = ["src/**/*", "LICENSE-*", "README.md", "!src/tests.rs"]
|
||||
description = "Date and time library. Fully interoperable with the standard library. Mostly compatible with #![no_std]."
|
||||
homepage = "https://time-rs.github.io"
|
||||
readme = "README.md"
|
||||
license = "MIT/Apache-2.0"
|
||||
keywords = ["date", "time", "calendar", "duration"]
|
||||
categories = ["date-and-time", "no-std", "parser-implementations", "value-formatting"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/time-rs/time"
|
||||
[dependencies.libc]
|
||||
version = "0.2.69"
|
||||
resolver = "2"
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "__time_03_docs"]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
|
||||
[dependencies.rustc-serialize]
|
||||
version = "0.3"
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[[bench]]
|
||||
name = "benchmarks"
|
||||
path = "benchmarks/main.rs"
|
||||
harness = false
|
||||
[dependencies.itoa]
|
||||
version = "1.0.1"
|
||||
optional = true
|
||||
[dev-dependencies.log]
|
||||
version = "0.4"
|
||||
|
||||
[dev-dependencies.winapi]
|
||||
version = "0.3.0"
|
||||
features = ["std", "processthreadsapi", "winbase"]
|
||||
[target."cfg(windows)".dependencies.winapi]
|
||||
version = "0.3.0"
|
||||
features = ["std", "minwinbase", "minwindef", "ntdef", "profileapi", "sysinfoapi", "timezoneapi"]
|
||||
[dependencies.quickcheck-dep]
|
||||
version = "1.0.3"
|
||||
optional = true
|
||||
default-features = false
|
||||
package = "quickcheck"
|
||||
|
||||
[dependencies.rand]
|
||||
version = "0.8.4"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.126"
|
||||
optional = true
|
||||
default-features = false
|
||||
|
||||
[dependencies.time-macros]
|
||||
version = "=0.2.4"
|
||||
optional = true
|
||||
[dev-dependencies.quickcheck_macros]
|
||||
version = "1.0.0"
|
||||
|
||||
[dev-dependencies.rand]
|
||||
version = "0.8.4"
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.serde]
|
||||
version = "1.0.126"
|
||||
features = ["derive"]
|
||||
default-features = false
|
||||
|
||||
[dev-dependencies.serde_json]
|
||||
version = "1.0.68"
|
||||
|
||||
[dev-dependencies.serde_test]
|
||||
version = "1.0.126"
|
||||
|
||||
[features]
|
||||
alloc = []
|
||||
default = ["std"]
|
||||
formatting = ["itoa", "std"]
|
||||
large-dates = ["time-macros/large-dates"]
|
||||
local-offset = ["std"]
|
||||
macros = ["time-macros"]
|
||||
parsing = []
|
||||
quickcheck = ["quickcheck-dep", "alloc"]
|
||||
serde-human-readable = ["serde", "formatting", "parsing"]
|
||||
serde-well-known = ["serde/alloc", "formatting", "parsing"]
|
||||
std = ["alloc"]
|
||||
[target."cfg(__ui_tests)".dev-dependencies.trybuild]
|
||||
version = "=1.0.34"
|
||||
[target."cfg(bench)".dev-dependencies.criterion]
|
||||
version = "0.3.5"
|
||||
|
||||
[target."cfg(bench)".dev-dependencies.criterion-cycles-per-byte]
|
||||
version = "0.1.2"
|
||||
[target."cfg(target_family = \"unix\")".dependencies.libc]
|
||||
version = "0.2.98"
|
||||
|
||||
[target."cfg(target_family = \"unix\")".dependencies.num_threads]
|
||||
version = "0.1.2"
|
||||
|
|
|
@ -0,0 +1,202 @@
|
|||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2022 Jacob Pratt et al.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -1,25 +1,19 @@
|
|||
Copyright (c) 2014 The Rust Project Developers
|
||||
Copyright (c) 2022 Jacob Pratt et al.
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
documentation files (the "Software"), to deal in the
|
||||
Software without restriction, including without
|
||||
limitation the rights to use, copy, modify, merge,
|
||||
publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software
|
||||
is furnished to do so, subject to the following
|
||||
conditions:
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice
|
||||
shall be included in all copies or substantial portions
|
||||
of the Software.
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
|
@ -1,31 +1,48 @@
|
|||
time
|
||||
====
|
||||
# time
|
||||
|
||||
Utilities for working with time-related functions in Rust
|
||||
[![minimum rustc: 1.53](https://img.shields.io/badge/minimum%20rustc-1.53-yellowgreen?logo=rust&style=flat-square)](https://www.whatrustisit.com)
|
||||
[![version](https://img.shields.io/crates/v/time?color=blue&logo=rust&style=flat-square)](https://crates.io/crates/time)
|
||||
[![build status](https://img.shields.io/github/workflow/status/time-rs/time/Build/main?style=flat-square)](https://github.com/time-rs/time/actions)
|
||||
[![codecov](https://codecov.io/gh/time-rs/time/branch/main/graph/badge.svg?token=yt4XSmQNKQ)](https://codecov.io/gh/time-rs/time)
|
||||
|
||||
[![build status](https://github.com/time-rs/time/workflows/Build/badge.svg?branch=v0.1)](https://github.com/time-rs/time/actions?query=branch%3Av0.1)
|
||||
[![Documentation](https://docs.rs/time/badge.svg?version=0.1)](https://docs.rs/time/~0.1)
|
||||
![rustc 1.21.0](https://img.shields.io/badge/rustc-1.21.0-blue)
|
||||
Documentation:
|
||||
- [latest release](https://docs.rs/time)
|
||||
- [main branch](https://time-rs.github.io/api/time)
|
||||
- [book](https://time-rs.github.io/book)
|
||||
|
||||
## time v0.1.x is Deprecated
|
||||
## Minimum Rust version policy
|
||||
|
||||
The 0.1.x series of this library is deprecated and in maintenance mode. No new
|
||||
features will be added. Active development now occurs in the 0.2.x series.
|
||||
The time crate is guaranteed to compile with any release of rustc from the past six months.
|
||||
Optional feature flags that enable interoperability with third-party crates (e.g. rand)
|
||||
follow the policy of that crate if stricter.
|
||||
|
||||
If you need additional functionality that this crate does not provide, check
|
||||
out the [`chrono`](https://github.com/chronotope/chrono) crate.
|
||||
## Contributing
|
||||
|
||||
## Usage
|
||||
Contributions are always welcome! If you have an idea, it's best to float it by me before working on
|
||||
it to ensure no effort is wasted. If there's already an open issue for it, knock yourself out.
|
||||
Internal documentation can be viewed [here](https://time-rs.github.io/internal-api/time).
|
||||
|
||||
Put this in your `Cargo.toml`:
|
||||
If you have any questions, feel free to use [Discussions] or [Codestream]. There are a few notes
|
||||
inline with Codestream, though questions can be asked directly! As a bonus, they are visible and
|
||||
searchable for others. Feedback (prior to opening a pull request) can be provided with Codestream
|
||||
and [VS Live Share]. Don't hesitate to ask questions — that's what I'm here for!
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
time = "0.1"
|
||||
```
|
||||
If using Codestream, just open up a local copy of this repository. It _should_ add you
|
||||
automatically.
|
||||
|
||||
And this in your crate root:
|
||||
[Discussions]: https://github.com/time-rs/time/discussions
|
||||
[Codestream]: https://codestream.com
|
||||
[VS Live Share]: https://code.visualstudio.com/learn/collaboration/live-share
|
||||
|
||||
```rust
|
||||
extern crate time;
|
||||
```
|
||||
## License
|
||||
|
||||
This project is licensed under either of
|
||||
|
||||
- [Apache License, Version 2.0](https://github.com/time-rs/time/blob/main/LICENSE-Apache)
|
||||
- [MIT license](https://github.com/time-rs/time/blob/main/LICENSE-MIT)
|
||||
|
||||
at your option.
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in
|
||||
time by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any
|
||||
additional terms or conditions.
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,93 @@
|
|||
//! Component range error
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// An error type indicating that a component provided to a method was out of range, causing a
|
||||
/// failure.
|
||||
// i64 is the narrowest type fitting all use cases. This eliminates the need for a type parameter.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ComponentRange {
|
||||
/// Name of the component.
|
||||
pub(crate) name: &'static str,
|
||||
/// Minimum allowed value, inclusive.
|
||||
pub(crate) minimum: i64,
|
||||
/// Maximum allowed value, inclusive.
|
||||
pub(crate) maximum: i64,
|
||||
/// Value that was provided.
|
||||
pub(crate) value: i64,
|
||||
/// The minimum and/or maximum value is conditional on the value of other
|
||||
/// parameters.
|
||||
pub(crate) conditional_range: bool,
|
||||
}
|
||||
|
||||
impl ComponentRange {
|
||||
/// Obtain the name of the component whose value was out of range.
|
||||
pub const fn name(self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
|
||||
/// Whether the value's permitted range is conditional, i.e. whether an input with this
|
||||
/// value could have succeeded if the values of other components were different.
|
||||
pub const fn is_conditional(self) -> bool {
|
||||
self.conditional_range
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ComponentRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{} must be in the range {}..={}",
|
||||
self.name, self.minimum, self.maximum
|
||||
)?;
|
||||
|
||||
if self.conditional_range {
|
||||
f.write_str(", given values of other parameters")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ComponentRange> for crate::Error {
|
||||
fn from(original: ComponentRange) -> Self {
|
||||
Self::ComponentRange(original)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<crate::Error> for ComponentRange {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: crate::Error) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
crate::Error::ComponentRange(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **This trait implementation is deprecated and will be removed in a future breaking release.**
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::de::Expected for ComponentRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"a value in the range {}..={}",
|
||||
self.minimum, self.maximum
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl ComponentRange {
|
||||
/// Convert the error to a deserialization error.
|
||||
pub(crate) fn into_de_error<E: serde::de::Error>(self) -> E {
|
||||
E::invalid_value(serde::de::Unexpected::Signed(self.value), &self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for ComponentRange {}
|
|
@ -0,0 +1,37 @@
|
|||
//! Conversion range error
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// An error type indicating that a conversion failed because the target type could not store the
|
||||
/// initial value.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ConversionRange;
|
||||
|
||||
impl fmt::Display for ConversionRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Source value is out of range for the target type")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for ConversionRange {}
|
||||
|
||||
impl From<ConversionRange> for crate::Error {
|
||||
fn from(err: ConversionRange) -> Self {
|
||||
Self::ConversionRange(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<crate::Error> for ConversionRange {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: crate::Error) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
crate::Error::ConversionRange(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
//! Different variant error
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
|
||||
/// An error type indicating that a [`TryFrom`](core::convert::TryFrom) call failed because the
|
||||
/// original value was of a different variant.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DifferentVariant;
|
||||
|
||||
impl fmt::Display for DifferentVariant {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "value was of a different variant than required")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for DifferentVariant {}
|
||||
|
||||
impl From<DifferentVariant> for crate::Error {
|
||||
fn from(err: DifferentVariant) -> Self {
|
||||
Self::DifferentVariant(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<crate::Error> for DifferentVariant {
|
||||
type Error = Self;
|
||||
|
||||
fn try_from(err: crate::Error) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
crate::Error::DifferentVariant(err) => Ok(err),
|
||||
_ => Err(Self),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
//! Error formatting a struct
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
use std::io;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// An error occurred when formatting.
|
||||
#[non_exhaustive]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "formatting")))]
|
||||
#[derive(Debug)]
|
||||
pub enum Format {
|
||||
/// The type being formatted does not contain sufficient information to format a component.
|
||||
#[non_exhaustive]
|
||||
InsufficientTypeInformation,
|
||||
/// The component named has a value that cannot be formatted into the requested format.
|
||||
///
|
||||
/// This variant is only returned when using well-known formats.
|
||||
InvalidComponent(&'static str),
|
||||
/// A value of `std::io::Error` was returned internally.
|
||||
StdIo(io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Format {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InsufficientTypeInformation => f.write_str(
|
||||
"The type being formatted does not contain sufficient information to format a \
|
||||
component.",
|
||||
),
|
||||
Self::InvalidComponent(component) => write!(
|
||||
f,
|
||||
"The {} component cannot be formatted into the requested format.",
|
||||
component
|
||||
),
|
||||
Self::StdIo(err) => err.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Format {
|
||||
fn from(err: io::Error) -> Self {
|
||||
Self::StdIo(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Format> for io::Error {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: Format) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
Format::StdIo(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for Format {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match *self {
|
||||
Self::InsufficientTypeInformation | Self::InvalidComponent(_) => None,
|
||||
Self::StdIo(ref err) => Some(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "formatting")))]
|
||||
impl From<Format> for crate::Error {
|
||||
fn from(original: Format) -> Self {
|
||||
Self::Format(original)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "formatting")))]
|
||||
impl TryFrom<crate::Error> for Format {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: crate::Error) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
crate::Error::Format(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Format {
|
||||
/// Obtain an error type for the serializer.
|
||||
#[doc(hidden)] // Exposed only for the `declare_format_string` macro
|
||||
pub fn into_invalid_serde_value<S: serde::Serializer>(self) -> S::Error {
|
||||
use serde::ser::Error;
|
||||
S::Error::custom(format!("{}", self))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
//! Indeterminate offset
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// The system's UTC offset could not be determined at the given datetime.
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "local-offset")))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct IndeterminateOffset;
|
||||
|
||||
impl fmt::Display for IndeterminateOffset {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("The system's UTC offset could not be determined")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for IndeterminateOffset {}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "local-offset")))]
|
||||
impl From<IndeterminateOffset> for crate::Error {
|
||||
fn from(err: IndeterminateOffset) -> Self {
|
||||
Self::IndeterminateOffset(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "std")))]
|
||||
impl TryFrom<crate::Error> for IndeterminateOffset {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: crate::Error) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
crate::Error::IndeterminateOffset(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
//! Invalid format description
|
||||
|
||||
use alloc::string::String;
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// The format description provided was not valid.
|
||||
#[cfg_attr(
|
||||
__time_03_docs,
|
||||
doc(cfg(all(any(feature = "formatting", feature = "parsing"), feature = "alloc")))
|
||||
)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum InvalidFormatDescription {
|
||||
/// There was a bracket pair that was opened but not closed.
|
||||
#[non_exhaustive]
|
||||
UnclosedOpeningBracket {
|
||||
/// The zero-based index of the opening bracket.
|
||||
index: usize,
|
||||
},
|
||||
/// A component name is not valid.
|
||||
#[non_exhaustive]
|
||||
InvalidComponentName {
|
||||
/// The name of the invalid component name.
|
||||
name: String,
|
||||
/// The zero-based index the component name starts at.
|
||||
index: usize,
|
||||
},
|
||||
/// A modifier is not valid.
|
||||
#[non_exhaustive]
|
||||
InvalidModifier {
|
||||
/// The value of the invalid modifier.
|
||||
value: String,
|
||||
/// The zero-based index the modifier starts at.
|
||||
index: usize,
|
||||
},
|
||||
/// A component name is missing.
|
||||
#[non_exhaustive]
|
||||
MissingComponentName {
|
||||
/// The zero-based index where the component name should start.
|
||||
index: usize,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
__time_03_docs,
|
||||
doc(cfg(all(any(feature = "formatting", feature = "parsing"), feature = "alloc")))
|
||||
)]
|
||||
impl From<InvalidFormatDescription> for crate::Error {
|
||||
fn from(original: InvalidFormatDescription) -> Self {
|
||||
Self::InvalidFormatDescription(original)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(
|
||||
__time_03_docs,
|
||||
doc(cfg(all(any(feature = "formatting", feature = "parsing"), feature = "alloc")))
|
||||
)]
|
||||
impl TryFrom<crate::Error> for InvalidFormatDescription {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: crate::Error) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
crate::Error::InvalidFormatDescription(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for InvalidFormatDescription {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use InvalidFormatDescription::*;
|
||||
match self {
|
||||
UnclosedOpeningBracket { index } => {
|
||||
write!(f, "unclosed opening bracket at byte index {}", index)
|
||||
}
|
||||
InvalidComponentName { name, index } => write!(
|
||||
f,
|
||||
"invalid component name `{}` at byte index {}",
|
||||
name, index
|
||||
),
|
||||
InvalidModifier { value, index } => {
|
||||
write!(f, "invalid modifier `{}` at byte index {}", value, index)
|
||||
}
|
||||
MissingComponentName { index } => {
|
||||
write!(f, "missing component name at byte index {}", index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for InvalidFormatDescription {}
|
|
@ -0,0 +1,35 @@
|
|||
//! Invalid variant error
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
|
||||
/// An error type indicating that a [`FromStr`](core::str::FromStr) call failed because the value
|
||||
/// was not a valid variant.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct InvalidVariant;
|
||||
|
||||
impl fmt::Display for InvalidVariant {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "value was not a valid variant")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for InvalidVariant {}
|
||||
|
||||
impl From<InvalidVariant> for crate::Error {
|
||||
fn from(err: InvalidVariant) -> Self {
|
||||
Self::InvalidVariant(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<crate::Error> for InvalidVariant {
|
||||
type Error = crate::error::DifferentVariant;
|
||||
|
||||
fn try_from(err: crate::Error) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
crate::Error::InvalidVariant(err) => Ok(err),
|
||||
_ => Err(crate::error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
//! Various error types returned by methods in the time crate.
|
||||
|
||||
mod component_range;
|
||||
mod conversion_range;
|
||||
mod different_variant;
|
||||
#[cfg(feature = "formatting")]
|
||||
mod format;
|
||||
#[cfg(feature = "local-offset")]
|
||||
mod indeterminate_offset;
|
||||
#[cfg(all(any(feature = "formatting", feature = "parsing"), feature = "alloc"))]
|
||||
mod invalid_format_description;
|
||||
mod invalid_variant;
|
||||
#[cfg(feature = "parsing")]
|
||||
mod parse;
|
||||
#[cfg(feature = "parsing")]
|
||||
mod parse_from_description;
|
||||
#[cfg(feature = "parsing")]
|
||||
mod try_from_parsed;
|
||||
|
||||
use core::fmt;
|
||||
|
||||
pub use component_range::ComponentRange;
|
||||
pub use conversion_range::ConversionRange;
|
||||
pub use different_variant::DifferentVariant;
|
||||
#[cfg(feature = "formatting")]
|
||||
pub use format::Format;
|
||||
#[cfg(feature = "local-offset")]
|
||||
pub use indeterminate_offset::IndeterminateOffset;
|
||||
#[cfg(all(any(feature = "formatting", feature = "parsing"), feature = "alloc"))]
|
||||
pub use invalid_format_description::InvalidFormatDescription;
|
||||
pub use invalid_variant::InvalidVariant;
|
||||
#[cfg(feature = "parsing")]
|
||||
pub use parse::Parse;
|
||||
#[cfg(feature = "parsing")]
|
||||
pub use parse_from_description::ParseFromDescription;
|
||||
#[cfg(feature = "parsing")]
|
||||
pub use try_from_parsed::TryFromParsed;
|
||||
|
||||
/// A unified error type for anything returned by a method in the time crate.
|
||||
///
|
||||
/// This can be used when you either don't know or don't care about the exact error returned.
|
||||
/// `Result<_, time::Error>` (or its alias `time::Result<_>`) will work in these situations.
|
||||
#[allow(missing_copy_implementations, variant_size_differences)]
|
||||
#[allow(clippy::missing_docs_in_private_items)] // variants only
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
ConversionRange(ConversionRange),
|
||||
ComponentRange(ComponentRange),
|
||||
#[cfg(feature = "local-offset")]
|
||||
IndeterminateOffset(IndeterminateOffset),
|
||||
#[cfg(feature = "formatting")]
|
||||
Format(Format),
|
||||
#[cfg(feature = "parsing")]
|
||||
ParseFromDescription(ParseFromDescription),
|
||||
#[cfg(feature = "parsing")]
|
||||
#[non_exhaustive]
|
||||
UnexpectedTrailingCharacters,
|
||||
#[cfg(feature = "parsing")]
|
||||
TryFromParsed(TryFromParsed),
|
||||
#[cfg(all(any(feature = "formatting", feature = "parsing"), feature = "alloc"))]
|
||||
InvalidFormatDescription(InvalidFormatDescription),
|
||||
DifferentVariant(DifferentVariant),
|
||||
InvalidVariant(InvalidVariant),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ConversionRange(e) => e.fmt(f),
|
||||
Self::ComponentRange(e) => e.fmt(f),
|
||||
#[cfg(feature = "local-offset")]
|
||||
Self::IndeterminateOffset(e) => e.fmt(f),
|
||||
#[cfg(feature = "formatting")]
|
||||
Self::Format(e) => e.fmt(f),
|
||||
#[cfg(feature = "parsing")]
|
||||
Self::ParseFromDescription(e) => e.fmt(f),
|
||||
#[cfg(feature = "parsing")]
|
||||
Self::UnexpectedTrailingCharacters => f.write_str("unexpected trailing characters"),
|
||||
#[cfg(feature = "parsing")]
|
||||
Self::TryFromParsed(e) => e.fmt(f),
|
||||
#[cfg(all(any(feature = "formatting", feature = "parsing"), feature = "alloc"))]
|
||||
Self::InvalidFormatDescription(e) => e.fmt(f),
|
||||
Self::DifferentVariant(e) => e.fmt(f),
|
||||
Self::InvalidVariant(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::ConversionRange(err) => Some(err),
|
||||
Self::ComponentRange(err) => Some(err),
|
||||
#[cfg(feature = "local-offset")]
|
||||
Self::IndeterminateOffset(err) => Some(err),
|
||||
#[cfg(feature = "formatting")]
|
||||
Self::Format(err) => Some(err),
|
||||
#[cfg(feature = "parsing")]
|
||||
Self::ParseFromDescription(err) => Some(err),
|
||||
#[cfg(feature = "parsing")]
|
||||
Self::UnexpectedTrailingCharacters => None,
|
||||
#[cfg(feature = "parsing")]
|
||||
Self::TryFromParsed(err) => Some(err),
|
||||
#[cfg(all(any(feature = "formatting", feature = "parsing"), feature = "alloc"))]
|
||||
Self::InvalidFormatDescription(err) => Some(err),
|
||||
Self::DifferentVariant(err) => Some(err),
|
||||
Self::InvalidVariant(err) => Some(err),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
//! Error that occurred at some stage of parsing
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
|
||||
use crate::error::{self, ParseFromDescription, TryFromParsed};
|
||||
|
||||
/// An error that occurred at some stage of parsing.
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
#[allow(variant_size_differences)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Parse {
|
||||
#[allow(clippy::missing_docs_in_private_items)]
|
||||
TryFromParsed(TryFromParsed),
|
||||
#[allow(clippy::missing_docs_in_private_items)]
|
||||
ParseFromDescription(ParseFromDescription),
|
||||
/// The input should have ended, but there were characters remaining.
|
||||
#[non_exhaustive]
|
||||
UnexpectedTrailingCharacters,
|
||||
}
|
||||
|
||||
impl fmt::Display for Parse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::TryFromParsed(err) => err.fmt(f),
|
||||
Self::ParseFromDescription(err) => err.fmt(f),
|
||||
Self::UnexpectedTrailingCharacters => f.write_str("unexpected trailing characters"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for Parse {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::TryFromParsed(err) => Some(err),
|
||||
Self::ParseFromDescription(err) => Some(err),
|
||||
Self::UnexpectedTrailingCharacters => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
impl From<TryFromParsed> for Parse {
|
||||
fn from(err: TryFromParsed) -> Self {
|
||||
Self::TryFromParsed(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
impl TryFrom<Parse> for TryFromParsed {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: Parse) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
Parse::TryFromParsed(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
impl From<ParseFromDescription> for Parse {
|
||||
fn from(err: ParseFromDescription) -> Self {
|
||||
Self::ParseFromDescription(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
impl TryFrom<Parse> for ParseFromDescription {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: Parse) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
Parse::ParseFromDescription(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
impl From<Parse> for crate::Error {
|
||||
fn from(err: Parse) -> Self {
|
||||
match err {
|
||||
Parse::TryFromParsed(err) => Self::TryFromParsed(err),
|
||||
Parse::ParseFromDescription(err) => Self::ParseFromDescription(err),
|
||||
Parse::UnexpectedTrailingCharacters => Self::UnexpectedTrailingCharacters,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
impl TryFrom<crate::Error> for Parse {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: crate::Error) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
crate::Error::ParseFromDescription(err) => Ok(Self::ParseFromDescription(err)),
|
||||
crate::Error::UnexpectedTrailingCharacters => Ok(Self::UnexpectedTrailingCharacters),
|
||||
crate::Error::TryFromParsed(err) => Ok(Self::TryFromParsed(err)),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
//! Error parsing an input into a [`Parsed`](crate::parsing::Parsed) struct
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// An error that occurred while parsing the input into a [`Parsed`](crate::parsing::Parsed) struct.
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ParseFromDescription {
|
||||
/// A string literal was not what was expected.
|
||||
#[non_exhaustive]
|
||||
InvalidLiteral,
|
||||
/// A dynamic component was not valid.
|
||||
InvalidComponent(&'static str),
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseFromDescription {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidLiteral => f.write_str("a character literal was not valid"),
|
||||
Self::InvalidComponent(name) => {
|
||||
write!(f, "the '{}' component could not be parsed", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for ParseFromDescription {}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
impl From<ParseFromDescription> for crate::Error {
|
||||
fn from(original: ParseFromDescription) -> Self {
|
||||
Self::ParseFromDescription(original)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
impl TryFrom<crate::Error> for ParseFromDescription {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: crate::Error) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
crate::Error::ParseFromDescription(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
//! Error converting a [`Parsed`](crate::parsing::Parsed) struct to another type
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
|
||||
use crate::error;
|
||||
|
||||
/// An error that occurred when converting a [`Parsed`](crate::parsing::Parsed) to another type.
|
||||
#[non_exhaustive]
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TryFromParsed {
|
||||
/// The [`Parsed`](crate::parsing::Parsed) did not include enough information to construct the
|
||||
/// type.
|
||||
InsufficientInformation,
|
||||
/// Some component contained an invalid value for the type.
|
||||
ComponentRange(error::ComponentRange),
|
||||
}
|
||||
|
||||
impl fmt::Display for TryFromParsed {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InsufficientInformation => f.write_str(
|
||||
"the `Parsed` struct did not include enough information to construct the type",
|
||||
),
|
||||
Self::ComponentRange(err) => err.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<error::ComponentRange> for TryFromParsed {
|
||||
fn from(v: error::ComponentRange) -> Self {
|
||||
Self::ComponentRange(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TryFromParsed> for error::ComponentRange {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: TryFromParsed) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
TryFromParsed::ComponentRange(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for TryFromParsed {
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
match self {
|
||||
Self::InsufficientInformation => None,
|
||||
Self::ComponentRange(err) => Some(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
impl From<TryFromParsed> for crate::Error {
|
||||
fn from(original: TryFromParsed) -> Self {
|
||||
Self::TryFromParsed(original)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
impl TryFrom<crate::Error> for TryFromParsed {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(err: crate::Error) -> Result<Self, Self::Error> {
|
||||
match err {
|
||||
crate::Error::TryFromParsed(err) => Ok(err),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,279 @@
|
|||
//! Extension traits.
|
||||
|
||||
use core::time::Duration as StdDuration;
|
||||
|
||||
use crate::Duration;
|
||||
|
||||
/// Sealed trait to prevent downstream implementations.
|
||||
mod sealed {
|
||||
/// A trait that cannot be implemented by downstream users.
|
||||
pub trait Sealed {}
|
||||
impl Sealed for i64 {}
|
||||
impl Sealed for u64 {}
|
||||
impl Sealed for f64 {}
|
||||
}
|
||||
|
||||
// region: NumericalDuration
|
||||
/// Create [`Duration`]s from numeric literals.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic construction of [`Duration`]s.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{Duration, ext::NumericalDuration};
|
||||
/// assert_eq!(5.nanoseconds(), Duration::nanoseconds(5));
|
||||
/// assert_eq!(5.microseconds(), Duration::microseconds(5));
|
||||
/// assert_eq!(5.milliseconds(), Duration::milliseconds(5));
|
||||
/// assert_eq!(5.seconds(), Duration::seconds(5));
|
||||
/// assert_eq!(5.minutes(), Duration::minutes(5));
|
||||
/// assert_eq!(5.hours(), Duration::hours(5));
|
||||
/// assert_eq!(5.days(), Duration::days(5));
|
||||
/// assert_eq!(5.weeks(), Duration::weeks(5));
|
||||
/// ```
|
||||
///
|
||||
/// Signed integers work as well!
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{Duration, ext::NumericalDuration};
|
||||
/// assert_eq!((-5).nanoseconds(), Duration::nanoseconds(-5));
|
||||
/// assert_eq!((-5).microseconds(), Duration::microseconds(-5));
|
||||
/// assert_eq!((-5).milliseconds(), Duration::milliseconds(-5));
|
||||
/// assert_eq!((-5).seconds(), Duration::seconds(-5));
|
||||
/// assert_eq!((-5).minutes(), Duration::minutes(-5));
|
||||
/// assert_eq!((-5).hours(), Duration::hours(-5));
|
||||
/// assert_eq!((-5).days(), Duration::days(-5));
|
||||
/// assert_eq!((-5).weeks(), Duration::weeks(-5));
|
||||
/// ```
|
||||
///
|
||||
/// Just like any other [`Duration`], they can be added, subtracted, etc.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::ext::NumericalDuration;
|
||||
/// assert_eq!(2.seconds() + 500.milliseconds(), 2_500.milliseconds());
|
||||
/// assert_eq!(2.seconds() - 500.milliseconds(), 1_500.milliseconds());
|
||||
/// ```
|
||||
///
|
||||
/// When called on floating point values, any remainder of the floating point value will be
|
||||
/// truncated. Keep in mind that floating point numbers are inherently imprecise and have limited
|
||||
/// capacity.
|
||||
pub trait NumericalDuration: sealed::Sealed {
|
||||
/// Create a [`Duration`] from the number of nanoseconds.
|
||||
fn nanoseconds(self) -> Duration;
|
||||
/// Create a [`Duration`] from the number of microseconds.
|
||||
fn microseconds(self) -> Duration;
|
||||
/// Create a [`Duration`] from the number of milliseconds.
|
||||
fn milliseconds(self) -> Duration;
|
||||
/// Create a [`Duration`] from the number of seconds.
|
||||
fn seconds(self) -> Duration;
|
||||
/// Create a [`Duration`] from the number of minutes.
|
||||
fn minutes(self) -> Duration;
|
||||
/// Create a [`Duration`] from the number of hours.
|
||||
fn hours(self) -> Duration;
|
||||
/// Create a [`Duration`] from the number of days.
|
||||
fn days(self) -> Duration;
|
||||
/// Create a [`Duration`] from the number of weeks.
|
||||
fn weeks(self) -> Duration;
|
||||
}
|
||||
|
||||
impl NumericalDuration for i64 {
|
||||
fn nanoseconds(self) -> Duration {
|
||||
Duration::nanoseconds(self)
|
||||
}
|
||||
|
||||
fn microseconds(self) -> Duration {
|
||||
Duration::microseconds(self)
|
||||
}
|
||||
|
||||
fn milliseconds(self) -> Duration {
|
||||
Duration::milliseconds(self)
|
||||
}
|
||||
|
||||
fn seconds(self) -> Duration {
|
||||
Duration::seconds(self)
|
||||
}
|
||||
|
||||
fn minutes(self) -> Duration {
|
||||
Duration::minutes(self)
|
||||
}
|
||||
|
||||
fn hours(self) -> Duration {
|
||||
Duration::hours(self)
|
||||
}
|
||||
|
||||
fn days(self) -> Duration {
|
||||
Duration::days(self)
|
||||
}
|
||||
|
||||
fn weeks(self) -> Duration {
|
||||
Duration::weeks(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl NumericalDuration for f64 {
|
||||
fn nanoseconds(self) -> Duration {
|
||||
Duration::nanoseconds(self as _)
|
||||
}
|
||||
|
||||
fn microseconds(self) -> Duration {
|
||||
Duration::nanoseconds((self * 1_000.) as _)
|
||||
}
|
||||
|
||||
fn milliseconds(self) -> Duration {
|
||||
Duration::nanoseconds((self * 1_000_000.) as _)
|
||||
}
|
||||
|
||||
fn seconds(self) -> Duration {
|
||||
Duration::nanoseconds((self * 1_000_000_000.) as _)
|
||||
}
|
||||
|
||||
fn minutes(self) -> Duration {
|
||||
Duration::nanoseconds((self * 60_000_000_000.) as _)
|
||||
}
|
||||
|
||||
fn hours(self) -> Duration {
|
||||
Duration::nanoseconds((self * 3_600_000_000_000.) as _)
|
||||
}
|
||||
|
||||
fn days(self) -> Duration {
|
||||
Duration::nanoseconds((self * 86_400_000_000_000.) as _)
|
||||
}
|
||||
|
||||
fn weeks(self) -> Duration {
|
||||
Duration::nanoseconds((self * 604_800_000_000_000.) as _)
|
||||
}
|
||||
}
|
||||
// endregion NumericalDuration
|
||||
|
||||
// region: NumericalStdDuration
|
||||
/// Create [`std::time::Duration`]s from numeric literals.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Basic construction of [`std::time::Duration`]s.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::ext::NumericalStdDuration;
|
||||
/// # use core::time::Duration;
|
||||
/// assert_eq!(5.std_nanoseconds(), Duration::from_nanos(5));
|
||||
/// assert_eq!(5.std_microseconds(), Duration::from_micros(5));
|
||||
/// assert_eq!(5.std_milliseconds(), Duration::from_millis(5));
|
||||
/// assert_eq!(5.std_seconds(), Duration::from_secs(5));
|
||||
/// assert_eq!(5.std_minutes(), Duration::from_secs(5 * 60));
|
||||
/// assert_eq!(5.std_hours(), Duration::from_secs(5 * 3_600));
|
||||
/// assert_eq!(5.std_days(), Duration::from_secs(5 * 86_400));
|
||||
/// assert_eq!(5.std_weeks(), Duration::from_secs(5 * 604_800));
|
||||
/// ```
|
||||
///
|
||||
/// Just like any other [`std::time::Duration`], they can be added, subtracted, etc.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::ext::NumericalStdDuration;
|
||||
/// assert_eq!(
|
||||
/// 2.std_seconds() + 500.std_milliseconds(),
|
||||
/// 2_500.std_milliseconds()
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// 2.std_seconds() - 500.std_milliseconds(),
|
||||
/// 1_500.std_milliseconds()
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// When called on floating point values, any remainder of the floating point value will be
|
||||
/// truncated. Keep in mind that floating point numbers are inherently imprecise and have limited
|
||||
/// capacity.
|
||||
pub trait NumericalStdDuration: sealed::Sealed {
|
||||
/// Create a [`std::time::Duration`] from the number of nanoseconds.
|
||||
fn std_nanoseconds(self) -> StdDuration;
|
||||
/// Create a [`std::time::Duration`] from the number of microseconds.
|
||||
fn std_microseconds(self) -> StdDuration;
|
||||
/// Create a [`std::time::Duration`] from the number of milliseconds.
|
||||
fn std_milliseconds(self) -> StdDuration;
|
||||
/// Create a [`std::time::Duration`] from the number of seconds.
|
||||
fn std_seconds(self) -> StdDuration;
|
||||
/// Create a [`std::time::Duration`] from the number of minutes.
|
||||
fn std_minutes(self) -> StdDuration;
|
||||
/// Create a [`std::time::Duration`] from the number of hours.
|
||||
fn std_hours(self) -> StdDuration;
|
||||
/// Create a [`std::time::Duration`] from the number of days.
|
||||
fn std_days(self) -> StdDuration;
|
||||
/// Create a [`std::time::Duration`] from the number of weeks.
|
||||
fn std_weeks(self) -> StdDuration;
|
||||
}
|
||||
|
||||
impl NumericalStdDuration for u64 {
|
||||
fn std_nanoseconds(self) -> StdDuration {
|
||||
StdDuration::from_nanos(self)
|
||||
}
|
||||
|
||||
fn std_microseconds(self) -> StdDuration {
|
||||
StdDuration::from_micros(self)
|
||||
}
|
||||
|
||||
fn std_milliseconds(self) -> StdDuration {
|
||||
StdDuration::from_millis(self)
|
||||
}
|
||||
|
||||
fn std_seconds(self) -> StdDuration {
|
||||
StdDuration::from_secs(self)
|
||||
}
|
||||
|
||||
fn std_minutes(self) -> StdDuration {
|
||||
StdDuration::from_secs(self * 60)
|
||||
}
|
||||
|
||||
fn std_hours(self) -> StdDuration {
|
||||
StdDuration::from_secs(self * 3_600)
|
||||
}
|
||||
|
||||
fn std_days(self) -> StdDuration {
|
||||
StdDuration::from_secs(self * 86_400)
|
||||
}
|
||||
|
||||
fn std_weeks(self) -> StdDuration {
|
||||
StdDuration::from_secs(self * 604_800)
|
||||
}
|
||||
}
|
||||
|
||||
impl NumericalStdDuration for f64 {
|
||||
fn std_nanoseconds(self) -> StdDuration {
|
||||
assert!(self >= 0.);
|
||||
StdDuration::from_nanos(self as _)
|
||||
}
|
||||
|
||||
fn std_microseconds(self) -> StdDuration {
|
||||
assert!(self >= 0.);
|
||||
StdDuration::from_nanos((self * 1_000.) as _)
|
||||
}
|
||||
|
||||
fn std_milliseconds(self) -> StdDuration {
|
||||
assert!(self >= 0.);
|
||||
StdDuration::from_nanos((self * 1_000_000.) as _)
|
||||
}
|
||||
|
||||
fn std_seconds(self) -> StdDuration {
|
||||
assert!(self >= 0.);
|
||||
StdDuration::from_nanos((self * 1_000_000_000.) as _)
|
||||
}
|
||||
|
||||
fn std_minutes(self) -> StdDuration {
|
||||
assert!(self >= 0.);
|
||||
StdDuration::from_nanos((self * 60_000_000_000.) as _)
|
||||
}
|
||||
|
||||
fn std_hours(self) -> StdDuration {
|
||||
assert!(self >= 0.);
|
||||
StdDuration::from_nanos((self * 3_600_000_000_000.) as _)
|
||||
}
|
||||
|
||||
fn std_days(self) -> StdDuration {
|
||||
assert!(self >= 0.);
|
||||
StdDuration::from_nanos((self * 86_400_000_000_000.) as _)
|
||||
}
|
||||
|
||||
fn std_weeks(self) -> StdDuration {
|
||||
assert!(self >= 0.);
|
||||
StdDuration::from_nanos((self * 604_800_000_000_000.) as _)
|
||||
}
|
||||
}
|
||||
// endregion NumericalStdDuration
|
|
@ -0,0 +1,167 @@
|
|||
//! Part of a format description.
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::format_description::modifier;
|
||||
#[cfg(feature = "alloc")]
|
||||
use crate::{error::InvalidFormatDescription, format_description::modifier::Modifiers};
|
||||
|
||||
/// A component of a larger format description.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Component {
|
||||
/// Day of the month.
|
||||
Day(modifier::Day),
|
||||
/// Month of the year.
|
||||
Month(modifier::Month),
|
||||
/// Ordinal day of the year.
|
||||
Ordinal(modifier::Ordinal),
|
||||
/// Day of the week.
|
||||
Weekday(modifier::Weekday),
|
||||
/// Week within the year.
|
||||
WeekNumber(modifier::WeekNumber),
|
||||
/// Year of the date.
|
||||
Year(modifier::Year),
|
||||
/// Hour of the day.
|
||||
Hour(modifier::Hour),
|
||||
/// Minute within the hour.
|
||||
Minute(modifier::Minute),
|
||||
/// AM/PM part of the time.
|
||||
Period(modifier::Period),
|
||||
/// Second within the minute.
|
||||
Second(modifier::Second),
|
||||
/// Subsecond within the second.
|
||||
Subsecond(modifier::Subsecond),
|
||||
/// Hour of the UTC offset.
|
||||
OffsetHour(modifier::OffsetHour),
|
||||
/// Minute within the hour of the UTC offset.
|
||||
OffsetMinute(modifier::OffsetMinute),
|
||||
/// Second within the minute of the UTC offset.
|
||||
OffsetSecond(modifier::OffsetSecond),
|
||||
}
|
||||
|
||||
/// A component with no modifiers present.
|
||||
#[cfg(feature = "alloc")]
|
||||
pub(crate) enum NakedComponent {
|
||||
/// Day of the month.
|
||||
Day,
|
||||
/// Month of the year.
|
||||
Month,
|
||||
/// Ordinal day of the year.
|
||||
Ordinal,
|
||||
/// Day of the week.
|
||||
Weekday,
|
||||
/// Week within the year.
|
||||
WeekNumber,
|
||||
/// Year of the date.
|
||||
Year,
|
||||
/// Hour of the day.
|
||||
Hour,
|
||||
/// Minute within the hour.
|
||||
Minute,
|
||||
/// AM/PM part of the time.
|
||||
Period,
|
||||
/// Second within the minute.
|
||||
Second,
|
||||
/// Subsecond within the second.
|
||||
Subsecond,
|
||||
/// Hour of the UTC offset.
|
||||
OffsetHour,
|
||||
/// Minute within the hour of the UTC offset.
|
||||
OffsetMinute,
|
||||
/// Second within the minute of the UTC offset.
|
||||
OffsetSecond,
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl NakedComponent {
|
||||
/// Parse a component (without its modifiers) from the provided name.
|
||||
pub(crate) fn parse(
|
||||
component_name: &[u8],
|
||||
component_index: usize,
|
||||
) -> Result<Self, InvalidFormatDescription> {
|
||||
match component_name {
|
||||
b"day" => Ok(Self::Day),
|
||||
b"month" => Ok(Self::Month),
|
||||
b"ordinal" => Ok(Self::Ordinal),
|
||||
b"weekday" => Ok(Self::Weekday),
|
||||
b"week_number" => Ok(Self::WeekNumber),
|
||||
b"year" => Ok(Self::Year),
|
||||
b"hour" => Ok(Self::Hour),
|
||||
b"minute" => Ok(Self::Minute),
|
||||
b"period" => Ok(Self::Period),
|
||||
b"second" => Ok(Self::Second),
|
||||
b"subsecond" => Ok(Self::Subsecond),
|
||||
b"offset_hour" => Ok(Self::OffsetHour),
|
||||
b"offset_minute" => Ok(Self::OffsetMinute),
|
||||
b"offset_second" => Ok(Self::OffsetSecond),
|
||||
b"" => Err(InvalidFormatDescription::MissingComponentName {
|
||||
index: component_index,
|
||||
}),
|
||||
_ => Err(InvalidFormatDescription::InvalidComponentName {
|
||||
name: String::from_utf8_lossy(component_name).into_owned(),
|
||||
index: component_index,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attach the necessary modifiers to the component.
|
||||
pub(crate) fn attach_modifiers(self, modifiers: &Modifiers) -> Component {
|
||||
match self {
|
||||
Self::Day => Component::Day(modifier::Day {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::Month => Component::Month(modifier::Month {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
repr: modifiers.month_repr.unwrap_or_default(),
|
||||
case_sensitive: modifiers.case_sensitive.unwrap_or(true),
|
||||
}),
|
||||
Self::Ordinal => Component::Ordinal(modifier::Ordinal {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::Weekday => Component::Weekday(modifier::Weekday {
|
||||
repr: modifiers.weekday_repr.unwrap_or_default(),
|
||||
one_indexed: modifiers.weekday_is_one_indexed.unwrap_or(true),
|
||||
case_sensitive: modifiers.case_sensitive.unwrap_or(true),
|
||||
}),
|
||||
Self::WeekNumber => Component::WeekNumber(modifier::WeekNumber {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
repr: modifiers.week_number_repr.unwrap_or_default(),
|
||||
}),
|
||||
Self::Year => Component::Year(modifier::Year {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
repr: modifiers.year_repr.unwrap_or_default(),
|
||||
iso_week_based: modifiers.year_is_iso_week_based.unwrap_or_default(),
|
||||
sign_is_mandatory: modifiers.sign_is_mandatory.unwrap_or_default(),
|
||||
}),
|
||||
Self::Hour => Component::Hour(modifier::Hour {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
is_12_hour_clock: modifiers.hour_is_12_hour_clock.unwrap_or_default(),
|
||||
}),
|
||||
Self::Minute => Component::Minute(modifier::Minute {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::Period => Component::Period(modifier::Period {
|
||||
is_uppercase: modifiers.period_is_uppercase.unwrap_or(true),
|
||||
case_sensitive: modifiers.case_sensitive.unwrap_or(true),
|
||||
}),
|
||||
Self::Second => Component::Second(modifier::Second {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::Subsecond => Component::Subsecond(modifier::Subsecond {
|
||||
digits: modifiers.subsecond_digits.unwrap_or_default(),
|
||||
}),
|
||||
Self::OffsetHour => Component::OffsetHour(modifier::OffsetHour {
|
||||
sign_is_mandatory: modifiers.sign_is_mandatory.unwrap_or_default(),
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::OffsetMinute => Component::OffsetMinute(modifier::OffsetMinute {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
Self::OffsetSecond => Component::OffsetSecond(modifier::OffsetSecond {
|
||||
padding: modifiers.padding.unwrap_or_default(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
//! Description of how types should be formatted and parsed.
|
||||
//!
|
||||
//! The formatted value will be output to the provided writer. Format descriptions can be
|
||||
//! [well-known](crate::format_description::well_known) or obtained by using the
|
||||
//! [`format_description!`](crate::macros::format_description) macro, the
|
||||
//! [`format_description::parse`](crate::format_description::parse()) function.
|
||||
|
||||
mod component;
|
||||
pub mod modifier;
|
||||
#[cfg(feature = "alloc")]
|
||||
pub(crate) mod parse;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use alloc::string::String;
|
||||
use core::convert::TryFrom;
|
||||
#[cfg(feature = "alloc")]
|
||||
use core::fmt;
|
||||
|
||||
pub use self::component::Component;
|
||||
#[cfg(feature = "alloc")]
|
||||
pub use self::parse::parse;
|
||||
use crate::error;
|
||||
|
||||
/// Helper methods.
|
||||
#[cfg(feature = "alloc")]
|
||||
mod helper {
|
||||
/// Consume all leading whitespace, advancing `index` as appropriate.
|
||||
#[must_use = "This does not modify the original slice."]
|
||||
pub(crate) fn consume_whitespace<'a>(bytes: &'a [u8], index: &mut usize) -> &'a [u8] {
|
||||
let first_non_whitespace = bytes
|
||||
.iter()
|
||||
.position(|c| !c.is_ascii_whitespace())
|
||||
.unwrap_or(bytes.len());
|
||||
*index += first_non_whitespace;
|
||||
&bytes[first_non_whitespace..]
|
||||
}
|
||||
}
|
||||
|
||||
/// Well-known formats, typically RFCs.
|
||||
pub mod well_known {
|
||||
/// The format described in [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6).
|
||||
///
|
||||
/// Format example: 1985-04-12T23:20:50.52Z
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{format_description::well_known::Rfc3339, macros::datetime, OffsetDateTime};
|
||||
/// assert_eq!(
|
||||
/// OffsetDateTime::parse("1985-04-12T23:20:50.52Z", &Rfc3339)?,
|
||||
/// datetime!(1985-04-12 23:20:50.52 +00:00)
|
||||
/// );
|
||||
/// # Ok::<_, time::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{format_description::well_known::Rfc3339, macros::datetime};
|
||||
/// assert_eq!(
|
||||
/// datetime!(1985-04-12 23:20:50.52 +00:00).format(&Rfc3339)?,
|
||||
/// "1985-04-12T23:20:50.52Z"
|
||||
/// );
|
||||
/// # Ok::<_, time::Error>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Rfc3339;
|
||||
|
||||
/// The format described in [RFC 2822](https://tools.ietf.org/html/rfc2822#section-3.3).
|
||||
///
|
||||
/// Example: Fri, 21 Nov 1997 09:55:06 -0600
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use time::{format_description::well_known::Rfc2822, macros::datetime, OffsetDateTime};
|
||||
/// assert_eq!(
|
||||
/// OffsetDateTime::parse("Sat, 12 Jun 1993 13:25:19 GMT", &Rfc2822)?,
|
||||
/// datetime!(1993-06-12 13:25:19 +00:00)
|
||||
/// );
|
||||
/// # Ok::<_, time::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{format_description::well_known::Rfc2822, macros::datetime};
|
||||
/// assert_eq!(
|
||||
/// datetime!(1997-11-21 09:55:06 -06:00).format(&Rfc2822)?,
|
||||
/// "Fri, 21 Nov 1997 09:55:06 -0600"
|
||||
/// );
|
||||
/// # Ok::<_, time::Error>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Rfc2822;
|
||||
}
|
||||
|
||||
/// A complete description of how to format and parse a type.
|
||||
#[non_exhaustive]
|
||||
#[cfg_attr(not(feature = "alloc"), derive(Debug))]
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum FormatItem<'a> {
|
||||
/// Bytes that are formatted as-is.
|
||||
///
|
||||
/// **Note**: If you call the `format` method that returns a `String`, these bytes will be
|
||||
/// passed through `String::from_utf8_lossy`.
|
||||
Literal(&'a [u8]),
|
||||
/// A minimal representation of a single non-literal item.
|
||||
Component(Component),
|
||||
/// A series of literals or components that collectively form a partial or complete
|
||||
/// description.
|
||||
Compound(&'a [Self]),
|
||||
/// A `FormatItem` that may or may not be present when parsing. If parsing fails, there will be
|
||||
/// no effect on the resulting `struct`.
|
||||
///
|
||||
/// This variant has no effect on formatting, as the value is guaranteed to be present.
|
||||
Optional(&'a Self),
|
||||
/// A series of `FormatItem`s where, when parsing, the first successful parse is used. When
|
||||
/// formatting, the first element of the slice is used. An empty slice is a no-op when
|
||||
/// formatting or parsing.
|
||||
First(&'a [Self]),
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl fmt::Debug for FormatItem<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
FormatItem::Literal(literal) => f.write_str(&String::from_utf8_lossy(literal)),
|
||||
FormatItem::Component(component) => component.fmt(f),
|
||||
FormatItem::Compound(compound) => compound.fmt(f),
|
||||
FormatItem::Optional(item) => f.debug_tuple("Optional").field(item).finish(),
|
||||
FormatItem::First(items) => f.debug_tuple("First").field(items).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Component> for FormatItem<'_> {
|
||||
fn from(component: Component) -> Self {
|
||||
Self::Component(component)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<FormatItem<'_>> for Component {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(value: FormatItem<'_>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
FormatItem::Component(component) => Ok(component),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [FormatItem<'_>]> for FormatItem<'a> {
|
||||
fn from(items: &'a [FormatItem<'_>]) -> FormatItem<'a> {
|
||||
FormatItem::Compound(items)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<FormatItem<'a>> for &[FormatItem<'a>] {
|
||||
type Error = error::DifferentVariant;
|
||||
|
||||
fn try_from(value: FormatItem<'a>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
FormatItem::Compound(items) => Ok(items),
|
||||
_ => Err(error::DifferentVariant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Component> for FormatItem<'_> {
|
||||
fn eq(&self, rhs: &Component) -> bool {
|
||||
matches!(self, FormatItem::Component(component) if component == rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<FormatItem<'_>> for Component {
|
||||
fn eq(&self, rhs: &FormatItem<'_>) -> bool {
|
||||
rhs == self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&[FormatItem<'_>]> for FormatItem<'_> {
|
||||
fn eq(&self, rhs: &&[FormatItem<'_>]) -> bool {
|
||||
matches!(self, FormatItem::Compound(compound) if compound == rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<FormatItem<'_>> for &[FormatItem<'_>] {
|
||||
fn eq(&self, rhs: &FormatItem<'_>) -> bool {
|
||||
rhs == self
|
||||
}
|
||||
}
|
|
@ -0,0 +1,509 @@
|
|||
//! Various modifiers for components.
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use alloc::string::String;
|
||||
#[cfg(feature = "alloc")]
|
||||
use core::mem;
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
use crate::{error::InvalidFormatDescription, format_description::helper};
|
||||
|
||||
// region: date modifiers
|
||||
/// Day of the month.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Day {
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
}
|
||||
|
||||
/// The representation of a month.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MonthRepr {
|
||||
/// The number of the month (January is 1, December is 12).
|
||||
Numerical,
|
||||
/// The long form of the month name (e.g. "January").
|
||||
Long,
|
||||
/// The short form of the month name (e.g. "Jan").
|
||||
Short,
|
||||
}
|
||||
|
||||
/// Month of the year.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Month {
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
/// What form of representation should be used?
|
||||
pub repr: MonthRepr,
|
||||
/// Is the value case sensitive when parsing?
|
||||
pub case_sensitive: bool,
|
||||
}
|
||||
|
||||
/// Ordinal day of the year.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Ordinal {
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
}
|
||||
|
||||
/// The representation used for the day of the week.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WeekdayRepr {
|
||||
/// The short form of the weekday (e.g. "Mon").
|
||||
Short,
|
||||
/// The long form of the weekday (e.g. "Monday").
|
||||
Long,
|
||||
/// A numerical representation using Sunday as the first day of the week.
|
||||
///
|
||||
/// Sunday is either 0 or 1, depending on the other modifier's value.
|
||||
Sunday,
|
||||
/// A numerical representation using Monday as the first day of the week.
|
||||
///
|
||||
/// Monday is either 0 or 1, depending on the other modifier's value.
|
||||
Monday,
|
||||
}
|
||||
|
||||
/// Day of the week.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Weekday {
|
||||
/// What form of representation should be used?
|
||||
pub repr: WeekdayRepr,
|
||||
/// When using a numerical representation, should it be zero or one-indexed?
|
||||
pub one_indexed: bool,
|
||||
/// Is the value case sensitive when parsing?
|
||||
pub case_sensitive: bool,
|
||||
}
|
||||
|
||||
/// The representation used for the week number.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WeekNumberRepr {
|
||||
/// Week 1 is the week that contains January 4.
|
||||
Iso,
|
||||
/// Week 1 begins on the first Sunday of the calendar year.
|
||||
Sunday,
|
||||
/// Week 1 begins on the first Monday of the calendar year.
|
||||
Monday,
|
||||
}
|
||||
|
||||
/// Week within the year.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct WeekNumber {
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
/// What kind of representation should be used?
|
||||
pub repr: WeekNumberRepr,
|
||||
}
|
||||
|
||||
/// The representation used for a year value.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum YearRepr {
|
||||
/// The full value of the year.
|
||||
Full,
|
||||
/// Only the last two digits of the year.
|
||||
LastTwo,
|
||||
}
|
||||
|
||||
/// Year of the date.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Year {
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
/// What kind of representation should be used?
|
||||
pub repr: YearRepr,
|
||||
/// Whether the value is based on the ISO week number or the Gregorian calendar.
|
||||
pub iso_week_based: bool,
|
||||
/// Whether the `+` sign is present when a positive year contains fewer than five digits.
|
||||
pub sign_is_mandatory: bool,
|
||||
}
|
||||
// endregion date modifiers
|
||||
|
||||
// region: time modifiers
|
||||
/// Hour of the day.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Hour {
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
/// Is the hour displayed using a 12 or 24-hour clock?
|
||||
pub is_12_hour_clock: bool,
|
||||
}
|
||||
|
||||
/// Minute within the hour.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Minute {
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
}
|
||||
|
||||
/// AM/PM part of the time.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Period {
|
||||
/// Is the period uppercase or lowercase?
|
||||
pub is_uppercase: bool,
|
||||
/// Is the value case sensitive when parsing?
|
||||
///
|
||||
/// Note that when `false`, the `is_uppercase` field has no effect on parsing behavior.
|
||||
pub case_sensitive: bool,
|
||||
}
|
||||
|
||||
/// Second within the minute.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Second {
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
}
|
||||
|
||||
/// The number of digits present in a subsecond representation.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SubsecondDigits {
|
||||
/// Exactly one digit.
|
||||
One,
|
||||
/// Exactly two digits.
|
||||
Two,
|
||||
/// Exactly three digits.
|
||||
Three,
|
||||
/// Exactly four digits.
|
||||
Four,
|
||||
/// Exactly five digits.
|
||||
Five,
|
||||
/// Exactly six digits.
|
||||
Six,
|
||||
/// Exactly seven digits.
|
||||
Seven,
|
||||
/// Exactly eight digits.
|
||||
Eight,
|
||||
/// Exactly nine digits.
|
||||
Nine,
|
||||
/// Any number of digits (up to nine) that is at least one. When formatting, the minimum digits
|
||||
/// necessary will be used.
|
||||
OneOrMore,
|
||||
}
|
||||
|
||||
/// Subsecond within the second.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Subsecond {
|
||||
/// How many digits are present in the component?
|
||||
pub digits: SubsecondDigits,
|
||||
}
|
||||
// endregion time modifiers
|
||||
|
||||
// region: offset modifiers
|
||||
/// Hour of the UTC offset.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OffsetHour {
|
||||
/// Whether the `+` sign is present on positive values.
|
||||
pub sign_is_mandatory: bool,
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
}
|
||||
|
||||
/// Minute within the hour of the UTC offset.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OffsetMinute {
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
}
|
||||
|
||||
/// Second within the minute of the UTC offset.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct OffsetSecond {
|
||||
/// The padding to obtain the minimum width.
|
||||
pub padding: Padding,
|
||||
}
|
||||
// endregion offset modifiers
|
||||
|
||||
/// Type of padding to ensure a minimum width.
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Padding {
|
||||
/// A space character (` `) should be used as padding.
|
||||
Space,
|
||||
/// A zero character (`0`) should be used as padding.
|
||||
Zero,
|
||||
/// There is no padding. This can result in a width below the otherwise minimum number of
|
||||
/// characters.
|
||||
None,
|
||||
}
|
||||
|
||||
/// Generate the provided code if and only if `pub` is present.
|
||||
macro_rules! if_pub {
|
||||
(pub $(#[$attr:meta])*; $($x:tt)*) => {
|
||||
$(#[$attr])*
|
||||
///
|
||||
/// This function exists since [`Default::default()`] cannot be used in a `const` context.
|
||||
/// It may be removed once that becomes possible. As the [`Default`] trait is in the
|
||||
/// prelude, removing this function in the future will not cause any resolution failures for
|
||||
/// the overwhelming majority of users; only users who use `#![no_implicit_prelude]` will be
|
||||
/// affected. As such it will not be considered a breaking change.
|
||||
$($x)*
|
||||
};
|
||||
($($_:tt)*) => {};
|
||||
}
|
||||
|
||||
/// Implement `Default` for the given type. This also generates an inherent implementation of a
|
||||
/// `default` method that is `const fn`, permitting the default value to be used in const contexts.
|
||||
// Every modifier should use this macro rather than a derived `Default`.
|
||||
macro_rules! impl_const_default {
|
||||
($($(#[$doc:meta])* $(@$pub:ident)? $type:ty => $default:expr;)*) => {$(
|
||||
impl $type {
|
||||
if_pub! {
|
||||
$($pub)?
|
||||
$(#[$doc])*;
|
||||
pub const fn default() -> Self {
|
||||
$default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(#[$doc])*
|
||||
impl Default for $type {
|
||||
fn default() -> Self {
|
||||
$default
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
impl_const_default! {
|
||||
/// Creates a modifier that indicates the value is [padded with zeroes](Padding::Zero).
|
||||
@pub Day => Self { padding: Padding::Zero };
|
||||
/// Creates a modifier that indicates the value uses the
|
||||
/// [`Numerical`](Self::Numerical) representation.
|
||||
MonthRepr => Self::Numerical;
|
||||
/// Creates an instance of this type that indicates the value uses the
|
||||
/// [`Numerical`](MonthRepr::Numerical) representation, is [padded with zeroes](Padding::Zero),
|
||||
/// and is case-sensitive when parsing.
|
||||
@pub Month => Self {
|
||||
padding: Padding::Zero,
|
||||
repr: MonthRepr::Numerical,
|
||||
case_sensitive: true,
|
||||
};
|
||||
/// Creates a modifier that indicates the value is [padded with zeroes](Padding::Zero).
|
||||
@pub Ordinal => Self { padding: Padding::Zero };
|
||||
/// Creates a modifier that indicates the value uses the [`Long`](Self::Long) representation.
|
||||
WeekdayRepr => Self::Long;
|
||||
/// Creates a modifier that indicates the value uses the [`Long`](WeekdayRepr::Long)
|
||||
/// representation and is case-sensitive when parsing. If the representation is changed to a
|
||||
/// numerical one, the instance defaults to one-based indexing.
|
||||
@pub Weekday => Self {
|
||||
repr: WeekdayRepr::Long,
|
||||
one_indexed: true,
|
||||
case_sensitive: true,
|
||||
};
|
||||
/// Creates a modifier that indicates that the value uses the [`Iso`](Self::Iso) representation.
|
||||
WeekNumberRepr => Self::Iso;
|
||||
/// Creates a modifier that indicates that the value is [padded with zeroes](Padding::Zero)
|
||||
/// and uses the [`Iso`](WeekNumberRepr::Iso) representation.
|
||||
@pub WeekNumber => Self {
|
||||
padding: Padding::Zero,
|
||||
repr: WeekNumberRepr::Iso,
|
||||
};
|
||||
/// Creates a modifier that indicates the value uses the [`Full`](Self::Full) representation.
|
||||
YearRepr => Self::Full;
|
||||
/// Creates a modifier that indicates the value uses the [`Full`](YearRepr::Full)
|
||||
/// representation, is [padded with zeroes](Padding::Zero), uses the Gregorian calendar as its
|
||||
/// base, and only includes the year's sign if necessary.
|
||||
@pub Year => Self {
|
||||
padding: Padding::Zero,
|
||||
repr: YearRepr::Full,
|
||||
iso_week_based: false,
|
||||
sign_is_mandatory: false,
|
||||
};
|
||||
/// Creates a modifier that indicates the value is [padded with zeroes](Padding::Zero) and
|
||||
/// has the 24-hour representation.
|
||||
@pub Hour => Self {
|
||||
padding: Padding::Zero,
|
||||
is_12_hour_clock: false,
|
||||
};
|
||||
/// Creates a modifier that indicates the value is [padded with zeroes](Padding::Zero).
|
||||
@pub Minute => Self { padding: Padding::Zero };
|
||||
/// Creates a modifier that indicates the value uses the upper-case representation and is
|
||||
/// case-sensitive when parsing.
|
||||
@pub Period => Self {
|
||||
is_uppercase: true,
|
||||
case_sensitive: true,
|
||||
};
|
||||
/// Creates a modifier that indicates the value is [padded with zeroes](Padding::Zero).
|
||||
@pub Second => Self { padding: Padding::Zero };
|
||||
/// Creates a modifier that indicates the stringified value contains [one or more
|
||||
/// digits](Self::OneOrMore).
|
||||
SubsecondDigits => Self::OneOrMore;
|
||||
/// Creates a modifier that indicates the stringified value contains [one or more
|
||||
/// digits](SubsecondDigits::OneOrMore).
|
||||
@pub Subsecond => Self { digits: SubsecondDigits::OneOrMore };
|
||||
/// Creates a modifier that indicates the value uses the `+` sign for all positive values
|
||||
/// and is [padded with zeroes](Padding::Zero).
|
||||
@pub OffsetHour => Self {
|
||||
sign_is_mandatory: true,
|
||||
padding: Padding::Zero,
|
||||
};
|
||||
/// Creates a modifier that indicates the value is [padded with zeroes](Padding::Zero).
|
||||
@pub OffsetMinute => Self { padding: Padding::Zero };
|
||||
/// Creates a modifier that indicates the value is [padded with zeroes](Padding::Zero).
|
||||
@pub OffsetSecond => Self { padding: Padding::Zero };
|
||||
/// Creates a modifier that indicates the value is [padded with zeroes](Self::Zero).
|
||||
Padding => Self::Zero;
|
||||
}
|
||||
|
||||
/// The modifiers parsed for any given component. `None` indicates the modifier was not present.
|
||||
#[cfg(feature = "alloc")]
|
||||
#[allow(clippy::missing_docs_in_private_items)] // fields
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Modifiers {
|
||||
pub(crate) padding: Option<Padding>,
|
||||
pub(crate) hour_is_12_hour_clock: Option<bool>,
|
||||
pub(crate) period_is_uppercase: Option<bool>,
|
||||
pub(crate) month_repr: Option<MonthRepr>,
|
||||
pub(crate) subsecond_digits: Option<SubsecondDigits>,
|
||||
pub(crate) weekday_repr: Option<WeekdayRepr>,
|
||||
pub(crate) weekday_is_one_indexed: Option<bool>,
|
||||
pub(crate) week_number_repr: Option<WeekNumberRepr>,
|
||||
pub(crate) year_repr: Option<YearRepr>,
|
||||
pub(crate) year_is_iso_week_based: Option<bool>,
|
||||
pub(crate) sign_is_mandatory: Option<bool>,
|
||||
pub(crate) case_sensitive: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl Modifiers {
|
||||
/// Parse the modifiers of a given component.
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub(crate) fn parse(
|
||||
component_name: &[u8],
|
||||
mut bytes: &[u8],
|
||||
index: &mut usize,
|
||||
) -> Result<Self, InvalidFormatDescription> {
|
||||
let mut modifiers = Self::default();
|
||||
|
||||
while !bytes.is_empty() {
|
||||
// Trim any whitespace between modifiers.
|
||||
bytes = helper::consume_whitespace(bytes, index);
|
||||
|
||||
let modifier;
|
||||
if let Some(whitespace_loc) = bytes.iter().position(u8::is_ascii_whitespace) {
|
||||
*index += whitespace_loc;
|
||||
modifier = &bytes[..whitespace_loc];
|
||||
bytes = &bytes[whitespace_loc..];
|
||||
} else {
|
||||
modifier = mem::take(&mut bytes);
|
||||
}
|
||||
|
||||
if modifier.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
match (component_name, modifier) {
|
||||
(
|
||||
b"day" | b"hour" | b"minute" | b"month" | b"offset_hour" | b"offset_minute"
|
||||
| b"offset_second" | b"ordinal" | b"second" | b"week_number" | b"year",
|
||||
b"padding:space",
|
||||
) => modifiers.padding = Some(Padding::Space),
|
||||
(
|
||||
b"day" | b"hour" | b"minute" | b"month" | b"offset_hour" | b"offset_minute"
|
||||
| b"offset_second" | b"ordinal" | b"second" | b"week_number" | b"year",
|
||||
b"padding:zero",
|
||||
) => modifiers.padding = Some(Padding::Zero),
|
||||
(
|
||||
b"day" | b"hour" | b"minute" | b"month" | b"offset_hour" | b"offset_minute"
|
||||
| b"offset_second" | b"ordinal" | b"second" | b"week_number" | b"year",
|
||||
b"padding:none",
|
||||
) => modifiers.padding = Some(Padding::None),
|
||||
(b"hour", b"repr:24") => modifiers.hour_is_12_hour_clock = Some(false),
|
||||
(b"hour", b"repr:12") => modifiers.hour_is_12_hour_clock = Some(true),
|
||||
(b"month" | b"period" | b"weekday", b"case_sensitive:true") => {
|
||||
modifiers.case_sensitive = Some(true)
|
||||
}
|
||||
(b"month" | b"period" | b"weekday", b"case_sensitive:false") => {
|
||||
modifiers.case_sensitive = Some(false)
|
||||
}
|
||||
(b"month", b"repr:numerical") => modifiers.month_repr = Some(MonthRepr::Numerical),
|
||||
(b"month", b"repr:long") => modifiers.month_repr = Some(MonthRepr::Long),
|
||||
(b"month", b"repr:short") => modifiers.month_repr = Some(MonthRepr::Short),
|
||||
(b"offset_hour" | b"year", b"sign:automatic") => {
|
||||
modifiers.sign_is_mandatory = Some(false);
|
||||
}
|
||||
(b"offset_hour" | b"year", b"sign:mandatory") => {
|
||||
modifiers.sign_is_mandatory = Some(true);
|
||||
}
|
||||
(b"period", b"case:upper") => modifiers.period_is_uppercase = Some(true),
|
||||
(b"period", b"case:lower") => modifiers.period_is_uppercase = Some(false),
|
||||
(b"subsecond", b"digits:1") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::One);
|
||||
}
|
||||
(b"subsecond", b"digits:2") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Two);
|
||||
}
|
||||
(b"subsecond", b"digits:3") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Three);
|
||||
}
|
||||
(b"subsecond", b"digits:4") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Four);
|
||||
}
|
||||
(b"subsecond", b"digits:5") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Five);
|
||||
}
|
||||
(b"subsecond", b"digits:6") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Six);
|
||||
}
|
||||
(b"subsecond", b"digits:7") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Seven);
|
||||
}
|
||||
(b"subsecond", b"digits:8") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Eight);
|
||||
}
|
||||
(b"subsecond", b"digits:9") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::Nine);
|
||||
}
|
||||
(b"subsecond", b"digits:1+") => {
|
||||
modifiers.subsecond_digits = Some(SubsecondDigits::OneOrMore);
|
||||
}
|
||||
(b"weekday", b"repr:short") => modifiers.weekday_repr = Some(WeekdayRepr::Short),
|
||||
(b"weekday", b"repr:long") => modifiers.weekday_repr = Some(WeekdayRepr::Long),
|
||||
(b"weekday", b"repr:sunday") => modifiers.weekday_repr = Some(WeekdayRepr::Sunday),
|
||||
(b"weekday", b"repr:monday") => modifiers.weekday_repr = Some(WeekdayRepr::Monday),
|
||||
(b"weekday", b"one_indexed:true") => modifiers.weekday_is_one_indexed = Some(true),
|
||||
(b"weekday", b"one_indexed:false") => {
|
||||
modifiers.weekday_is_one_indexed = Some(false);
|
||||
}
|
||||
(b"week_number", b"repr:iso") => {
|
||||
modifiers.week_number_repr = Some(WeekNumberRepr::Iso);
|
||||
}
|
||||
(b"week_number", b"repr:sunday") => {
|
||||
modifiers.week_number_repr = Some(WeekNumberRepr::Sunday);
|
||||
}
|
||||
(b"week_number", b"repr:monday") => {
|
||||
modifiers.week_number_repr = Some(WeekNumberRepr::Monday);
|
||||
}
|
||||
(b"year", b"repr:full") => modifiers.year_repr = Some(YearRepr::Full),
|
||||
(b"year", b"repr:last_two") => modifiers.year_repr = Some(YearRepr::LastTwo),
|
||||
(b"year", b"base:calendar") => modifiers.year_is_iso_week_based = Some(false),
|
||||
(b"year", b"base:iso_week") => modifiers.year_is_iso_week_based = Some(true),
|
||||
_ => {
|
||||
return Err(InvalidFormatDescription::InvalidModifier {
|
||||
value: String::from_utf8_lossy(modifier).into_owned(),
|
||||
index: *index,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(modifiers)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
//! Parse a format description into a standardized representation.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::error::InvalidFormatDescription;
|
||||
use crate::format_description::component::{Component, NakedComponent};
|
||||
use crate::format_description::{helper, modifier, FormatItem};
|
||||
|
||||
/// The item parsed and remaining chunk of the format description after one iteration.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ParsedItem<'a> {
|
||||
/// The item that was parsed.
|
||||
pub(crate) item: FormatItem<'a>,
|
||||
/// What is left of the input string after the item was parsed.
|
||||
pub(crate) remaining: &'a [u8],
|
||||
}
|
||||
|
||||
/// Parse a component from the format description. Neither the leading nor trailing bracket should
|
||||
/// be present in the parameter.
|
||||
fn parse_component(mut s: &[u8], index: &mut usize) -> Result<Component, InvalidFormatDescription> {
|
||||
// Trim any whitespace between the opening bracket and the component name.
|
||||
s = helper::consume_whitespace(s, index);
|
||||
|
||||
// Everything before the first whitespace is the component name.
|
||||
let component_index = *index;
|
||||
let whitespace_loc = s
|
||||
.iter()
|
||||
.position(u8::is_ascii_whitespace)
|
||||
.unwrap_or(s.len());
|
||||
*index += whitespace_loc;
|
||||
let component_name = &s[..whitespace_loc];
|
||||
s = &s[whitespace_loc..];
|
||||
s = helper::consume_whitespace(s, index);
|
||||
|
||||
Ok(NakedComponent::parse(component_name, component_index)?
|
||||
.attach_modifiers(&modifier::Modifiers::parse(component_name, s, index)?))
|
||||
}
|
||||
|
||||
/// Parse a literal string from the format description.
|
||||
fn parse_literal<'a>(s: &'a [u8], index: &mut usize) -> ParsedItem<'a> {
|
||||
let loc = s.iter().position(|&c| c == b'[').unwrap_or(s.len());
|
||||
*index += loc;
|
||||
ParsedItem {
|
||||
item: FormatItem::Literal(&s[..loc]),
|
||||
remaining: &s[loc..],
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse either a literal or a component from the format description.
|
||||
fn parse_item<'a>(
|
||||
s: &'a [u8],
|
||||
index: &mut usize,
|
||||
) -> Result<ParsedItem<'a>, InvalidFormatDescription> {
|
||||
if let [b'[', b'[', remaining @ ..] = s {
|
||||
*index += 2;
|
||||
return Ok(ParsedItem {
|
||||
item: FormatItem::Literal(&[b'[']),
|
||||
remaining,
|
||||
});
|
||||
};
|
||||
|
||||
if s.starts_with(&[b'[']) {
|
||||
if let Some(bracket_index) = s.iter().position(|&c| c == b']') {
|
||||
*index += 1; // opening bracket
|
||||
let ret_val = ParsedItem {
|
||||
item: FormatItem::Component(parse_component(&s[1..bracket_index], index)?),
|
||||
remaining: &s[bracket_index + 1..],
|
||||
};
|
||||
*index += 1; // closing bracket
|
||||
Ok(ret_val)
|
||||
} else {
|
||||
Err(InvalidFormatDescription::UnclosedOpeningBracket { index: *index })
|
||||
}
|
||||
} else {
|
||||
Ok(parse_literal(s, index))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a sequence of items from the format description.
|
||||
///
|
||||
/// The syntax for the format description can be found in [the
|
||||
/// book](https://time-rs.github.io/book/api/format-description.html).
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "alloc")))]
|
||||
pub fn parse(s: &str) -> Result<Vec<FormatItem<'_>>, InvalidFormatDescription> {
|
||||
let mut compound = Vec::new();
|
||||
let mut loc = 0;
|
||||
|
||||
let mut s = s.as_bytes();
|
||||
|
||||
while !s.is_empty() {
|
||||
let ParsedItem { item, remaining } = parse_item(s, &mut loc)?;
|
||||
s = remaining;
|
||||
compound.push(item);
|
||||
}
|
||||
|
||||
Ok(compound)
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
//! A trait that can be used to format an item from its components.
|
||||
|
||||
use core::ops::Deref;
|
||||
use std::io;
|
||||
|
||||
use crate::format_description::well_known::{Rfc2822, Rfc3339};
|
||||
use crate::format_description::FormatItem;
|
||||
use crate::formatting::{
|
||||
format_component, format_number_pad_zero, write, MONTH_NAMES, WEEKDAY_NAMES,
|
||||
};
|
||||
use crate::{error, Date, Time, UtcOffset};
|
||||
|
||||
/// A type that can be formatted.
|
||||
#[cfg_attr(__time_03_docs, doc(notable_trait))]
|
||||
pub trait Formattable: sealed::Sealed {}
|
||||
impl Formattable for FormatItem<'_> {}
|
||||
impl Formattable for [FormatItem<'_>] {}
|
||||
impl Formattable for Rfc3339 {}
|
||||
impl Formattable for Rfc2822 {}
|
||||
impl<T: Deref> Formattable for T where T::Target: Formattable {}
|
||||
|
||||
/// Seal the trait to prevent downstream users from implementing it.
|
||||
mod sealed {
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use super::*;
|
||||
|
||||
/// Format the item using a format description, the intended output, and the various components.
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "formatting")))]
|
||||
pub trait Sealed {
|
||||
/// Format the item into the provided output, returning the number of bytes written.
|
||||
fn format_into(
|
||||
&self,
|
||||
output: &mut impl io::Write,
|
||||
date: Option<Date>,
|
||||
time: Option<Time>,
|
||||
offset: Option<UtcOffset>,
|
||||
) -> Result<usize, error::Format>;
|
||||
|
||||
/// Format the item directly to a `String`.
|
||||
fn format(
|
||||
&self,
|
||||
date: Option<Date>,
|
||||
time: Option<Time>,
|
||||
offset: Option<UtcOffset>,
|
||||
) -> Result<String, error::Format> {
|
||||
let mut buf = Vec::new();
|
||||
self.format_into(&mut buf, date, time, offset)?;
|
||||
Ok(String::from_utf8_lossy(&buf).into_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region: custom formats
|
||||
impl<'a> sealed::Sealed for FormatItem<'a> {
|
||||
fn format_into(
|
||||
&self,
|
||||
output: &mut impl io::Write,
|
||||
date: Option<Date>,
|
||||
time: Option<Time>,
|
||||
offset: Option<UtcOffset>,
|
||||
) -> Result<usize, error::Format> {
|
||||
Ok(match *self {
|
||||
Self::Literal(literal) => write(output, literal)?,
|
||||
Self::Component(component) => format_component(output, component, date, time, offset)?,
|
||||
Self::Compound(items) => items.format_into(output, date, time, offset)?,
|
||||
Self::Optional(item) => item.format_into(output, date, time, offset)?,
|
||||
Self::First(items) => match items {
|
||||
[] => 0,
|
||||
[item, ..] => item.format_into(output, date, time, offset)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sealed::Sealed for [FormatItem<'a>] {
|
||||
fn format_into(
|
||||
&self,
|
||||
output: &mut impl io::Write,
|
||||
date: Option<Date>,
|
||||
time: Option<Time>,
|
||||
offset: Option<UtcOffset>,
|
||||
) -> Result<usize, error::Format> {
|
||||
let mut bytes = 0;
|
||||
for item in self.iter() {
|
||||
bytes += item.format_into(output, date, time, offset)?;
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Deref> sealed::Sealed for T
|
||||
where
|
||||
T::Target: sealed::Sealed,
|
||||
{
|
||||
fn format_into(
|
||||
&self,
|
||||
output: &mut impl io::Write,
|
||||
date: Option<Date>,
|
||||
time: Option<Time>,
|
||||
offset: Option<UtcOffset>,
|
||||
) -> Result<usize, error::Format> {
|
||||
self.deref().format_into(output, date, time, offset)
|
||||
}
|
||||
}
|
||||
// endregion custom formats
|
||||
|
||||
// region: well-known formats
|
||||
impl sealed::Sealed for Rfc2822 {
|
||||
fn format_into(
|
||||
&self,
|
||||
output: &mut impl io::Write,
|
||||
date: Option<Date>,
|
||||
time: Option<Time>,
|
||||
offset: Option<UtcOffset>,
|
||||
) -> Result<usize, error::Format> {
|
||||
let date = date.ok_or(error::Format::InsufficientTypeInformation)?;
|
||||
let time = time.ok_or(error::Format::InsufficientTypeInformation)?;
|
||||
let offset = offset.ok_or(error::Format::InsufficientTypeInformation)?;
|
||||
|
||||
let mut bytes = 0;
|
||||
|
||||
let (year, month, day) = date.to_calendar_date();
|
||||
|
||||
if year < 1900 {
|
||||
return Err(error::Format::InvalidComponent("year"));
|
||||
}
|
||||
if offset.seconds_past_minute() != 0 {
|
||||
return Err(error::Format::InvalidComponent("offset_second"));
|
||||
}
|
||||
|
||||
bytes += write(
|
||||
output,
|
||||
&WEEKDAY_NAMES[date.weekday().number_days_from_monday() as usize][..3],
|
||||
)?;
|
||||
bytes += write(output, b", ")?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, day)?;
|
||||
bytes += write(output, b" ")?;
|
||||
bytes += write(output, &MONTH_NAMES[month as usize - 1][..3])?;
|
||||
bytes += write(output, b" ")?;
|
||||
bytes += format_number_pad_zero::<_, _, 4>(output, year as u32)?;
|
||||
bytes += write(output, b" ")?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, time.hour())?;
|
||||
bytes += write(output, b":")?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, time.minute())?;
|
||||
bytes += write(output, b":")?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, time.second())?;
|
||||
bytes += write(output, b" ")?;
|
||||
bytes += write(output, if offset.is_negative() { b"-" } else { b"+" })?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, offset.whole_hours().unsigned_abs())?;
|
||||
bytes +=
|
||||
format_number_pad_zero::<_, _, 2>(output, offset.minutes_past_hour().unsigned_abs())?;
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl sealed::Sealed for Rfc3339 {
|
||||
fn format_into(
|
||||
&self,
|
||||
output: &mut impl io::Write,
|
||||
date: Option<Date>,
|
||||
time: Option<Time>,
|
||||
offset: Option<UtcOffset>,
|
||||
) -> Result<usize, error::Format> {
|
||||
let date = date.ok_or(error::Format::InsufficientTypeInformation)?;
|
||||
let time = time.ok_or(error::Format::InsufficientTypeInformation)?;
|
||||
let offset = offset.ok_or(error::Format::InsufficientTypeInformation)?;
|
||||
|
||||
let mut bytes = 0;
|
||||
|
||||
let year = date.year();
|
||||
|
||||
if !(0..10_000).contains(&year) {
|
||||
return Err(error::Format::InvalidComponent("year"));
|
||||
}
|
||||
if offset.seconds_past_minute() != 0 {
|
||||
return Err(error::Format::InvalidComponent("offset_second"));
|
||||
}
|
||||
|
||||
bytes += format_number_pad_zero::<_, _, 4>(output, year as u32)?;
|
||||
bytes += write(output, &[b'-'])?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, date.month() as u8)?;
|
||||
bytes += write(output, &[b'-'])?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, date.day())?;
|
||||
bytes += write(output, &[b'T'])?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, time.hour())?;
|
||||
bytes += write(output, &[b':'])?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, time.minute())?;
|
||||
bytes += write(output, &[b':'])?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, time.second())?;
|
||||
|
||||
#[allow(clippy::if_not_else)]
|
||||
if time.nanosecond() != 0 {
|
||||
let nanos = time.nanosecond();
|
||||
bytes += write(output, &[b'.'])?;
|
||||
bytes += if nanos % 10 != 0 {
|
||||
format_number_pad_zero::<_, _, 9>(output, nanos)
|
||||
} else if (nanos / 10) % 10 != 0 {
|
||||
format_number_pad_zero::<_, _, 8>(output, nanos / 10)
|
||||
} else if (nanos / 100) % 10 != 0 {
|
||||
format_number_pad_zero::<_, _, 7>(output, nanos / 100)
|
||||
} else if (nanos / 1_000) % 10 != 0 {
|
||||
format_number_pad_zero::<_, _, 6>(output, nanos / 1_000)
|
||||
} else if (nanos / 10_000) % 10 != 0 {
|
||||
format_number_pad_zero::<_, _, 5>(output, nanos / 10_000)
|
||||
} else if (nanos / 100_000) % 10 != 0 {
|
||||
format_number_pad_zero::<_, _, 4>(output, nanos / 100_000)
|
||||
} else if (nanos / 1_000_000) % 10 != 0 {
|
||||
format_number_pad_zero::<_, _, 3>(output, nanos / 1_000_000)
|
||||
} else if (nanos / 10_000_000) % 10 != 0 {
|
||||
format_number_pad_zero::<_, _, 2>(output, nanos / 10_000_000)
|
||||
} else {
|
||||
format_number_pad_zero::<_, _, 1>(output, nanos / 100_000_000)
|
||||
}?;
|
||||
}
|
||||
|
||||
if offset == UtcOffset::UTC {
|
||||
bytes += write(output, &[b'Z'])?;
|
||||
return Ok(bytes);
|
||||
}
|
||||
|
||||
bytes += write(
|
||||
output,
|
||||
if offset.is_negative() {
|
||||
&[b'-']
|
||||
} else {
|
||||
&[b'+']
|
||||
},
|
||||
)?;
|
||||
bytes += format_number_pad_zero::<_, _, 2>(output, offset.whole_hours().unsigned_abs())?;
|
||||
bytes += write(output, &[b':'])?;
|
||||
bytes +=
|
||||
format_number_pad_zero::<_, _, 2>(output, offset.minutes_past_hour().unsigned_abs())?;
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
// endregion well-known formats
|
|
@ -0,0 +1,457 @@
|
|||
//! Formatting for various types.
|
||||
|
||||
pub(crate) mod formattable;
|
||||
|
||||
use std::io;
|
||||
|
||||
pub use self::formattable::Formattable;
|
||||
use crate::format_description::{modifier, Component};
|
||||
use crate::{error, Date, Time, UtcOffset};
|
||||
|
||||
#[allow(clippy::missing_docs_in_private_items)]
|
||||
const MONTH_NAMES: [&[u8]; 12] = [
|
||||
b"January",
|
||||
b"February",
|
||||
b"March",
|
||||
b"April",
|
||||
b"May",
|
||||
b"June",
|
||||
b"July",
|
||||
b"August",
|
||||
b"September",
|
||||
b"October",
|
||||
b"November",
|
||||
b"December",
|
||||
];
|
||||
|
||||
#[allow(clippy::missing_docs_in_private_items)]
|
||||
const WEEKDAY_NAMES: [&[u8]; 7] = [
|
||||
b"Monday",
|
||||
b"Tuesday",
|
||||
b"Wednesday",
|
||||
b"Thursday",
|
||||
b"Friday",
|
||||
b"Saturday",
|
||||
b"Sunday",
|
||||
];
|
||||
|
||||
// region: extension trait
|
||||
/// A trait that indicates the formatted width of the value can be determined.
|
||||
///
|
||||
/// Note that this should not be implemented for any signed integers. This forces the caller to
|
||||
/// write the sign if desired.
|
||||
pub(crate) trait DigitCount {
|
||||
/// The number of digits in the stringified value.
|
||||
fn num_digits(self) -> u8;
|
||||
}
|
||||
impl DigitCount for u8 {
|
||||
fn num_digits(self) -> u8 {
|
||||
// Using a lookup table as with u32 is *not* faster in standalone benchmarks.
|
||||
if self < 10 {
|
||||
1
|
||||
} else if self < 100 {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
}
|
||||
impl DigitCount for u16 {
|
||||
fn num_digits(self) -> u8 {
|
||||
// Using a lookup table as with u32 is *not* faster in standalone benchmarks.
|
||||
if self < 10 {
|
||||
1
|
||||
} else if self < 100 {
|
||||
2
|
||||
} else if self < 1_000 {
|
||||
3
|
||||
} else if self < 10_000 {
|
||||
4
|
||||
} else {
|
||||
5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DigitCount for u32 {
|
||||
fn num_digits(self) -> u8 {
|
||||
/// Lookup table
|
||||
const TABLE: &[u64] = &[
|
||||
0x0001_0000_0000,
|
||||
0x0001_0000_0000,
|
||||
0x0001_0000_0000,
|
||||
0x0001_FFFF_FFF6,
|
||||
0x0002_0000_0000,
|
||||
0x0002_0000_0000,
|
||||
0x0002_FFFF_FF9C,
|
||||
0x0003_0000_0000,
|
||||
0x0003_0000_0000,
|
||||
0x0003_FFFF_FC18,
|
||||
0x0004_0000_0000,
|
||||
0x0004_0000_0000,
|
||||
0x0004_0000_0000,
|
||||
0x0004_FFFF_D8F0,
|
||||
0x0005_0000_0000,
|
||||
0x0005_0000_0000,
|
||||
0x0005_FFFE_7960,
|
||||
0x0006_0000_0000,
|
||||
0x0006_0000_0000,
|
||||
0x0006_FFF0_BDC0,
|
||||
0x0007_0000_0000,
|
||||
0x0007_0000_0000,
|
||||
0x0007_0000_0000,
|
||||
0x0007_FF67_6980,
|
||||
0x0008_0000_0000,
|
||||
0x0008_0000_0000,
|
||||
0x0008_FA0A_1F00,
|
||||
0x0009_0000_0000,
|
||||
0x0009_0000_0000,
|
||||
0x0009_C465_3600,
|
||||
0x000A_0000_0000,
|
||||
0x000A_0000_0000,
|
||||
];
|
||||
((self as u64 + TABLE[31_u32.saturating_sub(self.leading_zeros()) as usize]) >> 32) as _
|
||||
}
|
||||
}
|
||||
// endregion extension trait
|
||||
|
||||
/// Write all bytes to the output, returning the number of bytes written.
|
||||
fn write(output: &mut impl io::Write, bytes: &[u8]) -> io::Result<usize> {
|
||||
output.write_all(bytes)?;
|
||||
Ok(bytes.len())
|
||||
}
|
||||
|
||||
/// Format a number with the provided padding and width.
|
||||
///
|
||||
/// The sign must be written by the caller.
|
||||
pub(crate) fn format_number<W: io::Write, V: itoa::Integer + DigitCount + Copy, const WIDTH: u8>(
|
||||
output: &mut W,
|
||||
value: V,
|
||||
padding: modifier::Padding,
|
||||
) -> Result<usize, io::Error> {
|
||||
match padding {
|
||||
modifier::Padding::Space => format_number_pad_space::<_, _, WIDTH>(output, value),
|
||||
modifier::Padding::Zero => format_number_pad_zero::<_, _, WIDTH>(output, value),
|
||||
modifier::Padding::None => write(output, itoa::Buffer::new().format(value).as_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format a number with the provided width and spaces as padding.
|
||||
///
|
||||
/// The sign must be written by the caller.
|
||||
pub(crate) fn format_number_pad_space<
|
||||
W: io::Write,
|
||||
V: itoa::Integer + DigitCount + Copy,
|
||||
const WIDTH: u8,
|
||||
>(
|
||||
output: &mut W,
|
||||
value: V,
|
||||
) -> Result<usize, io::Error> {
|
||||
let mut bytes = 0;
|
||||
for _ in 0..(WIDTH.saturating_sub(value.num_digits())) {
|
||||
bytes += write(output, &[b' '])?;
|
||||
}
|
||||
bytes += write(output, itoa::Buffer::new().format(value).as_bytes())?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Format a number with the provided width and zeros as padding.
|
||||
///
|
||||
/// The sign must be written by the caller.
|
||||
pub(crate) fn format_number_pad_zero<
|
||||
W: io::Write,
|
||||
V: itoa::Integer + DigitCount + Copy,
|
||||
const WIDTH: u8,
|
||||
>(
|
||||
output: &mut W,
|
||||
value: V,
|
||||
) -> Result<usize, io::Error> {
|
||||
let mut bytes = 0;
|
||||
for _ in 0..(WIDTH.saturating_sub(value.num_digits())) {
|
||||
bytes += write(output, &[b'0'])?;
|
||||
}
|
||||
bytes += write(output, itoa::Buffer::new().format(value).as_bytes())?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Format the provided component into the designated output. An `Err` will be returned if the
|
||||
/// component requires information that it does not provide or if the value cannot be output to the
|
||||
/// stream.
|
||||
pub(crate) fn format_component(
|
||||
output: &mut impl io::Write,
|
||||
component: Component,
|
||||
date: Option<Date>,
|
||||
time: Option<Time>,
|
||||
offset: Option<UtcOffset>,
|
||||
) -> Result<usize, error::Format> {
|
||||
use Component::*;
|
||||
Ok(match (component, date, time, offset) {
|
||||
(Day(modifier), Some(date), ..) => fmt_day(output, date, modifier)?,
|
||||
(Month(modifier), Some(date), ..) => fmt_month(output, date, modifier)?,
|
||||
(Ordinal(modifier), Some(date), ..) => fmt_ordinal(output, date, modifier)?,
|
||||
(Weekday(modifier), Some(date), ..) => fmt_weekday(output, date, modifier)?,
|
||||
(WeekNumber(modifier), Some(date), ..) => fmt_week_number(output, date, modifier)?,
|
||||
(Year(modifier), Some(date), ..) => fmt_year(output, date, modifier)?,
|
||||
(Hour(modifier), _, Some(time), _) => fmt_hour(output, time, modifier)?,
|
||||
(Minute(modifier), _, Some(time), _) => fmt_minute(output, time, modifier)?,
|
||||
(Period(modifier), _, Some(time), _) => fmt_period(output, time, modifier)?,
|
||||
(Second(modifier), _, Some(time), _) => fmt_second(output, time, modifier)?,
|
||||
(Subsecond(modifier), _, Some(time), _) => fmt_subsecond(output, time, modifier)?,
|
||||
(OffsetHour(modifier), .., Some(offset)) => fmt_offset_hour(output, offset, modifier)?,
|
||||
(OffsetMinute(modifier), .., Some(offset)) => fmt_offset_minute(output, offset, modifier)?,
|
||||
(OffsetSecond(modifier), .., Some(offset)) => fmt_offset_second(output, offset, modifier)?,
|
||||
_ => return Err(error::Format::InsufficientTypeInformation),
|
||||
})
|
||||
}
|
||||
|
||||
// region: date formatters
|
||||
/// Format the day into the designated output.
|
||||
fn fmt_day(
|
||||
output: &mut impl io::Write,
|
||||
date: Date,
|
||||
modifier::Day { padding }: modifier::Day,
|
||||
) -> Result<usize, io::Error> {
|
||||
format_number::<_, _, 2>(output, date.day(), padding)
|
||||
}
|
||||
|
||||
/// Format the month into the designated output.
|
||||
fn fmt_month(
|
||||
output: &mut impl io::Write,
|
||||
date: Date,
|
||||
modifier::Month {
|
||||
padding,
|
||||
repr,
|
||||
case_sensitive: _, // no effect on formatting
|
||||
}: modifier::Month,
|
||||
) -> Result<usize, io::Error> {
|
||||
match repr {
|
||||
modifier::MonthRepr::Numerical => {
|
||||
format_number::<_, _, 2>(output, date.month() as u8, padding)
|
||||
}
|
||||
modifier::MonthRepr::Long => write(output, MONTH_NAMES[date.month() as usize - 1]),
|
||||
modifier::MonthRepr::Short => write(output, &MONTH_NAMES[date.month() as usize - 1][..3]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the ordinal into the designated output.
|
||||
fn fmt_ordinal(
|
||||
output: &mut impl io::Write,
|
||||
date: Date,
|
||||
modifier::Ordinal { padding }: modifier::Ordinal,
|
||||
) -> Result<usize, io::Error> {
|
||||
format_number::<_, _, 3>(output, date.ordinal(), padding)
|
||||
}
|
||||
|
||||
/// Format the weekday into the designated output.
|
||||
fn fmt_weekday(
|
||||
output: &mut impl io::Write,
|
||||
date: Date,
|
||||
modifier::Weekday {
|
||||
repr,
|
||||
one_indexed,
|
||||
case_sensitive: _, // no effect on formatting
|
||||
}: modifier::Weekday,
|
||||
) -> Result<usize, io::Error> {
|
||||
match repr {
|
||||
modifier::WeekdayRepr::Short => write(
|
||||
output,
|
||||
&WEEKDAY_NAMES[date.weekday().number_days_from_monday() as usize][..3],
|
||||
),
|
||||
modifier::WeekdayRepr::Long => write(
|
||||
output,
|
||||
WEEKDAY_NAMES[date.weekday().number_days_from_monday() as usize],
|
||||
),
|
||||
modifier::WeekdayRepr::Sunday => format_number::<_, _, 1>(
|
||||
output,
|
||||
date.weekday().number_days_from_sunday() + one_indexed as u8,
|
||||
modifier::Padding::None,
|
||||
),
|
||||
modifier::WeekdayRepr::Monday => format_number::<_, _, 1>(
|
||||
output,
|
||||
date.weekday().number_days_from_monday() + one_indexed as u8,
|
||||
modifier::Padding::None,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the week number into the designated output.
|
||||
fn fmt_week_number(
|
||||
output: &mut impl io::Write,
|
||||
date: Date,
|
||||
modifier::WeekNumber { padding, repr }: modifier::WeekNumber,
|
||||
) -> Result<usize, io::Error> {
|
||||
format_number::<_, _, 2>(
|
||||
output,
|
||||
match repr {
|
||||
modifier::WeekNumberRepr::Iso => date.iso_week(),
|
||||
modifier::WeekNumberRepr::Sunday => date.sunday_based_week(),
|
||||
modifier::WeekNumberRepr::Monday => date.monday_based_week(),
|
||||
},
|
||||
padding,
|
||||
)
|
||||
}
|
||||
|
||||
/// Format the year into the designated output.
|
||||
fn fmt_year(
|
||||
output: &mut impl io::Write,
|
||||
date: Date,
|
||||
modifier::Year {
|
||||
padding,
|
||||
repr,
|
||||
iso_week_based,
|
||||
sign_is_mandatory,
|
||||
}: modifier::Year,
|
||||
) -> Result<usize, io::Error> {
|
||||
let full_year = if iso_week_based {
|
||||
date.iso_year_week().0
|
||||
} else {
|
||||
date.year()
|
||||
};
|
||||
let value = match repr {
|
||||
modifier::YearRepr::Full => full_year,
|
||||
modifier::YearRepr::LastTwo => (full_year % 100).abs(),
|
||||
};
|
||||
let format_number = match repr {
|
||||
#[cfg(feature = "large-dates")]
|
||||
modifier::YearRepr::Full if value.abs() >= 100_000 => format_number::<_, _, 6>,
|
||||
#[cfg(feature = "large-dates")]
|
||||
modifier::YearRepr::Full if value.abs() >= 10_000 => format_number::<_, _, 5>,
|
||||
modifier::YearRepr::Full => format_number::<_, _, 4>,
|
||||
modifier::YearRepr::LastTwo => format_number::<_, _, 2>,
|
||||
};
|
||||
let mut bytes = 0;
|
||||
if repr != modifier::YearRepr::LastTwo {
|
||||
if full_year < 0 {
|
||||
bytes += write(output, &[b'-'])?;
|
||||
} else if sign_is_mandatory || cfg!(feature = "large-dates") && full_year >= 10_000 {
|
||||
bytes += write(output, &[b'+'])?;
|
||||
}
|
||||
}
|
||||
bytes += format_number(output, value.unsigned_abs(), padding)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
// endregion date formatters
|
||||
|
||||
// region: time formatters
|
||||
/// Format the hour into the designated output.
|
||||
fn fmt_hour(
|
||||
output: &mut impl io::Write,
|
||||
time: Time,
|
||||
modifier::Hour {
|
||||
padding,
|
||||
is_12_hour_clock,
|
||||
}: modifier::Hour,
|
||||
) -> Result<usize, io::Error> {
|
||||
let value = match (time.hour(), is_12_hour_clock) {
|
||||
(hour, false) => hour,
|
||||
(0 | 12, true) => 12,
|
||||
(hour, true) if hour < 12 => hour,
|
||||
(hour, true) => hour - 12,
|
||||
};
|
||||
format_number::<_, _, 2>(output, value, padding)
|
||||
}
|
||||
|
||||
/// Format the minute into the designated output.
|
||||
fn fmt_minute(
|
||||
output: &mut impl io::Write,
|
||||
time: Time,
|
||||
modifier::Minute { padding }: modifier::Minute,
|
||||
) -> Result<usize, io::Error> {
|
||||
format_number::<_, _, 2>(output, time.minute(), padding)
|
||||
}
|
||||
|
||||
/// Format the period into the designated output.
|
||||
fn fmt_period(
|
||||
output: &mut impl io::Write,
|
||||
time: Time,
|
||||
modifier::Period {
|
||||
is_uppercase,
|
||||
case_sensitive: _, // no effect on formatting
|
||||
}: modifier::Period,
|
||||
) -> Result<usize, io::Error> {
|
||||
match (time.hour() >= 12, is_uppercase) {
|
||||
(false, false) => write(output, b"am"),
|
||||
(false, true) => write(output, b"AM"),
|
||||
(true, false) => write(output, b"pm"),
|
||||
(true, true) => write(output, b"PM"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the second into the designated output.
|
||||
fn fmt_second(
|
||||
output: &mut impl io::Write,
|
||||
time: Time,
|
||||
modifier::Second { padding }: modifier::Second,
|
||||
) -> Result<usize, io::Error> {
|
||||
format_number::<_, _, 2>(output, time.second(), padding)
|
||||
}
|
||||
|
||||
/// Format the subsecond into the designated output.
|
||||
fn fmt_subsecond<W: io::Write>(
|
||||
output: &mut W,
|
||||
time: Time,
|
||||
modifier::Subsecond { digits }: modifier::Subsecond,
|
||||
) -> Result<usize, io::Error> {
|
||||
use modifier::SubsecondDigits::*;
|
||||
let nanos = time.nanosecond();
|
||||
|
||||
if digits == Nine || (digits == OneOrMore && nanos % 10 != 0) {
|
||||
format_number_pad_zero::<_, _, 9>(output, nanos)
|
||||
} else if digits == Eight || (digits == OneOrMore && (nanos / 10) % 10 != 0) {
|
||||
format_number_pad_zero::<_, _, 8>(output, nanos / 10)
|
||||
} else if digits == Seven || (digits == OneOrMore && (nanos / 100) % 10 != 0) {
|
||||
format_number_pad_zero::<_, _, 7>(output, nanos / 100)
|
||||
} else if digits == Six || (digits == OneOrMore && (nanos / 1_000) % 10 != 0) {
|
||||
format_number_pad_zero::<_, _, 6>(output, nanos / 1_000)
|
||||
} else if digits == Five || (digits == OneOrMore && (nanos / 10_000) % 10 != 0) {
|
||||
format_number_pad_zero::<_, _, 5>(output, nanos / 10_000)
|
||||
} else if digits == Four || (digits == OneOrMore && (nanos / 100_000) % 10 != 0) {
|
||||
format_number_pad_zero::<_, _, 4>(output, nanos / 100_000)
|
||||
} else if digits == Three || (digits == OneOrMore && (nanos / 1_000_000) % 10 != 0) {
|
||||
format_number_pad_zero::<_, _, 3>(output, nanos / 1_000_000)
|
||||
} else if digits == Two || (digits == OneOrMore && (nanos / 10_000_000) % 10 != 0) {
|
||||
format_number_pad_zero::<_, _, 2>(output, nanos / 10_000_000)
|
||||
} else {
|
||||
format_number_pad_zero::<_, _, 1>(output, nanos / 100_000_000)
|
||||
}
|
||||
}
|
||||
// endregion time formatters
|
||||
|
||||
// region: offset formatters
|
||||
/// Format the offset hour into the designated output.
|
||||
fn fmt_offset_hour(
|
||||
output: &mut impl io::Write,
|
||||
offset: UtcOffset,
|
||||
modifier::OffsetHour {
|
||||
padding,
|
||||
sign_is_mandatory,
|
||||
}: modifier::OffsetHour,
|
||||
) -> Result<usize, io::Error> {
|
||||
let mut bytes = 0;
|
||||
if offset.is_negative() {
|
||||
bytes += write(output, &[b'-'])?;
|
||||
} else if sign_is_mandatory {
|
||||
bytes += write(output, &[b'+'])?;
|
||||
}
|
||||
bytes += format_number::<_, _, 2>(output, offset.whole_hours().unsigned_abs(), padding)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
/// Format the offset minute into the designated output.
|
||||
fn fmt_offset_minute(
|
||||
output: &mut impl io::Write,
|
||||
offset: UtcOffset,
|
||||
modifier::OffsetMinute { padding }: modifier::OffsetMinute,
|
||||
) -> Result<usize, io::Error> {
|
||||
format_number::<_, _, 2>(output, offset.minutes_past_hour().unsigned_abs(), padding)
|
||||
}
|
||||
|
||||
/// Format the offset second into the designated output.
|
||||
fn fmt_offset_second(
|
||||
output: &mut impl io::Write,
|
||||
offset: UtcOffset,
|
||||
modifier::OffsetSecond { padding }: modifier::OffsetSecond,
|
||||
) -> Result<usize, io::Error> {
|
||||
format_number::<_, _, 2>(output, offset.seconds_past_minute().unsigned_abs(), padding)
|
||||
}
|
||||
// endregion offset formatters
|
|
@ -0,0 +1,262 @@
|
|||
//! The [`Instant`] struct and its associated `impl`s.
|
||||
|
||||
use core::cmp::{Ord, Ordering, PartialEq, PartialOrd};
|
||||
use core::convert::{TryFrom, TryInto};
|
||||
use core::ops::{Add, Sub};
|
||||
use core::time::Duration as StdDuration;
|
||||
use std::borrow::Borrow;
|
||||
use std::time::Instant as StdInstant;
|
||||
|
||||
use crate::Duration;
|
||||
|
||||
/// A measurement of a monotonically non-decreasing clock. Opaque and useful only with [`Duration`].
|
||||
///
|
||||
/// Instants are always guaranteed to be no less than any previously measured instant when created,
|
||||
/// and are often useful for tasks such as measuring benchmarks or timing how long an operation
|
||||
/// takes.
|
||||
///
|
||||
/// Note, however, that instants are not guaranteed to be **steady**. In other words, each tick of
|
||||
/// the underlying clock may not be the same length (e.g. some seconds may be longer than others).
|
||||
/// An instant may jump forwards or experience time dilation (slow down or speed up), but it will
|
||||
/// never go backwards.
|
||||
///
|
||||
/// Instants are opaque types that can only be compared to one another. There is no method to get
|
||||
/// "the number of seconds" from an instant. Instead, it only allows measuring the duration between
|
||||
/// two instants (or comparing two instants).
|
||||
///
|
||||
/// This implementation allows for operations with signed [`Duration`]s, but is otherwise identical
|
||||
/// to [`std::time::Instant`].
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "std")))]
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Instant(pub StdInstant);
|
||||
|
||||
impl Instant {
|
||||
// region: delegation
|
||||
/// Returns an `Instant` corresponding to "now".
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::Instant;
|
||||
/// println!("{:?}", Instant::now());
|
||||
/// ```
|
||||
pub fn now() -> Self {
|
||||
Self(StdInstant::now())
|
||||
}
|
||||
|
||||
/// Returns the amount of time elapsed since this instant was created. The duration will always
|
||||
/// be nonnegative if the instant is not synthetically created.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{Instant, ext::{NumericalStdDuration, NumericalDuration}};
|
||||
/// # use std::thread;
|
||||
/// let instant = Instant::now();
|
||||
/// thread::sleep(1.std_milliseconds());
|
||||
/// assert!(instant.elapsed() >= 1.milliseconds());
|
||||
/// ```
|
||||
pub fn elapsed(self) -> Duration {
|
||||
Self::now() - self
|
||||
}
|
||||
// endregion delegation
|
||||
|
||||
// region: checked arithmetic
|
||||
/// Returns `Some(t)` where `t` is the time `self + duration` if `t` can be represented as
|
||||
/// `Instant` (which means it's inside the bounds of the underlying data structure), `None`
|
||||
/// otherwise.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{Instant, ext::NumericalDuration};
|
||||
/// let now = Instant::now();
|
||||
/// assert_eq!(now.checked_add(5.seconds()), Some(now + 5.seconds()));
|
||||
/// assert_eq!(now.checked_add((-5).seconds()), Some(now + (-5).seconds()));
|
||||
/// ```
|
||||
pub fn checked_add(self, duration: Duration) -> Option<Self> {
|
||||
if duration.is_zero() {
|
||||
Some(self)
|
||||
} else if duration.is_positive() {
|
||||
self.0.checked_add(duration.abs_std()).map(Self)
|
||||
} else {
|
||||
debug_assert!(duration.is_negative());
|
||||
self.0.checked_sub(duration.abs_std()).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `Some(t)` where `t` is the time `self - duration` if `t` can be represented as
|
||||
/// `Instant` (which means it's inside the bounds of the underlying data structure), `None`
|
||||
/// otherwise.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{Instant, ext::NumericalDuration};
|
||||
/// let now = Instant::now();
|
||||
/// assert_eq!(now.checked_sub(5.seconds()), Some(now - 5.seconds()));
|
||||
/// assert_eq!(now.checked_sub((-5).seconds()), Some(now - (-5).seconds()));
|
||||
/// ```
|
||||
pub fn checked_sub(self, duration: Duration) -> Option<Self> {
|
||||
if duration.is_zero() {
|
||||
Some(self)
|
||||
} else if duration.is_positive() {
|
||||
self.0.checked_sub(duration.abs_std()).map(Self)
|
||||
} else {
|
||||
debug_assert!(duration.is_negative());
|
||||
self.0.checked_add(duration.abs_std()).map(Self)
|
||||
}
|
||||
}
|
||||
// endregion checked arithmetic
|
||||
|
||||
/// Obtain the inner [`std::time::Instant`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::Instant;
|
||||
/// let now = Instant::now();
|
||||
/// assert_eq!(now.into_inner(), now.0);
|
||||
/// ```
|
||||
pub const fn into_inner(self) -> StdInstant {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
// region: trait impls
|
||||
impl From<StdInstant> for Instant {
|
||||
fn from(instant: StdInstant) -> Self {
|
||||
Self(instant)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Instant> for StdInstant {
|
||||
fn from(instant: Instant) -> Self {
|
||||
instant.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Instant {
|
||||
type Output = Duration;
|
||||
|
||||
fn sub(self, other: Self) -> Self::Output {
|
||||
match self.0.cmp(&other.0) {
|
||||
Ordering::Equal => Duration::ZERO,
|
||||
Ordering::Greater => (self.0 - other.0)
|
||||
.try_into()
|
||||
.expect("overflow converting `std::time::Duration` to `time::Duration`"),
|
||||
Ordering::Less => -Duration::try_from(other.0 - self.0)
|
||||
.expect("overflow converting `std::time::Duration` to `time::Duration`"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<StdInstant> for Instant {
|
||||
type Output = Duration;
|
||||
|
||||
fn sub(self, other: StdInstant) -> Self::Output {
|
||||
self - Self(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Instant> for StdInstant {
|
||||
type Output = Duration;
|
||||
|
||||
fn sub(self, other: Instant) -> Self::Output {
|
||||
Instant(self) - other
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Duration> for Instant {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, duration: Duration) -> Self::Output {
|
||||
if duration.is_positive() {
|
||||
Self(self.0 + duration.abs_std())
|
||||
} else if duration.is_negative() {
|
||||
Self(self.0 - duration.abs_std())
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Duration> for StdInstant {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, duration: Duration) -> Self::Output {
|
||||
(Instant(self) + duration).0
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<StdDuration> for Instant {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, duration: StdDuration) -> Self::Output {
|
||||
Self(self.0 + duration)
|
||||
}
|
||||
}
|
||||
|
||||
impl_add_assign!(Instant: Duration, StdDuration);
|
||||
impl_add_assign!(StdInstant: Duration);
|
||||
|
||||
impl Sub<Duration> for Instant {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, duration: Duration) -> Self::Output {
|
||||
if duration.is_positive() {
|
||||
Self(self.0 - duration.abs_std())
|
||||
} else if duration.is_negative() {
|
||||
Self(self.0 + duration.abs_std())
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Duration> for StdInstant {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, duration: Duration) -> Self::Output {
|
||||
(Instant(self) - duration).0
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<StdDuration> for Instant {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, duration: StdDuration) -> Self::Output {
|
||||
Self(self.0 - duration)
|
||||
}
|
||||
}
|
||||
|
||||
impl_sub_assign!(Instant: Duration, StdDuration);
|
||||
impl_sub_assign!(StdInstant: Duration);
|
||||
|
||||
impl PartialEq<StdInstant> for Instant {
|
||||
fn eq(&self, rhs: &StdInstant) -> bool {
|
||||
self.0.eq(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<Instant> for StdInstant {
|
||||
fn eq(&self, rhs: &Instant) -> bool {
|
||||
self.eq(&rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<StdInstant> for Instant {
|
||||
fn partial_cmp(&self, rhs: &StdInstant) -> Option<Ordering> {
|
||||
self.0.partial_cmp(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd<Instant> for StdInstant {
|
||||
fn partial_cmp(&self, rhs: &Instant) -> Option<Ordering> {
|
||||
self.partial_cmp(&rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<StdInstant> for Instant {
|
||||
fn as_ref(&self) -> &StdInstant {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<StdInstant> for Instant {
|
||||
fn borrow(&self) -> &StdInstant {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
// endregion trait impls
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,114 @@
|
|||
//! Macros to construct statically known values.
|
||||
|
||||
/// Construct a [`Date`](crate::Date) with a statically known value.
|
||||
///
|
||||
/// The resulting expression can be used in `const` or `static` declarations.
|
||||
///
|
||||
/// Three formats are supported: year-week-weekday, year-ordinal, and year-month-day.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{Date, Weekday::*, Month, macros::date};
|
||||
/// assert_eq!(
|
||||
/// date!(2020 - W 01 - 3),
|
||||
/// Date::from_iso_week_date(2020, 1, Wednesday)?
|
||||
/// );
|
||||
/// assert_eq!(date!(2020 - 001), Date::from_ordinal_date(2020, 1)?);
|
||||
/// assert_eq!(
|
||||
/// date!(2020 - 01 - 01),
|
||||
/// Date::from_calendar_date(2020, Month::January, 1)?
|
||||
/// );
|
||||
/// # Ok::<_, time::Error>(())
|
||||
/// ```
|
||||
pub use time_macros::date;
|
||||
/// Construct a [`PrimitiveDateTime`] or [`OffsetDateTime`] with a statically known value.
|
||||
///
|
||||
/// The resulting expression can be used in `const` or `static` declarations.
|
||||
///
|
||||
/// The syntax accepted by this macro is the same as [`date!`] and [`time!`], with an optional
|
||||
/// [`offset!`], all space-separated. If an [`offset!`] is provided, the resulting value will
|
||||
/// be an [`OffsetDateTime`]; otherwise it will be a [`PrimitiveDateTime`].
|
||||
///
|
||||
/// [`OffsetDateTime`]: crate::OffsetDateTime
|
||||
/// [`PrimitiveDateTime`]: crate::PrimitiveDateTime
|
||||
pub use time_macros::datetime;
|
||||
/// Equivalent of performing [`format_description::parse()`] at compile time.
|
||||
///
|
||||
/// Using the macro instead of the function results in a static slice rather than a [`Vec`],
|
||||
/// such that it can be used in `#![no_alloc]` situations.
|
||||
///
|
||||
/// The resulting expression can be used in `const` or `static` declarations, and implements
|
||||
/// the sealed traits required for both formatting and parsing.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{format_description, macros::format_description};
|
||||
/// assert_eq!(
|
||||
/// format_description!("[hour]:[minute]:[second]"),
|
||||
/// format_description::parse("[hour]:[minute]:[second]")?
|
||||
/// );
|
||||
/// # Ok::<_, time::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// The syntax accepted by this macro is the same as [`format_description::parse()`], which can
|
||||
/// be found in [the book](https://time-rs.github.io/book/api/format-description.html).
|
||||
///
|
||||
/// [`format_description::parse()`]: crate::format_description::parse()
|
||||
#[cfg(any(feature = "formatting", feature = "parsing"))]
|
||||
pub use time_macros::format_description;
|
||||
/// Construct a [`UtcOffset`](crate::UtcOffset) with a statically known value.
|
||||
///
|
||||
/// The resulting expression can be used in `const` or `static` declarations.
|
||||
///
|
||||
/// A sign and the hour must be provided; minutes and seconds default to zero. `UTC` (both
|
||||
/// uppercase and lowercase) is also allowed.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{UtcOffset, macros::offset};
|
||||
/// assert_eq!(offset!(UTC), UtcOffset::from_hms(0, 0, 0)?);
|
||||
/// assert_eq!(offset!(utc), UtcOffset::from_hms(0, 0, 0)?);
|
||||
/// assert_eq!(offset!(+0), UtcOffset::from_hms(0, 0, 0)?);
|
||||
/// assert_eq!(offset!(+1), UtcOffset::from_hms(1, 0, 0)?);
|
||||
/// assert_eq!(offset!(-1), UtcOffset::from_hms(-1, 0, 0)?);
|
||||
/// assert_eq!(offset!(+1:30), UtcOffset::from_hms(1, 30, 0)?);
|
||||
/// assert_eq!(offset!(-1:30), UtcOffset::from_hms(-1, -30, 0)?);
|
||||
/// assert_eq!(offset!(+1:30:59), UtcOffset::from_hms(1, 30, 59)?);
|
||||
/// assert_eq!(offset!(-1:30:59), UtcOffset::from_hms(-1, -30, -59)?);
|
||||
/// assert_eq!(offset!(+23:59:59), UtcOffset::from_hms(23, 59, 59)?);
|
||||
/// assert_eq!(offset!(-23:59:59), UtcOffset::from_hms(-23, -59, -59)?);
|
||||
/// # Ok::<_, time::Error>(())
|
||||
/// ```
|
||||
pub use time_macros::offset;
|
||||
/// Construct a [`Time`](crate::Time) with a statically known value.
|
||||
///
|
||||
/// The resulting expression can be used in `const` or `static` declarations.
|
||||
///
|
||||
/// Hours and minutes must be provided, while seconds defaults to zero. AM/PM is allowed
|
||||
/// (either uppercase or lowercase). Any number of subsecond digits may be provided (though any
|
||||
/// past nine will be discarded).
|
||||
///
|
||||
/// All components are validated at compile-time. An error will be raised if any value is
|
||||
/// invalid.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::{Time, macros::time};
|
||||
/// assert_eq!(time!(0:00), Time::from_hms(0, 0, 0)?);
|
||||
/// assert_eq!(time!(1:02:03), Time::from_hms(1, 2, 3)?);
|
||||
/// assert_eq!(
|
||||
/// time!(1:02:03.004_005_006),
|
||||
/// Time::from_hms_nano(1, 2, 3, 4_005_006)?
|
||||
/// );
|
||||
/// assert_eq!(time!(12:00 am), Time::from_hms(0, 0, 0)?);
|
||||
/// assert_eq!(time!(1:02:03 am), Time::from_hms(1, 2, 3)?);
|
||||
/// assert_eq!(
|
||||
/// time!(1:02:03.004_005_006 am),
|
||||
/// Time::from_hms_nano(1, 2, 3, 4_005_006)?
|
||||
/// );
|
||||
/// assert_eq!(time!(12 pm), Time::from_hms(12, 0, 0)?);
|
||||
/// assert_eq!(time!(12:00 pm), Time::from_hms(12, 0, 0)?);
|
||||
/// assert_eq!(time!(1:02:03 pm), Time::from_hms(13, 2, 3)?);
|
||||
/// assert_eq!(
|
||||
/// time!(1:02:03.004_005_006 pm),
|
||||
/// Time::from_hms_nano(13, 2, 3, 4_005_006)?
|
||||
/// );
|
||||
/// # Ok::<_, time::Error>(())
|
||||
/// ```
|
||||
pub use time_macros::time;
|
|
@ -0,0 +1,165 @@
|
|||
//! The `Month` enum and its associated `impl`s.
|
||||
|
||||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
use core::num::NonZeroU8;
|
||||
use core::str::FromStr;
|
||||
|
||||
use self::Month::*;
|
||||
use crate::error;
|
||||
|
||||
/// Months of the year.
|
||||
#[allow(clippy::missing_docs_in_private_items)] // variants
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Month {
|
||||
January = 1,
|
||||
February = 2,
|
||||
March = 3,
|
||||
April = 4,
|
||||
May = 5,
|
||||
June = 6,
|
||||
July = 7,
|
||||
August = 8,
|
||||
September = 9,
|
||||
October = 10,
|
||||
November = 11,
|
||||
December = 12,
|
||||
}
|
||||
|
||||
impl Month {
|
||||
/// Create a `Month` from its numerical value.
|
||||
pub(crate) const fn from_number(n: NonZeroU8) -> Result<Self, error::ComponentRange> {
|
||||
match n.get() {
|
||||
1 => Ok(January),
|
||||
2 => Ok(February),
|
||||
3 => Ok(March),
|
||||
4 => Ok(April),
|
||||
5 => Ok(May),
|
||||
6 => Ok(June),
|
||||
7 => Ok(July),
|
||||
8 => Ok(August),
|
||||
9 => Ok(September),
|
||||
10 => Ok(October),
|
||||
11 => Ok(November),
|
||||
12 => Ok(December),
|
||||
n => Err(error::ComponentRange {
|
||||
name: "month",
|
||||
minimum: 1,
|
||||
maximum: 12,
|
||||
value: n as _,
|
||||
conditional_range: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the previous month.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::Month;
|
||||
/// assert_eq!(Month::January.previous(), Month::December);
|
||||
/// ```
|
||||
pub const fn previous(self) -> Self {
|
||||
match self {
|
||||
January => December,
|
||||
February => January,
|
||||
March => February,
|
||||
April => March,
|
||||
May => April,
|
||||
June => May,
|
||||
July => June,
|
||||
August => July,
|
||||
September => August,
|
||||
October => September,
|
||||
November => October,
|
||||
December => November,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next month.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use time::Month;
|
||||
/// assert_eq!(Month::January.next(), Month::February);
|
||||
/// ```
|
||||
pub const fn next(self) -> Self {
|
||||
match self {
|
||||
January => February,
|
||||
February => March,
|
||||
March => April,
|
||||
April => May,
|
||||
May => June,
|
||||
June => July,
|
||||
July => August,
|
||||
August => September,
|
||||
September => October,
|
||||
October => November,
|
||||
November => December,
|
||||
December => January,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Month {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
January => "January",
|
||||
February => "February",
|
||||
March => "March",
|
||||
April => "April",
|
||||
May => "May",
|
||||
June => "June",
|
||||
July => "July",
|
||||
August => "August",
|
||||
September => "September",
|
||||
October => "October",
|
||||
November => "November",
|
||||
December => "December",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Month {
|
||||
type Err = error::InvalidVariant;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"January" => Ok(January),
|
||||
"February" => Ok(February),
|
||||
"March" => Ok(March),
|
||||
"April" => Ok(April),
|
||||
"May" => Ok(May),
|
||||
"June" => Ok(June),
|
||||
"July" => Ok(July),
|
||||
"August" => Ok(August),
|
||||
"September" => Ok(September),
|
||||
"October" => Ok(October),
|
||||
"November" => Ok(November),
|
||||
"December" => Ok(December),
|
||||
_ => Err(error::InvalidVariant),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Month> for u8 {
|
||||
fn from(month: Month) -> Self {
|
||||
month as _
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for Month {
|
||||
type Error = error::ComponentRange;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match NonZeroU8::new(value) {
|
||||
Some(value) => Self::from_number(value),
|
||||
None => Err(error::ComponentRange {
|
||||
name: "month",
|
||||
minimum: 1,
|
||||
maximum: 12,
|
||||
value: 0,
|
||||
conditional_range: false,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -0,0 +1,192 @@
|
|||
//! Implementations of the low-level parser combinators.
|
||||
|
||||
pub(crate) mod rfc;
|
||||
|
||||
use crate::format_description::modifier::Padding;
|
||||
use crate::parsing::shim::{Integer, IntegerParseBytes};
|
||||
use crate::parsing::ParsedItem;
|
||||
|
||||
/// Parse a "+" or "-" sign. Returns the ASCII byte representing the sign, if present.
|
||||
pub(crate) const fn sign(input: &[u8]) -> Option<ParsedItem<'_, u8>> {
|
||||
match input {
|
||||
[sign @ (b'-' | b'+'), remaining @ ..] => Some(ParsedItem(remaining, *sign)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the first matching item, returning its associated value.
|
||||
pub(crate) fn first_match<'a, T>(
|
||||
options: impl IntoIterator<Item = (&'a [u8], T)>,
|
||||
case_sensitive: bool,
|
||||
) -> impl FnMut(&'a [u8]) -> Option<ParsedItem<'a, T>> {
|
||||
let mut options = options.into_iter();
|
||||
move |input| {
|
||||
options.find_map(|(expected, t)| {
|
||||
if case_sensitive {
|
||||
Some(ParsedItem(input.strip_prefix(expected)?, t))
|
||||
} else {
|
||||
let n = expected.len();
|
||||
if n <= input.len() {
|
||||
let (head, tail) = input.split_at(n);
|
||||
if head.eq_ignore_ascii_case(expected) {
|
||||
return Some(ParsedItem(tail, t));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume zero or more instances of the provided parser. The parser must return the unit value.
|
||||
pub(crate) fn zero_or_more<'a, P: Fn(&'a [u8]) -> Option<ParsedItem<'a, ()>>>(
|
||||
parser: P,
|
||||
) -> impl FnMut(&'a [u8]) -> ParsedItem<'a, ()> {
|
||||
move |mut input| {
|
||||
while let Some(remaining) = parser(input) {
|
||||
input = remaining.into_inner();
|
||||
}
|
||||
ParsedItem(input, ())
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume one of or more instances of the provided parser. The parser must produce the unit value.
|
||||
pub(crate) fn one_or_more<'a, P: Fn(&'a [u8]) -> Option<ParsedItem<'a, ()>>>(
|
||||
parser: P,
|
||||
) -> impl Fn(&'a [u8]) -> Option<ParsedItem<'a, ()>> {
|
||||
move |mut input| {
|
||||
input = parser(input)?.into_inner();
|
||||
while let Some(remaining) = parser(input) {
|
||||
input = remaining.into_inner();
|
||||
}
|
||||
Some(ParsedItem(input, ()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume between `n` and `m` instances of the provided parser.
|
||||
pub(crate) fn n_to_m<
|
||||
'a,
|
||||
T,
|
||||
P: Fn(&'a [u8]) -> Option<ParsedItem<'a, T>>,
|
||||
const N: u8,
|
||||
const M: u8,
|
||||
>(
|
||||
parser: P,
|
||||
) -> impl Fn(&'a [u8]) -> Option<ParsedItem<'a, &'a [u8]>> {
|
||||
debug_assert!(M >= N);
|
||||
move |mut input| {
|
||||
// We need to keep this to determine the total length eventually consumed.
|
||||
let orig_input = input;
|
||||
|
||||
// Mandatory
|
||||
for _ in 0..N {
|
||||
input = parser(input)?.0;
|
||||
}
|
||||
|
||||
// Optional
|
||||
for _ in N..M {
|
||||
match parser(input) {
|
||||
Some(parsed) => input = parsed.0,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
Some(ParsedItem(
|
||||
input,
|
||||
&orig_input[..(orig_input.len() - input.len())],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume between `n` and `m` digits, returning the numerical value.
|
||||
pub(crate) fn n_to_m_digits<T: Integer, const N: u8, const M: u8>(
|
||||
input: &[u8],
|
||||
) -> Option<ParsedItem<'_, T>> {
|
||||
debug_assert!(M >= N);
|
||||
n_to_m::<_, _, N, M>(any_digit)(input)?.flat_map(|value| value.parse_bytes())
|
||||
}
|
||||
|
||||
/// Consume exactly `n` digits, returning the numerical value.
|
||||
pub(crate) fn exactly_n_digits<T: Integer, const N: u8>(input: &[u8]) -> Option<ParsedItem<'_, T>> {
|
||||
n_to_m_digits::<_, N, N>(input)
|
||||
}
|
||||
|
||||
/// Consume exactly `n` digits, returning the numerical value.
|
||||
pub(crate) fn exactly_n_digits_padded<'a, T: Integer, const N: u8>(
|
||||
padding: Padding,
|
||||
) -> impl Fn(&'a [u8]) -> Option<ParsedItem<'a, T>> {
|
||||
n_to_m_digits_padded::<_, N, N>(padding)
|
||||
}
|
||||
|
||||
/// Consume between `n` and `m` digits, returning the numerical value.
|
||||
pub(crate) fn n_to_m_digits_padded<'a, T: Integer, const N: u8, const M: u8>(
|
||||
padding: Padding,
|
||||
) -> impl Fn(&'a [u8]) -> Option<ParsedItem<'a, T>> {
|
||||
debug_assert!(M >= N);
|
||||
move |mut input| match padding {
|
||||
Padding::None => n_to_m_digits::<_, 1, M>(input),
|
||||
Padding::Space => {
|
||||
debug_assert!(N > 0);
|
||||
|
||||
let mut orig_input = input;
|
||||
for _ in 0..(N - 1) {
|
||||
match ascii_char::<b' '>(input) {
|
||||
Some(parsed) => input = parsed.0,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
let pad_width = (orig_input.len() - input.len()) as u8;
|
||||
|
||||
orig_input = input;
|
||||
for _ in 0..(N - pad_width) {
|
||||
input = any_digit(input)?.0;
|
||||
}
|
||||
for _ in N..M {
|
||||
match any_digit(input) {
|
||||
Some(parsed) => input = parsed.0,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
ParsedItem(input, &orig_input[..(orig_input.len() - input.len())])
|
||||
.flat_map(|value| value.parse_bytes())
|
||||
}
|
||||
Padding::Zero => n_to_m_digits::<_, N, M>(input),
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume exactly one digit.
|
||||
pub(crate) const fn any_digit(input: &[u8]) -> Option<ParsedItem<'_, u8>> {
|
||||
match input {
|
||||
[c, remaining @ ..] if c.is_ascii_digit() => Some(ParsedItem(remaining, *c)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume exactly one of the provided ASCII characters.
|
||||
pub(crate) fn ascii_char<const CHAR: u8>(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
|
||||
debug_assert!(CHAR.is_ascii_graphic() || CHAR.is_ascii_whitespace());
|
||||
match input {
|
||||
[c, remaining @ ..] if *c == CHAR => Some(ParsedItem(remaining, ())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume exactly one of the provided ASCII characters, case-insensitive.
|
||||
pub(crate) fn ascii_char_ignore_case<const CHAR: u8>(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
|
||||
debug_assert!(CHAR.is_ascii_graphic() || CHAR.is_ascii_whitespace());
|
||||
match input {
|
||||
[c, remaining @ ..] if c.eq_ignore_ascii_case(&CHAR) => Some(ParsedItem(remaining, ())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Optionally consume an input with a given parser.
|
||||
pub(crate) fn opt<'a, T>(
|
||||
parser: impl Fn(&'a [u8]) -> Option<ParsedItem<'a, T>>,
|
||||
) -> impl Fn(&'a [u8]) -> ParsedItem<'a, Option<T>> {
|
||||
move |input| match parser(input) {
|
||||
Some(value) => value.map(Some),
|
||||
None => ParsedItem(input, None),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
//! Combinators for rules as defined in an RFC.
|
||||
//!
|
||||
//! These rules have been converted strictly following the ABNF syntax as specified in [RFC 2234].
|
||||
//!
|
||||
//! [RFC 2234]: https://datatracker.ietf.org/doc/html/rfc2234
|
||||
|
||||
pub(crate) mod rfc2234;
|
||||
pub(crate) mod rfc2822;
|
|
@ -0,0 +1,13 @@
|
|||
//! Rules defined in [RFC 2234].
|
||||
//!
|
||||
//! [RFC 2234]: https://datatracker.ietf.org/doc/html/rfc2234
|
||||
|
||||
use crate::parsing::ParsedItem;
|
||||
|
||||
/// Consume exactly one space or tab.
|
||||
pub(crate) const fn wsp(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
|
||||
match input {
|
||||
[b' ' | b'\t', rest @ ..] => Some(ParsedItem(rest, ())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
//! Rules defined in [RFC 2822].
|
||||
//!
|
||||
//! [RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
|
||||
|
||||
use crate::parsing::combinator::rfc::rfc2234::wsp;
|
||||
use crate::parsing::combinator::{ascii_char, one_or_more, zero_or_more};
|
||||
use crate::parsing::ParsedItem;
|
||||
|
||||
/// Consume the `fws` rule.
|
||||
// The full rule is equivalent to /\r\n[ \t]+|[ \t]+(?:\r\n[ \t]+)*/
|
||||
pub(crate) fn fws(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
|
||||
if let [b'\r', b'\n', rest @ ..] = input {
|
||||
one_or_more(wsp)(rest)
|
||||
} else {
|
||||
input = one_or_more(wsp)(input)?.into_inner();
|
||||
while let [b'\r', b'\n', rest @ ..] = input {
|
||||
input = one_or_more(wsp)(rest)?.into_inner();
|
||||
}
|
||||
Some(ParsedItem(input, ()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the `cfws` rule.
|
||||
// The full rule is equivalent to any combination of `fws` and `comment` so long as it is not empty.
|
||||
pub(crate) fn cfws(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
|
||||
one_or_more(|input| fws(input).or_else(|| comment(input)))(input)
|
||||
}
|
||||
|
||||
/// Consume the `comment` rule.
|
||||
fn comment(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
|
||||
input = ascii_char::<b'('>(input)?.into_inner();
|
||||
input = zero_or_more(fws)(input).into_inner();
|
||||
while let Some(rest) = ccontent(input) {
|
||||
input = rest.into_inner();
|
||||
input = zero_or_more(fws)(input).into_inner();
|
||||
}
|
||||
input = ascii_char::<b')'>(input)?.into_inner();
|
||||
|
||||
Some(ParsedItem(input, ()))
|
||||
}
|
||||
|
||||
/// Consume the `ccontent` rule.
|
||||
fn ccontent(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
|
||||
ctext(input)
|
||||
.or_else(|| quoted_pair(input))
|
||||
.or_else(|| comment(input))
|
||||
}
|
||||
|
||||
/// Consume the `ctext` rule.
|
||||
#[allow(clippy::unnecessary_lazy_evaluations)] // rust-lang/rust-clippy#8522
|
||||
fn ctext(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
|
||||
no_ws_ctl(input).or_else(|| match input {
|
||||
[33..=39 | 42..=91 | 93..=126, rest @ ..] => Some(ParsedItem(rest, ())),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Consume the `quoted_pair` rule.
|
||||
fn quoted_pair(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
|
||||
input = ascii_char::<b'\\'>(input)?.into_inner();
|
||||
|
||||
let old_input_len = input.len();
|
||||
|
||||
input = text(input).into_inner();
|
||||
|
||||
// If nothing is parsed, this means we hit the `obs-text` rule and nothing matched. This is
|
||||
// technically a success, but we should still check the `obs-qp` rule to ensure we consume
|
||||
// everything possible.
|
||||
if input.len() == old_input_len {
|
||||
match input {
|
||||
[0..=127, rest @ ..] => Some(ParsedItem(rest, ())),
|
||||
_ => Some(ParsedItem(input, ())),
|
||||
}
|
||||
} else {
|
||||
Some(ParsedItem(input, ()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the `no_ws_ctl` rule.
|
||||
const fn no_ws_ctl(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
|
||||
match input {
|
||||
[1..=8 | 11..=12 | 14..=31 | 127, rest @ ..] => Some(ParsedItem(rest, ())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the `text` rule.
|
||||
fn text<'a>(input: &'a [u8]) -> ParsedItem<'a, ()> {
|
||||
let new_text = |input: &'a [u8]| match input {
|
||||
[1..=9 | 11..=12 | 14..=127, rest @ ..] => Some(ParsedItem(rest, ())),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let obs_char = |input: &'a [u8]| match input {
|
||||
// This is technically allowed, but consuming this would mean the rest of the string is
|
||||
// eagerly consumed without consideration for where the comment actually ends.
|
||||
[b')', ..] => None,
|
||||
[0..=9 | 11..=12 | 14..=127, rest @ ..] => Some(rest),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let obs_text = |mut input| {
|
||||
input = zero_or_more(ascii_char::<b'\n'>)(input).into_inner();
|
||||
input = zero_or_more(ascii_char::<b'\r'>)(input).into_inner();
|
||||
while let Some(rest) = obs_char(input) {
|
||||
input = rest;
|
||||
input = zero_or_more(ascii_char::<b'\n'>)(input).into_inner();
|
||||
input = zero_or_more(ascii_char::<b'\r'>)(input).into_inner();
|
||||
}
|
||||
|
||||
ParsedItem(input, ())
|
||||
};
|
||||
|
||||
new_text(input).unwrap_or_else(|| obs_text(input))
|
||||
}
|
|
@ -0,0 +1,290 @@
|
|||
//! Parsing implementations for all [`Component`](crate::format_description::Component)s.
|
||||
|
||||
use core::num::{NonZeroU16, NonZeroU8};
|
||||
|
||||
use crate::format_description::modifier;
|
||||
#[cfg(feature = "large-dates")]
|
||||
use crate::parsing::combinator::n_to_m_digits_padded;
|
||||
use crate::parsing::combinator::{
|
||||
any_digit, exactly_n_digits, exactly_n_digits_padded, first_match, opt, sign,
|
||||
};
|
||||
use crate::parsing::ParsedItem;
|
||||
use crate::{Month, Weekday};
|
||||
|
||||
// region: date components
|
||||
/// Parse the "year" component of a `Date`.
|
||||
pub(crate) fn parse_year(input: &[u8], modifiers: modifier::Year) -> Option<ParsedItem<'_, i32>> {
|
||||
match modifiers.repr {
|
||||
modifier::YearRepr::Full => {
|
||||
let ParsedItem(input, sign) = opt(sign)(input);
|
||||
#[cfg(not(feature = "large-dates"))]
|
||||
let ParsedItem(input, year) =
|
||||
exactly_n_digits_padded::<u32, 4>(modifiers.padding)(input)?;
|
||||
#[cfg(feature = "large-dates")]
|
||||
let ParsedItem(input, year) =
|
||||
n_to_m_digits_padded::<u32, 4, 6>(modifiers.padding)(input)?;
|
||||
match sign {
|
||||
Some(b'-') => Some(ParsedItem(input, -(year as i32))),
|
||||
None if modifiers.sign_is_mandatory || year >= 10_000 => None,
|
||||
_ => Some(ParsedItem(input, year as i32)),
|
||||
}
|
||||
}
|
||||
modifier::YearRepr::LastTwo => {
|
||||
Some(exactly_n_digits_padded::<u32, 2>(modifiers.padding)(input)?.map(|v| v as i32))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the "month" component of a `Date`.
|
||||
pub(crate) fn parse_month(
|
||||
input: &[u8],
|
||||
modifiers: modifier::Month,
|
||||
) -> Option<ParsedItem<'_, Month>> {
|
||||
use Month::*;
|
||||
let ParsedItem(remaining, value) = first_match(
|
||||
match modifiers.repr {
|
||||
modifier::MonthRepr::Numerical => {
|
||||
return exactly_n_digits_padded::<_, 2>(modifiers.padding)(input)?
|
||||
.flat_map(|n| Month::from_number(n).ok());
|
||||
}
|
||||
modifier::MonthRepr::Long => [
|
||||
(&b"January"[..], January),
|
||||
(&b"February"[..], February),
|
||||
(&b"March"[..], March),
|
||||
(&b"April"[..], April),
|
||||
(&b"May"[..], May),
|
||||
(&b"June"[..], June),
|
||||
(&b"July"[..], July),
|
||||
(&b"August"[..], August),
|
||||
(&b"September"[..], September),
|
||||
(&b"October"[..], October),
|
||||
(&b"November"[..], November),
|
||||
(&b"December"[..], December),
|
||||
],
|
||||
modifier::MonthRepr::Short => [
|
||||
(&b"Jan"[..], January),
|
||||
(&b"Feb"[..], February),
|
||||
(&b"Mar"[..], March),
|
||||
(&b"Apr"[..], April),
|
||||
(&b"May"[..], May),
|
||||
(&b"Jun"[..], June),
|
||||
(&b"Jul"[..], July),
|
||||
(&b"Aug"[..], August),
|
||||
(&b"Sep"[..], September),
|
||||
(&b"Oct"[..], October),
|
||||
(&b"Nov"[..], November),
|
||||
(&b"Dec"[..], December),
|
||||
],
|
||||
},
|
||||
modifiers.case_sensitive,
|
||||
)(input)?;
|
||||
Some(ParsedItem(remaining, value))
|
||||
}
|
||||
|
||||
/// Parse the "week number" component of a `Date`.
|
||||
pub(crate) fn parse_week_number(
|
||||
input: &[u8],
|
||||
modifiers: modifier::WeekNumber,
|
||||
) -> Option<ParsedItem<'_, u8>> {
|
||||
exactly_n_digits_padded::<_, 2>(modifiers.padding)(input)
|
||||
}
|
||||
|
||||
/// Parse the "weekday" component of a `Date`.
|
||||
pub(crate) fn parse_weekday(
|
||||
input: &[u8],
|
||||
modifiers: modifier::Weekday,
|
||||
) -> Option<ParsedItem<'_, Weekday>> {
|
||||
first_match(
|
||||
match (modifiers.repr, modifiers.one_indexed) {
|
||||
(modifier::WeekdayRepr::Short, _) => [
|
||||
(&b"Mon"[..], Weekday::Monday),
|
||||
(&b"Tue"[..], Weekday::Tuesday),
|
||||
(&b"Wed"[..], Weekday::Wednesday),
|
||||
(&b"Thu"[..], Weekday::Thursday),
|
||||
(&b"Fri"[..], Weekday::Friday),
|
||||
(&b"Sat"[..], Weekday::Saturday),
|
||||
(&b"Sun"[..], Weekday::Sunday),
|
||||
],
|
||||
(modifier::WeekdayRepr::Long, _) => [
|
||||
(&b"Monday"[..], Weekday::Monday),
|
||||
(&b"Tuesday"[..], Weekday::Tuesday),
|
||||
(&b"Wednesday"[..], Weekday::Wednesday),
|
||||
(&b"Thursday"[..], Weekday::Thursday),
|
||||
(&b"Friday"[..], Weekday::Friday),
|
||||
(&b"Saturday"[..], Weekday::Saturday),
|
||||
(&b"Sunday"[..], Weekday::Sunday),
|
||||
],
|
||||
(modifier::WeekdayRepr::Sunday, false) => [
|
||||
(&b"1"[..], Weekday::Monday),
|
||||
(&b"2"[..], Weekday::Tuesday),
|
||||
(&b"3"[..], Weekday::Wednesday),
|
||||
(&b"4"[..], Weekday::Thursday),
|
||||
(&b"5"[..], Weekday::Friday),
|
||||
(&b"6"[..], Weekday::Saturday),
|
||||
(&b"0"[..], Weekday::Sunday),
|
||||
],
|
||||
(modifier::WeekdayRepr::Sunday, true) => [
|
||||
(&b"2"[..], Weekday::Monday),
|
||||
(&b"3"[..], Weekday::Tuesday),
|
||||
(&b"4"[..], Weekday::Wednesday),
|
||||
(&b"5"[..], Weekday::Thursday),
|
||||
(&b"6"[..], Weekday::Friday),
|
||||
(&b"7"[..], Weekday::Saturday),
|
||||
(&b"1"[..], Weekday::Sunday),
|
||||
],
|
||||
(modifier::WeekdayRepr::Monday, false) => [
|
||||
(&b"0"[..], Weekday::Monday),
|
||||
(&b"1"[..], Weekday::Tuesday),
|
||||
(&b"2"[..], Weekday::Wednesday),
|
||||
(&b"3"[..], Weekday::Thursday),
|
||||
(&b"4"[..], Weekday::Friday),
|
||||
(&b"5"[..], Weekday::Saturday),
|
||||
(&b"6"[..], Weekday::Sunday),
|
||||
],
|
||||
(modifier::WeekdayRepr::Monday, true) => [
|
||||
(&b"1"[..], Weekday::Monday),
|
||||
(&b"2"[..], Weekday::Tuesday),
|
||||
(&b"3"[..], Weekday::Wednesday),
|
||||
(&b"4"[..], Weekday::Thursday),
|
||||
(&b"5"[..], Weekday::Friday),
|
||||
(&b"6"[..], Weekday::Saturday),
|
||||
(&b"7"[..], Weekday::Sunday),
|
||||
],
|
||||
},
|
||||
modifiers.case_sensitive,
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// Parse the "ordinal" component of a `Date`.
|
||||
pub(crate) fn parse_ordinal(
|
||||
input: &[u8],
|
||||
modifiers: modifier::Ordinal,
|
||||
) -> Option<ParsedItem<'_, NonZeroU16>> {
|
||||
exactly_n_digits_padded::<_, 3>(modifiers.padding)(input)
|
||||
}
|
||||
|
||||
/// Parse the "day" component of a `Date`.
|
||||
pub(crate) fn parse_day(
|
||||
input: &[u8],
|
||||
modifiers: modifier::Day,
|
||||
) -> Option<ParsedItem<'_, NonZeroU8>> {
|
||||
exactly_n_digits_padded::<_, 2>(modifiers.padding)(input)
|
||||
}
|
||||
// endregion date components
|
||||
|
||||
// region: time components
|
||||
/// Indicate whether the hour is "am" or "pm".
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Period {
|
||||
#[allow(clippy::missing_docs_in_private_items)]
|
||||
Am,
|
||||
#[allow(clippy::missing_docs_in_private_items)]
|
||||
Pm,
|
||||
}
|
||||
|
||||
/// Parse the "hour" component of a `Time`.
|
||||
pub(crate) fn parse_hour(input: &[u8], modifiers: modifier::Hour) -> Option<ParsedItem<'_, u8>> {
|
||||
exactly_n_digits_padded::<_, 2>(modifiers.padding)(input)
|
||||
}
|
||||
|
||||
/// Parse the "minute" component of a `Time`.
|
||||
pub(crate) fn parse_minute(
|
||||
input: &[u8],
|
||||
modifiers: modifier::Minute,
|
||||
) -> Option<ParsedItem<'_, u8>> {
|
||||
exactly_n_digits_padded::<_, 2>(modifiers.padding)(input)
|
||||
}
|
||||
|
||||
/// Parse the "second" component of a `Time`.
|
||||
pub(crate) fn parse_second(
|
||||
input: &[u8],
|
||||
modifiers: modifier::Second,
|
||||
) -> Option<ParsedItem<'_, u8>> {
|
||||
exactly_n_digits_padded::<_, 2>(modifiers.padding)(input)
|
||||
}
|
||||
|
||||
/// Parse the "period" component of a `Time`. Required if the hour is on a 12-hour clock.
|
||||
pub(crate) fn parse_period(
|
||||
input: &[u8],
|
||||
modifiers: modifier::Period,
|
||||
) -> Option<ParsedItem<'_, Period>> {
|
||||
first_match(
|
||||
if modifiers.is_uppercase {
|
||||
[(&b"AM"[..], Period::Am), (&b"PM"[..], Period::Pm)]
|
||||
} else {
|
||||
[(&b"am"[..], Period::Am), (&b"pm"[..], Period::Pm)]
|
||||
},
|
||||
modifiers.case_sensitive,
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// Parse the "subsecond" component of a `Time`.
|
||||
pub(crate) fn parse_subsecond(
|
||||
input: &[u8],
|
||||
modifiers: modifier::Subsecond,
|
||||
) -> Option<ParsedItem<'_, u32>> {
|
||||
use modifier::SubsecondDigits::*;
|
||||
Some(match modifiers.digits {
|
||||
One => exactly_n_digits::<u32, 1>(input)?.map(|v| v * 100_000_000),
|
||||
Two => exactly_n_digits::<u32, 2>(input)?.map(|v| v * 10_000_000),
|
||||
Three => exactly_n_digits::<u32, 3>(input)?.map(|v| v * 1_000_000),
|
||||
Four => exactly_n_digits::<u32, 4>(input)?.map(|v| v * 100_000),
|
||||
Five => exactly_n_digits::<u32, 5>(input)?.map(|v| v * 10_000),
|
||||
Six => exactly_n_digits::<u32, 6>(input)?.map(|v| v * 1_000),
|
||||
Seven => exactly_n_digits::<u32, 7>(input)?.map(|v| v * 100),
|
||||
Eight => exactly_n_digits::<u32, 8>(input)?.map(|v| v * 10),
|
||||
Nine => exactly_n_digits::<u32, 9>(input)?,
|
||||
OneOrMore => {
|
||||
let ParsedItem(mut input, mut value) =
|
||||
any_digit(input)?.map(|v| (v - b'0') as u32 * 100_000_000);
|
||||
|
||||
let mut multiplier = 10_000_000;
|
||||
while let Some(ParsedItem(new_input, digit)) = any_digit(input) {
|
||||
value += (digit - b'0') as u32 * multiplier;
|
||||
input = new_input;
|
||||
multiplier /= 10;
|
||||
}
|
||||
|
||||
ParsedItem(input, value)
|
||||
}
|
||||
})
|
||||
}
|
||||
// endregion time components
|
||||
|
||||
// region: offset components
|
||||
/// Parse the "hour" component of a `UtcOffset`.
|
||||
pub(crate) fn parse_offset_hour(
|
||||
input: &[u8],
|
||||
modifiers: modifier::OffsetHour,
|
||||
) -> Option<ParsedItem<'_, i8>> {
|
||||
let ParsedItem(input, sign) = opt(sign)(input);
|
||||
let ParsedItem(input, hour) = exactly_n_digits_padded::<u8, 2>(modifiers.padding)(input)?;
|
||||
match sign {
|
||||
Some(b'-') => Some(ParsedItem(input, -(hour as i8))),
|
||||
None if modifiers.sign_is_mandatory => None,
|
||||
_ => Some(ParsedItem(input, hour as i8)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the "minute" component of a `UtcOffset`.
|
||||
pub(crate) fn parse_offset_minute(
|
||||
input: &[u8],
|
||||
modifiers: modifier::OffsetMinute,
|
||||
) -> Option<ParsedItem<'_, i8>> {
|
||||
Some(
|
||||
exactly_n_digits_padded::<u8, 2>(modifiers.padding)(input)?
|
||||
.map(|offset_minute| offset_minute as _),
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse the "second" component of a `UtcOffset`.
|
||||
pub(crate) fn parse_offset_second(
|
||||
input: &[u8],
|
||||
modifiers: modifier::OffsetSecond,
|
||||
) -> Option<ParsedItem<'_, i8>> {
|
||||
Some(
|
||||
exactly_n_digits_padded::<u8, 2>(modifiers.padding)(input)?
|
||||
.map(|offset_second| offset_second as _),
|
||||
)
|
||||
}
|
||||
// endregion offset components
|
|
@ -0,0 +1,49 @@
|
|||
//! Parsing for various types.
|
||||
|
||||
pub(crate) mod combinator;
|
||||
pub(crate) mod component;
|
||||
pub(crate) mod parsable;
|
||||
mod parsed;
|
||||
pub(crate) mod shim;
|
||||
|
||||
pub use self::parsable::Parsable;
|
||||
pub use self::parsed::Parsed;
|
||||
|
||||
/// An item that has been parsed. Represented as a `(remaining, value)` pair.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ParsedItem<'a, T>(pub(crate) &'a [u8], pub(crate) T);
|
||||
|
||||
impl<'a, T> ParsedItem<'a, T> {
|
||||
/// Map the value to a new value, preserving the remaining input.
|
||||
pub(crate) fn map<U>(self, f: impl FnOnce(T) -> U) -> ParsedItem<'a, U> {
|
||||
ParsedItem(self.0, f(self.1))
|
||||
}
|
||||
|
||||
/// Map the value to a new, optional value, preserving the remaining input.
|
||||
pub(crate) fn flat_map<U>(self, f: impl FnOnce(T) -> Option<U>) -> Option<ParsedItem<'a, U>> {
|
||||
Some(ParsedItem(self.0, f(self.1)?))
|
||||
}
|
||||
|
||||
/// Consume the stored value with the provided function. The remaining input is returned.
|
||||
#[must_use = "this returns the remaining input"]
|
||||
pub(crate) fn consume_value(self, f: impl FnOnce(T) -> Option<()>) -> Option<&'a [u8]> {
|
||||
f(self.1)?;
|
||||
Some(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ParsedItem<'a, ()> {
|
||||
/// Discard the unit value, returning the remaining input.
|
||||
#[must_use = "this returns the remaining input"]
|
||||
pub(crate) const fn into_inner(self) -> &'a [u8] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ParsedItem<'a, Option<()>> {
|
||||
/// Discard the potential unit value, returning the remaining input.
|
||||
#[must_use = "this returns the remaining input"]
|
||||
pub(crate) const fn into_inner(self) -> &'a [u8] {
|
||||
self.0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,492 @@
|
|||
//! A trait that can be used to parse an item from an input.
|
||||
|
||||
use core::convert::TryInto;
|
||||
use core::ops::Deref;
|
||||
|
||||
use crate::error::TryFromParsed;
|
||||
use crate::format_description::well_known::{Rfc2822, Rfc3339};
|
||||
use crate::format_description::FormatItem;
|
||||
use crate::parsing::{Parsed, ParsedItem};
|
||||
use crate::{error, Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday};
|
||||
|
||||
/// A type that can be parsed.
|
||||
#[cfg_attr(__time_03_docs, doc(notable_trait))]
|
||||
pub trait Parsable: sealed::Sealed {}
|
||||
impl Parsable for FormatItem<'_> {}
|
||||
impl Parsable for [FormatItem<'_>] {}
|
||||
impl Parsable for Rfc2822 {}
|
||||
impl Parsable for Rfc3339 {}
|
||||
impl<T: Deref> Parsable for T where T::Target: Parsable {}
|
||||
|
||||
/// Seal the trait to prevent downstream users from implementing it, while still allowing it to
|
||||
/// exist in generic bounds.
|
||||
mod sealed {
|
||||
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use super::*;
|
||||
|
||||
/// Parse the item using a format description and an input.
|
||||
#[cfg_attr(__time_03_docs, doc(cfg(feature = "parsing")))]
|
||||
pub trait Sealed {
|
||||
/// Parse the item into the provided [`Parsed`] struct.
|
||||
///
|
||||
/// This method can be used to parse a single component without parsing the full value.
|
||||
fn parse_into<'a>(
|
||||
&self,
|
||||
input: &'a [u8],
|
||||
parsed: &mut Parsed,
|
||||
) -> Result<&'a [u8], error::Parse>;
|
||||
|
||||
/// Parse the item into a new [`Parsed`] struct.
|
||||
///
|
||||
/// This method can only be used to parse a complete value of a type. If any characters
|
||||
/// remain after parsing, an error will be returned.
|
||||
fn parse(&self, input: &[u8]) -> Result<Parsed, error::Parse> {
|
||||
let mut parsed = Parsed::new();
|
||||
if self.parse_into(input, &mut parsed)?.is_empty() {
|
||||
Ok(parsed)
|
||||
} else {
|
||||
Err(error::Parse::UnexpectedTrailingCharacters)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a [`Date`] from the format description.
|
||||
fn parse_date(&self, input: &[u8]) -> Result<Date, error::Parse> {
|
||||
Ok(self.parse(input)?.try_into()?)
|
||||
}
|
||||
|
||||
/// Parse a [`Time`] from the format description.
|
||||
fn parse_time(&self, input: &[u8]) -> Result<Time, error::Parse> {
|
||||
Ok(self.parse(input)?.try_into()?)
|
||||
}
|
||||
|
||||
/// Parse a [`UtcOffset`] from the format description.
|
||||
fn parse_offset(&self, input: &[u8]) -> Result<UtcOffset, error::Parse> {
|
||||
Ok(self.parse(input)?.try_into()?)
|
||||
}
|
||||
|
||||
/// Parse a [`PrimitiveDateTime`] from the format description.
|
||||
fn parse_date_time(&self, input: &[u8]) -> Result<PrimitiveDateTime, error::Parse> {
|
||||
Ok(self.parse(input)?.try_into()?)
|
||||
}
|
||||
|
||||
/// Parse a [`OffsetDateTime`] from the format description.
|
||||
fn parse_offset_date_time(&self, input: &[u8]) -> Result<OffsetDateTime, error::Parse> {
|
||||
Ok(self.parse(input)?.try_into()?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region: custom formats
|
||||
impl sealed::Sealed for FormatItem<'_> {
|
||||
fn parse_into<'a>(
|
||||
&self,
|
||||
input: &'a [u8],
|
||||
parsed: &mut Parsed,
|
||||
) -> Result<&'a [u8], error::Parse> {
|
||||
Ok(parsed.parse_item(input, self)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl sealed::Sealed for [FormatItem<'_>] {
|
||||
fn parse_into<'a>(
|
||||
&self,
|
||||
input: &'a [u8],
|
||||
parsed: &mut Parsed,
|
||||
) -> Result<&'a [u8], error::Parse> {
|
||||
Ok(parsed.parse_items(input, self)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Deref> sealed::Sealed for T
|
||||
where
|
||||
T::Target: sealed::Sealed,
|
||||
{
|
||||
fn parse_into<'a>(
|
||||
&self,
|
||||
input: &'a [u8],
|
||||
parsed: &mut Parsed,
|
||||
) -> Result<&'a [u8], error::Parse> {
|
||||
self.deref().parse_into(input, parsed)
|
||||
}
|
||||
}
|
||||
// endregion custom formats
|
||||
|
||||
// region: well-known formats
|
||||
impl sealed::Sealed for Rfc2822 {
|
||||
fn parse_into<'a>(
|
||||
&self,
|
||||
input: &'a [u8],
|
||||
parsed: &mut Parsed,
|
||||
) -> Result<&'a [u8], error::Parse> {
|
||||
use crate::error::ParseFromDescription::{InvalidComponent, InvalidLiteral};
|
||||
use crate::parsing::combinator::rfc::rfc2822::{cfws, fws};
|
||||
use crate::parsing::combinator::{
|
||||
ascii_char, exactly_n_digits, first_match, n_to_m_digits, opt, sign,
|
||||
};
|
||||
|
||||
let colon = ascii_char::<b':'>;
|
||||
let comma = ascii_char::<b','>;
|
||||
|
||||
let input = opt(fws)(input).into_inner();
|
||||
let input = first_match(
|
||||
[
|
||||
(&b"Mon"[..], Weekday::Monday),
|
||||
(&b"Tue"[..], Weekday::Tuesday),
|
||||
(&b"Wed"[..], Weekday::Wednesday),
|
||||
(&b"Thu"[..], Weekday::Thursday),
|
||||
(&b"Fri"[..], Weekday::Friday),
|
||||
(&b"Sat"[..], Weekday::Saturday),
|
||||
(&b"Sun"[..], Weekday::Sunday),
|
||||
],
|
||||
false,
|
||||
)(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_weekday(value)))
|
||||
.ok_or(InvalidComponent("weekday"))?;
|
||||
let input = comma(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let input = cfws(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let input = n_to_m_digits::<_, 1, 2>(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_day(value)))
|
||||
.ok_or(InvalidComponent("day"))?;
|
||||
let input = cfws(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let input = first_match(
|
||||
[
|
||||
(&b"Jan"[..], Month::January),
|
||||
(&b"Feb"[..], Month::February),
|
||||
(&b"Mar"[..], Month::March),
|
||||
(&b"Apr"[..], Month::April),
|
||||
(&b"May"[..], Month::May),
|
||||
(&b"Jun"[..], Month::June),
|
||||
(&b"Jul"[..], Month::July),
|
||||
(&b"Aug"[..], Month::August),
|
||||
(&b"Sep"[..], Month::September),
|
||||
(&b"Oct"[..], Month::October),
|
||||
(&b"Nov"[..], Month::November),
|
||||
(&b"Dec"[..], Month::December),
|
||||
],
|
||||
false,
|
||||
)(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_month(value)))
|
||||
.ok_or(InvalidComponent("month"))?;
|
||||
let input = cfws(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let input = match exactly_n_digits::<u32, 4>(input) {
|
||||
Some(item) => {
|
||||
let input = item
|
||||
.flat_map(|year| if year >= 1900 { Some(year) } else { None })
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_year(value as _)))
|
||||
.ok_or(InvalidComponent("year"))?;
|
||||
let input = fws(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
input
|
||||
}
|
||||
None => {
|
||||
let input = exactly_n_digits::<u32, 2>(input)
|
||||
.and_then(|item| {
|
||||
item.map(|year| if year < 50 { year + 2000 } else { year + 1900 })
|
||||
.map(|year| year as _)
|
||||
.consume_value(|value| parsed.set_year(value))
|
||||
})
|
||||
.ok_or(InvalidComponent("year"))?;
|
||||
let input = cfws(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
input
|
||||
}
|
||||
};
|
||||
|
||||
let input = exactly_n_digits::<_, 2>(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_hour_24(value)))
|
||||
.ok_or(InvalidComponent("hour"))?;
|
||||
let input = opt(cfws)(input).into_inner();
|
||||
let input = colon(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let input = opt(cfws)(input).into_inner();
|
||||
let input = exactly_n_digits::<_, 2>(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_minute(value)))
|
||||
.ok_or(InvalidComponent("minute"))?;
|
||||
|
||||
let input = if let Some(input) = colon(opt(cfws)(input).into_inner()) {
|
||||
let input = input.into_inner(); // discard the colon
|
||||
let input = opt(cfws)(input).into_inner();
|
||||
let input = exactly_n_digits::<_, 2>(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_second(value)))
|
||||
.ok_or(InvalidComponent("second"))?;
|
||||
let input = cfws(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
input
|
||||
} else {
|
||||
cfws(input).ok_or(InvalidLiteral)?.into_inner()
|
||||
};
|
||||
|
||||
// The RFC explicitly allows leap seconds.
|
||||
parsed.set_leap_second_allowed(true);
|
||||
|
||||
#[allow(clippy::unnecessary_lazy_evaluations)] // rust-lang/rust-clippy#8522
|
||||
let zone_literal = first_match(
|
||||
[
|
||||
(&b"UT"[..], 0),
|
||||
(&b"GMT"[..], 0),
|
||||
(&b"EST"[..], -5),
|
||||
(&b"EDT"[..], -4),
|
||||
(&b"CST"[..], -6),
|
||||
(&b"CDT"[..], -5),
|
||||
(&b"MST"[..], -7),
|
||||
(&b"MDT"[..], -6),
|
||||
(&b"PST"[..], -8),
|
||||
(&b"PDT"[..], -7),
|
||||
],
|
||||
false,
|
||||
)(input)
|
||||
.or_else(|| match input {
|
||||
[
|
||||
b'a'..=b'i' | b'k'..=b'z' | b'A'..=b'I' | b'K'..=b'Z',
|
||||
rest @ ..,
|
||||
] => Some(ParsedItem(rest, 0)),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(zone_literal) = zone_literal {
|
||||
let input = zone_literal
|
||||
.consume_value(|value| parsed.set_offset_hour(value))
|
||||
.ok_or(InvalidComponent("offset hour"))?;
|
||||
parsed
|
||||
.set_offset_minute_signed(0)
|
||||
.ok_or(InvalidComponent("offset minute"))?;
|
||||
parsed
|
||||
.set_offset_second_signed(0)
|
||||
.ok_or(InvalidComponent("offset second"))?;
|
||||
return Ok(input);
|
||||
}
|
||||
|
||||
let ParsedItem(input, offset_sign) = sign(input).ok_or(InvalidComponent("offset hour"))?;
|
||||
let input = exactly_n_digits::<u8, 2>(input)
|
||||
.and_then(|item| {
|
||||
item.map(|offset_hour| {
|
||||
if offset_sign == b'-' {
|
||||
-(offset_hour as i8)
|
||||
} else {
|
||||
offset_hour as _
|
||||
}
|
||||
})
|
||||
.consume_value(|value| parsed.set_offset_hour(value))
|
||||
})
|
||||
.ok_or(InvalidComponent("offset hour"))?;
|
||||
let input = exactly_n_digits::<u8, 2>(input)
|
||||
.and_then(|item| {
|
||||
item.consume_value(|value| parsed.set_offset_minute_signed(value as _))
|
||||
})
|
||||
.ok_or(InvalidComponent("offset minute"))?;
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
}
|
||||
|
||||
impl sealed::Sealed for Rfc3339 {
|
||||
fn parse_into<'a>(
|
||||
&self,
|
||||
input: &'a [u8],
|
||||
parsed: &mut Parsed,
|
||||
) -> Result<&'a [u8], error::Parse> {
|
||||
use crate::error::ParseFromDescription::{InvalidComponent, InvalidLiteral};
|
||||
use crate::parsing::combinator::{
|
||||
any_digit, ascii_char, ascii_char_ignore_case, exactly_n_digits, sign,
|
||||
};
|
||||
|
||||
let dash = ascii_char::<b'-'>;
|
||||
let colon = ascii_char::<b':'>;
|
||||
|
||||
let input = exactly_n_digits::<u32, 4>(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_year(value as _)))
|
||||
.ok_or(InvalidComponent("year"))?;
|
||||
let input = dash(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let input = exactly_n_digits::<_, 2>(input)
|
||||
.and_then(|item| item.flat_map(|value| Month::from_number(value).ok()))
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_month(value)))
|
||||
.ok_or(InvalidComponent("month"))?;
|
||||
let input = dash(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let input = exactly_n_digits::<_, 2>(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_day(value)))
|
||||
.ok_or(InvalidComponent("day"))?;
|
||||
let input = ascii_char_ignore_case::<b'T'>(input)
|
||||
.ok_or(InvalidLiteral)?
|
||||
.into_inner();
|
||||
let input = exactly_n_digits::<_, 2>(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_hour_24(value)))
|
||||
.ok_or(InvalidComponent("hour"))?;
|
||||
let input = colon(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let input = exactly_n_digits::<_, 2>(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_minute(value)))
|
||||
.ok_or(InvalidComponent("minute"))?;
|
||||
let input = colon(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let input = exactly_n_digits::<_, 2>(input)
|
||||
.and_then(|item| item.consume_value(|value| parsed.set_second(value)))
|
||||
.ok_or(InvalidComponent("second"))?;
|
||||
let input = if let Some(ParsedItem(input, ())) = ascii_char::<b'.'>(input) {
|
||||
let ParsedItem(mut input, mut value) = any_digit(input)
|
||||
.ok_or(InvalidComponent("subsecond"))?
|
||||
.map(|v| (v - b'0') as u32 * 100_000_000);
|
||||
|
||||
let mut multiplier = 10_000_000;
|
||||
while let Some(ParsedItem(new_input, digit)) = any_digit(input) {
|
||||
value += (digit - b'0') as u32 * multiplier;
|
||||
input = new_input;
|
||||
multiplier /= 10;
|
||||
}
|
||||
|
||||
parsed
|
||||
.set_subsecond(value)
|
||||
.ok_or(InvalidComponent("subsecond"))?;
|
||||
input
|
||||
} else {
|
||||
input
|
||||
};
|
||||
|
||||
// The RFC explicitly allows leap seconds.
|
||||
parsed.set_leap_second_allowed(true);
|
||||
|
||||
if let Some(ParsedItem(input, ())) = ascii_char_ignore_case::<b'Z'>(input) {
|
||||
parsed
|
||||
.set_offset_hour(0)
|
||||
.ok_or(InvalidComponent("offset hour"))?;
|
||||
parsed
|
||||
.set_offset_minute_signed(0)
|
||||
.ok_or(InvalidComponent("offset minute"))?;
|
||||
parsed
|
||||
.set_offset_second_signed(0)
|
||||
.ok_or(InvalidComponent("offset second"))?;
|
||||
return Ok(input);
|
||||
}
|
||||
|
||||
let ParsedItem(input, offset_sign) = sign(input).ok_or(InvalidComponent("offset hour"))?;
|
||||
let input = exactly_n_digits::<u8, 2>(input)
|
||||
.and_then(|item| {
|
||||
item.map(|offset_hour| {
|
||||
if offset_sign == b'-' {
|
||||
-(offset_hour as i8)
|
||||
} else {
|
||||
offset_hour as _
|
||||
}
|
||||
})
|
||||
.consume_value(|value| parsed.set_offset_hour(value))
|
||||
})
|
||||
.ok_or(InvalidComponent("offset hour"))?;
|
||||
let input = colon(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let input = exactly_n_digits::<u8, 2>(input)
|
||||
.and_then(|item| {
|
||||
item.consume_value(|value| parsed.set_offset_minute_signed(value as _))
|
||||
})
|
||||
.ok_or(InvalidComponent("offset minute"))?;
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
fn parse_offset_date_time(&self, input: &[u8]) -> Result<OffsetDateTime, error::Parse> {
|
||||
use crate::error::ParseFromDescription::{InvalidComponent, InvalidLiteral};
|
||||
use crate::parsing::combinator::{
|
||||
any_digit, ascii_char, ascii_char_ignore_case, exactly_n_digits, sign,
|
||||
};
|
||||
|
||||
let dash = ascii_char::<b'-'>;
|
||||
let colon = ascii_char::<b':'>;
|
||||
|
||||
let ParsedItem(input, year) =
|
||||
exactly_n_digits::<u32, 4>(input).ok_or(InvalidComponent("year"))?;
|
||||
let input = dash(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let ParsedItem(input, month) =
|
||||
exactly_n_digits::<_, 2>(input).ok_or(InvalidComponent("month"))?;
|
||||
let input = dash(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let ParsedItem(input, day) =
|
||||
exactly_n_digits::<_, 2>(input).ok_or(InvalidComponent("day"))?;
|
||||
let input = ascii_char_ignore_case::<b'T'>(input)
|
||||
.ok_or(InvalidLiteral)?
|
||||
.into_inner();
|
||||
let ParsedItem(input, hour) =
|
||||
exactly_n_digits::<_, 2>(input).ok_or(InvalidComponent("hour"))?;
|
||||
let input = colon(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let ParsedItem(input, minute) =
|
||||
exactly_n_digits::<_, 2>(input).ok_or(InvalidComponent("minute"))?;
|
||||
let input = colon(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let ParsedItem(input, mut second) =
|
||||
exactly_n_digits::<_, 2>(input).ok_or(InvalidComponent("second"))?;
|
||||
let ParsedItem(input, mut nanosecond) =
|
||||
if let Some(ParsedItem(input, ())) = ascii_char::<b'.'>(input) {
|
||||
let ParsedItem(mut input, mut value) = any_digit(input)
|
||||
.ok_or(InvalidComponent("subsecond"))?
|
||||
.map(|v| (v - b'0') as u32 * 100_000_000);
|
||||
|
||||
let mut multiplier = 10_000_000;
|
||||
while let Some(ParsedItem(new_input, digit)) = any_digit(input) {
|
||||
value += (digit - b'0') as u32 * multiplier;
|
||||
input = new_input;
|
||||
multiplier /= 10;
|
||||
}
|
||||
|
||||
ParsedItem(input, value)
|
||||
} else {
|
||||
ParsedItem(input, 0)
|
||||
};
|
||||
let ParsedItem(input, offset) = {
|
||||
if let Some(ParsedItem(input, ())) = ascii_char_ignore_case::<b'Z'>(input) {
|
||||
ParsedItem(input, UtcOffset::UTC)
|
||||
} else {
|
||||
let ParsedItem(input, offset_sign) =
|
||||
sign(input).ok_or(InvalidComponent("offset hour"))?;
|
||||
let ParsedItem(input, offset_hour) =
|
||||
exactly_n_digits::<u8, 2>(input).ok_or(InvalidComponent("offset hour"))?;
|
||||
let input = colon(input).ok_or(InvalidLiteral)?.into_inner();
|
||||
let ParsedItem(input, offset_minute) =
|
||||
exactly_n_digits::<u8, 2>(input).ok_or(InvalidComponent("offset minute"))?;
|
||||
UtcOffset::from_hms(
|
||||
if offset_sign == b'-' {
|
||||
-(offset_hour as i8)
|
||||
} else {
|
||||
offset_hour as _
|
||||
},
|
||||
offset_minute as _,
|
||||
0,
|
||||
)
|
||||
.map(|offset| ParsedItem(input, offset))
|
||||
.map_err(|mut err| {
|
||||
// Provide the user a more accurate error.
|
||||
if err.name == "hours" {
|
||||
err.name = "offset hour";
|
||||
} else if err.name == "minutes" {
|
||||
err.name = "offset minute";
|
||||
}
|
||||
err
|
||||
})
|
||||
.map_err(TryFromParsed::ComponentRange)?
|
||||
}
|
||||
};
|
||||
|
||||
if !input.is_empty() {
|
||||
return Err(error::Parse::UnexpectedTrailingCharacters);
|
||||
}
|
||||
|
||||
// The RFC explicitly permits leap seconds. We don't currently support them, so treat it as
|
||||
// the preceding nanosecond. However, leap seconds can only occur as the last second of the
|
||||
// month UTC.
|
||||
let leap_second_input = if second == 60 {
|
||||
second = 59;
|
||||
nanosecond = 999_999_999;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let dt = Month::from_number(month)
|
||||
.and_then(|month| Date::from_calendar_date(year as _, month, day))
|
||||
.and_then(|date| date.with_hms_nano(hour, minute, second, nanosecond))
|
||||
.map(|date| date.assume_offset(offset))
|
||||
.map_err(TryFromParsed::ComponentRange)?;
|
||||
|
||||
if leap_second_input && !dt.is_valid_leap_second_stand_in() {
|
||||
return Err(error::Parse::TryFromParsed(TryFromParsed::ComponentRange(
|
||||
error::ComponentRange {
|
||||
name: "second",
|
||||
minimum: 0,
|
||||
maximum: 59,
|
||||
value: 60,
|
||||
conditional_range: true,
|
||||
},
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(dt)
|
||||
}
|
||||
}
|
||||
// endregion well-known formats
|
|
@ -0,0 +1,625 @@
|
|||
//! Information parsed from an input and format description.
|
||||
|
||||
use core::convert::{TryFrom, TryInto};
|
||||
use core::num::{NonZeroU16, NonZeroU8};
|
||||
|
||||
use crate::error::TryFromParsed::InsufficientInformation;
|
||||
use crate::format_description::modifier::{WeekNumberRepr, YearRepr};
|
||||
use crate::format_description::{Component, FormatItem};
|
||||
use crate::parsing::component::{
|
||||
parse_day, parse_hour, parse_minute, parse_month, parse_offset_hour, parse_offset_minute,
|
||||
parse_offset_second, parse_ordinal, parse_period, parse_second, parse_subsecond,
|
||||
parse_week_number, parse_weekday, parse_year, Period,
|
||||
};
|
||||
use crate::parsing::ParsedItem;
|
||||
use crate::{error, Date, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcOffset, Weekday};
|
||||
|
||||
/// All information parsed.
|
||||
///
|
||||
/// This information is directly used to construct the final values.
|
||||
///
|
||||
/// Most users will not need think about this struct in any way. It is public to allow for manual
|
||||
/// control over values, in the instance that the default parser is insufficient.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Parsed {
|
||||
/// Calendar year.
|
||||
year: Option<i32>,
|
||||
/// The last two digits of the calendar year.
|
||||
year_last_two: Option<u8>,
|
||||
/// Year of the [ISO week date](https://en.wikipedia.org/wiki/ISO_week_date).
|
||||
iso_year: Option<i32>,
|
||||
/// The last two digits of the ISO week year.
|
||||
iso_year_last_two: Option<u8>,
|
||||
/// Month of the year.
|
||||
month: Option<Month>,
|
||||
/// Week of the year, where week one begins on the first Sunday of the calendar year.
|
||||
sunday_week_number: Option<u8>,
|
||||
/// Week of the year, where week one begins on the first Monday of the calendar year.
|
||||
monday_week_number: Option<u8>,
|
||||
/// Week of the year, where week one is the Monday-to-Sunday period containing January 4.
|
||||
iso_week_number: Option<NonZeroU8>,
|
||||
/// Day of the week.
|
||||
weekday: Option<Weekday>,
|
||||
/// Day of the year.
|
||||
ordinal: Option<NonZeroU16>,
|
||||
/// Day of the month.
|
||||
day: Option<NonZeroU8>,
|
||||
/// Hour within the day.
|
||||
hour_24: Option<u8>,
|
||||
/// Hour within the 12-hour period (midnight to noon or vice versa). This is typically used in
|
||||
/// conjunction with AM/PM, which is indicated by the `hour_12_is_pm` field.
|
||||
hour_12: Option<NonZeroU8>,
|
||||
/// Whether the `hour_12` field indicates a time that "PM".
|
||||
hour_12_is_pm: Option<bool>,
|
||||
/// Minute within the hour.
|
||||
minute: Option<u8>,
|
||||
/// Second within the minute.
|
||||
second: Option<u8>,
|
||||
/// Nanosecond within the second.
|
||||
subsecond: Option<u32>,
|
||||
/// Whole hours of the UTC offset.
|
||||
offset_hour: Option<i8>,
|
||||
/// Minutes within the hour of the UTC offset.
|
||||
offset_minute: Option<i8>,
|
||||
/// Seconds within the minute of the UTC offset.
|
||||
offset_second: Option<i8>,
|
||||
/// Indicates whether a leap second is permitted to be parsed. This is required by some
|
||||
/// well-known formats.
|
||||
leap_second_allowed: bool,
|
||||
}
|
||||
|
||||
impl Parsed {
|
||||
/// Create a new instance of `Parsed` with no information known.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
year: None,
|
||||
year_last_two: None,
|
||||
iso_year: None,
|
||||
iso_year_last_two: None,
|
||||
month: None,
|
||||
sunday_week_number: None,
|
||||
monday_week_number: None,
|
||||
iso_week_number: None,
|
||||
weekday: None,
|
||||
ordinal: None,
|
||||
day: None,
|
||||
hour_24: None,
|
||||
hour_12: None,
|
||||
hour_12_is_pm: None,
|
||||
minute: None,
|
||||
second: None,
|
||||
subsecond: None,
|
||||
offset_hour: None,
|
||||
offset_minute: None,
|
||||
offset_second: None,
|
||||
leap_second_allowed: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a single [`FormatItem`], mutating the struct. The remaining input is returned as the
|
||||
/// `Ok` value.
|
||||
///
|
||||
/// If a [`FormatItem::Optional`] is passed, parsing will not fail; the input will be returned
|
||||
/// as-is if the expected format is not present.
|
||||
pub fn parse_item<'a>(
|
||||
&mut self,
|
||||
input: &'a [u8],
|
||||
item: &FormatItem<'_>,
|
||||
) -> Result<&'a [u8], error::ParseFromDescription> {
|
||||
match item {
|
||||
FormatItem::Literal(literal) => Self::parse_literal(input, literal),
|
||||
FormatItem::Component(component) => self.parse_component(input, *component),
|
||||
FormatItem::Compound(compound) => self.parse_items(input, compound),
|
||||
FormatItem::Optional(item) => self.parse_item(input, item).or(Ok(input)),
|
||||
FormatItem::First(items) => {
|
||||
let mut first_err = None;
|
||||
|
||||
for item in items.iter() {
|
||||
match self.parse_item(input, item) {
|
||||
Ok(remaining_input) => return Ok(remaining_input),
|
||||
Err(err) if first_err.is_none() => first_err = Some(err),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
match first_err {
|
||||
Some(err) => Err(err),
|
||||
// This location will be reached if the slice is empty, skipping the `for` loop.
|
||||
// As this case is expected to be uncommon, there's no need to check up front.
|
||||
None => Ok(input),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a sequence of [`FormatItem`]s, mutating the struct. The remaining input is returned as
|
||||
/// the `Ok` value.
|
||||
///
|
||||
/// This method will fail if any of the contained [`FormatItem`]s fail to parse. `self` will not
|
||||
/// be mutated in this instance.
|
||||
pub fn parse_items<'a>(
|
||||
&mut self,
|
||||
mut input: &'a [u8],
|
||||
items: &[FormatItem<'_>],
|
||||
) -> Result<&'a [u8], error::ParseFromDescription> {
|
||||
// Make a copy that we can mutate. It will only be set to the user's copy if everything
|
||||
// succeeds.
|
||||
let mut this = *self;
|
||||
for item in items {
|
||||
input = this.parse_item(input, item)?;
|
||||
}
|
||||
*self = this;
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
/// Parse a literal byte sequence. The remaining input is returned as the `Ok` value.
|
||||
pub fn parse_literal<'a>(
|
||||
input: &'a [u8],
|
||||
literal: &[u8],
|
||||
) -> Result<&'a [u8], error::ParseFromDescription> {
|
||||
input
|
||||
.strip_prefix(literal)
|
||||
.ok_or(error::ParseFromDescription::InvalidLiteral)
|
||||
}
|
||||
|
||||
/// Parse a single component, mutating the struct. The remaining input is returned as the `Ok`
|
||||
/// value.
|
||||
pub fn parse_component<'a>(
|
||||
&mut self,
|
||||
input: &'a [u8],
|
||||
component: Component,
|
||||
) -> Result<&'a [u8], error::ParseFromDescription> {
|
||||
use error::ParseFromDescription::InvalidComponent;
|
||||
|
||||
match component {
|
||||
Component::Day(modifiers) => parse_day(input, modifiers)
|
||||
.and_then(|parsed| parsed.consume_value(|value| self.set_day(value)))
|
||||
.ok_or(InvalidComponent("day")),
|
||||
Component::Month(modifiers) => parse_month(input, modifiers)
|
||||
.and_then(|parsed| parsed.consume_value(|value| self.set_month(value)))
|
||||
.ok_or(InvalidComponent("month")),
|
||||
Component::Ordinal(modifiers) => parse_ordinal(input, modifiers)
|
||||
.and_then(|parsed| parsed.consume_value(|value| self.set_ordinal(value)))
|
||||
.ok_or(InvalidComponent("ordinal")),
|
||||
Component::Weekday(modifiers) => parse_weekday(input, modifiers)
|
||||
.and_then(|parsed| parsed.consume_value(|value| self.set_weekday(value)))
|
||||
.ok_or(InvalidComponent("weekday")),
|
||||
Component::WeekNumber(modifiers) => {
|
||||
let ParsedItem(remaining, value) =
|
||||
parse_week_number(input, modifiers).ok_or(InvalidComponent("week number"))?;
|
||||
match modifiers.repr {
|
||||
WeekNumberRepr::Iso => {
|
||||
NonZeroU8::new(value).and_then(|value| self.set_iso_week_number(value))
|
||||
}
|
||||
WeekNumberRepr::Sunday => self.set_sunday_week_number(value),
|
||||
WeekNumberRepr::Monday => self.set_monday_week_number(value),
|
||||
}
|
||||
.ok_or(InvalidComponent("week number"))?;
|
||||
Ok(remaining)
|
||||
}
|
||||
Component::Year(modifiers) => {
|
||||
let ParsedItem(remaining, value) =
|
||||
parse_year(input, modifiers).ok_or(InvalidComponent("year"))?;
|
||||
match (modifiers.iso_week_based, modifiers.repr) {
|
||||
(false, YearRepr::Full) => self.set_year(value),
|
||||
(false, YearRepr::LastTwo) => self.set_year_last_two(value as _),
|
||||
(true, YearRepr::Full) => self.set_iso_year(value),
|
||||
(true, YearRepr::LastTwo) => self.set_iso_year_last_two(value as _),
|
||||
}
|
||||
.ok_or(InvalidComponent("year"))?;
|
||||
Ok(remaining)
|
||||
}
|
||||
Component::Hour(modifiers) => {
|
||||
let ParsedItem(remaining, value) =
|
||||
parse_hour(input, modifiers).ok_or(InvalidComponent("hour"))?;
|
||||
if modifiers.is_12_hour_clock {
|
||||
NonZeroU8::new(value).and_then(|value| self.set_hour_12(value))
|
||||
} else {
|
||||
self.set_hour_24(value)
|
||||
}
|
||||
.ok_or(InvalidComponent("hour"))?;
|
||||
Ok(remaining)
|
||||
}
|
||||
Component::Minute(modifiers) => parse_minute(input, modifiers)
|
||||
.and_then(|parsed| parsed.consume_value(|value| self.set_minute(value)))
|
||||
.ok_or(InvalidComponent("minute")),
|
||||
Component::Period(modifiers) => parse_period(input, modifiers)
|
||||
.and_then(|parsed| {
|
||||
parsed.consume_value(|value| self.set_hour_12_is_pm(value == Period::Pm))
|
||||
})
|
||||
.ok_or(InvalidComponent("period")),
|
||||
Component::Second(modifiers) => parse_second(input, modifiers)
|
||||
.and_then(|parsed| parsed.consume_value(|value| self.set_second(value)))
|
||||
.ok_or(InvalidComponent("second")),
|
||||
Component::Subsecond(modifiers) => parse_subsecond(input, modifiers)
|
||||
.and_then(|parsed| parsed.consume_value(|value| self.set_subsecond(value)))
|
||||
.ok_or(InvalidComponent("subsecond")),
|
||||
Component::OffsetHour(modifiers) => parse_offset_hour(input, modifiers)
|
||||
.and_then(|parsed| parsed.consume_value(|value| self.set_offset_hour(value)))
|
||||
.ok_or(InvalidComponent("offset hour")),
|
||||
Component::OffsetMinute(modifiers) => parse_offset_minute(input, modifiers)
|
||||
.and_then(|parsed| {
|
||||
parsed.consume_value(|value| self.set_offset_minute_signed(value))
|
||||
})
|
||||
.ok_or(InvalidComponent("offset minute")),
|
||||
Component::OffsetSecond(modifiers) => parse_offset_second(input, modifiers)
|
||||
.and_then(|parsed| {
|
||||
parsed.consume_value(|value| self.set_offset_second_signed(value))
|
||||
})
|
||||
.ok_or(InvalidComponent("offset second")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate getters for each of the fields.
|
||||
macro_rules! getters {
|
||||
($($name:ident: $ty:ty),+ $(,)?) => {$(
|
||||
/// Obtain the named component.
|
||||
pub const fn $name(&self) -> Option<$ty> {
|
||||
self.$name
|
||||
}
|
||||
)*}
|
||||
}
|
||||
|
||||
/// Getter methods
|
||||
impl Parsed {
|
||||
getters! {
|
||||
year: i32,
|
||||
year_last_two: u8,
|
||||
iso_year: i32,
|
||||
iso_year_last_two: u8,
|
||||
month: Month,
|
||||
sunday_week_number: u8,
|
||||
monday_week_number: u8,
|
||||
iso_week_number: NonZeroU8,
|
||||
weekday: Weekday,
|
||||
ordinal: NonZeroU16,
|
||||
day: NonZeroU8,
|
||||
hour_24: u8,
|
||||
hour_12: NonZeroU8,
|
||||
hour_12_is_pm: bool,
|
||||
minute: u8,
|
||||
second: u8,
|
||||
subsecond: u32,
|
||||
offset_hour: i8,
|
||||
}
|
||||
|
||||
/// Obtain the absolute value of the offset minute.
|
||||
#[deprecated(since = "0.3.8", note = "use `parsed.offset_minute_signed()` instead")]
|
||||
pub const fn offset_minute(&self) -> Option<u8> {
|
||||
Some(const_try_opt!(self.offset_minute_signed()).unsigned_abs())
|
||||
}
|
||||
|
||||
/// Obtain the offset minute as an `i8`.
|
||||
pub const fn offset_minute_signed(&self) -> Option<i8> {
|
||||
self.offset_minute
|
||||
}
|
||||
|
||||
/// Obtain the absolute value of the offset second.
|
||||
#[deprecated(since = "0.3.8", note = "use `parsed.offset_second_signed()` instead")]
|
||||
pub const fn offset_second(&self) -> Option<u8> {
|
||||
Some(const_try_opt!(self.offset_second_signed()).unsigned_abs())
|
||||
}
|
||||
|
||||
/// Obtain the offset second as an `i8`.
|
||||
pub const fn offset_second_signed(&self) -> Option<i8> {
|
||||
self.offset_second
|
||||
}
|
||||
|
||||
/// Obtain whether leap seconds are permitted in the current format.
|
||||
pub(crate) const fn leap_second_allowed(&self) -> bool {
|
||||
self.leap_second_allowed
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate setters for each of the fields.
|
||||
///
|
||||
/// This macro should only be used for fields where the value is not validated beyond its type.
|
||||
macro_rules! setters {
|
||||
($($setter_name:ident $name:ident: $ty:ty),+ $(,)?) => {$(
|
||||
/// Set the named component.
|
||||
pub fn $setter_name(&mut self, value: $ty) -> Option<()> {
|
||||
self.$name = Some(value);
|
||||
Some(())
|
||||
}
|
||||
)*}
|
||||
}
|
||||
|
||||
/// Setter methods
|
||||
///
|
||||
/// All setters return `Option<()>`, which is `Some` if the value was set, and `None` if not. The
|
||||
/// setters _may_ fail if the value is invalid, though behavior is not guaranteed.
|
||||
impl Parsed {
|
||||
setters! {
|
||||
set_year year: i32,
|
||||
set_year_last_two year_last_two: u8,
|
||||
set_iso_year iso_year: i32,
|
||||
set_iso_year_last_two iso_year_last_two: u8,
|
||||
set_month month: Month,
|
||||
set_sunday_week_number sunday_week_number: u8,
|
||||
set_monday_week_number monday_week_number: u8,
|
||||
set_iso_week_number iso_week_number: NonZeroU8,
|
||||
set_weekday weekday: Weekday,
|
||||
set_ordinal ordinal: NonZeroU16,
|
||||
set_day day: NonZeroU8,
|
||||
set_hour_24 hour_24: u8,
|
||||
set_hour_12 hour_12: NonZeroU8,
|
||||
set_hour_12_is_pm hour_12_is_pm: bool,
|
||||
set_minute minute: u8,
|
||||
set_second second: u8,
|
||||
set_subsecond subsecond: u32,
|
||||
set_offset_hour offset_hour: i8,
|
||||
}
|
||||
|
||||
/// Set the named component.
|
||||
#[deprecated(
|
||||
since = "0.3.8",
|
||||
note = "use `parsed.set_offset_minute_signed()` instead"
|
||||
)]
|
||||
pub fn set_offset_minute(&mut self, value: u8) -> Option<()> {
|
||||
if value > i8::MAX as u8 {
|
||||
None
|
||||
} else {
|
||||
self.set_offset_minute_signed(value as _)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `offset_minute` component.
|
||||
pub fn set_offset_minute_signed(&mut self, value: i8) -> Option<()> {
|
||||
self.offset_minute = Some(value);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Set the named component.
|
||||
#[deprecated(
|
||||
since = "0.3.8",
|
||||
note = "use `parsed.set_offset_second_signed()` instead"
|
||||
)]
|
||||
pub fn set_offset_second(&mut self, value: u8) -> Option<()> {
|
||||
if value > i8::MAX as u8 {
|
||||
None
|
||||
} else {
|
||||
self.set_offset_second_signed(value as _)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `offset_second` component.
|
||||
pub fn set_offset_second_signed(&mut self, value: i8) -> Option<()> {
|
||||
self.offset_second = Some(value);
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// Set the leap second allowed flag.
|
||||
pub(crate) fn set_leap_second_allowed(&mut self, value: bool) {
|
||||
self.leap_second_allowed = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate build methods for each of the fields.
|
||||
///
|
||||
/// This macro should only be used for fields where the value is not validated beyond its type.
|
||||
macro_rules! builders {
|
||||
($($builder_name:ident $name:ident: $ty:ty),+ $(,)?) => {$(
|
||||
/// Set the named component and return `self`.
|
||||
pub const fn $builder_name(mut self, value: $ty) -> Option<Self> {
|
||||
self.$name = Some(value);
|
||||
Some(self)
|
||||
}
|
||||
)*}
|
||||
}
|
||||
|
||||
/// Builder methods
|
||||
///
|
||||
/// All builder methods return `Option<Self>`, which is `Some` if the value was set, and `None` if
|
||||
/// not. The builder methods _may_ fail if the value is invalid, though behavior is not guaranteed.
|
||||
impl Parsed {
|
||||
builders! {
|
||||
with_year year: i32,
|
||||
with_year_last_two year_last_two: u8,
|
||||
with_iso_year iso_year: i32,
|
||||
with_iso_year_last_two iso_year_last_two: u8,
|
||||
with_month month: Month,
|
||||
with_sunday_week_number sunday_week_number: u8,
|
||||
with_monday_week_number monday_week_number: u8,
|
||||
with_iso_week_number iso_week_number: NonZeroU8,
|
||||
with_weekday weekday: Weekday,
|
||||
with_ordinal ordinal: NonZeroU16,
|
||||
with_day day: NonZeroU8,
|
||||
with_hour_24 hour_24: u8,
|
||||
with_hour_12 hour_12: NonZeroU8,
|
||||
with_hour_12_is_pm hour_12_is_pm: bool,
|
||||
with_minute minute: u8,
|
||||
with_second second: u8,
|
||||
with_subsecond subsecond: u32,
|
||||
with_offset_hour offset_hour: i8,
|
||||
}
|
||||
|
||||
/// Set the named component and return `self`.
|
||||
#[deprecated(
|
||||
since = "0.3.8",
|
||||
note = "use `parsed.with_offset_minute_signed()` instead"
|
||||
)]
|
||||
pub const fn with_offset_minute(self, value: u8) -> Option<Self> {
|
||||
if value > i8::MAX as u8 {
|
||||
None
|
||||
} else {
|
||||
self.with_offset_minute_signed(value as _)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `offset_minute` component and return `self`.
|
||||
pub const fn with_offset_minute_signed(mut self, value: i8) -> Option<Self> {
|
||||
self.offset_minute = Some(value);
|
||||
Some(self)
|
||||
}
|
||||
|
||||
/// Set the named component and return `self`.
|
||||
#[deprecated(
|
||||
since = "0.3.8",
|
||||
note = "use `parsed.with_offset_second_signed()` instead"
|
||||
)]
|
||||
pub const fn with_offset_second(self, value: u8) -> Option<Self> {
|
||||
if value > i8::MAX as u8 {
|
||||
None
|
||||
} else {
|
||||
self.with_offset_second_signed(value as _)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the `offset_second` component and return `self`.
|
||||
pub const fn with_offset_second_signed(mut self, value: i8) -> Option<Self> {
|
||||
self.offset_second = Some(value);
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Parsed> for Date {
|
||||
type Error = error::TryFromParsed;
|
||||
|
||||
fn try_from(parsed: Parsed) -> Result<Self, Self::Error> {
|
||||
/// Match on the components that need to be present.
|
||||
macro_rules! match_ {
|
||||
(_ => $catch_all:expr $(,)?) => {
|
||||
$catch_all
|
||||
};
|
||||
(($($name:ident),* $(,)?) => $arm:expr, $($rest:tt)*) => {
|
||||
if let ($(Some($name)),*) = ($(parsed.$name()),*) {
|
||||
$arm
|
||||
} else {
|
||||
match_!($($rest)*)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the value needed to adjust the ordinal day for Sunday and Monday-based week
|
||||
/// numbering.
|
||||
const fn adjustment(year: i32) -> i16 {
|
||||
match Date::__from_ordinal_date_unchecked(year, 1).weekday() {
|
||||
Weekday::Monday => 7,
|
||||
Weekday::Tuesday => 1,
|
||||
Weekday::Wednesday => 2,
|
||||
Weekday::Thursday => 3,
|
||||
Weekday::Friday => 4,
|
||||
Weekday::Saturday => 5,
|
||||
Weekday::Sunday => 6,
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Only the basics have been covered. There are many other valid values that are not
|
||||
// currently constructed from the information known.
|
||||
|
||||
match_! {
|
||||
(year, ordinal) => Ok(Self::from_ordinal_date(year, ordinal.get())?),
|
||||
(year, month, day) => Ok(Self::from_calendar_date(year, month, day.get())?),
|
||||
(iso_year, iso_week_number, weekday) => Ok(Self::from_iso_week_date(
|
||||
iso_year,
|
||||
iso_week_number.get(),
|
||||
weekday,
|
||||
)?),
|
||||
(year, sunday_week_number, weekday) => Ok(Self::from_ordinal_date(
|
||||
year,
|
||||
(sunday_week_number as i16 * 7 + weekday.number_days_from_sunday() as i16
|
||||
- adjustment(year)
|
||||
+ 1) as u16,
|
||||
)?),
|
||||
(year, monday_week_number, weekday) => Ok(Self::from_ordinal_date(
|
||||
year,
|
||||
(monday_week_number as i16 * 7 + weekday.number_days_from_monday() as i16
|
||||
- adjustment(year)
|
||||
+ 1) as u16,
|
||||
)?),
|
||||
_ => Err(InsufficientInformation),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Parsed> for Time {
|
||||
type Error = error::TryFromParsed;
|
||||
|
||||
fn try_from(parsed: Parsed) -> Result<Self, Self::Error> {
|
||||
let hour = match (parsed.hour_24(), parsed.hour_12(), parsed.hour_12_is_pm()) {
|
||||
(Some(hour), _, _) => hour,
|
||||
(_, Some(hour), Some(false)) if hour.get() == 12 => 0,
|
||||
(_, Some(hour), Some(true)) if hour.get() == 12 => 12,
|
||||
(_, Some(hour), Some(false)) => hour.get(),
|
||||
(_, Some(hour), Some(true)) => hour.get() + 12,
|
||||
_ => return Err(InsufficientInformation),
|
||||
};
|
||||
if parsed.hour_24().is_none()
|
||||
&& parsed.hour_12().is_some()
|
||||
&& parsed.hour_12_is_pm().is_some()
|
||||
&& parsed.minute().is_none()
|
||||
&& parsed.second().is_none()
|
||||
&& parsed.subsecond().is_none()
|
||||
{
|
||||
return Ok(Self::from_hms_nano(hour, 0, 0, 0)?);
|
||||
}
|
||||
let minute = parsed.minute().ok_or(InsufficientInformation)?;
|
||||
let second = parsed.second().unwrap_or(0);
|
||||
let subsecond = parsed.subsecond().unwrap_or(0);
|
||||
Ok(Self::from_hms_nano(hour, minute, second, subsecond)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Parsed> for UtcOffset {
|
||||
type Error = error::TryFromParsed;
|
||||
|
||||
fn try_from(parsed: Parsed) -> Result<Self, Self::Error> {
|
||||
let hour = parsed.offset_hour().ok_or(InsufficientInformation)?;
|
||||
let minute = parsed.offset_minute_signed().unwrap_or(0);
|
||||
let second = parsed.offset_second_signed().unwrap_or(0);
|
||||
|
||||
Self::from_hms(hour, minute, second).map_err(|mut err| {
|
||||
// Provide the user a more accurate error.
|
||||
if err.name == "hours" {
|
||||
err.name = "offset hour";
|
||||
} else if err.name == "minutes" {
|
||||
err.name = "offset minute";
|
||||
} else if err.name == "seconds" {
|
||||
err.name = "offset second";
|
||||
}
|
||||
err.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Parsed> for PrimitiveDateTime {
|
||||
type Error = error::TryFromParsed;
|
||||
|
||||
fn try_from(parsed: Parsed) -> Result<Self, Self::Error> {
|
||||
Ok(Self::new(parsed.try_into()?, parsed.try_into()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Parsed> for OffsetDateTime {
|
||||
type Error = error::TryFromParsed;
|
||||
|
||||
#[allow(clippy::unwrap_in_result)] // We know the values are valid.
|
||||
fn try_from(mut parsed: Parsed) -> Result<Self, Self::Error> {
|
||||
// Some well-known formats explicitly allow leap seconds. We don't currently support them,
|
||||
// so treat it as the nearest preceding moment that can be represented. Because leap seconds
|
||||
// always fall at the end of a month UTC, reject any that are at other times.
|
||||
let leap_second_input = if parsed.leap_second_allowed() && parsed.second() == Some(60) {
|
||||
parsed.set_second(59).expect("59 is a valid second");
|
||||
parsed
|
||||
.set_subsecond(999_999_999)
|
||||
.expect("999_999_999 is a valid subsecond");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
let dt = PrimitiveDateTime::try_from(parsed)?.assume_offset(parsed.try_into()?);
|
||||
if leap_second_input && !dt.is_valid_leap_second_stand_in() {
|
||||
return Err(error::TryFromParsed::ComponentRange(
|
||||
error::ComponentRange {
|
||||
name: "second",
|
||||
minimum: 0,
|
||||
maximum: 59,
|
||||
value: 60,
|
||||
conditional_range: true,
|
||||
},
|
||||
));
|
||||
}
|
||||
Ok(dt)
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче