diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 02deb5a95d..1b9dcdd593 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,2 @@ github: jhpratt -patreon: jhpratt custom: ["paypal.me/jhpratt"] diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 643283a3b6..e6124367a3 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,6 +6,25 @@ concurrency: env: CARGO_INCREMENTAL: 0 + TYPE_CHECK_TARGETS: '{ + "no_std": [ + "thumbv7em-none-eabihf" + ], + "std_no_offset": [ + "x86_64-unknown-netbsd", + "x86_64-unknown-illumos", + "wasm32-wasi" + ], + "std_with_offset": [ + "x86_64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-pc-windows-gnu" + ] + }' + +defaults: + run: + shell: bash on: push: @@ -26,142 +45,112 @@ on: - logo.svg jobs: - check-other-targets: - name: Type checking (${{ matrix.target.name }}, ${{ matrix.rust.name }}) + check-targets: + name: Type checking (${{ matrix.rust.name }}, ${{ matrix.kind.name }}) runs-on: ubuntu-20.04 - if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' }} + if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' strategy: matrix: rust: - - { version: "1.59", name: MSRV } + - { version: "1.59.0", name: MSRV } - { version: stable, name: stable } - target: - - name: NetBSD - triple: x86_64-unknown-netbsd - has_std: true - has_local_offset: false - - name: Illumos - triple: x86_64-unknown-illumos - has_std: true - has_local_offset: false - - name: wasi - triple: wasm32-wasi - has_std: true - has_local_offset: false - - name: embedded - triple: thumbv7em-none-eabihf - has_std: false - has_local_offset: false + kind: + - name: no_std + query: .no_std + .std_no_offset + .std_with_offset + exclude-features: + - std + - formatting + - serde-human-readable + - serde-well-known + - local-offset + - quickcheck + group-features: [] + - name: std_no_offset + query: .std_no_offset + .std_with_offset + exclude-features: [local-offset] + enable-features: [std] + group-features: + - [formatting, parsing] + - [serde-human-readable, serde-well-known] + - name: std_with_offset + query: .std_with_offset + enable-features: [std, local-offset] + group-features: + - [formatting, parsing] + - [serde-human-readable, serde-well-known] steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: Generate target list + run: jq -r 'env.TYPE_CHECK_TARGETS | ${{ matrix.kind.query }} | join(",") | "TARGETS=" + .' >> $GITHUB_ENV - name: Install toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal toolchain: ${{ matrix.rust.version }} - target: ${{ matrix.target.triple }} - override: true + targets: ${{ env.TARGETS }} - name: Install cargo-hack shell: bash run: | - curl -LsSf https://github.com/taiki-e/cargo-hack/releases/latest/download/cargo-hack-x86_64-unknown-linux-gnu.tar.gz | tar xzf - -C ~/.cargo/bin + curl -LsSf https://github.com/taiki-e/cargo-hack/releases/latest/download/cargo-hack-x86_64-unknown-linux-gnu.tar.gz \ + | tar xzf - -C ~/.cargo/bin - name: Cache cargo output - uses: Swatinem/rust-cache@v1 + uses: Swatinem/rust-cache@v2 with: - key: ${{ matrix.target.triple }} + key: ${{ matrix.kind.name }} - name: Check feature powerset - uses: actions-rs/cargo@v1 - with: - command: hack - args: | - check - --no-dev-deps - --feature-powerset - --optional-deps - --group-features serde,rand - --exclude-features default,std,formatting,serde-human-readable,serde-well-known,local-offset,quickcheck,quickcheck-dep,time-macros,itoa,js-sys,wasm-bindgen - --features macros - --exclude-all-features - --target ${{ matrix.target.triple }} - if: matrix.target.has_std == false - - # Unconditionally enable the local-offset flag when the target doesn't provide any useful - # information. - # This currently _does not_ include NetBSD or Solaris due to a soundness bug. - - name: Check feature powerset - uses: actions-rs/cargo@v1 - with: - command: hack - args: | - check - --no-dev-deps - --feature-powerset - --optional-deps - --group-features serde,rand - --group-features formatting,parsing - --group-features serde-human-readable,serde-well-known - --exclude-features default,quickcheck-dep,time-macros,itoa,js-sys,wasm-bindgen - --features macros,local-offset - --target ${{ matrix.target.triple }} - if: matrix.target.has_std == true && matrix.target.has_local_offset == false - - - name: Check feature powerset - uses: actions-rs/cargo@v1 - with: - command: hack - args: | - check - --no-dev-deps - --feature-powerset - --optional-deps - --group-features serde,rand - --group-features formatting,parsing - --group-features serde-human-readable,serde-well-known - --exclude-features default,quickcheck-dep,time-macros,itoa,js-sys,wasm-bindgen - --features macros - --target ${{ matrix.target.triple }} - if: matrix.target.has_std == true && matrix.target.has_local_offset == true + env: + GROUP_FEATURES: ${{ toJSON(matrix.kind.group-features) }} + run: | + jq -r 'env.GROUP_FEATURES | [.[] | join(",")] | map("--group-features " + .) | join(" ")' \ + | xargs -d" " \ + | ( \ + jq -r 'env.TYPE_CHECK_TARGETS | ${{ matrix.kind.query }} | map("--target " + .) | join(" ")' \ + | xargs -d" " \ + cargo hack check \ + --no-dev-deps \ + --feature-powerset \ + --optional-deps \ + --group-features serde,rand \ + --exclude-features default,quickcheck-dep,time-macros,itoa,js-sys,wasm-bindgen,${{ + join(matrix.kind.exclude-features) + }} \ + --features macros,${{ join(matrix.kind.enable-features) }} \ + --exclude-all-features \ + ) check-benchmarks: name: Type-check benchmarks runs-on: ubuntu-20.04 - if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' }} + if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + uses: dtolnay/rust-toolchain@stable - name: Cache cargo output - uses: Swatinem/rust-cache@v1 + uses: Swatinem/rust-cache@v2 - name: Type-check benchmarks - uses: actions-rs/cargo@v1 - with: - command: check - args: --benches --all-features + run: cargo check --benches --all-features env: - RUSTFLAGS: "--cfg bench" + RUSTFLAGS: --cfg bench test: name: Test (${{ matrix.os.name }}, ${{ matrix.rust.name }}) runs-on: ${{ matrix.os.value }} - if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' }} + if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' strategy: matrix: rust: - - { version: "1.59", name: MSRV } + - { version: "1.59.0", name: MSRV } - { version: stable, name: stable } os: - { name: Ubuntu, value: ubuntu-20.04 } @@ -170,17 +159,14 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal toolchain: ${{ matrix.rust.version }} - override: true - name: Install cargo-hack - shell: bash run: | host=$(rustc -Vv | grep host | sed 's/host: //') if [[ $host =~ windows ]]; then @@ -194,143 +180,120 @@ jobs: fi - name: Cache cargo output - uses: Swatinem/rust-cache@v1 - - - name: Check feature powerset - uses: actions-rs/cargo@v1 - with: - command: hack - args: | - check - --feature-powerset - --optional-deps - --group-features serde,rand - --group-features formatting,parsing - --group-features serde-human-readable,serde-well-known - --exclude-features default,quickcheck-dep,time-macros,itoa,js-sys,wasm-bindgen - --features macros - if: matrix.os.has_local_offset == true + uses: Swatinem/rust-cache@v2 - name: Test - uses: actions-rs/cargo@v1 - with: - command: test - args: --all-features + run: cargo test --all-features cross-build: name: Cross-build runs-on: ubuntu-20.04 - if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' }} + if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - profile: minimal - toolchain: stable - override: true - target: x86_64-pc-windows-gnu + targets: x86_64-pc-windows-gnu - name: Cache cargo output - uses: Swatinem/rust-cache@v1 + uses: Swatinem/rust-cache@v2 - name: Install dependencies run: sudo apt install gcc-mingw-w64 + # We're testing the linking, so running `cargo check` is insufficient. - name: Cross-build tests - uses: actions-rs/cargo@v1 - with: - # We're testing the linking, so running `cargo check` is insufficient. - command: build - args: --tests --all-features --target x86_64-pc-windows-gnu + run: cargo build --tests --all-features --target x86_64-pc-windows-gnu fmt: name: Formatting runs-on: ubuntu-20.04 - if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' }} + if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@nightly with: - profile: minimal - toolchain: nightly - override: true components: rustfmt - name: Check formatting - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + run: cargo fmt --all -- --check env: - RUSTFLAGS: "--cfg bench" + RUSTFLAGS: --cfg bench clippy: name: Clippy runs-on: ubuntu-20.04 - if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' }} + if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true + uses: dtolnay/rust-toolchain@stable + + - name: Install targets + run: | + rustup target add \ + x86_64-unknown-linux-gnu \ + aarch64-apple-darwin \ + x86_64-pc-windows-gnu \ + x86_64-unknown-netbsd \ + x86_64-unknown-illumos \ + wasm32-wasi - name: Cache cargo output - uses: Swatinem/rust-cache@v1 + uses: Swatinem/rust-cache@v2 - name: Run clippy - uses: actions-rs/clippy-check@v1 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features --benches --tests + run: | + cargo clippy \ + --all-features \ + --benches \ + --tests \ + --target x86_64-unknown-linux-gnu \ + --target aarch64-apple-darwin \ + --target x86_64-pc-windows-gnu \ + --target x86_64-unknown-netbsd \ + --target x86_64-unknown-illumos \ + --target wasm32-wasi env: - RUSTFLAGS: "--cfg bench" + RUSTFLAGS: --cfg bench documentation: name: Documentation runs-on: ubuntu-20.04 - if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' }} + if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false - name: Install toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true + uses: dtolnay/rust-toolchain@nightly - name: Cache cargo output - uses: Swatinem/rust-cache@v1 + uses: Swatinem/rust-cache@v2 - name: Document public API - uses: actions-rs/cargo@v1 - with: - command: doc - args: --all-features --no-deps -Zrustdoc-map + run: cargo doc --all-features --no-deps -Zrustdoc-map env: RUSTDOCFLAGS: --cfg __time_03_docs - name: Create top-level redirect run: | - echo "" > ./target/doc/index.html + echo "" \ + > ./target/doc/index.html - name: Publish public docs uses: JamesIves/github-pages-deploy-action@releases/v4 @@ -343,16 +306,14 @@ jobs: if: github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.master_branch) - name: Document internal API - uses: actions-rs/cargo@v1 - with: - command: doc - args: --all-features --no-deps -Zrustdoc-map --document-private-items + run: cargo doc --all-features --no-deps -Zrustdoc-map --document-private-items env: RUSTDOCFLAGS: --cfg __time_03_docs --document-hidden-items - name: Create top-level redirect run: | - echo "" > ./target/doc/index.html + echo "" \ + > ./target/doc/index.html - name: Publish internal docs uses: JamesIves/github-pages-deploy-action@releases/v4 @@ -367,31 +328,30 @@ jobs: coverage: name: Coverage runs-on: ubuntu-20.04 - if: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' }} + if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork) || github.event_name == 'push' steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: persist-credentials: false - name: Install toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true + uses: dtolnay/rust-toolchain@nightly - name: Install cargo-llvm-cov run: | - curl -LsSf https://github.com/taiki-e/cargo-llvm-cov/releases/latest/download/cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz | tar xzf - -C ~/.cargo/bin + curl -LsSf https://github.com/taiki-e/cargo-llvm-cov/releases/latest/download/cargo-llvm-cov-x86_64-unknown-linux-gnu.tar.gz \ + | tar xzf - -C ~/.cargo/bin - name: Generate coverage report run: | cargo llvm-cov clean --workspace cargo llvm-cov test --no-report --all-features -- --test-threads=1 - RUSTFLAGS="--cfg __ui_tests" cargo llvm-cov test --no-report --tests --all-features -- compile_fail + cargo llvm-cov test --no-report --tests --all-features -- compile_fail cargo llvm-cov report --lcov > lcov.txt + env: + RUSTFLAGS: --cfg __ui_tests - name: Upload coverage report uses: codecov/codecov-action@v1 diff --git a/.github/workflows/powerset.yaml b/.github/workflows/powerset.yaml index c357d50ec0..13de3bf727 100644 --- a/.github/workflows/powerset.yaml +++ b/.github/workflows/powerset.yaml @@ -2,11 +2,39 @@ name: "Check powerset" env: RUSTFLAGS: -Dwarnings + TARGETS: '{ + "no_std": [ + "thumbv7em-none-eabihf" + ], + "std_no_offset": [ + "aarch64-fuchsia", + "x86_64-fuchsia" + ], + "std_with_offset": [ + "aarch64-unknown-linux-gnu", + "i686-pc-windows-gnu", + "i686-pc-windows-msvc", + "i686-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-pc-windows-gnu", + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", + "aarch64-linux-android", + "wasm32-wasi", + "x86_64-linux-android", + "x86_64-unknown-netbsd", + "x86_64-unknown-illumos" + ] + }' concurrency: group: powerset-${{ github.head_ref }} cancel-in-progress: true +defaults: + run: + shell: bash + on: schedule: - cron: "0 0 * * 1" # midnight on Monday @@ -19,120 +47,71 @@ on: workflow_dispatch: jobs: - check-powerset: - name: Type checking (${{ matrix.target.name }}) + check: + name: Type checking (${{ matrix.kind.name }}, ${{ matrix.rust.name }}) runs-on: ubuntu-latest strategy: - fail-fast: true matrix: - target: - # All tier 1 platforms as of 2022-01-26 - - name: ARM64 Linux - triple: aarch64-unknown-linux-gnu - has_std: true - - name: 32-bit MinGW - triple: i686-pc-windows-gnu - has_std: true - - name: 32-bit MSVC - triple: i686-pc-windows-msvc - has_std: true - - name: 32-bit Linux - triple: i686-unknown-linux-gnu - has_std: true - - name: 64-bit macOS - triple: x86_64-apple-darwin - has_std: true - - name: 64-bit MinGW - triple: x86_64-pc-windows-gnu - has_std: true - - name: 64-bit MSVC - triple: x86_64-pc-windows-msvc - has_std: true - - name: 64-bit Linux - triple: x86_64-unknown-linux-gnu - has_std: true - # Select tier 2 platforms as of 2022-01-26 - - name: ARM64 Fuchsia - triple: aarch64-fuchsia - has_std: true - - name: ARM64 Android - triple: aarch64-linux-android - has_std: true - - name: Bare Cortex - triple: thumbv7em-none-eabihf - has_std: false - - name: WASI - triple: wasm32-wasi - has_std: true - - name: 64-bit Fuchsia - triple: x86_64-fuchsia - has_std: true - - name: 64-bit x86 Android - triple: x86_64-linux-android - has_std: true, - - name: NetBSD - triple: x86_64-unknown-netbsd - has_std: true - - name: Illumos - triple: x86_64-unknown-illumos - has_std: true + rust: + - { version: "1.59.0", name: MSRV } + - { version: stable, name: stable } + kind: + - name: no_std + query: .no_std + .std_no_offset + .std_with_offset + exclude_features: + - std + - local-offset + - quickcheck + - formatting + - serde-human-readable + - serde-well-known + - name: std_no_offset + query: .std_no_offset + .std_with_offset + exclude_features: [local-offset] + enable_features: [std] + - name: std_with_offset + query: .std_with_offset + enable_features: [std, local-offset] steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: Generate target list + run: jq -r 'env.TARGETS | ${{ matrix.kind.query }} | join(",") | "TARGET_LIST=" + .' >> $GITHUB_ENV - name: Install toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@master with: - profile: minimal - toolchain: stable - target: ${{ matrix.target.triple }} - override: true + targets: ${{ env.TARGET_LIST }} + toolchain: ${{ matrix.rust.version}} - name: Install cargo-hack shell: bash run: | - curl -LsSf https://github.com/taiki-e/cargo-hack/releases/latest/download/cargo-hack-x86_64-unknown-linux-gnu.tar.gz | tar xzf - -C ~/.cargo/bin + curl -LsSf https://github.com/taiki-e/cargo-hack/releases/latest/download/cargo-hack-x86_64-unknown-linux-gnu.tar.gz \ + | tar xzf - -C ~/.cargo/bin - name: Check feature powerset - uses: actions-rs/cargo@v1 - with: - command: hack - args: | - check - --no-dev-deps - --version-range 1.59.. - --clean-per-version - --feature-powerset - --optional-deps - --exclude-features default,std,local-offset,quickcheck,quickcheck-dep,time-macros,formatting,itoa,serde-human-readable,serde-well-known,js-sys,wasm-bindgen - --exclude-all-features - --target ${{ matrix.target.triple }} - if: matrix.target.has_std == false - - - name: Check feature powerset - uses: actions-rs/cargo@v1 - with: - command: hack - args: | - check - --no-dev-deps - --version-range 1.59.. - --clean-per-version - --feature-powerset - --optional-deps - --exclude-features default,quickcheck-dep,time-macros,itoa,js-sys,wasm-bindgen - --target ${{ matrix.target.triple }} - if: matrix.target.has_std == true + run: | + | jq -r 'env.TARGETS | ${{ matrix.kind.query }} | map("--target " + .) | join(" ")' \ + | xargs -d" " \ + cargo hack check \ + --no-dev-deps \ + --feature-powerset \ + --optional-deps \ + --exclude-features default,quickcheck-dep,time-macros,itoa,js-sys,wasm-bindgen,${{ + join(matrix.kind.exclude_features) + }} ${{ matrix.kind.enable_features && format('--features {0}', join(matrix.kind.enable_features)) }} release: name: Create release if: startsWith(github.ref, 'refs/tags') && github.run_attempt == 1 - needs: check-powerset + needs: check runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Create release uses: actions/create-release@v1 @@ -141,7 +120,7 @@ jobs: with: tag_name: ${{ github.ref }} release_name: ${{ github.ref }} - body: "See the [changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md) for details." + body: See the [changelog](https://github.com/time-rs/time/blob/main/CHANGELOG.md) for details. draft: false prerelease: | ${{ diff --git a/.github/workflows/scheduled.yaml b/.github/workflows/scheduled.yaml index eb1fba4b55..e959e88221 100644 --- a/.github/workflows/scheduled.yaml +++ b/.github/workflows/scheduled.yaml @@ -1,31 +1,33 @@ -name: "Scheduled tasks" +name: Scheduled tasks on: schedule: - cron: "0 0 * * 1,5" # midnight on Monday, Friday + workflow_dispatch: + +permissions: + pull-requests: write jobs: stale: name: Close stale PRs runs-on: ubuntu-20.04 steps: - - uses: actions/stale@v2 + - uses: actions/stale@v6 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-pr-message: "This pull request has not had any activity recently. It will be closed without further activity." - - days-before-stale: 14 + enable-statistics: false + stale-pr-message: This pull request has not had any activity recently. It will be closed without further activity. + days-before-stale: 30 days-before-close: 7 - - stale-pr-label: "C-stale" - exempt-pr-labels: "C-keep-open" + stale-pr-label: C-stale + exempt-pr-labels: C-keep-open security-audit: name: Security audit runs-on: ubuntu-20.04 steps: - name: Checkout sources - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Audit dependencies uses: actions-rs/audit-check@v1 diff --git a/src/duration.rs b/src/duration.rs index 59f55722a1..f8d916f451 100644 --- a/src/duration.rs +++ b/src/duration.rs @@ -227,13 +227,18 @@ impl Duration { /// assert_eq!(Duration::new(1, 2_000_000_000), 3.seconds()); /// ``` pub const fn new(mut seconds: i64, mut nanoseconds: i32) -> Self { - seconds += nanoseconds as i64 / 1_000_000_000; + seconds = expect_opt!( + seconds.checked_add(nanoseconds as i64 / 1_000_000_000), + "overflow constructing `time::Duration`" + ); nanoseconds %= 1_000_000_000; if seconds > 0 && nanoseconds < 0 { + // `seconds` cannot overflow here because it is positive. seconds -= 1; nanoseconds += 1_000_000_000; } else if seconds < 0 && nanoseconds > 0 { + // `seconds` cannot overflow here because it is negative. seconds += 1; nanoseconds -= 1_000_000_000; } @@ -249,7 +254,10 @@ impl Duration { /// assert_eq!(Duration::weeks(1), 604_800.seconds()); /// ``` pub const fn weeks(weeks: i64) -> Self { - Self::seconds(weeks * 604_800) + Self::seconds(expect_opt!( + weeks.checked_mul(604_800), + "overflow constructing `time::Duration`" + )) } /// Create a new `Duration` with the given number of days. Equivalent to @@ -260,7 +268,10 @@ impl Duration { /// assert_eq!(Duration::days(1), 86_400.seconds()); /// ``` pub const fn days(days: i64) -> Self { - Self::seconds(days * 86_400) + Self::seconds(expect_opt!( + days.checked_mul(86_400), + "overflow constructing `time::Duration`" + )) } /// Create a new `Duration` with the given number of hours. Equivalent to @@ -271,7 +282,10 @@ impl Duration { /// assert_eq!(Duration::hours(1), 3_600.seconds()); /// ``` pub const fn hours(hours: i64) -> Self { - Self::seconds(hours * 3_600) + Self::seconds(expect_opt!( + hours.checked_mul(3_600), + "overflow constructing `time::Duration`" + )) } /// Create a new `Duration` with the given number of minutes. Equivalent to @@ -282,7 +296,10 @@ impl Duration { /// assert_eq!(Duration::minutes(1), 60.seconds()); /// ``` pub const fn minutes(minutes: i64) -> Self { - Self::seconds(minutes * 60) + Self::seconds(expect_opt!( + minutes.checked_mul(60), + "overflow constructing `time::Duration`" + )) } /// Create a new `Duration` with the given number of seconds. @@ -303,6 +320,12 @@ impl Duration { /// assert_eq!(Duration::seconds_f64(-0.5), -0.5.seconds()); /// ``` pub fn seconds_f64(seconds: f64) -> Self { + if seconds > i64::MAX as f64 || seconds < i64::MIN as f64 { + crate::expect_failed("overflow constructing `time::Duration`"); + } + if seconds.is_nan() { + crate::expect_failed("passed NaN to `time::Duration::seconds_f64`"); + } Self::new_unchecked(seconds as _, ((seconds % 1.) * 1_000_000_000.) as _) } @@ -314,6 +337,12 @@ impl Duration { /// assert_eq!(Duration::seconds_f32(-0.5), (-0.5).seconds()); /// ``` pub fn seconds_f32(seconds: f32) -> Self { + if seconds > i64::MAX as f32 || seconds < i64::MIN as f32 { + crate::expect_failed("overflow constructing `time::Duration`"); + } + if seconds.is_nan() { + crate::expect_failed("passed NaN to `time::Duration::seconds_f32`"); + } Self::new_unchecked(seconds as _, ((seconds % 1.) * 1_000_000_000.) as _) } @@ -364,10 +393,14 @@ impl Duration { /// As the input range cannot be fully mapped to the output, this should only be used where it's /// known to result in a valid value. pub(crate) const fn nanoseconds_i128(nanoseconds: i128) -> Self { - Self::new_unchecked( - (nanoseconds / 1_000_000_000) as _, - (nanoseconds % 1_000_000_000) as _, - ) + let seconds = nanoseconds / 1_000_000_000; + let nanoseconds = nanoseconds % 1_000_000_000; + + if seconds > i64::MAX as i128 || seconds < i64::MIN as i128 { + crate::expect_failed("overflow constructing `time::Duration`"); + } + + Self::new_unchecked(seconds as _, nanoseconds as _) } // endregion constructors diff --git a/src/lib.rs b/src/lib.rs index 0583b16686..b13a7df2be 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -283,6 +283,18 @@ macro_rules! const_try_opt { } }; } + +/// Try to unwrap an expression, panicking if not possible. +/// +/// This is similar to `$e.expect($message)`, but is usable in `const` contexts. +macro_rules! expect_opt { + ($e:expr, $message:literal) => { + match $e { + Some(value) => value, + None => crate::expect_failed($message), + } + }; +} // endregion macros mod date; @@ -334,3 +346,11 @@ pub use crate::weekday::Weekday; /// An alias for [`std::result::Result`] with a generic error from the time crate. pub type Result = core::result::Result; + +/// This is a separate function to reduce the code size of `expect_opt!`. +#[inline(never)] +#[cold] +#[track_caller] +const fn expect_failed(message: &str) -> ! { + panic!("{}", message) +} diff --git a/tests/integration/duration.rs b/tests/integration/duration.rs index 66a2f3e174..b11ccb2c43 100644 --- a/tests/integration/duration.rs +++ b/tests/integration/duration.rs @@ -82,6 +82,9 @@ fn new() { assert_eq!(Duration::new(1, -1_400_000_000), (-400).milliseconds()); assert_eq!(Duration::new(2, -1_400_000_000), 600.milliseconds()); assert_eq!(Duration::new(3, -1_400_000_000), 1_600.milliseconds()); + + assert_panic!(Duration::new(i64::MAX, 1_000_000_000)); + assert_panic!(Duration::new(i64::MIN, -1_000_000_000)); } #[test] @@ -90,6 +93,9 @@ fn weeks() { assert_eq!(Duration::weeks(2), (2 * 604_800).seconds()); assert_eq!(Duration::weeks(-1), (-604_800).seconds()); assert_eq!(Duration::weeks(-2), (2 * -604_800).seconds()); + + assert_panic!(Duration::weeks(i64::MAX)); + assert_panic!(Duration::weeks(i64::MIN)); } #[test] @@ -106,6 +112,9 @@ fn days() { assert_eq!(Duration::days(2), (2 * 86_400).seconds()); assert_eq!(Duration::days(-1), (-86_400).seconds()); assert_eq!(Duration::days(-2), (2 * -86_400).seconds()); + + assert_panic!(Duration::days(i64::MAX)); + assert_panic!(Duration::days(i64::MIN)); } #[test] @@ -122,6 +131,9 @@ fn hours() { assert_eq!(Duration::hours(2), (2 * 3_600).seconds()); assert_eq!(Duration::hours(-1), (-3_600).seconds()); assert_eq!(Duration::hours(-2), (2 * -3_600).seconds()); + + assert_panic!(Duration::hours(i64::MAX)); + assert_panic!(Duration::hours(i64::MIN)); } #[test] @@ -138,6 +150,9 @@ fn minutes() { assert_eq!(Duration::minutes(2), (2 * 60).seconds()); assert_eq!(Duration::minutes(-1), (-60).seconds()); assert_eq!(Duration::minutes(-2), (2 * -60).seconds()); + + assert_panic!(Duration::minutes(i64::MAX)); + assert_panic!(Duration::minutes(i64::MIN)); } #[test] @@ -168,6 +183,10 @@ fn whole_seconds() { fn seconds_f64() { assert_eq!(Duration::seconds_f64(0.5), 0.5.seconds()); assert_eq!(Duration::seconds_f64(-0.5), (-0.5).seconds()); + + assert_panic!(Duration::seconds_f64(f64::MAX)); + assert_panic!(Duration::seconds_f64(f64::MIN)); + assert_panic!(Duration::seconds_f64(f64::NAN)); } #[test] @@ -185,6 +204,10 @@ fn as_seconds_f64() { fn seconds_f32() { assert_eq!(Duration::seconds_f32(0.5), 0.5.seconds()); assert_eq!(Duration::seconds_f32(-0.5), (-0.5).seconds()); + + assert_panic!(Duration::seconds_f32(f32::MAX)); + assert_panic!(Duration::seconds_f32(f32::MIN)); + assert_panic!(Duration::seconds_f32(f32::NAN)); } #[test] @@ -600,6 +623,9 @@ fn std_sub_assign_overflow() { fn mul_int() { assert_eq!(1.seconds() * 2, 2.seconds()); assert_eq!(1.seconds() * -2, (-2).seconds()); + + assert_panic!(Duration::MAX * 2); + assert_panic!(Duration::MIN * 2); } #[test] diff --git a/tests/integration/main.rs b/tests/integration/main.rs index c30534788e..7a0dcd2e18 100644 --- a/tests/integration/main.rs +++ b/tests/integration/main.rs @@ -71,6 +71,16 @@ macro_rules! modifier { (@value $field:ident $value:expr) => ($value); } +/// Assert that the given expression panics. +macro_rules! assert_panic { + ($($x:tt)*) => { + assert!(std::panic::catch_unwind(|| { + $($x)* + }) + .is_err()) + } +} + mod date; mod derives; mod duration; diff --git a/tests/integration/offset_date_time.rs b/tests/integration/offset_date_time.rs index 73baa9459f..c6af8933e5 100644 --- a/tests/integration/offset_date_time.rs +++ b/tests/integration/offset_date_time.rs @@ -51,14 +51,8 @@ fn to_offset() { #[test] fn to_offset_panic() { - assert!( - std::panic::catch_unwind(|| { PrimitiveDateTime::MAX.assume_utc().to_offset(offset!(+1)) }) - .is_err() - ); - assert!( - std::panic::catch_unwind(|| { PrimitiveDateTime::MIN.assume_utc().to_offset(offset!(-1)) }) - .is_err() - ); + assert_panic!(PrimitiveDateTime::MAX.assume_utc().to_offset(offset!(+1))); + assert_panic!(PrimitiveDateTime::MIN.assume_utc().to_offset(offset!(-1))); } #[test]