diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..1c9bc12 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [duesee] diff --git a/.github/actions/cache_restore/action.yml b/.github/actions/cache_restore/action.yml new file mode 100644 index 0000000..6534e60 --- /dev/null +++ b/.github/actions/cache_restore/action.yml @@ -0,0 +1,21 @@ +name: cache_restore +runs: + using: composite + steps: + - uses: actions/cache/restore@v4 + with: + path: | + # See https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci + ~/.cargo/.crates.toml + ~/.cargo/.crates2.json + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + # See https://doc.rust-lang.org/cargo/guide/build-cache.html + target + key: ${{ runner.os }}|${{ github.job }}|${{ github.run_attempt }} + restore-keys: | + ${{ runner.os }}|${{ github.job }} + ${{ runner.os }} + diff --git a/.github/actions/cache_save/action.yml b/.github/actions/cache_save/action.yml new file mode 100644 index 0000000..3846bf2 --- /dev/null +++ b/.github/actions/cache_save/action.yml @@ -0,0 +1,18 @@ +name: cache_save +runs: + using: composite + steps: + - uses: actions/cache/save@v4 + with: + path: | + # See https://doc.rust-lang.org/cargo/guide/cargo-home.html#caching-the-cargo-home-in-ci + ~/.cargo/.crates.toml + ~/.cargo/.crates2.json + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + # See https://doc.rust-lang.org/cargo/guide/build-cache.html + target + key: ${{ runner.os }}|${{ github.job }}|${{ github.run_attempt }} + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..3c0f0f2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + groups: + dependencies: + patterns: + - "*" diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 0000000..0765e83 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,26 @@ +name: audit +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + # 21:43 on Wednesday and Sunday. (Thanks, crontab.guru) + - cron: '43 21 * * 3,0' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + audit: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just audit + - uses: ./.github/actions/cache_save diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..94825f4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,95 @@ +name: main +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just check + - uses: ./.github/actions/cache_save + + test: + strategy: + matrix: + os: [ ubuntu-latest, macos-latest, windows-latest ] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just test + - uses: ./.github/actions/cache_save + +# benchmark: +# runs-on: ubuntu-latest + +# steps: +# - uses: actions/checkout@v6 + +# - uses: ./.github/actions/cache_restore +# - run: cargo install just +# - run: just bench_against_main +# - uses: ./.github/actions/cache_save + + coverage: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just coverage + - uses: ./.github/actions/cache_save + + - uses: coverallsapp/github-action@5cbfd81b66ca5d10c19b062c04de0199c215fb6e + with: + format: lcov + file: target/coverage/coverage.lcov + + fuzz: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just fuzz + - uses: ./.github/actions/cache_save + + check_msrv: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just check_msrv + - uses: ./.github/actions/cache_save + + check_minimal_dependency_versions: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/cache_restore + - run: cargo install just + - run: just check_minimal_dependency_versions + - uses: ./.github/actions/cache_save + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..788c8f8 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,42 @@ +name: release + +on: + push: + tags: + - 'smtp-codec/v*' + - 'smtp-types/v*' + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Extract crate name from Git tag + run: | + set -euo pipefail + tag_name=${GITHUB_REF#refs/tags/} + crate_name=${tag_name%/v*} + echo "Extracted crate name: $crate_name" + echo "CRATE_NAME=$crate_name" >> "$GITHUB_ENV" + + - uses: actions/checkout@v6 + + - name: Assert release version matches crate version + run: | + set -euo pipefail + + # Get release version from Git tag + tag_version=${GITHUB_REF#refs/tags/$CRATE_NAME/v} + + # Get crate version from Cargo.toml + cd $CRATE_NAME + crate_version=$(cargo read-manifest | jq -r .version) + + if [ "$tag_version" != "$crate_version" ]; then + echo "Error: Release version in Git tag (${tag_version}) does not match crate version in Cargo.toml (${crate_version}) for crate $CRATE_NAME." + exit 1 + fi + + - name: Publish crate to crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish -p $CRATE_NAME diff --git a/.gitignore b/.gitignore index 80cb495..9cfdee9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ -Cargo.lock -/target +target .idea + +# direnv (https://direnv.net/) +.envrc +.direnv + +nohup.out diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3856be8 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,30 @@ +# Welcome to smtp-codec's (and smtp-types') contributing guide + +Thanks for investing your time to help with this project! Keep in mind that this project is driven by volunteers. Be patient and polite, and empower others to improve. Always use your best judgment and be excellent to each other. + +## Principles + +### Misuse resistance + +We use strong-typing to eliminate invalid state. +Ask yourself: Can I instantiate a type with an invalid variable setting? +If yes, consider how to eliminate it. +If you're unsure, let's figure it out together! + +## Project management + +We use the [just](https://github.com/casey/just) command runner for Continuous Integration (CI). +The GitHub Actions infrastructure merely calls `just` to execute jobs. +This means that you can run all required tests for a PR using `just ci`. + +### Code formatting + +Please ensure that all code is formatted using `cargo +nightly fmt`. + +### Testing + +Run tests with `cargo test --all-features`. + +## License + +By contributing to this project, you agree to license your contributions under the same license as the project (MIT OR Apache-2.0). diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7850e53 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,115 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "abnf-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" +dependencies = [ + "nom", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smtp-codec" +version = "0.2.0" +dependencies = [ + "abnf-core", + "nom", + "smtp-types", +] + +[[package]] +name = "smtp-types" +version = "0.2.0" +dependencies = [ + "serde", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" diff --git a/Cargo.toml b/Cargo.toml index c4872be..66ca435 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,10 +1,9 @@ [workspace] resolver = "2" members = [ - "smtp-types", "smtp-codec", + "smtp-types", ] -[patch.crates-io] -smtp-types = { path = "smtp-types" } -smtp-codec = { path = "smtp-codec" } +[workspace.package] +rust-version = "1.85" diff --git a/deny.toml b/deny.toml index 9a95fcf..52276c2 100644 --- a/deny.toml +++ b/deny.toml @@ -1,9 +1,12 @@ -[bans] -multiple-versions = "deny" - [sources] unknown-registry = "deny" -unknown-git = "deny" +unknown-git = "deny" [licenses] -allow = [ "Apache-2.0", "MIT", "Unicode-DFS-2016" ] +allow = [ + "Apache-2.0", + "MIT", +] + +[licenses.private] +ignore = true diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6f01dd9 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1757023789, + "narHash": "sha256-roMtzAgp0M4ExsIsFScWvWY0t1vjWtJwsqaxFQ2hwk8=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "d689442f4e5a79df371c65b977ffff66d8fed809", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.05-small", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..047a0ee --- /dev/null +++ b/flake.nix @@ -0,0 +1,36 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05-small"; + }; + + outputs = + { self, nixpkgs, ... }: + let + eachSupportedSystem = nixpkgs.lib.genAttrs supportedSystems; + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + mkDevShells = + system: + let + pkgs = import nixpkgs { inherit system; }; + in + { + default = pkgs.mkShell { + strictDeps = true; + nativeBuildInputs = with pkgs; [ + just + rustPlatform.bindgenHook + rustup + ]; + }; + }; + in + { + devShells = eachSupportedSystem mkDevShells; + }; +} diff --git a/justfile b/justfile new file mode 100644 index 0000000..f488757 --- /dev/null +++ b/justfile @@ -0,0 +1,196 @@ +export RUSTFLAGS := "-D warnings" +export RUSTDOCFLAGS := "-D warnings" + +msrv := `sed -rn 's|^rust-version = \"(.*)\"$|\1|p' Cargo.toml` + +[private] +default: + just -l --unsorted + +########### +### RUN ### +########### + +# Run (local) CI +ci: (ci_impl "" "" ) \ + (ci_impl "" " --all-features") \ + (ci_impl " --release" "" ) \ + (ci_impl " --release" " --all-features") + +[private] +ci_impl mode features: (check_impl mode features) (test_impl mode features) + +# Check syntax, formatting, clippy, deny, semver, ... +check: (check_impl "" "" ) \ + (check_impl "" " --all-features") \ + (check_impl " --release" "" ) \ + (check_impl " --release" " --all-features") + +[private] +check_impl mode features: (cargo_check mode features) \ + (cargo_hack mode) \ + cargo_fmt \ + (cargo_clippy mode features) \ + cargo_deny \ + cargo_semver + +[private] +cargo_check mode features: + cargo check --workspace --all-targets{{ mode }}{{ features }} + cargo doc --no-deps --document-private-items --keep-going{{ mode }}{{ features }} + +[private] +cargo_hack mode: install_cargo_hack + cargo hack check --workspace --all-targets{{ mode }} + cargo hack check -p smtp-codec \ + --no-dev-deps \ + --exclude-features default \ + --feature-powerset \ + {{ mode }} + cargo hack check -p smtp-types \ + --no-dev-deps \ + --feature-powerset \ + --group-features \ + serde\ + {{ mode }} + +[private] +cargo_fmt: install_rust_nightly install_rust_nightly_fmt + cargo +nightly fmt --check + +[private] +cargo_clippy features mode: install_cargo_clippy + cargo clippy --workspace --all-targets{{ features }}{{ mode }} + +[private] +cargo_deny: install_cargo_deny + cargo deny check + +[private] +cargo_semver: install_cargo_semver_checks + cargo semver-checks check-release --only-explicit-features --baseline-rev HEAD -p smtp-codec + cargo semver-checks check-release --only-explicit-features --baseline-rev HEAD -p smtp-types + +# Test multiple configurations +test: (test_impl "" "" ) \ + (test_impl "" " --all-features") \ + (test_impl " --release" "" ) \ + (test_impl " --release" " --all-features") + +[private] +test_impl mode features: (cargo_test mode features) + +[private] +cargo_test features mode: + cargo test \ + --workspace \ + --all-targets \ + {{ features }}\ + {{ mode }} + +# Audit advisories, bans, licenses, and sources +audit: cargo_deny + +# Measure test coverage +coverage: install_rust_llvm_tools_preview install_cargo_grcov + rm -rf target/coverage/* + RUSTFLAGS="-Cinstrument-coverage" LLVM_PROFILE_FILE="$PWD/target/coverage/coverage-%m-%p.profraw" CARGO_TARGET_DIR="$PWD/target/coverage" cargo test -p smtp-codec -p smtp-types --all-features + grcov target/coverage \ + --source-dir . \ + --binary-path target/coverage/debug \ + --branch \ + --keep-only '{smtp-codec/src/**,smtp-types/src/**}' \ + --llvm \ + --output-types "html,lcov" \ + --output-path target/coverage/ + mv target/coverage/lcov target/coverage/coverage.lcov + rm target/coverage/*.profraw + rm -rf target/coverage/debug + +# Fuzz all targets +[linux] +fuzz runs="25000": install_cargo_fuzz + #!/usr/bin/env bash + set -euo pipefail + cd smtp-codec + for fuzz_target in $(cargo +nightly fuzz list) + do + echo "# Fuzzing ${fuzz_target}"; + cargo +nightly fuzz run --features=ext ${fuzz_target} -- -dict=fuzz/terminals.dict -max_len=256 -only_ascii=1 -runs={{ runs }}; + done + +# Check MSRV +check_msrv: install_rust_msrv + cargo '+{{ msrv }}' check --locked \ + --workspace \ + --all-targets --all-features + cargo '+{{ msrv }}' test --locked \ + --workspace \ + --all-targets --all-features + +# Check minimal dependency versions +check_minimal_dependency_versions: install_rust_nightly + cargo +nightly update -Z minimal-versions + cargo check \ + --workspace \ + --all-targets --all-features + cargo test \ + --workspace \ + --all-targets --all-features + cargo update + +############### +### INSTALL ### +############### + +# Install required tooling (ahead of time) +install: install_rust_msrv \ + install_rust_nightly \ + install_rust_nightly_fmt \ + install_rust_llvm_tools_preview \ + install_cargo_clippy \ + install_cargo_deny \ + install_cargo_fuzz \ + install_cargo_grcov \ + install_cargo_hack \ + install_cargo_semver_checks + +[private] +install_rust_msrv: + rustup toolchain install '{{ msrv }}' --profile minimal + +[private] +install_rust_nightly: + rustup toolchain install nightly --profile minimal + +[private] +install_rust_nightly_fmt: + rustup component add --toolchain nightly rustfmt + +[private] +install_rust_llvm_tools_preview: + rustup component add llvm-tools-preview + +[private] +install_cargo_clippy: + rustup component add clippy + +[private] +install_cargo_deny: + cargo install --locked cargo-deny + +[private] +install_cargo_grcov: + cargo install grcov + +[private] +install_cargo_hack: + cargo install --locked cargo-hack + +[private] +install_cargo_fuzz: install_rust_nightly + cargo install cargo-fuzz + +[private] +install_cargo_semver_checks: + cargo install --locked cargo-semver-checks diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..3ccec48 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +profile = "default" +components = [ "rust-src", "rust-analyzer" ] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..2cbf0d3 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +format_code_in_doc_comments=true +group_imports="StdExternalCrate" +imports_granularity="Crate" diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..9d07c80 --- /dev/null +++ b/shell.nix @@ -0,0 +1,13 @@ +# Compatiblity file for non-flake Nix users. +# +# +(import + ( + let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in + fetchTarball { + url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { src = ./.; } +).shellNix