diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bbdafc6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: Continuous Integration + +on: + pull_request: + paths-ignore: + - "docs/**" + - "*.md" + - "LICENSE" + push: + branches: + - main + paths-ignore: + - "docs/**" + - "*.md" + - "LICENSE" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always + +jobs: + fast-checks: + name: 🏁 Fast Checks (Format) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v5 + - name: Install rustfmt + run: | + rustup toolchain install + rustup component add rustfmt + - name: Cache taplo + uses: actions/cache@v5 + with: + path: ~/.cargo/bin/taplo + key: taplo-cli-${{ runner.os }} + - name: Install taplo + run: | + if ! command -v taplo >/dev/null 2>&1; then + cargo install taplo-cli --locked + fi + - name: Run format checks + run: | + taplo format --check --config taplo.toml + cargo fmt --all -- --check + + build-and-test-matrix: + name: 🛠️ Build & Test Matrix + needs: fast-checks + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + steps: + - uses: actions/checkout@v5 + - name: Install toolchain + run: rustup toolchain install + - name: Build + run: cargo build --locked + - name: Test + run: cargo test --locked + - name: Compile benches + run: cargo bench --no-run --locked + + analysis: + name: 🤖 Analysis (Clippy & Doc) + needs: fast-checks + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v5 + - name: Install toolchain + run: | + rustup toolchain install + rustup component add clippy + - name: Run clippy + run: cargo clippy --locked --all-targets --all-features -- -D warnings + - name: Generate documentation + run: cargo doc --locked --no-deps --all-features diff --git a/.github/workflows/create-release-proposal.yml b/.github/workflows/create-release-proposal.yml new file mode 100644 index 0000000..7a2a399 --- /dev/null +++ b/.github/workflows/create-release-proposal.yml @@ -0,0 +1,171 @@ +name: Create Release Proposal + +env: + CARGO_TERM_COLOR: always + +on: + workflow_dispatch: + inputs: + target_branch: + description: "Target branch for the PR (default: main)" + required: false + type: string + default: "main" + version_type: + description: "Type of version bump (major, minor, patch) or specify custom version" + required: true + default: "patch" + type: choice + options: + - patch + - minor + - major + - custom + custom_version: + description: 'Custom version string (e.g., v1.6.1). Only used if version_type is "custom". MUST start with "v"' + required: false + is_draft: + description: "Is this a draft release?" + required: true + type: boolean + default: false + +jobs: + calculate-next-version: + name: 🧮 Calculate Next Version + runs-on: ubuntu-latest + outputs: + new_version: ${{ steps.versioner.outputs.new_version }} + commit_sha_short: ${{ steps.vars.outputs.commit_sha_short }} + source_branch: ${{ steps.vars.outputs.source_branch }} + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Get current branch and commit SHA + id: vars + run: | + echo "commit_sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "source_branch=$(git rev-parse --abbrev-ref HEAD)" >> $GITHUB_OUTPUT + + - name: Get latest tag + id: latest_tag + run: | + latest_semver_tag=$(git tag -l "v[0-9]*.[0-9]*.[0-9]*" | sort -V | tail -n 1) + + # Historical tags are unprefixed (1.6.0). Treat those as v1.6.0. + if [ -z "$latest_semver_tag" ]; then + latest_unprefixed=$(git tag -l "[0-9]*.[0-9]*.[0-9]*" | sort -V | tail -n 1) + if [ -n "$latest_unprefixed" ]; then + latest_semver_tag="v$latest_unprefixed" + else + latest_semver_tag="v0.0.0" + fi + fi + + echo "latest_tag_found=$latest_semver_tag" >> $GITHUB_OUTPUT + echo "Latest semantic version tag found: $latest_semver_tag" + + - name: Calculate new version + id: versioner + env: + LATEST_TAG: ${{ steps.latest_tag.outputs.latest_tag_found }} + VERSION_TYPE: ${{ github.event.inputs.version_type }} + CUSTOM_VERSION: ${{ github.event.inputs.custom_version }} + run: | + current_version=${LATEST_TAG#v} + + if [[ "$VERSION_TYPE" == "custom" ]]; then + if [[ -z "$CUSTOM_VERSION" ]]; then + echo "Error: Custom version is selected but no custom_version string provided." + exit 1 + fi + if [[ ! "$CUSTOM_VERSION" =~ ^v ]]; then + echo "Error: Custom version string MUST start with 'v' (e.g., v1.2.3)." + exit 1 + fi + new_version="$CUSTOM_VERSION" + else + IFS='.' read -r major minor patch <<< "$current_version" + + if [[ "$VERSION_TYPE" == "major" ]]; then + major=$((major + 1)) + minor=0 + patch=0 + elif [[ "$VERSION_TYPE" == "minor" ]]; then + minor=$((minor + 1)) + patch=0 + elif [[ "$VERSION_TYPE" == "patch" ]]; then + patch=$((patch + 1)) + else + echo "Error: Invalid version_type: $VERSION_TYPE" + exit 1 + fi + new_version="v$major.$minor.$patch" + fi + + echo "New version: $new_version" + echo "new_version=$new_version" >> $GITHUB_OUTPUT + + update-version: + name: 📝 Update Version + needs: calculate-next-version + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install toolchain + run: rustup toolchain install + + - name: Create version bump branch and PR + env: + NEW_VERSION: ${{ needs.calculate-next-version.outputs.new_version }} + GITHUB_TOKEN: ${{ secrets.ADMIN_PAT }} + SOURCE_BRANCH: ${{ needs.calculate-next-version.outputs.source_branch }} + TARGET_BRANCH: ${{ github.event.inputs.target_branch }} + run: | + set -ex + new_cargo_version=${NEW_VERSION#v} + branch_name="release/${NEW_VERSION}" + + git checkout "$SOURCE_BRANCH" + git checkout -b "$branch_name" + + echo "Updating Cargo.toml to version: $new_cargo_version" + sed -i -E "s/^version\s*=\s*\"[0-9a-zA-Z.-]+\"/version = \"$new_cargo_version\"/" Cargo.toml + + cargo update --package quantus_ur + + git config user.name "${{ github.actor }}" + git config user.email "${{ github.actor }}@users.noreply.github.com" + + git add Cargo.toml Cargo.lock + git commit -m "ci: Automate version bump to $NEW_VERSION" + git push origin "$branch_name" + + PR_TITLE="ci: Automate version bump to $NEW_VERSION" + + PR_LABELS="automated,release-proposal" + if [[ "${{ github.event.inputs.is_draft }}" == "true" ]]; then + PR_LABELS="$PR_LABELS,draft-release" + fi + + gh label create automated --color "0E8A16" --description "Automated" --force + gh label create release-proposal --color "1D76DB" --description "Release proposal" --force + gh label create draft-release --color "FBCA04" --description "Draft release" --force + + gh pr create \ + --title "$PR_TITLE" \ + --body "$(printf "Automated version bump for release %s.\\n\\n%s\\n\\nTriggered by workflow run: %s\\n\\nType: %s" "$NEW_VERSION" "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" "${{ github.event.inputs.version_type }}")" \ + --base "$TARGET_BRANCH" \ + --head "$branch_name" \ + --label "$PR_LABELS" diff --git a/.github/workflows/create-release-tag-and-publish.yml b/.github/workflows/create-release-tag-and-publish.yml new file mode 100644 index 0000000..b564e25 --- /dev/null +++ b/.github/workflows/create-release-tag-and-publish.yml @@ -0,0 +1,170 @@ +name: Create Release Tag & Publish + +env: + CARGO_INCREMENTAL: 0 + CARGO_TERM_COLOR: always + +on: + pull_request: + types: [closed] + branches: + - main + +permissions: + contents: read + +jobs: + create-tag: + name: Create Tag + if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release-proposal') + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + version: ${{ steps.extract_version.outputs.version }} + is_draft: ${{ steps.extract_version.outputs.is_draft }} + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Extract version from PR title + id: extract_version + run: | + VERSION=$(echo "${{ github.event.pull_request.title }}" | grep -o 'v[0-9]\+\.[0-9]\+\.[0-9]\+') + if [ -z "$VERSION" ]; then + echo "Error: Could not extract version from PR title: ${{ github.event.pull_request.title }}" + exit 1 + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + + if [[ "${{ contains(github.event.pull_request.labels.*.name, 'draft-release') }}" == "true" ]]; then + echo "is_draft=true" >> $GITHUB_OUTPUT + else + echo "is_draft=false" >> $GITHUB_OUTPUT + fi + echo "Extracted version: $VERSION" + + - name: Create and push tag + run: | + git config user.name "${{ github.actor }}" + git config user.email "${{ github.actor }}@users.noreply.github.com" + git tag -a "${{ steps.extract_version.outputs.version }}" -m "Release ${{ steps.extract_version.outputs.version }}" + git push origin "${{ steps.extract_version.outputs.version }}" + + format-checks: + name: 🏁 Format Checks + needs: create-tag + runs-on: ubuntu-latest + steps: + - name: Checkout code at tag + uses: actions/checkout@v5 + with: + ref: ${{ needs.create-tag.outputs.version }} + + - name: Install rustfmt + run: | + rustup toolchain install + rustup component add rustfmt + + - name: Install taplo + run: cargo install taplo-cli --locked + + - name: Run format checks + run: | + taplo format --check --config taplo.toml + cargo fmt --all -- --check + + build-and-test: + name: 🛠️ Build & Test + needs: [create-tag, format-checks] + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: true + matrix: + os: + - ubuntu-latest + - macos-latest + steps: + - name: Checkout code at tag + uses: actions/checkout@v5 + with: + ref: ${{ needs.create-tag.outputs.version }} + + - name: Install toolchain + run: | + rustup toolchain install + rustup component add clippy + + - name: Build + run: cargo build --locked --release + + - name: Test + run: cargo test --locked --release + + - name: Run clippy + run: cargo clippy --locked --all-targets --all-features -- -D warnings + + - name: Build docs + run: cargo doc --locked --no-deps --all-features + + create-github-release: + name: 🚀 Create GitHub Release + needs: [create-tag, build-and-test] + if: always() && needs.build-and-test.result == 'success' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.create-tag.outputs.version }} + fetch-depth: 0 + fetch-tags: true + + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.ADMIN_PAT }} + NEW_VERSION: ${{ needs.create-tag.outputs.version }} + run: | + release_notes="Automated release for quantus_ur $NEW_VERSION. + + Full Changelog: https://github.com/${{ github.repository }}/commits/$NEW_VERSION" + + printf "%s" "$release_notes" > release_notes.txt + + if [[ "${{ needs.create-tag.outputs.is_draft }}" == "true" ]]; then + DRAFT_FLAG="--draft" + else + DRAFT_FLAG="" + fi + + echo "Creating release with DRAFT_FLAG: $DRAFT_FLAG" + + gh release create "$NEW_VERSION" \ + --repo "$GITHUB_REPOSITORY" \ + --title "quantus_ur - $NEW_VERSION" \ + --notes-file release_notes.txt \ + --target main \ + $DRAFT_FLAG + + publish-to-crates: + name: 📦 Publish to crates.io + needs: [create-tag, build-and-test] + if: always() && needs.build-and-test.result == 'success' && needs.create-tag.outputs.is_draft == 'false' + runs-on: ubuntu-latest + steps: + - name: Checkout code at tag + uses: actions/checkout@v5 + with: + ref: ${{ needs.create-tag.outputs.version }} + + - name: Install toolchain + run: rustup toolchain install + + - name: Publish to crates.io + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish --allow-dirty --token "$CARGO_REGISTRY_TOKEN" diff --git a/.github/workflows/dependency-cooldown-audit.yml b/.github/workflows/dependency-cooldown-audit.yml new file mode 100644 index 0000000..502a10b --- /dev/null +++ b/.github/workflows/dependency-cooldown-audit.yml @@ -0,0 +1,13 @@ +name: Dependency cooldown audit + +on: + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +permissions: + contents: read + +jobs: + audit: + uses: Quantus-Network/shared-workflows/.github/workflows/dependency-cooldown-audit.yml@v1 diff --git a/.github/workflows/dependency-cooldown.yml b/.github/workflows/dependency-cooldown.yml new file mode 100644 index 0000000..f4dc224 --- /dev/null +++ b/.github/workflows/dependency-cooldown.yml @@ -0,0 +1,12 @@ +name: Dependency cooldown + +on: + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled, edited] + +permissions: + contents: read + +jobs: + dependency-cooldown: + uses: Quantus-Network/shared-workflows/.github/workflows/dependency-cooldown.yml@v1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ff7148c..88f13ab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,10 +2,6 @@ name: Publish to crates.io on: workflow_dispatch: - push: - tags: - - "v*" - - "[0-9]+.*" permissions: contents: read @@ -23,5 +19,5 @@ jobs: env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} # `cargo publish` rewrites Cargo.lock while packaging; the tree is - # otherwise clean. Same flag as quantus-cli's publish job. + # otherwise clean. Same flag as the automated release publish job. run: cargo publish --allow-dirty --token "$CARGO_REGISTRY_TOKEN" diff --git a/.rustfmt.toml b/.rustfmt.toml index 705188d..7a93aa3 100644 --- a/.rustfmt.toml +++ b/.rustfmt.toml @@ -1,3 +1,3 @@ -indent_style="Block" -reorder_imports=true -binop_separator="Front" \ No newline at end of file +binop_separator = "Front" +indent_style = "Block" +reorder_imports = true diff --git a/Cargo.lock b/Cargo.lock index b00d4f6..b92e83e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -767,7 +767,7 @@ dependencies = [ [[package]] name = "quantus_ur" -version = "0.1.0" +version = "1.6.0" dependencies = [ "criterion", "hex", diff --git a/Cargo.toml b/Cargo.toml index a7bb5a9..dc2af13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,28 +1,33 @@ [package] -name = "quantus_ur" -version = "1.6.0" -edition = "2021" +categories = ["encoding"] description = "Encode and decode Quantus sign requests as UR QR codes" +edition = "2021" +keywords = [ + "cbor", + "qr", + "quantus", + "ur", +] license = "MIT" -repository = "https://github.com/Quantus-Network/quantus_ur" +name = "quantus_ur" readme = "README.md" -categories = ["encoding"] -keywords = ["cbor", "qr", "quantus", "ur"] +repository = "https://github.com/Quantus-Network/quantus_ur" +version = "1.6.0" [dependencies] hex = { version = "0.4.3", default-features = false, features = ["alloc"] } -ur-parse-lib = { version = "1.0.6", default-features = false } -ur = { package = "keystone-ur", version = "0.1.1", default-features = false } minicbor = { version = "0.19", default-features = false, features = ["alloc"] } +ur = { package = "keystone-ur", version = "0.1.1", default-features = false } +ur-parse-lib = { version = "1.0.6", default-features = false } [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } [[bench]] -name = "decode" harness = false +name = "decode" [features] +core = ["ur-parse-lib/core"] default = ["std"] std = ["ur-parse-lib/std"] -core = ["ur-parse-lib/core"] \ No newline at end of file diff --git a/clippy.sh b/clippy.sh new file mode 100755 index 0000000..d6815b0 --- /dev/null +++ b/clippy.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Run the same clippy command as CI (see .github/workflows/ci.yml) +set -euo pipefail + +cargo +nightly fmt +taplo format +cargo clippy --workspace --all-targets --all-features --locked -- -D warnings diff --git a/src/lib.rs b/src/lib.rs index 63ecc1c..4507ba0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,6 @@ extern crate alloc; use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; -use hex; use minicbor::{bytes::ByteVec, Decoder}; use ur_parse_lib::keystone_ur_encoder::probe_encode; @@ -60,7 +59,11 @@ const fn build_crc32_table() -> [u32; 256] { let mut c = i as u32; let mut k = 0; while k < 8 { - c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 }; + c = if c & 1 != 0 { + 0xEDB8_8320 ^ (c >> 1) + } else { + c >> 1 + }; k += 1; } table[i] = c; @@ -130,10 +133,7 @@ fn decode_ur_part(part: &str) -> Result<(ur::ur::Kind, Vec), QuantusUrError> let body = &part[UR_PREFIX.len()..]; match body.rsplit_once('/') { - None => Ok(( - ur::ur::Kind::SinglePart, - decode_minimal_bytewords(body)?, - )), + None => Ok((ur::ur::Kind::SinglePart, decode_minimal_bytewords(body)?)), Some((indices, payload)) => { let Some((index, index_total)) = indices.split_once('-') else { return Err(QuantusUrError::UrError("Invalid indices".to_string())); @@ -141,10 +141,7 @@ fn decode_ur_part(part: &str) -> Result<(ur::ur::Kind, Vec), QuantusUrError> if index.parse::().is_err() || index_total.parse::().is_err() { return Err(QuantusUrError::UrError("Invalid indices".to_string())); } - Ok(( - ur::ur::Kind::MultiPart, - decode_minimal_bytewords(payload)?, - )) + Ok((ur::ur::Kind::MultiPart, decode_minimal_bytewords(payload)?)) } } } @@ -160,9 +157,9 @@ pub enum QuantusUrError { impl core::fmt::Display for QuantusUrError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - QuantusUrError::HexError(e) => write!(f, "Hex decoding error: {}", e), - QuantusUrError::UrError(msg) => write!(f, "UR error: {}", msg), - QuantusUrError::CborError(msg) => write!(f, "CBOR error: {}", msg), + QuantusUrError::HexError(e) => write!(f, "Hex decoding error: {e}"), + QuantusUrError::UrError(msg) => write!(f, "UR error: {msg}"), + QuantusUrError::CborError(msg) => write!(f, "CBOR error: {msg}"), QuantusUrError::Incomplete => write!(f, "Decoding incomplete"), } } @@ -456,7 +453,8 @@ mod tests { let decoded = decode_minimal_bytewords(&encoded).expect("decode"); assert_eq!(decoded, vec![i as u8], "word index {i}"); // Case-insensitivity: same result from uppercase input. - let decoded_upper = decode_minimal_bytewords(&encoded.to_ascii_uppercase()).expect("decode"); + let decoded_upper = + decode_minimal_bytewords(&encoded.to_ascii_uppercase()).expect("decode"); assert_eq!(decoded_upper, vec![i as u8], "word index {i} uppercase"); } } @@ -474,18 +472,18 @@ mod tests { encode_bytes(&payload).expect("encode"), encode_bytes_with_options(&payload, 700).expect("encode"), ] { - for part in &parts { - let lower = part.to_ascii_lowercase(); - let expected = ur::ur::decode(&lower).expect("dependency decode"); - let actual = decode_ur_part(&lower).expect("fast decode"); - assert_eq!(expected.0, actual.0); - assert_eq!(expected.1, actual.1); - // The dependency requires lowercase; ours must accept the - // uppercase scan equally. - let actual_upper = decode_ur_part(part).expect("fast decode uppercase"); - assert_eq!(expected.0, actual_upper.0); - assert_eq!(expected.1, actual_upper.1); - } + for part in &parts { + let lower = part.to_ascii_lowercase(); + let expected = ur::ur::decode(&lower).expect("dependency decode"); + let actual = decode_ur_part(&lower).expect("fast decode"); + assert_eq!(expected.0, actual.0); + assert_eq!(expected.1, actual.1); + // The dependency requires lowercase; ours must accept the + // uppercase scan equally. + let actual_upper = decode_ur_part(part).expect("fast decode uppercase"); + assert_eq!(expected.0, actual_upper.0); + assert_eq!(expected.1, actual_upper.1); + } } } } @@ -498,7 +496,11 @@ mod tests { // Flip a character in the bytewords body: checksum must catch it. let mut corrupted = part.clone(); let last = corrupted.len() - 1; - let flipped = if corrupted.as_bytes()[last] == b'a' { b'e' } else { b'a' }; + let flipped = if corrupted.as_bytes()[last] == b'a' { + b'e' + } else { + b'a' + }; corrupted.replace_range(last.., core::str::from_utf8(&[flipped]).unwrap()); assert!(decode_ur_part(&corrupted).is_err()); @@ -514,7 +516,7 @@ mod tests { // 250 bytes of data let mut large_payload = String::with_capacity(500); for i in 0..250 { - large_payload.push_str(&format!("{:02x}", i)); + large_payload.push_str(&format!("{i:02x}")); } let encoded_parts = encode_hex(&large_payload).expect("Encoding failed"); @@ -539,50 +541,65 @@ mod tests { let hex_payload = "0200007416854906f03a9dff66e3270a736c44e15970ac03a638471523a03069f276ca0700e876481755010000007400000002000000"; let encoded_parts = encode_hex(hex_payload).expect("Encoding failed"); assert_eq!(encoded_parts.len(), 1, "Should be single part"); - assert!(is_complete(&encoded_parts), "Single part should be complete"); + assert!( + is_complete(&encoded_parts), + "Single part should be complete" + ); } #[test] fn test_is_complete_multi_part_complete() { let mut large_payload = String::with_capacity(500); for i in 0..250 { - large_payload.push_str(&format!("{:02x}", i)); + large_payload.push_str(&format!("{i:02x}")); } let encoded_parts = encode_hex(&large_payload).expect("Encoding failed"); assert!(encoded_parts.len() > 1, "Should be multi-part"); - assert!(is_complete(&encoded_parts), "Complete multi-part should return true"); + assert!( + is_complete(&encoded_parts), + "Complete multi-part should return true" + ); } #[test] fn test_is_complete_multi_part_incomplete() { let mut large_payload = String::with_capacity(500); for i in 0..250 { - large_payload.push_str(&format!("{:02x}", i)); + large_payload.push_str(&format!("{i:02x}")); } let encoded_parts = encode_hex(&large_payload).expect("Encoding failed"); assert!(encoded_parts.len() > 1, "Should be multi-part"); - + let incomplete_parts = &encoded_parts[..encoded_parts.len() - 1]; - assert!(!is_complete(incomplete_parts), "Incomplete multi-part should return false"); + assert!( + !is_complete(incomplete_parts), + "Incomplete multi-part should return false" + ); } #[test] fn test_is_complete_invalid_ur() { let invalid_parts = vec!["not-a-valid-ur".to_string()]; - assert!(!is_complete(&invalid_parts), "Invalid UR should return false"); + assert!( + !is_complete(&invalid_parts), + "Invalid UR should return false" + ); } #[test] fn test_is_complete_multi_part_partial() { let mut large_payload = String::with_capacity(500); for i in 0..250 { - large_payload.push_str(&format!("{:02x}", i)); + large_payload.push_str(&format!("{i:02x}")); } let encoded_parts = encode_hex(&large_payload).expect("Encoding failed"); assert!(encoded_parts.len() > 1, "Should be multi-part"); - + let partial_parts = &encoded_parts[..1]; - assert!(!is_complete(partial_parts), "Single part of multi-part should return false"); + assert!( + !is_complete(partial_parts), + "Single part of multi-part should return false" + ); } #[test] @@ -615,14 +632,8 @@ mod tests { let parts_1500 = encode_bytes_with_options(&payload, 1500).expect("Encoding failed"); assert_eq!(parts_1500.len(), 5, "7219 bytes at 1500 per fragment"); - assert_eq!( - decode_bytes(&parts_700).expect("Decoding failed"), - payload - ); - assert_eq!( - decode_bytes(&parts_1500).expect("Decoding failed"), - payload - ); + assert_eq!(decode_bytes(&parts_700).expect("Decoding failed"), payload); + assert_eq!(decode_bytes(&parts_1500).expect("Decoding failed"), payload); assert!(is_complete(&parts_1500)); } @@ -669,11 +680,11 @@ mod tests { /// substituting a foreign UR into a scan would. fn rewrite_ur_type(part: &str, new_type: &str) -> String { let lower = part.to_lowercase(); - let prefix = format!("ur:{}/", UR_TYPE); + let prefix = format!("ur:{UR_TYPE}/"); let body = lower .strip_prefix(&prefix) .expect("encoded part must use the quantus UR type"); - format!("ur:{}/{}", new_type, body) + format!("ur:{new_type}/{body}") } /// Builds a multipart fragment with arbitrary fountain metadata, bypassing the @@ -701,7 +712,7 @@ mod tests { .unwrap(); let body = ur::bytewords::encode(&cbor, ur::bytewords::Style::Minimal); - format!("ur:{}/{}-{}/{}", UR_TYPE, sequence, sequence_count, body) + format!("ur:{UR_TYPE}/{sequence}-{sequence_count}/{body}") } fn multi_part_payload() -> Vec { @@ -717,7 +728,10 @@ mod tests { matches!(decode_bytes(&foreign), Err(QuantusUrError::UrError(_))), "Foreign UR type should be rejected" ); - assert!(!is_complete(&foreign), "Foreign UR type should not be complete"); + assert!( + !is_complete(&foreign), + "Foreign UR type should not be complete" + ); } #[test] @@ -754,33 +768,54 @@ mod tests { matches!(decode_bytes(&mixed_parts), Err(QuantusUrError::UrError(_))), "A foreign-type fragment must be rejected" ); - assert!(!is_complete(&mixed_parts), "A foreign-type fragment must not be accepted"); + assert!( + !is_complete(&mixed_parts), + "A foreign-type fragment must not be accepted" + ); } #[test] fn test_multi_part_rejects_out_of_bounds_metadata() { // Tiny fragment claiming an enormous fragment count: the decoder must reject // it instead of sizing its work from attacker-supplied metadata. - let malicious = vec![craft_multipart_part(30_001, 30_000, 1, 0xdead_beef, &[0x41])]; + let malicious = vec![craft_multipart_part( + 30_001, + 30_000, + 1, + 0xdead_beef, + &[0x41], + )]; assert!( matches!(decode_bytes(&malicious), Err(QuantusUrError::UrError(_))), "Out-of-bounds fragment metadata should be rejected before decoding" ); - assert!(!is_complete(&malicious), "Out-of-bounds fragment should not be complete"); + assert!( + !is_complete(&malicious), + "Out-of-bounds fragment should not be complete" + ); } #[test] fn test_multi_part_rejects_oversized_fragment_data() { let data = vec![0x41; MAX_FRAGMENT_LENGTH + 1]; let message_length = data.len() as u32 * 2; - let malicious = vec![craft_multipart_part(1, 2, message_length, 0xdead_beef, &data)]; + let malicious = vec![craft_multipart_part( + 1, + 2, + message_length, + 0xdead_beef, + &data, + )]; assert!( matches!(decode_bytes(&malicious), Err(QuantusUrError::UrError(_))), "Fragments larger than the encoding envelope should be rejected" ); - assert!(!is_complete(&malicious), "Oversized fragment should not be complete"); + assert!( + !is_complete(&malicious), + "Oversized fragment should not be complete" + ); } #[test] @@ -793,7 +828,10 @@ mod tests { matches!(decode_bytes(&parts), Err(QuantusUrError::UrError(_))), "Fragments describing different messages should be rejected" ); - assert!(!is_complete(&parts), "Inconsistent fragments should not be complete"); + assert!( + !is_complete(&parts), + "Inconsistent fragments should not be complete" + ); } /// UR-encodes raw CBOR, mirroring `encode_internal` but without the canonical @@ -834,7 +872,10 @@ mod tests { let largest = vec![0u8; MAX_MESSAGE_LENGTH - 8]; let parts = encode_bytes(&largest).expect("Encoding failed"); - assert!(parts.len() <= MAX_FRAGMENT_COUNT, "Should stay within the fragment bound"); + assert!( + parts.len() <= MAX_FRAGMENT_COUNT, + "Should stay within the fragment bound" + ); assert_eq!(decode_bytes(&parts).expect("Decoding failed"), largest); } @@ -864,10 +905,10 @@ mod tests { fn test_decode_bytes_hex_equivalence() { let hex_payload = "0200007416854906f03a9dff66e3270a736c44e15970ac03a638471523a03069f276ca0700e876481755010000007400000002000000"; let encoded_parts = encode_hex(hex_payload).expect("Encoding failed"); - + let decoded_hex = decode_hex(&encoded_parts).expect("Decoding failed"); let decoded_bytes = decode_bytes(&encoded_parts).expect("Decoding failed"); - + assert_eq!(decoded_hex.to_lowercase(), hex_payload.to_lowercase()); assert_eq!(hex::encode(&decoded_bytes), decoded_hex); } diff --git a/taplo.toml b/taplo.toml new file mode 100644 index 0000000..9e70b0b --- /dev/null +++ b/taplo.toml @@ -0,0 +1,11 @@ +# all options https://taplo.tamasfe.dev/configuration/formatter-options.html +exclude = ["**/target/**"] +include = ["**/*.toml"] + +[formatting] +array_auto_collapse = false +array_auto_expand = true +indent_string = " " +inline_table_expand = false +reorder_arrays = true +reorder_keys = true